Skip to content

Commit 413876a

Browse files
Miscommunication: keep the reversed attraction/repulsion
1 parent 601c676 commit 413876a

3 files changed

Lines changed: 60 additions & 77 deletions

File tree

pyproject.toml

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

88
[project]
99
name = "pyqrackising"
10-
version = "9.15.1"
10+
version = "9.15.2"
1111
requires-python = ">=3.8"
1212
description = "Fast MAXCUT, TSP, and sampling heuristics from near-ideal transverse field Ising model (TFIM)"
1313
readme = {file = "README.txt", content-type = "text/markdown"}

pyqrackising/generate_tfim_samples.py

Lines changed: 58 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
epsilon = opencl_context.epsilon
5050

5151

52-
# Fixed-Hamming-weight Kawasaki swaps with acceptance probability, by Elara
52+
# ── Geometry ───────────────────────────────────────────────────────────────────
53+
5354
def random_fixed_hamming_state(n_qubits, h):
5455
bits = np.zeros(n_qubits, dtype=np.bool_)
5556
bits[:h] = True
@@ -59,139 +60,122 @@ def random_fixed_hamming_state(n_qubits, h):
5960
state <<= 1
6061
if b:
6162
state |= 1
62-
6363
return state
6464

6565

6666
def build_neighbors(n_rows, n_cols):
6767
L = n_rows * n_cols
6868
neighbors = [[] for _ in range(L)]
69-
7069
for i in range(n_rows):
7170
for j in range(n_cols):
7271
idx = i * n_cols + j
73-
7472
right = i * n_cols + ((j + 1) % n_cols)
75-
left = i * n_cols + ((j - 1) % n_cols)
76-
down = ((i + 1) % n_rows) * n_cols + j
77-
up = ((i - 1) % n_rows) * n_cols + j
78-
73+
left = i * n_cols + ((j - 1) % n_cols)
74+
down = ((i + 1) % n_rows) * n_cols + j
75+
up = ((i - 1) % n_rows) * n_cols + j
7976
neighbors[idx] = [right, left, down, up]
80-
8177
return neighbors
8278

8379

84-
def count_like_edges(state, neighbors):
85-
like = 0
80+
def count_unlike_edges(state, neighbors):
81+
unlike = 0
8682
visited = set()
87-
8883
for i, nbrs in enumerate(neighbors):
8984
si = (state >> i) & 1
9085
for j in nbrs:
9186
if (j, i) in visited:
9287
continue
93-
sj = (state >> j) & 1
94-
if si == sj:
95-
like += 1
88+
if si != ((state >> j) & 1):
89+
unlike += 1
9690
visited.add((i, j))
97-
98-
return like
91+
return unlike
9992

10093

10194
def delta_like_edges(state, i, j, neighbors):
10295
si = (state >> i) & 1
10396
sj = (state >> j) & 1
104-
10597
delta = 0
106-
10798
for idx in [i, j]:
10899
s_old = (state >> idx) & 1
109-
110100
for nbr in neighbors[idx]:
111101
if nbr == i or nbr == j:
112102
continue
113-
114-
s_nbr = (state >> nbr) & 1
115-
103+
s_nbr = (state >> nbr) & 1
116104
old_like = s_old == s_nbr
117105
new_spin = sj if idx == i else si
118106
new_like = new_spin == s_nbr
119-
120-
delta += int(new_like) - int(old_like)
121-
122-
# handle edge between i and j if neighbors
107+
delta += int(new_like) - int(old_like)
123108
if j in neighbors[i]:
124-
old_like = si == sj
125-
new_like = sj == si
126-
delta += int(new_like) - int(old_like)
127-
109+
delta += int(sj == si) - int(si == sj)
128110
return delta
129111

130112

113+
# ── Spatial sampler: repulsive Kawasaki ───────────────────────────────────────
114+
# Electrons carry intrinsic magnetic dipole moments; antiparallel (unlike)
115+
# neighbours have lower magnetic potential energy. The acceptance criterion
116+
# therefore unconditionally favours unlike neighbours, independent of J.
117+
131118
def sample_fixed_hamming_weight(h_weight, count, n_rows, n_cols, burnin=10):
132119
if count == 0:
133120
return []
134121

