|
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. |
17 | 1 | """ |
| 2 | +VQE on QuditCircuits. |
18 | 3 |
|
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: |
23 | 8 |
|
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) |
27 | 10 |
|
| 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) |
28 | 15 |
|
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 | +""" |
32 | 18 |
|
| 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) |
33 | 24 |
|
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 |
43 | 28 |
|
| 29 | +tc.set_backend("jax") |
| 30 | +tc.set_dtype("complex128") |
44 | 31 |
|
45 | | -# ---------- Ansatz ---------- |
46 | 32 |
|
| 33 | +def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float): |
| 34 | + """Build a QuditCircuit ansatz and compute ⟨H⟩. |
47 | 35 |
|
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) |
52 | 42 | """ |
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) |
156 | 52 |
|
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)] |
159 | 54 |
|
160 | | - def to_np(x): |
161 | | - return x if isinstance(x, np.ndarray) else bk.numpy(x) |
| 55 | + it = iter(param) |
162 | 56 |
|
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) |
172 | 77 |
|
173 | 78 |
|
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,) |
175 | 85 |
|
176 | 86 |
|
177 | 87 | 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)" |
184 | 90 | ) |
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)" |
194 | 93 | ) |
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" |
203 | 98 | ) |
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" |
220 | 101 | ) |
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 | + ) |
229 | 126 | ) |
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 | + ) |
239 | 159 | ) |
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") |
246 | 168 |
|
247 | 169 |
|
248 | 170 | if __name__ == "__main__": |
|
0 commit comments