# [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¶

#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.#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 #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:@attr.s(kw_only=True) class SympyModel: top: sp.Expr = attr.ib() # intensities: Dict[sp.Symbol, sp.Expr] = attr.ib(default={}) # amplitudes: Dict[sp.Symbol, sp.Expr] = attr.ib(default={}) dynamics: Dict[sp.Symbol, sp.Expr] = attr.ib(default={}) parameters: Set[sp.Symbol] variables: Set[sp.Symbol] # or: VariablePool def full_expression(self) -> sp.Expr: ...