# 8. Extracting Vertices ## Physics statement A Feynman vertex is read off the coefficient of a field monomial in the Lagrangian — literally $n$ functional derivatives with respect to the fields at that vertex, evaluated with every field set to zero. Once the Lagrangian is in the physical basis ({doc}`diagonalization`), extracting every vertex means: expand the Lagrangian, group terms by which fields (and how many of each) appear, convert any derivative coupling to a momentum-space tag, and multiply by the combinatorial factor that turns a monomial coefficient into an actual Feynman rule. ## The two-track split, revisited Bosonic fields are plain commuting `sympy.Symbol`s, so grouping "terms with the same field content" is literally polynomial-coefficient extraction — `vertices/extract.py` does this with SymPy's `Poly`. Fermion fields inside a `Bilinear` are never commuting symbols in the same sense (a fermion sandwich is a c-number *once fully contracted*, but its two legs are `Indexed` objects wrapped in an opaque `Function`), so `vertices/bilinear.py` runs a separate pass: group by which bilinear appears, then hand the *coefficient* of each bilinear (a pure boson expression) to the same `Poly`-based extractor for its boson legs. Both tracks converge on the same Feynman-rule formula (below) and the same closed vertex catalog. ## 8.1 The bosonic extractor `extract_interaction_coefficients(L, fields)` (`vertices/extract.py:68`) tries a fast path first: `L.as_poly(*fields, domain='EX')`, iterating `poly.terms()` to read off each monomial's exponent vector and coefficient directly — `domain='EX'` (SymPy's "extended" domain) lets the polynomial coefficients themselves be arbitrary symbolic expressions (couplings, VEVs, $i$, momentum tags), not just rationals. If `as_poly` fails for any reason (certain expressions don't coerce cleanly), `_extract_fallback` walks `L.as_ordered_terms()` term by term, manually counting each field's exponent by inspecting `Mul`/`Pow` structure — slower, but robust to anything the fast path chokes on. Both paths produce the identical nested dict `{n_fields: {sorted_field_tuple: coefficient}}`. ### Derivation: the Feynman-rule formula For a Lagrangian monomial $\mathcal L \supset c\,\phi_1^{k_1}\phi_2^{k_2} \cdots\phi_n^{k_n}$, the Feynman rule is $i$ times the fully-differentiated, zero-field-evaluated coefficient: $$ i\left.\frac{\partial^{k_1+\cdots+k_n}\mathcal L}{\partial\phi_1^{k_1}\cdots\partial\phi_n^{k_n}}\right|_{\phi=0}. $$ Differentiating a single power $\phi^k$ exactly $k$ times with respect to $\phi$ brings down $k(k-1)\cdots1 = k!$ and leaves a constant (no more $\phi$-dependence, so setting $\phi=0$ afterward changes nothing); any *cross* term (differentiating a monomial with respect to a field it doesn't contain enough powers of) either still has surviving field dependence or differentiates to zero, and vanishes at $\phi=0$ either way. So the whole expression collapses to $c\cdot\prod_f k_f!$, i.e. $$ \text{vertex} \;=\; i\times c\times\prod_f (\text{multiplicity of }f)! . $$ `vertex_multiplicity(field_tuple)` (`extract.py:118`) computes $\prod_f k_f!$ directly from a `collections.Counter` of the field tuple, and `feynman_rule(coefficient, field_tuple)` (`extract.py:131`) combines it with the $i\times c$ prefactor. The CONVENTIONS.md-pinned sanity check is $\mathcal L = -\lambda\phi^4/4!$: here $c=-\lambda/4!$, $k=4$, so the vertex is $i\cdot(-\lambda/4!)\cdot4! = -i\lambda$ — the familiar textbook $\phi^4$ rule, recovered mechanically rather than hand-derived per model. ## 8.2 Momentum-space substitution Any monomial that still contains `PartialMu` after `expand_derivatives` ({doc}`lagrangian`) gets the final substitution `∂_μφ → i p(φ) φ` via `to_momentum_space`, called by `Model.interactions` whenever the physical Lagrangian `has(PartialMu)`. The **sign** of this rule ($+i$, not $-i$) is a deliberate convention choice, not a universal QFT fact — it corresponds to momentum flowing *with* the field into the vertex under an $e^{+ip\cdot x}$ plane-wave convention (matching the DLRSM1 reference this library was built from), the opposite of the more common $\partial_\mu\phi\to-ip_\mu\phi$ convention some textbooks use for all-incoming momenta with $e^{-ip\cdot x}$. The overall consistency of this choice — that it reproduces correct, textbook-matching vertices once carried through every derivative coupling in the model — is pinned by the VSS test cited below, not asserted on its own. ## 8.3 The fermion bilinear track `extract_fermion_vertices(L, boson_fields)` (`vertices/bilinear.py`) first Leibniz/bilinear-expands `L`, then for every term in `L.as_ordered_terms()`: counts the `Bilinear` factors, divides them out (`sp.cancel(term / bil)`) to get a pure-boson coefficient, and accumulates coefficients under the bilinear key. Each accumulated coefficient is then handed to the *same* `extract_interaction_coefficients` used for bosons. `fermion_feynman_rule(coefficient, gamma, boson_tuple)` applies the identical $i\times c\times\prod n!$ formula, carrying the Dirac structure $\Gamma$ along as an explicit factor of the returned rule (it's not itself part of the combinatorial counting — a `Bilinear` node counts as exactly one leg regardless of $\Gamma$'s internal gamma-matrix content). A term with **one** `Bilinear` is the FFS/FFV (Yukawa / gauge-current) case, keyed by the flat `(bar, gamma, field)`. A term with **two** is the four-fermion (FFFF) case (§8.3.1). ### 8.3.1 Four-fermion (FFFF) operators Dimension-6 effective operators like the Fermi muon-decay operator $-(4G_F/\sqrt2)(\bar\nu_\mu\gamma^\mu P_L\mu)(\bar e\,\gamma_\mu P_L\nu_e)$ are the two-`Bilinear` case of the same track. The grouping key becomes a **nested** canonically-sorted pair `((bar,Γ,field), (bar',Γ′,field'))`, so consumers tell FFFF from FFS/FFV by `isinstance(key[0], tuple)`. Two design commitments (both pinned in `tests/test_four_fermion.py`): 1. **As-written bilinear basis** — feynlag does *not* Fierz-rearrange; a Fierz-equivalent rewriting is a *different input* (the same convention SMEFT UFO models use). The two Dirac structures $(\Gamma,\Gamma')$ are carried **separately**, never multiplied into one product — folding them would wrongly invoke the Clifford algebra *across* the two independent spinor chains (e.g. contract a shared $\gamma^\mu$). `four_fermion_feynman_rule` therefore returns the plain scalar $i\,c\,\prod n!$, with the chains riding on the key / `Vertex.meta`. 2. **Four distinct fermion components only.** If any component repeats among the four legs — including a squared bilinear $B^2$, which `atoms()` would deduplicate, so `_bilinear_factors` counts multiplicity by walking the factors — extraction raises `NotImplementedError`. This is not an arbitrary limit: identical legs generate cross-chain Wick contractions whose relative signs require genuine spinor-index Fierz algebra that the opaque-`Bilinear` representation (a fully-contracted c-number) cannot express. With four distinct legs there are no exchange contractions, so no extra Wick/symmetry factor is needed at all. A vector-current contraction $(\bar\psi\gamma^\mu P_L\psi)(\bar\chi\gamma_\mu P_L\chi)$ is written with a **shared index symbol** — `DiracGamma(mu)` on one leg, `DiracGammaLower(mu)` on the other — the shared $\mu$ *is* the contraction (same convention as `fermion_gauge_current`). The admissibility of these operators under the mass-dimension check is governed by the `max_dim` flag ({doc}`invariance`); the UFO Lorentz strings and the four-fermion vertex adder are in {doc}`export`. Worked model: `examples/fermi_theory.py`. ## 8.4 The closed vertex catalog Every extracted `(field_tuple, coefficient)` pair is classified by `classify_spins` (`vertices/vertex.py:33`) into one of exactly ten catalog entries — `SSS, SSSS, VSS, VVS, VVSS, VVV, VVVV, FFS, FFV, FFFF` — based purely on the spins of the legs (looked up in a `{symbol: spin}` map built by `Model.spin_map`). Any spin combination outside this list **raises** `ValueError` rather than returning a best-guess classification: the catalog is deliberately closed (R$_\xi$ ghosts and other combinations remain out of scope, {doc}`pipeline`), and silently mis-tagging an out-of-catalog vertex would be worse than refusing to extract it. `FFFF` is the one entry the sorted spin letters can't fully reconstruct — which fermion pairs into which Dirac chain is not visible from four `F`s — so a caller building a `Vertex` for it records the chain pairing in `Vertex.meta`. Each catalog entry also carries the UFO Lorentz-structure name(s) it maps to (`LORENTZ_CATALOG`), consumed directly by {doc}`export`. ## 8.5 Yang–Mills self-couplings Rather than expanding $-\tfrac14F^a_{\mu\nu}F^{a\mu\nu}$ symbolically with explicit Lorentz indices (which the rest of the pipeline deliberately avoids, {doc}`lagrangian`), the pure-gauge cubic and quartic self-couplings are computed **group-theoretically**, since their Lorentz structures are universal (fixed once the spin content is VVV or VVVV) and only the group-theory tensor multiplying them varies model to model. ### Derivation: cubic coupling With $F^a_{\mu\nu} = \partial_\mu A^a_\nu - \partial_\nu A^a_\mu + gf^{abc}A^b_\mu A^c_\nu$, the cubic self-interaction in $-\tfrac14F^aF^a$ comes from the cross term between the abelian piece and the non-abelian piece: $$ -\tfrac14\cdot2\cdot(\partial_\mu A^a_\nu-\partial_\nu A^a_\mu)\bigl(gf^{abc}A^{b\mu}A^{c\nu}\bigr) \;=\; -g f^{abc}(\partial_\mu A^a_\nu)A^{b\mu}A^{c\nu}, $$ the group-index tensor being exactly $-g\,f^{abc}$. `cubic_couplings` (`vertices/yangmills.py:70`) computes $f^{abc}$ from the fundamental representation (`structure_constants`, $f^{abc}=-2i\,\mathrm{Tr}([T^a,T^b]T^c)$, {doc}`declaration`) and, for a possibly-rotated physical basis $A^a = \sum_iU_{ai}V_i$ (e.g. $W^1,W^2,W^3,B\to W^\pm,Z,\gamma$), contracts $F_{ijk} = \sum_{abc}f^{abc}U_{ai}U_{bj}U_{ck}$ — the rotated tensor — returning `{(V_i,V_j,V_k): -g·F_ijk}` for every nonzero triple. ### Derivation: quartic coupling The quartic self-interaction comes from squaring the non-abelian piece alone: $$ -\tfrac14\bigl(gf^{abc}A^b_\mu A^c_\nu\bigr)\bigl(gf^{ade}A^{d\mu}A^{e\nu}\bigr) \;=\; -\frac{g^2}{4}\sum_a f^{abc}f^{ade}\,A^b_\mu A^c_\nu A^{d\mu}A^{e\nu}, $$ summed (implicitly, repeated index) over the adjoint index $a$ that pairs the two field-strength factors. `quartic_couplings` (`vertices/yangmills.py:105`) builds the rotated pairwise tensor $F^e_{ij} = \sum_{ab}f^{abe}U_{ai}U_{bj}$ and then $E_{(ij)(kl)} = \sum_eF^e_{ij}F^e_{kl}$, matching the sum over $a$ above with $e$ playing the role of the contracted adjoint index, returning `{(V_i,V_j,V_k,V_l): -g²/4·Σ_e F^e_ij F^e_kl}` for that *ordered* quadruple. ### Derivation: the VVVV → 3 UFO Lorentz structures (`vvvv.py`) UFO expresses a 4-vector self-coupling in three metric contractions, `VVVV1/2/3` (`export/ufo/lorentz_map.py`, $g^{14}g^{23}-g^{13}g^{24}$ and cyclic). A single ordered quadruple $(i,j,k,l)$ from `quartic_couplings` corresponds to one specific pairing of the four legs; the other two pairings come from the orderings $(i,k,j,l)$ and $(i,l,j,k)$ — the three ways to partition 4 objects into 2 unordered pairs. `assemble_vvvv` (`export/ufo/vvvv.py`) reads off exactly these three raw values: ```python VVVV1 = -4 * quartic_raw[(i, j, k, l)] VVVV2 = -4 * quartic_raw[(i, k, j, l)] VVVV3 = -4 * quartic_raw[(i, l, j, k)] ``` **The three structures are not independent**: $VVVV2 = VVVV1 + VVVV3$, so they span a 2-dimensional space and a vertex's decomposition is a 1-parameter family. Writing the vertex in the metric-pair basis $V = c_{12}M_{12}M_{34} + c_{13}M_{13}M_{24} + c_{14}M_{14}M_{23}$, the Yang–Mills quartic always obeys $c_{12}+c_{13}+c_{14}=0$, and the symmetric representative $\left(\tfrac{c_{14}-c_{13}}{3},\tfrac{c_{14}-c_{12}}{3}, \tfrac{c_{13}-c_{12}}{3}\right)$ reconstructs it exactly. The $-4$ is that $/3$; this module shipped with $-12$ and therefore reconstructed **3× the true vertex**. That survived because the "independent" ground truth was circular: it built its reference in the *same* over-complete convention and compared coefficient lists, which cannot see a common factor. `tests/test_yangmills.py` now works in the convention-free metric-pair basis and asserts that $\sum_s c_s \times \text{structure}_s$ reproduces the direct functional differentiation of $-\tfrac14F^aF^a$ — including in the **physical** electroweak basis reached through the complex $W^\pm$ rotation, where the four quartics are pinned against MadGraph's stock `sm` model (`TestMadGraphOracle`). `assemble_vvvv` returns Lagrangian-level coefficients; pass `feynman_rule=True` for the UFO coupling $i\times$ that. ### The field → particle leg sign (`export.ufo.legs`) feynlag's symbols label **fields**; a UFO leg labels a **particle**. The field $W^+$ annihilates a $W^+$ but *creates* a $W^-$, so the leg carrying the symbol `Wp` is UFO's `W-` leg. Emitting legs under their naive names therefore transposes each conjugate pair, and since `VVV1` is totally antisymmetric: - one conjugate pair ($\gamma W^+W^-$, $W^+W^-Z$) $\Rightarrow$ **−1**; - none ($ggg$) $\Rightarrow$ **+1**. That is the whole content of what was long recorded as an unresolved "cubic sign convention". It also explains why VVS/VVSS/VVVV never needed a flip: their structures are *invariant* under that transposition — checked by the writer's `_assert_relabelling_invariant`, which raises rather than assume it. **`VSS1` does get $-1$** when its two scalars are a conjugate pair, and nothing applied that until `export/ufo/legs.py` existed — so every exported Feynman-gauge `V S S` was wrong by a sign. The sign is applied by the **writer**, not here: it is a UFO convention, and the writer is the only layer that knows the particle/antiparticle pairing (`UFOParticle.antisymbol`). A `Vertex` coupling is therefore always in feynlag's own convention, and `Model.gauge_vertices` takes no `conjugates=`. Note `cubic_couplings`'s raw output also **contains the colour factor** ($-g_s f^{abc}$), the cubic twin of the quartic's colour-double-count above. The gluon export is correct only because it uses the $f^{123}=1$ triple. ### Colour-stripping for an unbroken group (`adjoint_vvv`/`adjoint_vvvv`) `quartic_couplings` works in the weak basis, so its entry for a component quadruple, $-g^2/4\sum_e f^{ije}f^{kle}$, **already contains the colour contraction**. An unbroken group's UFO vertex is ONE particle repeated four times with the adjoint index carried by a colour tensor, so feeding those component-specific values to the writer multiplies by colour twice. `adjoint_vvvv(group)` divides it back out, giving $ig^2$ on every structure — MadGraph's `GC_12` for $gggg$. Use it, together with `ADJOINT_VVVV_COLORS`, for any unbroken non-abelian group. **The cubic has the identical problem**: `cubic_couplings` returns $-g\,f^{abc}$, colour factor included. `adjoint_vvv(group)` strips it, giving $-g$ on every triple independent of $N$ (verified for SU(2), SU(3) and SU(4) against every non-zero structure constant) — MadGraph's `GC_10` for $ggg$ — to be emitted with `ADJOINT_VVV_COLOR = "f(1,2,3)"`. This hid far longer than the quartic version because the only triple ever exported was $(G_1,G_2,G_3)$, and $f^{123}=1$ makes the colour factor unity, so the emitted number was accidentally right. Note `adjoint_vvv` takes **no** `feynman_rule` flag while `adjoint_vvvv` does: the cubic carries one derivative, so the $i$ from $\partial_\mu\to ip_\mu$ cancels the Feynman-rule $i$ and `cubic_couplings`' output is already the vertex coefficient (real in a real basis — hence $ggg=-g_s$). The quartic has no derivative, so its $i$ survives. `Model.gauge_vertices` **refuses** a group no rotation touches, for exactly this reason: its "physical" basis is the weak-basis adjoint components, so every coupling would carry the group factor. Use the two helpers above, or call `cubic_couplings`/`quartic_couplings` directly for the weak-basis tensor (internal verification only). ## Design gotchas - **Never differentiate through `Bilinear`** — the extraction functions above only ever divide it out algebraically (`term / bil`) or match it structurally; see {doc}`invariance` for why differentiating through it is unsafe. - **One or two `Bilinear`s per term.** One is FFS/FFV; two is a four-fermion operator (§8.3.1) — restricted to four *distinct* fermion components, with a repeated leg (or three-plus bilinears) raising `NotImplementedError` rather than being silently mishandled. - **`Model.interactions(..., min_legs=3)`** drops any extracted monomial with fewer than 3 legs by default — 1- and 2-point "interactions" are tadpole/mass terms, already handled in {doc}`ssb`/{doc}`masses`, and including them in a vertex table would be a spurious duplicate. ## Validation - `tests/test_extract.py::test_pinned_symmetry_factor_phi4`, `::test_pinned_symmetry_factor_phi3`, `::test_round_trip_toy_potential` — the Feynman-rule formula, §8.1. - `tests/test_gauge_sector_sm.py::test_ZhG0_vss_structure` — the momentum convention, §8.2. - `tests/test_fermion_sector.py::TestGaugeCurrents` — the bilinear track, §8.3. - `tests/test_four_fermion.py` — the four-fermion (FFFF) track, §8.3.1 (Fermi-operator extraction/rule, the distinct-legs guard, and the `FFFF*` UFO round-trip). - `tests/test_gauge_sector_sm.py::test_vertex_objects_classified` — the closed catalog, §8.4. - `tests/test_qcd.py::test_ggg_coupling_pinned`, `::test_gggg_coupling_pinned`, `tests/test_gauge_sector_sm.py::test_cubic_gauge_couplings`, `::test_quartic_gauge_couplings` — §8.5's cubic/quartic tensors, pinned numerically (`ggg = -g_s`). - `tests/test_yangmills.py::TestVVVVAssemblyGroundTruth` — the independent functional-differentiation derivation of the VVVV assembly. ## Minimal snippet ```python rules = model.feynman_rules([h, W1, W2]) # {field_tuple: i * coeff * n!} vertices = model.vertices([h, W1, W2]) # typed Vertex objects ```