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 (7. Diagonalization and the Physical Basis), 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.Symbols, 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:
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.
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
(3. Writing the 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):
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_ruletherefore returns the plain scalar \(i\,c\,\prod n!\), with the chains riding on the key /Vertex.meta.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_factorscounts multiplicity by walking the factors — extraction raisesNotImplementedError. 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-Bilinearrepresentation (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 (4. Checking Invariance);
the UFO Lorentz strings and the four-fermion vertex adder are in
9. 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, 1. The 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 Fs — 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 9. 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, 3. Writing the 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:
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)\),
2. Declaration: Parameters, Fields, Groups) 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:
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:
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 4. Checking Invariance for why differentiating through it is unsafe.One or two
Bilinears 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) raisingNotImplementedErrorrather 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 5. Spontaneous Symmetry Breaking/6. Mass Matrices, 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 theFFFF*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¶
rules = model.feynman_rules([h, W1, W2]) # {field_tuple: i * coeff * n!}
vertices = model.vertices([h, W1, W2]) # typed Vertex objects