{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Model building with `feynlag`: `suggest` + anomaly cancellation\n", "\n", "The other tutorials (`SM_Feynman_Rules_Tutorial`, `SM_VLL_Tutorial`,\n", "`SM_U1X_Tutorial`) take a Lagrangian you have *already written by hand* and push it\n", "through the analysis pipeline. This notebook goes one step earlier — it shows the\n", "**model-building** tools that help you *write* a consistent Lagrangian in the first\n", "place:\n", "\n", "- `feynlag.suggest` — enumerate every gauge- and discrete-invariant operator your field\n", " content admits, up to a mass dimension, so you review a basis instead of hand-deriving\n", " invariants;\n", "- `feynlag.anomalies` — compute the triangle-anomaly coefficients symbolically, turning\n", " \"is this chiral gauge theory even consistent?\" into a set of polynomial conditions on\n", " the charges.\n", "\n", "We build a small **dark sector**: a gauged $U(1)_D$ dark force with a complex dark Higgs\n", "$S$ and a dark fermion $\\chi$. Along the way the tools will *tell us* which charge\n", "assignment is consistent, which operators are allowed, and catch a deliberate mistake." ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "## 1. Why enumerate?\n", "\n", "The FeynRules-style workflow inverts model building: you write down every term you think\n", "is allowed, then hope the invariance checker flags whatever you got wrong or forgot. For\n", "anything past the SM that is error-prone — a missing $(S^\\dagger S)^2$ quartic, a Yukawa\n", "whose charges don't actually close, a bare mass that a symmetry forbids.\n", "\n", "`feynlag.suggest` turns it around: **declare the fields and symmetries, and let the\n", "library enumerate the invariant operator basis.** The engine is feynlag's own,\n", "already-tested invariance machinery used as an oracle — candidates are built from\n", "gauge-invariant blocks, filtered for $U(1)$ neutrality, projected onto the\n", "discrete-invariant subspace, reduced to a linearly independent basis, and then every\n", "emitted term is re-verified against the full `check_*` battery." ] }, { "cell_type": "code", "execution_count": null, "id": "2", "metadata": {}, "outputs": [], "source": [ "import sympy as sp\n", "sp.init_printing()\n", "\n", "from feynlag import (\n", " ExternalParameter, InternalParameter, Lagrangian, Model, Scalar, U1,\n", " WeylFermion, dag, Dmu, diracPR, diracPL, DiracGamma, Bilinear,\n", " extract_fermion_vertices, fermion_feynman_rule, fermion_gauge_current,\n", " fermion_mass_matrix, check_anomaly_free, anomaly_coefficients,\n", " suggest_potential, suggest_yukawa, suggest_kinetic, build_lagrangian,\n", ")" ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "## 2. Fields with *symbolic* charges\n", "\n", "We gauge a single abelian factor $U(1)_D$ with coupling $g_D$. The dark Higgs $S$ carries\n", "charge $q_S$ and the dark fermion is a pair of Weyl fields $\\chi_L,\\chi_R$ carrying charge\n", "$q_\\chi$. We keep **both charges symbolic** — every pipeline stage (generators, invariance\n", "checking, gauge currents, mass matrices) handles symbol-valued $U(1)$ charges; the one\n", "exception is UFO export, whose particle table needs numeric charges.\n", "\n", "Declaring $\\chi_L$ and $\\chi_R$ with the **same** charge $q_\\chi$ makes the dark fermion\n", "*vector-like* (a Dirac fermion). Whether that is forced or a choice is exactly what the\n", "anomaly analysis in §3 will tell us." ] }, { "cell_type": "code", "execution_count": null, "id": "4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "chiL charge: q_chi\n", "chiR charge: q_chi (same -> vector-like Dirac fermion)\n", "S charge: q_S\n" ] } ], "source": [ "gD = ExternalParameter(\"gD\", 0.5, positive=True)\n", "U1D = U1(\"U1D\", coupling=gD)\n", "\n", "q_chi = sp.Symbol(\"q_chi\", real=True) # dark-fermion charge (symbolic)\n", "q_S = sp.Symbol(\"q_S\", real=True) # dark-Higgs charge (symbolic)\n", "\n", "chiL = WeylFermion(\"chiL\", reps={U1D: q_chi}, chirality=\"L\",\n", " component_names=[\"chiL\"])\n", "chiR = WeylFermion(\"chiR\", reps={U1D: q_chi}, chirality=\"R\",\n", " component_names=[\"chiR\"])\n", "S = Scalar(\"S\", reps={U1D: q_S}, component_names=[\"S0\"])\n", "\n", "print(\"chiL charge:\", chiL.reps[U1D])\n", "print(\"chiR charge:\", chiR.reps[U1D], \" (same -> vector-like Dirac fermion)\")\n", "print(\"S charge:\", S.reps[U1D])" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "## 3. Anomaly cancellation constrains the charges\n", "\n", "A chiral $U(1)$ gauge theory is only consistent if its triangle anomalies vanish. With a\n", "single abelian factor the relevant coefficients are the cubic $[U(1)_D]^3$ and the mixed\n", "gravitational–gauge $\\mathrm{grav}^2\\text{-}U(1)_D$. `anomaly_coefficients` reduces every\n", "fermion to a left-handed Weyl field (a right-handed field contributes with its charge\n", "sign flipped) and returns the coefficients as **polynomials in the charge symbols**.\n", "\n", "For our vector-like assignment ($q_{\\chi_L}=q_{\\chi_R}=q_\\chi$) the right-handed\n", "contribution exactly cancels the left-handed one:" ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'[U1D][U1D][U1D]': 0, 'grav^2-U1D': 0}" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model_vec = Model(\"darkU1_vec\", gauge_groups=[U1D], fields=[chiL, chiR])\n", "coeffs_vec = anomaly_coefficients(model_vec)\n", "coeffs_vec" ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "AnomalyReport(2 coefficients, anomaly-free)\n", "nonzero coefficients: none — anomaly-free for ANY q_chi\n" ] } ], "source": [ "report_vec = check_anomaly_free(model_vec)\n", "print(report_vec)\n", "print(\"nonzero coefficients:\", report_vec.nonzero or \"none — anomaly-free for ANY q_chi\")" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "Now suppose we had tried a **chiral** assignment instead — $\\chi_R$ with a *different*\n", "charge $q_R$. The anomaly coefficients no longer cancel automatically; they become\n", "conditions that pin $q_R$:" ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'[U1D][U1D][U1D]': -q_R**3 + q_chi**3, 'grav^2-U1D': -q_R + q_chi}" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "q_R = sp.Symbol(\"q_R\", real=True)\n", "chiR_chiral = WeylFermion(\"chiRx\", reps={U1D: q_R}, chirality=\"R\",\n", " component_names=[\"chiRx\"])\n", "model_chiral = Model(\"darkU1_chiral\", gauge_groups=[U1D],\n", " fields=[chiL, chiR_chiral])\n", "coeffs_chiral = anomaly_coefficients(model_chiral)\n", "coeffs_chiral" ] }, { "cell_type": "code", "execution_count": null, "id": "10", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "anomaly-free requires q_R = q_chi\n" ] } ], "source": [ "solution = sp.solve([coeffs_chiral[\"[U1D][U1D][U1D]\"],\n", " coeffs_chiral[\"grav^2-U1D\"]], [q_R], dict=True)\n", "print(\"anomaly-free requires q_R =\", solution[0][q_R])" ] }, { "cell_type": "markdown", "id": "11", "metadata": {}, "source": [ "So for a *single* dark fermion pair, anomaly freedom **forces** $q_R = q_\\chi$ — the\n", "fermion must be vector-like. (A genuinely chiral dark $U(1)$ needs extra fermions arranged\n", "so the cubes and the linear charges each sum to zero, exactly as the SM hypercharges do.)\n", "We take the minimal consistent option: a vector-like $\\chi$." ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "## 4. Enumerate the invariant operators\n", "\n", "With a consistent charge assignment in hand, let `suggest` write the Lagrangian's operator\n", "content.\n", "\n", "**Scalar potential.** Only $S^\\dagger S$ is a neutral quadratic (a bare $S$, $S^2$, $S^3$\n", "all carry net $U(1)_D$ charge), so the potential is the familiar Mexican-hat pair:" ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " dim 2: " ] }, { "data": { "text/latex": [ "$\\displaystyle S_{0} \\overline{S_{0}}$" ], "text/plain": [ "SuggestedTerm(S0 S0† [potential, dim 2])" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " dim 4: " ] }, { "data": { "text/latex": [ "$\\displaystyle S_{0}^{2} \\overline{S_{0}}^{2}$" ], "text/plain": [ "SuggestedTerm(S0 S0 S0† S0† [potential, dim 4])" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "[SuggestedTerm(S0 S0† [potential, dim 2]),\n", " SuggestedTerm(S0 S0 S0† S0† [potential, dim 4])]" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "potential = suggest_potential([S], [U1D])\n", "for t in potential:\n", " print(f\" dim {t.dim}: \", end=\"\")\n", " display(t)\n", "potential" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "**Fermion mass / Yukawa.** Because $\\chi$ is vector-like, the gauge-invariant operator is\n", "a **bare Dirac mass** $\\bar\\chi_L\\chi_R$ (dimension 3) — the dark Higgs cannot give $\\chi$\n", "a Yukawa mass here, since $\\bar\\chi_L S\\,\\chi_R$ would need $q_S = q_{\\chi_L}-q_{\\chi_R}=0$.\n", "`suggest_yukawa` returns exactly that bare mass, with its $+\\,\\text{h.c.}$ completed:" ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " dim 3: " ] }, { "data": { "text/latex": [ "$\\displaystyle \\bar{{\\bar{chiL}}_{i}}\\,P_R\\,{chiR}_{j} + \\bar{{\\bar{chiR}}_{j}}\\,P_L\\,{chiL}_{i}$" ], "text/plain": [ "SuggestedTerm(chiL̄·chiR + h.c. [yukawa, dim 3])" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "yukawa = suggest_yukawa([chiL, chiR], [S], [U1D])\n", "for t in yukawa:\n", " print(f\" dim {t.dim}: \", end=\"\")\n", " display(t)" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "**The tool catches a mistake.** Suppose we fat-fingered $\\chi_R$'s charge to\n", "$q_\\chi + 1$. Now *no* gauge-invariant mass term exists at all — the enumeration comes\n", "back **empty**, telling us the dark fermion would be massless and the model is incomplete:" ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "suggested mass terms for the mistuned charge: [] \n", "-> empty: no invariant mass, the model as declared is inconsistent\n" ] } ], "source": [ "chiR_bad = WeylFermion(\"chiRb\", reps={U1D: q_chi + 1}, chirality=\"R\",\n", " component_names=[\"chiRb\"])\n", "yukawa_bad = suggest_yukawa([chiL, chiR_bad], [S], [U1D])\n", "print(\"suggested mass terms for the mistuned charge:\", yukawa_bad,\n", " \"\\n-> empty: no invariant mass, the model as declared is inconsistent\")" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "## 5. Attach couplings with `build_lagrangian`\n", "\n", "`build_lagrangian` takes the suggested basis, attaches a fresh coupling to each operator\n", "(potential/Yukawa terms enter as $-c\\cdot\\mathcal{O}$, matching feynlag's\n", "$\\mathcal{L}\\supset -V$ convention; generated couplings carry the right mass dimension\n", "$4-\\dim$), and returns a filled `Lagrangian`. `suggest_kinetic` adds the canonical\n", "kinetic scaffolding — $(D_\\mu S)^\\dagger(D^\\mu S)$ and the $\\chi$ gauge currents." ] }, { "cell_type": "code", "execution_count": null, "id": "19", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "generated couplings: ['c1', 'c2', 'c3'] (dims: [2, 0, 1] )\n", "kinetic scaffolding terms: ['(DμS)†(DμS)', 'chiL gauge current', 'chiR gauge current']\n" ] } ], "source": [ "kinetic = suggest_kinetic([S, chiL, chiR])\n", "L_auto, gen_params = build_lagrangian(potential + yukawa + kinetic)\n", "print(\"generated couplings:\", [p.name for p in gen_params],\n", " \" (dims:\", [p.unit_dim for p in gen_params], \")\")\n", "print(\"kinetic scaffolding terms:\", [t.label for t in kinetic])" ] }, { "cell_type": "markdown", "id": "20", "metadata": {}, "source": [ "That `L_auto` already forms a consistent model. For the *physical* pipeline below, though,\n", "we adopt feynlag's standard parametrization (as in every `examples/` model): the\n", "$S^\\dagger S$ mass coefficient is an **`InternalParameter`** fixed by the tadpole\n", "condition, rather than a free external. The operator *content* is identical to what\n", "`suggest` enumerated — we are only choosing which parameter the vacuum equation solves for." ] }, { "cell_type": "markdown", "id": "21", "metadata": {}, "source": [ "## 6. Run the pipeline\n", "\n", "We build the physical model (charges kept symbolic), break $U(1)_D$ with $\\langle\n", "S\\rangle = v_D/\\sqrt2$, solve the tadpole, and read off the dark-photon mass, the $\\chi$\n", "Dirac mass, and the $Z_D\\chi\\chi$ gauge coupling." ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [], "source": [ "vD = ExternalParameter(\"vD\", 1000.0, positive=True, unit_dim=1)\n", "lamS = ExternalParameter(\"lamS\", 0.2, positive=True)\n", "muS2 = InternalParameter(\"muS2\", unit_dim=2) # fixed by the tadpole\n", "Mchi = ExternalParameter(\"Mchi\", 300.0, positive=True, unit_dim=1)\n", "\n", "Sp = Scalar(\"S\", reps={U1D: q_S}, component_names=[\"S0\"])\n", "Sp.expand_vev({Sp.components[0]: vD}) # S0 -> (vD + S0_r + i S0_i)/sqrt(2)\n", "cL = WeylFermion(\"chiL\", reps={U1D: q_chi}, chirality=\"L\", component_names=[\"chiL\"])\n", "cR = WeylFermion(\"chiR\", reps={U1D: q_chi}, chirality=\"R\", component_names=[\"chiR\"])\n", "Xb = U1D.bosons(\"X\") # the dark photon field\n", "\n", "SdS = (dag(Sp) * Sp.mat)[0]\n", "V = -muS2.s * SdS + lamS.s * SdS**2\n", "DS = Dmu(Sp)\n", "\n", "i, j = sp.symbols(\"i j\", integer=True)\n", "clc, crc = cL.components[0], cR.components[0]\n", "clb, crb = cL.bar_components[0], cR.bar_components[0]\n", "# bare Dirac mass -M (chibar_L chi_R + h.c.) -- the operator suggest_yukawa found\n", "Lmass = -(Mchi.s * Bilinear(clb[i], diracPR, crc[i])\n", " + Mchi.s * Bilinear(crb[i], diracPL, clc[i]))\n", "current = fermion_gauge_current(cL, i) + fermion_gauge_current(cR, i)\n", "\n", "L = Lagrangian()\n", "L.add((dag(DS) * DS)[0], sector=\"kinetic\")\n", "L.add(-V, sector=\"potential\")\n", "L.add(Lmass, sector=\"yukawa\")\n", "L.add(current, sector=\"yukawa\")\n", "\n", "model = Model(\"darkU1\", gauge_groups=[U1D], fields=[Sp, cL, cR, Xb],\n", " parameters=[gD, vD, lamS, muS2, Mchi], lagrangian=L)" ] }, { "cell_type": "code", "execution_count": null, "id": "23", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "InvarianceReport(11 checks, OK)\n", "tadpole solution: {muS2: lamS*vD**2}\n" ] } ], "source": [ "report = model.check_invariance()\n", "print(report)\n", "report.raise_on_failure()\n", "print(\"tadpole solution:\", model.solve_tadpoles([muS2]))" ] }, { "cell_type": "markdown", "id": "24", "metadata": {}, "source": [ "**Dark-photon mass.** The dark photon eats the CP-odd part of $S$ and picks up a mass\n", "$m_{Z_D}^2 = g_D^2\\,q_S^2\\,v_D^2$ — note the explicit $q_S$ dependence, carried symbolically\n", "straight through the mass matrix:" ] }, { "cell_type": "code", "execution_count": null, "id": "25", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "m_ZD^2 = gD**2*q_S**2*vD**2\n" ] } ], "source": [ "X0 = Xb.components[0]\n", "M_ZD2 = sp.simplify(model.gauge_mass_matrix([X0])[0, 0])\n", "print(\"m_ZD^2 =\", M_ZD2)" ] }, { "cell_type": "markdown", "id": "26", "metadata": {}, "source": [ "**Dark-fermion mass.** The bare Dirac term gives $\\chi$ the mass $M_\\chi$ directly:" ] }, { "cell_type": "code", "execution_count": null, "id": "27", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "chi Dirac mass matrix: Matrix([[Mchi]])\n" ] } ], "source": [ "M_chi = fermion_mass_matrix(Lmass, clb, crc, model.vacuum, 1, (i, j), gamma=diracPR)\n", "print(\"chi Dirac mass matrix:\", M_chi)" ] }, { "cell_type": "markdown", "id": "28", "metadata": {}, "source": [ "**Dark gauge coupling.** Extracting the fermion vertices, the $Z_D\\chi\\chi$ coupling is\n", "the expected vector current $i\\,g_D\\,q_\\chi\\,\\gamma^\\mu$ (same on both chiralities — $\\chi$\n", "is vector-like), with the symbolic charge intact:" ] }, { "cell_type": "code", "execution_count": null, "id": "29", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Z_D chiL chiL : I*gD*q_chi*gamma(mu)*PL\n", "Z_D chiR chiR : I*gD*q_chi*gamma(mu)*PR\n" ] } ], "source": [ "Lg = sp.expand(model.physical_lagrangian(sector=\"yukawa\"))\n", "table = extract_fermion_vertices(Lg, [X0])\n", "mu = sp.Symbol(\"mu\", integer=True)\n", "gL, gR = DiracGamma(mu) * diracPL, DiracGamma(mu) * diracPR\n", "\n", "cLcoup = table.get((clb[i], gL, clc[i]), {}).get(1, {}).get((X0,), 0)\n", "cRcoup = table.get((crb[i], gR, crc[i]), {}).get(1, {}).get((X0,), 0)\n", "print(\"Z_D chiL chiL :\", sp.simplify(fermion_feynman_rule(cLcoup, gL, (X0,))))\n", "print(\"Z_D chiR chiR :\", sp.simplify(fermion_feynman_rule(cRcoup, gR, (X0,))))" ] }, { "cell_type": "markdown", "id": "30", "metadata": {}, "source": [ "## 7. Recap\n", "\n", "Starting from nothing but *field declarations with symbolic charges*, the model-building\n", "tools:\n", "\n", "| Step | Tool | What it gave us |\n", "|------|------|-----------------|\n", "| Consistency | `anomaly_coefficients` / `check_anomaly_free` | anomaly-free ⟺ $\\chi$ vector-like ($q_R=q_\\chi$) |\n", "| Operator basis | `suggest_potential`, `suggest_yukawa` | $\\{S^\\dagger S,\\,(S^\\dagger S)^2\\}$ and the bare Dirac mass — and an *empty* result flagging a mistuned charge |\n", "| Assembly | `build_lagrangian`, `suggest_kinetic` | a filled, coupling-tagged `Lagrangian` |\n", "| Physics | the standard pipeline | $m_{Z_D}^2=g_D^2q_S^2v_D^2$, $m_\\chi=M_\\chi$, $Z_D\\chi\\chi = i g_D q_\\chi\\gamma^\\mu$ |\n", "\n", "Every suggested operator is re-verified against the full invariance battery\n", "(`verify=True` by default), so the basis you review is guaranteed consistent.\n", "\n", "**Where to go next:** `SM_Feynman_Rules_Tutorial` walks the full analysis pipeline for the\n", "Standard Model; `SM_VLL_Tutorial` covers vector-like fermions and mass-basis rotations;\n", "`SM_U1X_Tutorial` extends the SM by a $U(1)_X$ with tree-level $Z$–$Z'$ mixing. Each hand-\n", "writes its Lagrangian — the `suggest` tools here can bootstrap that first step for a new\n", "model of your own." ] } ], "metadata": { "kernelspec": { "display_name": "Python (lagrangian)", "language": "python", "name": "lagrangian" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.13" } }, "nbformat": 4, "nbformat_minor": 5 }