# 9. Export ## Physics statement Extracted vertices ({doc}`vertices`) are only useful once they leave `feynlag`'s internal representation: as a table for a paper, or as a model directory a Monte Carlo generator can actually load. `feynlag` supports both — a LaTeX vertex table, and a full UFO (Universal FeynRules Output) model directory importable by MadGraph and compatible tools. ## LaTeX tables `latex_feynman_table(interactions, simplify_coeff=None, extra_column=None)` (`export/latex.py:27`) unifies three near-duplicate table generators from the DLRSM1 reference into one configurable function. `_rows_from_interactions` (`latex.py:12`) accepts either extraction output shape — the nested `{n_fields: {field_tuple: coeff}}` from `extract_interaction_coefficients` directly, or an already-flattened `{field_tuple: coeff}` dict — so it can sit immediately downstream of either `Model.interactions` or a hand-assembled rule table. `extra_column` optionally adds a third column (e.g. a numerically simplified or series-approximated form of the same coefficient) without duplicating the two-column code path. ## UFO export: the parameter closure requirement A UFO `parameters.py` is a flat Python module where every `Parameter(...)` object must be defined **before** any later parameter that references it in its `value=` expression — i.e. it needs exactly the dependency-ordered list {doc}`declaration` already builds. `_UFOBuilder._parameters_py` (`export/ufo/writer.py:294`) calls `self.parameters.dependency_order()` directly and emits one `Parameter(...)` line per entry in that order — the UFO writer does not re-derive parameter ordering; it reuses {class}`~feynlag.parameters.ParameterSet`'s topological sort verbatim. ## The vertex catalog → UFO Lorentz structures `export/ufo/lorentz_map.py`'s `UFO_LORENTZ` dict is the concrete UFO string for every entry of the closed catalog from {doc}`vertices` — e.g. `VVS1 = "Metric(1,2)"`, `FFVL = "Gamma(3,2,-1)*ProjM(-1,1)"`. UFO's index convention numbers *legs* (not Lorentz indices in the abstract): `P(l, n)` is the momentum of leg `n` carried on Lorentz slot `l`; `Metric(m, n)` contracts legs `m` and `n`; negative integers are UFO's convention for internally-contracted (summed) indices, as in `Gamma(3,2,-1)*ProjM(-1,1)` contracting the spinor index `-1` between the gamma matrix and the chiral projector. `structures_for(vertex_type)` maps a catalog key to the list of Lorentz-structure names UFO needs populated for that vertex type — e.g. `FFV` always needs both `FFVL` and `FFVR` slots, even if one coupling is zero (`_UFOBuilder.add_fermion_vertex` simply skips emitting a slot whose coupling is exactly `0`). **Four-fermion (FFFF)** operators export through `_UFOBuilder.add_four_fermion_vertex` (and the `write_ufo(four_fermion_vertices=…)` kwarg). The two Dirac chains sit on legs `(1,2)` and `(3,4)`; each chain mirrors the single-fermion `FFSL`/`FFVL` spinor pattern, and the vector case shares one internally-contracted Lorentz index across the chains, e.g. `FFFFVLL = "Gamma(-1,2,-2)*ProjM(-2,1)*Gamma(-1,4,-3)*ProjM(-3,3)"` — the repeated `-1` is the $\gamma^\mu\!\otimes\!\gamma_\mu$ contraction, `-2`/`-3` the per-chain spinor dummies. There are eight such names (scalar⊗scalar and vector⊗vector, each with the four L/R projector combinations); a mixed scalar⊗vector structure is outside the v1 catalog and raises. As with the FFV path, the adder applies the Feynman-rule `i` to each Lagrangian coefficient (its omission is invisible in a single $|\mathcal M|^2$ but breaks interference). **Export the h.c. vertex too.** A Hermitian Lagrangian's `op + h.c.` is *two* four-fermion vertices (two distinct bilinear keys), and **MadGraph needs both** to route fermion-number flow through the contact interaction — a UFO carrying only one of the conjugate pair fails diagram generation with a `NoDiagramException`, even though the single vertex is otherwise well-formed. This is a MadGraph requirement, not a feynlag one (the exported Lorentz string is byte-for-byte the one MadGraph's own `taudecay_UFO` uses for τ decay). The validated recipe is in `scripts/madgraph_fermi.py`, which reproduces the muon lifetime Γ(μ→eνν) = $G_F^2 m_\mu^5/192\pi^3$ from the exported Fermi UFO. ## SU(3): real color-tensor strings, one particle per gluon Colored vertices need actual UFO color-tensor strings, not the hardcoded singlet `'1'` used for EW self-couplings after EWSB: - **`add_bosonic_vertex`** (the extractor-driven, color-singlet-only path used for Higgs/EW self-couplings) **raises** if any particle passed to it has `color != 1` — rather than silently emitting a wrong `color=['1']` for a vertex this code path genuinely doesn't know how to color-tensor. Colored self-couplings must go through the dedicated adders below. - **`add_vvv_vertex`/`add_vvvv_vertex`** take an explicit `color=` (or `colors=`) string. A `ggg` vertex uses `color='f(1,2,3)'` — the three gluon legs at positions 1, 2, 3 directly contracted with the adjoint structure constant. A `gggg` vertex needs **three** color tensors, one per independent Lorentz structure (`VVVV1/2/3`, {doc}`vertices`), matching the three `f·f` color factors UFO expects for a 4-gluon vertex. - **`add_fermion_vertex`**'s `color=` follows the leg ordering `[bar_symbol, field_symbol, boson]` (positions 1, 2, 3) — a `qqg` vertex uses `color='T(3,2,1)'`, i.e. $T^{a=\text{leg 3}}_{i=\text{leg 2},\, j=\text{leg 1}}$. UFO reads `T(a,i,j)` with **`i` the fundamental (quark) index and `j` the anti-fundamental**, so the field leg comes first and the bar leg second; MadGraph's stock `sm` emits the same thing for `[u~, u, g]`. This said `T(3,1,2)` until the QCD sector was first run in MadGraph. That reasoning came from `fermion_gauge_current`'s `T[r,c]` Lagrangian index order (row with the bar leg), which is **not** UFO's leg convention; transposing a hermitian $T^a$ conjugates it, flipping the sign of the $ggg$ interference. `u u~ > g g` failed MadGraph's Lorentz and gauge/Ward checks until it was corrected — see {doc}`../benchmark`. Critically, **an unbroken non-abelian self-coupling is exported as one physical particle referenced multiple times** (e.g. `ggg` triples the single `g` UFO particle), color summed entirely inside the color-tensor string — never as eight separate weak-basis adjoint components. The group's full weak-basis component dictionary (`group.bosons().components`) exists for internal symbolic verification only ({doc}`vertices`'s `cubic_couplings`/`quartic_couplings`), not for UFO particle declarations. That has a consequence which is easy to get wrong for the **quartic**: `quartic_couplings`'s weak-basis entry is $-g^2/4\sum_e f^{ije}f^{kle}$ and therefore **already contains the colour contraction**, so pairing it with a colour-tensor string multiplies by colour twice. Use `export.ufo.vvvv.adjoint_vvvv(group)` with `ADJOINT_VVVV_COLORS`, which strips it and gives $ig^2$ per structure — MadGraph's `GC_12` for $gggg$. The **cubic** is the same story: `cubic_couplings` returns $-g\,f^{abc}$, so use `adjoint_vvv(group)` with `ADJOINT_VVV_COLOR`, giving $-g$ — MadGraph's `GC_10`. It escaped notice far longer only because the exported triple is $(G_1,G_2,G_3)$ and $f^{123}=1$. For a **broken** group the physical-basis quartics are colour-singlet and come straight from `Model.gauge_vertices()` ({doc}`vertices`, `feynlag/gauge_basis.py`), whose `Vertex` objects carry their three per-structure couplings in `meta['structures']`. ### The field → particle leg sign feynlag's symbols label **fields**; a UFO leg labels a **particle**, and the field $W^+$ *creates* a $W^-$ — so the leg carrying the symbol `Wp` is UFO's `W-` leg. Emitting legs under their naive names transposes each conjugate pair, and the writer applies whatever that costs (`export/ufo/legs.py::structure_leg_sign`), reading the pairing off `UFOParticle.antisymbol`: | structure | sign | |---|---| | `VVV1` | permutation parity (totally antisymmetric) | | `VSS1` | $-1$ when the two scalars are a conjugate pair | | `VVS1`, `VVSS1`, `SSS1`, `SSSS1` | $+1$ | | `VVVV1/2/3` | must be invariant — verified, raises otherwise | | `FF*` | excluded (own bar/field leg convention) | **Charged Goldstones.** Two further factors map feynlag's conventions onto UFO's, both in `export/ufo/legs.py` beside the leg sign: - `VSS1` carries one power of momentum, and feynlag's $\partial_\mu\to ip_\mu$ differs from UFO's by a sign there, so a VSS picks up $-1$ **unconditionally**; - feynlag's charged Goldstone carries a phase $i^{-q}$ relative to MadGraph's, so each charged-Goldstone leg contributes $i^{q}$ (`charged_goldstone_phase`). A conjugate pair gives $i\cdot i^{-1}=1$, which is why $\gamma G^+G^-$, $ZG^-G^+$ and $W^+W^-G^+G^-$ matched all along. This was **a convention, not an error** (issue #22): before the alignment MadGraph already agreed between unitary and Feynman gauge, and a deliberately-broken phase failed that check. After it, all four of MadGraph's gauges agree to $3\times10^{-15}$ on $\gamma\gamma\to W^+W^-$ and every charged-Goldstone coupling matches stock `sm` entry-by-entry (`tests/test_ufo_export.py`). Note the $-1$ for `VSS1` used to be applied only when the two scalars were a conjugate pair. For $\gamma G^+G^-$ the two rules agree, so that vertex matched MadGraph and looked like confirmation; solving for the exact transformation across all eight charged-Goldstone vertices showed the pair condition was coincidental and the factor is uniform. Because the writer owns it, a `Vertex` coupling is always in feynlag's own convention and `gauge_self_couplings` takes no `conjugates=`. An unrecorded structure raises rather than defaulting to $+1$ — that default is what left every exported Feynman-gauge `VSS` wrong by a sign until this existed. ## Design gotchas - **UFO cannot export symbolic gauge charges.** UFO's particle table calls `float()` on each particle's charge internally; a model like `examples/sm_u1x.py` that deliberately keeps its U(1)_X charge assignment symbolic (`X = a·Y + b·(B−L)`, {doc}`declaration`) cannot be exported to UFO without first fixing `a`, `b` to numbers. This is a hard limitation of the UFO format, not a gap in `feynlag`'s writer. - **A `Parameter` referenced before its `dependency_order` slot is a bug, not a feature to special-case around** — if `_parameters_py` ever needs to reorder or retry, that indicates the underlying `ParameterSet` (or the caller's parameter list) is missing a dependency, and should be fixed at the declaration layer ({doc}`declaration`), not patched in the writer. ## Validation - `tests/test_latex.py::test_table_from_nested_dict`, `::test_table_from_flat_dict_with_extra_column` — both accepted input shapes. - `tests/test_ufo_export.py::test_ufo_imports`, `::test_ufo_parameters_resolve`, `::test_ufo_couplings_pinned` — the generated model actually imports, parameters resolve in dependency order, and `hWW` is pinned numerically. - `tests/test_ufo_sm_bosonic.py` — the exported SM UFO's four quartic gauge couplings (WWWW/WWZZ/WWAA/WWAZ) and its VVS/VVSS couplings, compared numerically against MadGraph's stock `sm` at the same parameter point, in the convention-free metric-pair basis (MG's VVVV basis differs from feynlag's). - `tests/test_gauge_basis.py` — the derived weak→physical `U` equals the matrix that used to be hand-typed, and the same four quartics come out of `Model.gauge_vertices()`. - `tests/test_ufo_qcd.py::test_qqg_color_string`, `::test_ggg_color_string_and_coupling`, `::test_gggg_color_strings_and_couplings`, `::test_gluon_particle_is_color_octet_self_conjugate` — the SU(3) color-tensor strings and the one-particle-per-gluon convention. ## Minimal snippet ```python from feynlag.export.ufo.writer import write_ufo write_ufo("out/SM_UFO", "SM", model.parameters, particle_specs, bosonic_vertices=[...], vvv={...}, vvvv={...}, fermion_vertices=[...]) ```