# [ADR-002] Inserting dynamics#

**Status**: proposed**Deciders**: @redeboer @spflueger

## Context and problem statement#

Physics models usually include assumptions that simplify the structure of the model. For example, splitting a model into a product of independent parts, in which every part contains a certain responsibility. In case of partial wave amplitude models, we can make a separation into a spin part and a dynamical part. While the spin part controls the probability w.r.t angular kinematic variables, the dynamics controls the probability on variable like the invariant mass of states.

Generally, a dynamics part is simply a function, which is defined in complex space, and consists of:

a mathematical

**expression**(`sympy.Expr`

)a set of

**parameters**in that expression that can be tweaked (optimized)a set of (kinematic)

**variables**to which the expression applies

### Technical story#

ComPWA/expertsystem#440: no way to supply custom dynamics. Or at least,

`tensorwaves`

does not understand those custom dynamics.ADR-001: parameters

*and*variables are to be expressed as`sympy.Symbol`

s.ComPWA/expertsystem#454: dynamics are specified as a mapping of

`sympy.Function`

to a`sympy.Expr`

, but now there is no way to supply those`sympy.Expr`

s with expected`sympy.Symbol`

s (parameters and variables).

### Issues with existing set-up#

There is no clear way to apply dynamics functions to a specific decaying particle, that is, to a specific edge of the

`StateTransitionGraph`

s (`STG`

). Currently, we just work with a mapping of`Particle`

s to some dynamics expression, but this is not feasible when there there are identical particles on the edges.The set of variables to which a dynamics expression applies, is determined by the position within the

`STG`

that it is applied to. For instance, a relativistic Breit-Wigner that is applied to the resonance in some 1-to-3 isobar decay (described by an`STG`

with final state edges 2, 3, 4 and intermediate edge 1) would work on the invariant mass of edge 3 and 4 (`mSq_3+4`

).Just like variables, parameters need to be identifiable from their position within the

`STG`

(take a relativistic Breit-Wigner*with form factors*, which would require break-up momentum as a parameter), but also require some suggested starting value (e.g. expected pole position). These starting values are usually taken from the edge and node properties within the`STG`

.

## Decision drivers#

The following points are nice to have or can influence the decision but are not essential and can be part of the users responsibility.

The parameters that a dynamics functions requires, are registered automatically and linked together.

Kinematic variables used in dynamics functions are also linked appropriately.

It is easy to define custom dynamics (no boilerplate code).

### Solution requirements#

It is easy to apply dynamics to specific components of the

`STG`

s. Note: itâ€™s important that the dynamics can be applied to resonances of some**selected**graphs and not generally all graphs in which the resonance appears.Where possible, suggested (initial) parameter values are provided as well.

It is possible to use and inspect the dynamics expression itself independently from the

`expertsystem`

.Follow open-closed principle. Probably the most important decision driver. The solution should be flexible enough to handle any possible scenario, without having to change the interface defined in requirement 1!

## Considered solutions#

### Group 1: expression builder#

To satisfy requirement 1, we propose the following syntax:

```
# model: ModelInfo
# graph: StateTransitionGraph
model.set_dynamics(graph, edge_id=1, expression_builder)
```

Another style would be to have `ModelInfo`

contain a reference to the list of
`StateTransitionGraph`

s. The user then needs some other way to express which edges to
apply the dynamics function to:

```
model.set_dynamics(
filter=lambda p: p.name.startswith("f(0)"),
edge_id=1,
expression_builder,
)
```

Here, `expression_builder`

is some function or method that can create a dynamics
expression. It can also be a class that contains both the implementation of the
expression and a static method to build itself from a `StateTransitionGraph`

.

The dynamics expression needs to be formulated in such a way that it satisfies
the rest of the requirements. The following options illustrate
three different ways of formulating a dynamics expression, each taking a relativistic
Breit-Wigner and a relativistic Breit-Wigner *with form factor* as example.

### Group 2: expression-only#

A second branch of solutions would propose the following interface:

```
# model: ModelInfo
# graph: StateTransitionGraph
model.set_dynamics(graph, edge_id=1, expression)
```

The key difference is the usage of general sympy expression `sympy.Expr`

as an argument
instead of constructing this through some builder object.

## Solution evaluation#

### 1: Expression builder#

All of the solutions have the drawback arising from the choice of interface using a
`expression_builder`

. This enforces the logic of correctly coupling variables and
parameters into these builders. This is extremely hard to get right, since the code has
to be able to handle arbitrarily complex models. And always knowing what the user would
like to do is more or less impossible. Therefore it is much better to use a already
built expression that is assumed to be correctly built (see
solution group 2).

