Skip to content

Commit cf61fca

Browse files
authored
graph utils and bug fixes (#27)
* minimum edge cover * graph utils * Grover for 1 qubit * do not use end point * bugfixes
1 parent 56091e4 commit cf61fca

File tree

8 files changed

+201
-42
lines changed

8 files changed

+201
-42
lines changed

examples/plotroutines.py

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import matplotlib.pyplot as pl
1+
import matplotlib.pyplot as plt
22
from mpl_toolkits.axes_grid1 import make_axes_locatable
33
from matplotlib.ticker import MaxNLocator
44

@@ -11,15 +11,15 @@
1111

1212
def __plot_landscape(A, extent, fig):
1313
if not fig:
14-
fig = pl.figure(figsize=(6, 6), dpi=80, facecolor="w", edgecolor="k")
15-
_ = pl.xlabel(r"$\gamma$")
16-
_ = pl.ylabel(r"$\beta$")
14+
fig = plt.figure(figsize=(6, 6), dpi=80, facecolor="w", edgecolor="k")
15+
_ = plt.xlabel(r"$\gamma$")
16+
_ = plt.ylabel(r"$\beta$")
1717
ax = fig.gca()
18-
_ = pl.title("Expectation value")
18+
_ = plt.title("Expectation value")
1919
im = ax.imshow(A, interpolation="bicubic", origin="lower", extent=extent)
2020
divider = make_axes_locatable(ax)
2121
cax = divider.append_axes("right", size="5%", pad=0.05)
22-
_ = pl.colorbar(im, cax=cax)
22+
_ = plt.colorbar(im, cax=cax)
2323

2424

2525
def plot_E(qaoa_instance, fig=None):
@@ -57,21 +57,21 @@ def plot_ApproximationRatio(
5757
exp = np.array(exp)
5858

5959
if not fig:
60-
ax = pl.figure().gca()
60+
ax = plt.figure().gca()
6161
else:
6262
ax = fig.gca()
63-
pl.hlines(1, 1, maxdepth, linestyles="solid", colors="black")
64-
pl.plot(
63+
plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black")
64+
plt.plot(
6565
np.arange(1, maxdepth + 1),
6666
(maxcost - exp) / (maxcost - mincost),
6767
style,
6868
label=label,
6969
)
70-
pl.ylim(0, 1.01)
71-
pl.xlim(1 - 0.25, maxdepth + 0.25)
72-
_ = pl.ylabel("appr. ratio")
73-
_ = pl.xlabel("depth")
74-
_ = pl.legend(loc="lower right", framealpha=1)
70+
plt.ylim(0, 1.01)
71+
plt.xlim(1 - 0.25, maxdepth + 0.25)
72+
_ = plt.ylabel("appr. ratio")
73+
_ = plt.xlabel("depth")
74+
_ = plt.legend(loc="lower right", framealpha=1)
7575
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
7676

7777

@@ -83,21 +83,21 @@ def plot_successprob(qaoa_instance, maxdepth, label, style="", fig=None, shots=1
8383
successp = np.array(successp)
8484

8585
if not fig:
86-
ax = pl.figure().gca()
86+
ax = plt.figure().gca()
8787
else:
8888
ax = fig.gca()
89-
pl.hlines(1, 1, maxdepth, linestyles="solid", colors="black")
90-
pl.plot(
89+
plt.hlines(1, 1, maxdepth, linestyles="solid", colors="black")
90+
plt.plot(
9191
np.arange(1, maxdepth + 1),
9292
successp,
9393
style,
9494
label=label,
9595
)
96-
pl.ylim(0, 1.01)
97-
pl.xlim(1 - 0.25, maxdepth + 0.25)
98-
_ = pl.ylabel("success prob")
99-
_ = pl.xlabel("depth")
100-
_ = pl.legend(loc="lower right", framealpha=1)
96+
plt.ylim(0, 1.01)
97+
plt.xlim(1 - 0.25, maxdepth + 0.25)
98+
_ = plt.ylabel("success prob")
99+
_ = plt.xlabel("depth")
100+
_ = plt.legend(loc="lower right", framealpha=1)
101101
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
102102

103103

@@ -126,24 +126,62 @@ def plot_angles(qaoa_instance, depth, label, style="", fig=None):
126126
angles = qaoa_instance.optimization_results[depth].get_best_angles()
127127

128128
if not fig:
129-
ax = pl.figure().gca()
129+
ax = plt.figure().gca()
130130
else:
131131
ax = fig.gca()
132132

133-
pl.plot(
133+
plt.plot(
134134
np.arange(1, depth + 1),
135135
angles[::2],
136136
"--" + style,
137137
label=r"$\gamma$ " + label,
138138
)
139-
pl.plot(
139+
plt.plot(
140140
np.arange(1, depth + 1),
141141
angles[1::2],
142142
"-" + style,
143143
label=r"$\beta$ " + label,
144144
)
145-
pl.xlim(1 - 0.25, depth + 0.25)
146-
_ = pl.ylabel("parameter")
147-
_ = pl.xlabel("depth")
148-
_ = pl.legend()
145+
plt.xlim(1 - 0.25, depth + 0.25)
146+
_ = plt.ylabel("parameter")
147+
_ = plt.xlabel("depth")
148+
_ = plt.legend()
149149
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
150+
151+
152+
def draw_colored_graph(G, edge_colors):
153+
# Draw the graph with colored edges
154+
# extend the color_map if necessary
155+
color_map = [
156+
"red",
157+
"blue",
158+
"green",
159+
"purple",
160+
"orange",
161+
"pink",
162+
"brown",
163+
"gray",
164+
"yellow",
165+
"cyan",
166+
]
167+
pos = nx.spring_layout(G) # Positions for all nodes
168+
169+
# Draw nodes
170+
nx.draw_networkx_nodes(G, pos, node_size=700, node_color="lightgray")
171+
172+
# Draw edges with colors
173+
for color_idx, edges in edge_colors.items():
174+
nx.draw_networkx_edges(
175+
G,
176+
pos,
177+
edgelist=edges,
178+
width=2,
179+
edge_color=color_map[color_idx % len(color_map)],
180+
)
181+
182+
# Draw labels
183+
nx.draw_networkx_labels(G, pos, font_size=20, font_family="sans-serif")
184+
185+
# Show the graph
186+
plt.axis("off")
187+
plt.show()

qaoa/mixers/grover_mixer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(self, subcircuit: InitialState) -> None:
1313
"""
1414
self.subcircuit = subcircuit
1515
self.mixer_param = Parameter("x_beta")
16+
self.N_qubits = subcircuit.N_qubits
1617

1718
def create_circuit(self):
1819
# given feasibel states f \in F,
@@ -27,7 +28,12 @@ def create_circuit(self):
2728
# X^n
2829
self.circuit.x(range(self.subcircuit.N_qubits))
2930
# C^{n-1}Phase
30-
phase_gate = PhaseGate(-self.mixer_param).control(self.subcircuit.N_qubits - 1)
31+
if self.subcircuit.N_qubits == 1:
32+
phase_gate = PhaseGate(-self.mixer_param)
33+
else:
34+
phase_gate = PhaseGate(-self.mixer_param).control(
35+
self.subcircuit.N_qubits - 1
36+
)
3137
self.circuit.append(phase_gate, self.circuit.qubits)
3238
# X^n
3339
self.circuit.x(range(self.subcircuit.N_qubits))

qaoa/problems/qubo_problem.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def __init__(self, Q=None, c=None, b=None) -> None:
2828
)
2929
n = Q.shape[0]
3030

31+
self.N_qubits = n
32+
3133
# Check if Q is lower triangular
3234
self.lower_triangular_Q = np.allclose(Q, np.tril(Q))
3335

qaoa/qaoa.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ def sample_cost_landscape(
288288
depth = 1
289289

290290
tmp = angles["gamma"]
291-
self.gamma_grid = np.linspace(tmp[0], tmp[1], tmp[2])
291+
self.gamma_grid = np.linspace(tmp[0], tmp[1], tmp[2], endpoint=False)
292292
tmp = angles["beta"]
293-
self.beta_grid = np.linspace(tmp[0], tmp[1], tmp[2])
293+
self.beta_grid = np.linspace(tmp[0], tmp[1], tmp[2], endpoint=False)
294294

295295
if self.backend.configuration().local:
296296
if self.sequential:

qaoa/util/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .statistic import Statistic
22
from .flip import BitFlip
33
from .post import *
4+
from .graphutils import GraphHandler

qaoa/util/flip.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def __init__(self, n):
77
self.circuit = None
88
self.N_qubits = n
99

10-
def boost_samples(self, problem, string, K = 5):
10+
def boost_samples(self, problem, string, K=5):
1111
"""
1212
Random bitflips on string/list of strings to increase cost.
1313
@@ -22,13 +22,13 @@ def boost_samples(self, problem, string, K = 5):
2222
old_string = string
2323
cost = problem.cost(string[::-1])
2424

25-
for _ in range (K):
25+
for _ in range(K):
2626
shuffled_indices = np.arange(self.N_qubits)
2727
np.random.shuffle(shuffled_indices)
2828

2929
for i in shuffled_indices:
3030
string_arr_altered = np.copy(string_arr)
31-
string_arr_altered[i] = not(string_arr[i])
31+
string_arr_altered[i] = not (string_arr[i])
3232
string_altered = "".join(map(str, string_arr_altered))
3333
new_cost = problem.cost(string_altered[::-1])
3434

qaoa/util/graphutils.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import networkx as nx
2+
3+
4+
class GraphHandler:
5+
def __init__(self, G):
6+
if isinstance(G, nx.DiGraph):
7+
raise Exception("Graph should be undirected.")
8+
if any(degree <= 1 for node, degree in G.degree()):
9+
print(
10+
"Graph contains nodes with one or zero edges. These can be removed to reduce the size of the problem."
11+
)
12+
13+
self.num_nodes = G.number_of_nodes()
14+
self.num_edges = G.number_of_edges()
15+
16+
# ensure graph has labels 0, 1, ..., num_V-1
17+
G_int = self.__ensure_integer_labels__(G)
18+
# relabel to make node n-1 the one with maximum degree
19+
self.G = self.__get_graph_maxdegree_last_node__(G_int)
20+
# to avoid a deep circuit, we partition the edges into sets which can be executed in parallel
21+
if not nx.is_isomorphic(G, self.G):
22+
raise Exception("Something went wrong.")
23+
24+
self.__minimum_edge_coloring__()
25+
26+
def __ensure_integer_labels__(self, G):
27+
# Check if nodes are already labeled as 0 to num_nodes-1
28+
if set(G.nodes) == set(range(self.num_nodes)):
29+
return (
30+
G # Return the graph unchanged if nodes are already labeled correctly
31+
)
32+
33+
# If nodes are not labeled correctly, create a mapping to relabel them
34+
node_mapping = {node: i for i, node in enumerate(G.nodes)}
35+
36+
# Create a new graph with relabeled nodes
37+
H = nx.relabel_nodes(G, node_mapping, copy=True)
38+
39+
return H
40+
41+
def __map_colors_to_edges__(self, line_graph_colors, original_graph):
42+
# Map colors to edges in the original graph G
43+
color_to_edges = {}
44+
45+
# Each node in the line graph corresponds to an edge in the original graph G
46+
for edge_in_line_graph, color in line_graph_colors.items():
47+
original_edge = edge_in_line_graph # This is the corresponding edge in G
48+
if color not in color_to_edges:
49+
color_to_edges[color] = []
50+
color_to_edges[color].append(original_edge)
51+
52+
# Perform consistency check
53+
54+
# Get the set of all edges in G
55+
edges_in_G = set(self.G.edges())
56+
# Get the set of all edges in color_to_edges
57+
edges_in_coloring = set(
58+
edge for edges in color_to_edges.values() for edge in edges
59+
)
60+
61+
if edges_in_G != edges_in_coloring:
62+
raise ValueError(
63+
"The colored edges do not match the edges in the original graph!"
64+
)
65+
66+
return color_to_edges
67+
68+
def __get_graph_maxdegree_last_node__(self, G):
69+
# Get node of highest degree
70+
j = sorted(G.degree(), key=lambda x: x[1], reverse=True)[0][0]
71+
if j == self.num_nodes - 1:
72+
return G
73+
else:
74+
# Create a mapping to swap node j and n-1
75+
mapping = {j: self.num_nodes - 1, self.num_nodes - 1: j}
76+
77+
# Relabel the nodes
78+
H = nx.relabel_nodes(G, mapping, copy=True)
79+
80+
return H
81+
82+
def __minimum_edge_coloring__(self, repetitions=100):
83+
#
84+
# a graph G
85+
# returns minimum edge coloring, i.e., a dict containting the edges for each color
86+
# this can be used to minimize the depth needed to implement diagonal cost Hamiltonians
87+
# example output
88+
# { 3: [(0, 1), (2, 7), (3, 9), (5, 6)],
89+
# 1: [(0, 3), (1, 5), (7, 9)],
90+
# 2: [(0, 9), (1, 6), (2, 5), (3, 8), (4, 7)],
91+
# 0: [(1, 4), (2, 9), (3, 7), (5, 8)] }
92+
#
93+
94+
# Convert the graph to its line graph
95+
line_G = nx.line_graph(self.G)
96+
97+
ncolors = self.num_edges + 1
98+
for _ in range(repetitions):
99+
# Apply greedy vertex coloring on the line graph
100+
line_graph_colors = nx.coloring.greedy_color(
101+
line_G, strategy="random_sequential"
102+
)
103+
104+
# groups of parallel edges
105+
pe = self.__map_colors_to_edges__(line_graph_colors, self.G)
106+
107+
num_classes = len(pe)
108+
if num_classes < ncolors:
109+
self.parallel_edges = pe
110+
ncolors = num_classes

qaoa/util/post.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import statistics as stat
22
import numpy as np
33

4+
45
def post_processing(instance, samples, K=5):
56
"""Performs classical post-processing on bitstrings by applying random bit flips.
67
Resets and updates self.stat
78
89
input:
910
instance: instance to perform function on
10-
samples (dict or list or str): The bitstring(s) to be processed.
11+
samples (dict or list or str): The bitstring(s) to be processed.
1112
The acceptable formats are:
1213
- dict{bitstring: count}: A dictionary with bitstrings as keys and their counts as values.
1314
- list(bitstring, bitstring, ...): A list of bitstrings.
1415
- str(bitstring): A single bitstring.
1516
K (int): The number of times to iterate through each bitstring and apply random bit flips.
1617
1718
returns:
18-
dict: A dictionary with the altered bitstrings as keys and their counts as values.
19+
dict: A dictionary with the altered bitstrings as keys and their counts as values.
1920
If no better bitstring is found, the original bitstring is the key.
2021
"""
2122
instance.stat.reset()
@@ -26,19 +27,20 @@ def post_processing(instance, samples, K=5):
2627

2728
for string in samples:
2829
boosted = instance.flipper.boost_samples(
29-
problem=instance.problem,
30-
string=string,
31-
K=K
30+
problem=instance.problem, string=string, K=K
3231
)
3332
try:
3433
count = samples[string]
3534
except:
3635
count = 1
3736

38-
instance.stat.add_sample(instance.problem.cost(boosted[::-1]), count, boosted[::-1])
37+
instance.stat.add_sample(
38+
instance.problem.cost(boosted[::-1]), count, boosted[::-1]
39+
)
3940
hist_post[boosted] = hist_post.get(boosted, 0) + count
4041
return hist_post
4142

43+
4244
def post_process_all_depths(instance, K=5):
4345
"""Performs post-processing of job.result().get_counts() 100 times after each layer.
4446
@@ -56,7 +58,7 @@ def post_process_all_depths(instance, K=5):
5658
for i in range(100):
5759
post_processing(
5860
instance=instance,
59-
samples=hist,
61+
samples=hist,
6062
K=K,
6163
)
6264
exp_in_layers[d] = exp_in_layers.get(d, []) + [-instance.stat.get_CVaR()]

0 commit comments

Comments
 (0)