{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction to OpenFermion\n", "\n", "In this tutorial we will learn how to use OpenFermion to represent and manipulate strongly-correlated Hamiltonians, and to interface it with Cirq.\n", "To install OpenFermion, please see the instructions at https://github.com/quantumlib/OpenFermion\n", "To install the OpenFermion-Cirq interface, please see the instructions at https://github.com/quantumlib/OpenFermion-Cirq.\n", "\n", "(Some of the code in this tutorial is adapted from tutorial 4 of OpenFermion-Cirq.) \n", "\n", "1. What is OpenFermion?\n", "1. Operators\n", " 1. The QubitOperator class\n", " 1. The FermionOperator class\n", " 1. The Jordan-Wigner transformation\n", "1. Quantum Chemistry\n", " 1. The MolecularData class\n", " 1. The InteractionOperator class\n", "1. The OpenFermion-Cirq interface\n", "1. Exercise: the unitary coupled cluster ansatz\n", " \n", "\n", "# What is OpenFermion?\n", "\n", "OpenFermion is a package that allows for the easy representation and manipulation of operators on strongly-correlated fermionic, bosonic, and qubit systems. This in turn allows it to act as an interface between computational chemistry software and quantum circuit design packages such as cirq. It also contains various methods for generating quantum circuits of use for quantum chemistry/strongly correlated materials.\n", "\n", "# Operators\n", "\n", "OpenFermion provides many convenient methods for representing the various operators that appear in strongly-correlated physics and chemistry problems. In particular, OpenFermion aims for these operators to be human-readable, and to not require storing an exponentially large matrix.\n", "\n", "## The QubitOperator class\n", "\n", "We have repeatedly encountered the Pauli operators on $N$ qubits, $\\{I,X,Y,Z\\}^{\\otimes N}$. OpenFermion represents these, and linear combinations of Pauli operators, using the QubitOperator class. Internally, the data about the operator is stored as a dictionary, with the names of individual Pauli operators used as keys.\n", "\n", "QubitOperators are initialized as single Pauli terms separated by space, and each Pauli term is written index-last. An optional coefficient may be provided to multiply the term:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from openfermion import QubitOperator\n", "qop1 = QubitOperator(\"X1 Y2\", 3.0)\n", "print(qop1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "QubitOperators may be added, multiplied, and multiplied by constants to produce other QubitOperators " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "qop2 = QubitOperator(\"Z1 Z2\")\n", "print(qop1 + 3 * qop2)\n", "print(qop1 * qop2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise 1:** Verify that QubitOperators on a single qubit obey the Pauli operator commutation relations." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Insert your code here!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "OpenFermion additionally has methods to convert QubitOperators into sparse matrix form, or to calculate the eigenspectrum." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from openfermion import eigenspectrum, get_sparse_operator\n", "print(eigenspectrum(qop1 + 3*qop2))\n", "matrix = get_sparse_operator(qop1 + 3*qop2).todense()\n", "print()\n", "print(matrix.round(3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise 2:** Verify that the matrix produced is identical to the matrix obtained using numpy's kron function, and check that the eigenvalues match those produced by eigenspectrum." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Insert your code here" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The FermionOperator class\n", "\n", "OpenFermion can also represent operators on fermionic systems via the FermionicOperator class. This stores a representation of an operator as a sum of creation and annihilation operators. Individual FermionOperators are instantiated in a similar way to QubitOperators, but instead of needing to write a Pauli operator, we simply write the indices, and represent creation operators with a '^' symbol." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from openfermion import FermionOperator\n", "\n", "fop1 = FermionOperator('2 1', 2)\n", "print(fop1)\n", "fop2 = FermionOperator('2^ 3', 3)\n", "print()\n", "print(fop1 * fop2)\n", "print()\n", "print(fop1 + 3 * fop2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "FermionOperators introduce a new issue - individual creation and annihilation operators no longer commute, so there is not an immediately obvious choice for the 'standard form' that we would always want to write a product of these operators in --- nor is it immediately obvious whether two FermionOperators are equal!\n", "\n", "One representation that is standard is 'normal ordering', where we first write all creation operators in decreasing order of their index and then all annihilation operators in decreasing order of their index. To convert a FermionOperator into its normal-ordered form, OpenFermion provides the normal_ordered function." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from openfermion import normal_ordered\n", "print(fop1 * fop2)\n", "print()\n", "print(normal_ordered(fop1 * fop2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that what was a single term in its original order becomes a sum of two terms when converting to normal order! This is why normal order is not always preferable - the number of terms can grow quite large. Normal-ordering is also quite computationally costly to perform, and so openfermion does not perform it at each step. This implies that equality testing between FermionicOperators requires you to perform normal ordering first!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fop1 = FermionOperator('1 2', 1.0)\n", "fop2 = FermionOperator('2 1', -1.0)\n", "print(fop1 == fop2)\n", "print(normal_ordered(fop1) == normal_ordered(fop2))\n", "print(fop1 - fop2)\n", "print(normal_ordered(fop1 - fop2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise 3:** Verify that FermionOperators obey the correct anti-commutation rules on a system with 2 indices." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Insert your code here!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above operations to generate a sparse matrix and eigenspectrum work just as well for fermion operators as they do qubit operators. However, remember that creation and annihilation operators are not themselves hermitian. To create an observable operator with real eigenspectrum, we should sum our fermion operators with their hermitian conjugates. OpenFermion provides the functions hermitian_conjugated and is_hermitian to help with this." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from openfermion import hermitian_conjugated, is_hermitian\n", "\n", "print(is_hermitian(fop1))\n", "print()\n", "hermitian_op = fop1 + hermitian_conjugated(fop1)\n", "print(hermitian_op)\n", "print()\n", "print(is_hermitian(hermitian_op))\n", "print()\n", "matrix = get_sparse_operator(hermitian_op)\n", "print(matrix.todense().round(2))\n", "print()\n", "print(eigenspectrum(fop1 + hermitian_conjugated(fop1)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise 4:** Investigate the matrix representation of the sum of a single creation and single annihilation operator. What does this remind you of?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "### Insert your code here!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Jordan-Wigner transformation\n", "\n", "Transforming between fermionic and qubit operators is a somewhat complicated task, as one needs to preserve the commutation relations of individual operators. Many choices for this transformation exist, all with various advantages and disadvantages. We will study this shortly in both the lectures and tutorials, but for the sake of this tutorial we will just use one possible transformation - the Jordan-Wigner transformation.\n", "\n", "In short, the Jordan-Wigner transformation maps $$\\hat{c}_i\\rightarrow\\frac{1}{2}\\otimes_{j