<jupyter_start><jupyter_text>OpenFermion hands-on tutorialOpenFermion is a library for obtaining and manipulating representations of fermionic and qubit Hamiltonians for the quantum simulation of chemistry and materials. This tutorial introduces the core data structures of OpenFermion and some basic functionality. BackgroundA system of $N$ fermionic modes isdescribed by a set of fermionic *annihilation operators*$\{a_p\}_{p=0}^{N-1}$ satisfying the *canonical anticommutation relations*$$\begin{aligned}    \{a_p, a_q\} &= 0, \\    \{a_p, a^\dagger_q\} &= \delta_{pq},  \end{aligned}$$ where $\{A, B\} := AB + BA$. The adjoint$a^\dagger_p$ of an annihilation operator $a_p$ is called a *creationoperator*, and we refer to creation and annihilation operators asfermionic *ladder operators*.    The canonical anticommutation relations impose a number of consequences on the structure of the vector space on which the ladder operators act; see [Michael Nielsen's notes](http://michaelnielsen.org/blog/archive/notes/fermions_and_jordan_wigner.pdf) for a good discussion. FermionOperator and QubitOperator FermionOperator- Stores a weighted sum (linear combination) of fermionic terms- A fermionic term is a product of ladder operators- Examples of FermionOperators:$$\begin{align}& a_1 \nonumber \\& 1.7 a^\dagger_3 \nonumber \\&-1.7 \, a^\dagger_3 a_1 \nonumber \\&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 \nonumber \\&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 - 1.7 \, a^\dagger_3 a_1 \nonumber\end{align}$$- A fermionic term is internally represented as a tuple of tuples- Each inner tuple represents a single ladder operator as (index, action)- Examples of fermionic terms:$$\begin{align}I & \mapsto () \nonumber \\a_1 & \mapsto ((1, 0),) \nonumber \\a^\dagger_3 & \mapsto ((3, 1),) \nonumber \\a^\dagger_3 a_1 & \mapsto ((3, 1), (1, 0)) \nonumber \\a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto ((4, 1), (3, 1), (9, 0), (1, 0)) \nonumber\end{align}$$- FermionOperator is a sum of terms, represented as a dictionary from term to coefficient<jupyter_code>import openfermion as of

op = of.FermionOperator(((4, 1), (3, 1), (9, 0), (1, 0)), 1+2j) + of.FermionOperator(((3, 1), (1, 0)), -1.7)

print(op.terms)<jupyter_output>{((4, 1), (3, 1), (9, 0), (1, 0)): (1+2j), ((3, 1), (1, 0)): -1.7}<jupyter_text>Alternative notation, useful when playing around:$$\begin{align}I & \mapsto \textrm{""} \nonumber \\a_1 & \mapsto \textrm{"1"} \nonumber \\a^\dagger_3 & \mapsto \textrm{"3^"} \nonumber \\a^\dagger_3 a_1 & \mapsto \textrm{"3^}\;\textrm{1"} \nonumber \\a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto \textrm{"4^}\;\textrm{3^}\;\textrm{9}\;\textrm{1"} \nonumber\end{align}$$<jupyter_code>op = of.FermionOperator('4^ 3^ 9 1', 1+2j) + of.FermionOperator('3^ 1', -1.7)

print(op.terms)<jupyter_output>{((4, 1), (3, 1), (9, 0), (1, 0)): (1+2j), ((3, 1), (1, 0)): -1.7}<jupyter_text>Just print the operator for a nice readable representation:<jupyter_code>print(op)<jupyter_output>-1.7 [3^ 1] +
(1+2j) [4^ 3^ 9 1]<jupyter_text>Use the `normal_ordered` function to normal-order FermionOperators. Normal-ordering a term means using the anticommutation relations to write the term into an equivalent form where all creation operators precede annihilation operators and higher indices precede lower indices.<jupyter_code>op = of.FermionOperator('0 1 3^ 2^')

print(of.normal_ordered(op))
print()

op = of.FermionOperator('0 0^')
print(of.normal_ordered(op))<jupyter_output>-1.0 [3^ 2^ 1 0]

1.0 [] +
-1.0 [0^ 0]<jupyter_text>Exercises- Use the `*`, `+`, and `**` operators and the `normal_ordered` function to verify the following:$$\begin{align}    (a_2)^2 &= 0 \\    a_2 a_7 + a_7 a_2 &= 0 \\    a_2 a_7^\dagger + a_7^\dagger a_2 &= 0\\    a_2 a_2^\dagger + a_2^\dagger a_2 &= 1\end{align}$$<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>- Use the `==` operator and the `normal_ordered` function to verify that$$a_1^\dagger a_2 a_2^\dagger a_4 = a_1^\dagger a_4 - a_1^\dagger a_2^\dagger a_2 a_4$$<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>QubitOperatorSame as FermionOperator, but the possible actions are 'X', 'Y', and 'Z' instead of 1 and 0.<jupyter_code>op = of.QubitOperator(((1, 'X'), (2, 'Y'), (3, 'Z')))
op += of.QubitOperator('X3 Z4', 3.0)

print(op)<jupyter_output>1.0 [X1 Y2 Z3] +
3.0 [X3 Z4]<jupyter_text>FermionOperator and QubitOperator actually inherit from the same parent class, SymbolicOperator.  Exercises- Use the `commutator` function to verify the following:$$\begin{align}[X_0, Y_0] &= 2 i Z_0 \\[Z_0, Y_0] &= -2 i X_0 \\\end{align}$$<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>The Jordan-Wigner and Bravyi-Kitaev transformsA fermionic transform maps FermionOperators to QubitOperators in a way that preserves the canonical anticommutation relations. The most basic transforms are the Jordan-Wigner transform (JWT) and Bravyi-Kitaev transform (BKT). Note that the BKT requires the total number of qubits to be predetermined. Whenever a fermionic transform is being applied implicitly, it is the JWT.<jupyter_code>op = of.FermionOperator('2^ 15')

print(of.jordan_wigner(op))
print()
print(of.bravyi_kitaev(op, n_qubits=16))<jupyter_output>(0.25+0j) [X2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 X15] +
0.25j [X2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 Y15] +
-0.25j [Y2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 X15] +
(0.25+0j) [Y2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 Y15]

(-0.25+0j) [Z1 X2 X3 X7 Z15] +
-0.25j [Z1 X2 X3 Y7 Z11 Z13 Z14] +
0.25j [Z1 Y2 X3 X7 Z15] +
(-0.25+0j) [Z1 Y2 X3 Y7 Z11 Z13 Z14]<jupyter_text>Exercises- Below are some examples of how FermionOperators are mapped to QubitOperators by the Jordan-Wigner transform (the notation 'h.c.' stands for 'hermitian conjugate'):$$\begin{align*}    a_p^\dagger &\mapsto \frac12 (X_p - i Y_p) Z_0 \cdots Z_{p-1}\\    a_p^\dagger a_p &\mapsto \frac12 (I - Z_p)\\    (\beta a_p^\dagger a_q + \text{h.c.}) &\mapsto \frac12 [\text{Re}(\beta) (X_p ZZ \cdots ZZ X_q + Y_p ZZ \cdots ZZ Y_q) + \text{Im}(\beta) (Y_p ZZ \cdots ZZ X_q - X_p ZZ \cdots ZZ Y_q)]\end{align*}$$Verify these mappings for $p=2$ and $q=7$. The `hermitian_conjugated` function may be useful here.<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>- Verify that after applying the JWT to ladder operators, the resulting QubitOperators still satisfy$$\begin{align}    (a_2)^2 &= 0 \\    a_2 a_7 + a_7 a_2 &= 0 \\    a_2 a_7^\dagger + a_7^\dagger a_2 &= 0\\    a_2 a_2^\dagger + a_2^\dagger a_2 &= 1\end{align}$$You can use the `anticommutator` function.<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>Sparse matrices for numerical computations- QubitOperators can be converted to Scipy sparse matrices using `get_sparse_operator`- Sparse matrices can be used to obtain eigenvalues, expectation values, etc.- As a demonstration, we will compute the ground state energy of a Hubbard model Hamiltonian:$$H = - t \sum_{\langle i,j \rangle} \sum_{\sigma}             (a^\dagger_{i, \sigma} a_{j, \sigma} +              a^\dagger_{j, \sigma} a_{i, \sigma})     + U \sum_{i} a^\dagger_{i, \uparrow} a_{i, \uparrow}                  a^\dagger_{j, \downarrow} a_{j, \downarrow}$$where $\langle i, j \rangle$ ranges over the edges in a 2-dimensional grid and $\sigma$ is either spin up or spin down.- The Hubbard model is an idealized Hamiltonian model that may capture qualitative aspects of high-temperature superconductors.<jupyter_code># Set Hubbard model parameters
x_dim = 2
y_dim = 2
tunneling = 1.0
coulomb = 4.0

# Create Hubbard model Hamiltonian as FermionOperator
hubbard_model = of.fermi_hubbard(x_dim, y_dim, tunneling, coulomb)

print(hubbard_model)
# Map to QubitOperator using the JWT
hubbard_jw = of.jordan_wigner(hubbard_model)

print(hubbard_jw)
# Convert to Scipy sparse matrix
hubbard_jw_sparse = of.get_sparse_operator(hubbard_jw)

# Compute ground state energy
ground_energy_jw, _ = of.get_ground_state(hubbard_jw_sparse)

print(ground_energy_jw)<jupyter_output>-3.418550718873853<jupyter_text>Exercises- Compute the ground energy of the same Hamiltonian, but via the Bravyi-Kitaev transform. Verify that you get the same value.<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>Array data structures- When FermionOperators have specialized structure we can store coefficients in numpy arrays, enabling easier numerical manipulation.- Array data structures can always be converted to FermionOperator using `get_fermion_operator`. InteractionOperator- Stores the one- and two-body tensors $T_{pq}$ and $V_{pqrs}$ of the molecular Hamiltonian$$\sum_{pq} T_{pq} a_p^\dagger a_q + \sum_{pqrs} V_{pqrs} a_p^\dagger a_q^\dagger a_r a_s$$- Default data structure for molecular Hamiltonians- Convert from FermionOperator using `get_interaction_operator` DiagonalCoulombHamiltonian- Stores the one- and two-body coefficient matrices $T_{pq}$ and $V_{pq}$ of a Hamiltonian with a diagonal Coulomb term:$$\sum_{pq} T_{pq} a_p^\dagger a_q + \sum_{pq} V_{pq} a_p^\dagger a_p a_q^\dagger a_q$$- Leads to especially efficient algorithms for quantum simulation- Convert from FermionOperator using `get_diagonal_coulomb_hamiltonian` QuadraticHamiltonian- Stores the Hermitian matrix $M_{pq}$ and antisymmetric matrix $\Delta_{pq}$ describing a general quadratic Hamiltonian$$\sum_{p, q} M_{pq} a^\dagger_p a_q+ \frac12 \sum_{p, q}    (\Delta_{pq} a^\dagger_p a^\dagger_q + \text{h.c.})$$- Routines included for efficient diagonalization (can handle thousands of fermionic modes)- Convert from FermionOperator using `get_quadratic_hamiltonian` The Hubbard model has the form of a DiagonalCoulombHamiltonian, so it can be converted to one. OpenFermion-Cirq contains specialized low-depth Hamiltonian simulation algorithms which take this data structure as input.<jupyter_code>hubbard_hamiltonian = of.get_diagonal_coulomb_hamiltonian(hubbard_model)

print(hubbard_hamiltonian.one_body)
print()
print(hubbard_hamiltonian.two_body)
print()
print(hubbard_model)<jupyter_output>[[ 0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j]
 [-1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j]
 [ 0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j]
 [-1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j]
 [ 0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j]
 [ 0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j]
 [ 0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j]]

[[0. 2. 0. 0. 0. 0. 0. 0.]
 [2. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 2. 0. 0. 0. 0.]
 [0. 0. 2. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 2. 0. 0.]
 [0. 0. 0. 0. 2. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 2.]
 [0. 0. 0. 0. 0. 0. 2. 0.]]

4.0 [0^ 0 1^ 1] +
-1.0 [0^ 2] +
-1.0 [0^ 4] +
-1.0 [1^ 3] +
-1.0 [1^ 5] +
-1.0 [2^ 0] +
4.0 [2^ 2 3^ 3] +
-1.0 [2^ 6] +
-1.0 [3^ 1] +
-1.0 [3^ 7] +
-1.0 [4^ 0] +
4.0 [4^ 4 5^ 5] +
-1.0 [4^ 6] +
-1.0 [5^ 1] +
-1.0 [5^ 7] +
-1.0 [6^ 2] +
-1.0 [6^[...]<jupyter_text>The cell below demonstrates using one of our electronic structure package plugins, OpenFermion-PySCF, to generate a molecular Hamiltonian for a hydrogen molecule.<jupyter_code>import openfermionpyscf as ofpyscf

# Set molecule parameters
geometry = [('H', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 0.8))]
basis = 'sto-3g'
multiplicity = 1
charge = 0

# Perform electronic structure calculations and
# obtain Hamiltonian as an InteractionOperator
hamiltonian = ofpyscf.generate_molecular_hamiltonian(
    geometry, basis, multiplicity, charge)

# Convert to a FermionOperator
hamiltonian_ferm_op = of.get_fermion_operator(hamiltonian)

print(hamiltonian_ferm_op)<jupyter_output>0.66147151365 [] +
-1.2178260299951058 [0^ 0] +
0.3316650744318082 [0^ 0^ 0 0] +
0.09231339177803066 [0^ 0^ 2 2] +
0.3316650744318082 [0^ 1^ 1 0] +
0.09231339177803066 [0^ 1^ 3 2] +
0.09231339177803066 [0^ 2^ 0 2] +
0.3267206861819477 [0^ 2^ 2 0] +
0.09231339177803066 [0^ 3^ 1 2] +
0.3267206861819477 [0^ 3^ 3 0] +
0.3316650744318082 [1^ 0^ 0 1] +
0.09231339177803066 [1^ 0^ 2 3] +
-1.2178260299951058 [1^ 1] +
0.3316650744318082 [1^ 1^ 1 1] +
0.09231339177803066 [1^ 1^ 3 3] +
0.09231339177803066 [1^ 2^ 0 3] +
0.3267206861819477 [1^ 2^ 2 1] +
0.09231339177803066 [1^ 3^ 1 3] +
0.3267206861819477 [1^ 3^ 3 1] +
0.32672068618194783 [2^ 0^ 0 2] +
0.09231339177803066 [2^ 0^ 2 0] +
0.32672068618194783 [2^ 1^ 1 2] +
0.09231339177803066 [2^ 1^ 3 0] +
-0.5096378744364826 [2^ 2] +
0.09231339177803066 [2^ 2^ 0 0] +
0.34339576784573445 [2^ 2^ 2 2] +
0.09231339177803066 [2^ 3^ 1 0] +
0.34339576784573445 [2^ 3^ 3 2] +
0.32672068618194783 [3^ 0^ 0 3] +
0.09231339177803066 [3^ 0^ 2 1] +
0.3267206861819478[...]<jupyter_text>Exercises- Create a FermionOperator that is quadratic and Hermitian and use `get_quadratic_hamiltonian` to convert it to a QuadraticHamiltonian.<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>- The BCS mean-field d-wave model of superconductivity has the Hamiltonian$$H = - t \sum_{\langle i,j \rangle} \sum_\sigma        (a^\dagger_{i, \sigma} a_{j, \sigma} +         a^\dagger_{j, \sigma} a_{i, \sigma})    - \sum_{\langle i,j \rangle} \Delta_{ij}      (a^\dagger_{i, \uparrow} a^\dagger_{j, \downarrow} -       a^\dagger_{i, \downarrow} a^\dagger_{j, \uparrow} +       a_{j, \downarrow} a_{i, \uparrow} -       a_{j, \uparrow} a_{i, \downarrow})$$Use the `mean_field_dwave` function to generate an instance of this model with dimensions 10x10 (Tip: Type `of.mean_field_dwave` and then press Shift-Tab. This will reveal the function arguments.)    - Convert the Hamiltonian to a QubitOperator with the JWT. What is the length of the longest Pauli string that appears?    - Convert the Hamiltonian to a QubitOperator with the BKT. What is the length of the longest Pauli string that appears?    - Convert the Hamiltonian to a QuadraticHamiltonian. Get its ground energy using the `ground_energy` method of QuadraticHamiltonian. What would happen if you tried to compute the ground energy by converting to a sparse matrix?<jupyter_code># Code cell for exercises<jupyter_output><empty_output><jupyter_text>Solutions to exercises<jupyter_code># Solutions to exercises

a2 = of.FermionOperator('2')
a2dag = of.FermionOperator('2^')
a7 = of.FermionOperator('7')
a7dag = of.FermionOperator('7^')

print(of.normal_ordered(a2**2))
print(of.normal_ordered(a2*a7 + a7*a2))
print(of.normal_ordered(a2*a7dag + a7dag*a2))
print(of.normal_ordered(a2*a2dag + a2dag*a2))
print()

# ----

op1 = of.FermionOperator('1^ 2 2^ 4')
op2 = of.FermionOperator('1^ 4') - of.FermionOperator('1^ 2^ 2 4')

print(of.normal_ordered(op1) == of.normal_ordered(op2))
print()

# ----

X = of.QubitOperator('X0')
Y = of.QubitOperator('Y0')
Z = of.QubitOperator('Z0')

print(of.commutator(X, Y))
print(of.commutator(Z, Y))
print()

# ----

print(of.jordan_wigner(a2dag))
print()
print(of.jordan_wigner(a2dag*a2))
print()

op = (2+3j)*a2dag*a7
op += of.hermitian_conjugated(op)
print(of.jordan_wigner(op))
print()

# ----

a2_jw = of.jordan_wigner(a2)
a2dag_jw = of.jordan_wigner(a2dag)
a7_jw = of.jordan_wigner(a7)
a7dag_jw = of.jordan_wigner(a7dag)

print(of.anticommutator(a2_jw, a7_jw))
print(of.anticommutator(a2_jw, a7dag_jw))
print(of.anticommutator(a2_jw, a2dag_jw))
print()

# ----

hubbard_bk = of.bravyi_kitaev(hubbard_model)
hubbard_bk_sparse = of.get_sparse_operator(hubbard_bk)
ground_energy_bk, _ = of.get_ground_state(hubbard_bk_sparse)

print(ground_energy_bk)
print()

# ----

op = of.FermionOperator('0^ 1') + of.FermionOperator('1^ 0')
quad_ham = of.get_quadratic_hamiltonian(op)

# ----

mean_field_model = of.mean_field_dwave(10, 10, 1.0, 1.0)

mean_field_jw = of.jordan_wigner(mean_field_model)
print(max(len(term) for term in mean_field_jw.terms))

mean_field_bk = of.bravyi_kitaev(mean_field_model)
print(max(len(term) for term in mean_field_bk.terms))

mean_field_quad_ham = of.get_quadratic_hamiltonian(mean_field_model)
print(mean_field_quad_ham.ground_energy())<jupyter_output>0
0
0
1.0 []

True

2j [Z0]
-2j [X0]

0.5 [Z0 Z1 X2] +
-0.5j [Z0 Z1 Y2]

(0.5+0j) [] +
(-0.5+0j) [Z2]

(1+0j) [X2 Z3 Z4 Z5 Z6 X7] +
(-1.5+0j) [X2 Z3 Z4 Z5 Z6 Y7] +
(1.5+0j) [Y2 Z3 Z4 Z5 Z6 X7] +
(1+0j) [Y2 Z3 Z4 Z5 Z6 Y7]

0
0
(1+0j) []

-3.4185507188738278

182
15
-206.99159932863176
{'fraction_non_alphanumeric': 0.15106862327224016, 'fraction_numerical': 0.09218778542288254, 'mean_word_length': 4.265790317409426, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '229314', 'license': 'Apache-2.0', 'n_tokens_mistral': 7485, 'n_tokens_neox': 6531, 'n_words': 1861, 'path': 'OpenFermion hands-on tutorial.ipynb', 'repo_name': 'kevinsung/OpenFermion-Hands-On-Tutorials', 'size': '16423'}