Skip to content

Commit e8b95ee

Browse files
Handle negative weights in exact MAXCUT
1 parent f866093 commit e8b95ee

3 files changed

Lines changed: 64 additions & 31 deletions

File tree

pyqrackising/solve_maxcut_exact.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@
1212
# --------
1313
# For each edge (i, j, w):
1414
# Introduce a selector variable s_ij (var index n + edge_idx + 1).
15-
# Hard clauses force s_ij = 1 iff x_i != x_j (edge is cut):
16-
# [ x_i v x_j v ~s_ij ]
17-
# [~x_i v ~x_j v ~s_ij ]
15+
# Hard clauses encode s_ij <=> (x_i XOR x_j):
16+
# [ x_i v x_j v ~s_ij ] xi=0,xj=0 -> s=0 (always added)
17+
# [~x_i v ~x_j v ~s_ij ] xi=1,xj=1 -> s=0 (always added)
18+
# [~x_i v x_j v s_ij ] xi=1,xj=0 -> s=1 (only when min_w < 0)
19+
# [ x_i v ~x_j v s_ij ] xi=0,xj=1 -> s=1 (only when min_w < 0)
20+
# When all weights are non-negative RC2 already wants s=1 to claim the
21+
# reward, so the ->s clauses are redundant and omitted to halve the
22+
# clause count. When negative edges are present the ->s clauses are
23+
# critical: without them s can stay 0 while the edge is cut, letting
24+
# RC2 fraudulently claim the [-s] soft reward.
1825
#
1926
# Positive weight w > 0: we *want* the edge cut.
2027
# Soft clause [s_ij] with weight round(w * SCALE).
@@ -70,7 +77,7 @@
7077
# Encoding helpers
7178
# ---------------------------------------------------------------------------
7279

73-
def _build_wcnf(edges, n):
80+
def _build_wcnf(edges, n, has_negative):
7481
"""
7582
Build a WCNF formula for MAXCUT on n nodes with the given edge list.
7683
@@ -84,16 +91,23 @@ def _build_wcnf(edges, n):
8491
"""
8592
wcnf = WCNF()
8693
total_pos_scaled = 0
94+
neg_scaled_total = 0
8795
baseline_neg = 0.0
8896

8997
for idx, (i, j, w) in enumerate(edges):
9098
xi = i + 1 # 1-indexed SAT variable for node i
9199
xj = j + 1 # 1-indexed SAT variable for node j
92100
s = n + idx + 1 # selector variable for this edge
93101

94-
# Hard clauses: s <=> (x_i XOR x_j)
95-
wcnf.append([ xi, xj, -s]) # ~s -> x_i v x_j
96-
wcnf.append([-xi, -xj, -s]) # ~s -> ~x_i v ~x_j
102+
# s=1 when edge not cut is always suboptimal for positive edges,
103+
# so the two ->s direction clauses are only needed when negative
104+
# edges are present (otherwise they would double the clause count
105+
# for no benefit).
106+
wcnf.append([ xi, xj, -s]) # xi=0,xj=0 -> s=0
107+
wcnf.append([-xi, -xj, -s]) # xi=1,xj=1 -> s=0
108+
if has_negative:
109+
wcnf.append([-xi, xj, s]) # xi=1,xj=0 -> s=1
110+
wcnf.append([ xi, -xj, s]) # xi=0,xj=1 -> s=1
97111

98112
if w >= 0.0:
99113
iw = max(1, int(round(w * _SCALE)))
@@ -102,9 +116,10 @@ def _build_wcnf(edges, n):
102116
else:
103117
iw = max(1, int(round(-w * _SCALE)))
104118
baseline_neg += w # this is already negative
119+
neg_scaled_total += iw
105120
wcnf.append([-s], weight=iw) # soft: want edge NOT cut
106121

107-
return wcnf, total_pos_scaled, baseline_neg
122+
return wcnf, total_pos_scaled, neg_scaled_total, baseline_neg
108123

109124