135-
n_qubits = n_rows * n_cols
136-
neighbors = build_neighbors(n_rows, n_cols)
137-
138-
like_edges = 0
139-
while like_edges == 0:
140-
state = random_fixed_hamming_state(n_qubits, h_weight)
141-
like_edges = count_like_edges(state, neighbors)
142-
143-
samples = []
144-
145-
burn = burnin * n_qubits
122+
n_qubits = n_rows * n_cols
123+
neighbors = build_neighbors(n_rows, n_cols)
124+
total_edges = 2 * n_qubits # 4 neighbours per site, each edge once → 2n
125+
126+
unlike_edges = 0
127+
attempts = 0
128+
while unlike_edges == 0:
129+
state = random_fixed_hamming_state(n_qubits, h_weight)
130+
unlike_edges = count_unlike_edges(state, neighbors)
131+
attempts += 1
132+
if attempts > 1000:
133+
return [state] * count
134+
135+
samples = []
136+
burn = burnin * n_qubits
146137
thinning = n_qubits
147138

148-
ones = set([i for i in range(n_qubits) if (state >> i) & 1])
149-
zeros = set([i for i in range(n_qubits) if not (state >> i) & 1])
139+
ones = set(i for i in range(n_qubits) if (state >> i) & 1)
140+
zeros = set(i for i in range(n_qubits) if not (state >> i) & 1)
150141

151142
for step in range(burn + thinning * count):
152-
# choose random 1-bit and 0-bit
153-
154143
i = random.choice(tuple(ones))
155144
j = random.choice(tuple(zeros))
156145

157-
delta = delta_like_edges(state, i, j, neighbors)
158-
new_like = like_edges + delta
146+
delta_like = delta_like_edges(state, i, j, neighbors)
147+
new_unlike = unlike_edges - delta_like # unlike = total - like
159148

160-
if new_like > 0:
161-
accept_prob = new_like / like_edges
162-
if np.random.random() < accept_prob:
149+
if new_unlike > 0:
150+
# Repulsive: accept proportional to new_unlike / unlike_edges
151+
if np.random.random() < new_unlike / unlike_edges:
163152
state ^= 1 << i
164-
ones.remove(i)
165-
zeros.add(i)
166-
153+
ones.remove(i); zeros.add(i)
167154
state ^= 1 << j
168-
zeros.remove(j)
169-
ones.add(j)
170-
171-
like_edges = new_like
155+
zeros.remove(j); ones.add(j)
156+
unlike_edges = new_unlike
172157

173158
if step >= burn and (step - burn) % thinning == 0:
174159
samples.append(state)
175160

176161
return samples
177162

178163

