In the QLM API, it is possible to submit jobs that contain a quantum circuit and some observable to sample on the output quantum state.
my_job = circuit.to_job(observable=my_obs)
These jobs are atomic computation tasks from the API point of view. In some cases, however, it can happen that some QPU does not natively supports observable evaluation.
The ObservableSplitter
plugin is here to fill this gap and allow any stack containing a "sampling only" QPU to be able to evaluate observables. The nice thing is that the algorithmic mechanics behind computing an observable using solely computational basis samples is entirely handled by the plugin, transparently for the user.
Lets see how the plugin works:
# To write circuits
from qat.lang.AQASM import *
# To define an observable
from qat.core import Observable, Term
# our Plugin
from qat.plugins import ObservableSplitter
# and a QPU
from qat.qpus import get_default_qpu
# Our circuit:
prog = Program()
qbits = prog.qalloc(2)
prog.apply(H, qbits[0])
prog.apply(CNOT, qbits)
bell = prog.to_circ()
# Our observable: it counts the parity of the quantum state
obs = Observable(2, pauli_terms=[Term(-0.5, "ZZ", [0, 1])],
constant_coeff=0.5)
print("Observable:\n", obs)
my_job = bell.to_job(observable=obs)
# We can always use our default qpu to directly run this job:
result = get_default_qpu().submit(my_job)
print("Result:", result.value)
This is however not realistic. If our QPU were to be a proper quantum device, or maybe just another simulator, it might not be able to handle observable sampling natively.
For this purpose, we can use the ObservableSplitter
plugin, like so:
stack = ObservableSplitter() | get_default_qpu()
print("Result:", stack.submit(my_job).value)
The plugin comes with two distinct algorithms to sample some observable:
# This observable has 4 terms that can be grouped into 2 groups of commutating terms.
obs = Observable(3, pauli_terms=[Term(1., "ZZZ", [0,1,2]),
Term(1., "X", [0]), Term(1., "X", [1]), Term(1., "X", [2])])
print(obs)
# We will use a dummy circuit:
prog = Program()
qbits = prog.qalloc(3)
circuit = prog.to_circ()
job = circuit.to_job(observable=obs)
from qat.core import Batch
batch = Batch(jobs=[job])
plugin_naive = ObservableSplitter(splitting_method="naive")
naive_batch = plugin_naive.compile(batch, None)
print("We need to sample", len(naive_batch.jobs), "circuits")
plugin_naive = ObservableSplitter(splitting_method="coloring")
coloring_batch = plugin_naive.compile(batch, None)
print("We need to sample", len(coloring_batch.jobs), "circuits")
In order to generate the sampling jobs, the Plugin needs to inject basis change instructions at the end of the initial circuit.
For instance, if one need to sample a $X$ operator, the plugin will append a $H$ gate at the end of the circuit, and sample the corresponding qubit in the computational basis ($Z$).
However, some hardware might not support $H$ gates. Luckily, the ObservableSplitter
allow us to provide any subcircuit performing the appropriate basis change. Lets have a look at its constructor:
help(ObservableSplitter)
The constructor requires 2 functions : x_basis_change
and y_basis_change
.
This functions take as parameter the index of the qubit to rotate and the total number of qubits, and should return a QRoutine of arity equal to the number of qubits (this is just to encompass the most general sceneari).
For instance, if our hardware does not supports Hadamard gates, one can imagine performing a sequence of $R_x(\pi/2)Rz(\pi/2)Rx(\pi/2)$:
import numpy as np
def my_x_basis_change(index, nbqbits):
rout = QRoutine()
wires = rout.new_wires(nbqbits)
rout.apply(RX(np.pi/2), wires[index])
rout.apply(RZ(np.pi/2), wires[index])
rout.apply(RX(np.pi/2), wires[index])
return rout
plugin_custom = ObservableSplitter(splitting_method="coloring", x_basis_change=my_x_basis_change)
plugin = ObservableSplitter(splitting_method="coloring")
default_batch = plugin.compile(batch, None)
custom_batch = plugin_custom.compile(batch, None)
print("Default plugin:")
for ind, job in enumerate(default_batch.jobs):
print("Circuit", ind)
for op in job.circuit.iterate_simple():
print(op)
print("Custom plugin:")
for ind, job in enumerate(custom_batch.jobs):
print("Circuit", ind)
for op in job.circuit.iterate_simple():
print(op)