6. Mass Matrices¶
Physics statement¶
Once the vacuum is fixed (5. Spontaneous Symmetry Breaking), the physical mass spectrum is read off the second derivatives of the potential (scalars) or the kinetic sector (gauge bosons) at that vacuum — the Hessian of \(V\) is, by definition, the mass-squared matrix of the fluctuation fields. Fermion masses come from a different place: the coefficient of the bilinear mass term in the Yukawa/mass sector, evaluated at the vacuum.
Real scalar fluctuations¶
scalar_mass_matrix(potential, vacuum, fields, tadpole_subs=None)
(vacuum/masses.py:38) is the direct implementation:
\(M^2_{ij} = \partial^2V/\partial\phi_i\partial\phi_j\vert_{\rm vacuum}\),
computed with sp.derive_by_array applied twice
(build_mass_matrix, masses.py:20) over the vacuum-shifted potential,
then evaluated at the vacuum and tadpole-substituted (_at_vacuum_matrix,
masses.py:29 — factor each nonzero entry so downstream diagonalization
sees the simplest possible symbolic form). fields here are real
fluctuation symbols — a CP-even block, a CP-odd block, or any other real
sub-block the caller wants a Hessian for.
The Dummy-conjugate trick (complex/charged fields)¶
A charged scalar’s physical mass term comes from
\(M^2_{ij} = \partial^2V/\partial\bar\phi_i\partial\phi_j\) — differentiating
with respect to the conjugate field. SymPy has no notion of
differentiating with respect to conjugate(phi) directly (it treats
conjugate(phi) as a function of phi, not an independent variable, so
sp.diff against it either errors or returns something meaningless).
charged_mass_matrix (vacuum/masses.py:55) works around this with the
Dummy-conjugate trick, ported from the DLRSM1 reference:
Replace every
conjugate(field)in the potential with a fresh, unrelatedsp.Dummysymbol:dummies = {conjugate(f): Dummy(f"{f.name}_conj") for f in fields}.Differentiate
build_mass_matrixwith respect to the Dummies (rows) and the originalfields(columns) — now a completely ordinary independent-variable derivative, since aDummyhas no algebraic relationship tofas far as SymPy is concerned.Substitute the Dummies back to
conjugate(field)in the resulting matrix entries.Evaluate at the vacuum and apply tadpole substitutions as before.
The trick works because \(\partial^2V/\partial\bar\phi_i\partial\phi_j\) only
needs \(\bar\phi_i\) to behave as an independent variable from \(\phi_i\) for
the duration of the differentiation — which is exactly what “treat \(\phi\)
and \(\bar\phi\) as independent for holomorphic/antiholomorphic calculus”
means in the Wirtinger-derivative sense physicists use implicitly when they
write \(\partial V/\partial\phi^*\). Introducing a literal SymPy Dummy in
place of conjugate(phi) makes that independence explicit and mechanical
rather than relying on SymPy inferring it (which it does not).
Gauge boson masses¶
gauge_mass_matrix(kinetic, vacuum, gauge_components, tadpole_subs=None)
(vacuum/masses.py:74) starts from the kinetic sector rather than the
potential: \(\mathcal L_{\rm kin} = (D_\mu\phi)^\dagger(D^\mu\phi)\)
evaluated at the vacuum. At the vacuum every scalar is constant (all
fluctuations set to zero, VEVs are genuine constants), so every
PartialMu term vanishes identically — the algorithm applies
at_vac.replace(PartialMu, lambda arg: 0) directly rather than going
through the momentum-space machinery, since there is nothing left to carry
a momentum tag once the derivative terms are gone. What survives is purely
the \(-igT^aA^a_\mu\langle\phi\rangle\) piece of \(D_\mu\phi\), squared — a
pure quadratic form in the gauge boson components, whose Hessian
\(M^2_{ab} = \partial^2\mathcal L_{\rm kin,vac}/\partial A^a\partial A^b\)
is the gauge mass matrix, with no sign flip (the vector mass term
in the Lagrangian is conventionally written \(+\tfrac12 M^2_{ab}A^aA^b\),
matching how \((D_\mu\phi)^\dagger(D^\mu\phi)\) already carries the correct
overall sign for a kinetic-derived mass term, unlike the potential-derived
scalar case where \(\mathcal L \supset -V\) flips the sign relative to a
naive Hessian of \(\mathcal L\) itself).
Fermion mass matrices¶
Fermion masses live on the other track entirely (8. Extracting Vertices):
fermion_mass_matrix(L_fermionic, bar_base, field_base, vacuum, nflavors, indices, gamma=None) (vertices/bilinear.py:218) collects the coefficient
of Bilinear(bar_base[i], Γ, field_base[j]) in the vacuum-evaluated
fermionic Lagrangian (after expand_bilinear distributes any composite
legs), normalizes each term’s flavor indices to the canonical (i, j)
symbols, and builds the matrix as \(M_{ij} = -\text{coefficient}\) — the
minus sign because the Lagrangian mass term is conventionally written
\(\mathcal L \supset -\bar\psi M\chi\) (CONVENTIONS.md), the fermionic analog
of the potential’s \(\mathcal L \supset -V\) sign flip above.
Validation¶
tests/test_scalar_pipeline_sm.py::test_higgs_and_goldstone_masses— the SM real/charged scalar Hessians, \(m_h^2 = 2\lambda v^2\).tests/test_scalar_pipeline_thdm.py::test_cp_even_mass_matrix,::test_charged_mass_matrix_and_mHp— the Dummy-conjugate trick exercised on the 2HDM charged sector.tests/test_gauge_sector_sm.py::test_gauge_mass_matrix— the SM \(W\)/\(B\) mass matrix from the kinetic sector, \(m_W = gv/2\).tests/test_fermion_sector.py::TestYukawa— the fermion mass matrix from a Yukawa sector.
Minimal snippet¶
h = sp.Symbol("H0_r", real=True)
Mh2 = model.mass_matrix([h]) # [[2 * lam * v**2]]
MW2 = model.gauge_mass_matrix([W1, W2, W3, B])