Skip to content

Commit 3dcffb1

Browse files
committed
fix according to the review
1 parent 8c666fa commit 3dcffb1

File tree

5 files changed

+301
-184
lines changed

5 files changed

+301
-184
lines changed

examples/vqe2d_lattice.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import time
2+
import optax
3+
import tensorcircuit as tc
4+
from tensorcircuit.templates.lattice import SquareLattice, get_compatible_layers
5+
from tensorcircuit.templates.hamiltonians import heisenberg_hamiltonian
6+
7+
# ===================
8+
# Backend and Hardware Configuration
9+
# ===================
10+
# Use JAX for high-performance, especially on GPU.
11+
# For CPU-only environments, TensorFlow can also be efficient.
12+
K = tc.set_backend("jax")
13+
tc.set_dtype("complex64")
14+
# Use a more powerful contractor for better performance on larger graphs.
15+
# On Windows, cotengra's multiprocessing can cause issues.
16+
# We disable it here to ensure stability.
17+
tc.set_contractor("cotengra-8192-8192", parallel=False)
18+
19+
20+
def run_vqe():
21+
# ===================
22+
# Lattice and Hamiltonian Definition
23+
# ===================
24+
# Define the 2D lattice dimensions and the number of VQE layers.
25+
n, m, nlayers = 4, 4, 6
26+
27+
# 1. Create a SquareLattice instance.
28+
# This object holds all geometric information, such as site coordinates and neighbors.
29+
lattice = SquareLattice(size=(n, m), pbc=True, precompute_neighbors=1)
30+
31+
# 2. Generate the Heisenberg Hamiltonian using the new interface.
32+
# This function directly takes the lattice object and coupling constants.
33+
# It's cleaner than the old method that required a separate graph object.
34+
h = heisenberg_hamiltonian(lattice, j_coupling=[1.0, 1.0, 0.8]) # Jx, Jy, Jz
35+
36+
# 3. Get nearest-neighbor bonds and partition them into compatible layers.
37+
# This is the core of the gate scheduling logic. `get_compatible_layers`
38+
# ensures that gates within each layer can be applied in parallel without overlap.
39+
nn_bonds = lattice.get_neighbor_pairs(k=1, unique=True)
40+
gate_layers = get_compatible_layers(nn_bonds)
41+
42+
# ===================
43+
# VQE Ansatz and Forward Pass
44+
# ===================
45+
46+
def singlet_init(
47+
circuit,
48+
): # A good initial state for Heisenberg ground state search
49+
nq = circuit._nqubits
50+
for i in range(0, nq - 1, 2):
51+
j = (i + 1) % nq
52+
circuit.X(i)
53+
circuit.H(i)
54+
circuit.cnot(i, j)
55+
circuit.X(j)
56+
return circuit
57+
58+
def vqe_forward(param):
59+
"""
60+
Defines the VQE ansatz and computes the energy expectation.
61+
62+
The ansatz structure is:
63+
- Initial state preparation (singlet pairs).
64+
- nlayers of parameterized blocks.
65+
- Each block consists of RZZ, RXX, and RYY entangling layers.
66+
- Gates within each entangling layer are applied according to the pre-computed
67+
`gate_layers` for maximum parallelism. All gates of the same type in a
68+
VQE layer share the same parameter.
69+
"""
70+
c = tc.Circuit(n * m)
71+
c = singlet_init(c)
72+
73+
for i in range(nlayers):
74+
# RZZ layer
75+
for layer in gate_layers:
76+
for j, k in layer:
77+
c.rzz(int(j), int(k), theta=param[i, 0])
78+
79+
# RXX layer
80+
for layer in gate_layers:
81+
for j, k in layer:
82+
c.rxx(int(j), int(k), theta=param[i, 1])
83+
84+
# RYY layer
85+
for layer in gate_layers:
86+
for j, k in layer:
87+
c.ryy(int(j), int(k), theta=param[i, 2])
88+
89+
# The Hamiltonian is a sparse matrix, so we use the corresponding expectation method.
90+
return tc.templates.measurements.operator_expectation(c, h)
91+
92+
# ===================
93+
# Training and Optimization (JAX-based for performance)
94+
# ===================
95+
# Value_and_grad for single (non-batched) training instance.
96+
vgf = K.jit(K.value_and_grad(vqe_forward))
97+
98+
# Parameters for a single training instance.
99+
# Shape: (nlayers, 3) -> 3 for RZZ, RXX, RYY angles per layer.
100+
param = tc.backend.implicit_randn(stddev=0.02, shape=[nlayers, 3])
101+
102+
# Use the Adam optimizer from Optax.
103+
optimizer = optax.adam(learning_rate=3e-3)
104+
opt_state = optimizer.init(param)
105+
106+
@K.jit
107+
def train_step(param, opt_state):
108+
"""
109+
A single training step, JIT-compiled for maximum speed.
110+
This follows the standard Optax optimization paradigm.
111+
"""
112+
loss_val, grads = vgf(param)
113+
updates, opt_state = optimizer.update(grads, opt_state, param)
114+
param = optax.apply_updates(param, updates)
115+
return param, opt_state, loss_val
116+
117+
# ===================
118+
# Main Training Loop
119+
# ===================
120+
print("Starting VQE optimization...")
121+
for i in range(1000):
122+
time0 = time.time()
123+
param, opt_state, loss = train_step(param, opt_state)
124+
time1 = time.time()
125+
if i % 10 == 0:
126+
print(
127+
f"Step {i:4d}: Loss = {loss:.6f} \t (Time per step: {time1 - time0:.4f}s)"
128+
)
129+
130+
print("Optimization finished.")
131+
# Example result on A800 GPU: ~-25.3
132+
print(f"Final Loss: {loss:.6f}")
133+
134+
135+
if __name__ == "__main__":
136+
run_vqe()

