Amplitude model with sum notation#
Problem description#
Challenge
Formulate the HelicityModel in such a way that:
The sum over the amplitudes is concise and expresses that the sum depends only on helicities.
It is the
sympy.Exprof the amplitude model can easily and uniquely be constructed from the data in theHelicityModel. (Currently, this is as simple asHelicityModel.expression.doit).All parameters under
parameter_defaultsare of typesympy.Symbol. This is important for a correct lambdification of the arguments withsympy.utilities.lambdify.lambdify().
This report presents two solutions:
ampform#245 implements spin alignment, which results in large sum combinatorics for all helicity combinations. The result is an amplitude model expression that is too large to be rendered as LaTeX.
To some extend, this is already the case with the current implementation of the ‘standard’ helicity formalism [Jacob and Wick, 1959, Richman, 1984, Kutschke, 1996, Chung, 2014]: many of the terms in the total intensity expression differ only by the helicities of the final and initial state.
reaction = qrules.generate_transitions(
initial_state="Lambda(c)+",
final_state=["K-", "p", "pi+"],
formalism="helicity",
allowed_intermediate_particles=["Delta(1600)++"],
particle_db=MODIFIED_PDG,
)
builder = ampform.get_builder(reaction)
model = builder.formulate()
Here, we did not insert any dynamics, but it is unusual that dynamics expressions depend on helicity or spin projection (see TwoBodyKinematicVariableSet).
Simplified notation with PoolSum#
Both Solution 1: Indexed coefficients and Solution 2: Indexed amplitude components require the definition of a special “PoolSum” class to simplify the summation over the amplitudes. The class mimics sympy.Sum in that it substitutes certain Symbols in an expression over which we symbol a range of values. The range of values in a PoolSum does not have to be a sequential range, but can be a collection of arbitrary items.
Here’s a sketch of how to construct the amplitude model with a PoolSum:
Solution 1: Indexed coefficients#
The current implementation of the HelicityAmplitudeBuilder has to be changed quite a bit to produce an amplitude model with PoolSums. First of all, we have to introduce special Symbols for the helicities, \(\lambda_i\), with \(i\) the state ID (taking a sum of attached final state IDs in case of a resonance ID). Next, formulate_wigner_d() has to be modified to insert these Symbols into the WignerD:
wigner_functions = {
sp.Mul(*[
formulate_wigner_d(transition, node_id) for node_id in transition.topology.nodes
])
for transition in reaction.transitions
}
display(*wigner_functions)
We also have to collect the allowed helicity values for each of these helicity symbols.
inner_helicities = get_helicities(reaction, which="inner")
outer_helicities = get_helicities(reaction, which="outer")
display(inner_helicities, outer_helicities)
{lambda_3: {-1/2, 1/2}}
{lambda: {-1/2, 1/2}, lambda_0: {0}, lambda_1: {-1/2, 1/2}, lambda_2: {0}}
These collected helicity values can then be combined with the Wigner-\(D\) expressions through a PoolSum:
def formulate_intensity(reaction: ReactionInfo):
wigner_functions = {
sp.Mul(*[
formulate_wigner_d(transition, node_id)
for node_id in transition.topology.nodes
])
for transition in reaction.transitions
}
inner_helicities = get_helicities(reaction, which="inner")
outer_helicities = get_helicities(reaction, which="outer")
return PoolSum(
sp.Abs(
PoolSum(
sum(wigner_functions),
*inner_helicities.items(),
)
)
** 2,
*outer_helicities.items(),
)
formulate_intensity(reaction)
This is indeed identical to the model as formulated with the existing implementation:
assert formulate_intensity(reaction).doit() == full_expression.doit()
Note how this approach also works in case there are two decay topologies:
formulate_intensity(reaction_two_resonances).cleanup()
Inserting coefficients#
There is a problem, though: the sums above could be written in sum form because the values over which we sum appear as arguments (args in the WignerD functions). This is only true because we previously set all coefficients to \(1\). The coefficient names are, however, also ‘dependent’ on the helicities in the final state over which we sum:
[C_{\Lambda_{c}^{+} \to \Delta_{-1/2} K^{-}_{0}; \Delta \to p_{+1/2} \pi^{+}_{0}},
C_{\Lambda_{c}^{+} \to \Delta_{+1/2} K^{-}_{0}; \Delta \to p_{+1/2} \pi^{+}_{0}}]
We therefore have to somehow introduce a dependence in these Symbols on the helicity values. An idea may be to use IndexedBase. Modifying the function introduced in Solution 1: Indexed coefficients:
C = sp.IndexedBase("C")
@cache
def formulate_intensity_with_coefficient(reaction: ReactionInfo):
amplitudes = {
sp.Mul(
C[
[
create_helicity_symbol(transition.topology, state_id)
for state_id in transition.final_states
]
],
*[
formulate_wigner_d(transition, node_id)
for node_id in transition.topology.nodes
],
)
for transition in reaction.transitions
}
inner_helicities = get_helicities(reaction, which="inner")
outer_helicities = get_helicities(reaction, which="outer")
return PoolSum(
sp.Abs(
PoolSum(
sum(amplitudes),
*inner_helicities.items(),
)
)
** 2,
*outer_helicities.items(),
)
indexed_coefficient_expr = formulate_intensity_with_coefficient(reaction)
indexed_coefficient_expr
Caveat
Using IndexedBase makes the coefficient names concise, but harder to understand.
This seems to work rather well, but there is a subtle problems introduced by writing the coefficients as a IndexedBase: the IndexedBase itself is considered listed under the free_symbols of the expression.
[C, C[0, -1/2, 0], C[0, 1/2, 0], phi_12, phi_1^12, theta_12, theta_1^12]
In addition, not all symbols in the expression are of type Symbol anymore:
{s: (type(s), isinstance(s, sp.Symbol)) for s in free_symbols}
{C: (sympy.core.symbol.Symbol, True),
C[0, -1/2, 0]: (sympy.tensor.indexed.Indexed, False),
C[0, 1/2, 0]: (sympy.tensor.indexed.Indexed, False),
phi_12: (sympy.core.symbol.Symbol, True),
phi_1^12: (sympy.core.symbol.Symbol, True),
theta_12: (sympy.core.symbol.Symbol, True),
theta_1^12: (sympy.core.symbol.Symbol, True)}
This will become problematic when lambdifying, because it results in an additional argument in the signature of the generated function:
func = sp.lambdify(free_symbols, indexed_coefficient_expr.doit())
inspect.signature(func)
<Signature (C, Dummy_24, Dummy_23, phi_12, Dummy_26, theta_12, Dummy_25)>
A solution may be to use symplot.substitute_indexed_symbols():
indexed_coefficient_expr_symbols_only = symplot.substitute_indexed_symbols(
indexed_coefficient_expr.doit()
)
indexed_coefficient_expr_symbols_only.free_symbols
{C_{0,-1/2,0}, C_{0,1/2,0}, phi_12, phi_1^12, theta_12, theta_1^12}
args = sorted(indexed_coefficient_expr_symbols_only.free_symbols, key=str)
func = sp.lambdify(args, indexed_coefficient_expr_symbols_only)
inspect.signature(func)
<Signature (Dummy_30, Dummy_29, phi_12, Dummy_28, theta_12, Dummy_27)>
Caveat
This seems clumsy, because substitute_indexed_symbols() would have to be actively called before creating a computational function with TensorWaves. It also becomes a hassle to keep track of the correct Symbol names in HelicityModel.parameter_defaults.
Inserting dynamics#
Dynamics pose a challenge that is similar to Inserting coefficients in that we have to introduce expressions that are dependent on spin. Still, as can be seen from the available attributes on a TwoBodyKinematicVariableSet (which serves as input to ResonanceDynamicsBuilders), dynamics (currently) cannot depend on helicities.
What may become a problem are \(LS\)-combinations. So far we have only considered a ReactionInfo that was created with formalism="helicity", but we also have to sum over \(LS\)-combinations when using formalism="canonical-helicity". This is particularly important when using dynamics with form factors, which depend on angular_momentum.
Note
The sympy.tensor.indexed.Indexed now also contains the names of the resonances.
def formulate_intensity_with_dynamics(
reaction: ReactionInfo,
dynamics_choices: dict[str, ResonanceDynamicsBuilder],
):
amplitudes = set()
for transition in reaction.transitions:
final_state_helicities = [
create_helicity_symbol(transition.topology, state_id)
for state_id in transition.final_states
]
resonances = [
Str(s.particle.latex) for s in transition.intermediate_states.values()
]
indices = [*final_state_helicities, *resonances]
coefficient = C[indices]
expr: sp.Expr = coefficient
for node_id in sorted(transition.topology.nodes):
expr *= formulate_wigner_d(transition, node_id)
decay = TwoBodyDecay.from_transition(transition, node_id)
parent_particle = decay.parent.particle
dynamics_builder = dynamics_choices.get(
parent_particle.name, create_non_dynamic
)
variables = _generate_kinematic_variable_set(transition, node_id)
dynamics, _ = dynamics_builder(parent_particle, variables)
expr *= dynamics
amplitudes.add(expr)
inner_helicities = get_helicities(reaction, which="inner")
outer_helicities = get_helicities(reaction, which="outer")
return PoolSum(
sp.Abs(
PoolSum(sum(amplitudes), *inner_helicities.items()),
evaluate=False,
)
** 2,
*outer_helicities.items(),
)
formulate_intensity_with_dynamics(
reaction,
dynamics_choices={
resonance.name: create_relativistic_breit_wigner
for resonance in reaction.get_intermediate_particles()
},
)
formulate_intensity_with_dynamics(
reaction_two_resonances,
dynamics_choices={
resonance.name: create_relativistic_breit_wigner
for resonance in reaction_two_resonances.get_intermediate_particles()
},
)
The resulting amplitude is again identical to the original HelicityModel.expression:
Solution 2: Indexed amplitude components#
The main problem with Solution 2: Indexed amplitude components is that it requires changing coefficient Symbols to instances of Indexed, which have to be substituted using substitute_indexed_symbols() (after calling doit()).
An alternative would be insert dynamics (and coefficients) into the PoolSums over the helicities is to index the amplitude itself. The HelicityModel.expression would then contain Indexed symbols that represent specific amplitudes. A definition of these amplitudes can be provided through HelicityModel.components or an equivalent attribute.
expression, amplitudes = formulate_intensity_indexed_amplitudes_only(
reaction_two_resonances,
dynamics_choices={
resonance.name: create_relativistic_breit_wigner
for resonance in reaction_two_resonances.get_intermediate_particles()
},
)
\(\dots\)
The resulting amplitude is indeed identical to the original HelicityModel.expression:
Tip
Currently, amplitudes with different resonances are put under a different amplitude symbol, identified by that resonance. Such resonances can be combined, e.g. \(\mathcal{A}_{\lambda_i} = \mathcal{A}_{\lambda_i,\Delta} + \mathcal{A}_{\lambda_i,\Lambda}\). This would also make it easier to introduce correct interference terms through the \(K\)-matrix.
Question
The helicity of the intermediate state is also passed to the indexed amplitude. This is required for the coefficient name, which has a helicity subscript for the intermediate state, e.g. \(C_{\Lambda_{c}^{+} \to \Lambda_{\color{red}+1/2} \pi^{+}_{0}; \Lambda \to K^{-}_{0} p_{+1/2}}\). Does it really make sense to distinguish coefficients for different helicities of intermediate states?