Skip to content

Commit d4d3d8d

Browse files
committed
refresh the whole file, making no complicated conversion
1 parent 52f9804 commit d4d3d8d

File tree

1 file changed

+141
-219
lines changed

1 file changed

+141
-219
lines changed

examples/vqe_qudit_example.py

Lines changed: 141 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -1,248 +1,170 @@
1-
r"""
2-
You must set the backend explicitly via --backend {jax, tensorflow}.
3-
AD-based optimization (gradient descent) is enabled for these backends.
4-
A fallback random-search optimizer is also provided.
5-
6-
Example runs:
7-
python vqe_qudit_example.py --backend jax --optimizer gd --dim 3 --layers 2 --steps 200 --lr 0.1 --jit
8-
python vqe_qudit_example.py --backend tensorflow --optimizer gd --dim 3 --layers 2 --steps 200 --lr 0.1
9-
python vqe_qudit_example.py --backend jax --optimizer random --dim 3 --layers 2 --iters 300
10-
11-
What this script does:
12-
- Builds a 2-qudit (d>=3) ansatz with native RY/RZ single-qudit rotations on adjacent levels
13-
and an RXX entangler on (0,1) level pairs.
14-
- Minimizes the expectation of a simple 2-site Hermitian Hamiltonian:
15-
H = N(0) + N(1) + J * [ X_sym(0)\otimes X_sym(1) + Z_sym(0)\otimes Z_sym(1) ]
16-
where N = diag(0,1,...,d-1), X_sym = (X + X^\dagger)/2, Z_sym = (Z + Z^\dagger)/2.
171
"""
2+
VQE on QuditCircuits.
183
19-
import argparse
20-
import math
21-
import sys
22-
from typing import Sequence, Tuple
4+
This example shows how to run a simple VQE on a qudit system using
5+
`tensorcircuit.QuditCircuit`. We build a compact ansatz using single-qudit
6+
rotations in selected two-level subspaces and RXX-type entanglers, then
7+
optimize the energy of a Hermitian "clock–shift" Hamiltonian:
238
24-
import numpy as np
25-
import tensorcircuit as tc
26-
from tensorcircuit.quditcircuit import QuditCircuit
9+
H(d) = - J * (X_c \otimes X_c) - h * (Z_c \otimes I + I \otimes Z_c)
2710
11+
where, for local dimension `d`,
12+
- Z_c = (Z + Z^\dagger)/2 is the Hermitian "clock" observable with Z = diag(1, \omega, \omega^2, ..., \omega^{d-1})
13+
- X_c = (S + S^\dagger)/2 is the Hermitian "shift" observable with S the cyclic shift
14+
- \omega = exp(2\pi i/d)
2815
29-
# ---------- Hamiltonian helpers ----------
30-
def symmetrize_hermitian(U: np.ndarray) -> np.ndarray:
31-
return 0.5 * (U + U.conj().T)
16+
The code defaults to a 2-qutrit (d=3) problem but can be changed via CLI flags.
17+
"""
3218

19+
# import os, sys
20+
#
21+
# base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
22+
# if base_dir not in sys.path:
23+
# sys.path.insert(0, base_dir)
3324

34-
def build_2site_hamiltonian(
35-
d: int, J: float
36-
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]:
37-
N = np.diag(np.arange(d))
38-
Xsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._x_matrix_func(d)))
39-
Zsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._z_matrix_func(d)))
40-
H0 = N.copy()
41-
H1 = N.copy()
42-
return H0, H1, Xsym, Zsym, J
25+
import time
26+
import argparse
27+
import tensorcircuit as tc
4328

29+
tc.set_backend("jax")
30+
tc.set_dtype("complex128")
4431

45-
# ---------- Ansatz ----------
4632

33+
def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float):
34+
"""Build a QuditCircuit ansatz and compute ⟨H⟩.
4735
48-
def apply_single_qudit_layer(c: QuditCircuit, qudit: int, thetas: Sequence) -> None:
49-
"""
50-
Apply RY(j,j+1) then RZ(j) for each adjacent level pair.
51-
Number of params per site = 2*(d-1).
36+
Ansatz:
37+
[ for L in 1...nlayers ]
38+
- On each site q:
39+
RX(q; θ_Lq^(01)) ∘ RY(q; θ_Lq^(12)) ∘ RZ(q; φ_Lq^(0))
40+
(subspace indices shown as superscripts)
41+
- Entangle neighboring pairs with RXX on subspaces (0,1)
5242
"""
53-
d = c._d
54-
idx = 0
55-
for j, k in [(p, p + 1) for p in range(d - 1)]:
56-
c.ry(qudit, theta=thetas[idx], j=j, k=k)
57-
idx += 1
58-
c.rz(qudit, theta=thetas[idx], j=j)
59-
idx += 1
60-
61-
62-
def apply_entangler(c: QuditCircuit, theta) -> None:
63-
# generalized RXX on (0,1) level pair for both qudits
64-
c.rxx(0, 1, theta=theta, j1=0, k1=1, j2=0, k2=1)
65-
66-
67-
def build_ansatz(nlayers: int, d: int, params: Sequence) -> QuditCircuit:
68-
c = QuditCircuit(2, dim=d)
69-
per_site = 2 * (d - 1)
70-
per_layer = 2 * per_site + 1 # two sites + entangler
71-
assert (
72-
len(params) == nlayers * per_layer
73-
), f"params length {len(params)} != {nlayers * per_layer}"
74-
off = 0
75-
for _ in range(nlayers):
76-
th0 = params[off : off + per_site]
77-
off += per_site
78-
th1 = params[off : off + per_site]
79-
off += per_site
80-
thE = params[off]
81-
off += 1
82-
apply_single_qudit_layer(c, 0, th0)
83-
apply_single_qudit_layer(c, 1, th1)
84-
apply_entangler(c, thE)
85-
return c
86-
87-
88-
# ---------- Energy ----------
89-
90-
91-
def energy_expectation_backend(
92-
params_b,
93-
d: int,
94-
nlayers: int,
95-
ham: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float],
96-
):
97-
"""
98-
params_b: 1D backend tensor (jax/tf) of shape [nparams].
99-
Returns backend scalar.
100-
"""
101-
bk = tc.backend
102-
# Keep differentiability by passing backend scalars into gates
103-
plist = [params_b[i] for i in range(params_b.shape[0])]
104-
c = build_ansatz(nlayers, d, plist)
105-
E = 0.0 + 0.0j
106-
H0, H1, Xsym, Zsym, J = ham
107-
# Local number operators
108-
E = E + c.expectation((tc.gates.Gate(H0), [0]))
109-
E = E + c.expectation((tc.gates.Gate(H1), [1]))
110-
# Coupling terms as products on separate sites (avoids 9x9 reshaping issues)
111-
E = E + J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1]))
112-
E = E + J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1]))
113-
return bk.real(E)
114-
115-
116-
def energy_expectation_numpy(
117-
params_np: np.ndarray,
118-
d: int,
119-
nlayers: int,
120-
ham: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float],
121-
) -> float:
122-
c = build_ansatz(nlayers, d, params_np.tolist())
123-
E = 0.0 + 0.0j
124-
H0, H1, Xsym, Zsym, J = ham
125-
E += c.expectation((tc.gates.Gate(H0), [0]))
126-
E += c.expectation((tc.gates.Gate(H1), [1]))
127-
# Coupling terms as products on separate sites (avoids 9x9 reshaping issues)
128-
E += J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1]))
129-
E += J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1]))
130-
return float(np.real(E))
131-
132-
133-
# ---------- Optimizers ----------
134-
135-
136-
def random_search(fun_numpy, x0_shape, iters=300, seed=42):
137-
rng = np.random.default_rng(seed)
138-
best_x, best_y = None, float("inf")
139-
for _ in range(iters):
140-
x = rng.uniform(-math.pi, math.pi, size=x0_shape)
141-
y = fun_numpy(x)
142-
if y < best_y:
143-
best_x, best_y = x, y
144-
return best_x, float(best_y)
145-
146-
147-
def gradient_descent_ad(energy_bk, x0_np: np.ndarray, steps=200, lr=0.1, jit=False):
148-
"""
149-
energy_bk: (backend_tensor[nparams]) -> backend_scalar
150-
Simple gradient descent in numpy space with backend-gradients.
151-
"""
152-
bk = tc.backend
153-
if jit:
154-
energy_bk = bk.jit(energy_bk)
155-
grad_f = bk.grad(energy_bk)
43+
if d < 3:
44+
raise ValueError("This example assblumes d >= 3 (qutrit or higher).")
45+
46+
S = tc.quditgates._x_matrix_func(d)
47+
Z = tc.quditgates._z_matrix_func(d)
48+
Sdag = tc.backend.adjoint(S)
49+
Zdag = tc.backend.adjoint(Z)
50+
51+
c = tc.QuditCircuit(nqudits, dim=d)
15652

