Skip to content
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
2037c83
Add quditgates.py and quditcircuit.py.
WeiguoMa Aug 28, 2025
3c062ab
add quditgates to __init__.py
WeiguoMa Aug 28, 2025
30c7025
Add functions from tc.Circuit
WeiguoMa Aug 29, 2025
da94086
Add tests for QuditCircuit
WeiguoMa Aug 29, 2025
6755d9f
Reported a potential bug.
WeiguoMa Aug 29, 2025
2b600b8
Add doc.
WeiguoMa Aug 29, 2025
4b96665
black
WeiguoMa Aug 29, 2025
dbbb685
Add QuditCircuit interface to __init__.py
WeiguoMa Aug 31, 2025
12f66c8
remove a useless pack
WeiguoMa Sep 1, 2025
4eb9dc2
remove useless pack.
WeiguoMa Sep 1, 2025
bde4f24
fixed a possible bug
WeiguoMa Sep 1, 2025
0212135
formatted codes.
WeiguoMa Sep 1, 2025
c910791
remove redundant code.
WeiguoMa Sep 1, 2025
255824a
add test_large_scale_sample
WeiguoMa Sep 2, 2025
edb97b1
Adjust the code framework
WeiguoMa Sep 3, 2025
b0b30d5
new docstring
WeiguoMa Sep 3, 2025
7585731
Add tests for quditgates
WeiguoMa Sep 3, 2025
0e6c053
formatted codes
WeiguoMa Sep 3, 2025
96a55b2
formatted codes
WeiguoMa Sep 3, 2025
9dbabaa
black
WeiguoMa Sep 3, 2025
35288d2
reform doc
WeiguoMa Sep 3, 2025
a3f2131
use np.eye
WeiguoMa Sep 3, 2025
5278f50
use np.diag
WeiguoMa Sep 3, 2025
62750f1
fixed an early notation
WeiguoMa Sep 3, 2025
3a265cc
use np.diag
WeiguoMa Sep 3, 2025
d88b9a9
remove useless line
WeiguoMa Sep 3, 2025
26dd999
change atol from 1e-4 to 1e-5
WeiguoMa Sep 3, 2025
60ce1e7
bug fixed
WeiguoMa Sep 3, 2025
8793b21
use highp
WeiguoMa Sep 3, 2025
2135ecf
remove `count_vector` from sample
WeiguoMa Sep 3, 2025
3e29f11
remove sympy
WeiguoMa Sep 3, 2025
8c86e1c
format codes according to pylint
WeiguoMa Sep 3, 2025
83378fb
change u8 __doc__
WeiguoMa Sep 3, 2025
d379cff
remove 'count_vector'
WeiguoMa Sep 3, 2025
995a3c8
remove y gate
WeiguoMa Sep 3, 2025
10cc46c
remove a y-related test info
WeiguoMa Sep 3, 2025
a64a3b9
remove y
WeiguoMa Sep 3, 2025
b6a8c10
rzz_gate from global to local (for consistence)
WeiguoMa Sep 4, 2025
92d98a3
np -> backend to ensure the auto-differential
WeiguoMa Sep 5, 2025
ee8b617
fixed rzz_gate
WeiguoMa Sep 5, 2025
9cceefa
fixed a docsing bug
WeiguoMa Sep 5, 2025
56902a5
Add expectation_before(), amplitude_before() functions
WeiguoMa Sep 5, 2025
d396ba2
add Gate
WeiguoMa Sep 5, 2025
8b224f0
Provide a corresponding uppercase entry for the doors.
WeiguoMa Sep 5, 2025
0944fb5
np -> backend, and this should solve auto-differential.
WeiguoMa Sep 5, 2025
3dd499b
Fixed a bug that could lead to backend pollution.
WeiguoMa Sep 5, 2025
5c3a082
add test_quditvqe.py for testing vqe, jit and vmap in QUDIT Interface.
WeiguoMa Sep 5, 2025
cdb1ee6
add tests for codecov test.
WeiguoMa Sep 5, 2025
90646ee
black .
WeiguoMa Sep 5, 2025
0e253e1
extend check_rotation()
WeiguoMa Sep 5, 2025
5a9a4ea
remove qudit gates lru_cache
WeiguoMa Sep 5, 2025
507c3ab
optimized codes
WeiguoMa Sep 5, 2025
ee11fd2
remove un-used import.
WeiguoMa Sep 5, 2025
9849ee0
fixed a bug of qudit swap
WeiguoMa Sep 5, 2025
a5246ae
add backend as para for tests/
WeiguoMa Sep 5, 2025
7a8759e
add ref to u8 gate and fixed logical bug of this
WeiguoMa Sep 5, 2025
80b04af
move jit, autograd, vmap tests to test_quditcircuit.py
WeiguoMa Sep 5, 2025
8ff6981
Add an example for qudit vqe.
WeiguoMa Sep 5, 2025
768b003
format
WeiguoMa Sep 5, 2025
68bb073
remove quditvqe test.
WeiguoMa Sep 6, 2025
8aa2695
change function name
WeiguoMa Sep 6, 2025
53669a2
add test for len(nodes) in amplitude_before_test
WeiguoMa Sep 6, 2025
52f9804
optimized examples/vqe_qudit_example.py according to comments.
WeiguoMa Sep 6, 2025
d4d3d8d
refresh the whole file, making no complicated conversion
WeiguoMa Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions examples/vqe_qudit_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
r"""
You must set the backend explicitly via --backend {jax, tensorflow}.
AD-based optimization (gradient descent) is enabled for these backends.
A fallback random-search optimizer is also provided.
Example runs:
python vqe_qudit_example.py --backend jax --optimizer gd --dim 3 --layers 2 --steps 200 --lr 0.1 --jit
python vqe_qudit_example.py --backend tensorflow --optimizer gd --dim 3 --layers 2 --steps 200 --lr 0.1
python vqe_qudit_example.py --backend jax --optimizer random --dim 3 --layers 2 --iters 300
What this script does:
- Builds a 2-qudit (d>=3) ansatz with native RY/RZ single-qudit rotations on adjacent levels
and an RXX entangler on (0,1) level pairs.
- Minimizes the expectation of a simple 2-site Hermitian Hamiltonian:
H = N(0) + N(1) + J * [ X_sym(0)\otimes X_sym(1) + Z_sym(0)\otimes Z_sym(1) ]
where N = diag(0,1,...,d-1), X_sym = (X + X^\dagger)/2, Z_sym = (Z + Z^\dagger)/2.
"""

