Ternary Tree Mappings#

Ternary trees are one of the most used classes of Fermion encodings, which includes the JordanWigner, BravyiKitaev, Parity and JKMN encodings.

You can create ones of these common ternary trees easily with built-in functions.

If you want a non-standard tree encoding it’s easy to build these yourself.

import ferrmion as fr
from ferrmion.visualise import draw_tt

Standard Encodings#

Ferrmion inbuild functions for the common encodings

  • JordanWigner() or JW()

  • BravyiKitaev() or BK()

  • ParityEncoding()

  • JKMN()

The most simple, and common is JordanWigner

jw = fr.JordanWigner(10)
draw_tt(jw, type="linear")
../_images/d426b93f6883752004cb743013ff0b9e858882c5a730aabada15a3a19f639787.png

As another example, he’re the Bravyi-Kitaev encoding for 10 modes.

jw = fr.BravyiKitaev(10)
draw_tt(jw)
../_images/4f6e27574fd281168460e7d4b2af8d99b8088dc093b32b744c10f7fa4d38e10d.png

Custom Encodings#

Several methods, such as the Bonsai Algorithm and HATT, require building ternary trees on the fly.

ferrmion allows you to do this by adding branches. Let’s see how to do this by recreating the BravyiKitaev encoding above.

mytt = fr.TernaryTree(n_modes=10)
draw_tt(mytt)
../_images/ecd84c19ff2ead0a199dd091f9405fc4535e1d28709e8cc0ec9a72270529c218.png

First lets add the left most branch. We can do this by adding a node at xxxx.

mytt.add_node("xxxx")
draw_tt(mytt)
../_images/e5285189cb40f3ebbfd8bc7540d3004cfa765862d5ecbdf6a5dd7f57eb4e3ceb.png

Because no nodes existed in the path to xxxx these were created too!

If we wanted to add the node at xxz, then all the nodes to this path exist already.

mytt.add_node("xxz")
draw_tt(mytt)
../_images/afdd5994610262fa9812d368876e7081a958cb528f09f3583895dfb589651eaf.png

Let’s finish growing the BK tree above.

mytt.add_node("xxxz")
mytt.add_node("xzx")
mytt.add_node("xzz")
draw_tt(mytt)
../_images/4f6e27574fd281168460e7d4b2af8d99b8088dc093b32b744c10f7fa4d38e10d.png

Enumeration Scheme#

One way to encode a fermionic Hamiltonian is majorana-string encoding. For each fermionic mode we build operators from a pair of majorana-operators. $\(a^{\dagger}_j = \frac{1}{2}(\gamma_{2j} - i\gamma_{2j+1})\)\( \)\(a_j = \frac{1}{2}(\gamma_{2j} + i\gamma_{2j+1})\)$

The first step in encoding \(N\) fermionic modes is then to produce a set of \(2N\) pauli-strings whcih have the same properties as the Majorana operators:

  1. Each Majorana is mapped to a Pauli string \(m_{j} \to S_{k}\in S\) for \(j,k=0,\dots,2N-1\)

  2. Commutation: The Pauli strings satisfy \(\{ S_{i},S_{j} \}=2\delta_{ij}\mathbb{1}\)

  3. The operators are linearly independent

  4. Algebraically independent: For all unequal subsets \(A \subseteq S\) and \(B \subseteq S\) such that \(A \ne B\), \(\prod_{S_{i}\in A}S_{i}\propto \prod_{S_{j}\in B}S_{j}\) is not fulfilled.

Ternary-trees give us a method to generate valid sets of pauli-strings, but we also need to specify which fermionic modes are represented by which pauli-strings, and which physical qubits map to which logical qubits.

To understand why this is important it’s easiest to go through some examples.

Let’s start by defining a JordanWigner Ternary Tree.

from ferrmion.visualise import draw_tt, symplectic_matshow
jw = fr.JordanWigner(5)
draw_tt(jw, type="linear")
../_images/7cf07eb2488cdf60dd1b54c31b6619a80cd15137cb686af15eb04cc5eb86af1e.png

For each node on the tree, we build up two Pauli strings to be used as Majorana operators.

jw.string_pairs
{'': ('x', 'y'),
 'z': ('zx', 'zy'),
 'zz': ('zzx', 'zzy'),
 'zzz': ('zzzx', 'zzzy'),
 'zzzz': ('zzzzx', 'zzzzy')}

Naive Enumeration#

The simplest (and default) way to create a JW encoding is with the naive enumeration, which assigns the node highest in the tree to mode 0 and qubit 0, with indices for both increasing in lock-step as we go down the tree.

jw_naive = fr.JordanWigner(4)
print(jw.default_enumeration_scheme())
draw_tt(
    jw_naive, enumeration_scheme=jw_naive.default_enumeration_scheme(), type="linear"
)
{'': (0, 0), 'z': (1, 1), 'zz': (2, 2), 'zzz': (3, 3), 'zzzz': (4, 4)}
../_images/5e59385f3a4acf9715855cde605caff6686e9c418273e60d5a7971a0f32eab52.png

If we look at the Pauli strings generated by this encoding, we see a very simple structure.

We’ll plot each of the pauli-strings, with color indicating which Pauli operator is applied to which qubit.

symplectic_matshow(jw_naive)
../_images/1180881f8c767e68050887026863fc53d0f0a48a588f954e2dd54198a157eecd.png

This is often thought of as “the” JordanWigner encoding, but we could assign each node in the tree to any of the N fermionic modes, and any of the N qubits. So really, there are \(N^2\) JordanWigner encodings!

Enumerating Fermionic Modes#

As an example, let’s swap modes 1 and 4, but keep the qubits the same.

from ferrmion.encode.ternary_tree import JordanWigner

jw = JordanWigner(4)
jw.enumeration_scheme = {
    "": (0, 0),
    "z": (2, 1),
    "zz": (1, 2),
    "zzz": (3, 3),
}
draw_tt(jw, enumeration_scheme=jw.enumeration_scheme, type="linear")
symplectic_matshow(jw)
../_images/840f45eecb26d65911d080f454323961962ce1b70bcc03fe587cad994c78f8e3.png ../_images/cee0dfc67f6ae180ff23711f0ff2126e4e19304bcfd4f640de0119d40d4dd84e.png

now the pauli operators for fermionic modes 3 and 4 have been swapped!

Enumerating Qubits#

We can do the same thing, but this time swap qubits 1 and 4, while leaving the fermion modes unaltered.

jw = JordanWigner(4)
jw.enumeration_scheme = {
    "": (0, 0),
    "z": (1, 2),
    "zz": (2, 1),
    "zzz": (3, 3),
}
draw_tt(jw, enumeration_scheme=jw.enumeration_scheme, type="linear")
symplectic_matshow(jw)
../_images/13b22db680d1fe58875658428d4ca1185d2d10d5ae8f20d8d977806babf937e1.png ../_images/9d55f9464f627442fdef794e62a3cf97e6259307d5343128f95af564e2ec024d.png

here the Pauli-strings for each fermionic mode contain the same operators, but the order of operators in the string (and therefore which qubit they apply to) has been changed!

If you want to know more about why this is important, see the notebook on Optimising for Pauli-weight or the Reduced Entanglement Ternary Tree.