Skip to content

Commit 0a93cbe

Browse files
Merge branch 'qudit_circuit_gate' of github.com:WeiguoMa/tensorcircuit-ng into ngpr45
2 parents 5fc4aac + d4d3d8d commit 0a93cbe

File tree

7 files changed

+2171
-20
lines changed

7 files changed

+2171
-20
lines changed

examples/vqe_qudit_example.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
VQE on QuditCircuits.
3+
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:
8+
9+
H(d) = - J * (X_c \otimes X_c) - h * (Z_c \otimes I + I \otimes Z_c)
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)
15+
16+
The code defaults to a 2-qutrit (d=3) problem but can be changed via CLI flags.
17+
"""
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)
24+
25+
import time
26+
import argparse
27+
import tensorcircuit as tc
28+
29+
tc.set_backend("jax")
30+
tc.set_dtype("complex128")
31+
32+
33+
def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float):
34+
"""Build a QuditCircuit ansatz and compute ⟨H⟩.
35+
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)
42+
"""
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)
52+
53+
pairs = [(i, i + 1) for i in range(nqudits - 1)]
54+
55+
it = iter(param)
56+
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)
77+
78+
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,)
85+
86+
87+
def main():
88+
parser = argparse.ArgumentParser(
89+
description="VQE on QuditCircuit (clock–shift model)"
90+
)
91+
parser.add_argument(
92+
"--d", type=int, default=3, help="Local dimension per site (>=3)"
93+
)
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"
98+
)
99+
parser.add_argument(
100+
"--h", type=float, default=0.6, help="Field strength for Zc terms"
101+
)
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+
)
126+
)
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+
)
159+
)
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")
168+
169+
170+
if __name__ == "__main__":
171+
main()

tensorcircuit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
runtime_contractor,
2424
) # prerun of set hooks
2525
from . import gates
26+
from . import quditgates
2627
from . import basecircuit
2728
from .gates import Gate
29+
from .quditcircuit import QuditCircuit
2830
from .circuit import Circuit, expectation
2931
from .mpscircuit import MPSCircuit
3032
from .densitymatrix import DMCircuit as DMCircuit_reference

tensorcircuit/basecircuit.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -480,28 +480,12 @@ def measure_jit(
480480

481481
def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]:
482482
r"""
483-
Returns the tensornetwor nodes for the amplitude of the circuit given a computational-basis label ``l``.
484-
For a state simulator, it computes :math:`\langle l \vert \psi\rangle`;
485-
for a density-matrix simulator, it computes :math:`\mathrm{Tr}(\rho \vert l\rangle\langle l\vert)`.
483+
Returns the tensornetwor nodes for the amplitude of the circuit given the bitstring l.
484+
For state simulator, it computes :math:`\langle l\vert \psi\rangle`,
485+
for density matrix simulator, it computes :math:`Tr(\rho \vert l\rangle \langle 1\vert)`
486486
Note how these two are different up to a square operation.
487487
488-
:Example:
489-
490-
>>> c = tc.Circuit(2)
491-
>>> c.X(0)
492-
>>> c.amplitude("10") # d=2, per-qubit digits
493-
array(1.+0.j, dtype=complex64)
494-
>>> c.CNOT(0, 1)
495-
>>> c.amplitude("11")
496-
array(1.+0.j, dtype=complex64)
497-
498-
For qudits (d>2, d<=36):
499-
>>> c = tc.Circuit(3, dim=12)
500-
>>> c.amplitude("0A2") # base-12 string, A stands for 10
501-
502-
:param l: Basis label.
503-
- If a string: it must be a base-d string of length ``nqubits``, using 0–9A–Z (A=10,…,Z=35) when ``d<=36``.
504-
- If a tensor/array/list: it should contain per-site integers in ``[0, d-1]`` with length ``nqubits``.
488+
:param l: The bitstring of 0 and 1s.
505489
:type l: Union[str, Tensor]
506490
:return: The tensornetwork nodes for the amplitude of the circuit.
507491
:rtype: List[Gate]

0 commit comments

Comments
 (0)