Skip to content

Commit d775876

Browse files
Smooth transition from exact MAXCUT (orig. from Elara, custom GPT) to heuristic
1 parent 5a884e5 commit d775876

File tree

9 files changed

+248
-50
lines changed

9 files changed

+248
-50
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
1010

1111
[project]
1212
name = "pyqrackising"
13-
version = "9.7.0"
13+
version = "9.7.1"
1414
requires-python = ">=3.8"
1515
description = "Fast MAXCUT, TSP, and sampling heuristics from near-ideal transverse field Ising model (TFIM)"
1616
readme = {file = "README.txt", content-type = "text/markdown"}

pyqrackising/maxcut_tfim.py

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from numba import njit, prange
44
import os
55

6-
from .maxcut_tfim_util import compute_cut, compute_energy, convert_bool_to_uint, get_cut, get_cut_base, init_thresholds, make_G_m_buf, make_theta_buf, maxcut_hamming_cdf, opencl_context, sample_mag, setup_opencl, bit_pick
6+
from .maxcut_tfim_util import compute_cut, compute_energy, convert_bool_to_uint, get_cut, get_cut_base, heuristic_threshold, init_thresholds, make_G_m_buf, make_theta_buf, maxcut_hamming_cdf, opencl_context, sample_mag, setup_opencl, bit_pick
77