All of the solutions in this group also have the following additional drawbacks. These are however more related to the correct building of the dynamics expression:

There is an implicit assumption on the signature of the expression: the first arguments are assumed to be the (kinematic)

`variables`

and the remaining arguments are`parameters`

. In addition, the arguments cannot be keywords, but have to be positional.The number of

`variables`

and`parameters`

is only verified at runtime (no static typing, other than a check that each of the elements is`sympy.Symbol`

).

Composition is the cleanest design, but is less in tune with the design of `sympy`

.
Subclassing sympy.Function and Subclassing sympy.Expr follow `sympy`

implementations, but result
in an obscure inheritance hierarchy with implicit conventions. This can result in some
nasty bugs, for instance if one were to `__call__`

method in either the `sympy.Function`

or `sympy.Expr`

implementation.

Pros and Cons that are specific to each of the implementations are listed below.

#### Using composition#

**Positive**Implementation of the expression is transparent

**Negative**The only way to see that

`relativistic_breit_wigner_from_graph`

is the builder for`relativistic_breit_wigner`

, is from its name. This makes it implementing custom dynamics inconvenient and error-prone.Signature of the builder can only be checked with a

`Protocol`

, see Type checking.

#### Subclassing sympy.Function#

**Positive**`DynamicsFunction`

behaves as a`Function`

Implementation of the builder is kept together with the implementation of the expression.

**Negative**Itâ€™s not possible to identify variables and parameters

#### Subclassing sympy.Expr#

**Positive**When recursing through the amplitude model, it is still possible to identify instances of

`DynamicsExpr`

(before`doit()`

has been called).Additional properties and methods can be added and carried around by the class.

**Negative**Boilerplate code required when implementing custom dynamics

### 2: Expression-only#

**Positive**: This choice of interface follows the principle of SOLID more than solution
group 1. By handing a complete expression of the dynamics to the setter, its sole
responsibility is to insert this expression at the correct place in the full model
expression.

**Negative**: There are no direct negative aspects to this solution, as it just splits
up responsibilities. The construction of the expression with the correct linking of
parameters and initial values etc has to be performed by some other code. This code is
subject to the same issues mentioned in the individual solutions of group 1.

## Decision outcome#

Use a composition based solution from group 2.

Important is the definition of the interface following solution group 2. This ensures to be open-closed and keep the responsibilities separated.

The `expertsystem`

favors **composition over inheritance**: we intend to use inheritance
only to define interfaces, not to insert behavior. As such, the design of the
`expertsystem`

is fundamentally different than that of SymPy. Thatâ€™s another reason to
favor composition here: the interfaces are not determined by the dependency and instead
remain contained within the dynamics class.

We decide to keep responsibilities as separated as possible. This means that:

The only responsibility of

`set_dynamics`

method is to attribute some expression (`sympy.Expr`

) the correct symbol within the complete amplitude model. For now, this position is specified using some`StateTransitionGraph`

and an edge ID, but this syntax may be improved later (see ComPWA/expertsystem#458ps://github.com/ComPWA/expertsystem/issues/458)):def set_dynamics( self, graph: StateTransitionGraph, edge_id: int, expression: sp.Expr, ) -> None: # dynamics_symbol = graph + edge_id # self.dynamics[dynamics_symbol] = expression pass

It is assumed that the

`expression`

is correct.The user has the responsibility of formulating the

`expression`

with the correct symbols. To aid the user in the construction of such expressions some building code can handle some of the common tasks, such asA

`VariablePool`

can facilitate finding the correct symbol names (to avoid typos).mass = variable_pool.get_invariant_mass(graph, edge_id)

A

`dynamics`

module provides descriptions of common line-shapes as well as some helper functions. An example would be:inv_mass, mass0, gamma0 = build_relativistic_breit_wigner(graph, edge_id, particle) rel_bw: sympy.Expr = relativistic_breit_wigner(inv_mass, mass0, gamma0) model.set_dynamics(graph, edge, rel_bw)

The

`SympyModel`

has the responsibility of defining a the full model in terms of an expression and keeping track of variables and parameters, for instance:from __future__ import annotations from attrs import define, field import sympy as sp @define class SympyModel: top: sp.Expr # intensities: dict[sp.Symbol, sp.Expr] = field(factory=dict) # amplitudes: dict[sp.Symbol, sp.Expr] = field(factory=dict) dynamics: dict[sp.Symbol, sp.Expr] = field(factory=dict) parameters: set[sp.Symbol] = field(factory=set) variables: set[sp.Symbol] = field(factory=set) # or: VariablePool def full_expression(self) -> sp.Expr: ...