import networkx as nx
# Create empty graph
= nx.Graph()
G
# Add edges to the graph (also adds nodes)
1,2),(1,3),(2,4),(3,4),(3,5),(4,5)])
G.add_edges_from([(
nx.draw(G)
QAOA in partice
As we have seen before, QAOA take the best of both worlds (ideally) considering the ansatz out of the AQC but variationaly training it to obtain the best scheduling function out of the free parameters.
Continuing with previous example….
import numpy as np
from collections import defaultdict
# Initialize our Q matrix
= defaultdict(int)
Q
# Update Q matrix for every edge in the graph
for i, j in G.edges:
+= -1
Q[(i,i)]+= -1
Q[(j,j)]+= 2
Q[(i,j)]
# Give it also a matrix shape to ease readability
= G.number_of_nodes()
size = np.zeros((size, size))
H_problem
for k, v in Q.items():
if not isinstance(k, int):
0]-1][k[1]-1] = v
H_problem[k[
H_problem
array([[-2., 2., 2., 0., 0.],
[ 0., -2., 0., 2., 0.],
[ 0., 0., -3., 2., 2.],
[ 0., 0., 0., -3., 2.],
[ 0., 0., 0., 0., -2.]])
For this simple example, brute-forcing the solution can be done to have a reference for it.
import numpy as np
def objective_value(x: np.ndarray, w: np.ndarray) -> float:
"""Compute the value of a cut.
Args:
x: Binary string as numpy array.
w: Adjacency matrix.
Returns:
Value of the cut.
"""
return np.dot(np.dot(x.T, w), x)
def bitfield(n: int, L: int) -> list[int]:
""" Get the binary representation"""
= np.binary_repr(n, L)
result return [int(digit) for digit in result] # [2:] to chop off the "0b" part
# use the brute-force way to generate the oracle
= G.number_of_nodes()
L max = 2**L
= np.inf
sol for i in range(max):
= bitfield(i, L)
cur
= objective_value(np.array(cur), H_problem)
cur_v if cur_v < sol:
= cur_v
sol
print(f'Objective value computed by the brute-force method is {sol}')
Objective value computed by the brute-force method is -5.0
The main challenge would be understanding if this one is the only solution in our example
= [0, 1, 1, 0, 0]
x print(np.dot(np.dot(np.transpose(x), H_problem), x))
-5.0
= [0, 1, 1, 0, 1]
x print(np.dot(np.dot(np.transpose(x), H_problem), x))
-5.0
In fact, for this example we know there are more than one optimal solutions…
from qiskit.visualization import plot_histogram
= {
solution '01101': 0.25,
'10010': 0.25,
'10011': 0.25,
'01100': 0.25,
}
plot_histogram(solution)
We would like to fully understand our solution landscape. Quantum Computing may help us by mapping the solution to a more complex superposed state that reveals all potential solutions to our problem.
We will need to build a Hamiltonian that maps our problem.
from qiskit.quantum_info import SparsePauliOp
= SparsePauliOp.from_list(
H_op
[# h
"ZIIII", -2),
("IZIII", -2),
("IIZII", -3),
("IIIZI", -3),
("IIIIZ", -2),
(# j
"ZZIII", 2),
("ZIZII", 2),
("IZIZI", 2),
("IIZIZ", 2),
("IIIZZ", 2),
(
]
)
print(f"Number of qubits: {H_op.num_qubits}")
Number of qubits: 5
In this case, we will start with the existing Qiskit functionality and do a drill-down to highlight what is done behind the scenes. Qiskit provides a ready to be used QAOA implementation that will only require the and optimizer and a way to extract information out of our quantum device (either Estimator or Sampler primitive).
from qiskit_algorithms.minimum_eigensolvers import QAOA
from qiskit_algorithms.optimizers import COBYLA
from qiskit.primitives import Sampler
= COBYLA()
optimizer = Sampler()
sampler
= QAOA(sampler, optimizer, reps=2)
qaoa = qaoa.compute_minimum_eigenvalue(H_op) result
/tmp/ipykernel_2879/2458342227.py:6: DeprecationWarning: The class ``qiskit.primitives.sampler.Sampler`` is deprecated as of qiskit 1.2. It will be removed no earlier than 3 months after the release date. All implementations of the `BaseSamplerV1` interface have been deprecated in favor of their V2 counterparts. The V2 alternative for the `Sampler` class is `StatevectorSampler`.
sampler = Sampler()
print(result)
{ 'aux_operators_evaluated': None,
'best_measurement': { 'bitstring': '01001',
'probability': np.float64(0.1243940641636303),
'state': 9,
'value': np.complex128(-10+0j)},
'cost_function_evals': np.int64(1000),
'eigenstate': {0: np.float64(0.021639099233232), 1: np.float64(0.042721910373222), 2: np.float64(0.043004844212537), 3: np.float64(0.014446663203729), 4: np.float64(0.043004844212537), 5: np.float64(0.014446663203729), 6: np.float64(0.089079383763242), 7: np.float64(0.000121848163714), 8: np.float64(0.050877026867609), 9: np.float64(0.119947139538904), 10: np.float64(0.017019361306882), 11: np.float64(0.000494869096641), 12: np.float64(0.10733008106455), 13: np.float64(0.02062584081233), 14: np.float64(0.018117452032663), 15: np.float64(0.003101894143553), 16: np.float64(0.050877026867609), 17: np.float64(0.119947139538904), 18: np.float64(0.10733008106455), 19: np.float64(0.02062584081233), 20: np.float64(0.017019361306882), 21: np.float64(0.000494869096641), 22: np.float64(0.018117452032663), 23: np.float64(0.003101894143553), 24: np.float64(0.017981294519271), 25: np.float64(0.029293771873445), 26: np.float64(0.00010312001065), 27: np.float64(0.003014498588115), 28: np.float64(0.00010312001065), 29: np.float64(0.003014498588115), 30: np.float64(0.002786523029701), 31: np.float64(0.000210587287846)},
'eigenvalue': np.float64(-6.205791869365092),
'optimal_circuit': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7fa1b2ca4860>,
'optimal_parameters': { ParameterVectorElement(β[0]): np.float64(-4.830395643681273),
ParameterVectorElement(β[1]): np.float64(6.7226243397721115),
ParameterVectorElement(γ[0]): np.float64(-0.0239723085581648),
ParameterVectorElement(γ[1]): np.float64(6.1559104997985505)},
'optimal_point': array([-4.83039564, 6.72262434, -0.02397231, 6.1559105 ]),
'optimal_value': np.float64(-6.205791869365092),
'optimizer_evals': None,
'optimizer_result': <qiskit_algorithms.optimizers.optimizer.OptimizerResult object at 0x7fa1b673de20>,
'optimizer_time': 6.238818883895874}
from qiskit.visualization import plot_histogram
= {}
counts for key in result.eigenstate:
= format(key, 'b').zfill(5)
key_str = result.eigenstate[key]
counts[key_str]
plot_histogram(counts)
from qiskit.quantum_info import Statevector
from qiskit.result import QuasiDistribution
def sample_most_likely(state_vector: QuasiDistribution | Statevector) -> np.ndarray:
"""Compute the most likely binary string from state vector.
Args:
state_vector: State vector or quasi-distribution.
Returns:
Binary string as an array of ints.
"""
if isinstance(state_vector, QuasiDistribution):
= list(state_vector.values())
values else:
= state_vector
values = int(np.log2(len(values)))
n = np.argmax(np.abs(values))
k = bitfield(k, n)
x
x.reverse()return np.asarray(x)
= sample_most_likely(result.eigenstate)
x
print(x)
print(f'Objective value computed by QAOA is {objective_value(x, H_problem)}')
[1 0 0 1 0]
Objective value computed by QAOA is -5.0
from qiskit_algorithms.optimizers import SLSQP
= SLSQP()
optimizer
= QAOA(sampler, optimizer, reps=4)
qaoa = qaoa.compute_minimum_eigenvalue(H_op)
result print(result)
{ 'aux_operators_evaluated': None,
'best_measurement': { 'bitstring': '01001',
'probability': np.float64(0.2701247492611271),
'state': 9,
'value': np.complex128(-10+0j)},
'cost_function_evals': 470,
'eigenstate': {0: np.float64(0.004064599606105), 1: np.float64(0.003076286573785), 2: np.float64(0.030706450648053), 3: np.float64(7.1755647798e-05), 4: np.float64(0.030706450648053), 5: np.float64(7.1755647798e-05), 6: np.float64(0.028320435762627), 7: np.float64(5.0818055342e-05), 8: np.float64(0.053839603831732), 9: np.float64(0.269749256047177), 10: np.float64(5.0953182088e-05), 11: np.float64(4.5176999086e-05), 12: np.float64(0.100059678333408), 13: np.float64(0.002598820866412), 14: np.float64(0.001161158163038), 15: np.float64(0.000619142808457), 16: np.float64(0.053839603831732), 17: np.float64(0.269749256047177), 18: np.float64(0.100059678333408), 19: np.float64(0.002598820866412), 20: np.float64(5.0953182088e-05), 21: np.float64(4.5176999086e-05), 22: np.float64(0.001161158163038), 23: np.float64(0.000619142808457), 24: np.float64(0.003603195962349), 25: np.float64(0.033604043998763), 26: np.float64(0.002110243058402), 27: np.float64(0.001555949397643), 28: np.float64(0.002110243058402), 29: np.float64(0.001555949397643), 30: np.float64(0.000451350984569), 31: np.float64(0.001692891089871)},
'eigenvalue': np.float64(-8.221858785028452),
'optimal_circuit': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7fa1b2af5ac0>,
'optimal_parameters': { ParameterVectorElement(γ[2]): np.float64(-5.946134192764529),
ParameterVectorElement(γ[3]): np.float64(1.6314848248665674),
ParameterVectorElement(β[3]): np.float64(-6.420119003008715),
ParameterVectorElement(β[0]): np.float64(-3.908617563045461),
ParameterVectorElement(γ[1]): np.float64(6.488849120678284),
ParameterVectorElement(γ[0]): np.float64(-6.182045438862662),
ParameterVectorElement(β[1]): np.float64(2.795613714170744),
ParameterVectorElement(β[2]): np.float64(-6.516566899789847)},
'optimal_point': array([-3.90861756, 2.79561371, -6.5165669 , -6.420119 , -6.18204544,
6.48884912, -5.94613419, 1.63148482]),
'optimal_value': np.float64(-8.221858785028452),
'optimizer_evals': None,
'optimizer_result': <qiskit_algorithms.optimizers.optimizer.OptimizerResult object at 0x7fa1c578a8d0>,
'optimizer_time': 3.6844029426574707}
= {}
counts for key in result.eigenstate:
= format(key, 'b').zfill(5)
key_str = result.eigenstate[key]
counts[key_str]
plot_histogram(counts)
= sample_most_likely(result.eigenstate)
x
print(x)
print(f'Objective value computed by QAOA is {objective_value(x, H_problem)}')
[1 0 0 1 0]
Objective value computed by QAOA is -5.0
= SLSQP(
optimizer =100000
maxiter
)
= QAOA(sampler, optimizer, reps=1)
qaoa = qaoa.compute_minimum_eigenvalue(H_op)
result print(result)
{ 'aux_operators_evaluated': None,
'best_measurement': { 'bitstring': '01001',
'probability': np.float64(0.1074237070767812),
'state': 9,
'value': np.complex128(-10+0j)},
'cost_function_evals': 44,
'eigenstate': {0: np.float64(0.045598314903732), 1: np.float64(0.062029433613113), 2: np.float64(0.060058176296937), 3: np.float64(0.011971652404294), 4: np.float64(0.060058176296937), 5: np.float64(0.011971652404294), 6: np.float64(0.074493641660212), 7: np.float64(0.000224040528429), 8: np.float64(0.07286652303499), 9: np.float64(0.107405832880699), 10: np.float64(0.014585421320138), 11: np.float64(0.000737000856106), 12: np.float64(0.093248646992269), 13: np.float64(0.01343793216785), 14: np.float64(0.01130276641885), 15: np.float64(0.00193658918612), 16: np.float64(0.07286652303499), 17: np.float64(0.107405832880699), 18: np.float64(0.093248646992269), 19: np.float64(0.01343793216785), 20: np.float64(0.014585421320138), 21: np.float64(0.000737000856106), 22: np.float64(0.01130276641885), 23: np.float64(0.00193658918612), 24: np.float64(0.015976627482065), 25: np.float64(0.021006773147314), 26: np.float64(5.2122244888e-05), 27: np.float64(0.001842883403979), 28: np.float64(5.2122244888e-05), 29: np.float64(0.001842883403979), 30: np.float64(0.001629904302817), 31: np.float64(0.000150169948081)},
'eigenvalue': np.float64(-6.1205242665487605),
'optimal_circuit': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7fa1b4f35eb0>,
'optimal_parameters': { ParameterVectorElement(β[0]): np.float64(-3.5737677054330645),
ParameterVectorElement(γ[0]): np.float64(-3.0127287460660455)},
'optimal_point': array([-3.57376771, -3.01272875]),
'optimal_value': np.float64(-6.1205242665487605),
'optimizer_evals': None,
'optimizer_result': <qiskit_algorithms.optimizers.optimizer.OptimizerResult object at 0x7fa1b2ae3560>,
'optimizer_time': 0.1510608196258545}
= {}
counts for key in result.eigenstate:
= format(key, 'b').zfill(5)
key_str = result.eigenstate[key]
counts[key_str]
plot_histogram(counts)
= sample_most_likely(result.eigenstate)
x
print(x)
print(f'Objective value computed by QAOA is {objective_value(x, H_problem)}')
[1 0 0 1 0]
Objective value computed by QAOA is -5.0
Let’s check what is going on behind it. We can check the ansatz that QAOA creates.
'mpl', fold=150) qaoa.ansatz.draw(
'mpl', fold=150) qaoa.ansatz.decompose().draw(
=2).draw('mpl', fold=150) qaoa.ansatz.decompose(reps
As we can see the ansatz is build following the Adiabatic Quantum Computing scheme where a block of \(X\) rotations is applied, and alternated with the problem hamiltonian (\(Z + ZZ\) interactions) as many times as repetitions are performed.
= QAOA(sampler, optimizer, reps=4)
qaoa = qaoa.compute_minimum_eigenvalue(H_op)
result print(result)
{ 'aux_operators_evaluated': None,
'best_measurement': { 'bitstring': '01001',
'probability': np.float64(0.344827135881408),
'state': 9,
'value': np.complex128(-10+0j)},
'cost_function_evals': 544,
'eigenstate': {0: np.float64(0.001238399892321), 1: np.float64(0.009492982540968), 2: np.float64(0.003688431063221), 3: np.float64(0.002508490269391), 4: np.float64(0.003688431063221), 5: np.float64(0.002508490269391), 6: np.float64(7.5811893712e-05), 7: np.float64(3.6744205009e-05), 8: np.float64(0.100644549242893), 9: np.float64(0.344684239341494), 10: np.float64(0.000255326907557), 11: np.float64(0.000893389549746), 12: np.float64(0.00297345829819), 13: np.float64(0.007625551299671), 14: np.float64(0.000877411225766), 15: np.float64(0.000138471084564), 16: np.float64(0.100644549242893), 17: np.float64(0.344684239341494), 18: np.float64(0.00297345829819), 19: np.float64(0.007625551299671), 20: np.float64(0.000255326907557), 21: np.float64(0.000893389549746), 22: np.float64(0.000877411225766), 23: np.float64(0.000138471084564), 24: np.float64(0.000542246793659), 25: np.float64(0.051827121434138), 26: np.float64(0.003639527275999), 27: np.float64(0.000235139553224), 28: np.float64(0.003639527275999), 29: np.float64(0.000235139553224), 30: np.float64(0.000365393864454), 31: np.float64(9.3329152307e-05)},
'eigenvalue': np.float64(-8.566230368532523),
'optimal_circuit': <qiskit.circuit.quantumcircuit.QuantumCircuit object at 0x7fa1aa99f170>,
'optimal_parameters': { ParameterVectorElement(β[0]): np.float64(5.7114312136037455),
ParameterVectorElement(β[1]): np.float64(-4.0004699962555215),
ParameterVectorElement(γ[1]): np.float64(-4.699165416536413),
ParameterVectorElement(β[3]): np.float64(-3.4480364333298197),
ParameterVectorElement(γ[3]): np.float64(3.7909065471419217),
ParameterVectorElement(β[2]): np.float64(-2.359185058052364),
ParameterVectorElement(γ[2]): np.float64(-6.149830986991044),
ParameterVectorElement(γ[0]): np.float64(6.3961840198422735)},
'optimal_point': array([ 5.71143121, -4.00047 , -2.35918506, -3.44803643, 6.39618402,
-4.69916542, -6.14983099, 3.79090655]),
'optimal_value': np.float64(-8.566230368532523),
'optimizer_evals': None,
'optimizer_result': <qiskit_algorithms.optimizers.optimizer.OptimizerResult object at 0x7fa1aaea3d70>,
'optimizer_time': 4.202273607254028}
'mpl', fold=150) qaoa.ansatz.draw(
'mpl', fold=150) qaoa.ansatz.decompose().decompose().draw(
Obtained solution distribution really points out to the solutions that provide solutions to our problem, superposed in a state that is the one minimizing the outcome of the circuit with respect to the expectation value of the hamitlonian.
= {}
counts for key in result.eigenstate:
= format(key, 'b').zfill(5)
key_str = result.eigenstate[key]
counts[key_str]
plot_histogram(counts)
Therefore, we are able to encode our problem into an Ising Hamiltonian, so that we can easily design a circuit to obtain the results from the potential solution landscape. We may need to set up the repetitions needed in order to map the optimal parameters to our variationaly trained scheduling function.
qaoa.ansatz.depth()
10
Small problems will render shallow circuits, but we need to be aware that introducing many repetitions of those blocks or layers, it will require more operations to be squeezed in, the lifespan of the cubits will need to be longer and the more operations we add, more noise it will enter into our final state.