88
IS_OPENCL_AVAILABLE = True
99
try:
@@ -242,6 +242,60 @@ def run_cut_opencl(best_energy, samples, G_m_buf, is_segmented, local_size, glob
242242
return samples[max_index_host[best_x]], energy
243243

244244

245+
@njit
246+
def exact_maxcut(G):
247+
"""Brute-force exact MAXCUT solver using Numba JIT."""
248+
n = G.shape[0]
249+
max_cut = -1.0
250+
best_mask = 0
251+
252+
# Enumerate all 2^n possible bitstrings
253+
for mask in range(1 << n):
254+
cut = 0.0
255+
for i in range(n):
256+
bi = (mask >> i) & 1
257+
for j in range(i + 1, n):
258+
if bi != ((mask >> j) & 1):
259+
cut += G[i, j]
260+
if cut > max_cut:
261+
max_cut = cut
262+
best_mask = mask
263+
264+
# Reconstruct best bitstring
265+
best_bits = np.zeros(n, dtype=np.bool_)
266+
for i in range(n):
267+
best_bits[i] = (best_mask >> i) & 1
268+
269+
return best_bits, max_cut
270+
271+
272+
@njit
273+
def exact_spin_glass(G):
274+
"""Brute-force exact spin-glass solver using Numba JIT."""
275+
n = G.shape[0]
276+
max_cut = -1.0
277+
best_mask = 0
278+
279+
# Enumerate all 2^n possible bitstrings
280+
for mask in range(1 << n):
281+
cut = 0.0
282+
for i in range(n):
283+
bi = (mask >> i) & 1
284+
for j in range(i + 1, n):
285+
val = G[i, j]
286+
cut += val if bi == ((mask >> j) & 1) else -val
287+
if cut > max_cut:
288+
max_cut = cut
289+
best_mask = mask
290+
291+
# Reconstruct best bitstring
292+
best_bits = np.zeros(n, dtype=np.bool_)
293+
for i in range(n):
294+
best_bits[i] = (best_mask >> i) & 1
295+
296+
return best_bits, max_cut
297+
298+
245299
def maxcut_tfim(
246300
G,
247301
quality=None,
@@ -264,22 +318,15 @@ def maxcut_tfim(
264318

265319
n_qubits = len(G_m)
266320

267-
if n_qubits < 3:
268-
empty = [nodes[0]]
269-
empty.clear()
270-
271-
if n_qubits == 0:
272-
return "", 0, (empty, empty.copy())
273-
274-
if n_qubits == 1:
275-
return "0", 0, (nodes, empty)
321+
if n_qubits < heuristic_threshold:
322+
best_solution, best_value = exact_spin_glass(G_m) if is_spin_glass else exact_maxcut(G_m)
323+
bit_string, l, r = get_cut(best_solution, nodes, n_qubits)
276324

277-
if n_qubits == 2:
278-
weight = G_m[0, 1]
279-
if weight < 0.0:
280-
return "00", 0, (nodes, empty)
325+
if best_value < 0.0:
326+
# Best cut is trivial partition, all/empty
327+
return '0' * n_qubits, 0.0, (nodes, [])
281328

282-
return "01", weight, ([nodes[0]], [nodes[1]])
329+
return bit_string, best_value, (l, r)
283330

284331
if quality is None:
285332
quality = 6

pyqrackising/maxcut_tfim_sparse.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from numba import njit, prange
44
import os
55

6-
from .maxcut_tfim_util import binary_search, compute_cut_sparse, compute_energy_sparse, convert_bool_to_uint, get_cut, get_cut_base, init_thresholds, make_G_m_csr_buf, make_theta_buf, maxcut_hamming_cdf, opencl_context, sample_mag, setup_opencl, bit_pick, to_scipy_sparse_upper_triangular
6+
from .maxcut_tfim_util import binary_search, compute_cut_sparse, compute_energy_sparse, convert_bool_to_uint, get_cut, get_cut_base, heuristic_threshold_sparse, init_thresholds, make_G_m_csr_buf, make_theta_buf, maxcut_hamming_cdf, opencl_context, sample_mag, setup_opencl, bit_pick, to_scipy_sparse_upper_triangular
77

88
IS_OPENCL_AVAILABLE = True
99
try:
@@ -256,6 +256,69 @@ def run_cut_opencl(best_energy, samples, G_data_buf, G_rows_buf, G_cols_buf, is_
256256
return samples[max_index_host[best_x]], energy
257257

258258

259+
@njit
260+
def exact_maxcut(G_data, G_rows, G_cols):
261+
"""Brute-force exact MAXCUT solver using Numba JIT."""
262+
n = G_rows.shape[0] - 1
263+
max_cut = -1.0
264+
best_mask = 0
265+
266+
# Enumerate all 2^n possible bitstrings
267+
for mask in range(1 << n):
268+
cut = 0.0
269+
for i in range(n):
270+
bi = (mask >> i) & 1
271+
for j in range(i + 1, n):
272+
if bi != ((mask >> j) & 1):
273+
u, v = (i, j) if i < j else (j, i)
274+
start = G_rows[u]
275+
end = G_rows[u + 1]
276+
k = binary_search(G_cols[start:end], v) + start
277+
if k < end:
278+
cut += G_data[k]
279+
if cut > max_cut:
280+
max_cut = cut
281+
best_mask = mask
282+
283+
# Reconstruct best bitstring
284+
best_bits = np.zeros(n, dtype=np.bool_)
285+
for i in range(n):
286+
best_bits[i] = (best_mask >> i) & 1
287+
288+
return best_bits, max_cut
289+
290+
291+
@njit
292+
def exact_spin_glass(G_data, G_rows, G_cols):
293+
"""Brute-force exact spin-glass solver using Numba JIT."""
294+
n = G_rows.shape[0] - 1
295+
max_cut = -1.0
296+
best_mask = 0
297+
298+
# Enumerate all 2^n possible bitstrings
299+
for mask in range(1 << n):
300+
cut = 0.0
301+
for i in range(n):
302+
bi = (mask >> i) & 1
303+
for j in range(i + 1, n):
304+
u, v = (i, j) if i < j else (j, i)
305+
start = G_rows[u]
306+
end = G_rows[u + 1]
307+
k = binary_search(G_cols[start:end], v) + start
308+
if k < end:
309+
val = G_data[k]
310+
cut += val if bi == ((mask >> j) & 1) else -val
311+
if cut > max_cut:
312+
max_cut = cut
313+
best_mask = mask
314+
315+
# Reconstruct best bitstring
316+
best_bits = np.zeros(n, dtype=np.bool_)
317+
for i in range(n):
318+
best_bits[i] = (best_mask >> i) & 1
319+
320+
return best_bits, max_cut
321+
259322

260323
def maxcut_tfim_sparse(
261324
G,
@@ -281,19 +344,15 @@ def maxcut_tfim_sparse(
281344
nodes = list(range(n_qubits))
282345
G_m = G
283346

284-
if n_qubits < 3:
285-
if n_qubits == 0:
286-
return "", 0, ([], [])
287-
288-
if n_qubits == 1:
289-
return "0", 0, (nodes, [])
347+
if n_qubits < heuristic_threshold_sparse:
348+
best_solution, best_value = exact_spin_glass(G_m.data, G_m.indptr, G_m.indices) if is_spin_glass else exact_maxcut(G_m.data, G_m.indptr, G_m.indices)
349+
bit_string, l, r = get_cut(best_solution, nodes, n_qubits)
290350

291-
if n_qubits == 2:
292-
weight = G_m[0, 1]
293-
if weight < 0.0:
294-
return "00", 0, (nodes, [])
351+
if best_value < 0.0:
352+
# Best cut is trivial partition, all/empty
353+
return '0' * n_qubits, 0.0, (nodes, [])
295354

296-
return "01", weight, ([nodes[0]], [nodes[1]])
355+
return bit_string, best_value, (l, r)
297356

298357
if quality is None:
299358
quality = 6

pyqrackising/maxcut_tfim_streaming.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from numba import njit, prange
44
import os
55

6-
from .maxcut_tfim_util import compute_cut_streaming, compute_energy_streaming, get_cut, get_cut_base, init_thresholds, maxcut_hamming_cdf, opencl_context, sample_mag, bit_pick
6+
from .maxcut_tfim_util import compute_cut_streaming, compute_energy_streaming, get_cut, get_cut_base, heuristic_threshold, init_thresholds, maxcut_hamming_cdf, opencl_context, sample_mag, bit_pick
77

88

99
epsilon = opencl_context.epsilon
@@ -148,6 +148,58 @@ def find_G_min(G_func, nodes, n_nodes):
148148
return G_min
149149

150150

151+
@njit
152+
def exact_maxcut(G_func, n):
153+
"""Brute-force exact MAXCUT solver using Numba JIT."""
154+
max_cut = -1.0
155+
best_mask = 0
156+
157+
# Enumerate all 2^n possible bitstrings
158+
for mask in range(1 << n):
159+
cut = 0.0
160+
for i in range(n):
161+
bi = (mask >> i) & 1
162+
for j in range(i + 1, n):
163+
if bi != ((mask >> j) & 1):
164+
cut += G_func(i, j)
165+
if cut > max_cut:
166+
max_cut = cut
167+
best_mask = mask
168+
169+
# Reconstruct best bitstring
170+
best_bits = np.zeros(n, dtype=np.bool_)
171+
for i in range(n):
172+
best_bits[i] = (best_mask >> i) & 1
173+
174+
return best_bits, max_cut
175+
176+
177+
@njit
178+
def exact_spin_glass(G_func, n):
179+
"""Brute-force exact spin-glass solver using Numba JIT."""
180+
max_cut = -1.0
181+
best_mask = 0
182+
183+
# Enumerate all 2^n possible bitstrings
184+
for mask in range(1 << n):
185+
cut = 0.0
186+
for i in range(n):
187+
bi = (mask >> i) & 1
188+
for j in range(i + 1, n):
189+
val = G_func(i, j)
190+
cut += val if bi == ((mask >> j) & 1) else -val
191+
if cut > max_cut:
192+
max_cut = cut
193+
best_mask = mask
194+
195+
# Reconstruct best bitstring
196+
best_bits = np.zeros(n, dtype=np.bool_)
197+
for i in range(n):
198+
best_bits[i] = (best_mask >> i) & 1
199+
200+
return best_bits, max_cut
201+
202+
151203
def maxcut_tfim_streaming(
152204
G_func,
153205
nodes,
@@ -161,19 +213,15 @@ def maxcut_tfim_streaming(
161213
wgs = opencl_context.work_group_size
162214
n_qubits = len(nodes)
163215

164-
if n_qubits < 3:
165-
if n_qubits == 0:
166-
return "", 0, ([], [])
167-
168-
if n_qubits == 1:
169-
return "0", 0, (nodes, [])
216+
if n_qubits < heuristic_threshold:
217+
best_solution, best_value = exact_spin_glass(G_func, n_qubits) if is_spin_glass else exact_maxcut(G_func, n_qubits)
218+
bit_string, l, r = get_cut(best_solution, nodes, n_qubits)
170219

171-
if n_qubits == 2:
172-
weight = G_func(nodes[0], nodes[1])
173-
if weight < 0.0:
174-
return "00", 0, (nodes, [])
220+
if best_value < 0.0:
221+
# Best cut is trivial partition, all/empty
222+
return '0' * n_qubits, 0.0, (nodes, [])
175223

176-
return "01", weight, ([nodes[0]], [nodes[1]])
224+
return bit_string, best_value, (l, r)
177225

178226
if quality is None:
179227
quality = 6

pyqrackising/maxcut_tfim_util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def __init__(self, a, b, g, d, e, f, c, q, i, j, k, l, m, n, o, p, x, y, z, w):
121121
print("PyOpenCL not installed. (If you have any OpenCL accelerator devices with available ICDs, you might want to optionally install pyopencl.)")
122122

123123
opencl_context = OpenCLContext(compute_units, IS_OPENCL_AVAILABLE, work_group_size, dtype, epsilon, max_alloc, ctx, queue, calculate_cut_kernel, calculate_cut_sparse_kernel, calculate_cut_segmented_kernel, calculate_cut_sparse_segmented_kernel, single_bit_flips_kernel, single_bit_flips_sparse_kernel, single_bit_flips_segmented_kernel, single_bit_flips_sparse_segmented_kernel, double_bit_flips_kernel, double_bit_flips_sparse_kernel, double_bit_flips_segmented_kernel, double_bit_flips_sparse_segmented_kernel)
124+
heuristic_threshold = 24
125+
heuristic_threshold_sparse = 23
124126

125127

126128
def setup_opencl(l, g, args_np):

pyqrackising/spin_glass_solver.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .maxcut_tfim import maxcut_tfim
2-
from .maxcut_tfim_util import compute_cut, compute_energy, get_cut, gray_code_next, gray_mutation, int_to_bitstring, make_G_m_buf, make_best_theta_buf, opencl_context, setup_opencl
2+
from .maxcut_tfim_util import compute_cut, compute_energy, get_cut, gray_code_next, gray_mutation, heuristic_threshold, int_to_bitstring, make_G_m_buf, make_best_theta_buf, opencl_context, setup_opencl
33
import networkx as nx
44
import numpy as np
55
from numba import njit, prange
@@ -296,6 +296,9 @@ def spin_glass_solver(
296296

297297
return "01", weight, ([nodes[0]], [nodes[1]]), -weight
298298

299+
if n_qubits < heuristic_threshold:
300+
best_guess = None
301+
299302
bitstring = ""
300303
if isinstance(best_guess, str):
301304
bitstring = best_guess
@@ -305,16 +308,27 @@ def spin_glass_solver(
305308
bitstring = "".join(["1" if b else "0" for b in best_guess])
306309
else:
307310
bitstring, cut_value, _ = maxcut_tfim(G_m, quality=quality, shots=shots, is_spin_glass=is_spin_glass, anneal_t=anneal_t, anneal_h=anneal_h, repulsion_base=repulsion_base, is_maxcut_gpu=is_maxcut_gpu, is_nested=True)
311+
308312
best_theta = np.array([b == "1" for b in list(bitstring)], dtype=np.bool_)
313+
max_energy = compute_energy(best_theta, G_m, n_qubits) if is_spin_glass else cut_value
314+
315+
if n_qubits < heuristic_threshold:
316+
bitstring, l, r = get_cut(best_theta, nodes, n_qubits)
317+
if is_spin_glass:
318+
cut_value = compute_cut(best_theta, G_m, n_qubits)
319+
min_energy = -max_energy
320+
else:
321+
cut_value = max_energy
322+
min_energy = compute_energy(best_theta, G_m, n_qubits)
323+
324+
return bitstring, float(cut_value), (l, r), float(min_energy)
309325

310326
if gray_iterations is None:
311327
gray_iterations = n_qubits * n_qubits
312328

313329
if gray_seed_multiple is None:
314330
gray_seed_multiple = os.cpu_count()
315331

316-
max_energy = compute_energy(best_theta, G_m, n_qubits) if is_spin_glass else cut_value
317-
318332
is_opencl = is_maxcut_gpu and IS_OPENCL_AVAILABLE
319333

320334
if is_opencl:

0 commit comments

Comments
 (0)