/
002.md
282 lines (217 loc) · 11.6 KB
/
002.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# [ADR-002] Inserting dynamics
- **Status**: proposed
- **Deciders**: @redeboer @spflueger
```{autolink-skip} file
```
## 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#124](https://github.com/ComPWA/expertsystem/issues/124): see {ref}`adr/002:Issues with existing set-up`.
- [ComPWA/expertsystem#440](https://github.com/ComPWA/expertsystem/issues/440): no way to supply custom dynamics. Or at least,
{mod}`tensorwaves` does not understand those custom dynamics.
- [ADR-001](./001.md): parameters _and_ variables are to be expressed as
`sympy.Symbol`s.
- [ComPWA/expertsystem#454](https://github.com/ComPWA/expertsystem/issues/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.
1. The parameters that a dynamics functions requires, are registered automatically and
linked together.
2. Kinematic variables used in dynamics functions are also linked appropriately.
3. It is easy to define custom dynamics (no boilerplate code).
### Solution requirements
1. 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.
2. Where possible, suggested (initial) parameter values are provided as well.
3. It is possible to use and inspect the dynamics expression itself independently from
the `expertsystem`.
4. 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](#solution-requirements), we propose the following syntax:
```python
# model: ModelInfo
# graph: StateTransitionGraph
model.set_dynamics(graph, edge_id=1, expression_builder)
```
:::{toggle}
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:
```python
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](#solution-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.
```{toctree}
---
maxdepth: 1
---
002/composition
002/function
002/expr
```
### Group 2: expression-only
A second branch of solutions would propose the following interface:
```python
# 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](#group-2-expression-only)).
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 {mod}`sympy`.
{doc}`002/function` and {doc}`002/expr` follow {mod}`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.
#### {doc}`002/composition`
- **Positive**
- Implementation of the expression is transparent
- **Negative**
- {ref}`adr/002/composition:Alternative signature`.
- 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 {class}`~typing.Protocol`, see
{ref}`adr/002/composition:Type checking`.
#### {doc}`002/function`
- **Positive**
- `DynamicsFunction` behaves as a {class}`~sympy.core.function.Function`
- Implementation of the builder is kept together with the implementation of the
expression.
- **Negative**
- It's not possible to identify variables and parameters
#### {doc}`002/expr`
- **Positive**
- When
{doc}`recursing through the amplitude model <sympy:tutorials/intro-tutorial/manipulation>`,
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
- {ref}`adr/002/expr:Issue with lambdify`
### 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 {doc}`contained within the dynamics class <002/composition>`.
We decide to keep responsibilities as separated as possible. This means that:
1. 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#458](https://github.com/ComPWA/expertsystem/issues/458)ps://github.com/ComPWA/expertsystem/issues/458)):
```python
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.
2. 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 as
- A `VariablePool` can facilitate finding the correct symbol names (to avoid typos).
```python
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:
```python
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)
```
3. 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:
```python
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: ...
```