forked from tensorcircuit/tensorcircuit-ng
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvqe_toric_code.py
More file actions
658 lines (543 loc) · 20 KB
/
Copy pathvqe_toric_code.py
File metadata and controls
658 lines (543 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
"""
Variational Quantum Eigensolver (VQE) for the Toric Code Model
This module implements VQE optimization for finding ground states of the
generalized 2D toric code Hamiltonian with open boundary conditions and provides comparison
among different ansätze: FLDC, GLDC, and FDC.
Reference:
- [arXiv:2311.01393](https://arxiv.org/pdf/2311.01393)
"""
from itertools import product
from typing import Dict, Tuple
import numpy as np
import tensorflow as tf
import scipy.sparse as sp
from scipy.sparse.linalg import eigs
import matplotlib.pyplot as plt
import tensorcircuit as tc
from tensorcircuit.quantum import PauliStringSum2COO
def build_toric_hamiltonian(
Lx: int, Ly: int, h: float, hx: float = 0.0, hz: float = 0.0
) -> Dict[str, float]:
r"""
Build the generalized 2D toric code Hamiltonian with open boundary conditions.
The Hamiltonian is: H = - (1 - h) \sum A_v - (1 - h) \sum B_p - h \sum (hx * X_i + hz * Z_i)
where A_v are vertex operators (X products) and B_p are plaquette operators (Z products).
**Operator Definitions with Open Boundaries:**
In this model, qubits reside on the edges of the lattice.
Vertex Operators (A_v):
* Vertex operators are defined on each **vertex** (i,j), where
`i` in [0, Lx] and `j` in [0, Ly]. There are `(Lx + 1) * (Ly + 1)` vertices.
* **Boundary Terms:** Vertices on the physical boundary connect to fewer edges:
* **Bulk vertices** (interior) connect to 4 edges. (Weight 1.0)
* **Edge vertices** (non-corner) connect to 3 edges. (Weight 0.75)
* **Corner vertices** connect to 2 edges. (Weight 0.5)
* The code correctly identifies these and applies the corresponding 2, 3, or 4-body
'X' operators with the specified weights.
Plaquette Operators (B_p):
* Plaquette operators are defined on each **plaquette** (or face) (i,j), where
`i` in [0, Lx-1] and `j` in [0, Ly-1]. There are `Lx * Ly` plaquettes.
* **No Boundary Terms:** In this lattice definition, *every* plaquette,
including those at the physical boundary, is surrounded by exactly four edges
(top, bottom, left, right).
* Therefore, all `B_p` operators are 4-body 'Z' terms, and no special
boundary weighting is applied.
**Qubit Indexing:**
The qubit indices for each edge are calculated as:
- Horizontal edges: indexed sequentially along rows (0 to num_horizontal - 1)
- Vertical edges: indexed after all horizontal edges (num_horizontal to num_qubits - 1)
- Top edge of plaquette (i,j): i * Ly + j
- Bottom edge of plaquette (i,j): (i + 1) * Ly + j
- Left edge of plaquette (i,j): num_horizontal + i * (Ly + 1) + j
- Right edge of plaquette (i,j): left + 1
:param Lx: Number of plaquettes in x-direction
:type Lx: int
:param Ly: Number of plaquettes in y-direction
:type Ly: int
:param h: Magnetic field parameter controlling topological phase (0 to 1)
:type h: float
:param hx: X-direction field strength (default: 0.0)
:type hx: float
:param hz: Z-direction field strength (default: 0.0)
:type hz: float
:return: Hamiltonian as a dictionary mapping Pauli strings to coefficients
:rtype: Dict[str, float]
"""
H = {}
# Qubit layout: horizontal edges followed by vertical edges
num_horizontal = (Lx + 1) * Ly
num_vertical = Lx * (Ly + 1)
num_qubits = num_horizontal + num_vertical
print(f"System size: {Lx}x{Ly} lattice, {num_qubits} qubits")
# Vertex operators A_v (X products on edges around each vertex)
for i, j in product(range(Lx + 1), range(Ly + 1)):
connected_edges = []
# Top vertical edge
if i > 0:
edge_idx = (i - 1) * (Ly + 1) + j + num_horizontal
connected_edges.append(edge_idx)
# Bottom vertical edge
if i < Lx:
edge_idx = i * (Ly + 1) + j + num_horizontal
connected_edges.append(edge_idx)
# Left horizontal edge
if j > 0:
edge_idx = i * Ly + (j - 1)
connected_edges.append(edge_idx)
# Right horizontal edge
if j < Ly:
edge_idx = i * Ly + j
connected_edges.append(edge_idx)
if connected_edges:
pauli_str = ["I"] * num_qubits
for idx in connected_edges:
pauli_str[idx] = "X"
pauli_str = "".join(pauli_str)
# Adjust weight for boundary vertices
weight = 1.0
if (i == 0 or i == Lx) and (j == 0 or j == Ly):
weight = 0.5 # Corner vertices
elif i == 0 or i == Lx or j == 0 or j == Ly:
weight = 0.75 # Edge vertices
H[pauli_str] = -weight * (1 - h)
# Plaquette operators B_p (Z products on edges around each plaquette)
for i, j in product(range(Lx), range(Ly)):
top = i * Ly + j
bottom = (i + 1) * Ly + j
left = num_horizontal + i * (Ly + 1) + j
right = left + 1
pauli_str = ["I"] * num_qubits
for idx in [top, bottom, left, right]:
pauli_str[idx] = "Z"
pauli_str = "".join(pauli_str)
H[pauli_str] = -(1 - h)
# External field terms
for q in range(num_qubits):
if hx != 0:
x_str = ["I"] * num_qubits
x_str[q] = "X"
H["".join(x_str)] = -hx * h
if hz != 0:
z_str = ["I"] * num_qubits
z_str[q] = "Z"
H["".join(z_str)] = -hz * h
print(f"Hamiltonian built: {len(H)} terms")
return H
def get_plaquette_qubits(i: int, j: int, Lx: int, Ly: int) -> Dict[str, int]:
"""
Get qubit indices for the four edges of plaquette (i,j).
:param i: Plaquette x-coordinate
:type i: int
:param j: Plaquette y-coordinate
:type j: int
:param Lx: Lattice width
:type Lx: int
:param Ly: Lattice height
:type Ly: int
:return: Dictionary with keys 'top', 'bottom', 'left', 'right' mapping to qubit indices
:rtype: Dict[str, int]
"""
num_horizontal = (Lx + 1) * Ly
return {
"top": int(i * Ly + j),
"bottom": int((i + 1) * Ly + j),
"left": int(num_horizontal + i * (Ly + 1) + j),
"right": int(num_horizontal + i * (Ly + 1) + j + 1),
}
def building_block_su4(
circuit: tc.Circuit, params: tf.Variable, q1: int, q2: int
) -> None:
"""
Apply a parameterized two-qubit SU(4) gate.
:param circuit: Quantum circuit
:type circuit: tc.Circuit
:param params: 15 parameters for the SU(4) gate
:type params: tf.Variable
:param q1: Qubit index 1
:type q1: int
:param q2: Qubit index 2
:type q2: int
"""
circuit.SU4(q1, q2, theta=params)
def fldc_claw_ansatz(
params: tf.Variable, Lx: int, Ly: int, layers: int = 1
) -> tc.Circuit:
"""
Finite local depth circuit (FLDC) ansatz for the toric code.
Applies gate sequences to (left,bottom), (top,bottom), (right,bottom) pairs
for each plaquette in sequence.
:param params: Variational parameters
:type params: tf.Variable
:param Lx: Lattice width
:type Lx: int
:param Ly: Lattice height
:type Ly: int
:param layers: Number of ansatz layers
:type layers: int
:return: Quantum circuit
:rtype: tc.Circuit
"""
num_qubits = (Lx + 1) * Ly + Lx * (Ly + 1)
circuit = tc.Circuit(num_qubits)
param_idx = 0
for _ in range(layers):
for i in range(Lx):
for j in range(Ly):
qubits = get_plaquette_qubits(i, j, Lx, Ly)
for pair in [
(qubits["left"], qubits["bottom"]),
(qubits["top"], qubits["bottom"]),
(qubits["right"], qubits["bottom"]),
]:
building_block_su4(
circuit, params[param_idx : param_idx + 15], pair[0], pair[1]
)
param_idx += 15
return circuit
def gldc_claw_ansatz(
params: tf.Variable, Lx: int, Ly: int, layers: int = 1
) -> tc.Circuit:
"""
Global linear depth circuit (GLDC) ansatz for the toric code.
Groups gates by pair type: all (left,bottom), then all (top,bottom),
then all (right,bottom).
:param params: Variational parameters
:type params: tf.Variable
:param Lx: Lattice width
:type Lx: int
:param Ly: Lattice height
:type Ly: int
:param layers: Number of ansatz layers, if not 1 (not FDC), set to num_qubits (which means full GLDC)
:type layers: int
:return: Quantum circuit
:rtype: tc.Circuit
"""
num_qubits = (Lx + 1) * Ly + Lx * (Ly + 1)
if layers != 1:
layers = num_qubits
circuit = tc.Circuit(num_qubits)
param_idx = 0
for _ in range(layers):
# Left-bottom pairs
for i in range(Lx):
for j in range(Ly):
qubits = get_plaquette_qubits(i, j, Lx, Ly)
building_block_su4(
circuit,
params[param_idx : param_idx + 15],
qubits["left"],
qubits["bottom"],
)
param_idx += 15
# Top-bottom pairs
for i in range(Lx):
for j in range(Ly):
qubits = get_plaquette_qubits(i, j, Lx, Ly)
building_block_su4(
circuit,
params[param_idx : param_idx + 15],
qubits["top"],
qubits["bottom"],
)
param_idx += 15
# Right-bottom pairs
for i in range(Lx):
for j in range(Ly):
qubits = get_plaquette_qubits(i, j, Lx, Ly)
building_block_su4(
circuit,
params[param_idx : param_idx + 15],
qubits["right"],
qubits["bottom"],
)
param_idx += 15
return circuit
def fdc_claw_ansatz(params: tf.Variable, Lx: int, Ly: int) -> tc.Circuit:
"""Finite depth circuit (FDC) ansatz - single layer GLDC."""
return gldc_claw_ansatz(params, Lx, Ly, layers=1)
def build_sparse_hamiltonian(hamiltonian_dict: Dict[str, float]) -> PauliStringSum2COO:
"""
Convert Hamiltonian dictionary to sparse matrix representation.
:param hamiltonian_dict: Hamiltonian as {pauli_string: coefficient}
:type hamiltonian_dict: Dict[str, float]
:return: Sparse Hamiltonian in COO format
:rtype: PauliStringSum2COO
"""
pauli_strings = list(hamiltonian_dict.keys())
coefficients = list(hamiltonian_dict.values())
pauli_map = {"I": 0, "X": 1, "Y": 2, "Z": 3}
pauli_sequences = [[pauli_map[p] for p in ps] for ps in pauli_strings]
return PauliStringSum2COO(pauli_sequences, weight=coefficients)
def energy_calc(c: tc.Circuit, H: PauliStringSum2COO) -> float:
"""Calculate energy expectation value for a given circuit and Hamiltonian."""
return tc.templates.measurements.operator_expectation(c, H)
def vqe_func(
params: tf.Variable,
ansatz_type: str,
H: PauliStringSum2COO,
Lx: int,
Ly: int,
h: float,
hx: float,
hz: float,
num_layers: int = 1,
) -> float:
"""VQE loss function for FLDC/GLDC/FDC ansatz."""
if ansatz_type == "fldc":
c = fldc_claw_ansatz(params, Lx, Ly, layers=num_layers)
elif ansatz_type == "gldc":
c = gldc_claw_ansatz(params, Lx, Ly, layers=num_layers)
elif ansatz_type == "fdc":
c = fdc_claw_ansatz(params, Lx, Ly)
else:
raise ValueError(f"Unknown ansatz type: {ansatz_type}")
return energy_calc(c, H)
def train_vqe(
vqe_vag,
ansatz_type: str,
H: PauliStringSum2COO,
Lx: int,
Ly: int,
maxiter: int,
h: float,
hx: float,
hz: float,
num_params: int,
num_layers: int = 1,
seed: int = 0,
lr: float = 1e-2,
) -> np.ndarray:
"""
Train VQE model.
:param vqe_vag: JIT-compiled value_and_grad function
:param Lx: Lattice width
:param Ly: Lattice height
:param maxiter: Maximum iterations
:param h: Hamiltonian parameter
:param hx: Hamiltonian parameter
:param hz: Hamiltonian parameter
:param num_params: Number of variational parameters
:param num_layers: Number of ansatz layers (for FLDC, set to 1 for FDC and set to num_qubits for GLDC)
:param seed: Random seed
:param lr: Learning rate
:return: Array of energy values during training
:rtype: np.ndarray
"""
energies = []
params = tf.Variable(
tf.random.uniform(
shape=[num_params, 1],
minval=0.0,
maxval=2 * np.pi,
dtype=getattr(tf, tc.rdtypestr),
seed=seed,
)
)
optimizer = tf.keras.optimizers.Adam(lr)
for i in range(maxiter):
energy, grad = vqe_vag(
params, ansatz_type, H, Lx, Ly, h, hx, hz, num_layers=num_layers
)
optimizer.apply_gradients([(grad, params)])
energies.append(energy)
if i % 200 == 0:
print(f"Iteration {i}: Energy = {energy:.6f}")
return np.array(energies)
def run_vqe_trials(
ansatz_type: str,
H: PauliStringSum2COO,
Lx: int,
Ly: int,
maxiter: int,
h: float,
hx: float,
hz: float,
fldc_num_layers: int = 1,
trials: int = 100,
avg_percent: float = 0.5,
) -> Tuple[float, np.ndarray]:
"""
Run multiple VQE trials and return averaged results.
:param ansatz_type: 'fldc', 'gldc', or 'fdc'
:param Lx: Lattice width
:param Ly: Lattice height
:param maxiter: Maximum iterations per trial
:param h: Hamiltonian parameter
:param hx: Hamiltonian parameter
:param hz: Hamiltonian parameter
:param fldc_num_layers: Number of FLDC ansatz layers
:param trials: Number of random trials
:param avg_percent: Fraction of best trials to average
:return: (best_energy_avg, avg_energy_trajectory)
:rtype: Tuple[float, np.ndarray]
"""
# Setup based on ansatz type
num_qubits = (Lx + 1) * Ly + Lx * (Ly + 1)
if ansatz_type == "fldc":
num_params = Lx * Ly * 15 * 3 * fldc_num_layers
elif ansatz_type == "gldc":
num_params = Lx * Ly * 15 * 3 * num_qubits
elif ansatz_type == "fdc":
num_params = Lx * Ly * 15 * 3
else:
raise ValueError(f"Unknown ansatz type: {ansatz_type}")
vqe_vag = tc.backend.jit(
tc.backend.value_and_grad(vqe_func), static_argnums=(1, 2, 3, 4)
)
final_energies = []
avg_energies = np.zeros(maxiter)
for trial in range(trials):
print(f"\nTrial {trial+1}/{trials}")
energies = train_vqe(
vqe_vag,
ansatz_type=ansatz_type,
H=H,
Lx=Lx,
Ly=Ly,
maxiter=maxiter,
h=h,
hx=hx,
hz=hz,
num_params=num_params,
num_layers=fldc_num_layers,
seed=trial,
)
final_energies.append(energies[-1])
avg_energies += energies / trials
print(f"Final energy: {energies[-1]:.6f}")
# Average best trials
final_energies = np.array(final_energies)
best_indices = np.argsort(final_energies)[: int(trials * avg_percent)]
best_energy = final_energies[best_indices].mean()
return best_energy, avg_energies
def compute_exact_ground_energy(sparse_H: PauliStringSum2COO) -> float:
"""
Compute exact ground state energy using sparse diagonalization.
:param sparse_H: Sparse Hamiltonian in COO format
:type sparse_H: PauliStringSum2COO
:return: Exact ground state energy
:rtype: float
"""
indices = sparse_H.indices.numpy()
values = sparse_H.values.numpy()
shape = sparse_H.dense_shape.numpy()
coo_mat = sp.coo_matrix((values, (indices[:, 0], indices[:, 1])), shape=shape)
csr_mat = coo_mat.tocsr()
eigenvalues, _ = eigs(csr_mat, k=1, which="SR")
return eigenvalues[0].real
def main():
"""Main execution function."""
# Configuration
tc.set_backend("tensorflow")
tc.set_dtype("complex128")
print(f"Backend: {tc.backend.name}")
print(f"Dtype: {tc.dtypestr} (real: {tc.rdtypestr})")
# Parameters
Lx, Ly = 2, 2
fldc_num_layers = 3
maxiter = 1000
# For each ansatz, there are 100 independent VQE trials
# And the data are averaged over the best half of the 100 training trajectories
# starting from different initializations.
# It's time consuming to run GLDC ansatz with 100 trials; consider reducing trials for testing.
# And GLDC ansatz requires NUM_QUBITS layers FDC structure, which may lead to very long training time.
trials = 100
avg_percent = 0.5
h_values = [0.1, 0.3, 0.5, 0.7, 0.9]
num_qubits = (Lx + 1) * Ly + Lx * (Ly + 1)
# Storage for results
results = {"ed": [], "fldc": [], "gldc": [], "fdc": []}
# Run experiments
for h in h_values:
print(f"\n{'='*60}")
print(f"Training for h={h:.1f}")
print(f"{'='*60}")
H = build_toric_hamiltonian(Lx=Lx, Ly=Ly, h=h, hx=h, hz=h)
sparse_H = build_sparse_hamiltonian(H)
# Exact diagonalization
ground_energy = compute_exact_ground_energy(sparse_H)
print(f"Exact ground energy: {ground_energy:.6f}")
results["ed"].append(ground_energy)
# VQE with different ansätze
for ansatz in ["fldc", "gldc", "fdc"]:
print(f"\n--- {ansatz.upper()} Ansatz ---")
best_energy, avg_traj = run_vqe_trials(
ansatz,
sparse_H,
Lx,
Ly,
maxiter,
h,
h,
h,
fldc_num_layers=fldc_num_layers if ansatz != "fdc" else 1,
trials=trials,
avg_percent=avg_percent,
)
results[ansatz].append(best_energy)
# Save first trajectory for plotting
if h == h_values[0]:
results[f"{ansatz}_traj"] = (avg_traj - ground_energy) / num_qubits
# Convert to arrays
for key in results:
if not key.endswith("_traj"):
results[key] = np.array(results[key])
h_array = np.array(h_values)
# Define a color map for consistency
# Using Matplotlib default color cycle (C0: blue, C1: orange, C2: green, C3: red)
color_map = {"Exact": "C0", "FDC": "C1", "FLDC": "C2", "GLDC": "C3"}
fig, ax_main = plt.subplots(figsize=(10, 6))
# Plot main graph: Energy per qubit vs h
ax_main.plot(
h_array,
results["ed"] / num_qubits,
"o-",
label="Exact",
linewidth=2,
color=color_map["Exact"],
)
ax_main.plot(
h_array, results["fdc"] / num_qubits, "s-", label="FDC", color=color_map["FDC"]
)
ax_main.plot(
h_array,
results["fldc"] / num_qubits,
"^-",
label="FLDC",
color=color_map["FLDC"],
)
ax_main.plot(
h_array,
results["gldc"] / num_qubits,
"d-",
label="GLDC",
color=color_map["GLDC"],
)
ax_main.set_xlabel("Magnetic field h")
ax_main.set_ylabel("Energy per qubit (E/N)")
ax_main.set_title("Ground State Energy vs Magnetic Field")
ax_main.legend()
ax_main.grid(False)
# Plot inset graph: Error convergence for h=0.1
# To control the position of the inset, you can adjust these values to change the position and size of the inset
inset_pos = [0.72, 0.63, 0.25, 0.25] # [left x, bottom y, width, height]
ax_inset = fig.add_axes(inset_pos)
for ansatz in ["fdc", "fldc", "gldc"]:
if f"{ansatz}_traj" in results:
ansatz_upper = ansatz.upper()
ax_inset.plot(
results[f"{ansatz}_traj"],
label=ansatz_upper,
color=color_map[ansatz_upper],
)
ax_inset.set_xlabel("Iteration", fontsize=10) # 使用稍小的字体
ax_inset.set_ylabel("Error per qubit", fontsize=10)
ax_inset.set_title(f"Training Error (h={h_values[0]})", fontsize=11)
ax_inset.grid(False)
ax_inset.tick_params(axis="both", which="major", labelsize=8)
# plt.savefig('combined_energy_plot.png', dpi=300)
plt.show()
print("\n" + "=" * 60)
print("Training completed!")
print("=" * 60)
if __name__ == "__main__":
main()