The 'Hello World' equivalent in the D-Wave world is the 2D checkerboard example. In this example, you are given the following square graph with 4 nodes:
Let's define that we colour vertex $\sigma_{i}$ black if $\sigma_{i} = -1$ and white if $\sigma_{i} = +1$. The goal is to create a checkerboard pattern with the four vertices in the graph. There is various ways of defining
$h$ and $J$ to achieve this result. First of all, there are two possible solutions to this problem:
The D-Wave quantum annealer minimizes the Ising Hamiltonian that we define and
it is important to understand the effect of the different coupler settings. Consider for example the $J_{0,1}$ coupler:
If we set it to $J_{0,1}=-1$, the Hamiltonian is minimized if both qubits take the same value. We say negative couplers correlate. Whereas if we set it to $J_{0,1}=+1$, the Hamiltonian is minimized if the two qubits take opposite values. Thus, positive couplers anti-correlate.
In the checkerboard example, we want to anti-correlate each pair of neighbouring qubits which gives rise to the following Hamiltonian:
$$H = \sigma_{0}\sigma_{1} + \sigma_{0}\sigma_{2} + \sigma_{1}\sigma_{3} + \sigma_{2}\sigma_{3}$$
For the sake of demonstration, we also add a bias term on the $0$-th qubit such that we only get solution #1. This solution requires $\sigma_{0}=-1$ and we therefore set its bias $h_{0}=1$. The final Hamiltonian is now:
$$H = \sigma_{0} + \sigma_{0}\sigma_{1} + \sigma_{0}\sigma_{2} + \sigma_{1}\sigma_{3} + \sigma_{2}\sigma_{3}$$
So let's code it up!
NOTE: You DO NEED access to D-Wave's Cloud Service for anything to work.
First of all, make sure you have the dwave_sapi2
(https://cloud.dwavesys.com/qubist/downloads/) Python package installed. Everything is going to be Python 2.7 since D-Wave currently doesn't support any higher Python version. That being said, let's import the essentials:
from dwave_sapi2.core import solve_ising
from dwave_sapi2.embedding import find_embedding, embed_problem, unembed_answer
from dwave_sapi2.util import get_hardware_adjacency
from dwave_sapi2.remote import RemoteConnection
In order to connect to the D-Wave Solver API you will need a valid API token for their SAPI solver, the SAPI URL and you need to decide which quantum processor you want to use:
DWAVE_SAPI_URL = 'https://cloud.dwavesys.com/sapi'
DWAVE_TOKEN = [your D-Wave API token]
DWAVE_SOLVER = 'DW_2000Q_VFYC_1'
I recommend using the D-Wave 2000Q Virtual Full Yield Chimera (VFYC) which is a fully functional chip without any dead qubits! Here's the Chimera chip layout:
At this point I am splitting the tutorial into two distinct pieces. In the first section, we are manually embedding the problem onto the Chimera hardware graph and in the second section we are using D-Wave's embedding heuristics to find a hardware embedding.
Manual embedding
The unit cell in the top left corner on the D-Wave 2000Q chip layout above looks like this:
Note, that not all couplers are visualized in this image. As you can see, there is no coupler between qubit $0$ and qubit $1$ which we would need to directly implement our square graph above. That's why we are now redefining $0\rightarrow0$, $1\rightarrow4$, $2\rightarrow7$ and $3\rightarrow3$. We then go on and define $h$ as a list and $J$ as a dictionary:
J = {(0,4): 1, (4,3): 1, (3,7): 1, (7,0): 1}
h = [-1,0,0,0,0,0,0,0,0]
$h$ has 8 entries since we use qubits 0 to 7. We now establish connection to the Solver API and request the D-Wave 2000Q VFYC solver:
connection = RemoteConnection(DWAVE_SAPI_URL, DWAVE_TOKEN)
solver = connection.get_solver(DWAVE_SOLVER)
Now, we can define the number of readouts and choose answer_mode
to be "histogram" which already sorts the results by the number of occurrences for us. We are now ready to solve the Ising instance with the D-Wave quantum annealer:
params = {"answer_mode": 'histogram', "num_reads": 10000}
results = solve_ising(solver, h, J, **params)
print results
You should get the following result:
{
'timing': {
'total_real_time': 1655206,
'anneal_time_per_run': 20,
'post_processing_overhead_time': 13588,
'qpu_sampling_time': 1640000,
'readout_time_per_run': 123,
'qpu_delay_time_per_sample': 21,
'qpu_anneal_time_per_sample': 20,
'total_post_processing_time': 97081,
'qpu_programming_time': 8748,
'run_time_chip': 1640000,
'qpu_access_time': 1655206,
'qpu_readout_time_per_sample': 123
},
'energies': [-5.0],
'num_occurrences': [10000],
'solutions': [
[1, 3, 3, 1, -1, 3, 3, -1, {
lots of 3 's that I am omitting}]]}
As you can see we got the correct ground state energy (energies
) of $-5.0$. The solution string is full of $3$'s which is the default outcome for unused/unmeasured qubits and if we apply the reverse transformations - $0\rightarrow0$, $4\rightarrow1$, $7\rightarrow2$ and $3\rightarrow3$ - we get the correct solution string $[1, -1, -1, 1]$. Done!
Heuristic embedding
If you start creating larger and larger Ising instances you will not be able to perform manual embedding. So let's suppose we can't manually embed our 2D checkerboard example. $J$ and $h$ then remain unchanged from our initial definitions:
J = {(0,1): 1, (0,2): 1, (1,3): 1, (2,3): 1}
h = [-1,0,0,0]
We again establish the remote connection and get the D-Wave 2000Q VFYC solver instance:
connection = RemoteConnection(DWAVE_SAPI_URL, DWAVE_TOKEN)
solver = connection.get_solver(DWAVE_SOLVER)
In order to find an embedding of our problem, we need to first get the adjacency matrix of the current hardware graph:
adjacency = get_hardware_adjacency(solver)
Now let's try to find an embedding of our problem:
embedding = find_embedding(J.keys(), adjacency)
If you are dealing with large Ising instances you might want to search for embeddings in multiple threads (parallelized over multiple CPUs) and then select the embedding with the smallest chain length! A chain is when multiple qubits are forced to act as a single qubit in order to increase the degree of connectivity. However, the longer the chain the more likely that it breaks. And broken chains give bad results!
We are now ready to embed our problem onto the graph:
[h, j0, jc, embeddings] = embed_problem(h, J, embedding, adjacency)
j0
contains the original couplings that we defined and jc
contains the couplings that enforce the integrity of the chains (they correlate the qubits within the chains). Thus, we need to combine them again into one big $J$ dictionary:
J = j0.copy()
J.update(jc)
Now, we're ready to solve the embedded problem:
params = {"answer_mode": 'histogram', "num_reads": 10000}
raw_results = solve_ising(solver, h, J, **params)
print 'Lowest energy found: {}'.format(raw_results['energies'])
print 'Number of occurences: {}'.format(raw_results['num_occurrences'])
The raw_results
will not make sense to us unless we unembed the problem. In case, some chains broke we are fixing them through a majority vote as defined by the optional argument broken_chains
:
unembedded_results = unembed_answer(raw_results['solutions'],
embedding, broken_chains='vote')
print 'Solution string: {}'.format(unembedded_results)
If you run this, you should get the correct result in all readouts:
Lowest energy found: [-5.0]
Number of occurences: [10000]
Solution string: [[1, -1, -1, 1]]
I hope this answered your question and I highly recommend checking out all the additional parameters that you can pass to the solve_ising
function to improve the quality of your solutions such as num_spin_reversal_transforms
or postprocess
.