{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# How to Continue Making a Wormhole\n", "\n", "## Part 2: Majorana Fermions and SYK" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To approach SYK (the Sachdev-Ye-Kitaev) model, first we need to recall about fermions.\n", "\n", "As you may know, we (often) classify particles in terms of whether they are bosons or fermions. Generally speaking, bosons are like force particles: examples are the photon, the gluon. Any number of bosons can be in the same state: hence, a laser beam. The bosonic particles are in themselves indistinguishable: they are permutation symmetric. We can use quantum harmonic oscillators to easily keep track of bosonic degrees of freedom. We assign a harmonic oscillator to each degree of freedom of the particle, and the number operator of each oscillator keeps track of how many particles are in *that state*. The use of harmonic oscillators automatically imposes for us that the particles be permutation symmetric. \n", "\n", "For each oscillator, we have \"creation\" and \"annihilation\" operators, which add and subtract quanta from each oscillator--and although they are not themselves hermitian, they can be used as building blocks for hermitian operators. If we have multiple oscillators, we can just tensor these creation and annihilation operators with identities so they act on the appropriate subspaces. These operators obey the following commutation relations:\n", "\n", "$$ [b_{i}, b_{j}] = [b^{\\dagger}_{i}, b^{\\dagger}_{j}] = 0 $$\n", "$$ [b_{i}, b^{\\dagger}_{j}] = \\delta_{ij} $$\n", "\n", "Where \\\\( b^{\\dagger} \\\\) is a creation operator, \\\\( b \\\\) is an annihilation operator, and: the commutator is \\\\( [A, B] = AB - BA \\\\). \n", "\n", "If we want to do the same thing for fermions, we have to make some changes. First, our oscillators can only have 0 or 1 excitations. In other words, at maximum only 1 fermion can be in a given state. This is the Pauli exclusion principle at work. Indeed, the tensor product has to work differently: the whole thing has to do with how fermions are permutation *antisymmetric* particles, unlike bosons which are permutation *symmetric*. So it doesn't matter the order in which we create bosons, but for fermions: \\\\( f^{\\dagger}_{i} f^{\\dagger}_{j} \\mid 0 \\rangle = -f^{\\dagger}_{j} f^{\\dagger}_{i} \\mid 0 \\rangle \\\\), where \\\\( \\mid 0 \\rangle \\\\) is the fermion vacuum aka the state of all oscillators having 0 quanta.\n", "\n", "The upshot is that the commutation relations between fermions involve the anticommutator instead of the commutator. The anticommutator is \\\\( \\{A, B\\} = AB + BA \\\\).\n", "\n", "$$ \\{f_{i}, f_{j}\\} = \\{f^{\\dagger}_{i}, f^{\\dagger}_{j}\\} = 0 $$\n", "$$ \\{f_{i}, f^{\\dagger}_{j}\\} = \\delta^{i,j} $$\n", "\n", "It's not hard to implement this. Suppose we have 5 fermions. The standard 2x2 matrices for creation and annihilation operators are just:\n", "\n", "$$ f^{\\dagger} = \\begin{pmatrix} 0 & 0 \\\\ 1 & 0 \\end{pmatrix} $$\n", "$$ f = \\begin{pmatrix} 0 & 1 \\\\ 0 & 0 \\end{pmatrix} $$\n", "\n", "To construct, for example, 5 pairs of creation and annihilation operators with the correct commutation relations, and so which preserve the antisymmetry of the fermions:\n", "\n", "$$ f_{0} = f \\otimes I \\otimes I \\otimes I \\otimes I $$\n", "$$ f_{1} = Z \\otimes f \\otimes I \\otimes I \\otimes I $$\n", "$$ f_{2} = Z \\otimes Z \\otimes f \\otimes I \\otimes I $$\n", "$$ f_{3} = Z \\otimes Z \\otimes Z \\otimes f \\otimes I $$\n", "$$ f_{4} = Z \\otimes Z \\otimes Z \\otimes Z \\otimes f $$\n", "\n", "Where $Z$ is Pauli $Z$. The idea is that there is a \"normal ordering\" for the fermions. When the operators are applied to the vacuum in the descending order, the $Z$'s in \\\\( f_{4} \\\\), say, don't matter since there are no excitations in those oscillators: we're in the vacuum after all. But when the operators are applied in the reverse order, they pick up a negative sign from the $Z$'s. You can check that this works\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "testing fermion operators: True\n" ] } ], "source": [ "import qutip as qt\n", "import numpy as np\n", "\n", "def fermion_operators(n):\n", " return [qt.tensor(*[qt.destroy(2) if i == j\\\n", " else (qt.sigmaz() if j < i\\\n", " else qt.identity(2))\\\n", " for j in range(n)])\\\n", " for i in range(n)]\n", "\n", "def anticommutator(a, b):\n", " return a*b + b*a\n", "\n", "def test_fermions(f):\n", " d = f[0].shape[0]\n", " for i in range(len(f)):\n", " for j in range(len(f)):\n", " test1 = anticommutator(f[i], f[j]).full()\n", " test2 = anticommutator(f[i], f[j].dag()).full()\n", " if not \\\n", " (np.isclose(test1, np.zeros((d,d))).all()\\\n", " and \\\n", " ((np.isclose(test2, np.zeros((d,d))).all() and i != j)\\\n", " or (np.isclose(test2, np.eye(d)).all() and i == j))):\n", " return False\n", " return True\n", "\n", "#########################################################################################\n", "\n", "n = 4\n", "f = fermion_operators(n)\n", "fvac = qt.basis(2**n)\n", "fvac.dims = [[2]*n, [1]*n]\n", "\n", "print(\"testing fermion operators: %s\" % test_fermions(f))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So we have some fermions. Now we're going to do a crazy thing. It turns out we can split a fermion into two \"Majorana fermions.\" The idea is that a Majorana fermion is its own antiparticle: its creation and annihilation operators *are the same operator*. In some sense, we can think of them like the \"square roots\" of normal fermions. At the same time, if we swap the order of Majorana operators, we pick up a negative sign, and so we are justified in calling them fermions.\n", "\n", "Given normal fermion operators \\\\(f\\\\) and \\\\(f^{\\dagger}\\\\), we form Majorana operators:\n", "\n", "$$ \\psi_{L} = f + f^{\\dagger} $$\n", "$$ \\psi_{R} = i(f - f^{\\dagger}) $$\n", "\n", "Here \\\\( \\psi \\\\) is the standard way of referring to a Majorana operator. We've chosen to call the two Majoranas we get by \"left\" and \"right\" for reasons we shall shortly see. And by the way, they are hermitian.\n", "\n", "So we we have $n$ fermions, we could imagine splitting them all into Majoranas. Given our choice of normalization, they'll obey the commutation relations:\n", "\n", "$$ \\{ \\psi_{i}, \\psi_{j} \\} = 2\\delta_{ij}$$" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "testing majorana operators: True\n", "anticommutes?: True\n", "is own antiparticle?: True\n" ] } ], "source": [ "def split_fermion(f):\n", " return (f + f.dag()), 1j*(f - f.dag())\n", "\n", "def test_majoranas(m):\n", " d = m[0].shape[0]\n", " for i in range(len(m)):\n", " for j in range(len(m)):\n", " test = anticommutator(m[i], m[j]).full()\n", " if not ((i == j and np.isclose(test, 2*np.eye(d)).all()) or\\\n", " (i != j and np.isclose(test, np.zeros((d,d))).all())):\n", " return False\n", " return True\n", "\n", "#########################################################################################\n", "\n", "m = []\n", "for i in range(n):\n", " m.extend(split_fermion(f[i]))\n", "\n", "print(\"testing majorana operators: %s\" % test_majoranas(m))\n", "print(\"anticommutes?: %s\" % (m[2]*m[1]*fvac == -m[1]*m[2]*fvac))\n", "print(\"is own antiparticle?: %s\" % (m[0]*m[0]*fvac == fvac))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We've seen how in the case of $n$ qubits, strings of $n$ Pauli's form a basis for hermitian/unitary operators. Something quite analogous works here with the Majoranas. If we have $n$ Majoranas, we can take all combinations of them of lengths $0,1,2,3,\\dots$ up to $n$, and these form an operator basis. In other words, we consider all combinations of Majoranas of different lengths (eg. \\\\( I, \\psi_{0}, \\psi_{1}, \\psi_{2}, \\psi_{0}\\psi_{1}, \\psi_{0}\\psi_{2}, \\psi_{1}\\psi_{2}, \\psi_{0}\\psi_{1}\\psi_{2} \\\\)) and take the inner product of our operator with each of them in turn. (In fact, due to our normalization convention, we'll be rescaling each of these operators by $2^{-\\frac{m}{4}}$, where $m$ is the number of Majoranas.) We can then recover the original operator by multiplying each of the obtained coefficients by the Majorana string operators and summing." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Recovered operator? True\n" ] } ], "source": [ "from itertools import combinations\n", "from functools import reduce \n", "\n", "def majorana_basis(m):\n", " n = len(m)\n", " prefac = 2**(-n/4)\n", " M = {}\n", " for i in range(n+1):\n", " if i == 0:\n", " M[\"I\"] = prefac*qt.identity(m[0].shape[0])\n", " M[\"I\"].dims = m[0].dims\n", " else:\n", " for string in combinations(list(range(n)), i):\n", " M[string] = prefac*reduce(lambda x, y: x*y, [m[j] for j in string])\n", " return M\n", "\n", "def op_coeffs(O, M):\n", " return dict([(string, (string_op.dag()*O).tr()) for string, string_op in M.items()])\n", "\n", "def coeffs_op(coeffs, M):\n", " return sum([coeffs[string]*string_op for string, string_op in M.items()])\n", "\n", "def prettyc(coeffs):\n", " for string, coeff in coeffs.items():\n", " if not np.isclose(coeff, 0):\n", " print(\"%s : %s\" % (string, coeff))\n", "\n", "#########################################################################################\n", "\n", "M = majorana_basis(m)\n", "O = qt.rand_herm(2**n)\n", "O.dims = [[2]*n, [2]*n]\n", "Oc = op_coeffs(O, M)\n", "O2 = coeffs_op(Oc, M)\n", "\n", "print(\"Recovered operator? %s\" % (O == O2))\n", "#print(\"Expansion in Majoranas:\")\n", "#prettyc(Oc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "