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)
Digitized AQC
We will use our previous example on MaxCut just to take it simple.
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)]
from dimod import ExactSolver
# Set up QPU parameters
= 8
chainstrength = 10
numruns
# Run the QUBO on the solver from your config file
= ExactSolver()
sampler = sampler.sample_qubo(Q) response
print('-' * 60)
print('{:>15s}{:>15s}{:^15s}{:^15s}'.format('Set 0','Set 1','Energy','Cut Size'))
print('-' * 60)
for sample, E in response.data(fields=['sample','energy']):
= [k for k,v in sample.items() if v == 0]
S0 = [k for k,v in sample.items() if v == 1]
S1 print('{:>15s}{:>15s}{:^15s}{:^15s}'.format(str(S0),str(S1),str(E),str(int(-1*E))))
------------------------------------------------------------
Set 0 Set 1 Energy Cut Size
------------------------------------------------------------
[1, 4, 5] [2, 3] -5.0 5
[2, 3, 5] [1, 4] -5.0 5
[1, 4] [2, 3, 5] -5.0 5
[2, 3] [1, 4, 5] -5.0 5
[2, 5] [1, 3, 4] -4.0 4
[2, 3, 4] [1, 5] -4.0 4
[1, 2, 5] [3, 4] -4.0 4
[1, 5] [2, 3, 4] -4.0 4
[3, 4] [1, 2, 5] -4.0 4
[1, 3, 4] [2, 5] -4.0 4
[1, 2, 3, 5] [4] -3.0 3
[1, 3, 5] [2, 4] -3.0 3
[3, 5] [1, 2, 4] -3.0 3
[1, 2, 4, 5] [3] -3.0 3
[4, 5] [1, 2, 3] -3.0 3
[2, 4, 5] [1, 3] -3.0 3
[4] [1, 2, 3, 5] -3.0 3
[2, 4] [1, 3, 5] -3.0 3
[1, 2, 4] [3, 5] -3.0 3
[1, 3] [2, 4, 5] -3.0 3
[3] [1, 2, 4, 5] -3.0 3
[1, 2, 3] [4, 5] -3.0 3
[3, 4, 5] [1, 2] -2.0 2
[1, 3, 4, 5] [2] -2.0 2
[2] [1, 3, 4, 5] -2.0 2
[5] [1, 2, 3, 4] -2.0 2
[2, 3, 4, 5] [1] -2.0 2
[1] [2, 3, 4, 5] -2.0 2
[1, 2, 3, 4] [5] -2.0 2
[1, 2] [3, 4, 5] -2.0 2
[1, 2, 3, 4, 5] [] 0.0 0
[][1, 2, 3, 4, 5] 0.0 0
from qiskit.visualization import plot_histogram
= {
solution '01101': 0.25,
'10010': 0.25,
'10011': 0.25,
'01100': 0.25,
}
plot_histogram(solution)
Ok, we don’t always know the solution in advance but it is important for educational purposes. Let’s take the three pieces from our digitized AQC and built block by block, starting by the quantum state that is the ground state of our \(H_{init}=-\sum_n \sigma_i^n\)
from qiskit import QuantumCircuit
= G.number_of_nodes()
size
= QuantumCircuit(size)
initial_state
# Hadamard gate
for qb_i in initial_state.qubits:
initial_state.h(qb_i)'mpl') initial_state.draw(
This is simple and we can check the output of that actual circuit. Check out how the distribution resolution changes depending on the shots parameters.
from qiskit_aer import AerSimulator
= QuantumCircuit(size, size)
qc = qc.compose(initial_state)
qc
# Measures added
range(size), range(size))
qc.measure(
# execute the quantum circuit
= AerSimulator()
backend = backend.run(qc, shots=10000).result()
result = result.get_counts(qc)
counts
plot_histogram(counts)
import qiskit.quantum_info as qi
= qi.Statevector.from_instruction(initial_state)
psi 'latex', prefix='|\\psi\\rangle = ') psi.draw(
\[|\psi\rangle = 0.1767766953 |00000\rangle+0.1767766953 |00001\rangle+0.1767766953 |00010\rangle+0.1767766953 |00011\rangle+0.1767766953 |00100\rangle+0.1767766953 |00101\rangle + \ldots +0.1767766953 |11011\rangle+0.1767766953 |11100\rangle+0.1767766953 |11101\rangle+0.1767766953 |11110\rangle+0.1767766953 |11111\rangle\]
from qiskit.visualization import plot_bloch_multivector
plot_bloch_multivector(psi)
Ok so the starting point is just simply all potential solutions: \(|+\rangle^{\otimes n}\)
Now we can add the blocks for \(H_{init}\) and \(H_{problem}\).
from qiskit.circuit import Parameter
# Create a parameter to instantiate
= Parameter("$\\beta$")
beta
= QuantumCircuit(size,size)
mixer for i in range(size):
2*beta*(-1), i)
mixer.rx(
'mpl') mixer.draw(
This following code is a particular version of the MaxCut instance as in a fully informed Ising model we would need to add \(Z\) rotations depending on the weight of the node and \(ZZ\) rotations accoding to coupling strengh of each edge. But given that we have a sparse matrix, we can take advantage of that.
import numpy as np
= 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.]])
from qiskit.circuit import Parameter
# Create a parameter to instantiate
= Parameter("$\\gamma$")
gamma
= QuantumCircuit(size,size)
problem
for node in range(size):
2*gamma*H_problem[node][node], node)
problem.rz(
for node_i in range(size):
for node_j in range(node_i + 1, size):
if H_problem[node_i][node_j] > 0:
2*gamma*H_problem[node_i][node_j], node_i, node_j)
problem.rzz('mpl') problem.draw(
We have the three main blocks for setting our digitized version of the problem. Now we need to define a composition for those blocks where:
- \(\beta\) : (1-\(\lambda(t)\))/\(n\)
- \(\gamma\) : (\(\lambda(t)\))/\(n\)
Wbeing \(n\) the number of trotter steps. We can do a first approach with a simple stepwise linear approach.
= QuantumCircuit(size,size)
maxcut =True)
maxcut.compose(initial_state, inplace
= 0.1
dt
for lt in np.arange(0.1, 1.1, dt):
=True)
maxcut.compose(problem.assign_parameters({gamma: lt}), inplace1-lt)}), inplace=True)
maxcut.compose(mixer.assign_parameters({beta: (range(size), range(size))
maxcut.measure('mpl', fold=500) maxcut.draw(
# execute the quantum circuit
= backend.run(maxcut, shots=10000).result()
result = result.get_counts(maxcut)
counts
plot_histogram(counts)
import matplotlib.pyplot as plt
= []
energy = []
success_probability
for bit_string in counts:
= [int(char) for char in bit_string]
solution
energy.append(np.dot(np.transpose(solution), np.dot(H_problem, solution)))/10000)
success_probability.append(counts[bit_string]
=success_probability, label="Digitized AQC")
plt.bar(energy, height plt.show()
Let’s increase the schedule so that slower evolution is considered.
= QuantumCircuit(size,size)
maxcut =True)
maxcut.compose(initial_state, inplace
= 0.01
dt
for lt in np.arange(0.1, 1.1, dt):
=True)
maxcut.compose(problem.assign_parameters({gamma: lt}), inplace1-lt)}), inplace=True)
maxcut.compose(mixer.assign_parameters({beta: (range(size), range(size))
maxcut.measure('mpl', fold=500) maxcut.draw(
= backend.run(maxcut, shots=10000).result()
result = result.get_counts(maxcut)
counts
plot_histogram(counts)
= []
energy = []
success_probability
for bit_string in counts:
= [int(char) for char in bit_string]
solution
energy.append(np.dot(np.transpose(solution), np.dot(H_problem, solution)))/10000)
success_probability.append(counts[bit_string]
=success_probability, label="Digitized AQC")
plt.bar(energy, height plt.show()
We could select a much more efficient scheduling function, but we should look into the energy evolution, the gap… let’s take a wild guess.
def scheduling_function(t, T):
return np.sin((np.pi/2)*np.sin((np.pi * t)/(2*T))**2)**2
= 1.0
T = 0.1
dt
= np.arange(0.1, T, dt)
timeline
= [scheduling_function(t, T) for t in timeline] val
="Schedule")
plt.plot(timeline, val, label
plt.show()
= QuantumCircuit(size,size)
maxcut =True)
maxcut.compose(initial_state, inplace
= 1.0
T = 0.1
dt
= np.arange(0.1, T, dt)
timeline
for t in np.arange(0.1, T, dt):
= scheduling_function(t, T)
lambda_t *lambda_t}), inplace=True)
maxcut.compose(problem.assign_parameters({gamma: dt*(1-lambda_t)}), inplace=True)
maxcut.compose(mixer.assign_parameters({beta: dtrange(size), range(size))
maxcut.measure('mpl', fold=500) maxcut.draw(
= backend.run(maxcut, shots=10000).result()
result = result.get_counts(maxcut)
counts
plot_histogram(counts)
= []
energy = []
success_probability
for bit_string in counts:
= [int(char) for char in bit_string]
solution
energy.append(np.dot(np.transpose(solution), np.dot(H_problem, solution)))/10000)
success_probability.append(counts[bit_string]
=success_probability, label="Digitized AQC")
plt.bar(energy, height plt.show()
Getting close…
= QuantumCircuit(size,size)
maxcut =True)
maxcut.compose(initial_state, inplace
= 5.0
T = 0.05
dt
= np.arange(0.1, T, dt)
timeline
for t in np.arange(0.1, T, dt):
= scheduling_function(t, T)
lambda_t *lambda_t}), inplace=True)
maxcut.compose(problem.assign_parameters({gamma: dt*(1-lambda_t)}), inplace=True)
maxcut.compose(mixer.assign_parameters({beta: dtrange(size), range(size))
maxcut.measure('mpl', fold=500) maxcut.draw(
= backend.run(maxcut, shots=10000).result()
result = result.get_counts(maxcut)
counts
plot_histogram(counts)
= []
energy = []
success_probability
for bit_string in counts:
= [int(char) for char in bit_string]
solution
energy.append(np.dot(np.transpose(solution), np.dot(H_problem, solution)))/10000)
success_probability.append(counts[bit_string]
=success_probability, label="Digitized AQC")
plt.bar(energy, height
plt.show()
Let’s play around a bit with the time parameters and see if we get a good concentration of -5 energy states. Ideally our sampling from the ground state should be below \(2^5 = 32\) samples, otherwise, brute forcing would be more efficient in this cases. But in absence of knowledge we should guess for a while.
We could rely on techniques known to be used to find parameters in a model. Yeah, machine learning techniques.