157-
x_np = x0_np.copy()
158-
best_x, best_y = x_np.copy(), float("inf")
53+
pairs = [(i, i + 1) for i in range(nqudits - 1)]
15954

160-
def to_np(x):
161-
return x if isinstance(x, np.ndarray) else bk.numpy(x)
55+
it = iter(param)
16256

163-
for _ in range(steps):
164-
x_b = bk.convert_to_tensor(x_np) # numpy -> backend tensor
165-
g_b = grad_f(x_b) # backend gradient
166-
g = to_np(g_b) # backend -> numpy
167-
x_np = x_np - lr * g # SGD step in numpy
168-
y = float(to_np(energy_bk(bk.convert_to_tensor(x_np))))
169-
if y < best_y:
170-
best_x, best_y = x_np.copy(), y
171-
return best_x, float(best_y)
57+
for _ in range(nlayers):
58+
for q in range(nqudits):
59+
c.rx(q, theta=next(it), j=0, k=1)
60+
c.ry(q, theta=next(it), j=1, k=2)
61+
c.rz(q, theta=next(it), j=0)
62+
63+
for i, j in pairs:
64+
c.rxx(i, j, theta=next(it), j1=0, k1=1, j2=0, k2=1)
65+
66+
# H = -J * 1/2 (S_i S_j^\dagger + S_i^\dagger S_j) - h * 1/2 (Z + Z^\dagger)
67+
energy = 0.0
68+
for i, j in pairs:
69+
e_ij = 0.5 * (
70+
c.expectation((S, [i]), (Sdag, [j])) + c.expectation((Sdag, [i]), (S, [j]))
71+
)
72+
energy += -J * tc.backend.real(e_ij)
73+
for q in range(nqudits):
74+
zq = 0.5 * (c.expectation((Z, [q])) + c.expectation((Zdag, [q])))
75+
energy += -h * tc.backend.real(zq)
76+
return tc.backend.real(energy)
17277