110125
def _model_to_bits(model, n):
@@ -236,7 +251,8 @@ def solve_maxcut_exact(
236251
bitstring, l, r = get_cut(warm_theta, nodes, n_qubits)
237252
return bitstring, 0.0, (l, r), 0.0, True
238253

239-
wcnf, total_pos_scaled, baseline_neg = _build_wcnf(edges, n_qubits)
254+
has_negative = any(w < 0.0 for _, _, w in edges)
255+
wcnf, total_pos_scaled, neg_scaled_total, baseline_neg = _build_wcnf(edges, n_qubits, has_negative)
240256

241257
if verbose:
242258
print("Starting RC2 MaxSAT solver...")
@@ -252,9 +268,9 @@ def solve_maxcut_exact(
252268
# RC2 uses Glucose3 internally; we set variable polarities to match the
253269
# warm start so the first SAT call explores the warm-start region first.
254270
try:
255-
for i, bit in enumerate(warm_theta):
256-
# oracle.set_phases expects a list of signed literals
257-
rc2.oracle.set_phases([i + 1 if bit else -(i + 1)])
271+
phases = [i + 1 if warm_theta[i] else -(i + 1)
272+
for i in range(len(warm_theta))]
273+
rc2.oracle.set_phases(phases)
258274
except AttributeError:
259275
pass # solver doesn't support phase setting; continue without
260276

@@ -274,7 +290,7 @@ def solve_maxcut_exact(
274290
best_theta = _model_to_bits(model, n_qubits)
275291

276292
if verbose and model is not None:
277-
cut_opt = (total_pos_scaled - cost_scaled) / _SCALE + baseline_neg
293+
cut_opt = baseline_neg + (total_pos_scaled + neg_scaled_total - cost_scaled) / _SCALE
278294
print(f"RC2 optimum: {cut_opt:.6f} ({elapsed:.3f}s)")
279295

280296
# --- final scoring (matches spin_glass_solver return convention) ---

pyqrackising/solve_maxcut_exact_sparse.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@
3636
_SCALE = 2 ** 32
3737

3838

39-
def _build_wcnf_sparse(G_data, G_rows, G_cols, n):
39+
def _build_wcnf_sparse(G_data, G_rows, G_cols, n, has_negative):
4040
wcnf = WCNF()
4141
total_pos_scaled = 0
42+
neg_scaled_total = 0
4243
baseline_neg = 0.0
4344
edge_idx = 0
4445
for i in range(n):
@@ -52,18 +53,22 @@ def _build_wcnf_sparse(G_data, G_rows, G_cols, n):
5253
xi = i + 1
5354
xj = j + 1
5455
s = n + edge_idx + 1
55-
wcnf.append([ xi, xj, -s])
56-
wcnf.append([-xi, -xj, -s])
56+
wcnf.append([ xi, xj, -s]) # xi=0,xj=0 -> s=0
57+
wcnf.append([-xi, -xj, -s]) # xi=1,xj=1 -> s=0
58+
if has_negative:
59+
wcnf.append([-xi, xj, s]) # xi=1,xj=0 -> s=1
60+
wcnf.append([ xi, -xj, s]) # xi=0,xj=1 -> s=1
5761
if w >= 0.0:
5862
iw = max(1, int(round(w * _SCALE)))
5963
total_pos_scaled += iw
6064
wcnf.append([s], weight=iw)
6165
else:
6266
iw = max(1, int(round(-w * _SCALE)))
6367
baseline_neg += w
68+
neg_scaled_total += iw
6469
wcnf.append([-s], weight=iw)
6570
edge_idx += 1
66-
return wcnf, total_pos_scaled, baseline_neg
71+
return wcnf, total_pos_scaled, neg_scaled_total, baseline_neg
6772

6873

6974
def _model_to_bits(model, n):
@@ -173,8 +178,9 @@ def solve_maxcut_exact_sparse(
173178
print(f"Heuristic value: {cut_value:.6f} ({time.monotonic()-t0:.3f}s)")
174179

175180
warm_theta = np.array([b == "1" for b in list(bitstring)], dtype=np.bool_)
176-
wcnf, total_pos_scaled, baseline_neg = _build_wcnf_sparse(
177-
G_data, G_rows, G_cols, n_qubits
181+
has_negative = float(G_data.min()) < 0.0 if len(G_data) > 0 else False
182+
wcnf, total_pos_scaled, neg_scaled_total, baseline_neg = _build_wcnf_sparse(
183+
G_data, G_rows, G_cols, n_qubits, has_negative
178184
)
179185

180186
if verbose:
@@ -187,8 +193,9 @@ def solve_maxcut_exact_sparse(
187193
t0 = time.monotonic()
188194
with RC2(wcnf, solver="g3", **rc2_kwargs) as rc2:
189195
try:
190-
for i, bit in enumerate(warm_theta):
191-
rc2.oracle.set_phases([i + 1 if bit else -(i + 1)])
196+
phases = [i + 1 if warm_theta[i] else -(i + 1)
197+
for i in range(len(warm_theta))]
198+
rc2.oracle.set_phases(phases)
192199
except AttributeError:
193200
pass
194201
model = rc2.compute()
@@ -206,7 +213,7 @@ def solve_maxcut_exact_sparse(
206213
best_theta = _model_to_bits(model, n_qubits)
207214

208215
if verbose and model is not None:
209-
cut_opt = (total_pos_scaled - cost_scaled) / _SCALE + baseline_neg
216+
cut_opt = baseline_neg + (total_pos_scaled + neg_scaled_total - cost_scaled) / _SCALE
210217
print(f"RC2 optimum: {cut_opt:.6f} ({elapsed:.3f}s)")
211218

212219
bitstring, l, r = get_cut(best_theta, nodes, n_qubits)

pyqrackising/solve_maxcut_exact_streaming.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
_SCALE = 2 ** 32
3535

3636

37-
def _build_wcnf_streaming(G_func, nodes, n):
37+
def _build_wcnf_streaming(G_func, nodes, n, has_negative):
3838
wcnf = WCNF()
3939
total_pos_scaled = 0
40+
neg_scaled_total = 0
4041
baseline_neg = 0.0
4142
edge_idx = 0
4243
for i in range(n):
@@ -47,18 +48,22 @@ def _build_wcnf_streaming(G_func, nodes, n):
4748
xi = i + 1
4849
xj = j + 1
4950
s = n + edge_idx + 1
50-
wcnf.append([ xi, xj, -s])
51-
wcnf.append([-xi, -xj, -s])
51+
wcnf.append([ xi, xj, -s]) # xi=0,xj=0 -> s=0
52+
wcnf.append([-xi, -xj, -s]) # xi=1,xj=1 -> s=0
53+
if has_negative:
54+
wcnf.append([-xi, xj, s]) # xi=1,xj=0 -> s=1
55+
wcnf.append([ xi, -xj, s]) # xi=0,xj=1 -> s=1
5256
if w >= 0.0:
5357
iw = max(1, int(round(w * _SCALE)))
5458
total_pos_scaled += iw
5559
wcnf.append([s], weight=iw)
5660
else:
5761
iw = max(1, int(round(-w * _SCALE)))
5862
baseline_neg += w
63+
neg_scaled_total += iw
5964
wcnf.append([-s], weight=iw)
6065
edge_idx += 1
61-
return wcnf, total_pos_scaled, baseline_neg
66+
return wcnf, total_pos_scaled, neg_scaled_total, baseline_neg
6267

6368

6469
def _model_to_bits(model, n):
@@ -160,8 +165,12 @@ def solve_maxcut_exact_streaming(
160165
print(f"Heuristic value: {cut_value:.6f} ({time.monotonic()-t0:.3f}s)")
161166

162167
warm_theta = np.array([b == "1" for b in list(bitstring)], dtype=np.bool_)
163-
wcnf, total_pos_scaled, baseline_neg = _build_wcnf_streaming(
164-
G_func, nodes, n_qubits
168+
has_negative = any(
169+
G_func(nodes[i], nodes[j]) < 0.0
170+
for i in range(n_qubits) for j in range(i + 1, n_qubits)
171+
)
172+
wcnf, total_pos_scaled, neg_scaled_total, baseline_neg = _build_wcnf_streaming(
173+
G_func, nodes, n_qubits, has_negative
165174
)
166175

167176
if verbose:
@@ -174,8 +183,9 @@ def solve_maxcut_exact_streaming(
174183
t0 = time.monotonic()
175184
with RC2(wcnf, solver="g3", **rc2_kwargs) as rc2:
176185
try:
177-
for i, bit in enumerate(warm_theta):
178-
rc2.oracle.set_phases([i + 1 if bit else -(i + 1)])
186+
phases = [i + 1 if warm_theta[i] else -(i + 1)
187+
for i in range(len(warm_theta))]
188+
rc2.oracle.set_phases(phases)
179189
except AttributeError:
180190
pass
181191
model = rc2.compute()
@@ -193,7 +203,7 @@ def solve_maxcut_exact_streaming(
193203
best_theta = _model_to_bits(model, n_qubits)
194204

195205
if verbose and model is not None:
196-
cut_opt = (total_pos_scaled - cost_scaled) / _SCALE + baseline_neg
206+
cut_opt = baseline_neg + (total_pos_scaled + neg_scaled_total - cost_scaled) / _SCALE
197207
print(f"RC2 optimum: {cut_opt:.6f} ({elapsed:.3f}s)")
198208

199209
bitstring, l, r = get_cut(best_theta, list(nodes), n_qubits)

0 commit comments

Comments
 (0)