QUBO as a QAOAProblem Instance#
In this tutorial you’ll be guided through the process of defining a new phase separator to be used within the scope of the Alternating Operator Ansatz focussed on solving various QUBO problems with only needing the QUBO matrix
After first translating the QUBO Hamiltonian
Not to get too ahead of ourselves with how to do it, let’s first show how to solve a QUBO problem using Qrisp. To obtain the optimal solutions to a problem, we only need to know the QUBO matrix
One can then simply minimize the cost function
Let’s borrow the QUBO matrix (explained in depth in the above mentioned tutorial) for the set partitioning problem. The QUBO matrix in that case is
Usually, QUBO matrices are upper triangular (by convention) - this means that only elements above the diagonal (with the diagonal included) are not equal to zero. This is because the variables in QUBO problems are binary, which results in the identity
Of course it’s easy to go from the conventional QUBO
import numpy as np
Q_up_triang = 2 * np.triu(Q_sym) - np.diag(np.diag(Q_sym))
Our implementation of solving QUBO problems using QAOA works for both the upper triangular, as well as symmetrized matrix conventions. As promised, we can now immediately solve the QUBO by calling the solve_QUBO
method:
from qrisp.qaoa import *
import numpy as np
Q = np.array(
[
[-17, 10, 10, 10, 0, 20],
[ 10, -18, 10, 10, 10, 20],
[ 10, 10, -29, 10, 20, 20],
[ 10, 10, 10, -19, 10, 10],
[ 0, 10, 20, 10, -17, 10],
[ 20, 20, 20, 10, 10, -28],
]
)
solve_QUBO(Q, depth = 1, shots=5000)[:5]
That’s it! You can try running this block of code on the website using the Thebe interactivity integration, or run it on your own Qrispy environment.
We see that we obtain the 5 best solutions with the corresponding bitsring of the binary variables.
You can test the statements above, and convert the symmetrized matrix into the upper triangular one, and run solve_QUBO
again.
We see, that we only needed to provide the matrix
From QUBO to QAOA#
Of course it’s beneficial to not only know the “how”, but also understand the “why”. So let’s dig in!
To construct such a circuit with quantum gates and run it on a quantum computer, one has to translate between the basis of
To find
Swapping the indices
Note that for each single
For the cost operator QAOAProblem
, we get
Here, the last factor correspods to a global phase.
Let’s translate this into a function:
def create_QUBO_cost_operator(Q):
def QUBO_cost_operator(qv, gamma):
# Rescaling for enhancing the performance of the QAOA
gamma = gamma/np.sqrt(np.linalg.norm(Q))
gphase(-gamma/4*(np.sum(Q)+np.trace(Q)),qv[0])
for i in range(len(Q)):
rz(-gamma/2*(sum(Q[i])+sum(Q[:,i])), qv[i])
for j in range(len(Q)):
if i != j and Q[i][j] != 0:
rzz(gamma/2*Q[i][j], qv[i], qv[j])
return QUBO_cost_operator
Like we did for MaxCut and MQUBOProblem
blueprint bringing everything together.
from qrisp import rzz, rz, gphase
import numpy as np
def QUBO_obj(bitstring, Q):
x = np.array(list(bitstring), dtype=int)
cost = x.T @ Q @ x
return cost
def create_QUBO_cl_cost_function(Q):
def cl_cost_function(counts):
def QUBO_obj(bitstring, Q):
x = np.array(list(bitstring), dtype=int)
cost = x.T @ Q @ x
return cost
energy = 0
for meas, meas_count in counts.items():
obj_for_meas = QUBO_obj(meas,Q)
energy += obj_for_meas * meas_count
return energy
return cl_cost_function
def QUBO_problem(Q):
from qrisp.qaoa import QAOAProblem, RX_mixer
return QAOAProblem(create_QUBO_cost_operator(Q), RX_mixer, create_QUBO_cl_cost_function(Q))
That’s it for the necessary ingredients you learned about in the QAOA theory 101 section! Let’s solve the set partitioning problem from above using this newly acquired information, and combine with how we already ran the QAOA algorithm using the run
method:
define the QUBO matrix
,define the quantum argument
qarg
as a QuantumArray of QuantumVariables,create the QUBO instance using
QUBO_problem
we defined above,run the algorithm using the
run
method, and last but not least,examine the QAOA solutions and perform for classical post processing: compute the cost functions, sort the solutions by their cost in ascending order, and print the solutions with their costs.
Warning
For small QUBO instance the number of shots
typically exceeds the number of possible solutions.
In this case, even QAOA with depth=0
, i.e., sampling from a uniform superposition, may yield the optimal solution as the classical post-processing amounts to brute force search!
Performance of solve_QUBO
for small instance may not be indicative of performance for large instances.
These are exactly the pieces in the mosaic of code that solve_QUBO
consists of and performs:
from qrisp.default_backend import def_backend
from qrisp import QuantumVariable, QuantumArray
from operator import itemgetter
Q = np.array(
[
[-17, 20, 20, 20, 0, 40],
[ 0, -18, 20, 20, 20, 40],
[ 0, 0, -29, 20, 40, 40],
[ 0, 0, 0, -19, 20, 20],
[ 0, 0, 0, 0, -17, 20],
[ 0, 0, 0, 0, 0, -28],
]
)
qarg = QuantumArray(qtype=QuantumVariable(1), shape=len(Q))
QUBO_instance = QUBO_problem(Q)
depth = 1
res = QUBO_instance.run(qarg, depth, mes_kwargs={"backend" : def_backend}, max_iter=50)
costs_and_solutions = [(QUBO_obj(bitstring, Q), bitstring) for bitstring in res.keys()]
sorted_costs_and_solutions = sorted(costs_and_solutions, key=itemgetter(0))
for i in range(5):
print(f"Solution {i+1}: {sorted_costs_and_solutions[i][1]} with cost: {sorted_costs_and_solutions[i][0]} and probability: {res[sorted_costs_and_solutions[i][1]]}")
Now you are prepared to solve all QUBOs you derive and want to solve. On the other hand, if you would just like to play around instead, try out some QUBOs from this list of QUBO formulations.