17378

174-
# ---------- CLI ----------
79+
def build_param_shape(nqudits: int, d: int, nlayers: int):
80+
# Per layer per qudit: RX^(01), RY^(12) (or dummy), RZ^(0) = 3 params
81+
# Per layer entanglers: len(pairs) parameters
82+
pairs = nqudits - 1
83+
per_layer = 3 * nqudits + pairs
84+
return (nlayers * per_layer,)
17585

17686

17787
def main():
178-
ap = argparse.ArgumentParser(description="Qudit VQE (explicit backend)")
179-
ap.add_argument(
180-
"--backend",
181-
required=True,
182-
choices=["jax", "tensorflow"],
183-
help="tensorcircuit backend",
88+
parser = argparse.ArgumentParser(
89+
description="VQE on QuditCircuit (clock–shift model)"
18490
)
185-
ap.add_argument("--dim", type=int, default=3, help="local qudit dimension d (>=3)")
186-
ap.add_argument("--layers", type=int, default=2, help="# ansatz layers")
187-
ap.add_argument("--J", type=float, default=0.5, help="coupling strength")
188-
ap.add_argument(
189-
"--optimizer",
190-
type=str,
191-
default="gd",
192-
choices=["gd", "random"],
193-
help="gradient descent (AD) or random search",
91+
parser.add_argument(
92+
"--d", type=int, default=3, help="Local dimension per site (>=3)"
19493
)
195-
ap.add_argument("--steps", type=int, default=200, help="GD steps")
196-
ap.add_argument("--lr", type=float, default=0.1, help="GD learning rate")
197-
ap.add_argument("--iters", type=int, default=300, help="random search steps")
198-
ap.add_argument("--seed", type=int, default=42, help="RNG seed")
199-
ap.add_argument(
200-
"--jit",
201-
action="store_true",
202-
help="enable backend JIT (all backends implement .jit; numpy backend no-ops)",
94+
parser.add_argument("--nqudits", type=int, default=2, help="Number of sites")
95+
parser.add_argument("--nlayers", type=int, default=3, help="Ansatz depth (layers)")
96+
parser.add_argument(
97+
"--J", type=float, default=1.0, help="Coupling strength for XcXc term"
20398
)
204-
args = ap.parse_args()
205-
206-
tc.set_backend(args.backend)
207-
208-
if args.dim < 3:
209-
print("Please use dim >= 3 for qudits.", file=sys.stderr)
210-
sys.exit(1)
211-
212-
d, L = args.dim, args.layers
213-
per_layer = 4 * (d - 1) + 1
214-
nparams = L * per_layer
215-
216-
ham = build_2site_hamiltonian(d, args.J)
217-
218-
print(
219-
f"[info] backend={args.backend}, d={d}, layers={L}, params={nparams}, J={args.J}"
99+
parser.add_argument(
100+
"--h", type=float, default=0.6, help="Field strength for Zc terms"
220101
)
221-
222-
if args.optimizer == "random":
223-
224-
def obj_np(theta_np):
225-
return energy_expectation_numpy(theta_np, d, L, ham)
226-
227-
x, y = random_search(
228-
obj_np, x0_shape=(nparams,), iters=args.iters, seed=args.seed
102+
parser.add_argument("--steps", type=int, default=200, help="Optimization steps")
103+
parser.add_argument("--lr", type=float, default=0.05, help="Learning rate")
104+
args = parser.parse_args()
105+
106+
assert args.d >= 3, "d must be >= 3"
107+
108+
shape = build_param_shape(args.nqudits, args.d, args.nlayers)
109+
param = tc.backend.random_uniform(shape, boundaries=(-0.1, 0.1), seed=42)
110+
111+
try:
112+
import optax
113+
114+
optimizer = optax.adam(args.lr)
115+
vgf = tc.backend.jit(
116+
tc.backend.value_and_grad(
117+
lambda p: vqe_forward(
118+
p,
119+
nqudits=args.nqudits,
120+
d=args.d,
121+
nlayers=args.nlayers,
122+
J=args.J,
123+
h=args.h,
124+
)
125+
)
229126
)
230-
else:
231-
232-
def obj_bk(theta_b):
233-
return energy_expectation_backend(theta_b, d, L, ham)
234-
235-
rng = np.random.default_rng(args.seed)
236-
x0 = rng.uniform(-math.pi, math.pi, size=(nparams,))
237-
x, y = gradient_descent_ad(
238-
obj_bk, x0_np=x0, steps=args.steps, lr=args.lr, jit=args.jit
127+
opt_state = optimizer.init(param)
128+
129+
@tc.backend.jit
130+
def train_step(p, opt_state):
131+
loss, grads = vgf(p)
132+
updates, opt_state = optimizer.update(grads, opt_state, p)
133+
p = optax.apply_updates(p, updates)
134+
return p, opt_state, loss
135+
136+
print("Starting VQE optimization (optax/adam)...")
137+
loss = None
138+
for i in range(args.steps):
139+
t0 = time.time()
140+
param, opt_state, loss = train_step(param, opt_state)
141+
# ensure sync for accurate timing
142+
_ = float(loss)
143+
if i % 20 == 0:
144+
dt = time.time() - t0
145+
print(f"Step {i:4d} loss={loss:.6f} dt/step={dt:.4f}s")
146+
print("Final loss:", float(loss) if loss is not None else "n/a")
147+
148+
except ModuleNotFoundError:
149+
print("Optax not available; using naive gradient descent.")
150+
value_and_grad = tc.backend.value_and_grad(
151+
lambda p: vqe_forward(
152+
p,
153+
nqudits=args.nqudits,
154+
d=args.d,
155+
nlayers=args.nlayers,
156+
J=args.J,
157+
h=args.h,
158+
)
239159
)
240-
241-
print("\n=== Result ===")
242-
print(f"Energy : {y:.6f}")
243-
print(f"Params shape: {x.shape}")
244-
np.set_printoptions(precision=4, suppress=True)
245-
print(x[: min(10, x.size)])
160+
lr = args.lr
161+
loss = None
162+
for i in range(args.steps):
163+
loss, grads = value_and_grad(param)
164+
param = param - lr * grads
165+
if i % 20 == 0:
166+
print(f"Step {i:4d} loss={float(loss):.6f}")
167+
print("Final loss:", float(loss) if loss is not None else "n/a")
246168

247169

248170
if __name__ == "__main__":

0 commit comments

Comments
 (0)