import argparse
import math
import sys
from dataclasses import dataclass
from typing import List, Sequence, Tuple

import numpy as np
import tensorcircuit as tc
from tensorcircuit.quditcircuit import QuditCircuit


# ---------- Hamiltonian helpers ----------


def number_op(d: int) -> np.ndarray:
return np.diag(np.arange(d, dtype=np.float32)).astype(np.complex64)


def x_unitary(d: int) -> np.ndarray:
X = np.zeros((d, d), dtype=np.complex64)
for j in range(d):
X[(j + 1) % d, j] = 1.0
return X


def z_unitary(d: int) -> np.ndarray:
omega = np.exp(2j * np.pi / d)
diag = np.array([omega**j for j in range(d)], dtype=np.complex64)
return np.diag(diag)


def symmetrize_hermitian(U: np.ndarray) -> np.ndarray:
return 0.5 * (U + U.conj().T)


def kron2(a: np.ndarray, b: np.ndarray) -> np.ndarray:
return np.kron(a, b).astype(np.complex64)


@dataclass
class Hamiltonian2Qudit:
H_local_0: np.ndarray
H_local_1: np.ndarray
H_couple: np.ndarray

def as_terms(self) -> List[Tuple[np.ndarray, Sequence[int]]]:
return [
(self.H_local_0, [0]),
(self.H_local_1, [1]),
(self.H_couple, [0, 1]),
]


def build_2site_hamiltonian(d: int, J: float) -> Hamiltonian2Qudit:
N = number_op(d)
Xsym = symmetrize_hermitian(x_unitary(d))
Zsym = symmetrize_hermitian(z_unitary(d))
H0 = N.copy()
H1 = N.copy()
HXX = kron2(Xsym, Xsym)
HZZ = kron2(Zsym, Zsym)
H01 = J * (HXX + HZZ)
return Hamiltonian2Qudit(H0, H1, H01)


# ---------- Ansatz ----------