tensorcircuit/templates/circuit_utils.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

tensorcircuit/templates/lattice.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Union,
1616
TYPE_CHECKING,
1717
cast,
18+
Set,
1819
)
1920

2021
logger = logging.getLogger(__name__)
@@ -1446,3 +1447,54 @@ def remove_sites(self, identifiers: List[SiteIdentifier]) -> None:
14461447
logger.info(
14471448
f"{len(ids_to_remove)} sites removed. Lattice now has {self.num_sites} sites."
14481449
)
1450+
1451+
1452+
def get_compatible_layers(bonds: List[Tuple[int, int]]) -> List[List[Tuple[int, int]]]:
1453+
"""
1454+
Partitions a list of pairs (bonds) into compatible layers for parallel
1455+
gate application using a greedy edge-coloring algorithm.
1456+
1457+
This function takes a list of pairs, representing connections like
1458+
nearest-neighbor (NN) or next-nearest-neighbor (NNN) bonds, and
1459+
partitions them into the minimum number of sets ("layers") where no two
1460+
pairs in a set share an index. This is a general utility for scheduling
1461+
non-overlapping operations.
1462+
1463+
:Example:
1464+
1465+
>>> from tensorcircuit.templates.lattice import SquareLattice
1466+
>>> sq_lattice = SquareLattice(size=(2, 2), pbc=False)
1467+
>>> nn_bonds = sq_lattice.get_neighbor_pairs(k=1, unique=True)
1468+
1469+
>>> gate_layers = get_compatible_layers(nn_bonds)
1470+
>>> print(gate_layers)
1471+
[[[0, 1], [2, 3]], [[0, 2], [1, 3]]]
1472+
1473+
:param bonds: A list of tuples, where each tuple represents a bond (i, j)
1474+
of site indices to be scheduled.
1475+
:type bonds: List[Tuple[int, int]]
1476+
:return: A list of layers. Each layer is a list of tuples, where each
1477+
tuple represents a bond. All bonds within a layer are non-overlapping.
1478+
:rtype: List[List[Tuple[int, int]]]
1479+
"""
1480+
uncolored_edges: Set[Tuple[int, int]] = {(min(bond), max(bond)) for bond in bonds}
1481+
1482+
layers: List[List[Tuple[int, int]]] = []
1483+
1484+
while uncolored_edges:
1485+
current_layer: List[Tuple[int, int]] = []
1486+
qubits_in_this_layer: Set[int] = set()
1487+
1488+
edges_to_process = sorted(list(uncolored_edges))
1489+
1490+
for edge in edges_to_process:
1491+
i, j = edge
1492+
if i not in qubits_in_this_layer and j not in qubits_in_this_layer:
1493+
current_layer.append(edge)
1494+
qubits_in_this_layer.add(i)
1495+
qubits_in_this_layer.add(j)
1496+
1497+
uncolored_edges -= set(current_layer)
1498+
layers.append(sorted(current_layer))
1499+
1500+
return layers

tests/test_circuit_utils.py

Lines changed: 0 additions & 125 deletions
This file was deleted.

0 commit comments

Comments
 (0)