Warning, /acts/docs/groups/sympy_codegen.md is written in an unsupported language. File is not indexed.
0001 @defgroup sympy_codegen Symbolic code generation
0002 @ingroup propagation
0003 @brief How the @ref Acts::SympyStepper kernels are derived with sympy, and the ATLAS-form arrangement they use
0004
0005 > [!tip]
0006 > This page is about how the stepping kernels are *produced*. For the stepper
0007 > interface and the rest of the propagation machinery see @ref propagation.
0008
0009 The @ref Acts::SympyStepper does not contain a hand-written Runge-Kutta step.
0010 Its two inner kernels — `rk4_vacuum` and `rk4_dense` — are emitted at build
0011 time by `Core/src/Propagator/generate_sympy_stepper.py`, which derives them
0012 symbolically with [sympy](https://www.sympy.org) and prints them as C++.
0013
0014 ## Why generate them
0015
0016 A Runge-Kutta stepper has to produce two things per step: the new track state,
0017 and the Jacobian of that state with respect to the old one. The second is
0018 simply the derivative of the first, but written by hand the two drift apart —
0019 a term added to the value path is easy to forget in the derivative path, and
0020 the resulting bug is a covariance that is quietly a few percent wrong rather
0021 than a test that fails.
0022
0023 Deriving the kernel symbolically removes that failure mode. The generator
0024 states the equations of motion once, applies the Runge-Kutta recursion to
0025 them, and lets sympy differentiate *the expressions that are actually
0026 evaluated*. Structural zeros, constant folding and — where it pays —
0027 common-subexpression elimination then happen on the symbolic form, before a
0028 single line of C++ exists.
0029
0030 The same machinery generates the bound/free Jacobian helpers
0031 (`generate_sympy_jac.py`) and the covariance transport
0032 (`generate_sympy_cov.py`); the printer and the symbolic helpers they share
0033 live in the `codegen` package at the repository root, which is also used by
0034 the [detray](https://github.com/acts-project/detray) backend.
0035
0036 ## How the build wires it up
0037
0038 `cmake/ActsCodegen.cmake` runs each generator through `uv` into a throw-away
0039 environment pinned by `codegen/requirements.txt`, so a build never depends on
0040 the ambient Python. `codegen/manifest.json` is the single place that records
0041 which generator produces which header; releases ship the generated files
0042 pre-built, and a build fully covered by them never invokes Python at all.
0043
0044 ## The equations of motion
0045
0046 With @f$\vec r@f$ the position, @f$\vec T@f$ the unit direction,
0047 @f$\lambda = q/p@f$, and @f$s@f$ the path length, the vacuum kernel integrates
0048
0049 @f[
0050 \frac{d\vec r}{ds} = \vec T, \qquad
0051 \frac{d\vec T}{ds} = \lambda\, \vec T \times \vec B, \qquad
0052 \frac{dt}{ds} = \frac{E}{p} = \sqrt{1 + \frac{m^2}{p^2}}, \qquad
0053 \frac{d\lambda}{ds} = 0 .
0054 @f]
0055
0056 This is a second-order system in @f$\vec r@f$, which is what makes the
0057 Runge-Kutta-Nyström form @cite Bugge:1981 the natural integrator. Stages 2 and
0058 3 share the midpoint, so the field is sampled at three points per step — at
0059 @f$\vec r@f$, at the midpoint, and near the end point — rather than four.
0060
0061 The dense kernel adds continuous energy loss, so @f$\lambda@f$ — and with it
0062 @f$dt/ds@f$ — evolve along the step:
0063
0064 @f[
0065 \frac{d\lambda}{ds} = \frac{\lambda^2}{q}\,\frac{dt}{ds}\,g ,
0066 \qquad
0067 \frac{d}{ds}\!\left(\frac{dt}{ds}\right) = \frac{m^2\lambda^3}{q^3}\, g ,
0068 @f]
0069
0070 with @f$g@f$ the mean energy loss per unit path length @cite Lund:2008ad.
0071 Because @f$\lambda@f$ now enters every stage, the dense kernel cannot use the
0072 pre-scaled arrangement below and builds the step Jacobian explicitly instead.
0073
0074 ## The ATLAS arrangement
0075
0076 The vacuum kernel does not evaluate the plain Runge-Kutta slopes
0077 @f$\vec k_i@f$. It follows the arrangement of the ATLAS `RungeKuttaPropagator`
0078 — which @ref Acts::AtlasStepper transcribes directly — and carries a
0079 **half-step bend vector** at each of the three field samples:
0080
0081 @f[
0082 \vec H_i = \frac{h\lambda}{2}\,\vec B_i .
0083 @f]
0084
0085 Every stage slope then comes out already scaled by @f$h/2@f$, as a bare cross
0086 product, and neither @f$h@f$ nor @f$\lambda@f$ appears again anywhere in the
0087 recursion:
0088
0089 @f[
0090 \begin{aligned}
0091 \texttt{kick1} &= \vec T \times \vec H_1 &&= \tfrac{h}{2}\vec k_1 \\
0092 \texttt{dir2} &= \vec T + \texttt{kick1} &&= \vec T + \tfrac{h}{2}\vec k_1 \\
0093 \texttt{dir\_half\_sum} &= \vec T + \texttt{dir2} &&= 2\vec T + \tfrac{h}{2}\vec k_1 \\
0094 \texttt{dir3} &= \vec T + \texttt{dir2} \times \vec H_2 &&= \vec T + \tfrac{h}{2}\vec k_2 \\
0095 \texttt{dir4} &= \vec T + \texttt{dir3} \times \vec H_2 &&= \vec T + \tfrac{h}{2}\vec k_3 \\
0096 \texttt{dir\_end} &= 2\,\texttt{dir4} - \vec T &&= \vec T + h\,\vec k_3 \\
0097 \texttt{kick4} &= \texttt{dir\_end} \times \vec H_3 &&= \tfrac{h}{2}\vec k_4
0098 \end{aligned}
0099 @f]
0100
0101 The two intermediate field sample points, the step result and the embedded
0102 error estimate all fall out of these:
0103
0104 @f[
0105 \begin{aligned}
0106 \vec r_2 &= \vec r + \tfrac{h}{4}\,\texttt{dir\_half\_sum}, \qquad
0107 \vec r_3 = \vec r + h\,\texttt{dir4}, \\
0108 \vec r' &= \vec r + \tfrac{h}{3}\left(\texttt{dir2} + \texttt{dir3} + \texttt{dir4}\right), \\
0109 3\,\vec T'_{\text{un}} &= \texttt{kick1} + 2\,\texttt{dir3} + \texttt{dir\_end} + \texttt{kick4}, \\
0110 \varepsilon &= 2|h| \left\|\,\texttt{dir\_half\_sum} + \texttt{kick4}
0111 - \texttt{dir3} - \texttt{dir4}\,\right\|_1
0112 = h^2 \left\|\vec k_1 - \vec k_2 - \vec k_3 + \vec k_4\right\|_1 .
0113 \end{aligned}
0114 @f]
0115
0116 > [!note]
0117 > ATLAS also replaces the direction normalisation @f$1/\|3\vec T'_{\text{un}}\|@f$
0118 > by a Taylor expansion around @f$3@f$, avoiding a square root and a division.
0119 > The generator can emit that form (`taylor_norm=True`), but it is off: it
0120 > trades the root for several multiplications on a chain that is not the
0121 > binding one, and measured neutral to slower.
0122
0123 ## Transporting the Jacobian
0124
0125 The kernel is handed the bound-to-free Jacobian @f$M@f$ and updates it in place,
0126 never forming the 8×8 free-to-free step Jacobian @f$D@f$. Each live column is
0127 pushed through *the same recursion as the state*, which is why the tangent block
0128 mirrors the value block line for line — as ATLAS' `d2A`/`d3A`/`d4A` block
0129 mirrors its `A0`..`A6`; see @ref sympy_codegen_naming for the two sets of
0130 names.
0131
0132 Rows are the eight free parameters @f$x_i@f$ **now**, columns the six bound
0133 parameters @f$b_j@f$ **at the start surface**,
0134 @f$M_{ij} = \partial x_i / \partial b_j@f$:
0135
0136 | | @f$l_0@f$ | @f$l_1@f$ | @f$\phi@f$ | @f$\theta@f$ | @f$\lambda@f$ | @f$t@f$ |
0137 |---|---|---|---|---|---|---|
0138 | @f$\vec r@f$ *(3 rows)* | hold | hold | step | step | step | · |
0139 | @f$t@f$ | · | · | · | · | step | 1 |
0140 | @f$\vec T@f$ *(3 rows)* | · | · | step | step | step | · |
0141 | @f$\lambda@f$ | · | · | · | · | dense | · |
0142
0143 `·` is a structural zero, `1` a constant one, `hold` an entry no step writes,
0144 `step` one every step writes, `dense` one only a dense step writes. The
0145 generator declares the sparsity in this one table and reads its index sets back
0146 off it.
0147
0148 ### The @f$q/p@f$ column
0149
0150 @f$\lambda@f$ enters the recursion only through the bend vector
0151 @f$\vec H = (h\lambda/2)\,\vec B@f$, so the @f$\lambda@f$ column is the only one
0152 with a term from the field's own @f$\lambda@f$ dependence: one 3-vector at each
0153 of the four stages that use a bend vector. Stored plainly, each of the four
0154 carries a factor @f$M_{\lambda\lambda}@f$, and the column has to be scaled by
0155 @f$\lambda@f$ into the recursion and unscaled out of it.
0156
0157 Storing it differentiated by @f$\log|\lambda|@f$ of the *current*
0158 @f$\lambda@f$, with the @f$\lambda@f$ row kept plain, removes both:
0159
0160 @f[
0161 M_{i\lambda} \;\equiv\; \frac{\partial x_i}{\partial \log|\lambda|}
0162 \;=\; \lambda \, \frac{\partial x_i / \partial \lambda_0}
0163 {\partial \lambda / \partial \lambda_0} ,
0164 \qquad i < 7 .
0165 @f]
0166
0167 This is exact because nothing but @f$\lambda_0@f$ can change @f$\lambda@f$, so
0168 @f$M_{\lambda\lambda}@f$ is the whole chain rule from
0169 @f$\partial/\partial\lambda_0@f$ to @f$\partial/\partial\lambda@f$: the division
0170 applies it, the factor carries on to the log, and the plain row inverts both.
0171
0172 It is cheaper because @f$\vec H@f$ is homogeneous of degree one in
0173 @f$\lambda@f$, so @f$\partial\vec H/\partial\log|\lambda| = \vec H@f$. Each
0174 stage's field term is then that stage's bend-linear part, with no
0175 @f$M_{\lambda\lambda}@f$ factor and no scaling around the recursion: nineteen
0176 multiplications and a division fewer per step, 395 floating-point operations
0177 with covariance transport instead of 415, against ATLAS' 394.
0178
0179 That identity is the one place the recursion departs from the plain chain rule,
0180 so the generator forms the chain-rule product as well and checks the two agree
0181 (`Derivation.check_same`).
0182
0183 @f$\lambda@f$ and @f$M_{\lambda\lambda}@f$ are constant across a vacuum step, so
0184 it carries the scaled form into itself; `rk4_dense` moves @f$\lambda@f$ and
0185 converts explicitly. The stepper state holds the scaled form, and
0186 `detail::sympy::toScaledBoundToFree` and its inverse convert where the
0187 covariance engine wants the plain Jacobian. The convention is singular at
0188 @f$\lambda = 0@f$, where the plain column already is.
0189
0190 ATLAS' `pVector[40]` block is the @f$M_{\lambda\lambda} = 1@f$ case: without
0191 dense material the row stays one, and the block stays permanently scaled by
0192 @f$\lambda@f$.
0193
0194 ## Naming {#sympy_codegen_naming}
0195
0196 The kernels were originally transcribed with ATLAS' variable names, which are
0197 positional rather than descriptive and are not defined in any published note —
0198 they are Athena source convention. The generator now uses names that say what
0199 the quantity is. The correspondence, for anyone reading the two side by side:
0200
0201 | generated name | ATLAS name | quantity |
0202 |---|---|---|
0203 | `pos`, `dir`, `qop` | `R`, `A`, `P[7]` (`CM`) | position, unit direction, @f$q/p@f$ |
0204 | `half_h_qop` | `PS2` (@f$=@f$ `Pi`@f$\cdot h@f$, with `Pi`@f$=\lambda/2@f$) | @f$h\lambda/2@f$ |
0205 | `bend1`, `bend2`, `bend3` | `H0`, `H1`, `H2` | @f$(h\lambda/2)\vec B_i@f$ at the three field samples |
0206 | `kick1` | `A0`, `B0`, `C0` | @f$\tfrac{h}{2}\vec k_1@f$ |
0207 | `dir2` | `A2`, `B2`, `C2` | @f$\vec T + \tfrac{h}{2}\vec k_1@f$ |
0208 | `dir_half_sum` | `A1`, `B1`, `C1` | @f$2\vec T + \tfrac{h}{2}\vec k_1@f$ |
0209 | `dir3` | `A3`, `B3`, `C3` | @f$\vec T + \tfrac{h}{2}\vec k_2@f$ |
0210 | `dir4` | `A4`, `B4`, `C4` | @f$\vec T + \tfrac{h}{2}\vec k_3@f$ |
0211 | `dir_end` | `A5`, `B5`, `C5` | @f$\vec T + h\vec k_3@f$ |
0212 | `kick4` | `A6`, `B6`, `C6` | @f$\tfrac{h}{2}\vec k_4@f$ |
0213 | `new_dir_x3` | — | @f$3\times@f$ the unnormalised new direction |
0214 | `h_third`, `h_quarter`, `two_over_h` | `S3`, `S4`, `Sl` | @f$h/3@f$, @f$h/4@f$, @f$2/h@f$ |
0215 | `dphi_*`, `dtheta_*`, `dqop_*` | `d2A*`, `d3A*`, `d4A*` | the tangent of the correspondingly named stage, one set per live column |
0216 | `M` | `pVector[8..55]` | bound-to-free Jacobian, column major |
0217 | `dEds`, `dEds1..4` | — | energy loss per unit path, per stage (@ref Acts::AtlasStepper has no material) |
0218
0219 Two things the rename fixes rather than preserves. ATLAS numbers its bend
0220 vectors `H0`..`H2` against field samples `B1`..`B3`; here both are numbered
0221 `1..3`. And the `A`-family index is not a stage index — `A1` is used *after*
0222 `A2` — whereas `kick1`/`dir2`/`dir3`/`dir4`/`dir_end`/`kick4` say which stage
0223 each belongs to.
0224
0225 ## References
0226
0227 The Runge-Kutta-Nyström track model and the semi-analytic transport of the
0228 derivatives alongside the trajectory come from @cite Bugge:1981. Adaptive
0229 Runge-Kutta-Nyström step control and error estimation in ATLAS are studied in
0230 @cite Lund:2009, and the extrapolation package the `RungeKuttaPropagator`
0231 belongs to is described in @cite Salzburger:2007.
0232
0233 None of these notes define the variable names the Athena implementation uses —
0234 they are source convention, and the table above is the only written record of
0235 what they mean.