def apply_single_qudit_layer(c: QuditCircuit, qudit: int, thetas: Sequence) -> None:
"""
Apply RY(j,j+1) then RZ(j) for each adjacent level pair.
Number of params per site = 2*(d-1).
"""
d = c._d
idx = 0
for j, k in [(p, p + 1) for p in range(d - 1)]:
c.ry(qudit, theta=thetas[idx], j=j, k=k)
idx += 1
c.rz(qudit, theta=thetas[idx], j=j)
idx += 1


def apply_entangler(c: QuditCircuit, theta) -> None:
# generalized RXX on (0,1) level pair for both qudits
c.rxx(0, 1, theta=theta, j1=0, k1=1, j2=0, k2=1)


def build_ansatz(nlayers: int, d: int, params: Sequence) -> QuditCircuit:
c = QuditCircuit(2, dim=d)
per_site = 2 * (d - 1)
per_layer = 2 * per_site + 1 # two sites + entangler
assert (
len(params) == nlayers * per_layer
), f"params length {len(params)} != {nlayers * per_layer}"
off = 0
for _ in range(nlayers):
th0 = params[off : off + per_site]
off += per_site
th1 = params[off : off + per_site]
off += per_site
thE = params[off]
off += 1
apply_single_qudit_layer(c, 0, th0)
apply_single_qudit_layer(c, 1, th1)
apply_entangler(c, thE)
return c


# ---------- Energy ----------


def energy_expectation_backend(params_b, d: int, nlayers: int, ham: Hamiltonian2Qudit):
"""
params_b: 1D backend tensor (jax/tf) of shape [nparams].
Returns backend scalar.
"""
bk = tc.backend
# Keep differentiability by passing backend scalars into gates
plist = [params_b[i] for i in range(params_b.shape[0])]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not directly use params_b?

c = build_ansatz(nlayers, d, plist)
E = 0.0 + 0.0j
for op, sites in ham.as_terms():
E = E + c.expectation((tc.gates.Gate(op), list(sites)))
return bk.real(E)


def energy_expectation_numpy(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function can be exactly the same with the backend version?

params_np: np.ndarray, d: int, nlayers: int, ham: Hamiltonian2Qudit
) -> float:
c = build_ansatz(nlayers, d, params_np.tolist())
E = 0.0 + 0.0j
for op, sites in ham.as_terms():
E += c.expectation((tc.gates.Gate(op), list(sites)))
return float(np.real(E))


# ---------- Optimizers ----------


def random_search(fun_numpy, x0_shape, iters=300, seed=42):
rng = np.random.default_rng(seed)
best_x, best_y = None, float("inf")
for _ in range(iters):
x = rng.uniform(-math.pi, math.pi, size=x0_shape).astype(np.float32)
y = fun_numpy(x)
if y < best_y:
best_x, best_y = x, y
return best_x, float(best_y)


def gradient_descent_ad(energy_bk, x0_np: np.ndarray, steps=200, lr=0.1, jit=False):
"""
energy_bk: (backend_tensor[nparams]) -> backend_scalar
Simple gradient descent in numpy space with backend-gradients.
"""
bk = tc.backend
if jit and hasattr(bk, "jit"):
energy_bk = bk.jit(energy_bk)
grad_f = bk.grad(energy_bk)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interchange the order of jit and grad, should be jit(grad(f))


x_np = x0_np.astype(np.float32).copy()
best_x, best_y = x_np.copy(), float("inf")

def to_np(x):
return x if isinstance(x, np.ndarray) else bk.numpy(x)

for _ in range(steps):
x_b = bk.convert_to_tensor(x_np) # numpy -> backend tensor
g_b = grad_f(x_b) # backend gradient
g = to_np(g_b) # backend -> numpy
x_np = x_np - lr * g # SGD step in numpy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why sgd in numpy? just keep to the corresponding backend and avoid those conversions from and to numpy

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the logic is complicated with random search, you can remove the random search and numpy backend

y = float(to_np(energy_bk(bk.convert_to_tensor(x_np))))
if y < best_y:
best_x, best_y = x_np.copy(), y
return best_x, float(best_y)


# ---------- CLI ----------


