def load_benchmark_plot_data(suffix, **kwargs):
"Loads the data required for the plotting."
= f"bench_plot_data_{suffix}.pkl"
name = Path("../benchmarks/")/name
bench_path if bench_path.exists():
with open(bench_path, 'rb') as f:
= pickle.load(f)
plot_data else:
= process_benchmark_data(suffix, **kwargs)
plot_data return plot_data
def process_benchmark_data(suffix, max_n=16, max_trains=50, algorithms=['DQN', 'MC', 'BFS'], save_path=None):
"Processes the benchmark data for the plots."
= {algorithm: {} for algorithm in algorithms}
results for N in range(5, max_n+1):
= 1, [i%3 for i in range(N)]
B, J = XXHamiltonian(Chain1D(N), B, J)
H
for algorithm in algorithms:
try:
= load_benchmark(H, budget, algorithm, suffix=suffix)
benchmark if algorithm == 'DQN':
= best_so_far(arrange_shape(benchmark['exploration']['oracle_rewards'][:max_trains]))
rewards else:
= best_so_far(arrange_shape(benchmark['oracle_rewards'][:max_trains]))
rewards = np.mean(rewards, axis=0)
best_mean = np.std(rewards, axis=0)
best_std try: over95 = np.where(best_mean >= 0.95)[0][0]
except: over95 = np.inf
= (best_mean, best_std, over95)
results[algorithm][N] except:
pass
if save_path is None: save_path = Path(f"../benchmarks/bench_plot_data_{suffix}.pkl")
with open(save_path, 'wb') as f:
=pickle.HIGHEST_PROTOCOL)
pickle.dump(results, f, protocolreturn results
Plots
Benchmark plots
In the paper we compare the performance of different methods. Here we provide the source code to reproduce the plots (Figure 5).
def plot_benchmark(results, suffix, ylim=(0, 2300), lw=3.5, ms=10, fs=18, ts=16, n_max=17, algorithms=["DQN", "MC", "BFS"]):
= ['-', ':', '--']
linestyles = ['s', 'o', 'h']
markers = plt.figure(figsize=(12, 5))
fig
for a, alg in enumerate(results.keys()):
if alg not in algorithms: continue
= False
plot_vline = [], []
ns, times for n, (_, _, over95) in results[alg].items():
if n <= n_max: ns.append(n); times.append(over95)
if times[-1] == np.inf: ns.pop(-1); times.pop(-1); plot_vline = True
elif ns[-1] < n_max: plot_vline = True
= alg if alg != 'DQN' else 'RL'
label =linestyles[a], linewidth=lw, marker=markers[a], ms=ms, label=label)
plt.plot(ns, times, linestyleif plot_vline:
-1], 0, ylim[1], linestyles='dashed', alpha=0.7)
plt.vlines(ns[-1], 0.93*ylim[1], r"$\rightarrow$ Not found", fontsize=fs)
plt.text(ns[
=0.5)
plt.grid(alpha=ts, loc="upper left")
plt.legend(fontsize"System size", fontsize=fs)
plt.xlabel("States to 95% optimality", fontsize=ts)
plt.ylabel(=ts)
plt.tick_params(labelsizef"../figures/benchmark_sizes_{suffix}.pdf"), bbox_inches='tight') plt.savefig(Path(
= 'half_3'
suffix = load_benchmark_plot_data(suffix) results_half
=16) plot_benchmark(results_half, suffix, n_max
= 'all_3'
suffix = load_benchmark_plot_data(suffix) results_all
=(0, 3500), n_max=16) plot_benchmark(results_all, suffix, ylim
= 11
n
= ['-.', ':', '--']
linestyles = 3.5
linewidth = 18
fs = 15
ticksize =(10, 5))
plt.figure(figsizefor a, algorithm in enumerate(algorithms):
0], linestyle=linestyles[a], linewidth=linewidth, label=algorithm)
plt.plot(results[algorithm][n][
plt.grid()=ticksize)
plt.legend(fontsize=ticksize)
plt.tick_params(labelsize"New visited states", fontsize=fs)
plt.xlabel("Proximity to optimal solution", fontsize=fs); plt.ylabel(
Transfer learning across phases
We analyse the effect of transfer learning across different phases of the same Hamiltonian. Here we provide the source code to reproduce the plots (Figure 6).
= 6
N = 185
budget = 5
B0 = Path(f"../benchmarks/TL_N{N}_{budget}_from_B{B0}.pkl")
path with open(path, 'rb') as f:
= pickle.load(f) TL_evaluation
def convergence_time(results, tol=5e-4, T=50, t_avg=20, return_diffs=False):
"Returns the convergence with criterion of not changing result by `tol` for `T` epochs."
if 'training' in results.keys(): results = results['training']
= arrange_shape(results['rewards'])
rewards = np.mean(rewards, axis=0)
mean_rewards = len(mean_rewards)
epochs = np.convolve(np.array([1/t_avg]*t_avg), mean_rewards, mode='valid')
moving_avg = np.abs(moving_avg[1:] - moving_avg[:-1])
diffs = np.convolve(np.array([1/T]*T), diffs, mode='valid')
diff_variation try: t = np.where(diff_variation <= tol)[0][0]
except: t = len(mean_rewards)
if return_diffs: return t + 2*T, moving_avg, diff_variation
return t + T
def indiv_convergence_time(results, max_epochs=800, **kwargs):
"Similar to `convergence_time` but with each agent."
= results['rewards']
results = [convergence_time({'rewards': [res[:max_epochs]]}, **kwargs) for res in results]
times return np.array(times)
def get_indiv_times(tl_eval, convergence_crit=None):
"Provides convergence times from individual ratios."
= {'T': 50, 't_avg': 100, 'tol': 2e-4}
default_crit = {**default_crit, **convergence_crit} if convergence_crit is not None else default_crit
convergence_crit = list(TL_evaluation.keys()); Bs.sort()
Bs = [], []
time_ratios, time_err for b in Bs:
= indiv_convergence_time(tl_eval[b]['tl'], **convergence_crit)
ts_tl = indiv_convergence_time(tl_eval[b]['vanilla'], **convergence_crit)
ts_0 = ts_0.mean(), ts_tl.mean()
t0, ttl = ttl/t0
ratio = np.sqrt((1/t0)**2*ts_tl.var() + (ttl/t0**2)**2*ts_0.var())
ratio_std
time_ratios.append(ratio)/np.sqrt(len(ts_0)))
time_err.append(ratio_stdreturn Bs, np.array(time_ratios), np.array(time_err)
= [0., 1.5, 2., 4.]
inset_Bs = 0.25, 0.2
ax_width, ax_length = 700
max_epochs = ["(a)", "(b)", "(c)", "(d)"]
refs = [np.mean(TL_evaluation[b]['tl']['rewards'], axis=0)[:max_epochs] for b in inset_Bs]
plot_rewards_tl = [np.mean(TL_evaluation[b]['vanilla']['rewards'], axis=0)[:max_epochs] for b in inset_Bs]
plot_rewards_cs
def plot_subplot(ax, tl_rewards, cs_rewards, xlabel=False, legend=False, ref="(a)", ylim=[-0.05, 1.05]):
=3, label="CS")
ax.plot(cs_rewards, linewidth=3, label="TL")
ax.plot(tl_rewards, linewidth=xlabel)
ax.tick_params(labelbottom0.1, 0.7, ref, fontsize=16)
ax.text(if xlabel: ax.set_xlabel("Training Episode", fontsize=20)
if legend: ax.legend(fontsize=14, loc='lower right')
=16)
ax.tick_params(labelsize
ax.set_ylim(ylim)
ax.grid()
= plt.figure(figsize=(14, 5))
fig = gs.GridSpec(1, 3, figure=fig)
gs0
= fig.add_subplot(gs0[:-1])
ax1
# Times
= get_indiv_times(TL_evaluation)
Bs, time_ratios, time_errs
-time_errs, time_ratios+time_errs, alpha=0.25)
ax1.fill_between(Bs, time_ratios's-', ms=7, lw=2)
ax1.plot(Bs, time_ratios, =16)
ax1.tick_params(labelsize"B/J", fontsize=20)
ax1.set_xlabel(r"$t_{TL}/t_0$", fontsize=20);
ax1.set_ylabel(=0.5)
ax1.grid(alphafor b, ref in zip(inset_Bs, refs):
if b != 2: dx, dy = 0.1, 0.05
else: dx, dy = 0.23, 0.
= b-dx, time_ratios[np.where(np.array(Bs) == b)[0][0]]+dy
x, y =16)
ax1.text(x, y, ref, fontsize= ax1.get_ylim()
ymin, ymax 0.75, ymin*1.2, ymax*0.95, linestyles='dashed', alpha=0.5)
ax1.vlines(1, ymin*1.2, ymax*0.95, linestyles='dashed', alpha=0.5)
ax1.vlines(2, ymin*1.2, ymax*0.95, linestyles='dashed', alpha=0.5)
ax1.vlines(
# Trainings
= gs0[-1].subgridspec(4, 1)
gs1
= [fig.add_subplot(gs1[i]) for i in range(4)]
axes2 for i, ax in enumerate(axes2):
=i==3, legend=i==0, ref=refs[i])
plot_subplot(ax, plot_rewards_tl[i], plot_rewards_cs[i], xlabel0.635, 0.5, "Reward", va='center', rotation='vertical', fontsize=20);
fig.text(f"../figures/TL_bench_N{N}_{budget}_from_B{B0}.pdf", bbox_inches="tight") plt.savefig(
Energy bounds at different system sizes
We also study the energy bounds provided by the same qualitative solutions at different system sizes in Appendix C. With the following code you can obtain the energy bounds along one phase of the XX Hamiltonian for several sets of constraints.
from bounce.environment import SdPEnvironment
from bounce.sdp import SdPEnergySolver
def test_configs(confs, N, Bs):
"Tests a list of configurations for the XX Hamiltonian in the range of Bs (J=1)."
= 1
J = []
energies for B in tqdm(Bs):
= XXHamiltonian(Chain1D(N), B, J)
H = SdPEnvironment(H, SdPEnergySolver(), budget)
e = [], [], []
c_energies, c_params, c_layouts for c in confs:
e.reset()= 1
e.state[:N] = 1
e.state[c] = e.get_values()
es, ps, _
c_energies.append(es)
c_params.append(ps)
c_layouts.append(e.layout)
energies.append(c_energies)return np.array(energies), np.array(c_params), c_layouts
def plot_tests(energies, params, labels, Bs, norm=False, figsize=(8, 4), fontsize=16, labelsize=14):
=figsize)
plt.figure(figsizefor energy, params, label in zip(energies.T, params, labels):
=label+f" ({params})")
plt.plot(Bs, energy, label
plt.grid()=labelsize)
plt.legend(fontsize2])
plt.xticks(Bs[::"B/J", fontsize=fontsize)
plt.xlabel("Energy bound per spin" if norm else "Energy bound", fontsize=fontsize)
plt.ylabel(=labelsize)
plt.tick_params(labelsize0, 0.75, facecolor='C0', alpha=0.1)
plt.axvspan(0.75, 1., facecolor='C1', alpha=0.1)
plt.axvspan(1., 2., facecolor='C2', alpha=0.1)
plt.axvspan(2., 2.1, facecolor='C3', alpha=0.1);
plt.axvspan(
def save_tests(energies, params, layouts, N):
= Path(f"../benchmarks/test_{N}.pkl")
save_path with open(save_path, 'wb') as f:
=pickle.HIGHEST_PROTOCOL)
pickle.dump((energies, params, layouts), f, protocol
def load_tests(N):
= Path(f"../benchmarks/test_{N}.pkl")
save_path with open(save_path, 'rb') as f:
= pickle.load(f)
energies, params, layouts return energies, params, layouts
In every system size, we compute the bounds obtained with different layouts that we store in the list confs
. In the code below, all the configurations that are never optimal are commented out. This way, we can call test_configs
to compute the bounds for each layout given several values of the external magnetic field \(B\) (assuming \(J=1\)).
We perform the entire analysis for sizes \(n=6, 12, 24\) and \(36\).
= 6
N = [np.arange(0, N, 2)+N,
confs 0]), np.arange(1, N-1, 2)))+N,
np.concatenate((np.array([# np.arange(0, N//2, 1)+N, # double overlap
0, N, 3)+N,
np.arange(0, 1)]
np.arange(= np.arange(0, 2.2, 0.1)
Bs
= test_configs(confs, N, Bs)
energies_6, params_6, layouts_6 save_tests(energies_6, params_6, layouts_6, N)
# Load previous results
= np.arange(0, 2.2, 0.1)
Bs = load_tests(6) energies_6, params_6, layouts_6
= ['(a) 3 T, 0 P', '(b) 3 T, 1 P', '(c) 2 T, 2 P', '(d) 0 T, 6 P']
labels # Absolute bounds
plot_tests(energies_6, params_6, labels, Bs)"N=6", fontsize=16);
plt.title("../figures/pattern_test_N6.pdf", bbox_inches="tight")
plt.savefig(# Normalized bounds
/6, params_6, labels, Bs, norm=True)
plot_tests(energies_6"N=6", fontsize=16);
plt.title("../figures/pattern_test_N6_norm.pdf", bbox_inches="tight") plt.savefig(
= 12
N = [np.arange(0, N, 2)+N,
confs 0]), np.arange(1, N//2-1, 2), np.array([N//2]), np.arange(N//2+1, N-1, 2)))+N,
np.concatenate((np.array([0, N, 3)+N,
np.arange(0, 1),
np.arange(0]), np.arange(1, N-1, 2)))+N,
np.concatenate((np.array([# np.sort(np.concatenate((np.arange(0, N, 4), np.arange(1, N, 4))))+N, # tiplet-triplet-pair
# np.arange(0, N, 4)+N, # triplet-pair-pair
]= np.arange(0, 2.2, 0.1)
Bs
= test_configs(confs, N, Bs)
energies_12, params_12, layouts_12 save_tests(energies_12, params_12, layouts_12, N)
# Load previous results
= np.arange(0, 2.2, 0.1)
Bs = load_tests(12) energies_12, params_12, layouts_12
= ['(a) 6 T, 0 P', '(b) 6 T, 2 P', '(c) 4 T, 4 P', '(d) 0 T, 12 P']
labels # Absolute bounds
-1], params_12[:-1], labels, Bs)
plot_tests(energies_12[:, :"N=12", fontsize=16);
plt.title("../figures/pattern_test_N12.pdf", bbox_inches="tight")
plt.savefig(# Normalized bounds
-1]/12, params_12[:-1], labels, Bs, norm=True)
plot_tests(energies_12[:, :"N=12", fontsize=16);
plt.title("../figures/pattern_test_N12_norm.pdf", bbox_inches="tight") plt.savefig(
= 24
N = [np.arange(0, N, 2)+N,
confs *(N//4)+1, (p+1)*N//4-1, 2) for p in range(4)] + [np.array([p*N//4]) for p in range(4)])+N,
np.concatenate([np.arange(p0, N, 3)+N,
np.arange(0, 1),
np.arange(*(N//3)+1, (p+1)*N//3-1, 2) for p in range(3)] + [np.array([p*N//3]) for p in range(3)])+N,
np.concatenate([np.arange(p0]), np.arange(1, N//2-1, 2), np.array([N//2]), np.arange(N//2+1, N-1, 2)))+N,
np.concatenate((np.array([0]), np.arange(1, N-1, 2)))+N,
np.concatenate((np.array([# np.sort(np.concatenate((np.arange(0, N, 4), np.arange(1, N, 4))))+N, # tiplet-triplet-pair
# np.arange(0, N, 4)+N, # triplet-pair-pair
]= np.arange(0, 2.2, 0.1)
Bs
= test_configs(confs, N, Bs)
energies_24, params_24, layouts_24 save_tests(energies_24, params_24, layouts_24, N)
# Load previous results
= np.arange(0, 2.2, 0.1)
Bs = load_tests(24) energies_24, params_24, layouts_24
= ['(a) 12 T, 0 P', '(b) 12 T, 4 P', '(c) 8 T, 8 P', '(d) 0 T, 24 P']
labels # Absolute bounds
-3], params_24[:-3], labels, Bs)
plot_tests(energies_24[:, :"N=24", fontsize=16);
plt.title("../figures/pattern_test_N24.pdf", bbox_inches="tight")
plt.savefig(# Normalized bounds
-3]/24, params_24[:-3], labels, Bs, norm=True)
plot_tests(energies_24[:, :"N=24", fontsize=16);
plt.title("../figures/pattern_test_N24_norm.pdf", bbox_inches="tight") plt.savefig(
= 36
N = [np.arange(0, N, 2)+N,
confs *(N//6)+1, (p+1)*N//6-1, 2) for p in range(6)] + [np.array([p*N//6]) for p in range(6)])+N,
np.concatenate([np.arange(p0, N, 3)+N,
np.arange(0, 1),
np.arange(*(N//4)+1, (p+1)*N//4-1, 2) for p in range(4)] + [np.array([p*N//4]) for p in range(4)])+N,
np.concatenate([np.arange(p*(N//3)+1, (p+1)*N//3-1, 2) for p in range(3)] + [np.array([p*N//3]) for p in range(3)])+N,
np.concatenate([np.arange(p0]), np.arange(1, N//2-1, 2), np.array([N//2]), np.arange(N//2+1, N-1, 2)))+N,
np.concatenate((np.array([0]), np.arange(1, N-1, 2)))+N,
np.concatenate((np.array([# np.sort(np.concatenate((np.arange(0, N, 4), np.arange(1, N, 4))))+N, # tiplet-triplet-pair
# np.arange(0, N, 4)+N, # triplet-pair-pair
]= np.arange(0, 2.2, 0.1)
Bs
= test_configs(confs, N, Bs)
energies_36, params_36, layouts_36 save_tests(energies_36, params_36, layouts_36, N)
# Load previous results
= np.arange(0, 2.2, 0.1)
Bs = load_tests(36) energies_36, params_36, layouts_36
= ['(a) 18 T, 0 P', '(b) 18 T, 6 P', '(c) 12 T, 12 P', '(d) 0 T, 36 P']
labels # Absolute bounds
-3], params_36[:-3], labels, Bs)
plot_tests(energies_36[:, :"N=36", fontsize=16);
plt.title("../figures/pattern_test_N36.pdf", bbox_inches="tight")
plt.savefig(# Normalized bounds
-3]/36, params_36[:-3], labels, Bs, norm=True)
plot_tests(energies_36[:, :"N=36", fontsize=16);
plt.title("../figures/pattern_test_N36_norm.pdf", bbox_inches="tight") plt.savefig(
Comparison between different lower bound methods
We compare the performance of our SdP-based approach with other techniques developed to lower bound the ground state energy of many-body Hamiltonians. See the SdP docs for more details about the methods.
Executing the following cells you will reproduce Figure 10 from Appendix D.
from bounce.sdp import SdPEnergySolver, SdPEnergyAndersonSolver, SdPEnergyUskovLichkovskiySolver
def compare_bounds(N, B_values, rdm_size=5):
"Returns the energy bounds obtained with Anderson, Uskov-Lichkovskiy and our methods."
= [[np.sort(np.arange(i, i + rdm_size)%N) for i in np.arange(0, N, rdm_size - s)]
layouts for s in range(1, rdm_size)]
= [], [], [[] for _ in range(len(layouts))]
anderson, uskov, our_bounds = SdPEnergyAndersonSolver()
anderson_solver = SdPEnergyUskovLichkovskiySolver()
uskov_solver = SdPEnergySolver()
our_solver
= 1
J for B in tqdm(B_values):
= XXHamiltonian(Chain1D(N), B, J)
H =rdm_size))
anderson.append(anderson_solver.solve(H.to_sdp(), cluster_size=rdm_size))
uskov.append(uskov_solver.solve(H.to_sdp, cluster_sizefor layout, bounds in zip(layouts, our_bounds):
bounds.append(our_solver.solve(H.to_sdp(), layout))
return (anderson, uskov, *our_bounds)
= 8
N = 5
rdm_size = np.arange(0, 3.1, 0.1) # Main plot with large spacing
Bs = np.arange(0, 0.11, 0.01) # Finer grid for the inset Bs_inset
# Data for main plot
= compare_bounds(N, Bs, rdm_size=rdm_size)
compare_data
= Path(f"../benchmarks/anderson_uskov_comparison_N{N}_cs{rdm_size}.pkl")
comparison_path =True)
comparison_path.mkdir(exist_okwith open(comparison_path, 'wb') as f:
=pickle.HIGHEST_PROTOCOL) pickle.dump(compare_data, f, protocol
# Data for inset
= compare_bounds(N, Bs_inset, rdm_size=rdm_size)
inset_data
= Path(f"../benchmarks/anderson_uskov_comparison_N{N}_cs{rdm_size}_inset.pkl")
inset_path =True)
inset_path.mkdir(exist_okwith open(inset_path, 'wb') as f:
=pickle.HIGHEST_PROTOCOL) pickle.dump(inset_data, f, protocol
# Load previous results
= 8
N = np.arange(0, 3.1, 0.1)
Bs = np.arange(0, 0.11, 0.01)
Bs_inset
= Path(f"../benchmarks/anderson_uskov_comparison_N{N}_cs{rdm_size}.pkl")
comparison_path with open(comparison_path, 'rb') as f:
= pickle.load(f)
comparison
= Path(f"../benchmarks/anderson_uskov_comparison_N{N}_cs{rdm_size}_inset.pkl")
inset_path with open(inset_path, 'rb') as f:
= pickle.load(f) inset
= (8, 5)
figsize = 16, 14
fontsize, labelsize = comparison[:-1]
plot_data = inset[:-1]
inset_data = ['Anderson bound', 'TI bound', '1-body overlap', '2-body overlap', '3-body overlap']
labels = plt.figure(figsize=figsize)
fig = fig.add_axes([0.1, 0.1, 0.9, 0.8])
ax1 = fig.add_axes([0.19, 0.17, 0.42, 0.45])
ax2 for bounds, ins, label in zip(plot_data, inset_data, labels):
/N, linewidth=2.2, label=label)
ax1.plot(Bs, np.array(bounds)/N, linewidth=2)
ax2.plot(Bs_inset, np.array(ins)
ax1.grid()=labelsize, loc="upper right")
ax1.legend(fontsize5])
ax1.set_xticks(Bs[::"B/J", fontsize=fontsize)
ax1.set_xlabel("Energy bound per spin", fontsize=fontsize)
ax1.set_ylabel(=labelsize);
ax1.tick_params(labelsize
ax2.grid()-1.375, -1.33])
ax2.set_ylim([-0.002, 0.075])
ax2.set_xlim([# ax2.set_yticks([-1.5, -1.45, -1.4, -1.35]);
"../figures/comparison_anderson_uskov.pdf", bbox_inches="tight") plt.savefig(