Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
NVIDIA
GitHub Repository: NVIDIA/cuda-q-academic
Path: blob/main/hybrid-workflows/auxiliary_files/labs_utils.py
1133 views
1
import numpy as np
2
3
def calculate_energy(sequence):
4
"""
5
Calculates the Energy (E) of a LABS sequence.
6
E is the sum of squares of aperiodic autocorrelations for all non-zero lags.
7
8
Args:
9
sequence (np.array): A binary sequence of 1s and -1s.
10
11
Returns:
12
float: The energy value (lower is better).
13
"""
14
N = len(sequence)
15
16
# Faster vectorized approach using np.correlate
17
# mode='full' gives correlations from lag -(N-1) to (N-1)
18
corr = np.correlate(sequence, sequence, mode='full')
19
20
# The center index (lag 0) is at N-1.
21
# We want lags 1 to N-1, which are indices N to 2N-2.
22
sidelobes = corr[N:]
23
24
return np.sum(sidelobes**2)
25
26
def Flip(s, i):
27
"""Flips the bit at index i of sequence s."""
28
s_new = s.copy()
29
s_new[i] *= -1
30
return s_new
31
32
def tabu_local_search(s0):
33
"""
34
Algorithm 2: TabuSearch
35
Performs a Tabu Search for local improvement on a starting sequence.
36
37
Input: starting sequence s0 (np.array)
38
Output: locally improved sequence s_best, s_best_energy
39
40
We assume the existence of a utility function 'calculate_energy(s)'
41
to compute E(s)
42
"""
43
N = len(s0)
44
45
# 1. Initialize variables
46
s_best = s0.copy() # ~s in the paper
47
p = s0.copy() # Current sequence p
48
s_best_energy = calculate_energy(s_best)
49
50
# 2. Initialize TabuList
51
# TabuList[i] stores the iteration 't' until which index 'i' is forbidden.
52
TabuList = np.zeros(N, dtype=int)
53
54
# 3. Determine number of iterations M
55
# M ∈ [N/2, 3N/2].
56
N_over_2 = N // 2
57
M_range = N - 1 # Range is from N/2 to 3N/2, which is N wide.
58
# To get a random int in [N/2, 3N/2], we can do N/2 + random_int[0, N]
59
# np.random.randint(low, high) is [low, high-1]. We need +1 for inclusive range.
60
M = np.random.randint(0, N + 1) + N_over_2
61
62
# 4. Loop for t = 1 to M
63
for t in range(1, M + 1):
64
65
# 5. Choose index i* with minimum energy among { i | TabuList[i] < t }
66
67
# Candidate set: indices that are NOT tabu at time t
68
non_tabu_indices = np.where(TabuList < t)[0]
69
70
if len(non_tabu_indices) == 0:
71
# If all moves are tabu, break the search
72
break
73
74
best_candidate_energy = np.inf
75
i_star = -1
76
77
# Evaluate all non-tabu neighbors
78
for i in non_tabu_indices:
79
# Generate the candidate sequence p_candidate by flipping bit i in the current p
80
p_candidate = Flip(p, i)
81
E_candidate = calculate_energy(p_candidate)
82
83
# Aspiration Criterion: If the move leads to a new global best,
84
# it should be chosen even if it is tabu.
85
# However, the algorithm does not explicitly list an aspiration criterion.
86
# Sticking strictly to line 5: Choose index i* with minimum energy among { i | TabuList[i] < t }
87
88
if E_candidate < best_candidate_energy:
89
best_candidate_energy = E_candidate
90
i_star = i
91
92
# The best non-tabu neighbor (p_star) is now identified by i_star and best_candidate_energy
93
94
# 6. Update current sequence p
95
p = Flip(p, i_star)
96
E_p = best_candidate_energy # E(p) is already calculated
97
98
# 7. Update TabuList for the chosen index i*
99
theta_min = int(M / 10)
100
theta_max = int(M / 50)
101
102
# If theta_min < theta_max, we might have an issue with integer division.
103
# Assuming theta_min should be the smaller bound (M/50) and theta_max the larger (M/10).
104
# Adjusting interpretation for standard range: [M/50, M/10]
105
if theta_min > theta_max:
106
theta_min, theta_max = theta_max, theta_min
107
108
# Ensure minimum tabu tenure is at least 1
109
if theta_min == 0:
110
theta_min = 1
111
if theta_max <= theta_min:
112
theta_max = theta_min + 1
113
114
# Tabu tenure: random int(theta_min, theta_max)
115
# np.random.randint(low, high) is [low, high-1]. We use [theta_min, theta_max + 1].
116
tenure = np.random.randint(theta_min, theta_max + 1)
117
118
TabuList[i_star] = t + tenure
119
120
# 8. Check for Global Best Update
121
if E_p < s_best_energy:
122
# 9. ~s <- p
123
s_best = p.copy()
124
s_best_energy = E_p
125
126
# 10. return ~s
127
return s_best, s_best_energy
128
129
def compute_topology_overlaps(G2, G4):
130
"""
131
Computes the topological invariants I_22, I_24, I_44 based on set overlaps.
132
I_alpha_beta counts how many sets share IDENTICAL elements.
133
"""
134
# Helper to count identical sets
135
def count_matches(list_a, list_b):
136
matches = 0
137
# Convert to sorted tuples to ensure order doesn't affect equality
138
set_b = set(tuple(sorted(x)) for x in list_b)
139
for item in list_a:
140
if tuple(sorted(item)) in set_b:
141
matches += 1
142
return matches
143
144
# For standard LABS/Ising chains, these overlaps are often 0 or specific integers
145
# We implement the general counting logic here.
146
I_22 = count_matches(G2, G2) # Self overlap is just len(G2)
147
I_44 = count_matches(G4, G4) # Self overlap is just len(G4)
148
I_24 = 0 # 2-body set vs 4-body set overlap usually 0 as sizes differ
149
150
return {'22': I_22, '44': I_44, '24': I_24}
151
152
from math import sin, cos, pi
153
154
def compute_theta(t, dt, total_time, N):
155
"""
156
Computes theta(t) using the analytical solutions for Gamma1 and Gamma2.
157
"""
158
159
# --- 1. Better Schedule (Trigonometric) ---
160
# lambda(t) = sin^2(pi * t / 2T)
161
# lambda_dot(t) = (pi / 2T) * sin(pi * t / T)
162
163
if total_time == 0:
164
return 0.0
165
166
# Argument for the trig functions
167
arg = (pi * t) / (2.0 * total_time)
168
169
lam = sin(arg)**2
170
# Derivative: (pi/2T) * sin(2 * arg) -> sin(pi * t / T)
171
lam_dot = (pi / (2.0 * total_time)) * sin((pi * t) / total_time)
172
173
# --- 2. Get Interactions and counts ---
174
G2, G4 = get_interactions(N)
175
176
# --- 3. Calculate Gamma Terms (LABS assumptions: h^x=1, h^b=0) ---
177
# For G2 (size 2): S_x = 2
178
# For G4 (size 4): S_x = 4
179
180
# Gamma 1 (Eq 16)
181
# Gamma1 = 16 * Sum_G2(S_x) + 64 * Sum_G4(S_x)
182
term_g1_2 = 16 * len(G2) * 2
183
term_g1_4 = 64 * len(G4) * 4
184
Gamma1 = term_g1_2 + term_g1_4
185
186
# Gamma 2 (Eq 17)
187
# G2 term: Sum (lambda^2 * S_x)
188
# S_x = 2
189
sum_G2 = len(G2) * (lam**2 * 2)
190
191
# G4 term: 4 * Sum (4*lambda^2 * S_x + (1-lambda)^2 * 8)
192
# S_x = 4
193
# Inner = 16*lam^2 + 8*(1-lam)^2
194
sum_G4 = 4 * len(G4) * (16 * (lam**2) + 8 * ((1 - lam)**2))
195
196
# Topology part
197
I_vals = compute_topology_overlaps(G2, G4)
198
term_topology = 4 * (lam**2) * (4 * I_vals['24'] + I_vals['22']) + 64 * (lam**2) * I_vals['44']
199
200
# Combine Gamma 2
201
Gamma2 = -256 * (term_topology + sum_G2 + sum_G4)
202
203
# --- 4. Alpha & Theta ---
204
if abs(Gamma2) < 1e-12:
205
alpha = 0.0
206
else:
207
alpha = - Gamma1 / Gamma2
208
209
return dt * alpha * lam_dot
210
211
def get_interactions(N):
212
"""
213
Generates the interaction sets G2 and G4 based on the loop limits in Eq. 15.
214
Returns standard 0-based indices as lists of lists of ints.
215
216
Args:
217
N (int): Sequence length.
218
219
Returns:
220
G2: List of lists containing two body term indices
221
G4: List of lists containing four body term indices
222
"""
223
224
G2 = []
225
# Eq 15: prod_{i=0}^{N-3} prod_{k=0}^{floor((N-i)/2)-1}
226
for i in range(1, N-2 +1):
227
limit_k = (N - i) // 2
228
for k in range(1, limit_k+1):
229
# Term: indices [i, i+k]
230
G2.append([i-1, i + k-1]) #subtract 1 for zero index
231
232
G4 = []
233
# Eq 15: prod_{i=1}^{N-3} prod_{t=1}^{floor((N-i-1)/2)} prod_{k=t+1}^{N-i-t}
234
for i in range(1, N - 3 + 1):
235
limit_t = (N - i - 1) // 2
236
for t in range(1, limit_t +1):
237
limit_k_loop = N - i - t
238
for k in range(t + 1, limit_k_loop +1):
239
# Term: indices [i, i+t, i+k, i+k+t]
240
G4.append([i-1, i + t-1, i + k-1, i + k + t-1]) #subtract 1 for zero indexing of qubits
241
242
#TODO END
243
244
return G2, G4
245