Analog QPUs on the QLM: a short demonstration¶

The jobs (Job) that are input to analog quantum computers (analog QPUs for short) are different from those fed to digital QPUs. While digital QPUs take jobs consisting of a Circuit, analog QPUs take jobs consisting of a Schedule, which encodes the evolution of a Hamiltonian for some given time.
Meanwhile, both analog and digital approaches can measure an Observable at the end of the simulation.

Defining the schedule¶

The first step is to define the time-dependent Hamiltonian $H(t)$ of the Schedule. Mathematically, one can always decompose $H(t)$ as:

$$ H(t) = \sum_i \lambda_i(t) H_i $$

To describe such a time-dependent Hamiltonian on the QLM, one should provide the drive of the Schedule with a list of pairs $\lbrace(\lambda_i(t), H_i)\rbrace$. In this list $\lambda_i(t)$ is a time-dependent expression specified via a Variable (or just a float) and $H_i$ is a time-independent Observable.

A simple one-qubit example¶

Let us take a look at the following time-dependent Hamiltonian:

$$H(t) = 2 t \cos(t) \left ( \sigma^z_0 + 3 \sigma^x_0 \right) + \Theta(t, 0, 0.5) \sigma^y_0$$

with $\Theta(t, t_1, t_2) = 1$ for $t_1 \leq t \leq t_2$.

We assume that the Hamiltonian would be executed for a duration of 2 units of time. Here is how to represent this $H(t)$ on the QLM:

In [1]:
%matplotlib inline
from qat.core import Observable, Term, Schedule
from qat.core.variables import Variable, cos, heaviside

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

# Encode the Hamiltonian and create a Schedule
drive = [(2 * t * cos(t),        Observable(1, pauli_terms=[Term(1, "Z", [0]), Term(3, "X", [0])])), 
         (heaviside(t, 0., 0.5), Observable(1, pauli_terms=[Term(1, "Y", [0])])) ]
evolution_duration = 2.0
schedule = Schedule(drive=drive,
                    tmax=evolution_duration)
schedule.display()
No description has been provided for this image

Building a Job to compute the expectation value of an observable¶

We start by defining the observable (sometimes called the target Hamiltonian), that we want to measure at the end of the evolution. For example:

$$H_\mathrm{target} = \sigma^x_0 $$

Ultimately, the analog QPU will return the expectation value $$\langle H_\mathrm{target} \rangle = \mathrm{Tr} \left[ \rho(t_\mathrm{f}) H_\mathrm{target} \right ]$$

with $\rho(t_\mathrm{f})$ the (mixed) density matrix of the system at the final time $t_\mathrm{f}$. Note that in the absence of noise, this becomes simply

$$\langle H_\mathrm{target} \rangle = \langle \Psi(t_\mathrm{f}) | H_\mathrm{target} | \Psi(t_\mathrm{f}) \rangle.$$

In [2]:
H_target = Observable(1, pauli_terms=[Term(1, 'X', [0])])
job_obs = schedule.to_job(job_type="OBS",  observable=H_target)

Noiseless simulations¶

A quantum system without noise can be evolved obeying the Schrödinger equation

$$i \hbar \frac{\partial \psi(t)}{\partial t} = H(t) \, \psi(t)$$

To simulate the evolution we would need to send the job to an analog QPU. The QLM provides two such QPUs - QutipQPU and AnalogQPU. Let us choose the first one, say (they are largely interchangeable, as in this case).

In [3]:
from qat.qpus import QutipQPU

qpu = QutipQPU() # no further arguments needed at this stage

# Measure the observable
res_obs = qpu.submit(job_obs)
print("<H_target> =", res_obs.value)
[1/1] Cythonizing qtcoeff_99053cb7cab9f6304df06c8370de3d.pyx
running build_ext
building 'qtcoeff_99053cb7cab9f6304df06c8370de3d' extension
creating build/temp.linux-x86_64-cpython-312
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_99053cb7cab9f6304df06c8370de3d.cpp -o build/temp.linux-x86_64-cpython-312/qtcoeff_99053cb7cab9f6304df06c8370de3d.o -w -O3 -funroll-loops
creating build/lib.linux-x86_64-cpython-312
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_99053cb7cab9f6304df06c8370de3d.o -L/usr/local/lib -o build/lib.linux-x86_64-cpython-312/qtcoeff_99053cb7cab9f6304df06c8370de3d.cpython-312-x86_64-linux-gnu.so
copying build/lib.linux-x86_64-cpython-312/qtcoeff_99053cb7cab9f6304df06c8370de3d.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
<H_target> = 0.632214189235936

Execution in SAMPLE mode¶

Similarly to quantum circuits, one can also execute jobs in SAMPLE mode, i.e ask for the probabilities (and also the amplitudes in case of noiseless simulations) contained in the final wavefunction. In this case no observable is given to the schedule.

In [4]:
# Create a 'sample' job and send it to the QPU
job_sample = schedule.to_job()
res_sample = qpu.submit(job_sample)

# The Result object will contain Samples - one per final state
for sample in res_sample:
    print(sample.state, sample.amplitude, sample.probability)
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
Wrapping results using 1 quantum registers
Post processing a list of results of length 1
|0> (-0.7748172693976974-0.509691612656145j) 0.8601273409689257
|1> (-0.20014965449215383-0.3159315983527856j) 0.1398726590310744

Noisy simulations - specifying a noise model¶

One way to describe the noisy interaction of a quantum system with the environment is via the so-called Lindblad jump operators $L_i$. These come from the Master equation evolution of the density matrix $\rho$:

$$\frac{\partial \rho}{\partial t} = -i[H, \rho] + \sum_i \left( L_i^\dagger \rho L_i - \frac{1}{2}L_i^\dagger L_i \rho - \frac{1}{2}\rho L_i^\dagger L_i \right)$$

On the QLM, $L_i$ are specified with Observable in a HardwareModel which enter the analog QPU (just like for NoisyQProc). Let us chose jump operators that correspond to dephasing noise, i.e. $L = 0.4 \sigma^z_0$ and define a noisy QutipQPU.

In [5]:
from qat.hardware import HardwareModel
hardware_model = HardwareModel(jump_operators=[Observable(1, pauli_terms=[(Term(0.4, 'Z', [0]))])])
qpu = QutipQPU(hardware_model=hardware_model,
               nsteps=100, # can specify the number of time steps for the integration
               sim_method="deterministic")

In the cell above, one can replace the default deterministic simulation method (a density-matrix based simulator) with stochastic (a stochastic Monte Carlo simulator). In such a case n_samples should also be provided, i.e. the number of trajectories to average over.
The QPU is now ready to simulate the jobs:

In [6]:
# OBS Mode
res_obs = qpu.submit(job_obs)
print("<H_target> =", res_obs.value)

# SAMPLE mode
job_sample = schedule.to_job(nbshots=0)
res_sample = qpu.submit(job_sample)
for sample in res_sample:
    amplitude = sample.amplitude
    print(sample.state, amplitude if amplitude is not None else "", sample.probability)
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
<H_target> = 0.39023399649953017
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
Wrapping results using 1 quantum registers
Post processing a list of results of length 1
|0>  0.7310309537470259
|1>  0.26896904625297413

Note that the Samples do not have amplitudes, since we are performing noisy simulations.

So far, only nbshots=0 is implemented, i.e we assume perfect statistics (infinite number of repetitions).

Further possibilities¶

  • Of course, we can go beyond one-qubit schedules or beyond one-level quantum systems - see this notebook.