11. Suggesting Invariant Terms

Note

This is an exploration-branch feature (explore/model-building-tools), sitting before stage 2 (3. Writing the Lagrangian) in the pipeline: instead of hand-writing every Lagrangian term and relying on 4. Checking Invariance to catch mistakes, the tool enumerates the terms a given field content and symmetry group actually admit.

Physics statement

Given the declared fields and symmetries, only finitely many operators up to mass dimension 4 are gauge- and discrete-invariant — that finite set is the most general renormalizable Lagrangian the model can have. Writing it out by hand is error-prone (a missed operator is a missed coupling; a wrong one fails invariance). The term-suggestion tool (feynlag.suggest) enumerates that set directly, turning model-building into “declare fields → review the suggested operator basis → attach couplings”. The engine is feynlag’s own, already-tested invariance machinery (4. Checking Invariance) used as an oracle: candidates are constructed from invariant building blocks, then every emitted term is re-verified against the full check_* battery.

Algorithm

suggest_potential / suggest_yukawa share one pipeline:

  1. Building blocks. Assemble the smallest non-abelian-gauge-invariant pieces, each tagged with its U(1) charge(s) and mass dimension. For scalars: singlet components and their conjugates (dim 1); doublet inner products \(A^\dagger B\) (dim 2, charge \(q_B-q_A\)); SU(2) \(\varepsilon\)- contractions \(A^T(i\sigma_2)B\) (dim 2, charge \(q_A+q_B\)). For Yukawas: the fermion bilinear \(\bar F_L\Gamma F_R\) sandwiched with a scalar leg (direct doublet contraction, the \(\tilde H\) pattern, or none for a bare mass), with any SU(3) color indices summed diagonally (the qcolor pattern of examples/sm_scalar_gauge.py).

  2. Monomials. Take multisets of blocks with total dimension in the window (2–4 for the potential), keeping only those that are exactly U(1)-neutral (exact rational/symbolic charge arithmetic — symbolic charges like U(1)_X’s a·Y+b·(B−L) are handled).

  3. Discrete projection. Reynolds-project each monomial onto the discrete-invariant subspace (below); drop the ones that project to zero.

  4. Hermitian completion. If a candidate isn’t already hermitian, emit m + m† and flag hc_added.

  5. Basis reduction. Expand every candidate into component monomials and keep only those that increase the exact rank of the coefficient matrix — a minimal linearly independent basis, matching literature operator counts.

  6. Oracle. Assert every survivor passes check_gauge_invariance, check_discrete_invariance, check_mass_dimension and check_hermiticity (4. Checking Invariance) — the house rule that no result is trusted on construction alone.

Derivation: the Reynolds (group-averaging) projector

For a non-abelian discrete symmetry like \(S_3\), individual monomials are not invariant — e.g. \(H_1^\dagger H_1\) maps into a combination of \(H_1^\dagger H_1\) and \(H_2^\dagger H_2\) under the group. Only specific linear combinations are invariant, so enumerate-and-filter (which works for rephasing \(Z_N\) symmetries, where monomials are charge eigenstates) fails.

The fix is the classical invariant-theory tool. For a finite group \(G\) acting linearly on the space of monomials, define the Reynolds operator

\[ P(m) \;=\; \frac{1}{|G|}\sum_{g\in G} g\cdot m . \]

\(P\) is a projector onto the invariant subspace: it is idempotent (\(P^2 = P\), since averaging an already-averaged — hence invariant — expression leaves it unchanged) and its image is exactly the \(G\)-invariants (for invariant \(m\), every \(g\cdot m = m\) so \(P(m)=m\); conversely any \(P(m)\) is invariant because left-multiplying the sum by a fixed \(g'\) just permutes the group elements). So \(P(m)\) is either \(0\) (the monomial has no invariant component) or a genuine invariant combination — precisely the \((H_1^\dagger H_1 + H_2^\dagger H_2)/2\)-type combinations that no single monomial realizes. reynolds_project (suggest.py) builds this by enumerating the group as words in its generators (_group_elements, telling elements apart by their action in the faithful direct sum of irreps) and applying each element letter by letter through the existing per-generator substitution maps — sequential substitution is composition, so no new group-action code is needed beyond what 4. Checking Invariance already has. For a \(Z_N\)-charged monomial, \(P\) collapses to the familiar “total charge \(\equiv 0 \pmod N\)” filter automatically (the average is \(m\) if neutral, \(0\) if charged).

Design gotchas

  • SymPy caches structurally-equal expressions to one object, so two value-equal candidates (e.g. the \(\lambda_5\) operator \((H_1^\dagger H_2)^2\) and \((H_2^\dagger H_1)^2\), identical after h.c. completion) share an id and cannot be told apart by identity. The basis reducer therefore returns and filters by positional index, never id().

  • conjugate(φ) is not an independent polynomial variable to SymPy, so the rank reducer substitutes each conjugate(...) node — and each opaque Bilinear atom — by a stable shared Dummy before extracting coefficient vectors (the same trick charged_mass_matrix uses, 6. Mass Matrices).

  • Reynolds over multiple discrete groups composes the per-group projectors; this lands on the joint-invariant subspace only when the group actions commute (true for independently-declared symmetries). The oracle re-verifies regardless, so a pathological case fails loudly rather than silently emitting a non-invariant term.

  • DiracFermion is unsupported (as everywhere in feynlag — model a vector-like fermion as two WeylFermions, 2. Declaration: Parameters, Fields, Groups); suggest_yukawa pairs opposite-chirality Weyls.

Validation

tests/test_suggest.py pins the enumeration against the hand-built example models via a span-equality helper (both operator sets expand to component-monomial coefficient matrices with equal exact row rank, plus the dual random-point numeric philosophy of 10. Verification Philosophy):

  • SM potential spans \(\{H^\dagger H,\ (H^\dagger H)^2\}\).

  • 2HDM with an exact \(Z_2\) → the 7-operator basis; the soft \(m_{12}^2\) term is correctly absent (and present when no \(Z_2\) is declared).

  • 3HDM+\(S_3\) → the 10-operator basis, spanning examples/thdm_s3.py’s CG-built potential — the Reynolds ground-truth test.

  • U(1)_X singlet → the portal basis, no bare (X-charged) \(S\)/\(S^2\)/\(S^3\).

  • SM Yukawa (e via \(H\), d via \(H\), u via \(\tilde H\), with the SU(3) color sum); VLL bare Dirac mass + mixing Yukawa (examples/sm_vll.py).

  • a parametrized oracle battery: every suggestion passes the full check_* set.

Minimal snippet

from feynlag import suggest_potential, suggest_yukawa, build_lagrangian

pot = suggest_potential([H1, H2, HS], [SU2L, U1Y], discrete_groups=[s3])
yuk = suggest_yukawa([QL, uR, dR], [H], [SU2L, U1Y, SU3c])
for t in pot:
    print(t)                       # e.g. SuggestedTerm((H1†H1) [potential, dim 2])

L, couplings = build_lagrangian(pot + yuk)   # fresh c1, c2, ... attached