P-vector model fit, single channel#
Import Python libraries
from __future__ import annotations
import logging
import os
import re
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
from typing import Any
import ampform
import attrs
import graphviz
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import qrules
import sympy as sp
from ampform.dynamics.builder import TwoBodyKinematicVariableSet
from ampform.helicity import ParameterValues
from ampform.io import aslatex, improve_latex_rendering
from ampform.kinematics.phasespace import Kallen
from ampform.sympy import perform_cached_doit, unevaluated
from attrs import define, field
from IPython.display import Math
from matplotlib.figure import Figure
from qrules.particle import Particle, ParticleCollection
from sympy import Abs
from tensorwaves.data import (
IntensityDistributionGenerator,
SympyDataTransformer,
TFPhaseSpaceGenerator,
TFUniformRealNumberGenerator,
TFWeightedPhaseSpaceGenerator,
)
from tensorwaves.estimator import UnbinnedNLL
from tensorwaves.function.sympy import create_parametrized_function
from tensorwaves.interface import DataSample, FitResult, ParametrizedFunction
from tensorwaves.optimizer import Minuit2
improve_latex_rendering()
logging.getLogger("absl").setLevel(logging.ERROR)
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
plt.rc("font", size=12)
Studied decay#
Define N* resonances
@lru_cache(maxsize=1)
def create_particle_database() -> ParticleCollection:
particles = qrules.load_default_particles()
for nstar in particles.filter(lambda p: p.name.startswith("N")):
particles.remove(nstar)
particles += create_nstar(mass=1.65, width=0.6, parity=-1, spin=0.5, idx=1)
particles += create_nstar(mass=1.75, width=0.6, parity=-1, spin=0.5, idx=2)
particles += create_nstar(mass=1.82, width=0.6, parity=+1, spin=1.5, idx=1)
particles += create_nstar(mass=1.92, width=0.6, parity=+1, spin=1.5, idx=2)
return particles
def create_nstar(
mass: float, width: float, parity: int, spin: float, idx: int
) -> Particle:
spin = sp.Rational(spin)
parity_symbol = "⁺" if parity > 0 else "⁻"
unicode_subscripts = list("₀₁₂₃₄₅₆₇₈₉")
return Particle(
name=f"N{unicode_subscripts[idx]}({spin}{parity_symbol})",
latex=Rf"N_{idx}({spin.numerator}/{spin.denominator}^-)",
pid=2024_05_00_00 + 100 * bool(parity + 1) + idx,
mass=mass,
width=width,
baryon_number=1,
charge=+1,
isospin=(0.5, +0.5),
parity=parity,
spin=1.5,
)
reaction = qrules.generate_transitions(
initial_state="J/psi(1S)",
final_state=["eta", "p", "p~"],
allowed_intermediate_particles=["N"],
allowed_interaction_types=["strong"],
formalism="helicity",
particle_db=create_particle_database(),
)
Show code cell source
dot = qrules.io.asdot(reaction, collapse_graphs=True)
graph = graphviz.Source(dot)
output_file = Path("qrules-output")
graph.render(output_file, format="svg")
output_file.unlink()
graph
Amplitude builder#
Dynamics builder with X symbols of J^PC channels
@define
class DynamicsSymbolBuilder:
collected_symbols: set[sp.Symbol, tuple[Particle, TwoBodyKinematicVariableSet]] = (
field(factory=lambda: defaultdict(set))
)
def __call__(
self, resonance: Particle, variable_pool: TwoBodyKinematicVariableSet
) -> tuple[sp.Expr, dict[sp.Symbol, float]]:
jp = render_jp(resonance)
charge = resonance.charge
if variable_pool.angular_momentum is not None:
L = sp.Rational(variable_pool.angular_momentum)
X = sp.Symbol(Rf"X_{{{jp}, Q={charge:+d}}}^{{l={L}}}")
else:
X = sp.Symbol(Rf"X_{{{jp}, Q={charge:+d}}}")
self.collected_symbols[X].add((resonance, variable_pool))
parameter_defaults = {}
return X, parameter_defaults
def render_jp(particle: Particle) -> str:
spin = sp.Rational(particle.spin)
j = (
str(spin)
if spin.denominator == 1
else Rf"\frac{{{spin.numerator}}}{{{spin.denominator}}}"
)
if particle.parity is None:
return f"J={j}"
p = "-" if particle.parity < 0 else "+"
return f"J^P={{{j}}}^{{{p}}}"
model_builder = ampform.get_builder(reaction)
model_builder.adapter.permutate_registered_topologies()
model_builder.config.scalar_initial_state_mass = True
model_builder.config.stable_final_state_ids = [0, 1, 2]
create_dynamics_symbol = DynamicsSymbolBuilder()
for resonance in reaction.get_intermediate_particles():
model_builder.set_dynamics(resonance.name, create_dynamics_symbol)
model = model_builder.formulate()
model.intensity.cleanup()
\[\displaystyle \sum_{m_{A}=-1}^{1} \sum_{m_{1}=-1/2}^{1/2} \sum_{m_{2}=-1/2}^{1/2}{\left|{A^{01}_{m_{A}, 0, m_{1}, m_{2}}}\right|^{2}}\]
Show code cell source
\[\begin{split}\displaystyle \begin{array}{rcl}
A^{01}_{0, 0, - \frac{1}{2}, - \frac{1}{2}} &=& - C_{J/\psi(1S) \to {N_1(1/2^-)}_{+1/2} \overline{p}_{+1/2}; N_1(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,0}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_1(1/2^-)}_{+1/2} \overline{p}_{-1/2}; N_1(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{\frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_1(1/2^-)}_{+3/2} \overline{p}_{+1/2}; N_1(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,-1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{3}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_1(3/2^-)}_{+1/2} \overline{p}_{+1/2}; N_1(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,0}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_1(3/2^-)}_{+1/2} \overline{p}_{-1/2}; N_1(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{\frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_1(3/2^-)}_{+3/2} \overline{p}_{+1/2}; N_1(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,-1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{3}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(1/2^-)}_{+1/2} \overline{p}_{+1/2}; N_2(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,0}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(1/2^-)}_{+1/2} \overline{p}_{-1/2}; N_2(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{\frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(1/2^-)}_{+3/2} \overline{p}_{+1/2}; N_2(1/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{-}, Q=+1} D^{1}_{0,-1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{3}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(3/2^-)}_{+1/2} \overline{p}_{+1/2}; N_2(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,0}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(3/2^-)}_{+1/2} \overline{p}_{-1/2}; N_2(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{\frac{1}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
&+& - C_{J/\psi(1S) \to {N_2(3/2^-)}_{+3/2} \overline{p}_{+1/2}; N_2(3/2^-) \to \eta_{0} p_{+1/2}} X_{J^P={\frac{3}{2}}^{+}, Q=+1} D^{1}_{0,-1}\left(- \phi_{01},\theta_{01},0\right) D^{\frac{3}{2}}_{- \frac{3}{2},\frac{1}{2}}\left(- \phi^{01}_{0},\theta^{01}_{0},0\right) \\
\end{array}\end{split}\]
Show code cell source
src = R"\begin{array}{cll}" "\n"
for symbol, resonances in create_dynamics_symbol.collected_symbols.items():
src += Rf" {symbol} \\" "\n"
for p, _ in resonances:
src += Rf" {p.latex} & m={p.mass:g}\text{{ GeV}} & \Gamma={p.width:g}\text{{ GeV}} \\"
src += "\n"
src += R"\end{array}"
Math(src)
\[\begin{split}\displaystyle \begin{array}{cll}
X_{J^P={\frac{3}{2}}^{-}, Q=+1} \\
N_1(1/2^-) & m=1.65\text{ GeV} & \Gamma=0.6\text{ GeV} \\
N_2(1/2^-) & m=1.75\text{ GeV} & \Gamma=0.6\text{ GeV} \\
X_{J^P={\frac{3}{2}}^{+}, Q=+1} \\
N_2(3/2^-) & m=1.92\text{ GeV} & \Gamma=0.6\text{ GeV} \\
N_1(3/2^-) & m=1.82\text{ GeV} & \Gamma=0.6\text{ GeV} \\
\end{array}\end{split}\]
Dynamics parametrization#
Phasespace factor#
Expression classes for phase space factors
@unevaluated(real=False)
class PhaseSpaceCM(sp.Expr):
s: Any
m1: Any
m2: Any
_latex_repr_ = R"\rho^\mathrm{{CM}}_{{{m1},{m2}}}\left({s}\right)"
def evaluate(self) -> sp.Expr:
s, m1, m2 = self.args
return -16 * sp.pi * sp.I * ChewMandelstam(s, m1, m2)
@unevaluated(real=False)
class ChewMandelstam(sp.Expr):
s: Any
m1: Any
m2: Any
_latex_repr_ = R"\Sigma\left({s}\right)"
def evaluate(self) -> sp.Expr:
s, m1, m2 = self.args
q = BreakupMomentum(s, m1, m2)
return (
(2 * q / sp.sqrt(s))
* sp.log(Abs((m1**2 + m2**2 - s + 2 * sp.sqrt(s) * q) / (2 * m1 * m2)))
- (m1**2 - m2**2) * (1 / s - 1 / (m1 + m2) ** 2) * sp.log(m1 / m2)
) / (16 * sp.pi**2)
@unevaluated(real=False)
class BreakupMomentum(sp.Expr):
s: Any
m1: Any
m2: Any
_latex_repr_ = R"q\left({s}\right)"
def evaluate(self) -> sp.Expr:
s, m1, m2 = self.args
return sp.sqrt(Kallen(s, m1**2, m2**2)) / (2 * sp.sqrt(s))
Show code cell source
\[\begin{split}\displaystyle \begin{array}{rcl}
\rho^\mathrm{CM}_{m_{1},m_{2}}\left(s\right) &=& - 16 i \pi \Sigma\left(s\right) \\
\Sigma\left(s\right) &=& \frac{- \left(m_{1}^{2} - m_{2}^{2}\right) \left(- \frac{1}{\left(m_{1} + m_{2}\right)^{2}} + \frac{1}{s}\right) \log{\left(\frac{m_{1}}{m_{2}} \right)} + \frac{2 \log{\left(\frac{\left|{m_{1}^{2} + m_{2}^{2} + 2 \sqrt{s} q\left(s\right) - s}\right|}{2 m_{1} m_{2}} \right)} q\left(s\right)}{\sqrt{s}}}{16 \pi^{2}} \\
q\left(s\right) &=& \frac{\sqrt{\lambda\left(s, m_{1}^{2}, m_{2}^{2}\right)}}{2 \sqrt{s}} \\
\end{array}\end{split}\]
Relativistic Breit-Wigner#
PARAMETERS_BW = dict(model.parameter_defaults)
def formulate_breit_wigner(
resonances: list[tuple[Particle, TwoBodyKinematicVariableSet]],
) -> sp.Expr:
(_, variables), *_ = resonances
s = variables.incoming_state_mass**2
m1 = variables.outgoing_state_mass1
m2 = variables.outgoing_state_mass2
ρ = PhaseSpaceCM(s, m1, m2)
m = [sp.Symbol(Rf"m_{{{p.latex}}}") for p, _ in resonances]
Γ0 = [sp.Symbol(Rf"\Gamma_{{{p.latex}}}") for p, _ in resonances]
β = [sp.Symbol(Rf"\beta_{{{p.latex}}}") for p, _ in resonances]
expr = sum(
(β_ * m_ * Γ0_) / (m_**2 - s - m_ * Γ0_ * ρ)
for m_, Γ0_, β_ in zip(m, Γ0, β, strict=True)
)
for i, (resonance, _) in enumerate(resonances):
PARAMETERS_BW[β[i]] = 1 + 0j
PARAMETERS_BW[m[i]] = resonance.mass
PARAMETERS_BW[Γ0[i]] = resonance.width
return expr
Show code cell source
dynamics_expressions_bw = {
symbol: formulate_breit_wigner(resonances)
for symbol, resonances in create_dynamics_symbol.collected_symbols.items()
}
model_bw = attrs.evolve(
model,
parameter_defaults=ParameterValues({
**model.parameter_defaults,
**PARAMETERS_BW,
}),
)
Math(aslatex(dynamics_expressions_bw))
\[\begin{split}\displaystyle \begin{array}{rcl}
X_{J^P={\frac{3}{2}}^{-}, Q=+1} &=& \frac{\Gamma_{N_1(1/2^-)} \beta_{N_1(1/2^-)} m_{N_1(1/2^-)}}{- \Gamma_{N_1(1/2^-)} m_{N_1(1/2^-)} \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) - m_{01}^{2} + \left(m_{N_1(1/2^-)}\right)^{2}} + \frac{\Gamma_{N_2(1/2^-)} \beta_{N_2(1/2^-)} m_{N_2(1/2^-)}}{- \Gamma_{N_2(1/2^-)} m_{N_2(1/2^-)} \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) - m_{01}^{2} + \left(m_{N_2(1/2^-)}\right)^{2}} \\
X_{J^P={\frac{3}{2}}^{+}, Q=+1} &=& \frac{\Gamma_{N_1(3/2^-)} \beta_{N_1(3/2^-)} m_{N_1(3/2^-)}}{- \Gamma_{N_1(3/2^-)} m_{N_1(3/2^-)} \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) - m_{01}^{2} + \left(m_{N_1(3/2^-)}\right)^{2}} + \frac{\Gamma_{N_2(3/2^-)} \beta_{N_2(3/2^-)} m_{N_2(3/2^-)}}{- \Gamma_{N_2(3/2^-)} m_{N_2(3/2^-)} \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) - m_{01}^{2} + \left(m_{N_2(3/2^-)}\right)^{2}} \\
\end{array}\end{split}\]
\(P\) vector#
PARAMETERS_F = dict(model.parameter_defaults)
def formulate_k_matrix(
resonances: list[tuple[Particle, TwoBodyKinematicVariableSet]],
) -> sp.Expr:
(_, variables), *_ = resonances
s = variables.incoming_state_mass**2
m = [sp.Symbol(Rf"m_{{{p.latex}}}") for p, _ in resonances]
g = [sp.Symbol(Rf"g_{{{p.latex}}}") for p, _ in resonances]
expr = sum((g_**2) / (m_**2 - s) for m_, g_ in zip(m, g, strict=True))
for i, (resonance, _) in enumerate(resonances):
PARAMETERS_F[m[i]] = resonance.mass
PARAMETERS_F[g[i]] = 1
return expr
def formulate_p_vector(
resonances: list[tuple[Particle, TwoBodyKinematicVariableSet]],
) -> sp.Expr:
(_, variables), *_ = resonances
s = variables.incoming_state_mass**2
g = [sp.Symbol(Rf"g_{{{p.latex}}}") for p, _ in resonances]
m = [sp.Symbol(Rf"m_{{{p.latex}}}") for p, _ in resonances]
β = [sp.Symbol(Rf"\beta_{{{p.latex}}}") for p, _ in resonances]
expr = sum((g_ * β_) / (m_**2 - s) for m_, g_, β_ in zip(m, g, β, strict=True))
for i, (resonance, _) in enumerate(resonances):
PARAMETERS_F[β[i]] = 1 + 0j
PARAMETERS_F[m[i]] = resonance.mass
PARAMETERS_F[g[i]] = 1
return expr
def formulate_f_vector(
resonances: list[tuple[Particle, TwoBodyKinematicVariableSet]],
) -> sp.Expr:
(_, variables), *_ = resonances
s = variables.incoming_state_mass**2
m1 = variables.outgoing_state_mass1
m2 = variables.outgoing_state_mass2
rho = PhaseSpaceCM(s, m1, m2)
K = formulate_k_matrix(resonances)
P = formulate_p_vector(resonances)
return (1 / (1 - rho * K)) * P
Show code cell source
dynamics_expressions_fvector = {
symbol: formulate_f_vector(resonances)
for symbol, resonances in create_dynamics_symbol.collected_symbols.items()
}
model_fvector = attrs.evolve(
model,
parameter_defaults=ParameterValues({
**model.parameter_defaults,
**PARAMETERS_F,
}),
)
Math(aslatex(dynamics_expressions_fvector))
\[\begin{split}\displaystyle \begin{array}{rcl}
X_{J^P={\frac{3}{2}}^{-}, Q=+1} &=& \frac{\frac{\beta_{N_1(1/2^-)} g_{N_1(1/2^-)}}{- m_{01}^{2} + \left(m_{N_1(1/2^-)}\right)^{2}} + \frac{\beta_{N_2(1/2^-)} g_{N_2(1/2^-)}}{- m_{01}^{2} + \left(m_{N_2(1/2^-)}\right)^{2}}}{- \left(\frac{\left(g_{N_1(1/2^-)}\right)^{2}}{- m_{01}^{2} + \left(m_{N_1(1/2^-)}\right)^{2}} + \frac{\left(g_{N_2(1/2^-)}\right)^{2}}{- m_{01}^{2} + \left(m_{N_2(1/2^-)}\right)^{2}}\right) \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) + 1} \\
X_{J^P={\frac{3}{2}}^{+}, Q=+1} &=& \frac{\frac{\beta_{N_1(3/2^-)} g_{N_1(3/2^-)}}{- m_{01}^{2} + \left(m_{N_1(3/2^-)}\right)^{2}} + \frac{\beta_{N_2(3/2^-)} g_{N_2(3/2^-)}}{- m_{01}^{2} + \left(m_{N_2(3/2^-)}\right)^{2}}}{- \left(\frac{\left(g_{N_1(3/2^-)}\right)^{2}}{- m_{01}^{2} + \left(m_{N_1(3/2^-)}\right)^{2}} + \frac{\left(g_{N_2(3/2^-)}\right)^{2}}{- m_{01}^{2} + \left(m_{N_2(3/2^-)}\right)^{2}}\right) \rho^\mathrm{CM}_{m_{0},m_{1}}\left(m_{01}^{2}\right) + 1} \\
\end{array}\end{split}\]
Create numerical functions#
full_expression_bw = perform_cached_doit(model_bw.expression).xreplace(
dynamics_expressions_bw
)
intensity_func_bw = create_parametrized_function(
expression=perform_cached_doit(full_expression_bw),
backend="jax",
parameters=PARAMETERS_BW,
)
full_expression_fvector = perform_cached_doit(model_fvector.expression).xreplace(
dynamics_expressions_fvector
)
intensity_func_fvector = create_parametrized_function(
expression=perform_cached_doit(full_expression_fvector),
backend="jax",
parameters=PARAMETERS_F,
)
Generate data#
Generate phase space sample#
rng = TFUniformRealNumberGenerator(seed=0)
phsp_generator = TFPhaseSpaceGenerator(
initial_state_mass=reaction.initial_state[-1].mass,
final_state_masses={i: p.mass for i, p in reaction.final_state.items()},
)
phsp_momenta = phsp_generator.generate(100_000, rng)
ε = 1e-8
transformer = SympyDataTransformer.from_sympy(model.kinematic_variables, backend="jax")
phsp = transformer(phsp_momenta)
phsp = {k: v + ε * 1j if re.match(r"^m_\d\d$", k) else v for k, v in phsp.items()}
Update function parameters#
toy_parameters_bw = {
R"m_{N_1(1/2^-)}": 1.65,
R"m_{N_2(1/2^-)}": 1.75,
R"m_{N_1(3/2^-)}": 1.85,
R"m_{N_2(3/2^-)}": 1.9,
R"\Gamma_{N_1(1/2^-)}": 1 / 1.65,
R"\Gamma_{N_2(1/2^-)}": 1 / 1.75,
R"\Gamma_{N_1(3/2^-)}": 1 / 1.85,
R"\Gamma_{N_2(3/2^-)}": 1 / 1.9,
}
intensity_func_bw.update_parameters(toy_parameters_bw)
toy_parameters_fvector = {
R"\beta_{N_1(1/2^-)}": 1 + 0j,
R"\beta_{N_2(1/2^-)}": 1 + 0j,
R"\beta_{N_1(3/2^-)}": 1 + 0j,
R"\beta_{N_2(3/2^-)}": 1 + 0j,
R"m_{N_1(1/2^-)}": 1.65,
R"m_{N_2(1/2^-)}": 1.75,
R"m_{N_1(3/2^-)}": 1.95,
R"m_{N_2(3/2^-)}": 1.9,
R"g_{N_1(1/2^-)}": 1.65,
R"g_{N_2(1/2^-)}": 1,
R"g_{N_1(3/2^-)}": 1,
R"g_{N_2(3/2^-)}": 1,
}
intensity_func_fvector.update_parameters(toy_parameters_fvector)
Plot sub-intensities#
Function for computing sub-intensities
def compute_sub_intensity(
func: ParametrizedFunction,
input_data: DataSample,
resonances: list[str],
coupling_pattern: str,
):
original_parameters = dict(func.parameters)
negative_lookahead = f"(?!{'|'.join(map(re.escape, resonances))})"
# https://regex101.com/r/WrgGyD/1
pattern = rf"^{coupling_pattern}({negative_lookahead}.)*$"
set_parameters_to_zero(func, pattern)
array = func(input_data)
func.update_parameters(original_parameters)
return array
def set_parameters_to_zero(func: ParametrizedFunction, name_pattern: str) -> None:
toy_parameters = dict(func.parameters)
for par_name in func.parameters:
if re.match(name_pattern, par_name) is not None:
toy_parameters[par_name] = 0
func.update_parameters(toy_parameters)
total_intensities_bw = intensity_func_bw(phsp)
sub_intensities_bw = {
p: compute_sub_intensity(
intensity_func_bw,
phsp,
resonances=[p.latex],
coupling_pattern=r"\\beta",
)
for symbol, resonances in create_dynamics_symbol.collected_symbols.items()
for p, _ in resonances
}
total_intensities_fvector = intensity_func_fvector(phsp)
sub_intensities_fvector = {
p: compute_sub_intensity(
intensity_func_fvector,
phsp,
resonances=[p.latex],
coupling_pattern=r"\\beta",
)
for symbol, resonances in create_dynamics_symbol.collected_symbols.items()
for p, _ in resonances
}
Function for plotting histograms with JAX
def fast_histogram(
data: jnp.ndarray,
weights: jnp.ndarray | None = None,
bins: int = 100,
density: bool | None = None,
fill: bool = True,
ax=plt,
**plot_kwargs,
) -> None:
bin_values, bin_edges = jnp.histogram(
data,
bins=bins,
density=density,
weights=weights,
)
if fill:
bin_rights = bin_edges[1:]
ax.fill_between(bin_rights, bin_values, step="pre", **plot_kwargs)
else:
bin_mids = (bin_edges[:-1] + bin_edges[1:]) / 2
ax.step(bin_mids, bin_values, **plot_kwargs)
Show code cell source
fig, ax = plt.subplots(figsize=(8, 5))
ax.set_xlim(2, 5)
ax.set_xlabel(R"$m_{p\eta}^{2}$ [GeV$^2$]")
ax.set_ylabel(R"Intensity [a. u.]")
ax.set_yticks([])
bins = 150
phsp_projection = np.real(phsp["m_01"]) ** 2
fast_histogram(
phsp_projection,
weights=total_intensities_fvector,
alpha=0.2,
bins=bins,
color="hotpink",
label="Full intensity $F$ vector",
ax=ax,
)
fast_histogram(
phsp_projection,
weights=total_intensities_bw,
alpha=0.2,
bins=bins,
color="grey",
label="Full intensity Breit-Wigner",
ax=ax,
)
for i, (p, v) in enumerate(sub_intensities_fvector.items()):
fast_histogram(
phsp_projection,
weights=v,
alpha=0.6,
bins=bins,
color=f"C{i}",
fill=False,
label=Rf"Resonance at ${p.mass}\,\mathrm{{GeV}}$ $F$ vector",
linewidth=2,
ax=ax,
)
for i, (p, v) in enumerate(sub_intensities_bw.items()):
fast_histogram(
phsp_projection,
weights=v,
alpha=0.6,
bins=bins,
color=f"C{i}",
fill=False,
label=Rf"Resonance at ${p.mass}\,\mathrm{{GeV^2}}$ Breit-Wigner",
linestyle="dashed",
ax=ax,
)
ax.set_ylim(0, None)
fig.legend(loc="upper right")
plt.tight_layout()
fig.savefig("contributions.svg", bbox_inches="tight")
plt.show()
Weighted data with \(F\) vector#
Show code cell source
fig, ax = plt.subplots(figsize=(9, 5))
fast_histogram(
phsp["m_01"].real,
bins=100,
weights=np.real(intensity_func_fvector(phsp)),
ax=ax,
)
ax.set_xlabel(R"$M^2\left(\eta p\right)\, \mathrm{[(GeV/c)^2]}$")
ax.set_ylabel(R"Intensity [a.u.]")
ax.set_ylim(0, None)
fig.tight_layout()
fig.savefig("weighted-phsp-f-vector.svg", bbox_inches="tight")
fig.show()
weighted_phsp_generator = TFWeightedPhaseSpaceGenerator(
initial_state_mass=model.reaction_info.initial_state[-1].mass,
final_state_masses={i: p.mass for i, p in model.reaction_info.final_state.items()},
)
data_generator = IntensityDistributionGenerator(
domain_generator=weighted_phsp_generator,
function=intensity_func_fvector,
domain_transformer=transformer,
)
data_momenta = data_generator.generate(50_000, rng)
data = transformer(data_momenta)
Show code cell source
fig, ax = plt.subplots(figsize=(9, 4))
fast_histogram(
np.real(data["m_01"]),
bins=200,
alpha=0.5,
density=True,
ax=ax,
)
mass_parameters = {p: v for p, v in toy_parameters_bw.items() if p.startswith("m_{")}
evenly_spaced_interval = np.linspace(0, 1, num=len(mass_parameters))
colors = [plt.cm.rainbow(x) for x in evenly_spaced_interval]
ax.set_xlabel("$m$ [GeV]")
for (k, v), color in zip(mass_parameters.items(), colors, strict=True):
ax.axvline(v, c=color, label=f"${k}$", ls="dotted")
ax.set_ylim(0, None)
ax.legend()
fig.savefig("data-f-vector.svg", bbox_inches="tight")
plt.show()
Perform fit#
Estimator definition#
estimator_bw = UnbinnedNLL(
intensity_func_bw,
data=data,
phsp=phsp,
backend="jax",
)
estimator_fvector = UnbinnedNLL(
intensity_func_fvector,
data=data,
phsp=phsp,
backend="jax",
)
Initial parameters#
Functions for comparing model to data
def indicate_masses(ax, intensity_func, ls: str, lw: float, typ: str):
mass_pars = {
k: v for k, v in intensity_func.parameters.items() if k.startswith("m_{")
}
for i, (k, v) in enumerate(mass_pars.items()):
ax.axvline(v, c=f"C{i}", label=f"${k}$ ({typ})", ls=ls, lw=lw)
def compare_model(
variable_name,
data,
phsp,
function1,
function2,
bins=100,
) -> Figure:
intensities1 = function1(phsp)
intensities2 = function2(phsp)
fig, ax = plt.subplots(figsize=(11, 4))
fig.subplots_adjust(right=0.85, top=0.95)
ax.set_xlabel(R"$m_{p\eta}$ [GeV]")
ax.set_ylabel("Intensity [a. u.]")
ax.set_yticks([])
data_projection = np.real(data[variable_name])
fast_histogram(
data_projection,
bins=bins,
alpha=0.5,
label="data",
density=True,
ax=ax,
)
phsp_projection = np.real(phsp[variable_name])
fast_histogram(
phsp_projection,
weights=np.array(intensities1),
bins=bins,
fill=False,
color="red",
label="Fit model with F vector",
density=True,
ax=ax,
)
fast_histogram(
phsp_projection,
weights=np.array(intensities2),
bins=bins,
fill=False,
color="blue",
label="Fit model with Breit-Wigner",
density=True,
ax=ax,
)
ax.set_ylim(0, None)
indicate_masses(ax, function1, ls="dashed", lw=1, typ="F vector")
indicate_masses(ax, function2, ls="dotted", lw=1, typ="Breit-Wigner")
fig.legend(loc="outside upper right")
fig.show()
return fig
initial_parameters_beta = {
R"\beta_{N_2(1/2^-)}": 1 + 0j,
R"\beta_{N_2(3/2^-)}": 1 + 0j,
}
initial_parameters_masses = {
R"m_{N_1(1/2^-)}": 1.6,
R"m_{N_2(1/2^-)}": 1.7,
R"m_{N_1(3/2^-)}": 1.8,
R"m_{N_2(3/2^-)}": 1.93,
}
initial_parameters_bw = {
**initial_parameters_beta,
**initial_parameters_masses,
R"\Gamma_{N_1(1/2^-)}": 1 / 1.6,
R"\Gamma_{N_2(1/2^-)}": 1 / 1.65,
R"\Gamma_{N_1(3/2^-)}": 1 / 1.85,
R"\Gamma_{N_2(3/2^-)}": 1 / 1.93,
}
initial_parameters_fvector = {
**initial_parameters_beta,
**initial_parameters_masses,
R"g_{N_1(1/2^-)}": 1.6,
R"g_{N_2(1/2^-)}": 1,
R"g_{N_1(3/2^-)}": 1.0,
R"g_{N_2(3/2^-)}": 1.0,
}
Show code cell source
original_parameters_bw = dict(intensity_func_bw.parameters)
intensity_func_bw.update_parameters(initial_parameters_bw)
original_parameters_fvector = dict(intensity_func_fvector.parameters)
intensity_func_fvector.update_parameters(initial_parameters_fvector)
fig = compare_model("m_01", data, phsp, intensity_func_fvector, intensity_func_bw)
fig.savefig("before-fit.svg", bbox_inches="tight")
Optimize parameters#
minuit2 = Minuit2()
fit_result_bw = minuit2.optimize(estimator_bw, initial_parameters_bw)
assert fit_result_bw.minimum_valid
fit_result_bw
FitResult(
minimum_valid=True,
execution_time=12.6900634765625,
function_calls=1682,
estimator_value=-7931.137816713118,
parameter_values={
'm_{N_1(1/2^-)}': 1.6539461175761394,
'm_{N_2(1/2^-)}': 1.7264292670507233,
'm_{N_1(3/2^-)}': 1.7328191451306136,
'm_{N_2(3/2^-)}': 1.908580828366668,
'\\Gamma_{N_1(1/2^-)}': 1.8524926041523075,
'\\Gamma_{N_2(1/2^-)}': 0.13046881396159438,
'\\Gamma_{N_1(3/2^-)}': 0.1573516791553556,
'\\Gamma_{N_2(3/2^-)}': 1.324380387465878,
'\\beta_{N_2(1/2^-)}': (-1.6897481365226579-1.1568947003976526j),
'\\beta_{N_2(3/2^-)}': (1.5375186053600138-0.44760512526013624j),
},
parameter_errors={
'm_{N_1(1/2^-)}': 0.004603319099772491,
'm_{N_2(1/2^-)}': 0.0006070545961158388,
'm_{N_1(3/2^-)}': 0.0015172566194625206,
'm_{N_2(3/2^-)}': 0.0077272263352300054,
'\\Gamma_{N_1(1/2^-)}': 0.06361609357610253,
'\\Gamma_{N_2(1/2^-)}': 0.005879299479538631,
'\\Gamma_{N_1(3/2^-)}': 0.019367706202832097,
'\\Gamma_{N_2(3/2^-)}': 0.05320290566507636,
'\\beta_{N_2(1/2^-)}': (0.1311293412261402+0.1278774780728592j),
'\\beta_{N_2(3/2^-)}': (0.11272653975271772+0.22792481789700972j),
},
)
fit_result_fvector = minuit2.optimize(estimator_fvector, initial_parameters_fvector)
assert fit_result_fvector.minimum_valid
fit_result_fvector
FitResult(
minimum_valid=True,
execution_time=13.932363510131836,
function_calls=1011,
estimator_value=-8199.598714182841,
parameter_values={
'm_{N_1(1/2^-)}': 1.6483798486877363,
'm_{N_2(1/2^-)}': 1.7483200793152158,
'm_{N_1(3/2^-)}': 1.8986142342242096,
'm_{N_2(3/2^-)}': 1.9496754881738385,
'g_{N_1(1/2^-)}': 1.6624278005128597,
'g_{N_2(1/2^-)}': 0.9493267500468856,
'g_{N_1(3/2^-)}': 1.0079832421160817,
'g_{N_2(3/2^-)}': 0.9800998848231933,
'\\beta_{N_2(1/2^-)}': (0.9606446724644881-0.00903038965870049j),
'\\beta_{N_2(3/2^-)}': (0.9709322547787542+0.008602453798822136j),
},
parameter_errors={
'm_{N_1(1/2^-)}': 0.001040462783404995,
'm_{N_2(1/2^-)}': 0.0006658939683350329,
'm_{N_1(3/2^-)}': 0.0011425836015682435,
'm_{N_2(3/2^-)}': 0.0015413452101229357,
'g_{N_1(1/2^-)}': 0.010998823395799513,
'g_{N_2(1/2^-)}': 0.018365301903530223,
'g_{N_1(3/2^-)}': 0.013236677643628186,
'g_{N_2(3/2^-)}': 0.03222752091156066,
'\\beta_{N_2(1/2^-)}': (0.015907378725063155+0.016622785545148663j),
'\\beta_{N_2(3/2^-)}': (0.040758323198047446+0.01968492218927783j),
},
)
Show code cell source
intensity_func_fvector.update_parameters(fit_result_fvector.parameter_values)
intensity_func_bw.update_parameters(fit_result_bw.parameter_values)
fig = compare_model("m_01", data, phsp, intensity_func_fvector, intensity_func_bw)
fig.savefig("after-fit.svg", bbox_inches="tight")
Fit result comparison#
Functions for inspecting fit result
def compute_aic_bic(fit_result: FitResult) -> tuple[float, float]:
n_real_par = fit_result.count_number_of_parameters(complex_twice=True)
n_events = len(next(iter(data.values())))
log_likelihood = -fit_result.estimator_value
aic = 2 * n_real_par - 2 * log_likelihood
bic = n_real_par * np.log(n_events) - 2 * log_likelihood
return aic, bic
def compare_parameters(initial: dict, optimized: dict, expected: dict) -> pd.DataFrame:
parameters = sorted(set(initial) | set(optimized))
df = pd.DataFrame(
{
f"${p}$": (
f"{initial[p]:.3g}",
f"{optimized[p]:.3g}",
f"{expected[p]:.3g}",
f"{100 * abs((optimized[p] - expected[p]) / expected[p]):.1f}%",
)
for p in parameters
},
).T
df.columns = ("initial", "fit result", "expected", "deviation")
return df
P vector#
compute_aic_bic(fit_result_fvector)
(-16375.197428365682, -16269.360088952759)
Show code cell source
compare_parameters(
initial=initial_parameters_fvector,
optimized=fit_result_fvector.parameter_values,
expected=original_parameters_fvector,
)
initial | fit result | expected | deviation | |
---|---|---|---|---|
$\beta_{N_2(1/2^-)}$ | 1+0j | 0.961-0.00903j | 1+0j | 4.0% |
$\beta_{N_2(3/2^-)}$ | 1+0j | 0.971+0.0086j | 1+0j | 3.0% |
$g_{N_1(1/2^-)}$ | 1.6 | 1.66 | 1.65 | 0.8% |
$g_{N_1(3/2^-)}$ | 1 | 1.01 | 1 | 0.8% |
$g_{N_2(1/2^-)}$ | 1 | 0.949 | 1 | 5.1% |
$g_{N_2(3/2^-)}$ | 1 | 0.98 | 1 | 2.0% |
$m_{N_1(1/2^-)}$ | 1.6 | 1.65 | 1.65 | 0.1% |
$m_{N_1(3/2^-)}$ | 1.8 | 1.9 | 1.95 | 2.6% |
$m_{N_2(1/2^-)}$ | 1.7 | 1.75 | 1.75 | 0.1% |
$m_{N_2(3/2^-)}$ | 1.93 | 1.95 | 1.9 | 2.6% |
Breit–Wigner#
compute_aic_bic(fit_result_bw)
(-15838.275633426236, -15732.438294013313)
Show code cell source
compare_parameters(
initial=initial_parameters_bw,
optimized=fit_result_bw.parameter_values,
expected=original_parameters_bw,
)
initial | fit result | expected | deviation | |
---|---|---|---|---|
$\Gamma_{N_1(1/2^-)}$ | 0.625 | 1.85 | 0.606 | 205.7% |
$\Gamma_{N_1(3/2^-)}$ | 0.541 | 0.157 | 0.541 | 70.9% |
$\Gamma_{N_2(1/2^-)}$ | 0.606 | 0.13 | 0.571 | 77.2% |
$\Gamma_{N_2(3/2^-)}$ | 0.518 | 1.32 | 0.526 | 151.6% |
$\beta_{N_2(1/2^-)}$ | 1+0j | -1.69-1.16j | 1+0j | 292.8% |
$\beta_{N_2(3/2^-)}$ | 1+0j | 1.54-0.448j | 1+0j | 69.9% |
$m_{N_1(1/2^-)}$ | 1.6 | 1.65 | 1.65 | 0.2% |
$m_{N_1(3/2^-)}$ | 1.8 | 1.73 | 1.85 | 6.3% |
$m_{N_2(1/2^-)}$ | 1.7 | 1.73 | 1.75 | 1.3% |
$m_{N_2(3/2^-)}$ | 1.93 | 1.91 | 1.9 | 0.5% |