Source code for feynlag.export.ufo.writer

"""UFO model directory writer.

v1 scope (see plan): unitary-gauge tree-level UFO with the closed vertex
catalog; no form factors, NLO, ghosts or propagators.py. Colored (SU(3))
vertices emit real color-tensor strings (T/f) — see add_vvv_vertex /
add_vvvv_vertex / add_fermion_vertex; the SSS/SSSS/VSS/VVS/VVSS catalog
(add_bosonic_vertex) remains color-singlet only. Coupling orders use a
simple heuristic (``QED`` power = #legs − 2) — refine per-model if a
generator needs the exact hierarchy.

The bosonic vertices come straight from :meth:`Model.vertices`.  Fermion
vertices must be flavor-resolved by the caller (each entry names concrete
particles) since UFO particles are per flavor eigenstate.
"""

import datetime
import keyword
import re
from dataclasses import dataclass
from pathlib import Path

import sympy as sp

from ...operators import momentum
from .legs import (charged_goldstone_phase, structure_leg_sign,
                   ufo_leg_sign)
from .lorentz_map import UFO_LORENTZ, structures_for
from .vvvv import metric_pair_coefficients, permute_vvvv
from .pycode import ufo_expr
from .static import FUNCTION_LIBRARY, INIT_TEMPLATE, OBJECT_LIBRARY

__all__ = ["UFOParticle", "write_ufo"]


