Creating and manipulating Hamiltonians

This page explains how to create and manipulate Hamiltonian using qat.fermion module

Defining a Hamiltonian

Many problems in physics are defined by their Hamiltonians. The basic objects we will manipulate are SpinHamiltonian and FermionHamiltonian. These classes can be seen as an Observable constructor, of which they inherit, while containing additional methods.

Let us see in more details how to define a SpinHamiltonian and a FermionHamiltonian.

Spin Hamiltonians

The spin Hamiltonian acting on 2 qubits \(0\) and \(1\) defined by \(H = 0.3 X_{0} - 0.4 Z_{0}Y_{1}\) can be written:

from qat.core import Term
from qat.fermion.hamiltonians import SpinHamiltonian

# Define the number of qubits
nqbits = 2

# Define the Hamiltonian
H = SpinHamiltonian(nqbits, [Term(0.3, "X", [0]), Term(-0.4, "ZY", [0, 1])])
print(f"H = {H}")
H = 0.3 * (X|[0]) +
-0.4 * (ZY|[0, 1])

Fermionic Hamiltonians

If we consider now a fermionic Hamiltonian \(H = 0.3 (C^{\dagger}_{0}C_{1} + C^{\dagger}_{1}C_{0}) + 1.4 C^{\dagger}_{0}C_{1}C^{\dagger}_{1}C_{0}\), we should write:

from qat.core import Term
from qat.fermion.hamiltonians import FermionHamiltonian

# Define the number of qubits
nqbits = 2

# Define the Hamiltonian
H = FermionHamiltonian(nqbits, [Term(0.3, "Cc", [0, 1]), Term(0.3, "Cc", [1, 0]), Term(1.4, "CcCc", [0, 1, 1, 0])])
print(f"H = {H}")
H = 0.3 * (Cc|[0, 1]) +
0.3 * (Cc|[1, 0]) +
1.4 * (Cc|[0, 0]) +
1.4 * (CCcc|[0, 1, 0, 1])

Note

When compatible, FermionHamiltonian can be transformed to ElectronicStructureHamiltonian via the method to_electronic().

Fermionic Hamiltonian using one and two-electrons integrals

In chemistry, problems are often more easily described using interaction terms instead of fermionic operators.

The electronic-structure Hamiltonian is defined by:

\[H = \sum_{pq} h_{pq}a_p^\dagger a_q + \frac{1}{2} \sum_{pqrs} h_{pqrs}a_p^\dagger a_q^\dagger a_r a_s + r \mathbb{I}\]

The definition of this Hamiltonian is done via the ElectronicStructureHamiltonian class, which accepts the one and two-body terms \(h_{pq}\) and \(h_{pqrs}\) as inputs.

import numpy as np
from qat.fermion import ElectronicStructureHamiltonian

# Define the interaction integrals
h_pq = 0.2 * np.array([[0, 1], [1, 0]])

h_pqrs = np.zeros((2, 2, 2, 2))
h_pqrs[0, 1, 1, 0] = 0.7
h_pqrs[1, 0, 0, 1] = 0.7

# Define the ElectronicStructureHamiltonian
H_elec = ElectronicStructureHamiltonian(h_pq, h_pqrs, -6)
print(f"H_elec is in {H_elec.htype.name} representation")
Traceback (most recent call last):
  File "<stdin>", line 13, in <module>
AttributeError: 'ElectronicStructureHamiltonian' object has no attribute 'htype'

ElectronicStructureHamiltonian can be transformed to FermionHamiltonian via the method to_fermion().

The ElectronicStructureHamiltonian inherits from the FermionHamiltonian class, and thus contains every method implemented in FermionHamiltonian.

Note

An alternative definition for the electronic-structure Hamiltonian is :