164+
# ── Numba helpers ──────────────────────────────────────────────────────────────
165+
179166
@njit(cache=True)
180167
def factor_width(width, is_transpose=False):
181168
col_len = math.floor(math.sqrt(width))
182169
while ((width // col_len) * col_len) != width:
183170
col_len -= 1
184171
row_len = width // col_len
185-
186172
return (col_len, row_len) if is_transpose else (row_len, col_len)
187173

188174

189175
@njit(cache=True)
190176
def comb(n, k):
191-
if (k < 0) or (k > n):
192-
return 0
193-
if (k == 0) or (k == n):
194-
return 1
177+
if (k < 0) or (k > n): return 0
178+
if (k == 0) or (k == n): return 1
195179
res = 1
196180
for i in range(1, k + 1):
197181
res = res * (n - k + i) // i
@@ -201,13 +185,12 @@ def comb(n, k):
201185
@njit(cache=True)
202186
def fix_cdf(hamming_prob):
203187
tot_prob = 0.0
204-
n_bias = len(hamming_prob)
188+
n_bias = len(hamming_prob)
205189
cum_prob = np.empty(n_bias, dtype=np.float64)
206190
for i in range(n_bias):
207-
tot_prob += hamming_prob[i]
208-
cum_prob[i] = tot_prob
191+
tot_prob += hamming_prob[i]
192+
cum_prob[i] = tot_prob
209193
cum_prob[-1] = 1.0
210-
211194
return cum_prob
212195

213196

@@ -232,6 +215,8 @@ def get_tfim_hamming_distribution(J=-1.0, h=2.0, z=4, theta=0.174532925199432957
232215
return probability_by_hamming_weight(J, h, z, theta, t, n_qubits + 1, normalized=True, omega=omega)
233216

234217

218+
# ── Public generators ──────────────────────────────────────────────────────────
219+
235220
def generate_tfim_samples(J=-1.0, h=2.0, z=4, theta=0.174532925199432957, t=5, n_qubits=56, shots=100):
236221
samples = []
237222

@@ -244,36 +229,34 @@ def generate_tfim_samples(J=-1.0, h=2.0, z=4, theta=0.174532925199432957, t=5, n
244229
if np.random.random() < prob:
245230
sample |= 1
246231
samples.append(sample)
247-
248232
return samples
249233

250234
n_rows, n_cols = factor_width(n_qubits)
251235

252-
# First dimension: Hamming weight (envelope applied inside)
253-
bias = get_tfim_hamming_distribution(J=J, h=h, z=z, theta=theta, t=t, n_qubits=n_qubits)
236+
# Dimension 1: Hamming weight with time-dependent envelope
237+
bias = get_tfim_hamming_distribution(J=J, h=h, z=z, theta=theta, t=t, n_qubits=n_qubits)
254238
counts = np.random.multinomial(shots, bias)
255239

256240
samples += [0] * counts[0]
257241
samples += [(1 << n_qubits) - 1] * counts[-1]
258242
if n_qubits > 1:
259243
samples += [int(1 << np.random.randint(n_qubits)) for _ in range(counts[1])]
260244
if n_qubits > 2:
261-
mask = (1 << n_qubits) - 1
245+
mask = (1 << n_qubits) - 1
262246
samples += [(mask ^ int(1 << np.random.randint(n_qubits))) for _ in range(counts[-2])]
263247

264-
# Second dimension: magnetic localization
248+
# Dimension 2: spatial magnetic localization (repulsive dipole coupling)
265249
for h_weight in range(2, len(bias) - 2):
266250
samples += sample_fixed_hamming_weight(h_weight, counts[h_weight], n_rows, n_cols)
267251

268252
np.random.shuffle(samples)
269-
270253
return samples
271254

272255

273256
def generate_fermi_hubbard_samples(J=-1.0, h=2.0, z=4, theta=0.174532925199432957, t=5, n_qubits=56, shots=100, omega=1.5 * np.pi):
274257
shots_x, shots_y, shots_z = np.random.multinomial(shots, [1 / 3, 1 / 3, 1 / 3])
275258
return (
276-
generate_tfim_samples(J=J, h=h, z=z, theta=theta, t=t, n_qubits=n_qubits, shots=shots_z)
259+
generate_tfim_samples(J=J, h=h, z=z, theta=theta, t=t, n_qubits=n_qubits, shots=shots_z)
277260
+ generate_tfim_samples(J=-h, h=-J, z=z, theta=theta + np.pi / 2, t=t, n_qubits=n_qubits, shots=shots_x)
278-
+ generate_tfim_samples(J=J, h=h, z=z, theta=theta + np.pi / 2, t=t, n_qubits=n_qubits, shots=shots_y)
261+
+ generate_tfim_samples(J=J, h=h, z=z, theta=theta + np.pi / 2, t=t, n_qubits=n_qubits, shots=shots_y)
279262
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name="pyqrackising",
10-
version="9.15.1",
10+
version="9.15.2",
1111
author="Dan Strano",
1212
author_email="stranoj@gmail.com",
1313
description="Fast MAXCUT, TSP, and sampling heuristics from near-ideal transverse field Ising model (TFIM)",

0 commit comments

Comments
 (0)