def main():
ap = argparse.ArgumentParser(description="Qudit VQE (explicit backend)")
ap.add_argument(
"--backend",
required=True,
choices=["jax", "tensorflow"],
help="tensorcircuit backend",
)
ap.add_argument("--dim", type=int, default=3, help="local qudit dimension d (>=3)")
ap.add_argument("--layers", type=int, default=2, help="# ansatz layers")
ap.add_argument("--J", type=float, default=0.5, help="coupling strength")
ap.add_argument(
"--optimizer",
type=str,
default="gd",
choices=["gd", "random"],
help="gradient descent (AD) or random search",
)
ap.add_argument("--steps", type=int, default=200, help="GD steps")
ap.add_argument("--lr", type=float, default=0.1, help="GD learning rate")
ap.add_argument("--iters", type=int, default=300, help="random search steps")
ap.add_argument("--seed", type=int, default=42, help="RNG seed")
ap.add_argument(
"--jit",
action="store_true",
help="enable backend JIT for energy/grad if available",
)
args = ap.parse_args()

tc.set_backend(args.backend)

if args.dim < 3:
print("Please use dim >= 3 for qudits.", file=sys.stderr)
sys.exit(1)

d, L = args.dim, args.layers
per_layer = 4 * (d - 1) + 1
nparams = L * per_layer

ham = build_2site_hamiltonian(d, args.J)

print(
f"[info] backend={args.backend}, d={d}, layers={L}, params={nparams}, J={args.J}"
)

if args.optimizer == "random":

def obj_np(theta_np):
return energy_expectation_numpy(theta_np, d, L, ham)

x, y = random_search(
obj_np, x0_shape=(nparams,), iters=args.iters, seed=args.seed
)
else:

def obj_bk(theta_b):
return energy_expectation_backend(theta_b, d, L, ham)

rng = np.random.default_rng(args.seed)
x0 = rng.uniform(-math.pi, math.pi, size=(nparams,)).astype(np.float32)
x, y = gradient_descent_ad(
obj_bk, x0_np=x0, steps=args.steps, lr=args.lr, jit=args.jit
)

print("\n=== Result ===")
print(f"Energy : {y:.6f}")
print(f"Params shape: {x.shape}")
np.set_printoptions(precision=4, suppress=True)
print(x[: min(10, x.size)])


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions tensorcircuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
runtime_contractor,
) # prerun of set hooks
from . import gates
from . import quditgates
from . import basecircuit
from .gates import Gate
from .quditcircuit import QuditCircuit
from .circuit import Circuit, expectation
from .mpscircuit import MPSCircuit
from .densitymatrix import DMCircuit as DMCircuit_reference
Expand Down
24 changes: 4 additions & 20 deletions tensorcircuit/basecircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,28 +480,12 @@ def measure_jit(

def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]:
r"""
Returns the tensornetwor nodes for the amplitude of the circuit given a computational-basis label ``l``.
For a state simulator, it computes :math:`\langle l \vert \psi\rangle`;
for a density-matrix simulator, it computes :math:`\mathrm{Tr}(\rho \vert l\rangle\langle l\vert)`.
Returns the tensornetwor nodes for the amplitude of the circuit given the bitstring l.
For state simulator, it computes :math:`\langle l\vert \psi\rangle`,
for density matrix simulator, it computes :math:`Tr(\rho \vert l\rangle \langle 1\vert)`
Note how these two are different up to a square operation.

:Example:

>>> c = tc.Circuit(2)
>>> c.X(0)
>>> c.amplitude("10") # d=2, per-qubit digits
array(1.+0.j, dtype=complex64)
>>> c.CNOT(0, 1)
>>> c.amplitude("11")
array(1.+0.j, dtype=complex64)

For qudits (d>2, d<=36):
>>> c = tc.Circuit(3, dim=12)
>>> c.amplitude("0A2") # base-12 string, A stands for 10

:param l: Basis label.
- If a string: it must be a base-d string of length ``nqubits``, using 0–9A–Z (A=10,…,Z=35) when ``d<=36``.
- If a tensor/array/list: it should contain per-site integers in ``[0, d-1]`` with length ``nqubits``.
:param l: The bitstring of 0 and 1s.
:type l: Union[str, Tensor]
:return: The tensornetwork nodes for the amplitude of the circuit.
:rtype: List[Gate]
Expand Down
Loading