\[H = \sum_{uv\sigma} I_{uv}c^{\dagger}_{u\sigma}c_{v\sigma} + \frac{1}{2}\sum_{uvwx}\sum_{\sigma \sigma'} I_{uvwx}c^{\dagger}_{u\sigma}c^{\dagger}_{v\sigma'}c_{k\sigma'}c_{l\sigma} + r\mathbb{I}\]

Should you need to define an ElectronicStructureHamiltonian using the one- and two-body integrals \(I_{uv}\) and \(I_{uvwx}\), you have two options:

For more information on this type of body integrals, see the class MolecularHamiltonian documentation.

You can also consult the Jupyter notebook on spin-fermion transforms.

The get_matrix() method

The objects SpinHamiltonian, FermionHamiltonian and ElectronicStructureHamiltonian allow for the direct usage of the underlying Hamiltonian matrix. Bare in mind that this method should not be used for big Hamiltonians, as the memory cost might be too much to handle on your machine. You can access the Hamiltonian matrix by using the get_matrix() method of the Hamiltonian classes.

from qat.core import Term
from qat.fermion import SpinHamiltonian

H = SpinHamiltonian(2, [Term(0.5, "Y", [0]), Term(0.5, "Y", [1])])
print(H.get_matrix())
[[0.+0.j  0.-0.5j 0.-0.5j 0.+0.j ]
 [0.+0.5j 0.+0.j  0.+0.j  0.-0.5j]
 [0.+0.5j 0.+0.j  0.+0.j  0.-0.5j]
 [0.+0.j  0.+0.5j 0.+0.5j 0.+0.j ]]

Fermionic to spin representation

A problem formulated in fermionic representation often needs to be converted to a spin representation, so that it can be handled by a quantum computer. To do so, one can use the to_spin() method.

Three transforms are available:

  • the Jordan-Wigner transform (default),

  • the Bravyi-Kitaev transform,

  • the parity method.

Example

Let us transform the previous ElectronicStructureHamiltonian to a spin Hamiltonian:

import numpy as np
from qat.fermion import ElectronicStructureHamiltonian

# Define the fermionic Hamiltonian
h_pq = 0.2 * np.array([[0, 1], [1, 0]])
h_pqrs = np.zeros((2, 2, 2, 2))
h_pqrs[0, 1, 1, 0] = 0.7
h_pqrs[1, 0, 0, 1] = 0.7

# Define the Hamiltonian
H_fermion = ElectronicStructureHamiltonian(h_pq, h_pqrs, -6)

# Transform it to a spin Hamiltonian using Bravyi-Kitaev transform
H_spin = H_fermion.to_spin(method="bravyi-kitaev")

# Similarly, we could have used "jordan-wigner" or "parity"
print(f"H = {H_spin}")
H = (-5.825+0j) * I^2 +
(0.1+0j) * (X|[0]) +
(-0.1+0j) * (XZ|[0, 1]) +
(0.175+0j) * (Z|[1]) +
(-0.175+0j) * (Z|[0]) +
(-0.175+0j) * (ZZ|[0, 1])

Transforming a ElectronicStructureHamiltonian into a spin Hamiltonian relies on functions located in the qat.fermion.transforms module:

Spin and fermionic Hamiltonian operations

Spin and fermionic Hamiltonian handle basic algebraic operations. This allows for the computation of commutators in both spin and fermionic representations:

import numpy as np
from qat.core import Term
from qat.fermion.hamiltonians import FermionHamiltonian

H_fermion1 = FermionHamiltonian(2, [Term(1.0, "Cc", [0, 1]), Term(0.5, "CCcc", [0, 1, 0, 1])])
H_fermion2 = FermionHamiltonian(2, [Term(1.0, "Cc", [1, 0]), Term(0.5, "CCcc", [1, 0, 1, 0])])

H_spin1 = H_fermion1.to_spin()
H_spin2 = H_fermion2.to_spin()

fermion_comutator_matrix = (H_fermion1 | H_fermion2).get_matrix()
spin_comutator_matrix = (H_spin1 | H_spin2).get_matrix()

is_equal_sign = "=" if np.all(np.equal(fermion_comutator_matrix, spin_comutator_matrix)) else "!="
print(f"Fermionic commutator {is_equal_sign} spin commutator matrix")
Fermionic commutator = spin commutator matrix

Note

In some cases, it is preferable to compute commutators in fermionic representation rather than in spin representation, as the built-in Wick ordering might simplify many fermionic terms, which may speed up the commutator computation as well as subsequent computations

Some Hamiltonian constructors

The Hubbard model or the Anderson model are very widely used. For that reason, we included several Hamiltonian constructors to help you define the system you are interested in more easily.

Module qat.fermion.hamiltonians provide tools to create Hamiltonian based on these models:

  • Hubbard Hamiltonian: the toy-model of strong correlations - this Hamiltonian can be created using make_hubbard_model()

  • Single-impurity Anderson Model: describes one correlated site embedded in a fermionic bath - this Hamiltonian can be created using make_anderson_model()

  • Embedded Hamiltonian: describes a cell of N correlated sites embedded in a N-level fermionic bath - this Hamiltonian can be created using make_embedded_model()