An example with a fermionic Hamiltonian¶

QutipQPU can support schedules and observables written in a second-quantized language (not necessarily only spins as is customary). Here, we show a small example on how to evolve a fermionic Hamiltonian.

Let us consider the case of two spinless fermions interacting via an interaction term with strength $U$ and a hopping (kinetic) term $J(t)$:

$$\large H(t) = J(t) \left( c_0^\dagger c_1 + c_1^\dagger c_0 \right ) + U n_0 n_1 $$

On the QLM we use the letter C for $c^\dagger$ and c for $c$ to encode the fermionic creation and annihilation operators, respectively. These come together to define the number operator $n_i = c^\dagger_i c_i$ in the above example. With this in mind, let's construct the Schedule encapsulating $H(t)$ and its evolution time:

In [1]:
import numpy as np
from qat.core import Observable, Term, Schedule
from qat.core.variables import Variable, sin

# Define a time Variable
t = Variable("t", float)

# Create and display the Schedule
J = sin(t)
U = 1.0
evolution_duration = np.pi
drive = [(J, Observable(2, pauli_terms=[Term(1, "Cc", [0, 1]), Term(1, "Cc", [1, 0])])), 
         (U, Observable(2, pauli_terms=[Term(1, "CcCc", [0, 0, 1, 1])])) ]
schedule = Schedule(drive=drive,
                    tmax=evolution_duration)
schedule.display()
No description has been provided for this image

Define an observable¶

We are interested in the expectation value of the following observable:

$ \hat O = n_0 $ i.e the occupation of site $0$.

In the cell below, we define such an observable:

In [2]:
obs = Observable(2, pauli_terms=[Term(1, 'Cc', [0, 0])])

Simulate in a noisy and noiseless setting¶

We then do a simulation by constructing a Job and submitting it to QutipQPU. In this job, we also specify the initial state, here $\left|01\right>$, i.e. orbital $0$ is empty while orbital $1$ is occupied.

We simulate with two processors:

  • an ideal processor (with no noise)
  • a processor with dephasing noise (via Lindblad operators $n_0$ and $n_1$)
In [3]:
# Construct the Job
job = schedule.to_job(job_type="OBS", observable=obs, psi_0="01")
In [4]:
from qat.qpus import QutipQPU

# Create a noiseless QPU and submit the job
qpu = QutipQPU(sim_method="deterministic",
               nsteps=50) # number of time steps for integration
res = qpu.submit(job)
print("noiseless <n_0 (t_max)> =", res.value)
[1/1] Cythonizing qtcoeff_f230db93af3a399e4166e330cf4663.pyx
running build_ext
building 'qtcoeff_f230db93af3a399e4166e330cf4663' extension
g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -I/usr/local/lib64/openmpi/../../include/openmpi-x86_64 -fPIC -I/usr/local/qaptiva/python_references/112/rhel9/ref312/lib/python3.12/site-packages/qutip/core/data -I/usr/local/qaptiva/python_references/112/rhel9/ref312/lib/python3.12/site-packages/numpy/_core/include -I/usr/local/qaptiva/python_references/112/rhel9/ref312/include -I/usr/local/include/python3.12 -c qtcoeff_f230db93af3a399e4166e330cf4663.cpp -o build/temp.linux-x86_64-cpython-312/qtcoeff_f230db93af3a399e4166e330cf4663.o -w -O3 -funroll-loops
g++ -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -I/usr/local/lib64/openmpi/../../include/openmpi-x86_64 -shared -L/usr/local/lib64/openmpi/lib -I/usr/local/lib64/openmpi/../../include/openmpi-x86_64 build/temp.linux-x86_64-cpython-312/qtcoeff_f230db93af3a399e4166e330cf4663.o -L/usr/local/lib -o build/lib.linux-x86_64-cpython-312/qtcoeff_f230db93af3a399e4166e330cf4663.cpython-312-x86_64-linux-gnu.so
copying build/lib.linux-x86_64-cpython-312/qtcoeff_f230db93af3a399e4166e330cf4663.cpython-312-x86_64-linux-gnu.so -> 
Done
Did not find any quantum register info: wrapping results with a dummy BITSTRING register of length 0
Post processing a list of results of length 1
noiseless <n_0 (t_max)> = 0.8268208165630487
In [5]:
from qat.hardware import HardwareModel

# Describe the noisy environment with jump operators
jump_ops=[Observable(2, pauli_terms=[(Term(0.5, 'Cc', [0, 0]))]), 
          Observable(2, pauli_terms=[(Term(0.5, 'Cc', [1, 1]))])]
hardware_model = HardwareModel(jump_operators=jump_ops)

# Create a 'noisy' QPU
qpu_noisy = QutipQPU(hardware_model=hardware_model,
                     sim_method="deterministic",
                     nsteps=50)
# Submit the job
res_noisy = qpu_noisy.submit(job)
print("noisy <n_0 (t_max)> =", res_noisy.value)
New batch of length 1 submitted
Compiling a batch of length 1
Starting compilation...
Returning compiled batch.
Resource management is not available, passing through.
Running jobs...
Done
Did not find any quantum register info: wrapping results with a dummy BITSTRING register of length 0
Post processing a list of results of length 1
noisy <n_0 (t_max)> = 0.7805360058248992

Observable's value over time¶

We can plot how the value of the observable changes over time - these values are stored in the value_data field of the Result object:

In [6]:
import matplotlib.pyplot as plt

# Plotting
time_points = [float(i) for i in res.value_data.keys()]
plt.plot(time_points, [res.value_data[k].re for k in res.value_data.keys()],
         label="ideal processor")
plt.plot(time_points, [res_noisy.value_data[k].re for k in res_noisy.value_data.keys()],
         label="noisy processor")

# Labelling
plt.legend(loc="best")
plt.xlabel(r"time $t$")
plt.ylabel(r"$\langle n_0 \rangle$")
plt.grid()
No description has been provided for this image

We see that as expected, at time $t=0$, the occupation of orbital $0$ is $0$, and that this occupation increases over time - less in the noisy setting.

For general fermionic computations, one can look at the dedicated library qat-fermion.