{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Discrete symmetries: representations, characters, and the construction of invariants\n", "\n", "`SUN_Groups_Tutorial` does this for **continuous** groups — generators, the Lie\n", "algebra, irreps of any dimension. This is the **discrete** counterpart: the finite\n", "flavour symmetries ($\\mathbb{Z}_N$, $S_3$, $A_4$) that model builders impose to forbid\n", "terms and force relations between the ones that survive.\n", "\n", "The pedagogy is **compute step by step, then develop judgment**. Nothing below is typed in from the answer. The character table is\n", "*derived* from generator matrices; the number of invariants is *predicted* by\n", "orthogonality and then *checked* against explicit construction and against the library's\n", "own enumerator.\n", "\n", "Each section runs four moves — **set up** (look at the raw object), **collect** (let the\n", "structure emerge), **recognise** (name what appeared and why it had to), **check** (from\n", "group theory, not by re-running the same algebra) — and a blockquote asks you to commit\n", "to an expectation *before* the cell that settles it.\n", "\n", "**The one question this notebook is really about.** You impose a flavour symmetry on a\n", "model. How many free parameters does the potential have? Which terms are forbidden? You\n", "can answer both **before writing a single term**, and that is what the character table is\n", "for.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "1", "metadata": {}, "outputs": [], "source": [ "import itertools\n", "\n", "import sympy as sp\n", "from sympy.printing.pretty.stringpict import prettyForm\n", "sp.init_printing(use_latex=\"mathjax\") # text/latex only -- no PNG mimetype\n", "\n", "from feynlag import (S3, ZN, Scalar, SU2, U1, ExternalParameter, dag,\n", " check_discrete_invariance)\n", "from feynlag.groups.discrete import DiscreteSymmetry\n", "from feynlag.suggest import reynolds_project, suggest_potential\n", "\n", "\n", "def ok(msg):\n", " print(\"\\u2713\", msg)\n", "\n", "\n", "def trace(msg, obj=None):\n", " # One line of commentary, optionally followed by a sympy object. Every\n", " # helper below takes `verbose=False` and routes its narration through\n", " # here, so the tracing style is uniform and switched off by default --\n", " # the cells that assert stay quiet, and the \"peek\" cells do the talking.\n", " print(msg)\n", " if obj is not None:\n", " display(obj)\n", "\n", "\n", "class MapsTo(sp.Basic):\n", " # lhs |-> rhs, for display only: no equation semantics, so no stray \"=\".\n", " def __new__(cls, lhs, rhs):\n", " return super().__new__(cls, sp.sympify(lhs), sp.sympify(rhs))\n", "\n", " lhs = property(lambda self: self.args[0])\n", " rhs = property(lambda self: self.args[1])\n", "\n", " def _latex(self, printer):\n", " return rf\"{printer._print(self.lhs)} \\mapsto {printer._print(self.rhs)}\"\n", "\n", " def _sympystr(self, printer):\n", " return f\"{printer._print(self.lhs)} |-> {printer._print(self.rhs)}\"\n", "\n", " def _pretty(self, printer):\n", " left, right = printer._print(self.lhs), printer._print(self.rhs)\n", " return prettyForm(*left.right(\" \\u21a6 \", right))\n" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "---\n", "## 1. The generators *are* the group\n", "\n", "A finite group is handed to you as a couple of matrices and a promise: multiply them in\n", "every possible order and you get the whole group, nothing more and nothing less. For\n", "$S_3$ (the permutations of three objects) the library ships two generators in the\n", "**real orthogonal basis** — a $2\\pi/3$ rotation $a$ and a reflection $b$:\n", "\n", "$$\\rho_2(a)=\\begin{pmatrix}-\\tfrac12&-\\tfrac{\\sqrt3}{2}\\\\[2pt]\\tfrac{\\sqrt3}{2}&-\\tfrac12\\end{pmatrix},\n", "\\qquad\n", "\\rho_2(b)=\\begin{pmatrix}1&0\\\\0&-1\\end{pmatrix}.$$\n", "\n", "Write them down rather than importing them — you should see the matrices you are\n", "reasoning about — and then check the library agrees.\n", "\n", "> **Before running the next cells.** $S_3$ has 6 elements. You have 2 matrices. Ask what\n", "> relations they must satisfy for the closure to stop at 6 rather than running forever:\n", "> a rotation by $2\\pi/3$ has some order, a reflection has some order, and their product\n", "> has a third. Guess all three. Then ask the harder question — if you only *multiply*\n", "> generators, do you ever need inverses?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\rho_{2(a)} = \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ -√3 ⎤\n", " ⎢-1/2 ────⎥\n", " ⎢ 2 ⎥\n", "ρ₂₍ₐ₎ = ⎢ ⎥\n", " ⎢ √3 ⎥\n", " ⎢ ── -1/2⎥\n", " ⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\rho_{2(b)} = \\left[\\begin{matrix}1 & 0\\\\0 & -1\\end{matrix}\\right]$" ], "text/plain": [ " ⎡1 0 ⎤\n", "ρ_2(b) = ⎢ ⎥\n", " ⎣0 -1⎦" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ---- MOVE 1: set up. The three irreps of S3, as matrices. ----------------\n", "c, s = sp.Rational(-1, 2), sp.sqrt(3) / 2\n", "a2 = sp.Matrix([[c, -s], [s, c]]) # rho_2(a): rotation by 2 pi / 3\n", "b2 = sp.Matrix([[1, 0], [0, -1]]) # rho_2(b): reflection\n", "\n", "# Keys are ASCII stand-ins for the printed labels: \"1p\" is 1', \"1pp\" is 1''\n", "# (a prime is not a legal identifier character, and these are dict keys).\n", "S3_GENS = {\n", " \"1\": [sp.Matrix([[1]]), sp.Matrix([[1]])], # trivial: everything -> +1\n", " \"1p\": [sp.Matrix([[1]]), sp.Matrix([[-1]])], # sign rep: odd permutations -> -1\n", " \"2\": [a2, b2], # the doublet\n", "}\n", "display(sp.Eq(sp.Symbol(\"rho_2(a)\"), a2, evaluate=False))\n", "display(sp.Eq(sp.Symbol(\"rho_2(b)\"), b2, evaluate=False))\n" ] }, { "cell_type": "code", "execution_count": null, "id": "4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " a^3 = e -> identity\n", " b^2 = e -> identity\n", " (ab)^2 = e -> identity\n", "✓ a^3 = b^2 = (ab)^2 = e -- the presentation of S_3\n", "✓ every inverse is a positive power, so multiplication alone closes the group\n" ] } ], "source": [ "# ---- MOVE 2: the defining relations. --------------------------------------\n", "ID2 = sp.eye(2) # NB: `I2` would collide with section 4.1's invariant I_2\n", "rel = {\"a^3 = e\": sp.simplify(a2**3), \"b^2 = e\": sp.simplify(b2**2),\n", " \"(ab)^2 = e\": sp.simplify((a2 * b2)**2)}\n", "for name, M in rel.items():\n", " print(f\" {name:<12} ->\", \"identity\" if M == ID2 else \"NOT the identity\")\n", " assert M == ID2\n", "ok(\"a^3 = b^2 = (ab)^2 = e -- the presentation of S_3\")\n", "\n", "# because a^3 = e, a^{-1} = a^2 is itself a product of generators; likewise\n", "# b^{-1} = b. In a FINITE group every inverse is a positive power, so closing\n", "# under multiplication alone is enough -- no inverses needed.\n", "ok(\"every inverse is a positive power, so multiplication alone closes the group\")\n" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "Now close them. Multiply until nothing new appears — that *is* the group.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "words found: [(), (0,), (1,), (0, 0), (0, 1), (1, 0)]\n", "|S_3| = 6\n", "✓ closing two matrices gives exactly 6 elements -- the generators ARE the group\n" ] } ], "source": [ "# ---- MOVE 3: close the generators into the whole group. -------------------\n", "def close_group(gens, verbose=False):\n", " # All words in `gens`. A word is a tuple of generator indices, so every\n", " # irrep can later be evaluated on the SAME element. (feynlag does this\n", " # internally too, in suggest._group_elements.)\n", " n = gens[0].shape[0]\n", " key = lambda M: tuple(sp.nsimplify(sp.expand(x)) for x in M)\n", " name = lambda w: \"\".join(\"abcd\"[i] for i in w) or \"e\"\n", " seen = {key(sp.eye(n)): ()}\n", " frontier = [()]\n", " while frontier:\n", " if verbose:\n", " trace(f\"--- frontier: words of length {len(frontier[0]) + 1} ---\")\n", " nxt = []\n", " for word in frontier:\n", " for i in range(len(gens)):\n", " cand = word + (i,)\n", " M = sp.eye(n)\n", " for j in cand:\n", " M = sp.expand(M * gens[j])\n", " if key(M) not in seen:\n", " seen[key(M)] = cand\n", " nxt.append(cand)\n", " if verbose:\n", " trace(f\" {name(cand):<4} = {cand!s:<8} NEW\", M)\n", " elif verbose:\n", " trace(f\" {name(cand):<4} = {cand!s:<8} already seen, \"\n", " f\"as {name(seen[key(M)])}\")\n", " frontier = nxt\n", " return list(seen.values())\n", "\n", "\n", "WORDS = close_group(S3_GENS[\"2\"])\n", "print(\"words found:\", WORDS)\n", "print(\"|S_3| =\", len(WORDS))\n", "assert len(WORDS) == 6\n", "ok(\"closing two matrices gives exactly 6 elements -- the generators ARE the group\")" ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- frontier: words of length 1 ---\n", " a = (0,) NEW\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ -√3 ⎤\n", "⎢-1/2 ────⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢ √3 ⎥\n", "⎢ ── -1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " b = (1,) NEW\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}1 & 0\\\\0 & -1\\end{matrix}\\right]$" ], "text/plain": [ "⎡1 0 ⎤\n", "⎢ ⎥\n", "⎣0 -1⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- frontier: words of length 2 ---\n", " aa = (0, 0) NEW\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\\\\- \\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ √3 ⎤\n", "⎢-1/2 ── ⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢-√3 ⎥\n", "⎢──── -1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " ab = (0, 1) NEW\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ √3 ⎤\n", "⎢-1/2 ── ⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢ √3 ⎥\n", "⎢ ── 1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " ba = (1, 0) NEW\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\- \\frac{\\sqrt{3}}{2} & \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ -√3 ⎤\n", "⎢-1/2 ────⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢-√3 ⎥\n", "⎢──── 1/2 ⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " bb = (1, 1) already seen, as e\n", "--- frontier: words of length 3 ---\n", " aaa = (0, 0, 0) already seen, as e\n", " aab = (0, 0, 1) already seen, as ba\n", " aba = (0, 1, 0) already seen, as b\n", " abb = (0, 1, 1) already seen, as a\n", " baa = (1, 0, 0) already seen, as ab\n", " bab = (1, 0, 1) already seen, as aa\n" ] } ], "source": [ "# ---- peek: the same closure, narrating itself. ---------------------------\n", "# The call above stays quiet; this one shows the loop. Watch the LAST\n", "# frontier in particular -- that is where termination comes from.\n", "_ = close_group(S3_GENS[\"2\"], verbose=True)" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "Six elements from two matrices, and the closure terminated on its own. The trace shows\n", "*why* it terminated: every length-3 word reproduces an element already found, so the\n", "frontier empties and the loop stops without anyone telling it the order of the group. That\n", "is the whole content of \"a group is generated by\": you never store the group, you store the\n", "generators and regenerate it on demand.\n", "\n", "Recall: \n", " - a = $(123)$ (rotation)\n", " - b = $(23)$ (reflection)\n", "\n", "| Word | Meaning | Permutation |\n", "| --- | --- | --- |\n", "| ``()`` | Identity | $()$ |\n", "| ``(0,)`` | $a$ | $(123)$ |\n", "| ``(0,0)`` | $a^2$ | $(132)$ |\n", "| ``(1,)`` | $b$ | $(23)$ |\n", "| ``(0,1)`` | $ab$ | $(12)$ |\n", "| ``(1,0)`` | $ba$ | $(13)$ |\n", "\n", "**Mind the order.** `rho` multiplies a word left to right, so `(0,1)` is the matrix\n", "$ab$ — and a matrix acts on a column vector **rightmost factor first**. So $ab$ means\n", "\"$b$, then $a$\": $b$ sends $1\\to1$ and $a$ sends $1\\to2$, giving $ab=(12)$. Reading it\n", "the other way round gets you $(13)$, which is $ba$.\n", "\n", "**The judgment to keep:** checking invariance under the *generators* is checking\n", "invariance under the *whole group*. That is why `feynlag`'s\n", "`check_discrete_invariance` only ever loops over generators — and why it is cheap." ] }, { "cell_type": "markdown", "id": "9", "metadata": {}, "source": [ "---\n", "## 2. A representation is a homomorphism\n", "\n", "A representation $\\rho$ assigns a matrix to each group element such that **multiplication\n", "is preserved**:\n", "\n", "$$\\rho(g)\\,\\rho(h) = \\rho(gh).$$\n", "\n", "That single equation is the whole definition. Everything else — characters, invariants,\n", "selection rules — is a consequence.\n", "\n", "> **Before running the next cell.** We have three candidate representations of the same\n", "> group, of dimensions 1, 1, 2. Ask whether the *one-dimensional* ones can really be\n", "> called representations of $S_3$ — they map six elements onto just $\\pm1$, so they are\n", "> badly non-injective. Does the definition above require injectivity? And if not, what is\n", "> the trivial rep telling you?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "10", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ rho(g) rho(h) = rho(gh) for all 36 pairs, in all three irreps\n" ] } ], "source": [ "# ---- MOVE 1 + 2: does rho(g) rho(h) = rho(gh) hold, in every irrep? -------\n", "def rho(label, word, gens=S3_GENS):\n", " M = sp.eye(gens[label][0].shape[0])\n", " for i in word:\n", " M = M * gens[label][i]\n", " return sp.expand(M)\n", "\n", "\n", "def compose(w1, w2, gens=S3_GENS, verbose=False):\n", " # The word for g h, found by matching rho_2(g) rho_2(h) in the group.\n", " target = sp.expand(rho(\"2\", w1, gens) * rho(\"2\", w2, gens))\n", " if verbose:\n", " trace(f\"rho({w1}) =\", rho(\"2\", w1, gens))\n", " trace(f\"rho({w2}) =\", rho(\"2\", w2, gens))\n", " trace(f\"their product, rho({w1}) rho({w2}) =\", target)\n", " for w in WORDS:\n", " if sp.simplify(rho(\"2\", w, gens) - target) == sp.zeros(2, 2):\n", " if verbose:\n", " trace(f\"found in WORDS: that matrix is the element {w}, \"\n", " f\"so gh = {''.join('abcd'[k] for k in w) or 'e'}\")\n", " return w\n", " raise AssertionError(\"product left the group -- not closed!\")\n", "\n", "\n", "for label in S3_GENS:\n", " for w1, w2 in itertools.product(WORDS, repeat=2):\n", " lhs = sp.expand(rho(label, w1) * rho(label, w2))\n", " rhs = rho(label, compose(w1, w2))\n", " assert sp.simplify(lhs - rhs) == sp.zeros(*lhs.shape), (label, w1, w2)\n", "ok(\"rho(g) rho(h) = rho(gh) for all 36 pairs, in all three irreps\")" ] }, { "cell_type": "markdown", "id": "11", "metadata": {}, "source": [ "Nothing required $\\rho$ to be injective. The trivial rep sends every element to $1$ and\n", "is a perfectly good representation — it is the statement \"this object does not transform\n", "at all\", i.e. **it is an invariant**. Hunting for invariants will turn out to be hunting\n", "for copies of the trivial rep, which is what §4 does.\n", "\n", "The sign rep $1'$ is the other non-injective one: it remembers only whether a permutation\n", "is even or odd. It is not the trivial rep, so a $1'$ object is *not* invariant — it flips\n", "sign under any reflection. Forgetting that is a standard way to write down a \"invariant\"\n", "that isn't.\n" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "**One pair, worked slowly.** The assertion above swept 36 pairs past you at once. Take a\n", "single pair, because the convention hiding inside it is the one that trips people later:\n", "$w_1=(0,)$ — the generator $a$, a rotation by $120^\\circ$ — and $w_2=(1,)$, the reflection\n", "$b$. Rather than reading the arithmetic off a page, hand it to `compose` in verbose mode:\n", "it multiplies $\\rho(w_1)\\rho(w_2)$ and then searches `WORDS` for the element carrying that\n", "matrix." ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "rho((0,)) =\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ -√3 ⎤\n", "⎢-1/2 ────⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢ √3 ⎥\n", "⎢ ── -1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "rho((1,)) =\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}1 & 0\\\\0 & -1\\end{matrix}\\right]$" ], "text/plain": [ "⎡1 0 ⎤\n", "⎢ ⎥\n", "⎣0 -1⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "their product, rho((0,)) rho((1,)) =\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ √3 ⎤\n", "⎢-1/2 ── ⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢ √3 ⎥\n", "⎢ ── 1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "found in WORDS: that matrix is the element (0, 1), so gh = ab\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left( 0, \\ 1\\right)$" ], "text/plain": [ "(0, 1)" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ---- peek: one pair of the 36, showing its work --------------------------\n", "compose((0,), (1,), verbose=True)" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "**Compare.** The left-hand side is the product matrix; the right-hand side is\n", "$\\rho\\big((0,1)\\big)$, the same matrix reached by a different route. They agree, so\n", "$\\rho(g)\\rho(h)=\\rho(gh)$ holds on this pair — and the loop above simply does this 36\n", "times, in all three irreps.\n", "\n", "**The same statement in permutations.** With $a=(123)$ and $b=(23)$,\n", "\n", "$$ab=(123)\\circ(23)=(12),$$\n", "\n", "since the rightmost factor acts first: $b$ fixes $1$ and then $a$ sends $1\\to2$; $b$ sends\n", "$2\\to3$ and then $a$ sends $3\\to1$; $3\\to2\\to3$. So the cell has confirmed that the matrix\n", "of $(12)$ really is the product of the matrices of $(123)$ and $(23)$.\n", "\n", "> Order matters here and is easy to get backwards — $ba=(13)$, not $(12)$. Both rows are\n", "> in the table above." ] }, { "cell_type": "markdown", "id": "15", "metadata": {}, "source": [ "### 2.1 The same matrices, inside `feynlag`\n", "\n", "Before going further, tie the matrices above to what the library actually does. Assigning\n", "a multiplet to an irrep and asking for `generator_maps()` returns the substitution\n", "$\\phi_i \\to \\sum_k M[i,k]\\,\\phi_k$ that invariance checking applies.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "16", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ feynlag's generator_maps() is exactly phi_i -> sum_k M[i,k] phi_k, with the matrices written above\n", "\n", " e.g. under generator a:\n" ] }, { "data": { "text/latex": [ "$\\displaystyle H_{1,1} \\mapsto = - \\frac{H_{1 1}}{2} - \\frac{\\sqrt{3} H_{2 1}}{2}$" ], "text/plain": [ " H₁ ₁ √3⋅H₂ ₁\n", "H_{1,1} \\mapsto = - ──── - ───────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "SU2L = SU2(\"SU2L\", coupling=ExternalParameter(\"gw\", 0.6535, positive=True))\n", "U1Y = U1(\"U1Y\", coupling=ExternalParameter(\"g1\", 0.3580, positive=True))\n", "higgs = lambda name: Scalar(name, {SU2L: 2, U1Y: sp.Rational(1, 2)})\n", "\n", "H1, H2, HS = higgs(\"H1\"), higgs(\"H2\"), higgs(\"HS\")\n", "s3 = S3()\n", "s3.assign(\"2\", H1, H2) # (H1, H2) is an S3 doublet\n", "s3.assign(\"1\", HS) # HS is an S3 singlet\n", "\n", "maps = s3.generator_maps()\n", "comp = [list(H1.components), list(H2.components)]\n", "for gi, M in enumerate(S3_GENS[\"2\"]):\n", " for i in range(2):\n", " for j in range(len(comp[0])):\n", " want = sum(M[i, k] * comp[k][j] for k in range(2))\n", " assert sp.simplify(maps[gi][comp[i][j]] - want) == 0\n", "ok(\"feynlag's generator_maps() is exactly phi_i -> sum_k M[i,k] phi_k, \"\n", " \"with the matrices written above\")\n", "print(\"\\n e.g. under generator a:\")\n", "display(sp.Eq(sp.Symbol(r\"H_{1,1} \\mapsto\"),\n", " sp.simplify(maps[0][comp[0][0]]), evaluate=False))\n" ] }, { "cell_type": "markdown", "id": "17", "metadata": {}, "source": [ "Worth looking at the two objects directly before trusting the assertion above. `comp` is\n", "the component grid — row $i$ is the $SU(2)\\times U(1)$ components of the $i$-th member of\n", "the $S_3$ doublet — and `maps` is one substitution dictionary per generator, keyed by those\n", "same component symbols.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "18", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\left( \\left[ \\left[ H_{1 1}, \\ H_{1 2}\\right], \\ \\left[ H_{2 1}, \\ H_{2 2}\\right]\\right], \\ \\left[ \\left\\{ H_{1 1} : - \\frac{H_{1 1}}{2} - \\frac{\\sqrt{3} H_{2 1}}{2}, \\ H_{1 2} : - \\frac{H_{1 2}}{2} - \\frac{\\sqrt{3} H_{2 2}}{2}, \\ H_{2 1} : \\frac{\\sqrt{3} H_{1 1}}{2} - \\frac{H_{2 1}}{2}, \\ H_{2 2} : \\frac{\\sqrt{3} H_{1 2}}{2} - \\frac{H_{2 2}}{2}, \\ HS_{1} : HS_{1}, \\ HS_{2} : HS_{2}, \\ \\operatorname{PartialMu}{\\left(H_{1 1} \\right)} : - \\frac{\\operatorname{PartialMu}{\\left(H_{1 1} \\right)}}{2} - \\frac{\\sqrt{3} \\operatorname{PartialMu}{\\left(H_{2 1} \\right)}}{2}, \\ \\operatorname{PartialMu}{\\left(H_{1 2} \\right)} : - \\frac{\\operatorname{PartialMu}{\\left(H_{1 2} \\right)}}{2} - \\frac{\\sqrt{3} \\operatorname{PartialMu}{\\left(H_{2 2} \\right)}}{2}, \\ \\operatorname{PartialMu}{\\left(H_{2 1} \\right)} : \\frac{\\sqrt{3} \\operatorname{PartialMu}{\\left(H_{1 1} \\right)}}{2} - \\frac{\\operatorname{PartialMu}{\\left(H_{2 1} \\right)}}{2}, \\ \\operatorname{PartialMu}{\\left(H_{2 2} \\right)} : \\frac{\\sqrt{3} \\operatorname{PartialMu}{\\left(H_{1 2} \\right)}}{2} - \\frac{\\operatorname{PartialMu}{\\left(H_{2 2} \\right)}}{2}, \\ \\operatorname{PartialMu}{\\left(HS_{1} \\right)} : \\operatorname{PartialMu}{\\left(HS_{1} \\right)}, \\ \\operatorname{PartialMu}{\\left(HS_{2} \\right)} : \\operatorname{PartialMu}{\\left(HS_{2} \\right)}\\right\\}, \\ \\left\\{ H_{1 1} : H_{1 1}, \\ H_{1 2} : H_{1 2}, \\ H_{2 1} : - H_{2 1}, \\ H_{2 2} : - H_{2 2}, \\ HS_{1} : HS_{1}, \\ HS_{2} : HS_{2}, \\ \\operatorname{PartialMu}{\\left(H_{1 1} \\right)} : \\operatorname{PartialMu}{\\left(H_{1 1} \\right)}, \\ \\operatorname{PartialMu}{\\left(H_{1 2} \\right)} : \\operatorname{PartialMu}{\\left(H_{1 2} \\right)}, \\ \\operatorname{PartialMu}{\\left(H_{2 1} \\right)} : - \\operatorname{PartialMu}{\\left(H_{2 1} \\right)}, \\ \\operatorname{PartialMu}{\\left(H_{2 2} \\right)} : - \\operatorname{PartialMu}{\\left(H_{2 2} \\right)}, \\ \\operatorname{PartialMu}{\\left(HS_{1} \\right)} : \\operatorname{PartialMu}{\\left(HS_{1} \\right)}, \\ \\operatorname{PartialMu}{\\left(HS_{2} \\right)} : \\operatorname{PartialMu}{\\left(HS_{2} \\right)}\\right\\}\\right]\\right)$" ], "text/plain": [ "⎛ ⎡⎧ H₁ ₁ √3⋅H₂ ₁ H₁ ₂ √3⋅H₂ ↪\n", "⎜[[H₁ ₁, H₁ ₂], [H₂ ₁, H₂ ₂]], ⎢⎨H₁ ₁: - ──── - ───────, H₁ ₂: - ──── - ────── ↪\n", "⎝ ⎣⎩ 2 2 2 2 ↪\n", "\n", "↪ ₂ √3⋅H₁ ₁ H₂ ₁ √3⋅H₁ ₂ H₂ ₂ ↪\n", "↪ ─, H₂ ₁: ─────── - ────, H₂ ₂: ─────── - ────, HS₁: HS₁, HS₂: HS₂, PartialMu ↪\n", "↪ 2 2 2 2 ↪\n", "\n", "↪ PartialMu(H₁ ₁) √3⋅PartialMu(H₂ ₁) PartialMu ↪\n", "↪ (H₁ ₁): - ─────────────── - ──────────────────, PartialMu(H₁ ₂): - ───────── ↪\n", "↪ 2 2 2 ↪\n", "\n", "↪ (H₁ ₂) √3⋅PartialMu(H₂ ₂) √3⋅PartialMu(H₁ ₁) PartialMu ↪\n", "↪ ────── - ──────────────────, PartialMu(H₂ ₁): ────────────────── - ───────── ↪\n", "↪ 2 2 2 ↪\n", "\n", "↪ (H₂ ₁) √3⋅PartialMu(H₁ ₂) PartialMu(H₂ ₂) ↪\n", "↪ ──────, PartialMu(H₂ ₂): ────────────────── - ───────────────, PartialMu(HS₁ ↪\n", "↪ 2 2 ↪\n", "\n", "↪ ⎫ ↪\n", "↪ ): PartialMu(HS₁), PartialMu(HS₂): PartialMu(HS₂)⎬, {H₁ ₁: H₁ ₁, H₁ ₂: H₁ ₂, ↪\n", "↪ ⎭ ↪\n", "\n", "↪ ↪\n", "↪ H₂ ₁: -H₂ ₁, H₂ ₂: -H₂ ₂, HS₁: HS₁, HS₂: HS₂, PartialMu(H₁ ₁): PartialMu(H₁ ↪\n", "↪ ↪\n", "\n", "↪ ↪\n", "↪ ₁), PartialMu(H₁ ₂): PartialMu(H₁ ₂), PartialMu(H₂ ₁): -PartialMu(H₂ ₁), Pa ↪\n", "↪ ↪\n", "\n", "↪ ↪\n", "↪ rtialMu(H₂ ₂): -PartialMu(H₂ ₂), PartialMu(HS₁): PartialMu(HS₁), PartialMu(H ↪\n", "↪ ↪\n", "\n", "↪ ⎤⎞\n", "↪ S₂): PartialMu(HS₂)}⎥⎟\n", "↪ ⎦⎠" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "comp, maps" ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "And the matrices those dictionaries encode, in generator order: index $0$ is $\\rho_2(a)$,\n", "index $1$ is $\\rho_2(b)$ — the same two written by hand in §1.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "generator 0\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ "⎡ -√3 ⎤\n", "⎢-1/2 ────⎥\n", "⎢ 2 ⎥\n", "⎢ ⎥\n", "⎢ √3 ⎥\n", "⎢ ── -1/2⎥\n", "⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "generator 1\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}1 & 0\\\\0 & -1\\end{matrix}\\right]$" ], "text/plain": [ "⎡1 0 ⎤\n", "⎢ ⎥\n", "⎣0 -1⎦" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "for gi, M in enumerate(S3_GENS[\"2\"]):\n", " print(\"generator\", gi) # the INDEX is a label, not mathematics\n", " display(M)\n" ] }, { "cell_type": "markdown", "id": "21", "metadata": {}, "source": [ "---\n", "## 3. Conjugacy classes and characters\n", "\n", "The **character** of a representation is just the trace, $\\chi_r(g)=\\operatorname{Tr}\\rho_r(g)$.\n", "It throws away almost all the information in the matrices — and that is precisely why it\n", "is useful:\n", "\n", "- it is **basis-independent** (trace is cyclic, so $\\operatorname{Tr}(U\\rho U^{-1})=\\operatorname{Tr}\\rho$);\n", "- it is **constant on conjugacy classes**, for the same reason;\n", "- and the characters of the irreps are **orthonormal**, which turns \"how many invariants\n", " are there?\" into an inner product.\n", "\n", "> **Before running the next cells.** Derive the conjugacy classes of $S_3$ by hand first —\n", "> conjugation is relabelling, so classes should group permutations by *shape*. How many\n", "> classes do you get? Then note that $\\chi_r(e)=\\dim r$ always, and recall\n", "> $\\sum_r (\\dim r)^2 = |G|$. With $|S_3|=6$ and three irreps, is the set\n", "> $\\{1,1,2\\}$ forced?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "conjugacy classes (as words in the generators):\n", " size 1 : [()]\n", " size 2 : [(0,), (0, 0)]\n", " size 3 : [(1,), (0, 1), (1, 0)]\n", "\n", "#classes = 3\n" ] } ], "source": [ "# ---- MOVE 1: conjugacy classes, by conjugating every element. -------------\n", "def conjugacy_classes(words, gens, faithful, verbose=False):\n", " # Orbits of g -> h g h^-1. `faithful` names an irrep that separates the\n", " # elements, so a matrix can be turned back into its word.\n", " d = gens[faithful][0].shape[0]\n", "\n", " def word_of(M):\n", " for w in words:\n", " if sp.simplify(rho(faithful, w, gens) - M) == sp.zeros(d, d):\n", " return w\n", " raise AssertionError(\"not a group element\")\n", "\n", " out, unassigned = [], set(words)\n", " while unassigned:\n", " g = min(unassigned, key=lambda w: (len(w), w))\n", " if verbose:\n", " trace(f\"--- seed g = {g} : conjugate it by every h ---\")\n", " orbit = set()\n", " for h in words:\n", " Mh = rho(faithful, h, gens)\n", " img = word_of(sp.expand(Mh * rho(faithful, g, gens) * Mh.inv()))\n", " if verbose:\n", " tag = \"new to this class\" if img not in orbit else \"\"\n", " trace(f\" h = {h!s:<8} h g h^-1 = {img!s:<8} {tag}\")\n", " orbit.add(img)\n", " out.append(sorted(orbit, key=lambda w: (len(w), w)))\n", " if verbose:\n", " trace(f\" => class of size {len(orbit)} : {out[-1]}\\n\")\n", " unassigned -= orbit\n", " return out\n", "\n", "\n", "classes = conjugacy_classes(WORDS, S3_GENS, \"2\")\n", "print(\"conjugacy classes (as words in the generators):\")\n", "for cl in classes:\n", " print(f\" size {len(cl)} :\", cl)\n", "print(\"\\n#classes =\", len(classes))" ] }, { "cell_type": "markdown", "id": "23", "metadata": {}, "source": [ "**The classes as an object.** The cell above printed them one line at a time; the value\n", "itself is still worth a look, because two later cells lean on its exact shape. It is a list of lists of *words* — the\n", "same tuples of generator indices §1 closed the group into — and each inner list arrives\n", "sorted by `(len(w), w)`. That sort is not cosmetic: the character table below takes\n", "`cl[0]` as the representative of each class, so a deterministic \"first\" is what makes the\n", "table's column order reproducible rather than incidental." ] }, { "cell_type": "code", "execution_count": null, "id": "24", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\left[ \\left[ \\left( \\right)\\right], \\ \\left[ \\left( 0,\\right), \\ \\left( 0, \\ 0\\right)\\right], \\ \\left[ \\left( 1,\\right), \\ \\left( 0, \\ 1\\right), \\ \\left( 1, \\ 0\\right)\\right]\\right]$" ], "text/plain": [ "[[()], [(0,), (0, 0)], [(1,), (0, 1), (1, 0)]]" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "classes" ] }, { "cell_type": "markdown", "id": "25", "metadata": {}, "source": [ "Sizes $1+2+3=6$, and the shapes are the ones conjugation should give: the identity alone,\n", "the two 3-cycles together, the three transpositions together. The next cell watches a\n", "single orbit close, which is where that grouping comes from." ] }, { "cell_type": "code", "execution_count": null, "id": "26", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- seed g = () : conjugate it by every h ---\n", " h = () h g h^-1 = () new to this class\n", " h = (0,) h g h^-1 = () \n", " h = (1,) h g h^-1 = () \n", " h = (0, 0) h g h^-1 = () \n", " h = (0, 1) h g h^-1 = () \n", " h = (1, 0) h g h^-1 = () \n", " => class of size 1 : [()]\n", "\n", "--- seed g = (0,) : conjugate it by every h ---\n", " h = () h g h^-1 = (0,) new to this class\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " h = (0,) h g h^-1 = (0,) \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " h = (1,) h g h^-1 = (0, 0) new to this class\n", " h = (0, 0) h g h^-1 = (0,) \n", " h = (0, 1) h g h^-1 = (0, 0) \n", " h = (1, 0) h g h^-1 = (0, 0) \n", " => class of size 2 : [(0,), (0, 0)]\n", "\n", "--- seed g = (1,) : conjugate it by every h ---\n", " h = () h g h^-1 = (1,) new to this class\n", " h = (0,) h g h^-1 = (1, 0) new to this class\n", " h = (1,) h g h^-1 = (1,) \n", " h = (0, 0) h g h^-1 = (0, 1) new to this class\n", " h = (0, 1) h g h^-1 = (1, 0) \n", " h = (1, 0) h g h^-1 = (0, 1) \n", " => class of size 3 : [(1,), (0, 1), (1, 0)]\n", "\n" ] } ], "source": [ "# ---- peek: watch one orbit close under conjugation -----------------------\n", "# Conjugation is relabelling, so the orbit stops as soon as every h maps the\n", "# seed back into elements already collected.\n", "_ = conjugacy_classes(WORDS, S3_GENS, \"2\", verbose=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "27", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " e a (3-cycle) b (transposition) \n", "-----------------------------------------------------------------------\n", "1 1 1 1 \n", "1p 1 1 -1 \n", "2 2 -1 0 \n", "✓ every character is constant on each conjugacy class\n" ] } ], "source": [ "# ---- MOVE 2: collect. The character table emerges. ----------------------\n", "chi = {lab: [sp.nsimplify(sp.expand(sp.trace(rho(lab, w)))) for w in WORDS]\n", " for lab in S3_GENS}\n", "\n", "reps = [cl[0] for cl in classes] # one element per class\n", "names = [\"e\", \"a (3-cycle)\", \"b (transposition)\"]\n", "hdr = f\"{'':<5}\" + \"\".join(f\"{n:<22}\" for n in names)\n", "print(hdr); print(\"-\" * len(hdr))\n", "for lab in (\"1\", \"1p\", \"2\"):\n", " row = f\"{lab:<5}\"\n", " for r in reps:\n", " row += f\"{chi[lab][WORDS.index(r)]!s:<22}\"\n", " print(row)\n", "\n", "# constant on classes -- verify, do not assume\n", "for lab in S3_GENS:\n", " for cl in classes:\n", " vals = {chi[lab][WORDS.index(w)] for w in cl}\n", " assert len(vals) == 1, (lab, cl, vals)\n", "ok(\"every character is constant on each conjugacy class\")\n" ] }, { "cell_type": "markdown", "id": "28", "metadata": {}, "source": [ "**The objects behind the table.** Three of them, and the indexing is the part worth\n", "fixing in your head. `WORDS` is the element ordering §1's closure happened to produce, and\n", "everything in this section is indexed by it — so `chi[lab]` holds one entry per *element*,\n", "six of them, not one per class. `reps` is the other half of the bookkeeping: the first word\n", "of each conjugacy class, taken from the `classes` above." ] }, { "cell_type": "code", "execution_count": null, "id": "29", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "([(), (0,), (1,)],\n", " [(), (0,), (1,), (0, 0), (0, 1), (1, 0)],\n", " {'1': [1, 1, 1, 1, 1, 1],\n", " '1p': [1, 1, -1, 1, -1, -1],\n", " '2': [2, -1, 0, -1, 0, 0]})" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "reps, WORDS, chi" ] }, { "cell_type": "markdown", "id": "30", "metadata": {}, "source": [ "So the printed row is a *lookup*, not a computation: `chi[lab][WORDS.index(r)]` picks the\n", "one entry per class that the loop just proved is shared by all of them. Printing three\n", "columns out of six is honest only because of the assertion sitting in the same cell." ] }, { "cell_type": "markdown", "id": "31", "metadata": {}, "source": [ "$$\\begin{array}{c|ccc}\n", " & e & a\\ (\\text{3-cycle}) & b\\ (\\text{transposition})\\\\\\hline\n", "\\mathbf{1} & 1 & 1 & 1\\\\\n", "\\mathbf{1'} & 1 & 1 & -1\\\\\n", "\\mathbf{2} & 2 & -1 & 0\n", "\\end{array}$$\n", "\n", "Read it: $\\mathbf{1'}$ agrees with $\\mathbf{1}$ on the 3-cycles and differs on the\n", "transpositions — it *is* the even/odd sign. And $\\chi_2(b)=0$ says the doublet's\n", "reflection is traceless, which for a $2\\times2$ orthogonal matrix means eigenvalues\n", "$\\pm1$: a genuine reflection, not a rotation.\n", "\n", "Now the three checks that make the table trustworthy.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "32", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ = delta_rs -- the irreps are orthonormal\n", "✓ sum_r (dim r)^2 = 6 = |S_3| -- so 1+1+2 is the ONLY option, and the list of irreps is complete\n", "✓ #irreps = #conjugacy classes = 3\n" ] } ], "source": [ "# ---- MOVE 3 + 4: check. Three independent constraints. -----------------\n", "G = len(WORDS)\n", "\n", "# (a) orthonormality: = delta_rs\n", "inner = lambda p, q: sp.simplify(\n", " sum(sp.conjugate(chi[p][i]) * chi[q][i] for i in range(G)) / G)\n", "for p in S3_GENS:\n", " for q in S3_GENS:\n", " assert inner(p, q) == (1 if p == q else 0), (p, q, inner(p, q))\n", "ok(\" = delta_rs -- the irreps are orthonormal\")\n", "\n", "# (b) sum of squared dimensions is the group order\n", "dims = {lab: S3_GENS[lab][0].shape[0] for lab in S3_GENS}\n", "assert sum(d**2 for d in dims.values()) == G\n", "ok(f\"sum_r (dim r)^2 = {G} = |S_3| -- so 1+1+2 is the ONLY option, and the \"\n", " \"list of irreps is complete\")\n", "\n", "# (c) #irreps = #conjugacy classes\n", "assert len(S3_GENS) == len(classes) == 3\n", "ok(\"#irreps = #conjugacy classes = 3\")\n" ] }, { "cell_type": "markdown", "id": "33", "metadata": {}, "source": [ "Check (b) is the one worth internalising. $\\sum_r(\\dim r)^2=|G|$ with $|G|=6$ and three\n", "classes forces $\\{1,1,2\\}$: there is **no fourth irrep of $S_3$ waiting to be found**, and\n", "no 3-dimensional one. When someone writes an $S_3$ triplet, they mean a *reducible*\n", "$1\\oplus 2$ or $1'\\oplus 2$ — worth knowing before you try to assign three generations to\n", "\"the triplet of $S_3$.\"\n" ] }, { "cell_type": "markdown", "id": "34", "metadata": {}, "source": [ "---\n", "## 4. Counting invariants **before** building them\n", "\n", "Here is the payoff. An invariant is a copy of the trivial representation. The number of\n", "times $\\mathbf{1}$ appears in a representation with character $\\chi$ is the inner product\n", "with the trivial character, which is just an average:\n", "\n", "$$n_{\\text{inv}} \\;=\\; \\langle \\chi, \\chi_{\\mathbf 1}\\rangle \\;=\\; \\frac{1}{|G|}\\sum_{g\\in G}\\chi(g).$$\n", "\n", "And characters multiply under tensor products, $\\chi_{V\\otimes W}=\\chi_V\\chi_W$. So for\n", "$n$ doublets,\n", "\n", "$$n_{\\text{inv}}\\big(\\mathbf 2^{\\otimes n}\\big)=\\frac1{|G|}\\sum_g \\chi_2(g)^n .$$\n", "\n", "One line, no Clebsch–Gordan tables, no construction.\n", "\n", "> **Before running the next cell.** Predict $n=2$: how many ways are there to contract two\n", "> doublets into a scalar? Then $n=3$ — is there a cubic invariant at all? (Think about\n", "> what $\\mathbf 2\\otimes\\mathbf 2$ decomposes into, and whether any piece can pair with\n", "> a third doublet.) Then $n=4$: guess before you look, and notice you probably guessed the\n", "> number of *pairings* $\\{12|34\\},\\{13|24\\},\\{14|23\\}$.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "35", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " invariants in 2^(x2) (2 DISTINCT doublets) : 1\n", " invariants in 2^(x3) (3 DISTINCT doublets) : 1\n", " invariants in 2^(x4) (4 DISTINCT doublets) : 3\n", " invariants in 2^(x5) (5 DISTINCT doublets) : 5\n", " invariants in 2^(x6) (6 DISTINCT doublets) : 11\n" ] } ], "source": [ "# ---- MOVE 1 + 2: the averaging formula, for n distinct doublets. ----------\n", "def n_invariants(chars, verbose=False, classes=None):\n", " # Copies of the trivial rep in the tensor product with these characters.\n", " total = sp.simplify(sum(sp.prod([c[i] for c in chars]) for i in range(G)) / G)\n", " if verbose:\n", " # Characters are constant on classes (section 3), so the |G|-term\n", " # average collapses to one row per class, weighted by the class size.\n", " rows = ([(cl[0], len(cl)) for cl in classes] if classes\n", " else [(w, 1) for w in WORDS])\n", " trace(f\" {'class rep':<10} {'size':>5} prod_i chi_i(g)\")\n", " pieces = []\n", " for rep_, size in rows:\n", " v = sp.simplify(sp.prod([c[WORDS.index(rep_)] for c in chars]))\n", " trace(f\" {rep_!s:<10} {size:5d} {v}\")\n", " pieces.append(f\"{size}*{v}\")\n", " trace(f\" (1/{G})[{' + '.join(pieces)}] = {total}\")\n", " return total\n", "\n", "\n", "for n in range(2, 7):\n", " print(f\" invariants in 2^(x{n}) ({n} DISTINCT doublets) : \"\n", " f\"{n_invariants([chi['2']] * n)}\")" ] }, { "cell_type": "markdown", "id": "36", "metadata": {}, "source": [ "**Every element, then every class.** The `n = 4` row above is a sum of $|G|=6$ terms, one\n", "per element. The first cell below prints all six; the second groups them by conjugacy\n", "class. Same number both times — the characters are constant on classes (§3), so each\n", "class contributes its size times one shared product." ] }, { "cell_type": "code", "execution_count": null, "id": "37", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " class rep size prod_i chi_i(g)\n", " () 1 16\n", " (0,) 1 1\n", " (1,) 1 0\n", " (0, 0) 1 1\n", " (0, 1) 1 0\n", " (1, 0) 1 0\n", " (1/6)[1*16 + 1*1 + 1*0 + 1*1 + 1*0 + 1*0] = 3\n" ] }, { "data": { "text/latex": [ "$\\displaystyle 3$" ], "text/plain": [ "3" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ---- peek: the same quartic count, all six elements ------------------------\n", "# With no `classes`, the trace walks every element of WORDS -- the raw |G|-term\n", "# average, exactly as the formula is written. The next cell collapses it.\n", "n_invariants([chi[\"2\"]] * 4, verbose=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " class rep size prod_i chi_i(g)\n", " () 1 16\n", " (0,) 2 1\n", " (1,) 3 0\n", " (1/6)[1*16 + 2*1 + 3*0] = 3\n" ] }, { "data": { "text/latex": [ "$\\displaystyle 3$" ], "text/plain": [ "3" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ---- peek: the quartic count, one class at a time ------------------------\n", "# The n = 4 row above is just this average. Three quartics, and you can see\n", "# which classes contributed.\n", "n_invariants([chi[\"2\"]] * 4, verbose=True, classes=classes)" ] }, { "cell_type": "markdown", "id": "39", "metadata": {}, "source": [ "So: **one** quadratic, **one** cubic, **three** quartic.\n", "\n", "The cubic is worth a pause. A single $S_3$ doublet admits a cubic invariant — that is the\n", "$\\mathbb{Z}_3$-covariant $\\mathrm{Re}\\big[(r_1+ir_2)^3\\big]=r_1^3-3r_1r_2^2$ that drives the\n", "whole 3HDM-$S_3$ scalar potential (see `THDM_S3_Tutorial`). It exists because $\\chi_2$ is\n", "real and $\\chi_2(a)=-1$, so the average survives.\n", "\n", "But there is a trap in the quartic count, and it is the single most common invariant-counting\n", "mistake in flavour model building.\n" ] }, { "cell_type": "markdown", "id": "40", "metadata": {}, "source": [ "### 4.1 Distinct multiplets versus one multiplet\n", "\n", "$n_{\\text{inv}}(\\mathbf 2^{\\otimes4})=3$ counts invariants built from **four independent\n", "doublets** $x,y,z,w$. If instead you have **one** doublet $x$ and want quartic terms\n", "$\\sim x^4$, the four slots are no longer independent — the product is *symmetrised*, and\n", "you must count in $\\mathrm{Sym}^4(\\mathbf 2)$, not $\\mathbf 2^{\\otimes4}$.\n", "\n", "The tool is the **Molien series**, which generates the symmetric-power counts at once:\n", "\n", "$$\\sum_{n\\ge0} \\dim\\big[\\mathrm{Sym}^n(V)\\big]^{G}\\, t^n\n", "\\;=\\; \\frac{1}{|G|}\\sum_{g\\in G}\\frac{1}{\\det\\!\\big(\\mathbb{1}-t\\,\\rho(g)\\big)} .$$\n", "\n", "> **Before running the next cell.** You already know a single $S_3$ doublet has one\n", "> quadratic and one cubic invariant. Give them names, since the rest of this section uses\n", "> them:\n", ">\n", "> $$I_2 \\equiv x_1^2+x_2^2, \\qquad I_3 \\equiv x_1^3-3x_1x_2^2$$\n", ">\n", "> — the subscript is the degree, and $I_3$ is §4's cubic\n", "> $\\mathrm{Re}\\big[(x_1+ix_2)^3\\big]$ with $r\\to x$. (The *other* cubic combination,\n", "> $x_2^3-3x_2x_1^2$, is **not** invariant: it is even under $a$ but flips sign under the\n", "> reflection $b$, making it a $\\mathbf 1'$ covariant — the same antisymmetry §5.1 returns\n", "> to.)\n", ">\n", "> If those two are the only *generators* of the invariant ring, what is the quartic count\n", "> — and at which degree does something genuinely new first appear? Predict the answer\n", "> before looking, then check the series against the guess \"everything is a polynomial in\n", "> $I_2$ and $I_3$\".\n" ] }, { "cell_type": "code", "execution_count": null, "id": "41", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle M(t) = \\frac{1}{\\left(t - 1\\right)^{2} \\left(t + 1\\right) \\left(t^{2} + t + 1\\right)}$" ], "text/plain": [ " 1 \n", "M(t) = ─────────────────────────────\n", " 2 ⎛ 2 ⎞\n", " (t - 1) ⋅(t + 1)⋅⎝t + t + 1⎠" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " the same series, regrouped as one quadratic x one cubic generator:\n" ] }, { "data": { "text/latex": [ "$\\displaystyle M(t) = \\frac{1}{\\left(1 - t^{2}\\right) \\left(1 - t^{3}\\right)}$" ], "text/plain": [ " 1 \n", "M(t) = ─────────────────\n", " ⎛ 2⎞ ⎛ 3⎞\n", " ⎝1 - t ⎠⋅⎝1 - t ⎠" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", " degree : 0 1 2 3 4 5 6 7 8\n", " #inv : 1 0 1 1 1 1 2 1 2\n" ] } ], "source": [ "# ---- MOVE 2: the Molien series for ONE doublet. --------------------------\n", "t = sp.Symbol(\"t\")\n", "molien = sp.simplify(\n", " sum(1 / sp.det(sp.eye(2) - t * rho(\"2\", w)) for w in WORDS) / G)\n", "display(sp.Eq(sp.Symbol(\"M(t)\"), sp.factor(molien), evaluate=False))\n", "\n", "# sp.factor groups it as (t-1)^2 (t+1) (t^2+t+1); the form quoted below is\n", "# (1-t^2)(1-t^3). Same series -- so PROVE it rather than asserting by eye.\n", "claimed = 1 / ((1 - t**2) * (1 - t**3))\n", "assert sp.simplify(molien - claimed) == 0\n", "print(\" the same series, regrouped as one quadratic x one cubic generator:\")\n", "display(sp.Eq(sp.Symbol(\"M(t)\"), claimed, evaluate=False))\n", "\n", "ser = sp.expand(sp.series(molien, t, 0, 9).removeO())\n", "counts = [(n, ser.coeff(t, n)) for n in range(9)]\n", "print(\"\\n degree :\", \"\".join(f\"{n:4d}\" for n, _ in counts))\n", "print(\" #inv :\", \"\".join(f\"{c!s:>4}\" for _, c in counts))\n" ] }, { "cell_type": "code", "execution_count": null, "id": "42", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " class () (size 1) contributes\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\frac{1}{t^{2} - 2 t + 1}$" ], "text/plain": [ " 1 \n", "────────────\n", " 2 \n", "t - 2⋅t + 1" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " class (0,) (size 2) contributes\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\frac{1}{t^{2} + t + 1}$" ], "text/plain": [ " 1 \n", "──────────\n", " 2 \n", "t + t + 1" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " class (1,) (size 3) contributes\n" ] }, { "data": { "text/latex": [ "$\\displaystyle - \\frac{1}{t^{2} - 1}$" ], "text/plain": [ " -1 \n", "──────\n", " 2 \n", "t - 1" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " the weighted sum over 6 elements, divided by |G| = 6 :\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\frac{1}{\\left(t - 1\\right)^{2} \\left(t + 1\\right) \\left(t^{2} + t + 1\\right)}$" ], "text/plain": [ " 1 \n", "─────────────────────────────\n", " 2 ⎛ 2 ⎞\n", "(t - 1) ⋅(t + 1)⋅⎝t + t + 1⎠" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "✓ the class-by-class average rebuilds M(t) -- nothing was skipped\n" ] } ], "source": [ "# ---- peek: the Molien average, class by class ----------------------------\n", "# Same collapse as above: 1/det(1 - t rho(g)) is a class function too.\n", "acc = 0\n", "for cl in classes:\n", " term = sp.simplify(1 / sp.det(sp.eye(2) - t * rho(\"2\", cl[0])))\n", " acc += len(cl) * term\n", " trace(f\" class {cl[0]!s:<8} (size {len(cl)}) contributes\", term)\n", "trace(f\" the weighted sum over {G} elements, divided by |G| = {G} :\",\n", " sp.factor(sp.simplify(acc / G)))\n", "assert sp.simplify(acc / G - molien) == 0\n", "ok(\"the class-by-class average rebuilds M(t) -- nothing was skipped\")" ] }, { "cell_type": "markdown", "id": "43", "metadata": {}, "source": [ "$$\\frac{1}{(1-t^2)(1-t^3)}$$\n", "\n", "— the invariant ring of one $S_3$ doublet is **freely generated** by one quadratic and one\n", "cubic. So the degree counts are the number of ways to write $n=2i+3j$: one invariant at\n", "degrees 2, 3, 4, 5, and the first **two** at degree 6 ($I_2^3$ and $I_3^2$).\n", "\n", "The quartic count is therefore **1**, not 3. Same group, same irrep — the answer depends\n", "entirely on whether the four legs are distinct fields or one field repeated.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "44", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ Molien = 1/((1-t^2)(1-t^3)) exactly -- one quadratic and one cubic generator, freely\n", "\n", " quartics from ONE doublet : 1\n", " quartics from FOUR doublets : 3\n", "✓ 1 versus 3 -- distinctness of the legs changes the answer, and neither number is a typo\n" ] } ], "source": [ "# ---- MOVE 3 + 4: recognise, then check against the free-ring claim. ------\n", "free_series = sp.series(1 / ((1 - t**2) * (1 - t**3)), t, 0, 9).removeO()\n", "assert sp.simplify(sp.expand(free_series - ser)) == 0\n", "ok(\"Molien = 1/((1-t^2)(1-t^3)) exactly -- one quadratic and one cubic \"\n", " \"generator, freely\")\n", "\n", "quartic_one = ser.coeff(t, 4)\n", "quartic_four = n_invariants([chi[\"2\"]] * 4)\n", "print(\"\\n quartics from ONE doublet :\", quartic_one)\n", "print(\" quartics from FOUR doublets :\", quartic_four)\n", "assert quartic_one == 1 and quartic_four == 3\n", "ok(\"1 versus 3 -- distinctness of the legs changes the answer, and neither \"\n", " \"number is a typo\")\n" ] }, { "cell_type": "markdown", "id": "45", "metadata": {}, "source": [ "---\n", "## 5. Clebsch–Gordan: actually building the invariant\n", "\n", "Counting says *how many*. To write them down you need the decomposition. For $S_3$ in the\n", "real orthogonal basis,\n", "\n", "$$\\mathbf 2\\otimes\\mathbf 2=\\mathbf 1\\oplus\\mathbf 1'\\oplus\\mathbf 2,$$\n", "\n", "and `feynlag` ships the coefficients as `S3.doublet_product`:\n", "\n", "$$\\mathbf 1: x_1y_1+x_2y_2,\\qquad\n", "\\mathbf 1': x_1y_2-x_2y_1,\\qquad\n", "\\mathbf 2: \\big(x_1y_1-x_2y_2,\\;-(x_1y_2+x_2y_1)\\big).$$\n", "\n", "> **Before running the next cell.** Check the dimensions add up: $2\\times2=1+1+2$. ✓. Now\n", "> the important one — set $y=x$ (one doublet, not two) and evaluate the $\\mathbf 1'$\n", "> piece by hand. What do you get, and what does that imply for how many invariants a\n", "> single doublet can form?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "46", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\{x \\otimes y\\}_{1} = x_{1} y_{1} + x_{2} y_{2}$" ], "text/plain": [ "\\{x \\otimes y\\}_{1} = x₁⋅y₁ + x₂⋅y₂" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\{x \\otimes y\\}_{1'} = x_{1} y_{2} - x_{2} y_{1}$" ], "text/plain": [ "\\{x \\otimes y\\}_{1'} = x₁⋅y₂ - x₂⋅y₁" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\{x \\otimes y\\}_{2} = \\left[\\begin{matrix}x_{1} y_{1} - x_{2} y_{2}\\\\- x_{1} y_{2} - x_{2} y_{1}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡x₁⋅y₁ - x₂⋅y₂ ⎤\n", "\\{x \\otimes y\\}_{2} = ⎢ ⎥\n", " ⎣-x₁⋅y₂ - x₂⋅y₁⎦" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ---- MOVE 1: the three channels, and how they transform. -----------------\n", "x = sp.symbols(\"x1 x2\"); y = sp.symbols(\"y1 y2\")\n", "cg = S3.doublet_product\n", "P = cg(x, y)\n", "for ch in (\"1\", \"1p\", \"2\"):\n", " lab = {\"1\": \"1\", \"1p\": \"1'\", \"2\": \"2\"}[ch] # 1p is ASCII for 1'\n", " display(sp.Eq(sp.Symbol(rf\"\\{{x \\otimes y\\}}_{{{lab}}}\"),\n", " sp.Matrix(P[ch]) if ch == \"2\" else P[ch], evaluate=False))\n" ] }, { "cell_type": "markdown", "id": "47", "metadata": {}, "source": [ "**What `doublet_product` hands back.** The display above is formatted; the return value is\n", "a plain dictionary keyed by the irrep labels `\"1\"`, `\"1p\"` and `\"2\"`. The asymmetry in it\n", "matters for everything that follows." ] }, { "cell_type": "code", "execution_count": null, "id": "48", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'1': x1*y1 + x2*y2, '1p': x1*y2 - x2*y1, '2': (x1*y1 - x2*y2, -x1*y2 - x2*y1)}" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "P" ] }, { "cell_type": "markdown", "id": "49", "metadata": {}, "source": [ "The two singlet channels are bare expressions, while `\"2\"` is a *tuple of two* — a doublet,\n", "in exactly the form `cg` accepts as an argument. That is why the cell above wraps only the\n", "`\"2\"` entry in `sp.Matrix` to display it, and why §5.2 can write\n", "`cg(cg(x, x)[\"2\"], cg(x, x)[\"2\"])[\"1\"]`: the doublet channel feeds straight back in as if\n", "it were a fresh pair of fields." ] }, { "cell_type": "code", "execution_count": null, "id": "50", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ the three channels transform as 1, 1' (with sign = det) and 2 -- verified, not quoted\n" ] } ], "source": [ "# ---- MOVE 2 + 4: each channel must transform as its label claims. --------\n", "def act(vec, M):\n", " # The doublet (v1, v2) under a generator matrix M.\n", " return tuple(sum(M[i, k] * vec[k] for k in range(2)) for i in range(2))\n", "\n", "\n", "for gi, M in enumerate(S3_GENS[\"2\"]):\n", " xp, yp = act(x, M), act(y, M)\n", " Pp = cg(xp, yp)\n", " # the singlet is untouched\n", " assert sp.simplify(sp.expand(Pp[\"1\"] - P[\"1\"])) == 0\n", " # the 1' picks up det(M) = +1 for the rotation, -1 for the reflection\n", " sign = sp.simplify(M.det())\n", " assert sp.simplify(sp.expand(Pp[\"1p\"] - sign * P[\"1p\"])) == 0\n", " # the doublet channel transforms as a doublet\n", " want = act(P[\"2\"], M)\n", " assert all(sp.simplify(sp.expand(Pp[\"2\"][i] - want[i])) == 0 for i in range(2))\n", "ok(\"the three channels transform as 1, 1' (with sign = det) and 2 -- verified, \"\n", " \"not quoted\")\n" ] }, { "cell_type": "markdown", "id": "51", "metadata": {}, "source": [ "### 5.1 The $\\mathbf 1'$ trap\n", "\n", "Now the thing that catches people.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "52", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\{x \\otimes x\\}_{1} = x_{1}^{2} + x_{2}^{2}$" ], "text/plain": [ " 2 2\n", "\\{x \\otimes x\\}_{1} = x₁ + x₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\{x \\otimes x\\}_{1'} = 0$" ], "text/plain": [ "\\{x \\otimes x\\}_{1'} = 0" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " ^ identically zero\n", "✓ 1' is ANTISYMMETRIC, so it vanishes when both legs are the same doublet\n" ] } ], "source": [ "# ---- the antisymmetric singlet vanishes on a repeated doublet ------------\n", "same = cg(x, x)\n", "display(sp.Eq(sp.Symbol(r\"\\{x \\otimes x\\}_{1}\"),\n", " sp.expand(same[\"1\"]), evaluate=False))\n", "display(sp.Eq(sp.Symbol(r\"\\{x \\otimes x\\}_{1'}\"),\n", " sp.expand(same[\"1p\"]), evaluate=False))\n", "print(\" ^ identically zero\")\n", "assert sp.expand(same[\"1p\"]) == 0\n", "ok(\"1' is ANTISYMMETRIC, so it vanishes when both legs are the same doublet\")\n" ] }, { "cell_type": "markdown", "id": "53", "metadata": {}, "source": [ "$x_1x_2-x_2x_1=0$. Obvious once written — and invisible if you copy a Clebsch–Gordan table\n", "into a model with one doublet and start counting terms. It is the same fact §4.1 measured\n", "from the other side: with one doublet, channels that need two *distinct* multiplets simply\n", "are not available.\n", "\n", "**Model-building consequence.** The number of terms in your potential depends on how many\n", "distinct multiplets you have, not just on the group. Two $S_3$ doublets $(H_1,H_2)$ and\n", "$(H_3,H_4)$ give you $\\mathbf 1'$ couplings that a single doublet cannot form.\n", "\n", "Now construct the quartics and confirm both counts from §4.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "54", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "built 9 candidate quartics from four distinct doublets:\n", " (xy)_1 (zw)_1\n", " (xy)_1p (zw)_1p\n", " (xy)_2 . (zw)_2\n", " (xz)_1 (yw)_1\n", " (xz)_1p (yw)_1p\n", " (xz)_2 . (yw)_2\n", " (xw)_1 (yz)_1\n", " (xw)_1p (yz)_1p\n", " (xw)_2 . (yz)_2\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", " independent among them: 3 (characters predicted 3)\n", "✓ 9 candidates, rank 3 -- six relations, and the count matches the character prediction exactly\n" ] } ], "source": [ "# ---- MOVE 3: construct, and check the rank against the prediction. -------\n", "z = sp.symbols(\"z1 z2\"); w = sp.symbols(\"w1 w2\")\n", "D = {\"x\": x, \"y\": y, \"z\": z, \"w\": w}\n", "allv = [*x, *y, *z, *w]\n", "\n", "def rank_of(exprs, gens, verbose=False, labels=None):\n", " monos = sorted({m for e in exprs for m in sp.Poly(e, *gens).monoms()})\n", " Mx = sp.Matrix([[sp.Poly(e, *gens).coeff_monomial(m) for m in monos]\n", " for e in exprs])\n", " if verbose:\n", " # One row per candidate, one column per monomial: the rank of that\n", " # matrix IS the number of independent invariants.\n", " trace(f\" coefficient matrix: {Mx.rows} candidates x \"\n", " f\"{Mx.cols} monomials\", Mx)\n", " names = labels or [f\"row {k}\" for k in range(Mx.rows)]\n", " keep, r = [], 0 # a maximal independent subset, in order\n", " for k in range(Mx.rows):\n", " if Mx[keep + [k], :].rank() > r:\n", " keep.append(k); r += 1\n", " trace(\" independent : \" + \", \".join(names[k] for k in keep))\n", " trace(\" dependent : \" + \", \".join(n for k, n in enumerate(names)\n", " if k not in keep))\n", " return Mx.rank()\n", "\n", "cands, labels = [], []\n", "for (p, q), (r_, u) in itertools.combinations(\n", " list(itertools.combinations(\"xyzw\", 2)), 2):\n", " if set(p + q) | set(r_ + u) != set(\"xyzw\"):\n", " continue\n", " for ch in (\"1\", \"1p\"): # pair each channel with itself\n", " cands.append(sp.expand(cg(D[p], D[q])[ch] * cg(D[r_], D[u])[ch]))\n", " labels.append(f\"({p}{q})_{ch} ({r_}{u})_{ch}\")\n", " d1, d2 = cg(D[p], D[q])[\"2\"], cg(D[r_], D[u])[\"2\"]\n", " cands.append(sp.expand(cg(d1, d2)[\"1\"])) # doublet x doublet -> singlet\n", " labels.append(f\"({p}{q})_2 . ({r_}{u})_2\")\n", "\n", "print(f\"built {len(cands)} candidate quartics from four distinct doublets:\")\n", "for l in labels:\n", " print(\" \", l)\n", "r4 = rank_of(cands, allv)\n", "print(\"\\n independent among them:\", r4, f\" (characters predicted {quartic_four})\")\n", "assert r4 == quartic_four == 3\n", "ok(\"9 candidates, rank 3 -- six relations, and the count matches the character \"\n", " \"prediction exactly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "55", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " coefficient matrix: 9 candidates x 8 monomials\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\left[\\begin{matrix}1 & 1 & 0 & 0 & 0 & 0 & 1 & 1\\\\0 & 0 & 1 & -1 & -1 & 1 & 0 & 0\\\\1 & -1 & 1 & 1 & 1 & 1 & -1 & 1\\\\1 & 0 & 1 & 0 & 0 & 1 & 0 & 1\\\\0 & 1 & 0 & -1 & -1 & 0 & 1 & 0\\\\1 & 1 & -1 & 1 & 1 & -1 & 1 & 1\\\\1 & 0 & 0 & 1 & 1 & 0 & 0 & 1\\\\0 & 1 & -1 & 0 & 0 & -1 & 1 & 0\\\\1 & 1 & 1 & -1 & -1 & 1 & 1 & 1\\end{matrix}\\right]$" ], "text/plain": [ "⎡1 1 0 0 0 0 1 1⎤\n", "⎢ ⎥\n", "⎢0 0 1 -1 -1 1 0 0⎥\n", "⎢ ⎥\n", "⎢1 -1 1 1 1 1 -1 1⎥\n", "⎢ ⎥\n", "⎢1 0 1 0 0 1 0 1⎥\n", "⎢ ⎥\n", "⎢0 1 0 -1 -1 0 1 0⎥\n", "⎢ ⎥\n", "⎢1 1 -1 1 1 -1 1 1⎥\n", "⎢ ⎥\n", "⎢1 0 0 1 1 0 0 1⎥\n", "⎢ ⎥\n", "⎢0 1 -1 0 0 -1 1 0⎥\n", "⎢ ⎥\n", "⎣1 1 1 -1 -1 1 1 1⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " independent : (xy)_1 (zw)_1, (xy)_1p (zw)_1p, (xy)_2 . (zw)_2\n", " dependent : (xz)_1 (yw)_1, (xz)_1p (yw)_1p, (xz)_2 . (yw)_2, (xw)_1 (yz)_1, (xw)_1p (yz)_1p, (xw)_2 . (yz)_2\n" ] } ], "source": [ "# ---- peek: WHICH three, and which six are the relations ------------------\n", "_ = rank_of(cands, allv, verbose=True, labels=labels)" ] }, { "cell_type": "code", "execution_count": null, "id": "56", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle (x\\cdot x)^{2} = x_{1}^{4} + 2 x_{1}^{2} x_{2}^{2} + x_{2}^{4}$" ], "text/plain": [ " 4 2 2 4\n", "(x\\cdot x)² = x₁ + 2⋅x₁ ⋅x₂ + x₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\{xx\\}_2\\cdot\\{xx\\}_2 = x_{1}^{4} + 2 x_{1}^{2} x_{2}^{2} + x_{2}^{4}$" ], "text/plain": [ " 4 2 2 4\n", "\\{xx\\}_2\\cdot\\{xx\\}_2 = x₁ + 2⋅x₁ ⋅x₂ + x₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", " independent: 1 (Molien predicted 1)\n", "✓ two ways to write it, one invariant -- the doublet channel gives nothing new\n" ] } ], "source": [ "# ---- and the one-doublet case, against the Molien prediction -------------\n", "one = [sp.expand(cg(x, x)[\"1\"]**2),\n", " sp.expand(cg(cg(x, x)[\"2\"], cg(x, x)[\"2\"])[\"1\"])]\n", "display(sp.Eq(sp.Symbol(r\"(x\\cdot x)^2\"), one[0], evaluate=False))\n", "display(sp.Eq(sp.Symbol(r\"\\{xx\\}_2\\cdot\\{xx\\}_2\"), one[1], evaluate=False))\n", "r1 = rank_of(one, list(x))\n", "print(\"\\n independent:\", r1, f\" (Molien predicted {quartic_one})\")\n", "assert r1 == quartic_one == 1\n", "ok(\"two ways to write it, one invariant -- the doublet channel gives nothing new\")\n" ] }, { "cell_type": "markdown", "id": "57", "metadata": {}, "source": [ "Both predictions land. This is the loop worth internalising:\n", "\n", "**count with characters → construct with Clebsch–Gordan → check the rank.**\n", "\n", "If the rank comes out *below* the character count you have made an algebra slip; if it\n", "comes out *above*, your \"invariants\" are not all invariant. Either way the disagreement\n", "is informative, which is what makes the check worth doing.\n" ] }, { "cell_type": "markdown", "id": "58", "metadata": {}, "source": [ "---\n", "## 6. The Reynolds projector: invariants without tables\n", "\n", "Clebsch–Gordan coefficients are elegant but they must be *derived per group* and they\n", "depend on the basis. There is a blunter tool that always works, needs no tables, and is\n", "what `feynlag` actually uses — **group averaging**:\n", "\n", "$$P(m)\\;=\\;\\frac{1}{|G|}\\sum_{g\\in G} g\\cdot m .$$\n", "\n", "$P$ is a projector onto the invariant subspace: averaging something already invariant\n", "returns it unchanged, and averaging anything else either produces its invariant part or\n", "kills it outright.\n", "\n", "> **Before running the next cell.** Apply $P$ to a monomial with a definite\n", "> $\\mathbb{Z}_N$ charge. The average is $\\frac1N\\sum_k \\omega^{kq}m$ — a geometric series.\n", "> For which charges $q$ is it $m$, and for which is it $0$? Convince yourself this\n", "> reproduces the familiar selection rule *without anyone having stated the rule*.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "59", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " P(A**3 ) = A**3 charge 3 = 0 mod 3\n", " P(A*B ) = A*B charge 1+2 = 0 mod 3\n", " P(A**2 ) = 0 charge 2 != 0 mod 3\n", " P(A*B**2 ) = 0 charge 5 = 2 != 0\n", "✓ the Reynolds average reproduces 'total charge = 0 mod N' with no rule written down\n" ] } ], "source": [ "# ---- Z_N: the Reynolds average IS the charge selection rule -------------\n", "Z3 = ZN(\"Z3\", 3)\n", "A, B = Scalar(\"A\", real=True), Scalar(\"B\", real=True)\n", "Z3.assign(1, A) # charge 1\n", "Z3.assign(2, B) # charge 2\n", "sA, sB = A.components[0], B.components[0]\n", "\n", "for expr, why in ((sA**3, \"charge 3 = 0 mod 3\"), (sA * sB, \"charge 1+2 = 0 mod 3\"),\n", " (sA**2, \"charge 2 != 0 mod 3\"), (sA * sB**2, \"charge 5 = 2 != 0\")):\n", " # expand_complex: sympy leaves the omega average as\n", " # (1 - (-1)^(1/3) + (-1)^(2/3))/3, which IS zero but does not display so\n", " P_ = sp.simplify(sp.expand_complex(reynolds_project(expr, [Z3])))\n", " print(f\" P({expr!s:<8}) = {P_!s:<8} {why}\")\n", "ok(\"the Reynolds average reproduces 'total charge = 0 mod N' with no rule \"\n", " \"written down\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "60", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1}^{2} \\right)} = \\frac{X_{1}^{2}}{2} + \\frac{X_{2}^{2}}{2}$" ], "text/plain": [ " 2 2\n", " ⎛ 2⎞ X₁ X₂ \n", "P⎝X₁ ⎠ = ─── + ───\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1} X_{2} \\right)} = 0$" ], "text/plain": [ "P(X₁⋅X₂) = 0" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1}^{3} \\right)} = \\frac{X_{1} \\left(X_{1}^{2} - 3 X_{2}^{2}\\right)}{4}$" ], "text/plain": [ " ⎛ 2 2⎞\n", " ⎛ 3⎞ X₁⋅⎝X₁ - 3⋅X₂ ⎠\n", "P⎝X₁ ⎠ = ────────────────\n", " 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1}^{2} X_{2} \\right)} = 0$" ], "text/plain": [ " ⎛ 2 ⎞ \n", "P⎝X₁ ⋅X₂⎠ = 0" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1}^{2} + X_{2}^{2} \\right)} = X_{1}^{2} + X_{2}^{2}$" ], "text/plain": [ " ⎛ 2 2⎞ 2 2\n", "P⎝X₁ + X₂ ⎠ = X₁ + X₂ " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ---- S3: averaging produces genuine invariant COMBINATIONS --------------\n", "X1, X2 = Scalar(\"X1\", real=True), Scalar(\"X2\", real=True)\n", "s3b = S3(); s3b.assign(\"2\", X1, X2)\n", "r1_, r2_ = X1.components[0], X2.components[0]\n", "\n", "for expr in (r1_**2, r1_ * r2_, r1_**3, r1_**2 * r2_, r1_**2 + r2_**2):\n", " display(sp.Eq(sp.Function(\"P\")(expr),\n", " sp.simplify(sp.expand(reynolds_project(expr, [s3b]))),\n", " evaluate=False))\n" ] }, { "cell_type": "markdown", "id": "61", "metadata": {}, "source": [ "**The average, done by hand.** The cell above asked `reynolds_project` for $P(r_1^2)$. Here\n", "is the formula $P(m)=\\frac1{|G|}\\sum_g g\\cdot m$ with nothing hidden: for each of the six\n", "elements of `WORDS`, take its doublet matrix, substitute $r_i\\mapsto\\sum_k\\rho_2(g)_{ik}\\,r_k$\n", "into $m=r_1^2$, and add the result to a running sum. The substitution must be\n", "*simultaneous* — sequential `subs` would apply the $r_2$ rule to the $r_2$ that the $r_1$\n", "rule just introduced — which is why the cell uses `xreplace`." ] }, { "cell_type": "code", "execution_count": null, "id": "62", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- g = () ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}1 & 0\\\\0 & 1\\end{matrix}\\right]$" ], "text/plain": [ " ⎡1 0⎤\n", "\\rho(g) = ⎢ ⎥\n", " ⎣0 1⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto X_{1}$" ], "text/plain": [ "X₁ ↦ X₁" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto X_{2}$" ], "text/plain": [ "X₂ ↦ X₂" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = X_{1}^{2}$" ], "text/plain": [ " 2\n", "g \\cdot m = X₁ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = X_{1}^{2}$" ], "text/plain": [ " 2\n", "\\mathrm{running\\ sum} = X₁ " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- g = (0,) ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ -√3 ⎤\n", " ⎢-1/2 ────⎥\n", " ⎢ 2 ⎥\n", "\\rho(g) = ⎢ ⎥\n", " ⎢ √3 ⎥\n", " ⎢ ── -1/2⎥\n", " ⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto - \\frac{X_{1}}{2} - \\frac{\\sqrt{3} X_{2}}{2}$" ], "text/plain": [ " X₁ √3⋅X₂\n", "X₁ ↦ - ── - ─────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto \\frac{\\sqrt{3} X_{1}}{2} - \\frac{X_{2}}{2}$" ], "text/plain": [ " √3⋅X₁ X₂\n", "X₂ ↦ ───── - ──\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = \\frac{X_{1}^{2}}{4} + \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "g \\cdot m = ─── + ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = \\frac{5 X_{1}^{2}}{4} + \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " 5⋅X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "\\mathrm{running\\ sum} = ───── + ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- g = (1,) ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}1 & 0\\\\0 & -1\\end{matrix}\\right]$" ], "text/plain": [ " ⎡1 0 ⎤\n", "\\rho(g) = ⎢ ⎥\n", " ⎣0 -1⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto X_{1}$" ], "text/plain": [ "X₁ ↦ X₁" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto - X_{2}$" ], "text/plain": [ "X₂ ↦ -X₂" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = X_{1}^{2}$" ], "text/plain": [ " 2\n", "g \\cdot m = X₁ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = \\frac{9 X_{1}^{2}}{4} + \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " 9⋅X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "\\mathrm{running\\ sum} = ───── + ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- g = (0, 0) ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}- \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\\\\- \\frac{\\sqrt{3}}{2} & - \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ √3 ⎤\n", " ⎢-1/2 ── ⎥\n", " ⎢ 2 ⎥\n", "\\rho(g) = ⎢ ⎥\n", " ⎢-√3 ⎥\n", " ⎢──── -1/2⎥\n", " ⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto - \\frac{X_{1}}{2} + \\frac{\\sqrt{3} X_{2}}{2}$" ], "text/plain": [ " X₁ √3⋅X₂\n", "X₁ ↦ - ── + ─────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto - \\frac{\\sqrt{3} X_{1}}{2} - \\frac{X_{2}}{2}$" ], "text/plain": [ " √3⋅X₁ X₂\n", "X₂ ↦ - ───── - ──\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = \\frac{X_{1}^{2}}{4} - \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "g \\cdot m = ─── - ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = \\frac{5 X_{1}^{2}}{2} + \\frac{3 X_{2}^{2}}{2}$" ], "text/plain": [ " 2 2\n", " 5⋅X₁ 3⋅X₂ \n", "\\mathrm{running\\ sum} = ───── + ─────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- g = (0, 1) ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}- \\frac{1}{2} & \\frac{\\sqrt{3}}{2}\\\\\\frac{\\sqrt{3}}{2} & \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ √3 ⎤\n", " ⎢-1/2 ── ⎥\n", " ⎢ 2 ⎥\n", "\\rho(g) = ⎢ ⎥\n", " ⎢ √3 ⎥\n", " ⎢ ── 1/2⎥\n", " ⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto - \\frac{X_{1}}{2} + \\frac{\\sqrt{3} X_{2}}{2}$" ], "text/plain": [ " X₁ √3⋅X₂\n", "X₁ ↦ - ── + ─────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto \\frac{\\sqrt{3} X_{1}}{2} + \\frac{X_{2}}{2}$" ], "text/plain": [ " √3⋅X₁ X₂\n", "X₂ ↦ ───── + ──\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = \\frac{X_{1}^{2}}{4} - \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "g \\cdot m = ─── - ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = \\frac{11 X_{1}^{2}}{4} - \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{9 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " 11⋅X₁ √3⋅X₁⋅X₂ 9⋅X₂ \n", "\\mathrm{running\\ sum} = ────── - ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "--- g = (1, 0) ---\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho(g) = \\left[\\begin{matrix}- \\frac{1}{2} & - \\frac{\\sqrt{3}}{2}\\\\- \\frac{\\sqrt{3}}{2} & \\frac{1}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ -√3 ⎤\n", " ⎢-1/2 ────⎥\n", " ⎢ 2 ⎥\n", "\\rho(g) = ⎢ ⎥\n", " ⎢-√3 ⎥\n", " ⎢──── 1/2 ⎥\n", " ⎣ 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{1} \\mapsto - \\frac{X_{1}}{2} - \\frac{\\sqrt{3} X_{2}}{2}$" ], "text/plain": [ " X₁ √3⋅X₂\n", "X₁ ↦ - ── - ─────\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X_{2} \\mapsto - \\frac{\\sqrt{3} X_{1}}{2} + \\frac{X_{2}}{2}$" ], "text/plain": [ " √3⋅X₁ X₂\n", "X₂ ↦ - ───── + ──\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle g \\cdot m = \\frac{X_{1}^{2}}{4} + \\frac{\\sqrt{3} X_{1} X_{2}}{2} + \\frac{3 X_{2}^{2}}{4}$" ], "text/plain": [ " 2 2\n", " X₁ √3⋅X₁⋅X₂ 3⋅X₂ \n", "g \\cdot m = ─── + ──────── + ─────\n", " 4 2 4 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\mathrm{running\\ sum} = 3 X_{1}^{2} + 3 X_{2}^{2}$" ], "text/plain": [ " 2 2\n", "\\mathrm{running\\ sum} = 3⋅X₁ + 3⋅X₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(X_{1}^{2} \\right)} = \\frac{X_{1}^{2}}{2} + \\frac{X_{2}^{2}}{2}$" ], "text/plain": [ " 2 2\n", " ⎛ 2⎞ X₁ X₂ \n", "P⎝X₁ ⎠ = ─── + ───\n", " 2 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "✓ the average by hand matches reynolds_project: P(X1^2) = (X1^2 + X2^2)/2\n" ] } ], "source": [ "# ---- peek: P(r1^2), one group element at a time ----------------------------\n", "running = sp.S.Zero\n", "for wd in WORDS: # `w` is taken: section 5's fourth doublet\n", " M = rho(\"2\", wd) # the doublet matrix for g\n", " sub = {r1_: M[0, 0] * r1_ + M[0, 1] * r2_,\n", " r2_: M[1, 0] * r1_ + M[1, 1] * r2_}\n", " term = sp.expand((r1_**2).xreplace(sub)) # simultaneous substitution\n", " running += term\n", " trace(f\"--- g = {wd} ---\", sp.Eq(sp.Symbol(r\"\\rho(g)\"), M, evaluate=False))\n", " display(MapsTo(r1_, sub[r1_]), MapsTo(r2_, sub[r2_]))\n", " display(sp.Eq(sp.Symbol(r\"g \\cdot m\"), term, evaluate=False))\n", " display(sp.Eq(sp.Symbol(r\"\\mathrm{running\\ sum}\"), running, evaluate=False))\n", "\n", "avg = sp.expand(running / G)\n", "display(sp.Eq(sp.Function(\"P\")(r1_**2), avg, evaluate=False))\n", "assert sp.expand(avg - reynolds_project(r1_**2, [s3b])) == 0\n", "ok(\"the average by hand matches reynolds_project: P(X1^2) = (X1^2 + X2^2)/2\")" ] }, { "cell_type": "markdown", "id": "63", "metadata": {}, "source": [ "Watch the running sum rather than the last line. The cross term $\\sqrt3\\,X_1X_2$ appears,\n", "flips sign and cancels between elements, and the $X_2^2$ piece is fed in by the elements\n", "that rotate $X_1$ into $X_2$. No single $g\\cdot X_1^2$ is proportional to $X_1^2+X_2^2$ —\n", "only the sum over the whole group is." ] }, { "cell_type": "markdown", "id": "64", "metadata": {}, "source": [ "Note the difference in character between the two groups. For $\\mathbb{Z}_N$ every monomial\n", "is an eigenvector of the projector — it survives whole or dies. For $S_3$ the projector\n", "*mixes* monomials: $P(r_1^2)=\\tfrac12(r_1^2+r_2^2)$ is a combination that **no single\n", "monomial realises**. That is exactly why an abelian symmetry can be enforced by\n", "bookkeeping charges, while a non-abelian one cannot.\n", "\n", "Check it against §5's Clebsch–Gordan answer: the projections must lie in the span the CG\n", "construction produced.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "65", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " quadratic : P = (1/2) x the CG invariant\n", " cubic : P = (1/4) x the CG invariant\n", "✓ the Reynolds projections are proportional to the CG invariants -- two independent routes to the same subspace\n", "✓ P^2 = P\n" ] } ], "source": [ "# ---- MOVE 4: Reynolds and Clebsch-Gordan must agree ---------------------\n", "I2_cg = cg((r1_, r2_), (r1_, r2_))[\"1\"] # r1^2 + r2^2\n", "I3_cg = sp.expand(cg((r1_, r2_), cg((r1_, r2_), (r1_, r2_))[\"2\"])[\"1\"])\n", "\n", "P2 = sp.expand(reynolds_project(r1_**2, [s3b]))\n", "P3 = sp.expand(reynolds_project(r1_**3, [s3b]))\n", "for P_, I_, name in ((P2, I2_cg, \"quadratic\"), (P3, I3_cg, \"cubic\")):\n", " ratio = sp.simplify(sp.cancel(P_ / I_))\n", " print(f\" {name:<9} : P = ({ratio}) x the CG invariant\")\n", " assert ratio.is_number and ratio != 0\n", "ok(\"the Reynolds projections are proportional to the CG invariants -- two \"\n", " \"independent routes to the same subspace\")\n", "\n", "# and the projector really is a projector: P(P(m)) = P(m)\n", "assert sp.simplify(sp.expand(reynolds_project(P2, [s3b]) - P2)) == 0\n", "ok(\"P^2 = P\")\n" ] }, { "cell_type": "markdown", "id": "66", "metadata": {}, "source": [ "---\n", "## 7. Basis dependence — and why we counted with characters\n", "\n", "The $S_3$ doublet has (at least) two bases in common use: the **real orthogonal** one this\n", "library commits to, and a **complex** one in which the $2\\pi/3$ rotation is diagonal,\n", "$\\rho(a)=\\mathrm{diag}(\\omega,\\omega^2)$ with $\\omega=e^{2\\pi i/3}$. Papers use both, often\n", "without saying which — and every Clebsch–Gordan coefficient differs between them.\n", "\n", "> **Before running the next cell.** A change of basis is $\\rho\\to U\\rho U^{-1}$. Which of\n", "> the following survive it: the individual matrix entries, the trace, the determinant, the\n", "> eigenvalues, the number of invariants? Decide before looking, then note which of those\n", "> the counting in §4 relied on.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "67", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "complex basis:\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\rho'_{2(a)} = \\left[\\begin{matrix}- \\frac{1}{2} + \\frac{\\sqrt{3} i}{2} & 0\\\\0 & - \\frac{1}{2} - \\frac{\\sqrt{3} i}{2}\\end{matrix}\\right]$" ], "text/plain": [ " ⎡ 1 √3⋅ⅈ ⎤\n", " ⎢- ─ + ──── 0 ⎥\n", " ⎢ 2 2 ⎥\n", "\\rho'₂₍ₐ₎ = ⎢ ⎥\n", " ⎢ 1 √3⋅ⅈ⎥\n", " ⎢ 0 - ─ - ────⎥\n", " ⎣ 2 2 ⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle \\rho'_{2(b)} = \\left[\\begin{matrix}0 & 1\\\\1 & 0\\end{matrix}\\right]$" ], "text/plain": [ " ⎡0 1⎤\n", "\\rho'_2(b) = ⎢ ⎥\n", " ⎣1 0⎦" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", " chi_2 (real basis) : [2, -1, 0, -1, 0, 0]\n", " chi_2 (complex basis) : [2, -1, 0, -1, 0, 0]\n", "✓ the matrices changed completely; the character did not\n" ] } ], "source": [ "# ---- the same rep, a different basis ------------------------------------\n", "U = sp.Matrix([[1, sp.I], [1, -sp.I]]) / sp.sqrt(2)\n", "a2c = sp.simplify(U * a2 * U.inv())\n", "b2c = sp.simplify(U * b2 * U.inv())\n", "print(\"complex basis:\")\n", "display(sp.Eq(sp.Symbol(r\"\\rho'_2(a)\"), a2c, evaluate=False))\n", "display(sp.Eq(sp.Symbol(r\"\\rho'_2(b)\"), b2c, evaluate=False))\n", "\n", "COMPLEX = {\"1\": S3_GENS[\"1\"], \"1p\": S3_GENS[\"1p\"], \"2\": [a2c, b2c]}\n", "chi_c = {lab: [sp.nsimplify(sp.expand(sp.trace(rho(lab, w, COMPLEX))))\n", " for w in WORDS] for lab in COMPLEX}\n", "print(\"\\n chi_2 (real basis) :\", chi[\"2\"])\n", "print(\" chi_2 (complex basis) :\", chi_c[\"2\"])\n", "assert all(sp.simplify(chi[\"2\"][i] - chi_c[\"2\"][i]) == 0 for i in range(G))\n", "ok(\"the matrices changed completely; the character did not\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "68", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 2^(x2): real basis 1 complex basis 1\n", " 2^(x3): real basis 1 complex basis 1\n", " 2^(x4): real basis 3 complex basis 3\n", "✓ identical invariant counts in both bases -- the PHYSICS cannot depend on the basis, and characters are the basis-independent way to see it\n" ] } ], "source": [ "# ---- so every count from section 4 is unchanged -------------------------\n", "def n_inv_basis(chars, n):\n", " return sp.simplify(sum(chars[i]**n for i in range(G)) / G)\n", "\n", "for n in (2, 3, 4):\n", " a_, b_ = n_inv_basis(chi[\"2\"], n), n_inv_basis(chi_c[\"2\"], n)\n", " print(f\" 2^(x{n}): real basis {a_} complex basis {b_}\")\n", " assert a_ == b_\n", "ok(\"identical invariant counts in both bases -- the PHYSICS cannot depend on \"\n", " \"the basis, and characters are the basis-independent way to see it\")\n" ] }, { "cell_type": "markdown", "id": "69", "metadata": {}, "source": [ "**The judgment:** counts are basis-independent, *coefficients are not*. When you take a\n", "Clebsch–Gordan table from a paper you are importing its basis convention along with it,\n", "and mixing two papers' tables silently gives wrong relative factors. The count is the safe\n", "thing to compare across sources; the coefficients are not.\n", "\n", "`feynlag` states its convention explicitly in `S3.doublet_product`'s docstring — the real\n", "orthogonal basis, with the $\\mathbf 2$ channel derived from $D=\\overline{w\\,v}$, where\n", "$w=x_1+ix_2$ and $v=y_1+iy_2$ package the two doublets as complex numbers. (These $w,v$\n", "are local to that formula — not §5's fourth doublet $w$, and not the $\\omega$ of §10.)\n" ] }, { "cell_type": "markdown", "id": "70", "metadata": {}, "source": [ "---\n", "## 8. Complex fields and the conjugate leg\n", "\n", "Real scalars are the easy case. A realistic model has **complex** fields, and then a term\n", "like $\\bar\\psi\\,\\psi$ has one leg transforming with $M$ and the other with something else.\n", "Getting that \"something else\" wrong is a silent error — the term looks invariant and is\n", "not.\n", "\n", "Requiring $\\sum_i \\bar\\psi'_i\\psi'_i = \\sum_i\\bar\\psi_i\\psi_i$ under\n", "$\\psi_i\\to\\sum_k M[i,k]\\psi_k$ and $\\bar\\psi_i\\to\\sum_k X[i,k]\\bar\\psi_k$ forces\n", "$X^{\\mathsf T}M=\\mathbb 1$, i.e.\n", "\n", "$$X = (M^{-1})^{\\mathsf T}.$$\n", "\n", "> **Before running the next cell.** For which kind of $M$ does $X$ coincide with $M$\n", "> itself? Check your answer against the two groups in play: $S_3$'s irreps are real\n", "> orthogonal, $\\mathbb{Z}_N$'s are complex phases. Predict which one has $X\\neq M$ — and\n", "> what would break if you used $M$ on both legs there.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "71", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "S_3 doublet:\n", " generator 0 : X == M ? True (M orthogonal? True )\n", " generator 1 : X == M ? True (M orthogonal? True )\n", "✓ for S_3, X = M exactly -- because the irreps are REAL ORTHOGONAL\n", "\n", "Z_3 charge-1 irrep:\n" ] }, { "data": { "text/latex": [ "$\\displaystyle M = e^{\\frac{2 i \\pi}{3}}$" ], "text/plain": [ " 2⋅ⅈ⋅π\n", " ─────\n", " 3 \n", "M = ℯ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle X = e^{- \\frac{2 i \\pi}{3}}$" ], "text/plain": [ " -2⋅ⅈ⋅π \n", " ───────\n", " 3 \n", "X = ℯ " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " X == conj(M)? True\n", "✓ for Z_3, X = conj(M) != M -- using M on both legs would give charge 2, not 0, and the 'invariant' would not be one\n" ] } ], "source": [ "# ---- X = (M^-1)^T, computed for both groups ------------------------------\n", "print(\"S_3 doublet:\")\n", "for gi, M in enumerate(S3_GENS[\"2\"]):\n", " X = sp.simplify(M.inv().T)\n", " print(f\" generator {gi} : X == M ?\", sp.simplify(X - M) == sp.zeros(2, 2),\n", " \" (M orthogonal?\", sp.simplify(M.T * M) == sp.eye(2), \")\")\n", " assert sp.simplify(X - M) == sp.zeros(2, 2)\n", "ok(\"for S_3, X = M exactly -- because the irreps are REAL ORTHOGONAL\")\n", "\n", "print(\"\\nZ_3 charge-1 irrep:\")\n", "M = sp.Matrix([[sp.exp(2 * sp.pi * sp.I / 3)]])\n", "X = sp.simplify(M.inv().T)\n", "display(sp.Eq(sp.Symbol(\"M\"), M[0], evaluate=False))\n", "display(sp.Eq(sp.Symbol(\"X\"), X[0], evaluate=False))\n", "print(\" X == conj(M)?\", sp.simplify(X[0] - sp.conjugate(M[0])) == 0)\n", "assert sp.simplify(X[0] - M[0]) != 0\n", "ok(\"for Z_3, X = conj(M) != M -- using M on both legs would give charge 2, \"\n", " \"not 0, and the 'invariant' would not be one\")\n" ] }, { "cell_type": "markdown", "id": "72", "metadata": {}, "source": [ "This is not a hypothetical: it is exactly what `DiscreteSymmetry.fermion_generator_data`\n", "computes, via `M.inv().T` rather than by assuming. The library derives it because the two\n", "cases genuinely differ, and the $S_3$ coincidence would otherwise hide the bug until\n", "someone used $\\mathbb{Z}_N$.\n", "\n", "Let the invariance checker demonstrate the failure directly.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "73", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle C D^{2}$" ], "text/plain": [ " 2\n", "C⋅D " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " invariant? True\n" ] }, { "data": { "text/latex": [ "$\\displaystyle C D$" ], "text/plain": [ "C⋅D" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " invariant? False\n", " residual: " ] }, { "data": { "text/latex": [ "$\\displaystyle \\frac{C D \\left(-3 - \\sqrt{3} i\\right)}{2}$" ], "text/plain": [ "C⋅D⋅(-3 - √3⋅ⅈ)\n", "───────────────\n", " 2 " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "✓ the checker accepts total charge 0 mod 3 and rejects the rest, with the offending residual shown\n" ] } ], "source": [ "# ---- a wrong assignment must FAIL the check -----------------------------\n", "C_, D_ = Scalar(\"C\", real=True), Scalar(\"D\", real=True)\n", "z3b = ZN(\"Z3b\", 3); z3b.assign(1, C_); z3b.assign(1, D_) # both charge +1\n", "sC, sD = C_.components[0], D_.components[0]\n", "\n", "good, bad = sC * sD**2, sC * sD # charges 3 = 0 mod 3, and 2 != 0\n", "for expr in (good, bad):\n", " inv, residual = check_discrete_invariance(expr, z3b)\n", " display(expr)\n", " print(\" invariant?\", inv)\n", " if not inv:\n", " print(\" residual:\", end=\" \")\n", " display(sp.simplify(sp.expand_complex(residual[0][1])))\n", "assert check_discrete_invariance(good, z3b)[0]\n", "assert not check_discrete_invariance(bad, z3b)[0]\n", "ok(\"the checker accepts total charge 0 mod 3 and rejects the rest, with the \"\n", " \"offending residual shown\")\n" ] }, { "cell_type": "markdown", "id": "74", "metadata": {}, "source": [ "---\n", "## 9. Into a model: the $S_3$ three-Higgs-doublet potential\n", "\n", "Everything so far was group theory. Now the model-building question: assign three Higgs\n", "doublets to $S_3$ as $\\mathbf 2\\oplus\\mathbf 1$ — the doublet $(H_1,H_2)$ and the singlet\n", "$H_S$ — and ask **how many terms the potential has**.\n", "\n", "The fields are complex $SU(2)$ doublets, so the potential is built from gauge-invariant\n", "bilinears $H_a^\\dagger H_b$, and both conjugate and non-conjugate legs appear. For $S_3$\n", "that costs us nothing: §8 showed $X=M$ and §3 showed the characters are real, so\n", "$\\bar\\chi=\\chi$ and the counting is unchanged. (For $\\mathbb{Z}_N$ it would not be.)\n", "\n", "> **Before running the next cell.** The library's enumerator will report a number. Before\n", "> you see it, ask what it *should* be: how many quadratic terms can you form\n", "> ($H^\\dagger H$ is degree 2 in fields), and roughly how many quartic? Then, more usefully,\n", "> ask what the symmetry has **forbidden** — a general 3HDM potential has far more\n", "> parameters than this.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "75", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "suggest_potential found 10 independent operators\n", " quadratic (mass) terms : 2\n", " quartic (coupling) : 8\n", "✓ 2 mass + 8 quartic = 10 operators -- the S3 3HDM basis\n" ] } ], "source": [ "# ---- MOVE 1: enumerate the S3-invariant potential ------------------------\n", "terms = suggest_potential([H1, H2, HS], [SU2L, U1Y], discrete_groups=[s3])\n", "print(f\"suggest_potential found {len(terms)} independent operators\")\n", "\n", "fieldsyms = [c for f in (H1, H2, HS) for c in list(f.components)]\n", "allsyms = fieldsyms + [sp.conjugate(c) for c in fieldsyms]\n", "deg = lambda e: sp.Poly(sp.expand(e), *allsyms).total_degree()\n", "quad = [t for t in terms if deg(t.expr) == 2]\n", "quart = [t for t in terms if deg(t.expr) == 4]\n", "print(\" quadratic (mass) terms :\", len(quad))\n", "print(\" quartic (coupling) :\", len(quart))\n", "assert (len(terms), len(quad), len(quart)) == (10, 2, 8)\n", "ok(\"2 mass + 8 quartic = 10 operators -- the S3 3HDM basis\")\n" ] }, { "cell_type": "markdown", "id": "76", "metadata": {}, "source": [ "Two mass terms and eight quartics. Compare that with an **unconstrained** three-Higgs-doublet\n", "potential, where every $H_a^\\dagger H_b$ pairing is allowed independently — that is where\n", "the symmetry earns its keep.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "77", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "without S3 : 33 operators\n", "with S3 : 10 operators\n", "forbidden : 23\n", "✓ imposing S3 removes 23 of the 33 parameters -- that is the entire point of a flavour symmetry\n" ] } ], "source": [ "# ---- MOVE 2 + 3: what did the symmetry forbid? --------------------------\n", "free = suggest_potential([H1, H2, HS], [SU2L, U1Y]) # no discrete group\n", "print(f\"without S3 : {len(free)} operators\")\n", "print(f\"with S3 : {len(terms)} operators\")\n", "print(f\"forbidden : {len(free) - len(terms)}\")\n", "ok(f\"imposing S3 removes {len(free) - len(terms)} of the {len(free)} parameters \"\n", " \"-- that is the entire point of a flavour symmetry\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "78", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ all 10 enumerated operators pass check_discrete_invariance independently of how they were produced\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "✓ (H1^dag H1)^2 alone is correctly REJECTED -- the doublet must appear symmetrically, so the check has teeth\n" ] } ], "source": [ "# ---- MOVE 4: every surviving term really is invariant --------------------\n", "for t in terms:\n", " inv, residual = check_discrete_invariance(sp.expand(t.expr), s3)\n", " assert inv, (t.expr, residual)\n", "ok(f\"all {len(terms)} enumerated operators pass check_discrete_invariance \"\n", " \"independently of how they were produced\")\n", "\n", "# and a deliberately S3-breaking term must be rejected\n", "bad_term = sp.expand((dag(H1) * H1.mat)[0]**2) # H1 alone, not the doublet\n", "assert not check_discrete_invariance(bad_term, s3)[0]\n", "ok(\"(H1^dag H1)^2 alone is correctly REJECTED -- the doublet must appear \"\n", " \"symmetrically, so the check has teeth\")\n" ] }, { "cell_type": "markdown", "id": "79", "metadata": {}, "source": [ "The 10-operator basis is what `THDM_S3_Tutorial` and 3HDM-$S_3$ model building in general\n", "build on: 2 mass parameters $\\mu^2$ and 8 quartics $\\lambda_{1..8}$. You now know where the\n", "number 8 comes from, and — more importantly — that you could have predicted it.\n" ] }, { "cell_type": "markdown", "id": "80", "metadata": {}, "source": [ "### 9.1 What `suggest_potential` actually does\n", "\n", "That was a black box, and it should not stay one — because it is not a new algorithm. It is\n", "the tools of §5, §6 and §8, run in sequence:\n", "\n", "| # | stage | the section it comes from |\n", "|---|---|---|\n", "| 1 | build the gauge blocks $H_a^\\dagger H_b$ | the gauge side — §9's opening paragraph |\n", "| 2 | keep only the **charge-neutral** monomials | §6: the $\\mathbb{Z}_N$ average *is* this rule |\n", "| 3 | **`reynolds_project`** onto the invariant subspace | §6 — literally the function demoed there |\n", "| 4 | hermitian completion, $\\mathcal O\\to\\mathcal O+\\mathcal O^\\dagger$ | §8, the conjugate leg |\n", "| 5 | drop the linearly dependent ones | §5: *construct, then check the rank* |\n", "| 6 | assert every survivor really is invariant | §8, and MOVE 4 above |\n", "\n", "Nothing on that list is new, so rather than take the \"10\" on trust, rebuild it. The next two\n", "cells run stages 1–5 with nothing but `dag`, `reynolds_project` and §5's own `rank_of`, and\n", "finish by comparing against `suggest_potential`'s own answer.\n", "\n", "> **Before running the next cells.** Commit to three numbers first. (i) Three doublets, and\n", "> a block is $H_a^\\dagger H_b$ — how many blocks? (ii) A potential term is one block\n", "> (dimension 2) or two (dimension 4), and $H_a^\\dagger H_b\\,H_c^\\dagger H_d$ is the same\n", "> operator whichever order you write the factors in — how many monomials? (iii) The one\n", "> worth pausing on: is the **unconstrained** potential's parameter count that monomial\n", "> number, or something smaller? Look at $H_1^\\dagger H_2$ and ask whether it can appear in a\n", "> real Lagrangian on its own.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "81", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "blocks built : 18\n", " survive the charge rule : 9 ['(H1+H1)', '(H1+H2)', '(H1+HS)', '(H2+H1)', '(H2+H2)', '(H2+HS)', '(HS+H1)', '(HS+H2)', '(HS+HS)']\n", " killed by hypercharge : 9 e.g. (H1.H1) carries Y = 1\n", "\n", "charge-neutral monomials of dimension 2 and 4 : 54\n", "✓ half the blocks die on hypercharge before S_3 is ever consulted -- 9 of 18 survive, and they generate 54 monomials\n" ] } ], "source": [ "# ---- MOVE 1: stages 1 and 2. Build the blocks, then apply the charge rule.\n", "Hs = [H1, H2, HS]\n", "\n", "blocks, dropped = {}, []\n", "for A, B in itertools.product(Hs, repeat=2):\n", " # the TWO gauge-invariant ways to pair two SU(2) doublets, with the\n", " # hypercharge each one carries\n", " pairings = [\n", " (f\"({A.name}+{B.name})\", # A^dag B\n", " sp.expand((dag(A) * B.mat)[0]),\n", " B.charge(U1Y) - A.charge(U1Y)),\n", " (f\"({A.name}.{B.name})\", # A^T (i sigma_2) B\n", " sp.expand(A.components[0] * B.components[1]\n", " - A.components[1] * B.components[0]),\n", " A.charge(U1Y) + B.charge(U1Y)),\n", " ]\n", " for label, expr, Y in pairings:\n", " if Y == 0:\n", " blocks[label] = expr\n", " else:\n", " dropped.append((label, Y))\n", "\n", "print(\"blocks built :\", len(blocks) + len(dropped))\n", "print(\" survive the charge rule :\", len(blocks), \" \", sorted(blocks))\n", "print(\" killed by hypercharge :\", len(dropped),\n", " f\" e.g. {dropped[0][0]} carries Y = {dropped[0][1]}\")\n", "\n", "# a term is one block (dim 2) or two (dim 4); order does not make a new operator\n", "mono = []\n", "for n in (1, 2):\n", " for combo in itertools.combinations_with_replacement(sorted(blocks), n):\n", " mono.append((sp.expand(sp.Mul(*[blocks[k] for k in combo])),\n", " \" \".join(combo)))\n", "print(\"\\ncharge-neutral monomials of dimension 2 and 4 :\", len(mono))\n", "\n", "assert (len(blocks), len(dropped), len(mono)) == (9, 9, 54)\n", "ok(\"half the blocks die on hypercharge before S_3 is ever consulted -- 9 of 18 \"\n", " \"survive, and they generate 54 monomials\")\n" ] }, { "cell_type": "markdown", "id": "82", "metadata": {}, "source": [ "**Open the funnel's input.** `blocks` is a dictionary from a label to the expanded\n", "component expression it stands for, and `mono` is the list of candidate operators, each\n", "paired with a label built by joining its blocks' names. Three of the fifty-four are enough\n", "to see the shape." ] }, { "cell_type": "code", "execution_count": null, "id": "83", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "({'(H1+H1)': H1_1*conjugate(H1_1) + H1_2*conjugate(H1_2),\n", " '(H1+H2)': H2_1*conjugate(H1_1) + H2_2*conjugate(H1_2),\n", " '(H1+HS)': HS_1*conjugate(H1_1) + HS_2*conjugate(H1_2),\n", " '(H2+H1)': H1_1*conjugate(H2_1) + H1_2*conjugate(H2_2),\n", " '(H2+H2)': H2_1*conjugate(H2_1) + H2_2*conjugate(H2_2),\n", " '(H2+HS)': HS_1*conjugate(H2_1) + HS_2*conjugate(H2_2),\n", " '(HS+H1)': H1_1*conjugate(HS_1) + H1_2*conjugate(HS_2),\n", " '(HS+H2)': H2_1*conjugate(HS_1) + H2_2*conjugate(HS_2),\n", " '(HS+HS)': HS_1*conjugate(HS_1) + HS_2*conjugate(HS_2)},\n", " [(H1_1*conjugate(H1_1) + H1_2*conjugate(H1_2), '(H1+H1)'),\n", " (H2_1*conjugate(H1_1) + H2_2*conjugate(H1_2), '(H1+H2)'),\n", " (HS_1*conjugate(H1_1) + HS_2*conjugate(H1_2), '(H1+HS)')])" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "blocks, mono[:3]" ] }, { "cell_type": "markdown", "id": "84", "metadata": {}, "source": [ "Those labels are not decoration. They are what lets the next cells report *which*\n", "monomials the projection killed and which got paired with a conjugate — a rank alone would\n", "say $54\\to33\\to10$ and leave you no way to check the claim against the operators you would\n", "have written by hand." ] }, { "cell_type": "code", "execution_count": null, "id": "85", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " blocks monomials after P + h.c. basis\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " with S_3 9 54 26 12 10\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " no discrete group 9 54 54 42 33\n", "✓ the hand-built funnel reproduces suggest_potential exactly -- 10 with S_3 and 33 without -- so it really is sections 5, 6 and 8 composed\n" ] } ], "source": [ "# ---- MOVE 2 + 3 + 4: project, complete, take the rank -- then check. -----\n", "def independent(exprs):\n", " # Section 5's rank_of is the right tool, but it cannot be pointed at these\n", " # directly: they carry conjugate(...) nodes, and sympy's Poly will not\n", " # accept those as generators. Map each conjugate to a Dummy -- SHARED\n", " # across every candidate, so the same conjugate becomes the same Dummy\n", " # everywhere and the coefficient rows stay comparable. (This is what\n", " # suggest._normalize_atoms does; vacuum/masses.py uses the same trick to\n", " # build charged mass matrices.) Section 5 needed none of it: real doublets.\n", " reg = {}\n", "\n", " def norm(e):\n", " e = sp.expand(e)\n", " for node in e.atoms(sp.conjugate):\n", " reg.setdefault(node, sp.Dummy())\n", " return e.xreplace(reg)\n", "\n", " rows = [norm(e) for e in exprs]\n", " gens = sorted({s for e in rows for s in e.free_symbols},\n", " key=sp.default_sort_key)\n", " return rank_of(rows, gens) # section 5's helper, unchanged\n", "\n", "\n", "def funnel(groups, verbose=False, sample=4):\n", " # stage 3: project onto the invariant subspace (a no-op with no group)\n", " projected = [(sp.expand(reynolds_project(e, groups)) if groups else e, l)\n", " for e, l in mono]\n", " kept = [(e, l) for e, l in projected if e != 0]\n", " killed = [l for e, l in projected if e == 0]\n", " # stage 4: hermitian completion. O + O^dag, unless O is already hermitian\n", " completed, hc, paired = [], 0, []\n", " for e, l in kept:\n", " if sp.expand(e - sp.conjugate(e)) == 0:\n", " completed.append(e)\n", " else:\n", " completed.append(sp.expand(e + sp.conjugate(e)))\n", " hc += 1\n", " paired.append(l)\n", " basis = independent(completed) # stage 5\n", " if verbose:\n", " trace(f\" stage 3 (project) : {len(kept)} of {len(mono)} monomials \"\n", " \"survive\")\n", " if killed:\n", " trace(\" killed, e.g. : \" + \", \".join(killed[:sample]))\n", " trace(f\" stage 4 (+ h.c.) : {hc} of {len(kept)} are not \"\n", " f\"self-conjugate and get paired with their conjugate\")\n", " if paired:\n", " trace(\" paired, e.g. : \" + \", \".join(paired[:sample]))\n", " trace(f\" stage 5 (rank) : {basis} independent operators\")\n", " return len(kept), hc, basis\n", "\n", "\n", "print(f\"{'':<20} {'blocks':>8} {'monomials':>10} {'after P':>9} \"\n", " f\"{'+ h.c.':>8} {'basis':>7}\")\n", "result = {}\n", "for name, groups in ((\"with S_3\", [s3]), (\"no discrete group\", [])):\n", " after_P, hc, basis = funnel(groups)\n", " result[name] = basis\n", " print(f\" {name:<18} {len(blocks):8d} {len(mono):10d} {after_P:9d} \"\n", " f\"{hc:8d} {basis:7d}\")\n", "\n", "assert result[\"with S_3\"] == len(terms) == 10\n", "assert result[\"no discrete group\"] == len(free) == 33\n", "ok(\"the hand-built funnel reproduces suggest_potential exactly -- 10 with S_3 \"\n", " \"and 33 without -- so it really is sections 5, 6 and 8 composed\")" ] }, { "cell_type": "code", "execution_count": null, "id": "86", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " stage 3 (project) : 54 of 54 monomials survive\n", " stage 4 (+ h.c.) : 42 of 54 are not self-conjugate and get paired with their conjugate\n", " paired, e.g. : (H1+H2), (H1+HS), (H2+H1), (H2+HS)\n", " stage 5 (rank) : 33 independent operators\n" ] } ], "source": [ "# ---- peek: what the funnel throws away, with no discrete group at all ----\n", "# This is the branch the paragraph below is about: hermiticity alone, before\n", "# S_3 is ever consulted, is what takes 54 monomials down to 33 parameters.\n", "_ = funnel([], verbose=True)" ] }, { "cell_type": "markdown", "id": "87", "metadata": {}, "source": [ "Both numbers land, from tools that were on the table by §6. Two things the funnel shows\n", "that the bare \"10\" could not.\n", "\n", "**Hermiticity is doing more work than the symmetry gets credit for.** Look at the `+ h.c.`\n", "column: without a discrete group, $42$ of the $54$ monomials are not self-conjugate and get\n", "paired with their conjugates at stage 4. That pairing is what takes $54\\to33$ — and it\n", "happens *before* any flavour symmetry is imposed. $H_1^\\dagger H_2$ cannot stand alone in a\n", "real Lagrangian; it needs $H_2^\\dagger H_1$ beside it, and the two together are one\n", "parameter, not two. So the reduction is really two steps: **hermiticity $54\\to33$, then\n", "$S_3$ $33\\to10$.** The cell in §9 reports only the second.\n", "\n", "**Stage 2 is not a formality.** MOVE 1 built $18$ blocks, not $9$ — the other nine are the\n", "$SU(2)$ $\\varepsilon$-contractions $A^{\\mathsf T}(i\\sigma_2)B$, which are every bit as\n", "gauge-invariant. Every one carries $Y_A+Y_B=1\\neq0$ for three $Y=\\tfrac12$ doublets, so the\n", "charge rule deletes all nine before $S_3$ is consulted. That is §6's abelian selection rule\n", "removing half the candidates on its own. (In a model with a $Y=-\\tfrac12$ doublet they would\n", "survive, and the potential would be larger.)\n", "\n", "Stage 6 is also why §9's MOVE 4 was worth running. The enumerator already applies the\n", "invariance oracle to its own output — so re-checking each term with\n", "`check_discrete_invariance` is not repetition, it is confirming the answer does not depend\n", "on the route that produced it. If the projector of stage 3 and the checker's generator\n", "substitution ever disagreed, that cell is where it would surface.\n" ] }, { "cell_type": "markdown", "id": "88", "metadata": {}, "source": [ "### 9.2 The same count, for a Yukawa term\n", "\n", "§9 counted the potential, where every factor was an $H_a^\\dagger H_b$ block built from\n", "scalars in the *same* pair of irreps. A **Yukawa** term is the more general problem, and\n", "the one flavour model building actually runs into:\n", "\n", "$$\\mathcal L_Y \\supset \\bar L\\,H\\,e_R ,$$\n", "\n", "three *different* multiplets, each free to sit in $\\mathbf1$ or $\\mathbf2$ independently.\n", "Nothing so far has counted that — §4 did $n$ copies of one irrep, §9 did the potential.\n", "\n", "You already have the tool, though. §4's `n_invariants` takes a **list** of characters, one\n", "per factor; it was only ever handed $n$ copies of the same one. Hand it three different\n", "ones and it answers the Yukawa question unchanged:\n", "\n", "$$n_{\\text{inv}}\\big(r_{\\bar L}\\otimes r_H\\otimes r_{e_R}\\big)\n", "=\\frac1{|G|}\\sum_g \\chi_{\\bar L}(g)\\,\\chi_H(g)\\,\\chi_{e_R}(g).$$\n", "\n", "> **Before running the next cell.** There are $2^3=8$ ways to assign\n", "> $(\\bar L,H,e_R)$ to $\\{\\mathbf2,\\mathbf1\\}$. Predict how many carry an invariant. Two\n", "> hints: $\\mathbf1\\otimes\\mathbf1\\otimes\\mathbf1$ obviously does, and any assignment with\n", "> **exactly one** $\\mathbf2$ cannot — a lone doublet has no invariant component, which is\n", "> $\\langle\\chi_2,\\chi_1\\rangle=0$ from §3. How many does that leave, and are they all\n", "> non-zero?\n", "\n", "Nothing about the fermions enters the count. $\\bar L$ contributes a conjugate leg, but §3\n", "showed $S_3$'s characters are **real**, so $\\bar\\chi=\\chi$ and the arithmetic is the same\n", "one §4 ran — exactly the observation §9 opened with.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "89", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(Lbar, H, eR) # invariants\n", " (2 2 2 ) 1\n", " (2 2 1 ) 1\n", " (2 1 2 ) 1\n", " (2 1 1 ) 0\n", " (1 2 2 ) 1\n", " (1 2 1 ) 0\n", " (1 1 2 ) 0\n", " (1 1 1 ) 1\n", "\n", " total S3-invariant Yukawa structures : 5\n", " the same count if 1' is also allowed : 11 (over 27 assignments)\n", "✓ five Yukawa structures from 1 and 2 alone -- and the (2,2,2) row is the (1/6)[1*8 + 3*0 + 2*(-1)] = 1 that a 3HDM-S3 lepton sector relies on\n" ] } ], "source": [ "# ---- the Yukawa problem: THREE multiplets, in MIXED irreps ---------------\n", "# n_invariants already accepts a list of characters -- section 4 simply never\n", "# called it with different ones. No new machinery.\n", "print(f\"{'(Lbar, H, eR)':<18} # invariants\")\n", "yuk = {}\n", "for combo in itertools.product((\"2\", \"1\"), repeat=3):\n", " yuk[combo] = n_invariants([chi[r] for r in combo])\n", " c1, c2, c3 = combo\n", " print(f\" ({c1:<2} {c2:<2} {c3:<2}){'':>6} {yuk[combo]}\")\n", "\n", "total = sum(yuk.values())\n", "print(\"\\n total S3-invariant Yukawa structures :\", total)\n", "assert total == 5\n", "assert yuk[(\"2\", \"2\", \"2\")] == 1 # the multiplicity 3HDM-S3 models rely on\n", "assert all(v == 0 for k, v in yuk.items() if list(k).count(\"2\") == 1)\n", "\n", "# and the five are a model-building CHOICE: allowing 1' as well opens up more\n", "with_1p = sum(n_invariants([chi[r] for r in c])\n", " for c in itertools.product((\"1\", \"1p\", \"2\"), repeat=3))\n", "print(\" the same count if 1' is also allowed :\", with_1p, \"(over 27 assignments)\")\n", "\n", "ok(\"five Yukawa structures from 1 and 2 alone -- and the (2,2,2) row is the \"\n", " \"(1/6)[1*8 + 3*0 + 2*(-1)] = 1 that a 3HDM-S3 lepton sector relies on\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "90", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " class rep size prod_i chi_i(g)\n", " () 1 8\n", " (0,) 2 -1\n", " (1,) 3 0\n", " (1/6)[1*8 + 2*-1 + 3*0] = 1\n" ] }, { "data": { "text/latex": [ "$\\displaystyle 1$" ], "text/plain": [ "1" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ---- peek: the (2,2,2) row, the one a 3HDM-S3 lepton sector relies on\n", "n_invariants([chi[\"2\"]] * 3, verbose=True, classes=classes)" ] }, { "cell_type": "markdown", "id": "91", "metadata": {}, "source": [ "Five, and each is a term someone has to write down. Before the table, the notation — it\n", "packs three different contractions into two symbols, and it is worth unpacking once.\n", "\n", "> **Notation for this table.**\n", "> Everything here acts on the **$S_3$ index only**. The $SU(2)$ contraction and the spinor\n", "> sandwich are the same in every row, so they are suppressed.\n", ">\n", "> *Fields.* $\\bar L=(\\bar L_1,\\bar L_2)$, $H=(H_1,H_2)$ and $e_R=(e_{1R},e_{2R})$ are\n", "> $S_3$ doublets; $\\bar L_S$, $H_S$ and $e_{SR}$ are the singlets (the $S$ is the one in\n", "> `HS` in the code).\n", ">\n", "> *One operation, two shorthands* — both from §5:\n", "> - $\\{a\\otimes b\\}_{\\mathbf r}$ is channel $\\mathbf r$ of $\\mathbf2\\otimes\\mathbf2$, i.e.\n", "> `S3.doublet_product(a, b)[\"r\"]`. The $\\mathbf2$ channel is itself a doublet,\n", "> $\\{a\\otimes b\\}_{\\mathbf2}=\\big(a_1b_1-a_2b_2,\\;-(a_1b_2+a_2b_1)\\big)$.\n", "> - $a\\cdot b\\equiv\\{a\\otimes b\\}_{\\mathbf1}=a_1b_1+a_2b_2$. The dot is **just the singlet\n", "> channel**, so it never needs a subscript.\n", ">\n", "> The literature often writes $[\\,\\cdot\\,]_{\\mathbf r}$ for the same braces, and\n", "> $(\\bar L\\cdot H)_{\\mathbf1}$ for what is here simply $\\bar L\\cdot H$." ] }, { "cell_type": "markdown", "id": "92", "metadata": {}, "source": [ "| $(\\bar L, H, e_R)$ | the invariant | written out |\n", "|---|---|---|\n", "| $(\\mathbf2,\\mathbf2,\\mathbf2)$ | $\\{\\bar L\\otimes H\\}_{\\mathbf2}\\cdot e_R$ | $(\\bar L_1H_1-\\bar L_2H_2)\\,e_{1R}-(\\bar L_1H_2+\\bar L_2H_1)\\,e_{2R}$ |\n", "| $(\\mathbf2,\\mathbf2,\\mathbf1)$ | $(\\bar L\\cdot H)\\,e_{SR}$ | $(\\bar L_1H_1+\\bar L_2H_2)\\,e_{SR}$ |\n", "| $(\\mathbf2,\\mathbf1,\\mathbf2)$ | $(\\bar L\\cdot e_R)\\,H_S$ | $\\bar L_1H_Se_{1R}+\\bar L_2H_Se_{2R}$ |\n", "| $(\\mathbf1,\\mathbf2,\\mathbf2)$ | $\\bar L_S\\,(H\\cdot e_R)$ | $\\bar L_S\\,(H_1e_{1R}+H_2e_{2R})$ |\n", "| $(\\mathbf1,\\mathbf1,\\mathbf1)$ | $\\bar L_S H_S e_{SR}$ | $\\bar L_S H_S e_{SR}$ |\n", "\n", "Read the middle column by counting doublets. Two doublets and a singlet: dot the doublets,\n", "and the singlet rides along. Three doublets: there is nothing to dot all three with, so\n", "fuse two into a new doublet with $\\{\\,\\otimes\\,\\}_{\\mathbf2}$, then dot that with the\n", "third. Only the first row takes two steps — and it raises the obvious question of whether\n", "it matters *which* two you fuse. The next cell answers that, and one more thing." ] }, { "cell_type": "code", "execution_count": null, "id": "93", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " {Lb x H}_2 . eR : H1*Lb1*e1 - H1*Lb2*e2 - H2*Lb1*e2 - H2*Lb2*e1\n", " Lb . {H x eR}_2 : H1*Lb1*e1 - H1*Lb2*e2 - H2*Lb1*e2 - H2*Lb2*e1\n", " {Lb x eR}_2 . H : H1*Lb1*e1 - H1*Lb2*e2 - H2*Lb1*e2 - H2*Lb2*e1\n", "\n", " nonzero C_ijk : {(1, 1, 1): 1, (1, 2, 2): -1, (2, 1, 2): -1, (2, 2, 1): -1}\n" ] }, { "data": { "text/latex": [ "$\\displaystyle \\{X\\otimes X\\}_{2}\\cdot X = X_{1}^{3} - 3 X_{1} X_{2}^{2}$" ], "text/plain": [ " 3 2\n", "\\{X\\otimes X\\}_{2}\\cdot X = X₁ - 3⋅X₁⋅X₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " rotate by 2*pi/3 : dot changes by 0, cubic changes by 0\n", " rotate by pi/2 : dot changes by 0, cubic changes by -X1**3 + 3*X1**2*X2 + 3*X1*X2**2 - X2**3\n", "✓ the (2,2,2) invariant is one symmetric tensor, fusion order is irrelevant, and it is the cubic Re(z^3) -- invariant under S3's rotations, not under all of them\n" ] } ], "source": [ "# ---- peek: the (2,2,2) invariant, taken apart ------------------------------\n", "Lb_, H_, eR_ = sp.symbols(\"Lb1 Lb2\"), sp.symbols(\"H1 H2\"), sp.symbols(\"e1 e2\")\n", "dot = lambda a, b: cg(a, b)[\"1\"]\n", "fuse2 = lambda a, b: cg(a, b)[\"2\"]\n", "\n", "# (1) the dot IS the singlet channel\n", "assert sp.expand(dot(Lb_, H_) - (Lb_[0] * H_[0] + Lb_[1] * H_[1])) == 0\n", "\n", "# (2) fuse any two first -- the invariant does not care\n", "orders = {\"{Lb x H}_2 . eR\": dot(fuse2(Lb_, H_), eR_),\n", " \"Lb . {H x eR}_2\": dot(Lb_, fuse2(H_, eR_)),\n", " \"{Lb x eR}_2 . H\": dot(fuse2(Lb_, eR_), H_)}\n", "T2 = sp.expand(orders[\"{Lb x H}_2 . eR\"])\n", "for label, expr in orders.items():\n", " print(f\" {label:<18}:\", sp.expand(expr))\n", " assert sp.expand(expr - T2) == 0\n", "\n", "# (3) ...because its coefficient tensor C_ijk is fully symmetric\n", "poly = sp.Poly(T2, *Lb_, *H_, *eR_)\n", "Cijk = {(i + 1, j + 1, k + 1): poly.coeff_monomial(Lb_[i] * H_[j] * eR_[k])\n", " for i, j, k in itertools.product(range(2), repeat=3)}\n", "print(\"\\n nonzero C_ijk :\", {ijk: c for ijk, c in Cijk.items() if c != 0})\n", "assert all(Cijk[p] == c for ijk, c in Cijk.items()\n", " for p in itertools.permutations(ijk))\n", "\n", "# (4) all three legs on ONE doublet: the cubic invariant section 6 projected\n", "cubic = sp.expand(dot(fuse2((r1_, r2_), (r1_, r2_)), (r1_, r2_)))\n", "display(sp.Eq(sp.Symbol(r\"\\{X\\otimes X\\}_{2}\\cdot X\"), cubic, evaluate=False))\n", "assert sp.expand(cubic - 4 * reynolds_project(r1_**3, [s3b])) == 0\n", "\n", "# (5) the dot survives ANY rotation; the cubic only S3's 120-degree ones\n", "def rotated_change(expr, theta):\n", " c, s = sp.cos(theta), sp.sin(theta)\n", " return sp.simplify(sp.expand(\n", " expr.xreplace({r1_: c * r1_ - s * r2_, r2_: s * r1_ + c * r2_}) - expr))\n", "\n", "square = sp.expand(dot((r1_, r2_), (r1_, r2_)))\n", "for theta in (2 * sp.pi / 3, sp.pi / 2):\n", " print(f\" rotate by {str(theta):<8}: dot changes by {rotated_change(square, theta)},\"\n", " f\" cubic changes by {rotated_change(cubic, theta)}\")\n", "assert rotated_change(square, sp.pi / 2) == 0\n", "assert rotated_change(cubic, 2 * sp.pi / 3) == 0\n", "assert rotated_change(cubic, sp.pi / 2) != 0\n", "ok(\"the (2,2,2) invariant is one symmetric tensor, fusion order is irrelevant, and \"\n", " \"it is the cubic Re(z^3) -- invariant under S3's rotations, not under all of them\")" ] }, { "cell_type": "markdown", "id": "94", "metadata": {}, "source": [ "Three things to read off, each a callback:\n", "\n", "- **The all-doublet row needs §5, but not a choice.** The invariant is a single, fully\n", " symmetric 3-index tensor, $C_{111}=1$ and $C_{122}=C_{212}=C_{221}=-1$. Any pairing of\n", " the legs reads off the same tensor, so \"fuse two, keep the $\\mathbf2$, dot with the\n", " third\" is a *recipe for finding* it, not a decision that changes it. The count above is\n", " the other half of the guarantee: multiplicity one means there is nothing else to find.\n", "- **Only that row is basis-sensitive (§7).** Write a doublet as $z=x_1+ix_2$. The dot is\n", " $\\lvert z\\rvert^2$, untouched by *every* rotation, so the four dot rows look the same in\n", " any orthogonal doublet basis. The triple structure, with all legs set equal, is\n", " $x_1^3-3x_1x_2^2=\\mathrm{Re}\\,z^3$ — the very cubic §6 projected — and a rotation by\n", " $\\theta$ multiplies $z^3$ by $e^{3i\\theta}$. It is invariant only for $S_3$'s own\n", " $120^\\circ$ rotations, so its coefficients depend on how the doublet basis sits relative\n", " to them: the fermionic analogue of $\\lambda_4$ in the potential. Counts transfer between\n", " papers; these coefficients do not.\n", "- **$\\mathbf1'$ was a choice.** Allowing it too gives 11 invariants over 27 assignments. A\n", " $\\mathbf1'$ field would also need the third channel,\n", " $\\{a\\otimes b\\}_{\\mathbf1'}=a_1b_2-a_2b_1$, which no dot covers. Assigning every field to\n", " $\\mathbf1$ or $\\mathbf2$ is a model-building decision, and the number five is downstream\n", " of it.\n", "\n", "This is where a real model starts. A 3HDM-$S_3$ lepton sector writes one Yukawa coupling\n", "per structure — five, not the $3\\times3\\times3$ a flavour-blind model would allow. Check\n", "each with `check_discrete_invariance`, and confirm the count independently with\n", "`suggest_yukawa`, the fermionic sibling of the pipeline §9.1 walked through. And when\n", "importing the all-doublet structure from a paper, transcribe it *through* its basis: written\n", "in the other doublet basis, it **fails** the invariance check here — the second bullet\n", "above, biting in practice." ] }, { "cell_type": "markdown", "id": "95", "metadata": {}, "source": [ "---\n", "## 10. Capstone: $A_4$ from scratch\n", "\n", "$S_3$ is the warm-up. The workhorse of flavour model building is $A_4$ — the even\n", "permutations of four objects, the rotation group of the tetrahedron, order 12 — because it\n", "has a **3-dimensional irrep** that can hold three generations at once.\n", "\n", "`feynlag` ships only $\\mathbb{Z}_N$ and $S_3$. But `DiscreteSymmetry` advertises itself as\n", "extensible, and the contract is small: supply `_irrep_generators`, one matrix per\n", "generator per irrep. Let us hold it to that.\n", "\n", "$A_4$ is presented by $S^2=T^3=(ST)^3=e$, with irreps $\\mathbf 1,\\mathbf 1',\\mathbf 1'',\n", "\\mathbf 3$ (note $1+1+1+9=12$ ✓). In the standard basis\n", "\n", "$$S=\\mathrm{diag}(1,-1,-1),\\qquad T=\\begin{pmatrix}0&1&0\\\\0&0&1\\\\1&0&0\\end{pmatrix},$$\n", "\n", "and the three singlets are distinguished by $T\\to 1,\\omega,\\omega^2$.\n", "\n", "> **Before running the next cells.** $A_4$ has four irreps, so four conjugacy classes.\n", "> Predict $\\chi_3$: $\\chi_3(e)=3$ certainly. $S$ is a double transposition — what is\n", "> $\\mathrm{Tr}\\,S$? $T$ is a 3-cycle, and a cyclic permutation matrix has trace equal to\n", "> the number of fixed points — how many does it have? Fill in the row before you look.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "96", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " S^2 = identity ✓\n", " T^3 = identity ✓\n", " (ST)^3 = identity ✓\n", "✓ S^2 = T^3 = (ST)^3 = e -- the presentation of A_4\n" ] } ], "source": [ "# ---- MOVE 1: define A4 on top of the shipped base class -----------------\n", "omega = sp.exp(2 * sp.pi * sp.I / 3) # `w` is taken: section 5's fourth doublet\n", "S_ = sp.Matrix([[1, 0, 0], [0, -1, 0], [0, 0, -1]])\n", "T_ = sp.Matrix([[0, 1, 0], [0, 0, 1], [1, 0, 0]])\n", "\n", "\n", "class A4(DiscreteSymmetry):\n", " # The alternating group A_4: irreps 1, 1', 1'' and 3.\n", "\n", " def __init__(self, name=\"A4\"):\n", " super().__init__(name)\n", " self._irrep_generators = {\n", " \"1\": [sp.Matrix([[1]]), sp.Matrix([[1]])],\n", " \"1p\": [sp.Matrix([[1]]), sp.Matrix([[omega]])],\n", " \"1pp\": [sp.Matrix([[1]]), sp.Matrix([[omega**2]])],\n", " \"3\": [S_, T_],\n", " }\n", "\n", "\n", "A4_GENS = A4()._irrep_generators\n", "for name, M, n in ((\"S^2\", S_**2, 2), (\"T^3\", T_**3, 3), (\"(ST)^3\", (S_ * T_)**3, 3)):\n", " assert sp.simplify(M) == sp.eye(3), name\n", " print(f\" {name:<7} = identity ✓\")\n", "ok(\"S^2 = T^3 = (ST)^3 = e -- the presentation of A_4\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "97", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "|A_4| = 12\n", "✓ order 12, and sum (dim r)^2 = 1+1+1+9 = 12 -- the irrep list is complete\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "conjugacy classes: [1, 3, 4, 4] (sizes)\n", "\n", " e S (double transp.) T (3-cycle) T^2 (3-cycle) \n", "------------------------------------------------------------------------------------------------------\n", "1 1 1 1 1 \n", "1p 1 1 -1/2 + sqrt(3)*I/2 -1/2 - sqrt(3)*I/2 \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "1pp 1 1 -1/2 - sqrt(3)*I/2 -1/2 + sqrt(3)*I/2 \n", "3 3 -1 0 0 \n", "✓ A_4's characters are constant on its 4 conjugacy classes\n" ] } ], "source": [ "# ---- MOVE 2: close it, and derive the character table -------------------\n", "W4 = close_group(A4_GENS[\"3\"])\n", "G4 = len(W4)\n", "print(\"|A_4| =\", G4)\n", "assert G4 == 12\n", "assert sum(A4_GENS[l][0].shape[0]**2 for l in A4_GENS) == G4\n", "ok(\"order 12, and sum (dim r)^2 = 1+1+1+9 = 12 -- the irrep list is complete\")\n", "\n", "chi4 = {lab: [sp.nsimplify(sp.expand(sp.trace(rho(lab, wd, A4_GENS))))\n", " for wd in W4] for lab in A4_GENS}\n", "\n", "classes4 = conjugacy_classes(W4, A4_GENS, \"3\")\n", "print(\"\\nconjugacy classes:\", [len(cl) for cl in classes4], \" (sizes)\")\n", "names4 = [\"e\", \"S (double transp.)\", \"T (3-cycle)\", \"T^2 (3-cycle)\"]\n", "hdr4 = f\"{'':<6}\" + \"\".join(f\"{n:<24}\" for n in names4)\n", "print(); print(hdr4); print(\"-\" * len(hdr4))\n", "for lab in (\"1\", \"1p\", \"1pp\", \"3\"):\n", " row = f\"{lab:<6}\"\n", " for cl in classes4:\n", " val = sp.simplify(sp.expand_complex(chi4[lab][W4.index(cl[0])]))\n", " row += f\"{val!s:<24}\"\n", " print(row)\n", "\n", "for lab in A4_GENS: # constant on classes, as before\n", " for cl in classes4:\n", " vals = {sp.simplify(chi4[lab][W4.index(w)] - chi4[lab][W4.index(cl[0])])\n", " for w in cl}\n", " assert vals == {0}, (lab, cl)\n", "ok(\"A_4's characters are constant on its 4 conjugacy classes\")\n" ] }, { "cell_type": "markdown", "id": "98", "metadata": {}, "source": [ "**Why the shipped helpers accepted a group they had never seen.** Put $A_4$'s\n", "bookkeeping next to $S_3$'s from §3 — the conjugacy classes, and the characters of\n", "the three-dimensional irrep." ] }, { "cell_type": "code", "execution_count": null, "id": "99", "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$\\displaystyle \\left( \\left[ \\left[ \\left( \\right)\\right], \\ \\left[ \\left( 0,\\right), \\ \\left( 1, \\ 0, \\ 1, \\ 1\\right), \\ \\left( 1, \\ 1, \\ 0, \\ 1\\right)\\right], \\ \\left[ \\left( 1,\\right), \\ \\left( 0, \\ 1\\right), \\ \\left( 1, \\ 0\\right), \\ \\left( 0, \\ 1, \\ 0\\right)\\right], \\ \\left[ \\left( 1, \\ 1\\right), \\ \\left( 0, \\ 1, \\ 1\\right), \\ \\left( 1, \\ 0, \\ 1\\right), \\ \\left( 1, \\ 1, \\ 0\\right)\\right]\\right], \\ \\left[ 3, \\ -1, \\ 0, \\ 0, \\ 0, \\ 0, \\ 0, \\ 0, \\ 0, \\ 0, \\ -1, \\ -1\\right]\\right)$" ], "text/plain": [ "([[()], [(0,), (1, 0, 1, 1), (1, 1, 0, 1)], [(1,), (0, 1), (1, 0), (0, 1, 0)], ↪\n", "\n", "↪ [(1, 1), (0, 1, 1), (1, 0, 1), (1, 1, 0)]], [3, -1, 0, 0, 0, 0, 0, 0, 0, 0, ↪\n", "\n", "↪ -1, -1])" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "classes4, chi4[\"3\"]" ] }, { "cell_type": "markdown", "id": "100", "metadata": {}, "source": [ "Same two shapes as before: a list of lists of words, and one character per *element*,\n", "indexed by the closure order `W4` rather than by class. Nothing in `close_group`, `rho`\n", "or `conjugacy_classes` ever asked which group it was holding, so supplying\n", "`_irrep_generators` really was the whole contract." ] }, { "cell_type": "code", "execution_count": null, "id": "101", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ = delta_rs for all 16 pairs -- A_4's character table is consistent, and nothing about it was typed in\n", "\n", " invariants in 3^(x2) (distinct triplets) : 1\n", " invariants in 3^(x3) (distinct triplets) : 2\n", " invariants in 3^(x4) (distinct triplets) : 7\n" ] } ], "source": [ "# ---- MOVE 4: orthonormality, on a group the library never saw -----------\n", "inner4 = lambda p, q: sp.simplify(\n", " sum(sp.conjugate(chi4[p][i]) * chi4[q][i] for i in range(G4)) / G4)\n", "for p in A4_GENS:\n", " for q in A4_GENS:\n", " assert inner4(p, q) == (1 if p == q else 0), (p, q, inner4(p, q))\n", "ok(\" = delta_rs for all 16 pairs -- A_4's character table is \"\n", " \"consistent, and nothing about it was typed in\")\n", "\n", "print()\n", "for n in (2, 3, 4):\n", " n_inv = sp.simplify(sum(chi4[\"3\"][i]**n for i in range(G4)) / G4)\n", " print(f\" invariants in 3^(x{n}) (distinct triplets) : {n_inv}\")\n" ] }, { "cell_type": "markdown", "id": "102", "metadata": {}, "source": [ "$\\chi_3=(3,-1,0,0)$: the double transposition $S$ has trace $-1$, and the 3-cycles are\n", "traceless because a 3-cycle permutation matrix has no fixed points.\n", "\n", "The counts are the reason $A_4$ is the flavour group of choice: **one** invariant in\n", "$\\mathbf 3\\otimes\\mathbf 3$ (so a mass term exists), and **two** in\n", "$\\mathbf 3\\otimes\\mathbf 3\\otimes\\mathbf 3$ — the symmetric and antisymmetric cubics, which\n", "is exactly the freedom a Yukawa sector needs to generate non-trivial mixing.\n", "\n", "Finally: do the *shipped tools* work on this group they have never met?\n" ] }, { "cell_type": "code", "execution_count": null, "id": "103", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " check_discrete_invariance(F1^2 + F2^2 + F3^2) : True\n", " check_discrete_invariance(F1^2) : False residual: " ] }, { "data": { "text/latex": [ "$\\displaystyle - F_{1}^{2} + F_{2}^{2}$" ], "text/plain": [ " 2 2\n", "- F₁ + F₂ " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(F_{1}^{2} \\right)} = \\frac{F_{1}^{2}}{3} + \\frac{F_{2}^{2}}{3} + \\frac{F_{3}^{2}}{3}$" ], "text/plain": [ " 2 2 2\n", " ⎛ 2⎞ F₁ F₂ F₃ \n", "P⎝F₁ ⎠ = ─── + ─── + ───\n", " 3 3 3 " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/latex": [ "$\\displaystyle P{\\left(F_{1} F_{2} \\right)} = 0$" ], "text/plain": [ "P(F₁⋅F₂) = 0" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "✓ check_discrete_invariance and reynolds_project both work on A_4 with no library change -- the projector finds (F1^2+F2^2+F3^2)/3 and kills F1 F2\n" ] } ], "source": [ "# ---- the library's machinery, unchanged, on A4 --------------------------\n", "a4 = A4()\n", "F1, F2, F3 = (Scalar(n, real=True) for n in (\"F1\", \"F2\", \"F3\"))\n", "a4.assign(\"3\", F1, F2, F3)\n", "f1, f2, f3 = (f.components[0] for f in (F1, F2, F3))\n", "\n", "inv, _ = check_discrete_invariance(f1**2 + f2**2 + f3**2, a4)\n", "notinv, residual = check_discrete_invariance(f1**2, a4)\n", "print(\" check_discrete_invariance(F1^2 + F2^2 + F3^2) :\", inv)\n", "print(\" check_discrete_invariance(F1^2) :\", notinv,\n", " \" residual:\", end=\" \")\n", "display(sp.simplify(residual[0][1]))\n", "assert inv and not notinv\n", "\n", "display(sp.Eq(sp.Function(\"P\")(f1**2),\n", " sp.simplify(reynolds_project(f1**2, [a4])), evaluate=False))\n", "display(sp.Eq(sp.Function(\"P\")(f1 * f2),\n", " sp.simplify(reynolds_project(f1 * f2, [a4])), evaluate=False))\n", "assert sp.simplify(reynolds_project(f1 * f2, [a4])) == 0\n", "ok(\"check_discrete_invariance and reynolds_project both work on A_4 with no \"\n", " \"library change -- the projector finds (F1^2+F2^2+F3^2)/3 and kills F1 F2\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "104", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " projecting all 6 quadratic monomials: 3 survive, rank 1\n", " characters predicted: 1\n", "✓ one quadratic invariant, predicted and constructed -- the same loop as section 5, on a brand-new group\n" ] } ], "source": [ "# ---- and the count agrees with explicit construction --------------------\n", "quad_inv = sp.expand(reynolds_project(f1**2, [a4]))\n", "n_quad = sp.simplify(sum(chi4[\"3\"][i]**2 for i in range(G4)) / G4)\n", "basis = [sp.expand(reynolds_project(m, [a4]))\n", " for m in (f1**2, f2**2, f3**2, f1*f2, f1*f3, f2*f3)]\n", "nz = [b for b in basis if b != 0]\n", "r = sp.Matrix([[sp.Poly(b, f1, f2, f3).coeff_monomial(m)\n", " for m in sorted({m for e in nz\n", " for m in sp.Poly(e, f1, f2, f3).monoms()})]\n", " for b in nz]).rank()\n", "print(f\" projecting all 6 quadratic monomials: {len(nz)} survive, rank {r}\")\n", "print(\" characters predicted:\", n_quad)\n", "assert r == n_quad == 1\n", "ok(\"one quadratic invariant, predicted and constructed -- the same loop as \"\n", " \"section 5, on a brand-new group\")\n" ] }, { "cell_type": "markdown", "id": "105", "metadata": {}, "source": [ "---\n", "## 11. Recap\n", "\n", "**The mechanics.**\n", "\n", "1. A finite group is its generators; closing them under multiplication regenerates it.\n", " Checking invariance under generators *is* checking it under the group.\n", "2. A representation is any $\\rho$ with $\\rho(g)\\rho(h)=\\rho(gh)$ — injectivity not required.\n", " The trivial rep is \"invariant\"; the sign rep $\\mathbf 1'$ is not.\n", "3. Characters are traces. Basis-independent, constant on classes, orthonormal:\n", " $\\sum_r(\\dim r)^2=|G|$ and #irreps = #classes tell you when your list is complete.\n", "\n", "**The tools, in the order you should reach for them.**\n", "\n", "| question | tool |\n", "|---|---|\n", "| how many invariants, $n$ *distinct* multiplets? | $\\frac1{\\|G\\|}\\sum_g\\prod_i\\chi_i(g)$ |\n", "| how many, from **one** multiplet? | the Molien series |\n", "| what are they, explicitly? | Clebsch–Gordan (`S3.doublet_product`) |\n", "| …without a CG table? | the Reynolds projector (`reynolds_project`) |\n", "| is this term invariant? | `check_discrete_invariance` |\n", "| what is the whole basis? | `suggest_potential(..., discrete_groups=[...])` — stages 2–5 of §9.1, i.e. the four rows above it, composed |\n", "\n", "**The four traps**, each of which produced a wrong answer above before the check caught it:\n", "\n", "- **$\\mathbf 1'$ vanishes on a repeated multiplet.** One $S_3$ doublet forms no\n", " antisymmetric singlet with itself.\n", "- **Distinct legs versus one field.** Four distinct doublets give 3 quartic invariants;\n", " one doublet gives 1. Use characters for the first, Molien for the second.\n", "- **Basis conventions.** CG coefficients differ between the real and complex $S_3$ bases;\n", " counts do not. Compare counts across papers, never coefficients.\n", "- **The conjugate leg.** $X=(M^{-1})^{\\mathsf T}$, which equals $M$ only for real\n", " orthogonal irreps. True for $S_3$, false for $\\mathbb{Z}_N$ — so the $S_3$ coincidence\n", " hides the error.\n", "\n", "**And the model-building punchline.** Imposing $S_3$ on three Higgs doublets cuts the\n", "potential to 2 mass terms and 8 quartics; the forbidden operators are what the symmetry\n", "buys you. You can compute that number from the character table before writing a single\n", "term — and `A_4`, a group the library does not ship, needed no library change at all.\n", "\n", "Where to go next: `THDM_S3_Tutorial` builds the 3HDM-$S_3$ potential from these\n", "invariants and breaks the symmetry; `ModelBuilding_Tutorial` does the same enumeration\n", "for continuous $U(1)$ charges with anomaly cancellation; `SUN_Groups_Tutorial` is the\n", "continuous-group counterpart to this notebook.\n" ] } ], "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 }