[docs] @dataclass class UFOParticle: """Specification of one UFO particle. Attributes: symbol: physical SymPy symbol used in the vertex tables. pdg: PDG code. name / antiname: UFO names (equal → self-conjugate). spin: UFO ``2s+1`` (1 scalar, 2 fermion, 3 vector). mass / width: parameter names, or ``'ZERO'``. color: 1 (singlet), 3, 8. charge: electric charge. antisymbol: the symbol used for the conjugate in vertex tables (e.g. the ``Gm`` partner of ``Gp``); None if self-conjugate. goldstone: mark Goldstone bosons. texname / antitexname: LaTeX (defaults to names). """ symbol: object pdg: int name: str antiname: str = None spin: int = 1 mass: str = "ZERO" width: str = "ZERO" color: int = 1 charge: float = 0 antisymbol: object = None goldstone: bool = False texname: str = None antitexname: str = None def __post_init__(self): if self.antiname is None: self.antiname = self.name if self.texname is None: self.texname = self.name if self.antitexname is None: self.antitexname = self.antiname @property def self_conjugate(self): return self.name == self.antiname
def _pyname(name): """Valid python identifier for a particle name (FeynRules-style).""" replacements = [("+", "__plus__"), ("-", "__minus__"), ("~", "__tilde__"), ("@", "__at__"), ("!", "__exclam__"), ("?", "__quest__"), ("*", "__star__")] for orig, sub in replacements: name = name.replace(orig, sub) name = re.sub(r"\W", "_", name) if keyword.iskeyword(name) or name[0].isdigit(): name = "_" + name return name def _vss_split(coupling, scalars): """Split a VSS rule ``c·(p(a) − p(b))`` into (ordered scalars, c). Returns ``((a, b), c)`` such that the rule equals ``c*(p(a) − p(b))``. """ a, b = scalars pa, pb = momentum(a), momentum(b) coupling = sp.expand(coupling) c_a = sp.simplify(coupling.coeff(pa)) c_b = sp.simplify(coupling.coeff(pb)) if sp.simplify(c_a + c_b) != 0: raise ValueError(f"VSS coupling is not antisymmetric in momenta: " f"{coupling}") residual = sp.simplify(coupling - c_a * pa - c_b * pb) if residual != 0: raise ValueError(f"VSS coupling has non-momentum part: {coupling}") return (a, b), c_a #: colour tensors that carry a colour charge — anything built from the #: fundamental generators, structure constants or the symmetric d-symbol. #: ``'1'`` and ``Identity(i,j)`` are BOTH colour singlets: the latter is what #: UFO uses for a coloured fermion pair with a colourless boson (q qbar gamma), #: so treating every non-``'1'`` string as QCD would tag the whole electroweak #: quark sector QCD — the mirror image of the bug that tagged gluons QED. _COLOURED_TENSORS = ("T(", "f(", "d(") def _order_name(color): """``'QCD'`` if the colour tensor carries colour, else ``'QED'``.""" text = str(color).strip() return "QCD" if any(t in text for t in _COLOURED_TENSORS) else "QED" class _UFOBuilder: def __init__(self, model_name, parameters, particles): self.model_name = model_name self.parameters = parameters self.specs = {p.symbol: p for p in particles} for p in particles: if p.antisymbol is not None: self.specs[p.antisymbol] = p self.particles = particles self.couplings = {} # (value, order) -> (GC name, order) # (particles, lorentz names, couplings, color strings) — colors has # either 1 entry (broadcast to every lorentz/coupling slot, the # color-singlet default) or exactly len(lorentz names) entries (one # color tensor per independent structure, e.g. the 3-way f*f # decomposition of a 4-gluon vertex). self.vertex_entries = [] self.used_lorentz = set() # -------------------------------------------------------------- helpers def _particle_ref(self, symbol): spec = self.specs.get(symbol) if spec is None: raise KeyError(f"no UFOParticle registered for symbol {symbol}") if symbol == spec.antisymbol and not spec.self_conjugate: return f"P.{_pyname(spec.antiname)}" return f"P.{_pyname(spec.name)}" def _conjugate_symbol(self, symbol): """The symbol of this leg's antiparticle (itself if self-conjugate). The writer is the only layer that knows this pairing, which is why it owns the field->particle leg sign (see :mod:`.legs`). """ spec = self.specs.get(symbol) if spec is None: raise KeyError(f"no UFOParticle registered for symbol {symbol}") if spec.self_conjugate or spec.antisymbol is None: return symbol return spec.antisymbol if symbol == spec.symbol else spec.symbol def _conjugates(self, legs): return {leg: self._conjugate_symbol(leg) for leg in legs} def _emitted_charge(self, symbol): """Charge of the particle actually emitted on this leg (the antiparticle's, when the leg carries the antisymbol).""" spec = self.specs[symbol] if symbol == spec.antisymbol and not spec.self_conjugate: return -spec.charge return spec.charge def _goldstone_phase(self, legs): """Phase aligning feynlag's charged-Goldstone convention with UFO's (see :func:`.legs.charged_goldstone_phase`).""" charges = [self._emitted_charge(leg) for leg in legs if self.specs[leg].goldstone and self.specs[leg].charge != 0] return charged_goldstone_phase(charges) def _leg_sign(self, structure, legs): """Sign the emitted coupling picks up under the field->particle relabelling of ``legs`` (see :mod:`.legs`).""" return structure_leg_sign(structure, legs, self._conjugates(legs)) def _coupling(self, expr, n_legs, color="1"): """Register a coupling and return its ``GC_n`` name. The coupling ORDER matters to MadGraph: it selects diagrams by it and defines the perturbative expansion. A non-singlet colour tensor means the vertex is colour-charged, hence QCD; everything else is QED. The power ``max(n_legs - 2, 1)`` is right either way (ggg -> 1, gggg -> 2, qqg -> 1). Tagging the gluon vertices QED — which this did for every coupling — makes MadGraph refuse the model outright: ``CRITICAL: Model with non QCD emission of gluon``, after which it builds the wrong diagram set and any QCD result is meaningless. Found by ``scripts/madgraph_qcd.py``; nothing symbolic could see it. """ value = ufo_expr(sp.nsimplify(expr, rational=False) if expr.is_number else expr) order = {_order_name(color): max(n_legs - 2, 1)} # The order is part of the coupling's identity: two vertices sharing a # VALUE but differing in colour must not collapse onto one GC_n, or # whichever registered first would decide the order for both (a gluon # vertex silently inheriting QED is the failure this whole check # exists to prevent). key = (value, tuple(sorted(order.items()))) if key not in self.couplings: cname = f"GC_{len(self.couplings) + 1}" self.couplings[key] = (cname, order) return self.couplings[key][0] # -------------------------------------------------------------- vertices def add_bosonic_vertex(self, vertex): """Add a feynlag Vertex (SSS/SSSS/VSS/VVS/VVSS). This catalog is color-singlet only in v1 (Higgs/EW self-couplings after EWSB) — raises if a non-singlet (color-charged) particle is passed in, rather than silently emitting a wrong ``color=['1']`` for a vertex this adder doesn't actually know how to color-tensor (colored self-couplings go through :meth:`add_vvv_vertex` / :meth:`add_vvvv_vertex`, colored fermion currents through :meth:`add_fermion_vertex`). """ vtype = vertex.vertex_type particles = list(vertex.particles) coupling = vertex.coupling n = len(particles) for p in particles: if self.specs[p].color != 1: raise ValueError( f"add_bosonic_vertex is color-singlet only; {p} has " f"color {self.specs[p].color} — use add_vvv_vertex/" f"add_vvvv_vertex for colored self-couplings") if vtype in ("SSS", "SSSS", "VVS", "VVSS"): spins = {p: self.specs[p].spin for p in particles} ordered = sorted(particles, key=lambda p: -spins[p]) lorentz = structures_for(vtype)[0] cname = self._coupling( self._leg_sign(lorentz, ordered) * self._goldstone_phase(ordered) * coupling, n) elif vtype == "VSS": vector = [p for p in particles if self.specs[p].spin == 3] scalars = [p for p in particles if self.specs[p].spin == 1] if len(vector) != 1 or len(scalars) != 2: raise ValueError(f"bad VSS content {particles}") (a, b), c = _vss_split(coupling, scalars) ordered = [vector[0], a, b] lorentz = "VSS1" # A charged pair in legs 2,3 (e.g. A G+ G-) flips VSS1, which is # antisymmetric in them. Nothing applied this before, so every # exported Feynman-gauge VSS was wrong by a sign. cname = self._coupling(self._leg_sign(lorentz, ordered) * self._goldstone_phase(ordered) * c, n) else: raise ValueError(f"add_bosonic_vertex cannot handle {vtype}; " f"use the dedicated adders") self.used_lorentz.add(lorentz) self.vertex_entries.append( ([self._particle_ref(p) for p in ordered], [lorentz], [cname], ["1"])) def add_vvv_vertex(self, triple, coupling, color="1"): """Yang–Mills cubic vertex: ``coupling`` multiplies the VVV1 structure (one representative ordering per set of three bosons). Args: color: UFO color-tensor string (default ``'1'``, singlet — e.g. EW self-couplings after EWSB). An unbroken non-abelian self-coupling (e.g. ggg) is ONE physical particle repeated three times with ``color='f(1,2,3)'`` — never built by iterating the group's full weak-basis component dict (that dict is for internal verification only, see yangmills.py). """ self.used_lorentz.add("VVV1") # VVV1 is totally antisymmetric, so a conjugate pair among the legs # (A W+ W-, W+ W- Z) contributes -1 while ggg contributes +1 — the # whole of the old "cubic sign convention" puzzle (see .legs). cname = self._coupling( self._leg_sign("VVV1", list(triple)) * coupling, 3, color) self.vertex_entries.append( ([self._particle_ref(p) for p in triple], ["VVV1"], [cname], [color])) def _assert_relabelling_invariant(self, quadruple, structures): """A VVVV's structures must be INVARIANT under the field->particle relabelling, not merely pick up a sign. Unlike VVV1 the quartic structures are not totally antisymmetric, so there is no signature to apply — either the relabelling is a symmetry of this vertex's structures (it is, for all four electroweak quartics: the coefficients it would exchange are equal) or the vertex cannot be emitted under naive leg labels at all. Checked rather than assumed. """ conjugates = self._conjugates(quadruple) if all(conjugates[leg] == leg for leg in quadruple): return target = [conjugates[leg] for leg in quadruple] remaining = list(range(len(quadruple))) perm = [] for t in target: for i in remaining: if quadruple[i] == t: perm.append(i) remaining.remove(i) break else: raise ValueError( f"{tuple(quadruple)}: the field->particle relabelling is " f"not a permutation of this vertex's legs") moved = permute_vvvv(structures, tuple(perm)) # compare by simplification: `moved` has been through permute_vvvv's # sp.simplify while `structures` has only been expanded, so a # structurally-different-but-equal form (radicals from a mixing angle) # would otherwise raise on a perfectly invariant vertex. if any(sp.simplify(a - b) != 0 for a, b in zip(metric_pair_coefficients(moved), metric_pair_coefficients(structures))): raise NotImplementedError( f"quartic {tuple(quadruple)} is not invariant under the " f"field->particle relabelling {perm}; emitting it under " f"naive leg labels would be wrong and no signature can fix a " f"non-antisymmetric structure") def add_vvvv_vertex(self, quadruple, couplings, colors=None): """Quartic gauge vertex: dict ``{VVVV structure name: coupling}``. Args: colors: optional ``{VVVV structure name: color string}``, one color tensor per independent Lorentz structure (e.g. the three ``f*f`` color factors of a 4-gluon vertex, paired with VVVV1/2/3 respectively — see export/ufo/vvvv.py). Defaults to broadcasting the singlet ``'1'`` to every structure. """ self._assert_relabelling_invariant(quadruple, couplings) names, cnames, clist = [], [], [] for lname, coupling in couplings.items(): self.used_lorentz.add(lname) names.append(lname) ccolor = (colors or {}).get(lname, "1") cnames.append(self._coupling(coupling, 4, ccolor)) clist.append(ccolor) self.vertex_entries.append( ([self._particle_ref(p) for p in quadruple], names, cnames, clist)) def add_fermion_vertex(self, bar_symbol, field_symbol, bosons, left_coupling=0, right_coupling=0, color="1"): """Flavor-resolved FFS/FFV vertex. Args: bar_symbol / field_symbol: particle-spec symbols of the fermions (the ψ̄ leg first). bosons: tuple with the boson leg symbol(s) (length 1 in v1). left_coupling / right_coupling: coefficients of P_L / P_R (or γ^μ P_L / γ^μ P_R). color: UFO color-tensor string (default ``'1'``, singlet — e.g. a lepton current). A qqg vertex uses ``'T(3,2,1)'`` with this adder's ``[bar, field, boson]`` leg order: the gluon's adjoint index at leg 3, then **the fundamental (quark) index FIRST** — UFO reads ``T(a,i,j)`` with ``i`` the 3 and ``j`` the 3bar, so it is ``T^a_{field, bar}`` = legs ``(2,1)``, which is also what MadGraph's stock ``sm`` emits for ``[u~, u, g]``. This used to say ``'T(3,1,2)'``, reasoning from ``fermion_gauge_current``'s ``T[r,c]`` Lagrangian index order (row with the bar leg). That conflates the Lagrangian index order with UFO's leg convention and transposes the generator; since ``T^a`` is hermitian the transpose is the complex conjugate, which flips the sign of the ggg interference. ``u u~ > g g`` failed MadGraph's Lorentz and gauge/Ward checks because of it (``scripts/madgraph_qcd.py``) — the one place any of this is observable. """ if len(bosons) != 1: raise ValueError("v1 fermion vertices have exactly one boson leg") boson_spin = self.specs[bosons[0]].spin base = "FFV" if boson_spin == 3 else "FFS" ordered = [bar_symbol, field_symbol, bosons[0]] names, cnames = [], [] # ``left/right`` are Lagrangian coefficients; the UFO coupling value # carries the Feynman-rule ``i`` (as the bosonic/VVV couplings do), so # apply it here — its omission is invisible to a single vertex but # breaks FFV↔VVV interference (e.g. e+e- → W+W- gauge cancellation). for suffix, coupling in (("L", sp.I * left_coupling), ("R", sp.I * right_coupling)): if coupling == 0: continue lname = base + suffix self.used_lorentz.add(lname) names.append(lname) cnames.append(self._coupling( self._goldstone_phase(ordered) * coupling, 3, color)) if not names: return self.vertex_entries.append( ([self._particle_ref(p) for p in ordered], names, cnames, [color])) def add_four_fermion_vertex(self, bar1, field1, bar2, field2, couplings, color="1"): """Four-fermion (FFFF) contact vertex, two Dirac chains. Args: bar1, field1: the ψ̄/ψ of the first Dirac chain (UFO legs 1, 2). bar2, field2: the ψ̄/ψ of the second chain (UFO legs 3, 4). couplings: dict ``{(chain1, chain2): coefficient}`` where each ``chainN`` is one of ``'SL'``, ``'SR'`` (scalar bilinear, P_L/P_R), ``'VL'``, ``'VR'`` (vector current, γ^μP_L/γ^μP_R). Both chains must be the same *type* (both scalar or both vector — the contracted-Lorentz FFFF catalog covers S⊗S and V⊗V; a mixed S⊗V structure raises). The **Lagrangian** coefficient is passed; the UFO coupling value carries the Feynman-rule ``i`` (added here, exactly as :meth:`add_fermion_vertex` does — omitting it is invisible in a single |M|² but breaks interference). color: UFO color-tensor string (default ``'1'``, singlet). """ ordered = [bar1, field1, bar2, field2] names, cnames = [], [] for (c1, c2), coupling in couplings.items(): if coupling == 0: continue t1, p1 = c1[0], c1[1] t2, p2 = c2[0], c2[1] if t1 != t2: raise ValueError( f"four-fermion chains must be the same type; got {c1!r} " f"and {c2!r} (mixed scalar⊗vector is outside the v1 " f"FFFF catalog)") lname = f"FFFF{t1}{p1}{p2}" self.used_lorentz.add(lname) names.append(lname) cnames.append(self._coupling(sp.I * coupling, 4)) if not names: return self.vertex_entries.append( ([self._particle_ref(p) for p in ordered], names, cnames, [color])) # ----------------------------------------------------------------- write def write(self, path): path = Path(path) path.mkdir(parents=True, exist_ok=True) (path / "object_library.py").write_text(OBJECT_LIBRARY) (path / "function_library.py").write_text(FUNCTION_LIBRARY) (path / "__init__.py").write_text(INIT_TEMPLATE.format( author="feynlag", date=datetime.date.today().isoformat(), version="0.1")) (path / "coupling_orders.py").write_text(self._coupling_orders_py()) (path / "parameters.py").write_text(self._parameters_py()) (path / "particles.py").write_text(self._particles_py()) (path / "lorentz.py").write_text(self._lorentz_py()) (path / "couplings.py").write_text(self._couplings_py()) (path / "vertices.py").write_text(self._vertices_py()) return path def _coupling_orders_py(self): return ( "from object_library import all_orders, CouplingOrder\n\n" "QCD = CouplingOrder(name='QCD', expansion_order=99, " "hierarchy=1)\n" "QED = CouplingOrder(name='QED', expansion_order=99, " "hierarchy=2)\n") def _parameters_py(self): lines = ["# This file was automatically created by feynlag", "import cmath", "from object_library import all_parameters, Parameter", "from function_library import (complexconjugate, re, im, " "csc, sec, acsc, asec, cot)", "", ""] lines.append("ZERO = Parameter(name='ZERO', nature='internal', " "type='real', value='0.0', texname='0')") lines.append("") for idx, p in enumerate(self.parameters.externals, start=1): if p.value is None: raise ValueError(f"external parameter {p.name} needs a " f"numeric value for UFO export") value = float(sp.sympify(p.value)) lines.append( f"{p.name} = Parameter(name='{p.name}', nature='external', " f"type='real', value={value!r}, texname='{p.tex}', " f"lhablock='FEYNLAG', lhacode=[{idx}])") lines.append("") for p in self.parameters.dependency_order(): value = ufo_expr(p.expr) lines.append( f"{p.name} = Parameter(name='{p.name}', nature='internal', " f"type='complex', value={value!r}, texname='{p.tex}')") lines.append("") return "\n".join(lines) def _particles_py(self): lines = ["# This file was automatically created by feynlag", "from object_library import all_particles, Particle", "import parameters as Param", "", ""] for spec in self.particles: mass = "Param.ZERO" if spec.mass == "ZERO" else f"Param.{spec.mass}" width = ("Param.ZERO" if spec.width == "ZERO" else f"Param.{spec.width}") lines.append( f"{_pyname(spec.name)} = Particle(pdg_code={spec.pdg}, " f"name='{spec.name}', antiname='{spec.antiname}', " f"spin={spec.spin}, color={spec.color}, mass={mass}, " f"width={width}, texname='{spec.texname}', " f"antitexname='{spec.antitexname}', charge={spec.charge}, " f"goldstoneboson={spec.goldstone})") if not spec.self_conjugate: lines.append(f"{_pyname(spec.antiname)} = " f"{_pyname(spec.name)}.anti()") lines.append("") return "\n".join(lines) def _lorentz_py(self): lines = ["# This file was automatically created by feynlag", "from object_library import all_lorentz, Lorentz", "", ""] for name in sorted(self.used_lorentz): spins, structure = UFO_LORENTZ[name] lines.append(f"{name} = Lorentz(name='{name}', spins={spins}, " f"structure='{structure}')") lines.append("") return "\n".join(lines) def _couplings_py(self): lines = ["# This file was automatically created by feynlag", "import cmath", "from object_library import all_couplings, Coupling", "from function_library import (complexconjugate, re, im, " "csc, sec, acsc, asec, cot)", "", ""] for (value, _), (cname, order) in self.couplings.items(): lines.append(f"{cname} = Coupling(name='{cname}', " f"value={value!r}, order={order})") lines.append("") return "\n".join(lines) def _vertices_py(self): lines = ["# This file was automatically created by feynlag", "from object_library import all_vertices, Vertex", "import particles as P", "import couplings as C", "import lorentz as L", "", ""] for n, (parts, lnames, cnames, colors) in enumerate( self.vertex_entries, start=1): lorentz = ", ".join(f"L.{ln}" for ln in lnames) if len(colors) == 1: # single shared color structure — every lorentz/coupling # slot pairs with color-tensor index 0 (UFO's (0,i) key). couplings = ", ".join(f"(0,{i}):C.{cn}" for i, cn in enumerate(cnames)) elif len(colors) == len(cnames): # one color tensor per lorentz structure (e.g. the 3-way # f*f decomposition of a 4-gluon vertex) — paired diagonally. couplings = ", ".join(f"({i},{i}):C.{cn}" for i, cn in enumerate(cnames)) else: raise ValueError( f"vertex V_{n}: {len(colors)} color structures for " f"{len(cnames)} couplings — must be 1 (broadcast) or " f"match exactly") color = "[" + ", ".join(f"'{c}'" for c in colors) + "]" lines.append( f"V_{n} = Vertex(name='V_{n}',\n" f" particles=[{', '.join(parts)}],\n" f" color={color},\n" f" lorentz=[{lorentz}],\n" f" couplings={{{couplings}}})") lines.append("") return "\n".join(lines)
[docs] def write_ufo(path, model_name, parameters, particles, bosonic_vertices=(), vvv=None, vvvv=None, fermion_vertices=(), four_fermion_vertices=(), vvv_colors=None, vvvv_colors=None): """Write a UFO model directory. Args: path: output directory. model_name: UFO model name. parameters: :class:`~feynlag.parameters.ParameterSet` (externals need numeric values; internals need defined ``expr``). particles: iterable of :class:`UFOParticle`. bosonic_vertices: feynlag ``Vertex`` objects (SSS/SSSS/VSS/VVS/VVSS) — color-singlet only (see :meth:`_UFOBuilder.add_bosonic_vertex`). vvv: dict ``{(V1,V2,V3): coupling}`` (one representative ordering per boson triple), e.g. from ``cubic_couplings`` for an EWSB-rotated, color-singlet physical basis (Wp/Wm/Z/A-style). An unbroken non-abelian self-coupling (e.g. ggg) is instead ONE physical particle repeated three times, still expressible as a single-entry ``{(g,g,g): coupling}`` dict — pair it with ``vvv_colors``. vvvv: dict ``{(V1,V2,V3,V4): {lorentz-name: coupling}}``. vvv_colors / vvvv_colors: optional ``{key: color-string}`` / ``{key: {lorentz-name: color-string}}`` matching ``vvv``/ ``vvvv``'s keys, for non-singlet (color-tensor) structures — defaults to broadcasting the singlet ``'1'`` when a key is absent (see :meth:`_UFOBuilder.add_vvv_vertex`/ :meth:`_UFOBuilder.add_vvvv_vertex`). fermion_vertices: iterable of dicts with keys ``bar``, ``field``, ``bosons``, ``left``, ``right`` (flavor-resolved symbols), and optionally ``color`` (default ``'1'``; e.g. ``'T(3,2,1)'`` for a qqg vertex). four_fermion_vertices: iterable of dicts with keys ``bar1``, ``field1``, ``bar2``, ``field2`` (the two Dirac chains' legs), ``couplings`` (``{(chain1, chain2): coefficient}``, see :meth:`_UFOBuilder.add_four_fermion_vertex`), and optionally ``color`` (default ``'1'``). Returns: the written path. """ builder = _UFOBuilder(model_name, parameters, list(particles)) for v in bosonic_vertices: builder.add_bosonic_vertex(v) for triple, coupling in (vvv or {}).items(): builder.add_vvv_vertex(triple, coupling, color=(vvv_colors or {}).get(triple, "1")) for quad, couplings in (vvvv or {}).items(): builder.add_vvvv_vertex(quad, couplings, colors=(vvvv_colors or {}).get(quad)) for fv in fermion_vertices: builder.add_fermion_vertex(fv["bar"], fv["field"], fv["bosons"], fv.get("left", 0), fv.get("right", 0), color=fv.get("color", "1")) for ffv in four_fermion_vertices: builder.add_four_fermion_vertex( ffv["bar1"], ffv["field1"], ffv["bar2"], ffv["field2"], ffv["couplings"], color=ffv.get("color", "1")) return builder.write(path)