From 2037c83bf4ba32943a461ca1326d67434ab22cda Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Thu, 28 Aug 2025 10:11:57 +0800 Subject: [PATCH 01/64] Add quditgates.py and quditcircuit.py. --- tensorcircuit/quditcircuit.py | 299 ++++++++++++++++++++++++ tensorcircuit/quditgates.py | 419 ++++++++++++++++++++++++++++++++++ 2 files changed, 718 insertions(+) create mode 100644 tensorcircuit/quditcircuit.py create mode 100644 tensorcircuit/quditgates.py diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py new file mode 100644 index 00000000..a093c5b3 --- /dev/null +++ b/tensorcircuit/quditcircuit.py @@ -0,0 +1,299 @@ +from functools import lru_cache, reduce, partial +from typing import Any, Dict, List, Optional, Tuple, Sequence, Union + +import numpy as np +import tensornetwork as tn + +from .utils import arg_alias +from .basecircuit import BaseCircuit +from .circuit import Circuit +from .quantum import QuOperator +from .quditgates import ( + _i_matrix_func, + _x_matrix_func, + _y_matrix_func, + _z_matrix_func, + _h_matrix_func, + _u8_matrix_func, + _cphase_matrix_func, + _csum_matrix_func, + _rx_matrix_func, + _ry_matrix_func, + _rz_matrix_func, + _rxx_matrix_func, + _rzz_matrix_func, +) + + +Tensor = Any + + +SINGLE_BUILDERS = { + "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), + "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), + "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), + "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), + "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), + "RX": ( + ("theta", "j", "k"), + lambda d, omega, **kw: _rx_matrix_func(d, kw["theta"], kw["j"], kw["k"]), + ), + "RY": ( + ("theta", "j", "k"), + lambda d, omega, **kw: _ry_matrix_func(d, kw["theta"], kw["j"], kw["k"]), + ), + "RZ": ( + ("theta", "j"), + lambda d, omega, **kw: _rz_matrix_func(d, kw["theta"], kw["j"]), + ), + "U8": ( + ("gamma", "z", "eps"), + lambda d, omega, **kw: _u8_matrix_func( + d, kw["gamma"], kw["z"], kw["eps"], omega + ), + ), +} + +TWO_BUILDERS = { + "RXX": ( + ("theta", "j1", "k1", "j2", "k2"), + lambda d, omega, **kw: _rxx_matrix_func( + d, kw["theta"], kw["j1"], kw["k1"], kw["j2"], kw["k2"] + ), + ), + "RZZ": (("theta",), lambda d, omega, **kw: _rzz_matrix_func(d, kw["theta"])), + "CPHASE": (("cv",), lambda d, omega, **kw: _cphase_matrix_func(d, kw["cv"], omega)), + "CSUM": (("cv",), lambda d, omega, **kw: _csum_matrix_func(d, kw["cv"])), +} + + +@lru_cache(maxsize=None) +def _cached_matrix( + kind: str, + name: str, + d: int, + omega: Optional[float] = None, + key: Optional[tuple] = (), +) -> Tensor: + """ + kind: "single" or "two"; key is a parameter tuple sorted by signature for caching + """ + builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS + _, builder = builders[name] + sig = builders[name][0] + kwargs = {k: v for k, v in zip(sig, key)} + return builder(d, omega, **kwargs) + + +class QuditCircuit: + r""" + ``QuditCircuit`` class. + + Qudit quick example (d=3): + .. code-block:: python + + c = tc.Circuit(2, d=3) + c.h(0) + c.x(1) + c.csum(0, 1) + c.sample(1024, format="count_dict_bin") + # For d <= 36, string samples use base-d characters 0–9A–Z (A=10, ...). + """ + + is_dm = False + + def __init__( + self, + nqubits: int, + dim: int, + inputs: Optional[Tensor] = None, + mps_inputs: Optional[QuOperator] = None, + split: Optional[Dict[str, Any]] = None, + ): + self._set_dim(dim=dim) + self._nqubits = nqubits + + self._circ = Circuit( + nqubits=nqubits, + dim=dim, + inputs=inputs, + mps_inputs=mps_inputs, + split=split, + qudit=True, + ) + self._omega = np.exp(2j * np.pi / self._d) + self.circuit_param = self._circ.circuit_param + + def _set_dim(self, dim: int) -> None: + if not isinstance(dim, int) or dim <= 2: + raise ValueError( + f"QuditCircuit is only for qudits (dim>=3). " + f"You passed dim={dim}. For qubits, please use `Circuit` instead." + ) + # Require integer d>=2; current string-encoded IO supports d<=36 (0–9A–Z digits). + if dim > 36: + raise NotImplementedError( + "The Qudit interface is only supported for dimension < 36 now." + ) + self._d = dim + + @property + def dim(self) -> int: + return self._d + + @property + def nqubits(self) -> int: + return self._nqubits + + def _apply_gate(self, *indices, name: str, **kwargs): + if len(indices) == 1 and name in SINGLE_BUILDERS: + sig, _ = SINGLE_BUILDERS[name] + key = tuple(kwargs.get(k) for k in sig if k != "none") + mat = _cached_matrix( + kind="single", name=name, d=self._d, omega=self._omega, key=key + ) + self._circ.unitary(indices[0], unitary=mat, name=name, dim=self._d) # type: ignore + elif len(indices) == 2 and name in TWO_BUILDERS: + sig, _ = TWO_BUILDERS[name] + key = tuple(kwargs.get(k) for k in sig if k != "none") + mat = _cached_matrix( + kind="two", name=name, d=self._d, omega=self._omega, key=key + ) + self._circ.unitary( # type: ignore + indices[0], indices[1], unitary=mat, name=name, dim=self._d + ) + else: + raise ValueError(f"Unsupported gate/arity: {name} on {len(indices)} qudits") + + def i(self, index: int) -> None: + self._apply_gate(index, name="I") + + def x(self, index: int) -> None: + self._apply_gate(index, name="X") + + def y(self, index: int) -> None: + self._apply_gate(index, name="Y") + + def z(self, index: int) -> None: + self._apply_gate(index, name="Z") + + def h(self, index: int) -> None: + self._apply_gate(index, name="H") + + def u8( + self, index: int, gamma: float = 2.0, z: float = 1.0, eps: float = 0.0 + ) -> None: + self._apply_gate(index, name="U8", extra=(gamma, z, eps)) + + def rx(self, index: int, theta: float, j: int = 0, k: int = 1): + self._apply_gate(index, name="RX", theta=theta, j=j, k=k) + + def ry(self, index: int, theta: float, j: int = 0, k: int = 1): + self._apply_gate(index, name="RY", theta=theta, j=j, k=k) + + def rz(self, index: int, theta: float, j: int = 0): + self._apply_gate(index, name="RZ", theta=theta, j=j) + + def rxx( + self, + *indices: int, + theta: float, + j1: int = 0, + k1: int = 1, + j2: int = 0, + k2: int = 1, + ): + self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) + + def rzz(self, *indices: int, theta: float): + self._apply_gate(*indices, name="RZZ", theta=theta) + + def cphase(self, *indices: int, cv: Optional[int] = None) -> None: + self._apply_gate(*indices, name="CPHASE", cv=cv) + + def csum(self, *indices: int, cv: Optional[int] = None) -> None: + self._apply_gate(*indices, name="CSUM", cv=cv) + + # Functional + def wavefunction(self, form: str = "default") -> tn.Node.tensor: + return self._circ.wavefunction(form) + + state = wavefunction + + def get_quoperator(self) -> QuOperator: + return self._circ.quoperator() + + quoperator = get_quoperator + + get_circuit_as_quoperator = get_quoperator + get_state_as_quvector = BaseCircuit.quvector + + def matrix(self) -> Tensor: + return self._circ.matrix() + + def measure_reference( + self, *index: int, with_prob: bool = False + ) -> Tuple[str, float]: + return self._circ.measure_reference(*index, with_prob=with_prob) + + def expectation( + self, + *ops: Tuple[tn.Node, List[int]], + reuse: bool = True, + enable_lightcone: bool = False, + nmc: int = 1000, + status: Optional[Tensor] = None, + **kws: Any, + ) -> Tensor: + return self._circ.expectation( + *ops, + reuse=reuse, + enable_lightcone=enable_lightcone, + noise_conf=None, + nmc=nmc, + status=status, + **kws, + ) + + def measure_jit( + self, *index: int, with_prob: bool = False, status: Optional[Tensor] = None + ) -> Tuple[Tensor, Tensor]: + return self._circ.measure_jit(*index, with_prob=with_prob, status=status) + + measure = measure_jit + + def amplitude(self, l: Union[str, Tensor]) -> Tensor: + return self._circ.amplitude(l) + + def probability(self) -> Tensor: + return self._circ.probability() + + @partial(arg_alias, alias_dict={"format": ["format_"]}) + def sample( + self, + batch: Optional[int] = None, + allow_state: bool = False, + readout_error: Optional[Sequence[Any]] = None, + format: Optional[str] = None, + random_generator: Optional[Any] = None, + status: Optional[Tensor] = None, + jittable: bool = True, + ) -> Any: + return self._circ.sample( + batch=batch, + allow_state=allow_state, + readout_error=readout_error, + format=format, + random_generator=random_generator, + status=status, + jittable=jittable, + ) + + def projected_subsystem(self, traceout: Tensor, left: Tuple[int, ...]) -> Tensor: + return self._circ.projected_subsystem( + traceout=traceout, + left=left, + ) + + def replace_inputs(self, inputs: Tensor) -> None: + return self._circ.replace_inputs(inputs) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py new file mode 100644 index 00000000..76688bcd --- /dev/null +++ b/tensorcircuit/quditgates.py @@ -0,0 +1,419 @@ +from typing import Any, Optional + +import numpy as np +from sympy import mod_inverse, Mod + +from .cons import npdtype + +Tensor = Any + + +def _is_prime(n: int) -> bool: + if n < 2: + return False + if n in (2, 3, 5, 7): + return True + if n % 2 == 0 or n % 3 == 0: + return False + + r = int(n**0.5) + 1 + for i in range(5, r, 6): + if n % i == 0 or n % (i + 2) == 0: + return False + return True + + +def _i_matrix_func(d: int) -> Tensor: + matrix = np.zeros((d, d), dtype=npdtype) + for i in range(d): + matrix[i, i] = 1.0 + return matrix + + +def _x_matrix_func(d: int) -> Tensor: + r""" + X_d\ket{j} = \ket{(j + 1) mod d} + """ + matrix = np.zeros((d, d), dtype=npdtype) + for j in range(d): + matrix[(j + 1) % d, j] = 1.0 + return matrix + + +def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: + r""" + Z_d\ket{j} = \omega^{j}\ket{j} + """ + omega = np.exp(2j * np.pi / d) if omega is None else omega + matrix = np.zeros((d, d), dtype=npdtype) + for j in range(d): + matrix[j, j] = omega**j + return matrix + + +def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: + r""" + Generalized Pauli-Y (Y) Gate for qudits. + + The Y gate represents a combination of the X and Z gates, generalizing the Pauli-Y gate + from qubits to higher dimensions. It is defined as + + .. math:: + + Y = \frac{1}{i}\, Z \cdot X, + + where the generalized Pauli-X and Pauli-Z gates are applied to the target qudits. + """ + return np.matmul(_z_matrix_func(d, omega=omega), _x_matrix_func(d)) / 1j + + +def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: + r""" + H_d\ket{j} = \frac{1}{\sqrt{d}}\sum_{k=0}^{d-1}\omega^{jk}\ket{k} + """ + omega = np.exp(2j * np.pi / d) if omega is None else omega + matrix = np.zeros((d, d), dtype=npdtype) + for j in range(d): + for k in range(d): + matrix[j, k] = omega ** (j * k) / np.sqrt(d) + return matrix.T + + +def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: + r""" + S_d\ket{j} = \omega^{j(j + p_d) / 2}\ket{j} + """ + omega = np.exp(2j * np.pi / d) if omega is None else omega + _pd = 0 if d % 2 == 0 else 1 + matrix = np.zeros((d, d), dtype=complex) + for j in range(d): + phase_exp = (j * (j + _pd)) / 2 + matrix[j, j] = omega**phase_exp + return matrix + + +def _check_rotation(d: int, j: int, k: int) -> None: + if not (0 <= j < d) or not (0 <= k < d): + raise ValueError(f"Indices j={j}, k={k} must satisfy 0 <= j,k < d (d={d}).") + if j == k: + raise ValueError("RX rotation requires two distinct levels j != k.") + + +def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: + r""" + Rotation-X (RX) Gate for qudits. + + The RX gate represents a rotation about the X-axis of the Bloch sphere in a qudit system. + For a qubit (2-level system), the matrix representation is given by + + .. math:: + + RX(\theta) = + \begin{pmatrix} + \cos(\theta/2) & -i\sin(\theta/2) \\ + -i\sin(\theta/2) & \cos(\theta/2) + \end{pmatrix} + + For higher-dimensional qudits, the RX gate affects only the specified two levels (indexed by + \(j\) and \(k\)), leaving all other levels unchanged. + + Args: + d (int): Dimension of the qudit Hilbert space. + theta (float): Rotation angle θ. + j (int): First level index (default 0). + k (int): Second level index (default 1). + + Returns: + Tensor: A (d x d) numpy array of dtype `npdtype` representing the RX gate. + """ + _check_rotation(d, j, k) + matrix = np.eye(d, dtype=npdtype) + c, s = np.cos(theta / 2.0), np.sin(theta / 2.0) + matrix[j, j] = c + matrix[k, k] = c + matrix[j, k] = -1j * s + matrix[k, j] = -1j * s + return matrix + + +def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: + r""" + Rotation-Y (RY) Gate for qudits. + + Acts as a standard qubit RY(θ) on the two-level subspace spanned by |j> and |k>, + and as identity on all other levels: + + .. math:: + + RY(\theta) = + \begin{pmatrix} + \cos(\theta/2) & -\sin(\theta/2) \\ + \sin(\theta/2) & \cos(\theta/2) + \end{pmatrix} + + Args: + d (int): Dimension of the qudit Hilbert space. + theta (float): Rotation angle θ. + j (int): First level index (default 0). + k (int): Second level index (default 1). + + Returns: + Tensor: A (d x d) numpy array of dtype `npdtype` representing the RY gate. + """ + _check_rotation(d, j, k) + matrix = np.eye(d, dtype=npdtype) + c, s = np.cos(theta / 2.0), np.sin(theta / 2.0) + matrix[j, j] = c + matrix[k, k] = c + matrix[j, k] = -s + matrix[k, j] = s + return matrix + + +def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: + r""" + Rotation-Z (RZ) Gate for qudits. + + .. math:: + + RZ(\theta) = + \begin{pmatrix} + e^{-i\theta/2} & 0 \\ + 0 & e^{i\theta/2} + \end{pmatrix} + + For qudits (d >= 2), apply a phase e^{iθ} only to level |j>, leaving others unchanged: + (RZ_d)_{mm} = e^{iθ} if m == j else 1 + + Args: + d (int): Dimension of the qudit Hilbert space. + theta (float): Rotation angle θ. + j (int): First level index (default 0). + k (int): Second level index (default 1). + + Returns: + Tensor: A (d x d) numpy array of dtype `npdtype` representing the RZ gate. + """ + matrix = np.eye(d, dtype=npdtype) + matrix[j, j] = np.exp(1j * theta) + return matrix + + +def _swap_matrix_func(d: int) -> Tensor: + r""" + SWAP gate for two qudits of dimensions d. + + Exchanges the states |i⟩|j⟩ -> |j⟩|i⟩. + + Args: + d (int): Dimension of the qudit. + + Returns: + Tensor: A numpy array representing the SWAP gate. + """ + D = d * d + matrix = np.zeros((D, D), dtype=npdtype) + for i in range(d): + for j in range(d): + idx_in = i * d + j + idx_out = j * d + i + matrix[idx_out, idx_in] = 1.0 + return matrix + + +def _rzz_matrix_func(d: int, theta: float) -> Tensor: + r""" + Two-qudit RZZ(\theta) gate for qudits. + + .. math:: + + Z_H = \mathrm{diag}(d-1,\, d-3,\, \ldots,\,-(d-1)) + .. math:: + RZZ(\theta) = \exp\!\left(-i \tfrac{\theta}{2} \, \bigl(Z_H \otimes Z_H\bigr)\right) + + For :math:`d=2`, this reduces to the standard qubit RZZ gate. + + Args: + d (int): Dimension of the qudits (assumed equal for both). + theta (float): Rotation angle. + + Returns: + Tensor: A ``(d*d, d*d)`` numpy array representing the RZZ gate. + """ + lam = np.array( + [d - 1 - 2 * j for j in range(d)], dtype=float + ) # [d-1, d-3, ..., -(d-1)] + D = d * d + diag = np.empty(D, dtype=npdtype) + idx = 0 + for a in range(d): + for b in range(d): + diag[idx] = np.exp(-1j * (theta / 2.0) * lam[a] * lam[b]) + idx += 1 + return np.diag(diag) + + +def _rxx_matrix_func( + d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 +) -> Tensor: + r""" + Two-qudit RXX(θ) on a selected two-state subspace. + + Acts like a qubit RXX on the subspace spanned by |j1, j2> and |k1, k2>: + + .. math:: + + RXX(\theta) = + \begin{pmatrix} + \cos\!\left(\tfrac{\theta}{2}\right) & -i \sin\!\left(\tfrac{\theta}{2}\right) \\ + -i \sin\!\left(\tfrac{\theta}{2}\right) & \cos\!\left(\tfrac{\theta}{2}\right) + \end{pmatrix} + All other basis states are unchanged. + + Args: + d (int): Dimension for both qudits (assumed equal). + theta (float): Rotation angle. + j1, k1 (int): Levels on qudit-1. + j2, k2 (int): Levels on qudit-2. + + Returns: + Tensor: A ``(d*d, d*d)`` numpy array representing the RXX gate. + """ + D = d * d + M = np.eye(D, dtype=npdtype) + + # flatten basis index: |a,b> ↦ a*d + b + idx_a = j1 * d + j2 + idx_b = k1 * d + k2 + + c = np.cos(theta / 2.0) + s = np.sin(theta / 2.0) + + # Overwrite the chosen 2x2 block + M[idx_a, idx_a] = c + M[idx_b, idx_b] = c + M[idx_a, idx_b] = -1j * s + M[idx_b, idx_a] = -1j * s + + return M + + +def _u8_matrix_func( + d: int, + gamma: float = 2.0, + z: float = 1.0, + eps: float = 0.0, + omega: Optional[float] = None, +) -> Tensor: + if not _is_prime(d): + raise ValueError( + f"Dimension d={d} is not prime, U8 gate requires a prime dimension." + ) + if gamma == 0.0: + raise ValueError("gamma must be non-zero") + + vks = [0] * d + if d == 3: + vks = [0, 1, 8] + else: + try: + inv_12 = mod_inverse(12, d) + except ValueError: + raise ValueError( + f"Inverse of 12 mod {d} does not exist. Choose a prime d that does not divide 12." + ) + + for i in range(1, d): + a = inv_12 * i * (gamma + i * (6 * z + (2 * i - 3) * gamma)) + eps * i + vks[i] = Mod(a, d) + + # print(vks) + sum_vks = Mod(sum(vks), d) + if sum_vks != 0: + raise ValueError( + f"Sum of v_k's is not 0 mod {d}. Got {sum_vks}. Check parameters." + ) + + omega = np.exp(2j * np.pi / d) if omega is None else omega + matrix = np.zeros((d, d), dtype=npdtype) + for j in range(d): + matrix[j, j] = omega ** vks[j] + return matrix + + +def _cphase_matrix_func( + d: int, cv: Optional[int] = None, omega: Optional[float] = None +) -> Tensor: + r""" + Qudit Controlled-z gate + \ket{r}\ket{s} \rightarrow \omega^{rs}\ket{r}\ket{s} = \ket{r}Z^r\ket{s} + + This gate is also called SUMZ gate, where Z represents Z_d gate. + ┌─ ─┐ + │ I_d 0 0 ... 0 │ + │ 0 Z_d 0 ... 0 │ + SUMZ_d = │ 0 0 Z_d^2 ... 0 │ + │ . . . . . │ + │ 0 0 0 ... Z_d^{d-1} │ + └ ─┘ + """ + omega = np.exp(2j * np.pi / d) if omega is None else omega + size = d**2 + z_matrix = _z_matrix_func(d=d, omega=omega) + + if cv is None: + z_pows = [np.eye(d, dtype=npdtype)] + for _ in range(1, d): + z_pows.append(z_pows[-1] @ z_matrix) + + matrix = np.zeros((size, size), dtype=npdtype) + for a in range(d): + rs = a * d + matrix[rs : rs + d, rs : rs + d] = z_pows[a] + return matrix + + if not (0 <= cv < d): + raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") + + matrix = np.eye(size, dtype=npdtype) + rs = cv * d + matrix[rs : rs + d, rs : rs + d] = z_matrix + + return matrix + + +def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: + r""" + Qudit Controlled-NOT gate + \ket{r}\ket{s} \rightarrow \ket{r}\ket{r+s} = \ket{r}X^r\ket{s} = \ket{r}\ket{(r+s) mod d} + + This gate is also called SUMX gate, where X represents X_d gate. + ┌─ ─┐ + │ I_d 0 0 ... 0 │ + │ 0 X_d 0 ... 0 │ + SUMX_d = │ 0 0 X_d^2 ... 0 │ + │ . . . . . │ + │ 0 0 0 ... X_d^{d-1} │ + └ ─┘ + """ + size = d**2 + x_matrix = _x_matrix_func(d=d) + + if cv is None: + x_pows = [np.eye(d, dtype=npdtype)] + for _ in range(1, d): + x_pows.append(x_pows[-1] @ x_matrix) + + matrix = np.zeros((size, size), dtype=npdtype) + for a in range(d): + rs = a * d + matrix[rs : rs + d, rs : rs + d] = x_pows[a] + return matrix + + if not (0 <= cv < d): + raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") + matrix = np.eye(size, dtype=npdtype) + rs = cv * d + matrix[rs : rs + d, rs : rs + d] = x_matrix + + return matrix From 3c062ab6cc9b87007126a1d39217879d34580eae Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Thu, 28 Aug 2025 14:23:51 +0800 Subject: [PATCH 02/64] add quditgates to __init__.py --- tensorcircuit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tensorcircuit/__init__.py b/tensorcircuit/__init__.py index b6c4ef09..75971d0b 100644 --- a/tensorcircuit/__init__.py +++ b/tensorcircuit/__init__.py @@ -23,6 +23,7 @@ runtime_contractor, ) # prerun of set hooks from . import gates +from . import quditgates from . import basecircuit from .gates import Gate from .circuit import Circuit, expectation From 30c702593e8b3d38038fd31c48597de21f28a6e3 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 29 Aug 2025 15:33:15 +0800 Subject: [PATCH 03/64] Add functions from tc.Circuit --- tensorcircuit/quditcircuit.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index a093c5b3..7c7873cc 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -7,7 +7,7 @@ from .utils import arg_alias from .basecircuit import BaseCircuit from .circuit import Circuit -from .quantum import QuOperator +from .quantum import QuOperator, QuVector from .quditgates import ( _i_matrix_func, _x_matrix_func, @@ -152,7 +152,7 @@ def _apply_gate(self, *indices, name: str, **kwargs): mat = _cached_matrix( kind="single", name=name, d=self._d, omega=self._omega, key=key ) - self._circ.unitary(indices[0], unitary=mat, name=name, dim=self._d) # type: ignore + self._circ.unitary(*indices, unitary=mat, name=name, dim=self._d) # type: ignore elif len(indices) == 2 and name in TWO_BUILDERS: sig, _ = TWO_BUILDERS[name] key = tuple(kwargs.get(k) for k in sig if k != "none") @@ -160,11 +160,16 @@ def _apply_gate(self, *indices, name: str, **kwargs): kind="two", name=name, d=self._d, omega=self._omega, key=key ) self._circ.unitary( # type: ignore - indices[0], indices[1], unitary=mat, name=name, dim=self._d + *indices, unitary=mat, name=name, dim=self._d ) else: raise ValueError(f"Unsupported gate/arity: {name} on {len(indices)} qudits") + def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: + self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) + + unitary = any + def i(self, index: int) -> None: self._apply_gate(index, name="I") @@ -279,6 +284,10 @@ def sample( status: Optional[Tensor] = None, jittable: bool = True, ) -> Any: + if format in ["sample_int", "count_tuple", "count_dict_int"]: + raise NotImplementedError( + "`int` representation is not friendly for d-dimensional systems." + ) return self._circ.sample( batch=batch, allow_state=allow_state, @@ -297,3 +306,18 @@ def projected_subsystem(self, traceout: Tensor, left: Tuple[int, ...]) -> Tensor def replace_inputs(self, inputs: Tensor) -> None: return self._circ.replace_inputs(inputs) + + def mid_measurement(self, index: int, keep: int = 0) -> Tensor: + return self._circ.mid_measurement(index, keep=keep) + + mid_measure = mid_measurement + post_select = mid_measurement + post_selection = mid_measurement + + def get_quvector(self) -> QuVector: + return self._circ.quvector() + + quvector = get_quvector + + def replace_mps_inputs(self, mps_inputs: QuOperator) -> None: + return self._circ.replace_mps_inputs(mps_inputs) From da940863e166f2d829e67c94e4ac1b0ad70205fd Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 29 Aug 2025 15:33:43 +0800 Subject: [PATCH 04/64] Add tests for QuditCircuit --- tests/test_quditcircuit.py | 353 +++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 tests/test_quditcircuit.py diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py new file mode 100644 index 00000000..c1286b96 --- /dev/null +++ b/tests/test_quditcircuit.py @@ -0,0 +1,353 @@ +# pylint: disable=invalid-name + +import os +import sys +from functools import partial + +import numpy as np +import opt_einsum as oem +import pytest +from pytest_lazyfixture import lazy_fixture as lf + +# see https://stackoverflow.com/questions/56307329/how-can-i-parametrize-tests-to-run-with-different-fixtures-in-pytest + +thisfile = os.path.abspath(__file__) +modulepath = os.path.dirname(os.path.dirname(thisfile)) + +sys.path.insert(0, modulepath) +import tensorcircuit as tc + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) +def test_basics(backend): + c = tc.QuditCircuit(2, 3) + c.x(0) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("10")), np.array(1.0)) + c.csum(0, 1) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("11")), np.array(1.0)) + c.csum(0, 1) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("12")), np.array(1.0)) + + c = tc.QuditCircuit(2, 3) + c.x(0) + c.x(0) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("20")), np.array(1.0)) + c.csum(0, 1, cv=1) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("21")), np.array(0.0)) + c.csum(0, 1, cv=2) + np.testing.assert_allclose(tc.backend.numpy(c.amplitude("21")), np.array(1.0)) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) +def test_measure(backend): + c = tc.QuditCircuit(2, 3) + c.h(0) + c.x(1) + c.csum(0, 1) + assert c.measure(1)[0] in [0, 1, 2] + + +# @pytest.mark.parametrize("backend", [lf("jaxb")]) +# def test_large_scale_sample(backend): +# L = 30 +# d = 3 +# c = tc.QuditCircuit(L, d) +# c.h(0) +# for i in range(L - 1): +# c.csum(i, i + 1) +# +# batch = 1024 +# results = c.sample( +# allow_state=False, batch=batch, format="count_dict_bin", jittable=False +# ) +# # print(results) +# # +# # samples = c.sample(allow_state=False, batch=20000, format="sample_bin", jittable=False) +# # first = np.array(samples)[:, 0].astype(int) +# # print(first) +# # print(np.unique(first, return_counts=True)) +# # +# # results = c.sample(allow_state=False, batch=1024, format="count_dict_bin", jittable=False) +# # print(sorted(results.items(), key=lambda kv: kv[1], reverse=True)[:5]) +# +# k0, k1, k2 = "0" * L, "1" * L, "2" * L +# c0, c1, c2 = results.get(k0, 0), results.get(k1, 0), results.get(k2, 0) +# assert c0 + c1 + c2 == batch +# +# probs = np.array([c0, c1, c2], dtype=float) / batch +# np.testing.assert_allclose(probs, np.ones(3) / 3, rtol=0.2, atol=0.0) +# +# for a, b in [(c0, c1), (c1, c2), (c0, c2)]: +# ratio = (a + 1e-12) / (b + 1e-12) +# assert 0.8 <= ratio <= 1.25 + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) +def test_expectation(backend): + c = tc.QuditCircuit(2, 3) + c.h(0) + np.testing.assert_allclose( + tc.backend.numpy(c.expectation((tc.quditgates._z_matrix_func(3), [0]))), + 0, + atol=1e-7, + ) + + +def test_complex128(highp, tfb): + c = tc.QuditCircuit(2, 3) + c.h(1) + c.rx(0, theta=1j) + c.wavefunction() + np.testing.assert_allclose( + c.expectation((tc.quditgates._z_matrix_func(3), [1])), 0, atol=1e-15 + ) + + +def test_single_qubit(): + c = tc.QuditCircuit(1, 10) + c.h(0) + w = c.state()[0] + np.testing.assert_allclose( + w, + np.array( + [ + 1, + ] + * 10 + ) + / np.sqrt(10), + atol=1e-4, + ) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) +def test_expectation_between_two_states_qudit(backend): + d = 3 + X3 = tc.quditgates._x_matrix_func(d) + Y3 = tc.quditgates._y_matrix_func(d) # ZX/i + Z3 = tc.quditgates._z_matrix_func(d) + H3 = tc.quditgates._h_matrix_func(d) + X3_dag = np.conjugate(X3.T) + + e0 = np.array([1.0, 0.0, 0.0], dtype=np.complex64) + e1 = np.array([0.0, 1.0, 0.0], dtype=np.complex64) + val = tc.expectation((tc.gates.Gate(Y3), [0]), ket=e0, bra=e1, d=d) + omega = np.exp(2j * np.pi / d) + expected = omega / 1j + np.testing.assert_allclose(tc.backend.numpy(val), expected, rtol=1e-6, atol=1e-6) + + c = tc.QuditCircuit(3, d) + c.unitary(0, unitary=tc.gates.Gate(H3)) + c.ry(1, theta=0.8, j=0, k=1) + state = c.wavefunction() + x1z2 = [(tc.gates.Gate(X3), [0]), (tc.gates.Gate(Z3), [1])] + e1 = c.expectation(*x1z2) + e2 = tc.expectation(*x1z2, ket=state, bra=state, normalization=True, d=d) + np.testing.assert_allclose(tc.backend.numpy(e2), tc.backend.numpy(e1)) + + c = tc.QuditCircuit(3, d) + c.unitary(0, unitary=tc.gates.Gate(H3)) + c.ry(1, theta=0.8 + 0.7j, j=0, k=1) + state = c.wavefunction() + e1 = c.expectation(*x1z2) / (tc.backend.norm(state) ** 2) + e2 = tc.expectation(*x1z2, ket=state, normalization=True, d=d) + np.testing.assert_allclose(tc.backend.numpy(e2), tc.backend.numpy(e1)) + + c1 = tc.QuditCircuit(2, d) + c1.unitary(1, unitary=tc.gates.Gate(X3)) + s1 = c1.state() + + c2 = tc.QuditCircuit(2, d) + c2.unitary(0, unitary=tc.gates.Gate(X3)) + s2 = c2.state() + + c3 = tc.QuditCircuit(2, d) + c3.unitary(1, unitary=tc.gates.Gate(H3)) + s3 = c3.state() + + x1x2_fixed = [(tc.gates.Gate(X3), [0]), (tc.gates.Gate(X3_dag), [1])] + e = tc.expectation(*x1x2_fixed, ket=s1, bra=s2, d=d) + np.testing.assert_allclose(tc.backend.numpy(e), 1.0) + + e2 = tc.expectation(*x1x2_fixed, ket=s3, bra=s2, d=d) + np.testing.assert_allclose(tc.backend.numpy(e2), 1.0 / np.sqrt(3)) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb"), lf("cpb")]) +def test_any_inputs_state_qudit_true_gates(backend): + d = 3 + Xd = tc.quditgates._x_matrix_func(d) + Zd = tc.quditgates._z_matrix_func(d) + omega = np.exp(2j * np.pi / d) + + def idx(j0, j1): + return d * j0 + j1 + + vec = np.zeros(d * d, dtype=np.complex64) + vec[idx(2, 0)] = 1.0 + c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c.unitary(0, unitary=tc.gates.Gate(Xd)) + z0 = c.expectation((tc.gates.Gate(Zd), [0])) + np.testing.assert_allclose(tc.backend.numpy(z0), 1.0 + 0j, rtol=1e-6, atol=1e-6) + + vec = np.zeros(d * d, dtype=np.complex64) + vec[idx(0, 0)] = 1.0 + c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c.unitary(0, unitary=tc.gates.Gate(Xd)) + z0 = c.expectation((tc.gates.Gate(Zd), [0])) + np.testing.assert_allclose(tc.backend.numpy(z0), omega, rtol=1e-6, atol=1e-6) + + vec = np.zeros(d * d, dtype=np.complex64) + vec[idx(1, 0)] = 1.0 + c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c.unitary(0, unitary=tc.gates.Gate(Xd)) + z0 = c.expectation((tc.gates.Gate(Zd), [0])) + np.testing.assert_allclose(tc.backend.numpy(z0), omega**2, rtol=1e-6, atol=1e-6) + + vec = np.zeros(d * d, dtype=np.complex64) + vec[idx(0, 0)] = 1 / np.sqrt(2) + vec[idx(1, 0)] = 1 / np.sqrt(2) + c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c.unitary(0, unitary=tc.gates.Gate(Xd)) + z0 = c.expectation((tc.gates.Gate(Zd), [0])) + np.testing.assert_allclose(tc.backend.numpy(z0), -0.5 + 0j, rtol=1e-6, atol=1e-6) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("cpb")]) +def test_postselection(backend): + c = tc.QuditCircuit(3, 3) + c.h(1) + c.h(2) + c.mid_measurement(1, 1) + c.mid_measurement(2, 1) + s = c.wavefunction() + np.testing.assert_allclose(tc.backend.numpy(s[4]).real, 1.0 / 3.0, rtol=1e-6) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) +def test_unitary(backend): + c = tc.QuditCircuit(2, dim=3, inputs=np.eye(9)) + c.x(0) + c.z(1) + answer = tc.backend.numpy( + np.kron(tc.quditgates._x_matrix_func(3), tc.quditgates._z_matrix_func(3)) + ) + np.testing.assert_allclose( + tc.backend.numpy(c.wavefunction().reshape([9, 9])), answer, atol=1e-4 + ) + + +def test_probability(): + c = tc.QuditCircuit(2, 3) + c.h(0) + c.h(1) + np.testing.assert_allclose(c.probability(), np.ones(9) / 9, atol=1e-5) + + +def test_circuit_add_demo(): + dim = 3 + c = tc.QuditCircuit(2, dim=dim) + c.x(0) # |00> -> |10> + c2 = tc.QuditCircuit(2, dim=dim, mps_inputs=c.quvector()) + c2.x(0) # |00> -> |20> + answer = np.zeros(dim * dim, dtype=np.complex64) + answer[dim * 2 + 0] = 1.0 + np.testing.assert_allclose(c2.wavefunction(), answer, atol=1e-4) + c3 = tc.QuditCircuit(2, dim=dim) + c3.x(0) + c3.replace_mps_inputs(c.quvector()) + np.testing.assert_allclose(c3.wavefunction(), answer, atol=1e-4) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_circuit_matrix(backend): + c = tc.QuditCircuit(2, 3) + c.x(1) + c.csum(0, 1) + + U = c.matrix() + U_np = tc.backend.numpy(U) + + row_10 = U_np[3] + expected_row_10 = np.zeros(9, dtype=row_10.dtype) + expected_row_10[4] = 1.0 + np.testing.assert_allclose(row_10, expected_row_10, atol=1e-5) + + state = tc.backend.numpy(c.state()) + expected_state = np.zeros(9, dtype=state.dtype) + expected_state[1] = 1.0 + np.testing.assert_allclose(state, expected_state, atol=1e-5) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_batch_sample(backend): + c = tc.QuditCircuit(3, 3) + c.h(0) + c.csum(0, 1) + print(c.sample()) + print(c.sample(batch=8, status=np.random.uniform(size=[8, 3]))) + print(c.sample(batch=8)) + print(c.sample(random_generator=tc.backend.get_random_state(42))) + print(c.sample(allow_state=True)) + print(c.sample(batch=8, allow_state=True)) + print( + c.sample( + batch=8, allow_state=True, random_generator=tc.backend.get_random_state(42) + ) + ) + print( + c.sample( + batch=8, + allow_state=True, + status=np.random.uniform(size=[8]), + format="sample_bin", + ) + ) + print( + c.sample( + batch=8, + allow_state=False, + status=np.random.uniform(size=[8, 3]), + format="sample_bin", + ) + ) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_sample_format(backend): + c = tc.QuditCircuit(2, 3) + c.h(0) + c.csum(0, 1) + key = tc.backend.get_random_state(42) + for allow_state in [False, True]: + print("allow_state: ", allow_state) + for batch in [None, 1, 3]: + print(" batch: ", batch) + for format_ in [ + None, + "sample_bin", + "count_vector", + "count_dict_bin", + ]: + print(" format: ", format_) + print( + " ", + c.sample( + batch=batch, + allow_state=allow_state, + format_=format_, + random_generator=key, + ), + ) + + +def test_sample_representation(): + _ALPHBET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + c = tc.QuditCircuit(1, 36) + (result,) = c.sample(1, format="count_dict_bin").keys() + assert result == _ALPHBET[0] + + for i in range(1, 35): + c.x(0) + (result,) = c.sample(1, format="count_dict_bin").keys() + assert result == _ALPHBET[i] From 6755d9f8caad18d14b0f5025b81bc04bbb8f37e0 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 29 Aug 2025 15:38:50 +0800 Subject: [PATCH 05/64] Reported a potential bug. --- tensorcircuit/quditcircuit.py | 5 +++++ tests/test_quditcircuit.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 7c7873cc..9e58fc49 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -284,6 +284,11 @@ def sample( status: Optional[Tensor] = None, jittable: bool = True, ) -> Any: + """ + A bug was reported in the JAX backend: by default integers use int32 precision. + As a result, values like 3^29 (and even 3^19) exceed the representable range, + causing errors during the conversion step in sample/count. + """ if format in ["sample_int", "count_tuple", "count_dict_int"]: raise NotImplementedError( "`int` representation is not friendly for d-dimensional systems." diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index c1286b96..01a52928 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -46,7 +46,11 @@ def test_measure(backend): c.csum(0, 1) assert c.measure(1)[0] in [0, 1, 2] - +""" +A bug was reported in the JAX backend: by default integers use int32 precision. + As a result, values like 3^29 (and even 3^19) exceed the representable range, + causing errors during the conversion step in sample/count. +""" # @pytest.mark.parametrize("backend", [lf("jaxb")]) # def test_large_scale_sample(backend): # L = 30 From 2b600b8319fe4693008cda666ac74c5af1c28102 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 29 Aug 2025 16:26:09 +0800 Subject: [PATCH 06/64] Add doc. --- tensorcircuit/quditcircuit.py | 149 +++++++++++++++++++++++++++++++++- tensorcircuit/quditgates.py | 51 ++++++++++++ 2 files changed, 198 insertions(+), 2 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 9e58fc49..55d8e353 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -76,7 +76,33 @@ def _cached_matrix( key: Optional[tuple] = (), ) -> Tensor: """ - kind: "single" or "two"; key is a parameter tuple sorted by signature for caching + Build and cache a matrix using a registered builder function. + + This function looks up a builder from either `SINGLE_BUILDERS` or + `TWO_BUILDERS` (depending on `kind`), and calls it with the given + arguments. Results are cached with `functools.lru_cache`, so repeated + calls with the same inputs return the cached tensor instead of + rebuilding it. + + Args: + kind: Either `"single"` (use `SINGLE_BUILDERS`) or `"two"` (use `TWO_BUILDERS`). + name: The builder name to look up in the chosen dictionary. + d: The dimension of the matrix. + omega: Optional frequency or scaling parameter, passed to the builder. + key: Tuple of extra parameters, matched in order to the builder’s + expected signature. + + Returns: + Tensor: The matrix built by the selected builder. + + Notes: + - The cache key depends on all arguments (`kind`, `name`, `d`, `omega`, `key`). + - The `key` tuple must have the same order as the builder’s signature. + - The same inputs will always return the same cached tensor. + + Raises: + KeyError: If the builder `name` is not found. + TypeError/ValueError: If `key` does not match the builder’s expected parameters. """ builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS _, builder = builders[name] @@ -146,6 +172,26 @@ def nqubits(self) -> int: return self._nqubits def _apply_gate(self, *indices, name: str, **kwargs): + """ + Apply a quantum gate (unitary) to one or two qudits in the circuit. + + The gate matrix is looked up by name in either `SINGLE_BUILDERS` (for + single-qudit gates) or `TWO_BUILDERS` (for two-qudit gates). The matrix + is built (and cached) via `_cached_matrix`, then applied to the circuit + at the given indices. + + Args: + *indices: The qudit indices the gate should act on. + - One index → single-qudit gate. + - Two indices → two-qudit gate. + name: The name of the gate (must exist in the chosen builder set). + **kwargs: Extra parameters for the gate. These are matched against + the gate’s signature from the builder definition. + + Raises: + ValueError: If `name` is not found, or if the number of indices + does not match the gate type (single vs two). + """ if len(indices) == 1 and name in SINGLE_BUILDERS: sig, _ = SINGLE_BUILDERS[name] key = tuple(kwargs.get(k) for k in sig if k != "none") @@ -166,37 +212,102 @@ def _apply_gate(self, *indices, name: str, **kwargs): raise ValueError(f"Unsupported gate/arity: {name} on {len(indices)} qudits") def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: - self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) + self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) # type: ignore unitary = any def i(self, index: int) -> None: + """ + Apply the identity (I) gate on the given qudit index. + + Args: + index: Qudit index to apply the gate on. + """ self._apply_gate(index, name="I") def x(self, index: int) -> None: + """ + Apply the X gate on the given qudit index. + + Args: + index: Qudit index to apply the gate on. + """ self._apply_gate(index, name="X") def y(self, index: int) -> None: + """ + Apply the Y gate on the given qudit index. + + Args: + index: Qudit index to apply the gate on. + """ self._apply_gate(index, name="Y") def z(self, index: int) -> None: + """ + Apply the Z gate on the given qudit index. + + Args: + index: Qudit index to apply the gate on. + """ self._apply_gate(index, name="Z") def h(self, index: int) -> None: + """ + Apply the Hadamard-like (H) gate on the given qudit index. + + Args: + index: Qudit index to apply the gate on. + """ self._apply_gate(index, name="H") def u8( self, index: int, gamma: float = 2.0, z: float = 1.0, eps: float = 0.0 ) -> None: + """ + Apply the U8 parameterized single-qudit gate. + + Args: + index: Qudit index to apply the gate on. + gamma: Gate parameter gamma (default 2.0). + z: Gate parameter z (default 1.0). + eps: Gate parameter eps (default 0.0). + """ self._apply_gate(index, name="U8", extra=(gamma, z, eps)) def rx(self, index: int, theta: float, j: int = 0, k: int = 1): + """ + Apply the single-qudit RX rotation on `index`. + + Args: + index: Qudit index to apply the gate on. + theta: Rotation angle. + j: Source level of the rotation subspace (default 0). + k: Target level of the rotation subspace (default 1). + """ self._apply_gate(index, name="RX", theta=theta, j=j, k=k) def ry(self, index: int, theta: float, j: int = 0, k: int = 1): + """ + Apply the single-qudit RY rotation on `index`. + + Args: + index: Qudit index to apply the gate on. + theta: Rotation angle. + j: Source level of the rotation subspace (default 0). + k: Target level of the rotation subspace (default 1). + """ self._apply_gate(index, name="RY", theta=theta, j=j, k=k) def rz(self, index: int, theta: float, j: int = 0): + """ + Apply the single-qudit RZ rotation on `index`. + + Args: + index: Qudit index to apply the gate on. + theta: Rotation angle around Z. + j: Level where the phase rotation is applied (default 0). + """ self._apply_gate(index, name="RZ", theta=theta, j=j) def rxx( @@ -208,17 +319,51 @@ def rxx( j2: int = 0, k2: int = 1, ): + """ + Apply a two-qudit RXX-type interaction on the given indices. + + Args: + *indices: Two qudit indices. + theta: Interaction strength/angle. + j1: Source level of the first qudit subspace (default 0). + k1: Target level of the first qudit subspace (default 1). + j2: Source level of the second qudit subspace (default 0). + k2: Target level of the second qudit subspace (default 1). + """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) def rzz(self, *indices: int, theta: float): + """ + Apply a two-qudit RZZ interaction on the given indices. + + Args: + *indices: Two qudit indices. + theta: Interaction angle. + """ self._apply_gate(*indices, name="RZZ", theta=theta) def cphase(self, *indices: int, cv: Optional[int] = None) -> None: + """ + Apply a controlled phase (CPHASE) gate. + + Args: + *indices: Two qudit indices (control, target). + cv: Optional control value. If None, default cv=1. + """ self._apply_gate(*indices, name="CPHASE", cv=cv) def csum(self, *indices: int, cv: Optional[int] = None) -> None: + """ + Apply a controlled-sum (CSUM) gate. + + Args: + *indices: Two qudit indices (control, target). + cv: Optional control value. If None, default cv=1. + """ self._apply_gate(*indices, name="CSUM", cv=cv) + cnot = csum + # Functional def wavefunction(self, form: str = "default") -> tn.Node.tensor: return self._circ.wavefunction(form) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 76688bcd..ad1e1d3d 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -305,6 +305,57 @@ def _u8_matrix_func( eps: float = 0.0, omega: Optional[float] = None, ) -> Tensor: + r""" + U8 diagonal single-qudit gate for prime dimensions. + + This gate is defined only when :math:`d` is prime. It is a diagonal + operator of size :math:`d \times d`: + + .. py:math:: + + U_8(d; \gamma, z, \epsilon) = + \mathrm{diag}\!\left(\omega^{v_0}, \omega^{v_1}, \ldots, \omega^{v_{d-1}}\right), + + where :math:`\omega = e^{2\pi i / d}` is a primitive :math:`d`-th root + of unity, and the exponents :math:`v_k` are computed from modular + polynomials depending on parameters :math:`\gamma, z, \epsilon`. + + For :math:`d=3`, the exponents are fixed as + + .. py:math:: + + (v_0, v_1, v_2) = (0, 1, 8). + + For general prime :math:`d`, the exponents are determined by + + .. py:math:: + + v_i \equiv \tfrac{1}{12} i \bigl(\gamma + i (6z + (2i-3)\gamma)\bigr) + \epsilon i + \pmod d, \quad i = 1, \ldots, d-1, + + with :math:`v_0 = 0`. The sequence :math:`(v_0,\ldots,v_{d-1})` must + also satisfy + + .. py:math:: + + \sum_{k=0}^{d-1} v_k \equiv 0 \pmod d. + + Args: + d: Qudit dimension (must be prime). + gamma: Gate parameter (must be non-zero). + z: Gate parameter. + eps: Gate parameter. + omega: Optional primitive :math:`d`-th root of unity. Defaults to + :math:`\exp(2\pi i / d)`. + + Returns: + Tensor: A :math:`(d, d)` diagonal numpy array of dtype ``npdtype``. + + Raises: + ValueError: If ``d`` is not prime; if ``gamma = 0``; if 12 has no + modular inverse modulo ``d``; or if the computed :math:`v_k` do not + sum to 0 modulo :math:`d`. + """ if not _is_prime(d): raise ValueError( f"Dimension d={d} is not prime, U8 gate requires a prime dimension." From 4b966650927cd27a73594b41155334e0e73c5d6d Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 29 Aug 2025 16:31:50 +0800 Subject: [PATCH 07/64] black --- tests/test_quditcircuit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 01a52928..9f708884 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -46,6 +46,7 @@ def test_measure(backend): c.csum(0, 1) assert c.measure(1)[0] in [0, 1, 2] + """ A bug was reported in the JAX backend: by default integers use int32 precision. As a result, values like 3^29 (and even 3^19) exceed the representable range, From dbbb685a972b106cfe99355c94d704c3b45af9fa Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Sun, 31 Aug 2025 14:37:12 +0800 Subject: [PATCH 08/64] Add QuditCircuit interface to __init__.py --- tensorcircuit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tensorcircuit/__init__.py b/tensorcircuit/__init__.py index 75971d0b..c4aea55c 100644 --- a/tensorcircuit/__init__.py +++ b/tensorcircuit/__init__.py @@ -26,6 +26,7 @@ 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 From 12f66c890044ed7ee9d1b40ded5447dad4a598c6 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Mon, 1 Sep 2025 18:53:06 +0800 Subject: [PATCH 09/64] remove a useless pack --- tensorcircuit/quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 55d8e353..45ba4d3c 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -1,4 +1,4 @@ -from functools import lru_cache, reduce, partial +from functools import lru_cache, partial from typing import Any, Dict, List, Optional, Tuple, Sequence, Union import numpy as np From 4eb9dc21130990776f861be8bf8874b6ced9fd04 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Mon, 1 Sep 2025 18:53:44 +0800 Subject: [PATCH 10/64] remove useless pack. --- tests/test_quditcircuit.py | 60 ++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 9f708884..9806a998 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -2,10 +2,8 @@ import os import sys -from functools import partial import numpy as np -import opt_einsum as oem import pytest from pytest_lazyfixture import lazy_fixture as lf @@ -127,92 +125,92 @@ def test_single_qubit(): @pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) def test_expectation_between_two_states_qudit(backend): - d = 3 - X3 = tc.quditgates._x_matrix_func(d) - Y3 = tc.quditgates._y_matrix_func(d) # ZX/i - Z3 = tc.quditgates._z_matrix_func(d) - H3 = tc.quditgates._h_matrix_func(d) + dim = 3 + X3 = tc.quditgates._x_matrix_func(dim) + Y3 = tc.quditgates._y_matrix_func(dim) # ZX/i + Z3 = tc.quditgates._z_matrix_func(dim) + H3 = tc.quditgates._h_matrix_func(dim) X3_dag = np.conjugate(X3.T) e0 = np.array([1.0, 0.0, 0.0], dtype=np.complex64) e1 = np.array([0.0, 1.0, 0.0], dtype=np.complex64) - val = tc.expectation((tc.gates.Gate(Y3), [0]), ket=e0, bra=e1, d=d) - omega = np.exp(2j * np.pi / d) + val = tc.expectation((tc.gates.Gate(Y3), [0]), ket=e0, bra=e1, dim=dim) + omega = np.exp(2j * np.pi / dim) expected = omega / 1j np.testing.assert_allclose(tc.backend.numpy(val), expected, rtol=1e-6, atol=1e-6) - c = tc.QuditCircuit(3, d) + c = tc.QuditCircuit(3, dim) c.unitary(0, unitary=tc.gates.Gate(H3)) c.ry(1, theta=0.8, j=0, k=1) state = c.wavefunction() x1z2 = [(tc.gates.Gate(X3), [0]), (tc.gates.Gate(Z3), [1])] e1 = c.expectation(*x1z2) - e2 = tc.expectation(*x1z2, ket=state, bra=state, normalization=True, d=d) + e2 = tc.expectation(*x1z2, ket=state, bra=state, normalization=True, dim=dim) np.testing.assert_allclose(tc.backend.numpy(e2), tc.backend.numpy(e1)) - c = tc.QuditCircuit(3, d) + c = tc.QuditCircuit(3, dim) c.unitary(0, unitary=tc.gates.Gate(H3)) c.ry(1, theta=0.8 + 0.7j, j=0, k=1) state = c.wavefunction() e1 = c.expectation(*x1z2) / (tc.backend.norm(state) ** 2) - e2 = tc.expectation(*x1z2, ket=state, normalization=True, d=d) + e2 = tc.expectation(*x1z2, ket=state, normalization=True, dim=dim) np.testing.assert_allclose(tc.backend.numpy(e2), tc.backend.numpy(e1)) - c1 = tc.QuditCircuit(2, d) + c1 = tc.QuditCircuit(2, dim) c1.unitary(1, unitary=tc.gates.Gate(X3)) s1 = c1.state() - c2 = tc.QuditCircuit(2, d) + c2 = tc.QuditCircuit(2, dim) c2.unitary(0, unitary=tc.gates.Gate(X3)) s2 = c2.state() - c3 = tc.QuditCircuit(2, d) + c3 = tc.QuditCircuit(2, dim) c3.unitary(1, unitary=tc.gates.Gate(H3)) s3 = c3.state() x1x2_fixed = [(tc.gates.Gate(X3), [0]), (tc.gates.Gate(X3_dag), [1])] - e = tc.expectation(*x1x2_fixed, ket=s1, bra=s2, d=d) + e = tc.expectation(*x1x2_fixed, ket=s1, bra=s2, dim=dim) np.testing.assert_allclose(tc.backend.numpy(e), 1.0) - e2 = tc.expectation(*x1x2_fixed, ket=s3, bra=s2, d=d) + e2 = tc.expectation(*x1x2_fixed, ket=s3, bra=s2, dim=dim) np.testing.assert_allclose(tc.backend.numpy(e2), 1.0 / np.sqrt(3)) @pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb"), lf("cpb")]) def test_any_inputs_state_qudit_true_gates(backend): - d = 3 - Xd = tc.quditgates._x_matrix_func(d) - Zd = tc.quditgates._z_matrix_func(d) - omega = np.exp(2j * np.pi / d) + dim = 3 + Xd = tc.quditgates._x_matrix_func(dim) + Zd = tc.quditgates._z_matrix_func(dim) + omega = np.exp(2j * np.pi / dim) def idx(j0, j1): - return d * j0 + j1 + return dim * j0 + j1 - vec = np.zeros(d * d, dtype=np.complex64) + vec = np.zeros(dim * dim, dtype=np.complex64) vec[idx(2, 0)] = 1.0 - c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c = tc.QuditCircuit(2, dim, inputs=tc.array_to_tensor(vec)) c.unitary(0, unitary=tc.gates.Gate(Xd)) z0 = c.expectation((tc.gates.Gate(Zd), [0])) np.testing.assert_allclose(tc.backend.numpy(z0), 1.0 + 0j, rtol=1e-6, atol=1e-6) - vec = np.zeros(d * d, dtype=np.complex64) + vec = np.zeros(dim * dim, dtype=np.complex64) vec[idx(0, 0)] = 1.0 - c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c = tc.QuditCircuit(2, dim, inputs=tc.array_to_tensor(vec)) c.unitary(0, unitary=tc.gates.Gate(Xd)) z0 = c.expectation((tc.gates.Gate(Zd), [0])) np.testing.assert_allclose(tc.backend.numpy(z0), omega, rtol=1e-6, atol=1e-6) - vec = np.zeros(d * d, dtype=np.complex64) + vec = np.zeros(dim * dim, dtype=np.complex64) vec[idx(1, 0)] = 1.0 - c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c = tc.QuditCircuit(2, dim, inputs=tc.array_to_tensor(vec)) c.unitary(0, unitary=tc.gates.Gate(Xd)) z0 = c.expectation((tc.gates.Gate(Zd), [0])) np.testing.assert_allclose(tc.backend.numpy(z0), omega**2, rtol=1e-6, atol=1e-6) - vec = np.zeros(d * d, dtype=np.complex64) + vec = np.zeros(dim * dim, dtype=np.complex64) vec[idx(0, 0)] = 1 / np.sqrt(2) vec[idx(1, 0)] = 1 / np.sqrt(2) - c = tc.QuditCircuit(2, d, inputs=tc.array_to_tensor(vec)) + c = tc.QuditCircuit(2, dim, inputs=tc.array_to_tensor(vec)) c.unitary(0, unitary=tc.gates.Gate(Xd)) z0 = c.expectation((tc.gates.Gate(Zd), [0])) np.testing.assert_allclose(tc.backend.numpy(z0), -0.5 + 0j, rtol=1e-6, atol=1e-6) From bde4f24518268c4cf3b064ffbfb944ef08ca5d7c Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Mon, 1 Sep 2025 19:02:20 +0800 Subject: [PATCH 11/64] fixed a possible bug --- tensorcircuit/quditcircuit.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 45ba4d3c..50471e14 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -105,9 +105,13 @@ def _cached_matrix( TypeError/ValueError: If `key` does not match the builder’s expected parameters. """ builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS - _, builder = builders[name] - sig = builders[name][0] - kwargs = {k: v for k, v in zip(sig, key)} + try: + sig, builder = builders[name] + except KeyError as e: + raise KeyError(f"Unknown builder '{name}' for kind '{kind}'") from e + + extras: Tuple[Any, ...] = () if key is None else key # normalized & typed + kwargs = {k: v for k, v in zip(sig, extras)} return builder(d, omega, **kwargs) From 02121350382ab580fcbfa8e008dffbeb8ae49f85 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Mon, 1 Sep 2025 19:02:47 +0800 Subject: [PATCH 12/64] formatted codes. --- tensorcircuit/quditcircuit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 50471e14..0301fd85 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -73,7 +73,7 @@ def _cached_matrix( name: str, d: int, omega: Optional[float] = None, - key: Optional[tuple] = (), + key: Optional[tuple[Any, ...]] = (), ) -> Tensor: """ Build and cache a matrix using a registered builder function. @@ -175,7 +175,7 @@ def dim(self) -> int: def nqubits(self) -> int: return self._nqubits - def _apply_gate(self, *indices, name: str, **kwargs): + def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: """ Apply a quantum gate (unitary) to one or two qudits in the circuit. @@ -279,7 +279,7 @@ def u8( """ self._apply_gate(index, name="U8", extra=(gamma, z, eps)) - def rx(self, index: int, theta: float, j: int = 0, k: int = 1): + def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ Apply the single-qudit RX rotation on `index`. @@ -291,7 +291,7 @@ def rx(self, index: int, theta: float, j: int = 0, k: int = 1): """ self._apply_gate(index, name="RX", theta=theta, j=j, k=k) - def ry(self, index: int, theta: float, j: int = 0, k: int = 1): + def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ Apply the single-qudit RY rotation on `index`. @@ -303,7 +303,7 @@ def ry(self, index: int, theta: float, j: int = 0, k: int = 1): """ self._apply_gate(index, name="RY", theta=theta, j=j, k=k) - def rz(self, index: int, theta: float, j: int = 0): + def rz(self, index: int, theta: float, j: int = 0) -> None: """ Apply the single-qudit RZ rotation on `index`. @@ -322,7 +322,7 @@ def rxx( k1: int = 1, j2: int = 0, k2: int = 1, - ): + ) -> None: """ Apply a two-qudit RXX-type interaction on the given indices. @@ -336,7 +336,7 @@ def rxx( """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) - def rzz(self, *indices: int, theta: float): + def rzz(self, *indices: int, theta: float) -> None: """ Apply a two-qudit RZZ interaction on the given indices. From c9107916cad4aec34d8751d397d34570abda528a Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Mon, 1 Sep 2025 20:17:39 +0800 Subject: [PATCH 13/64] remove redundant code. --- tensorcircuit/quditcircuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 0301fd85..fc22de06 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -149,7 +149,6 @@ def __init__( inputs=inputs, mps_inputs=mps_inputs, split=split, - qudit=True, ) self._omega = np.exp(2j * np.pi / self._d) self.circuit_param = self._circ.circuit_param From 255824a7dd1c60e0b0539bc40f07f6f9dc37c7b2 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Tue, 2 Sep 2025 08:59:12 +0800 Subject: [PATCH 14/64] add test_large_scale_sample --- tests/test_quditcircuit.py | 62 +++++++++++++++----------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 9806a998..f119185e 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -45,44 +45,30 @@ def test_measure(backend): assert c.measure(1)[0] in [0, 1, 2] -""" -A bug was reported in the JAX backend: by default integers use int32 precision. - As a result, values like 3^29 (and even 3^19) exceed the representable range, - causing errors during the conversion step in sample/count. -""" -# @pytest.mark.parametrize("backend", [lf("jaxb")]) -# def test_large_scale_sample(backend): -# L = 30 -# d = 3 -# c = tc.QuditCircuit(L, d) -# c.h(0) -# for i in range(L - 1): -# c.csum(i, i + 1) -# -# batch = 1024 -# results = c.sample( -# allow_state=False, batch=batch, format="count_dict_bin", jittable=False -# ) -# # print(results) -# # -# # samples = c.sample(allow_state=False, batch=20000, format="sample_bin", jittable=False) -# # first = np.array(samples)[:, 0].astype(int) -# # print(first) -# # print(np.unique(first, return_counts=True)) -# # -# # results = c.sample(allow_state=False, batch=1024, format="count_dict_bin", jittable=False) -# # print(sorted(results.items(), key=lambda kv: kv[1], reverse=True)[:5]) -# -# k0, k1, k2 = "0" * L, "1" * L, "2" * L -# c0, c1, c2 = results.get(k0, 0), results.get(k1, 0), results.get(k2, 0) -# assert c0 + c1 + c2 == batch -# -# probs = np.array([c0, c1, c2], dtype=float) / batch -# np.testing.assert_allclose(probs, np.ones(3) / 3, rtol=0.2, atol=0.0) -# -# for a, b in [(c0, c1), (c1, c2), (c0, c2)]: -# ratio = (a + 1e-12) / (b + 1e-12) -# assert 0.8 <= ratio <= 1.25 +@pytest.mark.parametrize("backend", [lf("jaxb")]) +def test_large_scale_sample(backend): + L = 30 + d = 3 + c = tc.QuditCircuit(L, d) + c.h(0) + for i in range(L - 1): + c.csum(i, i + 1) + + batch = 1024 + results = c.sample( + allow_state=False, batch=batch, format="count_dict_bin", jittable=False + ) + + k0, k1, k2 = "0" * L, "1" * L, "2" * L + c0, c1, c2 = results.get(k0, 0), results.get(k1, 0), results.get(k2, 0) + assert c0 + c1 + c2 == batch + + probs = np.array([c0, c1, c2], dtype=float) / batch + np.testing.assert_allclose(probs, np.ones(3) / 3, rtol=0.2, atol=0.0) + + for a, b in [(c0, c1), (c1, c2), (c0, c2)]: + ratio = (a + 1e-12) / (b + 1e-12) + assert 0.8 <= ratio <= 1.25 @pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) From edb97b1907508e8bafae94dfecf1b6075157c43d Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 08:24:07 +0800 Subject: [PATCH 15/64] Adjust the code framework --- tensorcircuit/quditcircuit.py | 105 +--------------------------------- tensorcircuit/quditgates.py | 90 ++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 104 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index fc22de06..80600169 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -1,4 +1,4 @@ -from functools import lru_cache, partial +from functools import partial from typing import Any, Dict, List, Optional, Tuple, Sequence, Union import numpy as np @@ -8,113 +8,12 @@ from .basecircuit import BaseCircuit from .circuit import Circuit from .quantum import QuOperator, QuVector -from .quditgates import ( - _i_matrix_func, - _x_matrix_func, - _y_matrix_func, - _z_matrix_func, - _h_matrix_func, - _u8_matrix_func, - _cphase_matrix_func, - _csum_matrix_func, - _rx_matrix_func, - _ry_matrix_func, - _rz_matrix_func, - _rxx_matrix_func, - _rzz_matrix_func, -) +from .quditgates import SINGLE_BUILDERS, TWO_BUILDERS, _cached_matrix Tensor = Any -SINGLE_BUILDERS = { - "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), - "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), - "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), - "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), - "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), - "RX": ( - ("theta", "j", "k"), - lambda d, omega, **kw: _rx_matrix_func(d, kw["theta"], kw["j"], kw["k"]), - ), - "RY": ( - ("theta", "j", "k"), - lambda d, omega, **kw: _ry_matrix_func(d, kw["theta"], kw["j"], kw["k"]), - ), - "RZ": ( - ("theta", "j"), - lambda d, omega, **kw: _rz_matrix_func(d, kw["theta"], kw["j"]), - ), - "U8": ( - ("gamma", "z", "eps"), - lambda d, omega, **kw: _u8_matrix_func( - d, kw["gamma"], kw["z"], kw["eps"], omega - ), - ), -} - -TWO_BUILDERS = { - "RXX": ( - ("theta", "j1", "k1", "j2", "k2"), - lambda d, omega, **kw: _rxx_matrix_func( - d, kw["theta"], kw["j1"], kw["k1"], kw["j2"], kw["k2"] - ), - ), - "RZZ": (("theta",), lambda d, omega, **kw: _rzz_matrix_func(d, kw["theta"])), - "CPHASE": (("cv",), lambda d, omega, **kw: _cphase_matrix_func(d, kw["cv"], omega)), - "CSUM": (("cv",), lambda d, omega, **kw: _csum_matrix_func(d, kw["cv"])), -} - - -@lru_cache(maxsize=None) -def _cached_matrix( - kind: str, - name: str, - d: int, - omega: Optional[float] = None, - key: Optional[tuple[Any, ...]] = (), -) -> Tensor: - """ - Build and cache a matrix using a registered builder function. - - This function looks up a builder from either `SINGLE_BUILDERS` or - `TWO_BUILDERS` (depending on `kind`), and calls it with the given - arguments. Results are cached with `functools.lru_cache`, so repeated - calls with the same inputs return the cached tensor instead of - rebuilding it. - - Args: - kind: Either `"single"` (use `SINGLE_BUILDERS`) or `"two"` (use `TWO_BUILDERS`). - name: The builder name to look up in the chosen dictionary. - d: The dimension of the matrix. - omega: Optional frequency or scaling parameter, passed to the builder. - key: Tuple of extra parameters, matched in order to the builder’s - expected signature. - - Returns: - Tensor: The matrix built by the selected builder. - - Notes: - - The cache key depends on all arguments (`kind`, `name`, `d`, `omega`, `key`). - - The `key` tuple must have the same order as the builder’s signature. - - The same inputs will always return the same cached tensor. - - Raises: - KeyError: If the builder `name` is not found. - TypeError/ValueError: If `key` does not match the builder’s expected parameters. - """ - builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS - try: - sig, builder = builders[name] - except KeyError as e: - raise KeyError(f"Unknown builder '{name}' for kind '{kind}'") from e - - extras: Tuple[Any, ...] = () if key is None else key # normalized & typed - kwargs = {k: v for k, v in zip(sig, extras)} - return builder(d, omega, **kwargs) - - class QuditCircuit: r""" ``QuditCircuit`` class. diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index ad1e1d3d..444e4666 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -1,4 +1,5 @@ -from typing import Any, Optional +from functools import lru_cache +from typing import Any, Optional, Tuple import numpy as np from sympy import mod_inverse, Mod @@ -8,6 +9,93 @@ Tensor = Any +SINGLE_BUILDERS = { + "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), + "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), + "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), + "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), + "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), + "RX": ( + ("theta", "j", "k"), + lambda d, omega, **kw: _rx_matrix_func(d, kw["theta"], kw["j"], kw["k"]), + ), + "RY": ( + ("theta", "j", "k"), + lambda d, omega, **kw: _ry_matrix_func(d, kw["theta"], kw["j"], kw["k"]), + ), + "RZ": ( + ("theta", "j"), + lambda d, omega, **kw: _rz_matrix_func(d, kw["theta"], kw["j"]), + ), + "U8": ( + ("gamma", "z", "eps"), + lambda d, omega, **kw: _u8_matrix_func( + d, kw["gamma"], kw["z"], kw["eps"], omega + ), + ), +} + +TWO_BUILDERS = { + "RXX": ( + ("theta", "j1", "k1", "j2", "k2"), + lambda d, omega, **kw: _rxx_matrix_func( + d, kw["theta"], kw["j1"], kw["k1"], kw["j2"], kw["k2"] + ), + ), + "RZZ": (("theta",), lambda d, omega, **kw: _rzz_matrix_func(d, kw["theta"])), + "CPHASE": (("cv",), lambda d, omega, **kw: _cphase_matrix_func(d, kw["cv"], omega)), + "CSUM": (("cv",), lambda d, omega, **kw: _csum_matrix_func(d, kw["cv"])), +} + + +@lru_cache(maxsize=None) +def _cached_matrix( + kind: str, + name: str, + d: int, + omega: Optional[float] = None, + key: Optional[tuple[Any, ...]] = (), +) -> Tensor: + """ + Build and cache a matrix using a registered builder function. + + This function looks up a builder from either `SINGLE_BUILDERS` or + `TWO_BUILDERS` (depending on `kind`), and calls it with the given + arguments. Results are cached with `functools.lru_cache`, so repeated + calls with the same inputs return the cached tensor instead of + rebuilding it. + + Args: + kind: Either `"single"` (use `SINGLE_BUILDERS`) or `"two"` (use `TWO_BUILDERS`). + name: The builder name to look up in the chosen dictionary. + d: The dimension of the matrix. + omega: Optional frequency or scaling parameter, passed to the builder. + key: Tuple of extra parameters, matched in order to the builder’s + expected signature. + + Returns: + Tensor: The matrix built by the selected builder. + + Notes: + - The cache key depends on all arguments (`kind`, `name`, `d`, `omega`, `key`). + - The `key` tuple must have the same order as the builder’s signature. + - The same inputs will always return the same cached tensor. + + Raises: + KeyError: If the builder `name` is not found. + TypeError/ValueError: If `key` does not match the builder’s expected parameters. + """ + builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS + try: + sig, builder = builders[name] + except KeyError as e: + raise KeyError(f"Unknown builder '{name}' for kind '{kind}'") from e + + extras: Tuple[Any, ...] = () if key is None else key # normalized & typed + kwargs = {k: v for k, v in zip(sig, extras)} + return builder(d, omega, **kwargs) + + def _is_prime(n: int) -> bool: if n < 2: return False From b0b30d51a6cb1368d12ac11372e76159b52e941a Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 10:05:35 +0800 Subject: [PATCH 16/64] new docstring --- tensorcircuit/quditcircuit.py | 181 ++++++++++++++++++++++++++++++++-- tensorcircuit/quditgates.py | 7 +- 2 files changed, 177 insertions(+), 11 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 80600169..09a46ed8 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -1,6 +1,19 @@ +""" +Quantum circuit: the state simulator. +Supports qudit (3 <= dim <= 36) systems. +For string-encoded samples/counts, digits use 0–9A–Z where A=10, …, Z=35. +""" + from functools import partial from typing import Any, Dict, List, Optional, Tuple, Sequence, Union +try: + from typing import Literal # py>=3.8 +except ImportError: + from typing_extensions import Literal # py<3.8 + +SAMPLE_FORMAT = Literal["sample_bin", "count_vector", "count_dict_bin"] + import numpy as np import tensornetwork as tn @@ -21,7 +34,7 @@ class QuditCircuit: Qudit quick example (d=3): .. code-block:: python - c = tc.Circuit(2, d=3) + c = tc.QuditCircuit(2, d=3) c.h(0) c.x(1) c.csum(0, 1) @@ -67,10 +80,12 @@ def _set_dim(self, dim: int) -> None: @property def dim(self) -> int: + """dimension of the qudit circuit""" return self._d @property def nqubits(self) -> int: + """qudit number of the circuit""" return self._nqubits def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: @@ -114,6 +129,14 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: raise ValueError(f"Unsupported gate/arity: {name} on {len(indices)} qudits") def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: + """ + Apply a quantum gate (unitary) to one or two qudits in the circuit. + + Args: + *indices: The qudit indices the gate should act on. + unitary: The unitary to apply to the qudit. + name: The name of the gate. + """ self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) # type: ignore unitary = any @@ -273,6 +296,12 @@ def wavefunction(self, form: str = "default") -> tn.Node.tensor: state = wavefunction def get_quoperator(self) -> QuOperator: + """ + Get the ``QuOperator`` MPO like representation of the circuit unitary without contraction. + + :return: ``QuOperator`` object for the circuit unitary (open indices for the input state) + :rtype: QuOperator + """ return self._circ.quoperator() quoperator = get_quoperator @@ -281,12 +310,18 @@ def get_quoperator(self) -> QuOperator: get_state_as_quvector = BaseCircuit.quvector def matrix(self) -> Tensor: + """ + Get the unitary matrix for the circuit irrespective with the circuit input state. + + :return: The circuit unitary matrix + :rtype: Tensor + """ return self._circ.matrix() - def measure_reference( - self, *index: int, with_prob: bool = False - ) -> Tuple[str, float]: - return self._circ.measure_reference(*index, with_prob=with_prob) + # def measure_reference( + # self, *index: int, with_prob: bool = False + # ) -> Tuple[str, float]: + # return self._circ.measure_reference(*index, with_prob=with_prob) def expectation( self, @@ -310,14 +345,55 @@ def expectation( def measure_jit( self, *index: int, with_prob: bool = False, status: Optional[Tensor] = None ) -> Tuple[Tensor, Tensor]: + """ + Take measurement on the given site indices (computational basis). + This method is jittable! + + :param index: Measure on which site (wire) index. + :type index: int + :param with_prob: If true, theoretical probability is also returned. + :type with_prob: bool, optional + :param status: external randomness, with shape [index], defaults to None + :type status: Optional[Tensor] + :return: The sample output and probability (optional) of the quantum line. + :rtype: Tuple[Tensor, Tensor] + """ return self._circ.measure_jit(*index, with_prob=with_prob, status=status) measure = measure_jit def amplitude(self, l: Union[str, Tensor]) -> Tensor: + r""" + Returns 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.QuditCircuit(2, dim=3) + >>> c.x(0) + >>> c.x(1) + >>> c.amplitude("20") + array(1.+0.j, dtype=complex64) + >>> c.csum(0, 1, cv=2) + >>> c.amplitude("21") + array(1.+0.j, dtype=complex64) + + :param l: The bitstring of base-d characters. + :type l: Union[str, Tensor] + :return: The amplitude of the circuit. + :rtype: tn.Node.tensor + """ return self._circ.amplitude(l) def probability(self) -> Tensor: + """ + get the d^n length probability vector over computational basis + + :return: probability vector of shape [dim**n] + :rtype: Tensor + """ return self._circ.probability() @partial(arg_alias, alias_dict={"format": ["format_"]}) @@ -326,15 +402,45 @@ def sample( batch: Optional[int] = None, allow_state: bool = False, readout_error: Optional[Sequence[Any]] = None, - format: Optional[str] = None, + format: Optional[SAMPLE_FORMAT] = None, random_generator: Optional[Any] = None, status: Optional[Tensor] = None, jittable: bool = True, ) -> Any: - """ - A bug was reported in the JAX backend: by default integers use int32 precision. - As a result, values like 3^29 (and even 3^19) exceed the representable range, - causing errors during the conversion step in sample/count. + r""" + batched sampling from state or circuit tensor network directly + + :param batch: number of samples, defaults to None + :type batch: Optional[int], optional + :param allow_state: if true, we sample from the final state + if memory allows, True is preferred, defaults to False + :type allow_state: bool, optional + :param readout_error: readout_error, defaults to None + :type readout_error: Optional[Sequence[Any]]. Tensor, List, Tuple + :param format: sample format, defaults to None as backward compatibility + check the doc in :py:meth:`tensorcircuit.quantum.measurement_results` + Six formats of measurement counts results: + + "sample_bin": # [np.array([1, 0]), np.array([1, 0])] + + "count_vector": # np.array([2, 0, 0, 0]) + + "count_dict_bin": # {"00": 2, "01": 0, "10": 0, "11": 0} + for cases d\in [11, 36], use 0–9A–Z digits (e.g., 'A' -> 10, …, 'Z' -> 35); + + :type format: Optional[str] + :param random_generator: random generator, defaults to None + :type random_generator: Optional[Any], optional + :param status: external randomness given by tensor uniformly from [0, 1], + if set, can overwrite random_generator, shape [batch] for `allow_state=True` + and shape [batch, nqubits] for `allow_state=False` using perfect sampling implementation + :type status: Optional[Tensor] + :param jittable: when converting to count, whether keep the full size. if false, may be conflict + external jit, if true, may fail for large scale system with actual limited count results + :type jittable: bool, defaults true + :return: List (if batch) of tuple (binary configuration tensor and corresponding probability) + if the format is None, and consistent with format when given + :rtype: Any """ if format in ["sample_int", "count_tuple", "count_dict_int"]: raise NotImplementedError( @@ -351,15 +457,42 @@ def sample( ) def projected_subsystem(self, traceout: Tensor, left: Tuple[int, ...]) -> Tensor: + """ + remaining wavefunction or density matrix on sites in ``left``, with other sites + fixed to given digits (0..d-1) as indicated by ``traceout`` + + :param traceout: can be jitted + :type traceout: Tensor + :param left: cannot be jitted + :type left: Tuple + :return: _description_ + :rtype: Tensor + """ return self._circ.projected_subsystem( traceout=traceout, left=left, ) def replace_inputs(self, inputs: Tensor) -> None: + """ + Replace the input state with the circuit structure unchanged. + + :param inputs: Input wavefunction. + :type inputs: Tensor + """ return self._circ.replace_inputs(inputs) def mid_measurement(self, index: int, keep: int = 0) -> Tensor: + """ + Middle measurement in z-basis on the circuit, note the wavefunction output is not normalized + with ``mid_measurement`` involved, one should normalize the state manually if needed. + This is a post-selection method as keep is provided as a prior. + + :param index: The index of qubit that the Z direction postselection applied on. + :type index: int + :param keep: the post-selected digit in {0, ..., d-1}, defaults to be 0. + :type keep: int, optional + """ return self._circ.mid_measurement(index, keep=keep) mid_measure = mid_measurement @@ -367,9 +500,37 @@ def mid_measurement(self, index: int, keep: int = 0) -> Tensor: post_selection = mid_measurement def get_quvector(self) -> QuVector: + """ + Get the representation of the output state in the form of ``QuVector`` + while maintaining the circuit uncomputed + + :return: ``QuVector`` representation of the output state from the circuit + :rtype: QuVector + """ return self._circ.quvector() quvector = get_quvector def replace_mps_inputs(self, mps_inputs: QuOperator) -> None: + """ + Replace the input state in MPS representation while keep the circuit structure unchanged. + + :Example: + >>> c = tc.QuditCircuit(2, dim=3) + >>> c.x(0) + >>> + >>> c2 = tc.QuditCircuit(2, dim=3, mps_inputs=c.quvector()) + >>> c2.x(0) + >>> c2.wavefunction() + array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) + >>> + >>> c3 = tc.QuditCircuit(2, dim=3) + >>> c3.x(0) + >>> c3.replace_mps_inputs(c.quvector()) + >>> c3.wavefunction() + array([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], dtype=complex64) + + :param mps_inputs: (Nodes, dangling Edges) for a MPS like initial wavefunction. + :type mps_inputs: Tuple[Sequence[Gate], Sequence[Edge]] + """ return self._circ.replace_mps_inputs(mps_inputs) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 444e4666..600b970b 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -97,6 +97,7 @@ def _cached_matrix( def _is_prime(n: int) -> bool: + """ Check if `n` is a prime number. """ if n < 2: return False if n in (2, 3, 5, 7): @@ -112,6 +113,7 @@ def _is_prime(n: int) -> bool: def _i_matrix_func(d: int) -> Tensor: + """ identity matrix function. """ matrix = np.zeros((d, d), dtype=npdtype) for i in range(d): matrix[i, i] = 1.0 @@ -181,10 +183,13 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: def _check_rotation(d: int, j: int, k: int) -> None: + """ + Check rotation of qudit `j` in `d` qubits. + """ if not (0 <= j < d) or not (0 <= k < d): raise ValueError(f"Indices j={j}, k={k} must satisfy 0 <= j,k < d (d={d}).") if j == k: - raise ValueError("RX rotation requires two distinct levels j != k.") + raise ValueError("R- rotation requires two distinct levels j != k.") def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: From 7585731d2457834054889923ea3497e7da3b8def Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 10:05:58 +0800 Subject: [PATCH 17/64] Add tests for quditgates --- tests/test_quditgates.py | 241 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 tests/test_quditgates.py diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py new file mode 100644 index 00000000..17a1f6b8 --- /dev/null +++ b/tests/test_quditgates.py @@ -0,0 +1,241 @@ +# tests/test_quditgates.py +import numpy as np +import pytest + +from tensorcircuit.quditgates import ( + _i_matrix_func, _x_matrix_func, _z_matrix_func, _y_matrix_func, _h_matrix_func, + _s_matrix_func, _rx_matrix_func, _ry_matrix_func, _rz_matrix_func, _swap_matrix_func, + _rzz_matrix_func, _rxx_matrix_func, _u8_matrix_func, _cphase_matrix_func, + _csum_matrix_func, _cached_matrix, SINGLE_BUILDERS, TWO_BUILDERS, npdtype +) + +if npdtype in (np.complex64, np.float32): + ATOL = 1e-6 + RTOL = 1e-6 +else: + ATOL = 1e-12 + RTOL = 1e-12 + +def is_unitary(M, atol=None, rtol=None): + if atol is None or rtol is None: + atol = ATOL + rtol = RTOL + Mc = M.astype(np.complex128, copy=False) + I = np.eye(M.shape[0], dtype=np.complex128) + return (np.allclose(Mc.conj().T @ Mc, I, atol=atol, rtol=rtol) and + np.allclose(Mc @ Mc.conj().T, I, atol=atol, rtol=rtol)) + +@pytest.mark.parametrize("d", [2, 3, 4, 5]) +def test_I_X_Z_shapes_and_unitarity(d): + I = _i_matrix_func(d) + X = _x_matrix_func(d) + Z = _z_matrix_func(d) + assert I.shape == (d, d) and X.shape == (d, d) and Z.shape == (d, d) + assert is_unitary(X) + assert is_unitary(Z) + assert np.allclose(I, np.eye(d, dtype=npdtype), atol=ATOL) + +@pytest.mark.parametrize("d", [2, 3, 4]) +def test_X_is_right_cyclic_shift(d): + X = _x_matrix_func(d) + for j in range(d): + v = np.zeros(d, dtype=npdtype); v[j] = 1 + out = X @ v + expected = np.zeros(d, dtype=npdtype); expected[(j+1) % d] = 1 + assert np.allclose(out, expected, atol=ATOL) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_Z_diagonal_and_value(d): + omega = np.exp(2j*np.pi/d) + Z = _z_matrix_func(d, omega) + assert np.allclose(Z, np.diag([omega**j for j in range(d)]), atol=ATOL) + assert is_unitary(Z) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_Y_equals_ZX_over_i(d): + Y = _y_matrix_func(d) + ZX_over_i = (_z_matrix_func(d) @ _x_matrix_func(d)) / 1j + assert np.allclose(Y, ZX_over_i, atol=ATOL) + assert is_unitary(Y) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_H_is_fourier_like_and_unitary(d): + H = _h_matrix_func(d) + assert H.shape == (d, d) + assert is_unitary(H) + omega = np.exp(2j*np.pi/d) + F = (1/np.sqrt(d)) * np.array([[omega**(j*k) for k in range(d)] for j in range(d)], dtype=npdtype).T + assert np.allclose(H.astype(np.complex128), F.astype(np.complex128), atol=ATOL, rtol=RTOL) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_S_is_diagonal(d): + S = _s_matrix_func(d) + assert np.allclose(S, np.diag(np.diag(S)), atol=ATOL) + +@pytest.mark.parametrize("d", [3, 5]) +def test_RX_RY_only_affect_subspace(d): + theta = 0.7 + j, k = 0, 1 + RX = _rx_matrix_func(d, theta, j, k) + RY = _ry_matrix_func(d, theta, j, k) + assert is_unitary(RX) and is_unitary(RY) + for t in range(d): + if t not in (j, k): + e = np.zeros(d, dtype=npdtype); e[t]=1 + outx = RX @ e; outy = RY @ e + assert np.allclose(outx, e, atol=ATOL) + assert np.allclose(outy, e, atol=ATOL) + +def test_RZ_phase_on_single_level(): + d, theta, j = 5, 1.234, 2 + RZ = _rz_matrix_func(d, theta, j) + assert is_unitary(RZ) + diag = np.ones(d, dtype=npdtype); diag[j] = np.exp(1j*theta) + assert np.allclose(RZ, np.diag(diag), atol=ATOL) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_SWAP_permutation(d): + SW = _swap_matrix_func(d) + D = d*d + assert SW.shape == (D, D) + assert is_unitary(SW) + for i in range(min(d,3)): + for j in range(min(d,3)): + v = np.zeros(D, dtype=npdtype); v[i*d + j] = 1 + out = SW @ v + exp = np.zeros(D, dtype=npdtype); exp[j*d + i] = 1 + assert np.allclose(out, exp, atol=ATOL) + +@pytest.mark.parametrize("d", [2, 3, 5]) +def test_RZZ_diagonal(d): + theta = 0.37 + RZZ = _rzz_matrix_func(d, theta) + assert is_unitary(RZZ) + assert np.allclose(RZZ, np.diag(np.diag(RZZ)), atol=ATOL) # 对角阵 + +def test_RXX_selected_block(): + d = 4; theta = 0.81 + j1,k1 = 0,2 + j2,k2 = 1,3 + RXX = _rxx_matrix_func(d, theta, j1,k1,j2,k2) + assert is_unitary(RXX) + D = d*d + I = np.eye(D, dtype=npdtype) + idx_a = j1*d + j2 + idx_b = k1*d + k2 + for t in range(D): + for s in range(D): + if {t,s} & {idx_a, idx_b}: + continue + assert np.isclose(RXX[t, s], I[t, s], atol=ATOL, rtol=RTOL) + +@pytest.mark.parametrize("d", [3, 5]) +def test_CPHASE_blocks(d): + omega = np.exp(2j*np.pi/d) + Z = _z_matrix_func(d, omega) + M = _cphase_matrix_func(d, cv=None, omega=omega) + for a in range(d): + rs = a*d + block = M[rs:rs+d, rs:rs+d] + Za = np.linalg.matrix_power(Z, a) + assert np.allclose(block, Za, atol=ATOL) + assert is_unitary(M) + + cv = 1 + M2 = _cphase_matrix_func(d, cv=cv, omega=omega) + for a in range(d): + rs = a*d + block = M2[rs:rs+d, rs:rs+d] + if a == cv: + assert np.allclose(block, Z, atol=ATOL) + else: + assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + +@pytest.mark.parametrize("d", [3, 5]) +def test_CSUM_blocks(d): + X = _x_matrix_func(d) + M = _csum_matrix_func(d, cv=None) + for a in range(d): + rs = a*d + block = M[rs:rs+d, rs:rs+d] + Xa = np.linalg.matrix_power(X, a) + assert np.allclose(block, Xa, atol=ATOL) + assert is_unitary(M) + + cv = 2 % d + M2 = _csum_matrix_func(d, cv=cv) + for a in range(d): + rs = a*d + block = M2[rs:rs+d, rs:rs+d] + if a == cv: + assert np.allclose(block, X, atol=ATOL) + else: + assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + +def test_CSUM_mapping_small_d(): + d = 3 + M = _csum_matrix_func(d) + for r in range(d): + for s in range(d): + v = np.zeros(d*d, dtype=npdtype); v[r*d + s] = 1 + out = M @ v + exp = np.zeros(d*d, dtype=npdtype); exp[r*d + ((r+s)%d)] = 1 + assert np.allclose(out, exp, atol=ATOL) + +def test_rotation_index_errors(): + d = 4 + with pytest.raises(ValueError): + _rx_matrix_func(d, 0.1, j=-1, k=1) + with pytest.raises(ValueError): + _ry_matrix_func(d, 0.1, j=0, k=4) + with pytest.raises(ValueError): + _rx_matrix_func(d, 0.1, j=2, k=2) + +def test_U8_errors_and_values(): + with pytest.raises(ValueError): + _u8_matrix_func(d=4) + with pytest.raises(ValueError): + _u8_matrix_func(d=3, gamma=0.0) + d = 3 + U = _u8_matrix_func(d, gamma=2.0, z=1.0, eps=0.0) + omega = np.exp(2j*np.pi/d) + expected = np.diag([omega**0, omega**1, omega**8]) + assert np.allclose(U, expected, atol=ATOL) + +def test_CPHASE_CSUM_cv_range(): + d = 5 + with pytest.raises(ValueError): + _cphase_matrix_func(d, cv=-1) + with pytest.raises(ValueError): + _cphase_matrix_func(d, cv=d) + with pytest.raises(ValueError): + _csum_matrix_func(d, cv=-1) + with pytest.raises(ValueError): + _csum_matrix_func(d, cv=d) + +def test_cached_matrix_identity_and_x(): + A1 = _cached_matrix("single", "I", d=3, omega=None, key=()) + A2 = _cached_matrix("single", "I", d=3, omega=None, key=()) + assert A1 is A2 + + X1 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.1,0,1)) + X2 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.2,0,1)) + assert X1 is not X2 + assert X1.shape == (3,3) and X2.shape == (3,3) + +def test_builders_smoke(): + d = 3 + for name, (sig, _) in SINGLE_BUILDERS.items(): + defaults = { + "theta": 0.1, "gamma": 0.1, "z": 0.1, "eps": 0.1, + "j": 0, "k": 1, # ensure j != k when present + } + key = tuple(defaults.get(s, 0) for s in sig) + M = _cached_matrix("single", name, d, None, key) + assert M.shape == (d, d) + + for name, (sig, _) in TWO_BUILDERS.items(): + defaults = {"theta":0.1, "j1":0, "k1":1, "j2":0, "k2":1, "cv":None} + key = tuple(defaults[s] for s in sig) + M = _cached_matrix("two", name, d, None, key) + assert M.shape == (d*d, d*d) \ No newline at end of file From 0e6c053f695db85c6e876ffba186ab69b52c9032 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 12:09:30 +0800 Subject: [PATCH 18/64] formatted codes --- tensorcircuit/quditcircuit.py | 10 ++-------- tensorcircuit/quditgates.py | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 09a46ed8..325579ab 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -5,14 +5,7 @@ """ from functools import partial -from typing import Any, Dict, List, Optional, Tuple, Sequence, Union - -try: - from typing import Literal # py>=3.8 -except ImportError: - from typing_extensions import Literal # py<3.8 - -SAMPLE_FORMAT = Literal["sample_bin", "count_vector", "count_dict_bin"] +from typing import Any, Dict, List, Optional, Tuple, Sequence, Union, Literal import numpy as np import tensornetwork as tn @@ -25,6 +18,7 @@ Tensor = Any +SAMPLE_FORMAT = Literal["sample_bin", "count_vector", "count_dict_bin"] class QuditCircuit: diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 600b970b..7cc4dbcc 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -97,7 +97,7 @@ def _cached_matrix( def _is_prime(n: int) -> bool: - """ Check if `n` is a prime number. """ + """Check if `n` is a prime number.""" if n < 2: return False if n in (2, 3, 5, 7): @@ -113,7 +113,7 @@ def _is_prime(n: int) -> bool: def _i_matrix_func(d: int) -> Tensor: - """ identity matrix function. """ + """identity matrix function.""" matrix = np.zeros((d, d), dtype=npdtype) for i in range(d): matrix[i, i] = 1.0 From 96a55b272ef69b22e0f84ffca31799a3bd0a240e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 12:09:46 +0800 Subject: [PATCH 19/64] formatted codes --- tests/test_quditgates.py | 147 ++++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 17a1f6b8..edf897a7 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -3,10 +3,25 @@ import pytest from tensorcircuit.quditgates import ( - _i_matrix_func, _x_matrix_func, _z_matrix_func, _y_matrix_func, _h_matrix_func, - _s_matrix_func, _rx_matrix_func, _ry_matrix_func, _rz_matrix_func, _swap_matrix_func, - _rzz_matrix_func, _rxx_matrix_func, _u8_matrix_func, _cphase_matrix_func, - _csum_matrix_func, _cached_matrix, SINGLE_BUILDERS, TWO_BUILDERS, npdtype + _i_matrix_func, + _x_matrix_func, + _z_matrix_func, + _y_matrix_func, + _h_matrix_func, + _s_matrix_func, + _rx_matrix_func, + _ry_matrix_func, + _rz_matrix_func, + _swap_matrix_func, + _rzz_matrix_func, + _rxx_matrix_func, + _u8_matrix_func, + _cphase_matrix_func, + _csum_matrix_func, + _cached_matrix, + SINGLE_BUILDERS, + TWO_BUILDERS, + npdtype, ) if npdtype in (np.complex64, np.float32): @@ -16,14 +31,17 @@ ATOL = 1e-12 RTOL = 1e-12 + def is_unitary(M, atol=None, rtol=None): if atol is None or rtol is None: atol = ATOL rtol = RTOL Mc = M.astype(np.complex128, copy=False) I = np.eye(M.shape[0], dtype=np.complex128) - return (np.allclose(Mc.conj().T @ Mc, I, atol=atol, rtol=rtol) and - np.allclose(Mc @ Mc.conj().T, I, atol=atol, rtol=rtol)) + return np.allclose(Mc.conj().T @ Mc, I, atol=atol, rtol=rtol) and np.allclose( + Mc @ Mc.conj().T, I, atol=atol, rtol=rtol + ) + @pytest.mark.parametrize("d", [2, 3, 4, 5]) def test_I_X_Z_shapes_and_unitarity(d): @@ -35,22 +53,27 @@ def test_I_X_Z_shapes_and_unitarity(d): assert is_unitary(Z) assert np.allclose(I, np.eye(d, dtype=npdtype), atol=ATOL) + @pytest.mark.parametrize("d", [2, 3, 4]) def test_X_is_right_cyclic_shift(d): X = _x_matrix_func(d) for j in range(d): - v = np.zeros(d, dtype=npdtype); v[j] = 1 + v = np.zeros(d, dtype=npdtype) + v[j] = 1 out = X @ v - expected = np.zeros(d, dtype=npdtype); expected[(j+1) % d] = 1 + expected = np.zeros(d, dtype=npdtype) + expected[(j + 1) % d] = 1 assert np.allclose(out, expected, atol=ATOL) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_Z_diagonal_and_value(d): - omega = np.exp(2j*np.pi/d) + omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) assert np.allclose(Z, np.diag([omega**j for j in range(d)]), atol=ATOL) assert is_unitary(Z) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_Y_equals_ZX_over_i(d): Y = _y_matrix_func(d) @@ -58,20 +81,27 @@ def test_Y_equals_ZX_over_i(d): assert np.allclose(Y, ZX_over_i, atol=ATOL) assert is_unitary(Y) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_H_is_fourier_like_and_unitary(d): H = _h_matrix_func(d) assert H.shape == (d, d) assert is_unitary(H) - omega = np.exp(2j*np.pi/d) - F = (1/np.sqrt(d)) * np.array([[omega**(j*k) for k in range(d)] for j in range(d)], dtype=npdtype).T - assert np.allclose(H.astype(np.complex128), F.astype(np.complex128), atol=ATOL, rtol=RTOL) + omega = np.exp(2j * np.pi / d) + F = (1 / np.sqrt(d)) * np.array( + [[omega ** (j * k) for k in range(d)] for j in range(d)], dtype=npdtype + ).T + assert np.allclose( + H.astype(np.complex128), F.astype(np.complex128), atol=ATOL, rtol=RTOL + ) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_S_is_diagonal(d): S = _s_matrix_func(d) assert np.allclose(S, np.diag(np.diag(S)), atol=ATOL) + @pytest.mark.parametrize("d", [3, 5]) def test_RX_RY_only_affect_subspace(d): theta = 0.7 @@ -81,31 +111,39 @@ def test_RX_RY_only_affect_subspace(d): assert is_unitary(RX) and is_unitary(RY) for t in range(d): if t not in (j, k): - e = np.zeros(d, dtype=npdtype); e[t]=1 - outx = RX @ e; outy = RY @ e + e = np.zeros(d, dtype=npdtype) + e[t] = 1 + outx = RX @ e + outy = RY @ e assert np.allclose(outx, e, atol=ATOL) assert np.allclose(outy, e, atol=ATOL) + def test_RZ_phase_on_single_level(): d, theta, j = 5, 1.234, 2 RZ = _rz_matrix_func(d, theta, j) assert is_unitary(RZ) - diag = np.ones(d, dtype=npdtype); diag[j] = np.exp(1j*theta) + diag = np.ones(d, dtype=npdtype) + diag[j] = np.exp(1j * theta) assert np.allclose(RZ, np.diag(diag), atol=ATOL) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_SWAP_permutation(d): SW = _swap_matrix_func(d) - D = d*d + D = d * d assert SW.shape == (D, D) assert is_unitary(SW) - for i in range(min(d,3)): - for j in range(min(d,3)): - v = np.zeros(D, dtype=npdtype); v[i*d + j] = 1 + for i in range(min(d, 3)): + for j in range(min(d, 3)): + v = np.zeros(D, dtype=npdtype) + v[i * d + j] = 1 out = SW @ v - exp = np.zeros(D, dtype=npdtype); exp[j*d + i] = 1 + exp = np.zeros(D, dtype=npdtype) + exp[j * d + i] = 1 assert np.allclose(out, exp, atol=ATOL) + @pytest.mark.parametrize("d", [2, 3, 5]) def test_RZZ_diagonal(d): theta = 0.37 @@ -113,30 +151,33 @@ def test_RZZ_diagonal(d): assert is_unitary(RZZ) assert np.allclose(RZZ, np.diag(np.diag(RZZ)), atol=ATOL) # 对角阵 + def test_RXX_selected_block(): - d = 4; theta = 0.81 - j1,k1 = 0,2 - j2,k2 = 1,3 - RXX = _rxx_matrix_func(d, theta, j1,k1,j2,k2) + d = 4 + theta = 0.81 + j1, k1 = 0, 2 + j2, k2 = 1, 3 + RXX = _rxx_matrix_func(d, theta, j1, k1, j2, k2) assert is_unitary(RXX) - D = d*d + D = d * d I = np.eye(D, dtype=npdtype) - idx_a = j1*d + j2 - idx_b = k1*d + k2 + idx_a = j1 * d + j2 + idx_b = k1 * d + k2 for t in range(D): for s in range(D): - if {t,s} & {idx_a, idx_b}: + if {t, s} & {idx_a, idx_b}: continue assert np.isclose(RXX[t, s], I[t, s], atol=ATOL, rtol=RTOL) + @pytest.mark.parametrize("d", [3, 5]) def test_CPHASE_blocks(d): - omega = np.exp(2j*np.pi/d) + omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) M = _cphase_matrix_func(d, cv=None, omega=omega) for a in range(d): - rs = a*d - block = M[rs:rs+d, rs:rs+d] + rs = a * d + block = M[rs : rs + d, rs : rs + d] Za = np.linalg.matrix_power(Z, a) assert np.allclose(block, Za, atol=ATOL) assert is_unitary(M) @@ -144,20 +185,21 @@ def test_CPHASE_blocks(d): cv = 1 M2 = _cphase_matrix_func(d, cv=cv, omega=omega) for a in range(d): - rs = a*d - block = M2[rs:rs+d, rs:rs+d] + rs = a * d + block = M2[rs : rs + d, rs : rs + d] if a == cv: assert np.allclose(block, Z, atol=ATOL) else: assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + @pytest.mark.parametrize("d", [3, 5]) def test_CSUM_blocks(d): X = _x_matrix_func(d) M = _csum_matrix_func(d, cv=None) for a in range(d): - rs = a*d - block = M[rs:rs+d, rs:rs+d] + rs = a * d + block = M[rs : rs + d, rs : rs + d] Xa = np.linalg.matrix_power(X, a) assert np.allclose(block, Xa, atol=ATOL) assert is_unitary(M) @@ -165,23 +207,27 @@ def test_CSUM_blocks(d): cv = 2 % d M2 = _csum_matrix_func(d, cv=cv) for a in range(d): - rs = a*d - block = M2[rs:rs+d, rs:rs+d] + rs = a * d + block = M2[rs : rs + d, rs : rs + d] if a == cv: assert np.allclose(block, X, atol=ATOL) else: assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + def test_CSUM_mapping_small_d(): d = 3 M = _csum_matrix_func(d) for r in range(d): for s in range(d): - v = np.zeros(d*d, dtype=npdtype); v[r*d + s] = 1 + v = np.zeros(d * d, dtype=npdtype) + v[r * d + s] = 1 out = M @ v - exp = np.zeros(d*d, dtype=npdtype); exp[r*d + ((r+s)%d)] = 1 + exp = np.zeros(d * d, dtype=npdtype) + exp[r * d + ((r + s) % d)] = 1 assert np.allclose(out, exp, atol=ATOL) + def test_rotation_index_errors(): d = 4 with pytest.raises(ValueError): @@ -198,10 +244,11 @@ def test_U8_errors_and_values(): _u8_matrix_func(d=3, gamma=0.0) d = 3 U = _u8_matrix_func(d, gamma=2.0, z=1.0, eps=0.0) - omega = np.exp(2j*np.pi/d) + omega = np.exp(2j * np.pi / d) expected = np.diag([omega**0, omega**1, omega**8]) assert np.allclose(U, expected, atol=ATOL) + def test_CPHASE_CSUM_cv_range(): d = 5 with pytest.raises(ValueError): @@ -213,29 +260,35 @@ def test_CPHASE_CSUM_cv_range(): with pytest.raises(ValueError): _csum_matrix_func(d, cv=d) + def test_cached_matrix_identity_and_x(): A1 = _cached_matrix("single", "I", d=3, omega=None, key=()) A2 = _cached_matrix("single", "I", d=3, omega=None, key=()) assert A1 is A2 - X1 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.1,0,1)) - X2 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.2,0,1)) + X1 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.1, 0, 1)) + X2 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.2, 0, 1)) assert X1 is not X2 - assert X1.shape == (3,3) and X2.shape == (3,3) + assert X1.shape == (3, 3) and X2.shape == (3, 3) + def test_builders_smoke(): d = 3 for name, (sig, _) in SINGLE_BUILDERS.items(): defaults = { - "theta": 0.1, "gamma": 0.1, "z": 0.1, "eps": 0.1, - "j": 0, "k": 1, # ensure j != k when present + "theta": 0.1, + "gamma": 0.1, + "z": 0.1, + "eps": 0.1, + "j": 0, + "k": 1, # ensure j != k when present } key = tuple(defaults.get(s, 0) for s in sig) M = _cached_matrix("single", name, d, None, key) assert M.shape == (d, d) for name, (sig, _) in TWO_BUILDERS.items(): - defaults = {"theta":0.1, "j1":0, "k1":1, "j2":0, "k2":1, "cv":None} + defaults = {"theta": 0.1, "j1": 0, "k1": 1, "j2": 0, "k2": 1, "cv": None} key = tuple(defaults[s] for s in sig) M = _cached_matrix("two", name, d, None, key) - assert M.shape == (d*d, d*d) \ No newline at end of file + assert M.shape == (d * d, d * d) From 9dbabaa42630282ec593c14f2b393350ec76efe8 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 12:24:32 +0800 Subject: [PATCH 20/64] black --- tests/test_quditgates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index edf897a7..bf380918 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -237,6 +237,7 @@ def test_rotation_index_errors(): with pytest.raises(ValueError): _rx_matrix_func(d, 0.1, j=2, k=2) + def test_U8_errors_and_values(): with pytest.raises(ValueError): _u8_matrix_func(d=4) From 35288d2218c5de3500feb4a465d397691ed9644a Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:29:04 +0800 Subject: [PATCH 21/64] reform doc --- tensorcircuit/quditcircuit.py | 156 +++++++------ tensorcircuit/quditgates.py | 409 +++++++++++++++++----------------- 2 files changed, 289 insertions(+), 276 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 325579ab..689a3783 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -86,22 +86,17 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: """ Apply a quantum gate (unitary) to one or two qudits in the circuit. - The gate matrix is looked up by name in either `SINGLE_BUILDERS` (for - single-qudit gates) or `TWO_BUILDERS` (for two-qudit gates). The matrix - is built (and cached) via `_cached_matrix`, then applied to the circuit - at the given indices. - - Args: - *indices: The qudit indices the gate should act on. - - One index → single-qudit gate. - - Two indices → two-qudit gate. - name: The name of the gate (must exist in the chosen builder set). - **kwargs: Extra parameters for the gate. These are matched against - the gate’s signature from the builder definition. - - Raises: - ValueError: If `name` is not found, or if the number of indices - does not match the gate type (single vs two). + The gate matrix is looked up by name in either ``SINGLE_BUILDERS`` (for single-qudit gates) or ``TWO_BUILDERS`` (for two-qudit gates). The matrix is built (and cached) via ``_cached_matrix``, then applied to the circuit at the given indices. + + :param indices: The qudit indices the gate should act on. + - One index → single-qudit gate. + - Two indices → two-qudit gate. + :type indices: int + :param name: The name of the gate (must exist in the chosen builder set). + :type name: str + :param kwargs: Extra parameters for the gate matched against the builder signature. + :type kwargs: Any + :raises ValueError: If ``name`` is not found, or if the number of indices does not match the gate type (single vs two). """ if len(indices) == 1 and name in SINGLE_BUILDERS: sig, _ = SINGLE_BUILDERS[name] @@ -126,10 +121,12 @@ def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: """ Apply a quantum gate (unitary) to one or two qudits in the circuit. - Args: - *indices: The qudit indices the gate should act on. - unitary: The unitary to apply to the qudit. - name: The name of the gate. + :param indices: The qudit indices the gate should act on. + :type indices: int + :param unitary: The unitary matrix to apply to the qudit(s). + :type unitary: Tensor + :param name: The name to record for this gate. + :type name: str """ self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) # type: ignore @@ -139,8 +136,8 @@ def i(self, index: int) -> None: """ Apply the identity (I) gate on the given qudit index. - Args: - index: Qudit index to apply the gate on. + :param index: Qudit index to apply the gate on. + :type index: int """ self._apply_gate(index, name="I") @@ -148,8 +145,8 @@ def x(self, index: int) -> None: """ Apply the X gate on the given qudit index. - Args: - index: Qudit index to apply the gate on. + :param index: Qudit index to apply the gate on. + :type index: int """ self._apply_gate(index, name="X") @@ -157,8 +154,8 @@ def y(self, index: int) -> None: """ Apply the Y gate on the given qudit index. - Args: - index: Qudit index to apply the gate on. + :param index: Qudit index to apply the gate on. + :type index: int """ self._apply_gate(index, name="Y") @@ -166,8 +163,8 @@ def z(self, index: int) -> None: """ Apply the Z gate on the given qudit index. - Args: - index: Qudit index to apply the gate on. + :param index: Qudit index to apply the gate on. + :type index: int """ self._apply_gate(index, name="Z") @@ -175,8 +172,8 @@ def h(self, index: int) -> None: """ Apply the Hadamard-like (H) gate on the given qudit index. - Args: - index: Qudit index to apply the gate on. + :param index: Qudit index to apply the gate on. + :type index: int """ self._apply_gate(index, name="H") @@ -186,46 +183,57 @@ def u8( """ Apply the U8 parameterized single-qudit gate. - Args: - index: Qudit index to apply the gate on. - gamma: Gate parameter gamma (default 2.0). - z: Gate parameter z (default 1.0). - eps: Gate parameter eps (default 0.0). + :param index: Qudit index to apply the gate on. + :type index: int + :param gamma: Gate parameter ``gamma``. + :type gamma: float + :param z: Gate parameter ``z``. + :type z: float + :param eps: Gate parameter ``eps``. + :type eps: float """ self._apply_gate(index, name="U8", extra=(gamma, z, eps)) def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ - Apply the single-qudit RX rotation on `index`. + Apply the single-qudit RX rotation on ``index``. - Args: - index: Qudit index to apply the gate on. - theta: Rotation angle. - j: Source level of the rotation subspace (default 0). - k: Target level of the rotation subspace (default 1). + :param index: Qudit index to apply the gate on. + :type index: int + :param theta: Rotation angle. + :type theta: float + :param j: Source level of the rotation subspace. + :type j: int + :param k: Target level of the rotation subspace. + :type k: int """ self._apply_gate(index, name="RX", theta=theta, j=j, k=k) def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ - Apply the single-qudit RY rotation on `index`. + Apply the single-qudit RY rotation on ``index``. - Args: - index: Qudit index to apply the gate on. - theta: Rotation angle. - j: Source level of the rotation subspace (default 0). - k: Target level of the rotation subspace (default 1). + :param index: Qudit index to apply the gate on. + :type index: int + :param theta: Rotation angle. + :type theta: float + :param j: Source level of the rotation subspace. + :type j: int + :param k: Target level of the rotation subspace. + :type k: int """ self._apply_gate(index, name="RY", theta=theta, j=j, k=k) def rz(self, index: int, theta: float, j: int = 0) -> None: """ - Apply the single-qudit RZ rotation on `index`. + Apply the single-qudit RZ rotation on ``index``. - Args: - index: Qudit index to apply the gate on. - theta: Rotation angle around Z. - j: Level where the phase rotation is applied (default 0). + :param index: Qudit index to apply the gate on. + :type index: int + :param theta: Rotation angle around Z. + :type theta: float + :param j: Level where the phase rotation is applied. + :type j: int """ self._apply_gate(index, name="RZ", theta=theta, j=j) @@ -241,13 +249,18 @@ def rxx( """ Apply a two-qudit RXX-type interaction on the given indices. - Args: - *indices: Two qudit indices. - theta: Interaction strength/angle. - j1: Source level of the first qudit subspace (default 0). - k1: Target level of the first qudit subspace (default 1). - j2: Source level of the second qudit subspace (default 0). - k2: Target level of the second qudit subspace (default 1). + :param indices: Two qudit indices. + :type indices: int + :param theta: Interaction strength/angle. + :type theta: float + :param j1: Source level of the first qudit subspace. + :type j1: int + :param k1: Target level of the first qudit subspace. + :type k1: int + :param j2: Source level of the second qudit subspace. + :type j2: int + :param k2: Target level of the second qudit subspace. + :type k2: int """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) @@ -255,9 +268,10 @@ def rzz(self, *indices: int, theta: float) -> None: """ Apply a two-qudit RZZ interaction on the given indices. - Args: - *indices: Two qudit indices. - theta: Interaction angle. + :param indices: Two qudit indices. + :type indices: int + :param theta: Interaction angle. + :type theta: float """ self._apply_gate(*indices, name="RZZ", theta=theta) @@ -265,9 +279,10 @@ def cphase(self, *indices: int, cv: Optional[int] = None) -> None: """ Apply a controlled phase (CPHASE) gate. - Args: - *indices: Two qudit indices (control, target). - cv: Optional control value. If None, default cv=1. + :param indices: Two qudit indices (control, target). + :type indices: int + :param cv: Optional control value. If ``None``, defaults to ``1``. + :type cv: Optional[int] """ self._apply_gate(*indices, name="CPHASE", cv=cv) @@ -275,9 +290,10 @@ def csum(self, *indices: int, cv: Optional[int] = None) -> None: """ Apply a controlled-sum (CSUM) gate. - Args: - *indices: Two qudit indices (control, target). - cv: Optional control value. If None, default cv=1. + :param indices: Two qudit indices (control, target). + :type indices: int + :param cv: Optional control value. If ``None``, defaults to ``1``. + :type cv: Optional[int] """ self._apply_gate(*indices, name="CSUM", cv=cv) @@ -383,9 +399,9 @@ def amplitude(self, l: Union[str, Tensor]) -> Tensor: def probability(self) -> Tensor: """ - get the d^n length probability vector over computational basis + Get the ``d^n`` length probability vector over the computational basis. - :return: probability vector of shape [dim**n] + :return: Probability vector of shape ``[dim**n]``. :rtype: Tensor """ return self._circ.probability() diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 7cc4dbcc..906123b6 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -59,31 +59,25 @@ def _cached_matrix( """ Build and cache a matrix using a registered builder function. - This function looks up a builder from either `SINGLE_BUILDERS` or - `TWO_BUILDERS` (depending on `kind`), and calls it with the given - arguments. Results are cached with `functools.lru_cache`, so repeated - calls with the same inputs return the cached tensor instead of - rebuilding it. - - Args: - kind: Either `"single"` (use `SINGLE_BUILDERS`) or `"two"` (use `TWO_BUILDERS`). - name: The builder name to look up in the chosen dictionary. - d: The dimension of the matrix. - omega: Optional frequency or scaling parameter, passed to the builder. - key: Tuple of extra parameters, matched in order to the builder’s - expected signature. - - Returns: - Tensor: The matrix built by the selected builder. - - Notes: - - The cache key depends on all arguments (`kind`, `name`, `d`, `omega`, `key`). - - The `key` tuple must have the same order as the builder’s signature. - - The same inputs will always return the same cached tensor. - - Raises: - KeyError: If the builder `name` is not found. - TypeError/ValueError: If `key` does not match the builder’s expected parameters. + Looks up a builder in ``SINGLE_BUILDERS`` (for single–qudit gates) or + ``TWO_BUILDERS`` (for two–qudit gates) according to ``kind``, constructs the + matrix, and caches the result via ``functools.lru_cache``. + + :param kind: Either ``"single"`` (use ``SINGLE_BUILDERS``) or ``"two"`` (use ``TWO_BUILDERS``). + :type kind: str + :param name: Builder name to look up in the chosen dictionary. + :type name: str + :param d: Dimension of the (sub)system. + :type d: int + :param omega: Optional frequency/scaling parameter passed to the builder. + :type omega: Optional[float] + :param key: Tuple of extra parameters matched positionally to the builder's signature. + :type key: Optional[tuple[Any, ...]] + :return: Matrix built by the selected builder. + :rtype: Tensor + :raises KeyError: If the builder ``name`` is not found. + :raises TypeError: If ``key`` does not match the builder’s expected parameters. + :raises ValueError: If ``key`` does not match the builder’s expected parameters. """ builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS try: @@ -97,7 +91,14 @@ def _cached_matrix( def _is_prime(n: int) -> bool: - """Check if `n` is a prime number.""" + """ + Check whether a number is prime. + + :param n: Integer to test. + :type n: int + :return: ``True`` if ``n`` is prime, else ``False``. + :rtype: bool + """ if n < 2: return False if n in (2, 3, 5, 7): @@ -113,7 +114,14 @@ def _is_prime(n: int) -> bool: def _i_matrix_func(d: int) -> Tensor: - """identity matrix function.""" + """ + Identity matrix of size ``d``. + + :param d: Qudit dimension. + :type d: int + :return: ``(d, d)`` identity matrix. + :rtype: Tensor + """ matrix = np.zeros((d, d), dtype=npdtype) for i in range(d): matrix[i, i] = 1.0 @@ -122,7 +130,14 @@ def _i_matrix_func(d: int) -> Tensor: def _x_matrix_func(d: int) -> Tensor: r""" - X_d\ket{j} = \ket{(j + 1) mod d} + Generalized Pauli-X on a ``d``-level system. + + .. math:: X_d\lvert j \rangle = \lvert (j+1) \bmod d \rangle + + :param d: Qudit dimension. + :type d: int + :return: ``(d, d)`` matrix for :math:`X_d`. + :rtype: Tensor """ matrix = np.zeros((d, d), dtype=npdtype) for j in range(d): @@ -132,7 +147,16 @@ def _x_matrix_func(d: int) -> Tensor: def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: r""" - Z_d\ket{j} = \omega^{j}\ket{j} + Generalized Pauli-Z on a ``d``-level system. + + .. math:: Z_d\lvert j \rangle = \omega^{j}\lvert j \rangle,\quad \omega=e^{2\pi i/d} + + :param d: Qudit dimension. + :type d: int + :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. + :type omega: Optional[float] + :return: ``(d, d)`` matrix for :math:`Z_d`. + :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega matrix = np.zeros((d, d), dtype=npdtype) @@ -143,23 +167,32 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: r""" - Generalized Pauli-Y (Y) Gate for qudits. - - The Y gate represents a combination of the X and Z gates, generalizing the Pauli-Y gate - from qubits to higher dimensions. It is defined as - - .. math:: + Generalized Pauli-Y (Y) gate for qudits. - Y = \frac{1}{i}\, Z \cdot X, + Defined (up to a global phase) via :math:`Y \propto Z\,X`. - where the generalized Pauli-X and Pauli-Z gates are applied to the target qudits. + :param d: Qudit dimension. + :type d: int + :param omega: Optional primitive ``d``-th root of unity used by ``Z``. + :type omega: Optional[float] + :return: ``(d, d)`` matrix for :math:`Y`. + :rtype: Tensor """ return np.matmul(_z_matrix_func(d, omega=omega), _x_matrix_func(d)) / 1j def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: r""" - H_d\ket{j} = \frac{1}{\sqrt{d}}\sum_{k=0}^{d-1}\omega^{jk}\ket{k} + Discrete Fourier transform (Hadamard-like) on ``d`` levels. + + .. math:: H_d\lvert j \rangle = \frac{1}{\sqrt{d}} \sum_{k=0}^{d-1} \omega^{jk}\lvert k \rangle + + :param d: Qudit dimension. + :type d: int + :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. + :type omega: Optional[float] + :return: ``(d, d)`` matrix for :math:`H_d`. + :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega matrix = np.zeros((d, d), dtype=npdtype) @@ -171,7 +204,16 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: r""" - S_d\ket{j} = \omega^{j(j + p_d) / 2}\ket{j} + Diagonal phase gate ``S_d`` on ``d`` levels. + + .. math:: S_d\lvert j \rangle = \omega^{j(j+p_d)/2}\lvert j \rangle,\quad p_d = (d \bmod 2) + + :param d: Qudit dimension. + :type d: int + :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. + :type omega: Optional[float] + :return: ``(d, d)`` diagonal matrix for :math:`S_d`. + :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega _pd = 0 if d % 2 == 0 else 1 @@ -184,7 +226,15 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: def _check_rotation(d: int, j: int, k: int) -> None: """ - Check rotation of qudit `j` in `d` qubits. + Validate rotation subspace indices for a ``d``-level system. + + :param d: Qudit dimension. + :type d: int + :param j: First level index. + :type j: int + :param k: Second level index. + :type k: int + :raises ValueError: If indices are out of range or if ``j == k``. """ if not (0 <= j < d) or not (0 <= k < d): raise ValueError(f"Indices j={j}, k={k} must satisfy 0 <= j,k < d (d={d}).") @@ -194,30 +244,20 @@ def _check_rotation(d: int, j: int, k: int) -> None: def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: r""" - Rotation-X (RX) Gate for qudits. - - The RX gate represents a rotation about the X-axis of the Bloch sphere in a qudit system. - For a qubit (2-level system), the matrix representation is given by - - .. math:: - - RX(\theta) = - \begin{pmatrix} - \cos(\theta/2) & -i\sin(\theta/2) \\ - -i\sin(\theta/2) & \cos(\theta/2) - \end{pmatrix} - - For higher-dimensional qudits, the RX gate affects only the specified two levels (indexed by - \(j\) and \(k\)), leaving all other levels unchanged. - - Args: - d (int): Dimension of the qudit Hilbert space. - theta (float): Rotation angle θ. - j (int): First level index (default 0). - k (int): Second level index (default 1). - - Returns: - Tensor: A (d x d) numpy array of dtype `npdtype` representing the RX gate. + Rotation-X (``RX``) gate on a selected two-level subspace of a qudit. + + Acts like the qubit :math:`RX(\theta)` on levels ``j`` and ``k``, identity elsewhere. + + :param d: Qudit dimension. + :type d: int + :param theta: Rotation angle :math:`\theta`. + :type theta: float + :param j: First level index. + :type j: int + :param k: Second level index. + :type k: int + :return: ``(d, d)`` matrix for :math:`RX(\theta)` on the ``j,k`` subspace. + :rtype: Tensor """ _check_rotation(d, j, k) matrix = np.eye(d, dtype=npdtype) @@ -231,27 +271,18 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: r""" - Rotation-Y (RY) Gate for qudits. - - Acts as a standard qubit RY(θ) on the two-level subspace spanned by |j> and |k>, - and as identity on all other levels: - - .. math:: - - RY(\theta) = - \begin{pmatrix} - \cos(\theta/2) & -\sin(\theta/2) \\ - \sin(\theta/2) & \cos(\theta/2) - \end{pmatrix} - - Args: - d (int): Dimension of the qudit Hilbert space. - theta (float): Rotation angle θ. - j (int): First level index (default 0). - k (int): Second level index (default 1). - - Returns: - Tensor: A (d x d) numpy array of dtype `npdtype` representing the RY gate. + Rotation-Y (``RY``) gate on a selected two-level subspace of a qudit. + + :param d: Qudit dimension. + :type d: int + :param theta: Rotation angle :math:`\theta`. + :type theta: float + :param j: First level index. + :type j: int + :param k: Second level index. + :type k: int + :return: ``(d, d)`` matrix for :math:`RY(\theta)` on the ``j,k`` subspace. + :rtype: Tensor """ _check_rotation(d, j, k) matrix = np.eye(d, dtype=npdtype) @@ -265,27 +296,19 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: r""" - Rotation-Z (RZ) Gate for qudits. - - .. math:: - - RZ(\theta) = - \begin{pmatrix} - e^{-i\theta/2} & 0 \\ - 0 & e^{i\theta/2} - \end{pmatrix} - - For qudits (d >= 2), apply a phase e^{iθ} only to level |j>, leaving others unchanged: - (RZ_d)_{mm} = e^{iθ} if m == j else 1 - - Args: - d (int): Dimension of the qudit Hilbert space. - theta (float): Rotation angle θ. - j (int): First level index (default 0). - k (int): Second level index (default 1). - - Returns: - Tensor: A (d x d) numpy array of dtype `npdtype` representing the RZ gate. + Rotation-Z (``RZ``) gate for qudits. + + For qubits it reduces to the usual :math:`RZ(\theta)`. For general ``d``, it + applies a phase :math:`e^{i\theta}` to level ``j`` and leaves others unchanged. + + :param d: Qudit dimension. + :type d: int + :param theta: Rotation angle :math:`\theta`. + :type theta: float + :param j: Level index receiving the phase. + :type j: int + :return: ``(d, d)`` diagonal matrix implementing :math:`RZ(\theta)` on level ``j``. + :rtype: Tensor """ matrix = np.eye(d, dtype=npdtype) matrix[j, j] = np.exp(1j * theta) @@ -293,16 +316,15 @@ def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: def _swap_matrix_func(d: int) -> Tensor: - r""" - SWAP gate for two qudits of dimensions d. - - Exchanges the states |i⟩|j⟩ -> |j⟩|i⟩. + """ + SWAP gate for two qudits of dimension ``d``. - Args: - d (int): Dimension of the qudit. + Exchanges basis states ``|i⟩|j⟩ → |j⟩|i⟩``. - Returns: - Tensor: A numpy array representing the SWAP gate. + :param d: Qudit dimension (for each register). + :type d: int + :return: ``(d*d, d*d)`` matrix representing SWAP. + :rtype: Tensor """ D = d * d matrix = np.zeros((D, D), dtype=npdtype) @@ -316,22 +338,16 @@ def _swap_matrix_func(d: int) -> Tensor: def _rzz_matrix_func(d: int, theta: float) -> Tensor: r""" - Two-qudit RZZ(\theta) gate for qudits. - - .. math:: - - Z_H = \mathrm{diag}(d-1,\, d-3,\, \ldots,\,-(d-1)) - .. math:: - RZZ(\theta) = \exp\!\left(-i \tfrac{\theta}{2} \, \bigl(Z_H \otimes Z_H\bigr)\right) + Two-qudit ``RZZ(\theta)`` interaction for qudits. - For :math:`d=2`, this reduces to the standard qubit RZZ gate. + .. math:: RZZ(\theta) = \exp\!\left(-i \tfrac{\theta}{2} (Z_H \otimes Z_H)\right) - Args: - d (int): Dimension of the qudits (assumed equal for both). - theta (float): Rotation angle. - - Returns: - Tensor: A ``(d*d, d*d)`` numpy array representing the RZZ gate. + :param d: Dimension of each qudit (assumed equal). + :type d: int + :param theta: Rotation angle. + :type theta: float + :return: ``(d*d, d*d)`` matrix representing :math:`RZZ(\theta)`. + :rtype: Tensor """ lam = np.array( [d - 1 - 2 * j for j in range(d)], dtype=float @@ -350,27 +366,24 @@ def _rxx_matrix_func( d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 ) -> Tensor: r""" - Two-qudit RXX(θ) on a selected two-state subspace. - - Acts like a qubit RXX on the subspace spanned by |j1, j2> and |k1, k2>: - - .. math:: - - RXX(\theta) = - \begin{pmatrix} - \cos\!\left(\tfrac{\theta}{2}\right) & -i \sin\!\left(\tfrac{\theta}{2}\right) \\ - -i \sin\!\left(\tfrac{\theta}{2}\right) & \cos\!\left(\tfrac{\theta}{2}\right) - \end{pmatrix} - All other basis states are unchanged. - - Args: - d (int): Dimension for both qudits (assumed equal). - theta (float): Rotation angle. - j1, k1 (int): Levels on qudit-1. - j2, k2 (int): Levels on qudit-2. - - Returns: - Tensor: A ``(d*d, d*d)`` numpy array representing the RXX gate. + Two-qudit ``RXX(\theta)`` on a selected two-state subspace. + + Acts like a qubit :math:`RXX` on the subspace spanned by ``|j1, j2⟩`` and ``|k1, k2⟩``. + + :param d: Dimension of each qudit (assumed equal). + :type d: int + :param theta: Rotation angle. + :type theta: float + :param j1: Level on qudit-1. + :type j1: int + :param k1: Level on qudit-1. + :type k1: int + :param j2: Level on qudit-2. + :type j2: int + :param k2: Level on qudit-2. + :type k2: int + :return: ``(d*d, d*d)`` matrix representing :math:`RXX(\theta)` on the selected subspace. + :rtype: Tensor """ D = d * d M = np.eye(D, dtype=npdtype) @@ -399,55 +412,25 @@ def _u8_matrix_func( omega: Optional[float] = None, ) -> Tensor: r""" - U8 diagonal single-qudit gate for prime dimensions. - - This gate is defined only when :math:`d` is prime. It is a diagonal - operator of size :math:`d \times d`: - - .. py:math:: - - U_8(d; \gamma, z, \epsilon) = - \mathrm{diag}\!\left(\omega^{v_0}, \omega^{v_1}, \ldots, \omega^{v_{d-1}}\right), - - where :math:`\omega = e^{2\pi i / d}` is a primitive :math:`d`-th root - of unity, and the exponents :math:`v_k` are computed from modular - polynomials depending on parameters :math:`\gamma, z, \epsilon`. - - For :math:`d=3`, the exponents are fixed as - - .. py:math:: - - (v_0, v_1, v_2) = (0, 1, 8). - - For general prime :math:`d`, the exponents are determined by - - .. py:math:: - - v_i \equiv \tfrac{1}{12} i \bigl(\gamma + i (6z + (2i-3)\gamma)\bigr) + \epsilon i - \pmod d, \quad i = 1, \ldots, d-1, - - with :math:`v_0 = 0`. The sequence :math:`(v_0,\ldots,v_{d-1})` must - also satisfy - - .. py:math:: - - \sum_{k=0}^{d-1} v_k \equiv 0 \pmod d. - - Args: - d: Qudit dimension (must be prime). - gamma: Gate parameter (must be non-zero). - z: Gate parameter. - eps: Gate parameter. - omega: Optional primitive :math:`d`-th root of unity. Defaults to - :math:`\exp(2\pi i / d)`. - - Returns: - Tensor: A :math:`(d, d)` diagonal numpy array of dtype ``npdtype``. - - Raises: - ValueError: If ``d`` is not prime; if ``gamma = 0``; if 12 has no - modular inverse modulo ``d``; or if the computed :math:`v_k` do not - sum to 0 modulo :math:`d`. + ``U8`` diagonal single-qudit gate for prime dimensions. + + Defined for prime ``d`` with phases determined by modular polynomials depending + on parameters :math:`\gamma, z, \epsilon`. + + :param d: Qudit dimension (must be prime). + :type d: int + :param gamma: Gate parameter (must be non-zero). + :type gamma: float + :param z: Gate parameter. + :type z: float + :param eps: Gate parameter. + :type eps: float + :param omega: Optional primitive :math:`d`-th root of unity. Defaults to :math:`e^{2\pi i/d}`. + :type omega: Optional[float] + :return: ``(d, d)`` diagonal matrix of dtype ``npdtype``. + :rtype: Tensor + :raises ValueError: If ``d`` is not prime; if ``gamma==0``; + if 12 has no modular inverse mod ``d``; or if the computed exponents do not sum to 0 mod ``d``. """ if not _is_prime(d): raise ValueError( @@ -489,10 +472,8 @@ def _cphase_matrix_func( d: int, cv: Optional[int] = None, omega: Optional[float] = None ) -> Tensor: r""" - Qudit Controlled-z gate - \ket{r}\ket{s} \rightarrow \omega^{rs}\ket{r}\ket{s} = \ket{r}Z^r\ket{s} - - This gate is also called SUMZ gate, where Z represents Z_d gate. + Qudit controlled-phase (``CPHASE``) gate. + Implements ``|r⟩|s⟩ → ω^{rs}|r⟩|s⟩``; optionally condition on a specific control value ``cv``. ┌─ ─┐ │ I_d 0 0 ... 0 │ │ 0 Z_d 0 ... 0 │ @@ -500,6 +481,16 @@ def _cphase_matrix_func( │ . . . . . │ │ 0 0 0 ... Z_d^{d-1} │ └ ─┘ + + :param d: Qudit dimension (for each register). + :type d: int + :param cv: Optional control value in ``[0, d-1]``. If ``None``, builds the full SUMZ block-diagonal. + :type cv: Optional[int] + :param omega: Optional primitive ``d``-th root of unity for ``Z_d``. + :type omega: Optional[float] + :return: ``(d*d, d*d)`` matrix representing the controlled-phase. + :rtype: Tensor + :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ omega = np.exp(2j * np.pi / d) if omega is None else omega size = d**2 @@ -528,10 +519,8 @@ def _cphase_matrix_func( def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: r""" - Qudit Controlled-NOT gate - \ket{r}\ket{s} \rightarrow \ket{r}\ket{r+s} = \ket{r}X^r\ket{s} = \ket{r}\ket{(r+s) mod d} - - This gate is also called SUMX gate, where X represents X_d gate. + Qudit controlled-sum (``CSUM`` / ``SUMX``) gate. + Implements ``|r⟩|s⟩ → |r⟩|r+s (\bmod d)⟩``; optionally condition on a specific control value ``cv``. ┌─ ─┐ │ I_d 0 0 ... 0 │ │ 0 X_d 0 ... 0 │ @@ -539,6 +528,14 @@ def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: │ . . . . . │ │ 0 0 0 ... X_d^{d-1} │ └ ─┘ + + :param d: Qudit dimension (for each register). + :type d: int + :param cv: Optional control value in ``[0, d-1]``. If ``None``, builds the full SUMX block-diagonal. + :type cv: Optional[int] + :return: ``(d*d, d*d)`` matrix representing the controlled-sum. + :rtype: Tensor + :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ size = d**2 x_matrix = _x_matrix_func(d=d) From a3f21315a765dc121510182c661fc219d071b32f Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:30:05 +0800 Subject: [PATCH 22/64] use np.eye --- tensorcircuit/quditgates.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 906123b6..b0286665 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -122,10 +122,7 @@ def _i_matrix_func(d: int) -> Tensor: :return: ``(d, d)`` identity matrix. :rtype: Tensor """ - matrix = np.zeros((d, d), dtype=npdtype) - for i in range(d): - matrix[i, i] = 1.0 - return matrix + return np.eye(d, dtype=npdtype) def _x_matrix_func(d: int) -> Tensor: From 5278f50cfa28e8f75bbbc66986049717c60f6551 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:31:43 +0800 Subject: [PATCH 23/64] use np.diag --- tensorcircuit/quditgates.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index b0286665..b8a15d3f 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -156,10 +156,7 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega - matrix = np.zeros((d, d), dtype=npdtype) - for j in range(d): - matrix[j, j] = omega**j - return matrix + return np.diag([omega**j for j in range(d)]).astype(npdtype) def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: From 62750f11e43d7ce1c09325321992b8ca3dba17dc Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:33:31 +0800 Subject: [PATCH 24/64] fixed an early notation --- tensorcircuit/quditgates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index b8a15d3f..abcaa041 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -192,8 +192,8 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: matrix = np.zeros((d, d), dtype=npdtype) for j in range(d): for k in range(d): - matrix[j, k] = omega ** (j * k) / np.sqrt(d) - return matrix.T + matrix[k, j] = omega ** (j * k) / np.sqrt(d) + return matrix def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: From 3a265cc6c8d441230d56b95decc1959c9071bcbf Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:34:21 +0800 Subject: [PATCH 25/64] use np.diag --- tensorcircuit/quditgates.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index abcaa041..0e8ae850 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -456,10 +456,7 @@ def _u8_matrix_func( ) omega = np.exp(2j * np.pi / d) if omega is None else omega - matrix = np.zeros((d, d), dtype=npdtype) - for j in range(d): - matrix[j, j] = omega ** vks[j] - return matrix + return np.diag([omega ** vks[j] ** j for j in range(d)]).astype(npdtype) def _cphase_matrix_func( From d88b9a9f253acb71f84e1d2325d745669abb2251 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:37:08 +0800 Subject: [PATCH 26/64] remove useless line --- tests/test_quditgates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index bf380918..810094a6 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -1,4 +1,3 @@ -# tests/test_quditgates.py import numpy as np import pytest From 26dd999584f96bf100663b4d8ba881f0d5bccbf5 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 16:38:12 +0800 Subject: [PATCH 27/64] change atol from 1e-4 to 1e-5 --- tests/test_quditcircuit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index f119185e..ccbdfdbf 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -105,7 +105,7 @@ def test_single_qubit(): * 10 ) / np.sqrt(10), - atol=1e-4, + atol=1e-5, ) @@ -222,7 +222,7 @@ def test_unitary(backend): np.kron(tc.quditgates._x_matrix_func(3), tc.quditgates._z_matrix_func(3)) ) np.testing.assert_allclose( - tc.backend.numpy(c.wavefunction().reshape([9, 9])), answer, atol=1e-4 + tc.backend.numpy(c.wavefunction().reshape([9, 9])), answer, atol=1e-5 ) @@ -241,11 +241,11 @@ def test_circuit_add_demo(): c2.x(0) # |00> -> |20> answer = np.zeros(dim * dim, dtype=np.complex64) answer[dim * 2 + 0] = 1.0 - np.testing.assert_allclose(c2.wavefunction(), answer, atol=1e-4) + np.testing.assert_allclose(c2.wavefunction(), answer, atol=1e-5) c3 = tc.QuditCircuit(2, dim=dim) c3.x(0) c3.replace_mps_inputs(c.quvector()) - np.testing.assert_allclose(c3.wavefunction(), answer, atol=1e-4) + np.testing.assert_allclose(c3.wavefunction(), answer, atol=1e-5) @pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) From 60ce1e7ca0f2447f7940ae0c6d3537c8950fa760 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 17:26:11 +0800 Subject: [PATCH 28/64] bug fixed --- tensorcircuit/quditgates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 0e8ae850..c00c6f34 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -456,7 +456,7 @@ def _u8_matrix_func( ) omega = np.exp(2j * np.pi / d) if omega is None else omega - return np.diag([omega ** vks[j] ** j for j in range(d)]).astype(npdtype) + return np.diag([omega ** vks[j] for j in range(d)]).astype(npdtype) def _cphase_matrix_func( From 8793b218b5212dbda678d768bf2bf82024d62a7f Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 17:54:14 +0800 Subject: [PATCH 29/64] use highp --- tests/test_quditgates.py | 126 +++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 810094a6..0c1c8f61 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -1,5 +1,12 @@ -import numpy as np import pytest +import sys +import os +import numpy as np + +thisfile = os.path.abspath(__file__) +modulepath = os.path.dirname(os.path.dirname(thisfile)) + +sys.path.insert(0, modulepath) from tensorcircuit.quditgates import ( _i_matrix_func, @@ -20,89 +27,78 @@ _cached_matrix, SINGLE_BUILDERS, TWO_BUILDERS, - npdtype, ) -if npdtype in (np.complex64, np.float32): - ATOL = 1e-6 - RTOL = 1e-6 -else: - ATOL = 1e-12 - RTOL = 1e-12 - -def is_unitary(M, atol=None, rtol=None): - if atol is None or rtol is None: - atol = ATOL - rtol = RTOL +def is_unitary(M): Mc = M.astype(np.complex128, copy=False) I = np.eye(M.shape[0], dtype=np.complex128) - return np.allclose(Mc.conj().T @ Mc, I, atol=atol, rtol=rtol) and np.allclose( - Mc @ Mc.conj().T, I, atol=atol, rtol=rtol + return np.allclose(Mc.conj().T @ Mc, I, atol=1e-5, rtol=1e-5) and np.allclose( + Mc @ Mc.conj().T, I, atol=1e-5, rtol=1e-5 ) @pytest.mark.parametrize("d", [2, 3, 4, 5]) -def test_I_X_Z_shapes_and_unitarity(d): +def test_I_X_Z_shapes_and_unitarity(d, highp): I = _i_matrix_func(d) X = _x_matrix_func(d) Z = _z_matrix_func(d) assert I.shape == (d, d) and X.shape == (d, d) and Z.shape == (d, d) assert is_unitary(X) assert is_unitary(Z) - assert np.allclose(I, np.eye(d, dtype=npdtype), atol=ATOL) + np.testing.assert_allclose(I, np.eye(d), atol=1e-5) @pytest.mark.parametrize("d", [2, 3, 4]) -def test_X_is_right_cyclic_shift(d): +def test_X_is_right_cyclic_shift(d, highp): X = _x_matrix_func(d) for j in range(d): - v = np.zeros(d, dtype=npdtype) + v = np.zeros(d) v[j] = 1 out = X @ v - expected = np.zeros(d, dtype=npdtype) + expected = np.zeros(d) expected[(j + 1) % d] = 1 - assert np.allclose(out, expected, atol=ATOL) + np.testing.assert_allclose(out, expected, atol=1e-5) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_Z_diagonal_and_value(d): +def test_Z_diagonal_and_value(d, highp): omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) - assert np.allclose(Z, np.diag([omega**j for j in range(d)]), atol=ATOL) + np.testing.assert_allclose(Z, np.diag([omega**j for j in range(d)]), atol=1e-5) assert is_unitary(Z) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_Y_equals_ZX_over_i(d): +def test_Y_equals_ZX_over_i(d, highp): Y = _y_matrix_func(d) ZX_over_i = (_z_matrix_func(d) @ _x_matrix_func(d)) / 1j - assert np.allclose(Y, ZX_over_i, atol=ATOL) + np.testing.assert_allclose(Y, ZX_over_i, atol=1e-5) assert is_unitary(Y) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_H_is_fourier_like_and_unitary(d): +def test_H_is_fourier_like_and_unitary(d, highp): H = _h_matrix_func(d) assert H.shape == (d, d) assert is_unitary(H) omega = np.exp(2j * np.pi / d) F = (1 / np.sqrt(d)) * np.array( - [[omega ** (j * k) for k in range(d)] for j in range(d)], dtype=npdtype + [[omega ** (j * k) for k in range(d)] for j in range(d)] ).T - assert np.allclose( - H.astype(np.complex128), F.astype(np.complex128), atol=ATOL, rtol=RTOL + np.testing.assert_allclose( + H.astype(np.complex128), F.astype(np.complex128), atol=1e-5, rtol=1e-5 ) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_S_is_diagonal(d): +def test_S_is_diagonal(d, highp): S = _s_matrix_func(d) - assert np.allclose(S, np.diag(np.diag(S)), atol=ATOL) + np.testing.assert_allclose(S, np.diag(np.diag(S)), atol=1e-5) @pytest.mark.parametrize("d", [3, 5]) -def test_RX_RY_only_affect_subspace(d): +def test_RX_RY_only_affect_subspace(d, highp): theta = 0.7 j, k = 0, 1 RX = _rx_matrix_func(d, theta, j, k) @@ -110,48 +106,48 @@ def test_RX_RY_only_affect_subspace(d): assert is_unitary(RX) and is_unitary(RY) for t in range(d): if t not in (j, k): - e = np.zeros(d, dtype=npdtype) + e = np.zeros(d) e[t] = 1 outx = RX @ e outy = RY @ e - assert np.allclose(outx, e, atol=ATOL) - assert np.allclose(outy, e, atol=ATOL) + np.testing.assert_allclose(outx, e, atol=1e-5) + np.testing.assert_allclose(outy, e, atol=1e-5) -def test_RZ_phase_on_single_level(): +def test_RZ_phase_on_single_level(highp): d, theta, j = 5, 1.234, 2 RZ = _rz_matrix_func(d, theta, j) assert is_unitary(RZ) - diag = np.ones(d, dtype=npdtype) + diag = np.ones(d, dtype=np.complex64) diag[j] = np.exp(1j * theta) - assert np.allclose(RZ, np.diag(diag), atol=ATOL) + assert np.allclose(RZ, np.diag(diag), atol=1e-5) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_SWAP_permutation(d): +def test_SWAP_permutation(d, highp): SW = _swap_matrix_func(d) D = d * d assert SW.shape == (D, D) assert is_unitary(SW) for i in range(min(d, 3)): for j in range(min(d, 3)): - v = np.zeros(D, dtype=npdtype) + v = np.zeros(D) v[i * d + j] = 1 out = SW @ v - exp = np.zeros(D, dtype=npdtype) + exp = np.zeros(D) exp[j * d + i] = 1 - assert np.allclose(out, exp, atol=ATOL) + np.testing.assert_allclose(out, exp, atol=1e-5) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_RZZ_diagonal(d): +def test_RZZ_diagonal(d, highp): theta = 0.37 RZZ = _rzz_matrix_func(d, theta) assert is_unitary(RZZ) - assert np.allclose(RZZ, np.diag(np.diag(RZZ)), atol=ATOL) # 对角阵 + np.testing.assert_allclose(RZZ, np.diag(np.diag(RZZ)), atol=1e-5) # 对角阵 -def test_RXX_selected_block(): +def test_RXX_selected_block(highp): d = 4 theta = 0.81 j1, k1 = 0, 2 @@ -159,18 +155,18 @@ def test_RXX_selected_block(): RXX = _rxx_matrix_func(d, theta, j1, k1, j2, k2) assert is_unitary(RXX) D = d * d - I = np.eye(D, dtype=npdtype) + I = np.eye(D) idx_a = j1 * d + j2 idx_b = k1 * d + k2 for t in range(D): for s in range(D): if {t, s} & {idx_a, idx_b}: continue - assert np.isclose(RXX[t, s], I[t, s], atol=ATOL, rtol=RTOL) + np.testing.assert_allclose(RXX[t, s], I[t, s], atol=1e-5, rtol=1e-5) @pytest.mark.parametrize("d", [3, 5]) -def test_CPHASE_blocks(d): +def test_CPHASE_blocks(d, highp): omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) M = _cphase_matrix_func(d, cv=None, omega=omega) @@ -178,7 +174,7 @@ def test_CPHASE_blocks(d): rs = a * d block = M[rs : rs + d, rs : rs + d] Za = np.linalg.matrix_power(Z, a) - assert np.allclose(block, Za, atol=ATOL) + np.testing.assert_allclose(block, Za, atol=1e-5) assert is_unitary(M) cv = 1 @@ -187,20 +183,20 @@ def test_CPHASE_blocks(d): rs = a * d block = M2[rs : rs + d, rs : rs + d] if a == cv: - assert np.allclose(block, Z, atol=ATOL) + np.testing.assert_allclose(block, Z, atol=1e-5) else: - assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + np.testing.assert_allclose(block, np.eye(d), atol=1e-5) @pytest.mark.parametrize("d", [3, 5]) -def test_CSUM_blocks(d): +def test_CSUM_blocks(d, highp): X = _x_matrix_func(d) M = _csum_matrix_func(d, cv=None) for a in range(d): rs = a * d block = M[rs : rs + d, rs : rs + d] Xa = np.linalg.matrix_power(X, a) - assert np.allclose(block, Xa, atol=ATOL) + np.testing.assert_allclose(block, Xa, atol=1e-5) assert is_unitary(M) cv = 2 % d @@ -209,25 +205,25 @@ def test_CSUM_blocks(d): rs = a * d block = M2[rs : rs + d, rs : rs + d] if a == cv: - assert np.allclose(block, X, atol=ATOL) + np.testing.assert_allclose(block, X, atol=1e-5) else: - assert np.allclose(block, np.eye(d, dtype=npdtype), atol=ATOL) + np.testing.assert_allclose(block, np.eye(d), atol=1e-5) -def test_CSUM_mapping_small_d(): +def test_CSUM_mapping_small_d(highp): d = 3 M = _csum_matrix_func(d) for r in range(d): for s in range(d): - v = np.zeros(d * d, dtype=npdtype) + v = np.zeros(d * d) v[r * d + s] = 1 out = M @ v - exp = np.zeros(d * d, dtype=npdtype) + exp = np.zeros(d * d) exp[r * d + ((r + s) % d)] = 1 - assert np.allclose(out, exp, atol=ATOL) + np.testing.assert_allclose(out, exp, atol=1e-5) -def test_rotation_index_errors(): +def test_rotation_index_errors(highp): d = 4 with pytest.raises(ValueError): _rx_matrix_func(d, 0.1, j=-1, k=1) @@ -237,7 +233,7 @@ def test_rotation_index_errors(): _rx_matrix_func(d, 0.1, j=2, k=2) -def test_U8_errors_and_values(): +def test_U8_errors_and_values(highp): with pytest.raises(ValueError): _u8_matrix_func(d=4) with pytest.raises(ValueError): @@ -246,10 +242,10 @@ def test_U8_errors_and_values(): U = _u8_matrix_func(d, gamma=2.0, z=1.0, eps=0.0) omega = np.exp(2j * np.pi / d) expected = np.diag([omega**0, omega**1, omega**8]) - assert np.allclose(U, expected, atol=ATOL) + assert np.allclose(U, expected, atol=1e-5) -def test_CPHASE_CSUM_cv_range(): +def test_CPHASE_CSUM_cv_range(highp): d = 5 with pytest.raises(ValueError): _cphase_matrix_func(d, cv=-1) @@ -261,7 +257,7 @@ def test_CPHASE_CSUM_cv_range(): _csum_matrix_func(d, cv=d) -def test_cached_matrix_identity_and_x(): +def test_cached_matrix_identity_and_x(highp): A1 = _cached_matrix("single", "I", d=3, omega=None, key=()) A2 = _cached_matrix("single", "I", d=3, omega=None, key=()) assert A1 is A2 @@ -272,7 +268,7 @@ def test_cached_matrix_identity_and_x(): assert X1.shape == (3, 3) and X2.shape == (3, 3) -def test_builders_smoke(): +def test_builders_smoke(highp): d = 3 for name, (sig, _) in SINGLE_BUILDERS.items(): defaults = { From 2135ecfc32fc3c2f366b9b211d859ea7e0fbfae4 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:00:31 +0800 Subject: [PATCH 30/64] remove `count_vector` from sample --- tensorcircuit/quditcircuit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 689a3783..a7aa6499 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -18,7 +18,7 @@ Tensor = Any -SAMPLE_FORMAT = Literal["sample_bin", "count_vector", "count_dict_bin"] +SAMPLE_FORMAT = Literal["sample_bin", "count_dict_bin"] class QuditCircuit: @@ -452,7 +452,7 @@ def sample( if the format is None, and consistent with format when given :rtype: Any """ - if format in ["sample_int", "count_tuple", "count_dict_int"]: + if format in ["sample_int", "count_tuple", "count_dict_int", "count_vector"]: raise NotImplementedError( "`int` representation is not friendly for d-dimensional systems." ) From 3e29f11b243c82cb8973f7a3a6ed3ba4129f47ef Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:05:37 +0800 Subject: [PATCH 31/64] remove sympy --- tensorcircuit/quditgates.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index c00c6f34..6b8611d7 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -2,7 +2,6 @@ from typing import Any, Optional, Tuple import numpy as np -from sympy import mod_inverse, Mod from .cons import npdtype @@ -438,7 +437,7 @@ def _u8_matrix_func( vks = [0, 1, 8] else: try: - inv_12 = mod_inverse(12, d) + inv_12 = pow(12, -1, d) except ValueError: raise ValueError( f"Inverse of 12 mod {d} does not exist. Choose a prime d that does not divide 12." @@ -446,13 +445,11 @@ def _u8_matrix_func( for i in range(1, d): a = inv_12 * i * (gamma + i * (6 * z + (2 * i - 3) * gamma)) + eps * i - vks[i] = Mod(a, d) + vks[i] = int(a) % d - # print(vks) - sum_vks = Mod(sum(vks), d) - if sum_vks != 0: + if sum(vks) % d != 0: raise ValueError( - f"Sum of v_k's is not 0 mod {d}. Got {sum_vks}. Check parameters." + f"Sum of v_k's is not 0 mod {d}. Got {sum(vks) % d}. Check parameters." ) omega = np.exp(2j * np.pi / d) if omega is None else omega From 8c86e1cb383f2f4e83536288a360044ed98b1909 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:08:16 +0800 Subject: [PATCH 32/64] format codes according to pylint --- tensorcircuit/quditcircuit.py | 7 +++++-- tests/test_quditgates.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index a7aa6499..8e0c6775 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -86,7 +86,9 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: """ Apply a quantum gate (unitary) to one or two qudits in the circuit. - The gate matrix is looked up by name in either ``SINGLE_BUILDERS`` (for single-qudit gates) or ``TWO_BUILDERS`` (for two-qudit gates). The matrix is built (and cached) via ``_cached_matrix``, then applied to the circuit at the given indices. + The gate matrix is looked up by name in either ``SINGLE_BUILDERS`` (for single-qudit gates) + or ``TWO_BUILDERS`` (for two-qudit gates). The matrix is built (and cached) via ``_cached_matrix``, + then applied to the circuit at the given indices. :param indices: The qudit indices the gate should act on. - One index → single-qudit gate. @@ -96,7 +98,8 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: :type name: str :param kwargs: Extra parameters for the gate matched against the builder signature. :type kwargs: Any - :raises ValueError: If ``name`` is not found, or if the number of indices does not match the gate type (single vs two). + :raises ValueError: If ``name`` is not found, + or if the number of indices does not match the gate type (single vs two). """ if len(indices) == 1 and name in SINGLE_BUILDERS: sig, _ = SINGLE_BUILDERS[name] diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 0c1c8f61..84bea228 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -1,6 +1,6 @@ -import pytest import sys import os +import pytest import numpy as np thisfile = os.path.abspath(__file__) From 83378fb786b6280577939cede1996a34a0942166 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:10:44 +0800 Subject: [PATCH 33/64] change u8 __doc__ --- tensorcircuit/quditgates.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 6b8611d7..a66be733 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -407,8 +407,14 @@ def _u8_matrix_func( r""" ``U8`` diagonal single-qudit gate for prime dimensions. - Defined for prime ``d`` with phases determined by modular polynomials depending - on parameters :math:`\gamma, z, \epsilon`. + This gate represents a canonical nontrivial diagonal Clifford element + in prime-dimensional qudit systems. Together with generalized Pauli + operators, it generates the full single-qudit Clifford group. In the + qubit case (``d=2``), it reduces to the well-known π/8 gate. For higher + prime dimensions, the phases are defined through modular polynomials + depending on :math:`\gamma, z, \epsilon`. Its explicit inclusion ensures + coverage of the complete Clifford generating set across prime qudit + dimensions. :param d: Qudit dimension (must be prime). :type d: int @@ -422,8 +428,8 @@ def _u8_matrix_func( :type omega: Optional[float] :return: ``(d, d)`` diagonal matrix of dtype ``npdtype``. :rtype: Tensor - :raises ValueError: If ``d`` is not prime; if ``gamma==0``; - if 12 has no modular inverse mod ``d``; or if the computed exponents do not sum to 0 mod ``d``. + :raises ValueError: If ``d`` is not prime; if ``gamma==0``; if 12 has no modular + inverse mod ``d``; or if the computed exponents do not sum to 0 mod ``d``. """ if not _is_prime(d): raise ValueError( From d379cff9dd95795d28dd5dfce00ffbd029a054a0 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:38:14 +0800 Subject: [PATCH 34/64] remove 'count_vector' --- tests/test_quditcircuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index ccbdfdbf..c7cd11da 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -315,7 +315,6 @@ def test_sample_format(backend): for format_ in [ None, "sample_bin", - "count_vector", "count_dict_bin", ]: print(" format: ", format_) From 995a3c83fcd39101517c8843290230aa97eae9c5 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:38:41 +0800 Subject: [PATCH 35/64] remove y gate --- tensorcircuit/quditcircuit.py | 16 ++++++++-------- tensorcircuit/quditgates.py | 30 +++++++++++++++--------------- tests/test_quditgates.py | 14 +++++++------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 8e0c6775..dd46662b 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -153,14 +153,14 @@ def x(self, index: int) -> None: """ self._apply_gate(index, name="X") - def y(self, index: int) -> None: - """ - Apply the Y gate on the given qudit index. - - :param index: Qudit index to apply the gate on. - :type index: int - """ - self._apply_gate(index, name="Y") + # def y(self, index: int) -> None: + # """ + # Apply the Y gate on the given qudit index. + # + # :param index: Qudit index to apply the gate on. + # :type index: int + # """ + # self._apply_gate(index, name="Y") def z(self, index: int) -> None: """ diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index a66be733..7fe72e28 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -11,7 +11,7 @@ SINGLE_BUILDERS = { "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), - "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), + # "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), "RX": ( @@ -158,20 +158,20 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: return np.diag([omega**j for j in range(d)]).astype(npdtype) -def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: - r""" - Generalized Pauli-Y (Y) gate for qudits. - - Defined (up to a global phase) via :math:`Y \propto Z\,X`. - - :param d: Qudit dimension. - :type d: int - :param omega: Optional primitive ``d``-th root of unity used by ``Z``. - :type omega: Optional[float] - :return: ``(d, d)`` matrix for :math:`Y`. - :rtype: Tensor - """ - return np.matmul(_z_matrix_func(d, omega=omega), _x_matrix_func(d)) / 1j +# def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: +# r""" +# Generalized Pauli-Y (Y) gate for qudits. +# +# Defined (up to a global phase) via :math:`Y \propto Z\,X`. +# +# :param d: Qudit dimension. +# :type d: int +# :param omega: Optional primitive ``d``-th root of unity used by ``Z``. +# :type omega: Optional[float] +# :return: ``(d, d)`` matrix for :math:`Y`. +# :rtype: Tensor +# """ +# return np.matmul(_z_matrix_func(d, omega=omega), _x_matrix_func(d)) / 1j def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 84bea228..31290aa1 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -12,7 +12,7 @@ _i_matrix_func, _x_matrix_func, _z_matrix_func, - _y_matrix_func, + # _y_matrix_func, _h_matrix_func, _s_matrix_func, _rx_matrix_func, @@ -69,12 +69,12 @@ def test_Z_diagonal_and_value(d, highp): assert is_unitary(Z) -@pytest.mark.parametrize("d", [2, 3, 5]) -def test_Y_equals_ZX_over_i(d, highp): - Y = _y_matrix_func(d) - ZX_over_i = (_z_matrix_func(d) @ _x_matrix_func(d)) / 1j - np.testing.assert_allclose(Y, ZX_over_i, atol=1e-5) - assert is_unitary(Y) +# @pytest.mark.parametrize("d", [2, 3, 5]) +# def test_Y_equals_ZX_over_i(d, highp): +# Y = _y_matrix_func(d) +# ZX_over_i = (_z_matrix_func(d) @ _x_matrix_func(d)) / 1j +# np.testing.assert_allclose(Y, ZX_over_i, atol=1e-5) +# assert is_unitary(Y) @pytest.mark.parametrize("d", [2, 3, 5]) From 10cc46c1d38ad6d79e1025e3ac5d607fe8d4fb49 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:43:08 +0800 Subject: [PATCH 36/64] remove a y-related test info --- tests/test_quditcircuit.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index c7cd11da..8286adca 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -118,12 +118,12 @@ def test_expectation_between_two_states_qudit(backend): H3 = tc.quditgates._h_matrix_func(dim) X3_dag = np.conjugate(X3.T) - e0 = np.array([1.0, 0.0, 0.0], dtype=np.complex64) - e1 = np.array([0.0, 1.0, 0.0], dtype=np.complex64) - val = tc.expectation((tc.gates.Gate(Y3), [0]), ket=e0, bra=e1, dim=dim) - omega = np.exp(2j * np.pi / dim) - expected = omega / 1j - np.testing.assert_allclose(tc.backend.numpy(val), expected, rtol=1e-6, atol=1e-6) + # e0 = np.array([1.0, 0.0, 0.0], dtype=np.complex64) + # e1 = np.array([0.0, 1.0, 0.0], dtype=np.complex64) + # val = tc.expectation((tc.gates.Gate(Y3), [0]), ket=e0, bra=e1, dim=dim) + # omega = np.exp(2j * np.pi / dim) + # expected = omega / 1j + # np.testing.assert_allclose(tc.backend.numpy(val), expected, rtol=1e-6, atol=1e-6) c = tc.QuditCircuit(3, dim) c.unitary(0, unitary=tc.gates.Gate(H3)) From a64a3b9c896c5e51b6f6e30f83df58e9df642593 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 3 Sep 2025 18:43:14 +0800 Subject: [PATCH 37/64] remove y --- tests/test_quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 8286adca..877fe125 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -113,7 +113,7 @@ def test_single_qubit(): def test_expectation_between_two_states_qudit(backend): dim = 3 X3 = tc.quditgates._x_matrix_func(dim) - Y3 = tc.quditgates._y_matrix_func(dim) # ZX/i + # Y3 = tc.quditgates._y_matrix_func(dim) # ZX/i Z3 = tc.quditgates._z_matrix_func(dim) H3 = tc.quditgates._h_matrix_func(dim) X3_dag = np.conjugate(X3.T) From b6a8c10b785217bc59108c4469465dfe6a737216 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Thu, 4 Sep 2025 17:51:40 +0800 Subject: [PATCH 38/64] rzz_gate from global to local (for consistence) --- tensorcircuit/quditgates.py | 49 ++++++++++++++++++++++++++----------- tests/test_quditgates.py | 16 ++++++++++-- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 7fe72e28..6d6dfd10 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -329,30 +329,51 @@ def _swap_matrix_func(d: int) -> Tensor: return matrix -def _rzz_matrix_func(d: int, theta: float) -> Tensor: +def _rzz_matrix_func( + d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 +) -> Tensor: r""" - Two-qudit ``RZZ(\theta)`` interaction for qudits. + Two-qudit ``RZZ(θ)`` on a selected two-state subspace. - .. math:: RZZ(\theta) = \exp\!\left(-i \tfrac{\theta}{2} (Z_H \otimes Z_H)\right) + Acts like a qubit :math:`RZZ(θ)=\exp(-i\,\tfrac{θ}{2}\,\sigma_z)` on the + two-dimensional subspace spanned by ``|j1, j2⟩`` and ``|k1, k2⟩``, + and as identity elsewhere. The resulting block is diagonal with phases + :math:`\mathrm{diag}(e^{-iθ/2},\, e^{+iθ/2})`. :param d: Dimension of each qudit (assumed equal). :type d: int :param theta: Rotation angle. :type theta: float - :return: ``(d*d, d*d)`` matrix representing :math:`RZZ(\theta)`. + :param j1: Level on qudit-1 for the first basis state. + :type j1: int + :param k1: Level on qudit-1 for the second basis state. + :type k1: int + :param j2: Level on qudit-2 for the first basis state. + :type j2: int + :param k2: Level on qudit-2 for the second basis state. + :type k2: int + :return: ``(d*d, d*d)`` matrix representing subspace :math:`RZZ(θ)`. :rtype: Tensor + :raises ValueError: If indices are out of range or select the same basis state. """ - lam = np.array( - [d - 1 - 2 * j for j in range(d)], dtype=float - ) # [d-1, d-3, ..., -(d-1)] + if not (0 <= j1 < d and 0 <= k1 < d and 0 <= j2 < d and 0 <= k2 < d): + raise ValueError("Indices j1,k1,j2,k2 must be in [0, d-1].") + if j1 == k1 and j2 == k2: + raise ValueError("Selected basis states must be different: (j1,j2) ≠ (k1,k2).") + D = d * d - diag = np.empty(D, dtype=npdtype) - idx = 0 - for a in range(d): - for b in range(d): - diag[idx] = np.exp(-1j * (theta / 2.0) * lam[a] * lam[b]) - idx += 1 - return np.diag(diag) + M = np.eye(D, dtype=npdtype) + + idx_a = j1 * d + j2 + idx_b = k1 * d + k2 + + phase_minus = np.exp(-1j * theta / 2.0) + phase_plus = np.exp(+1j * theta / 2.0) + + M[idx_a, idx_a] = phase_minus + M[idx_b, idx_b] = phase_plus + + return M def _rxx_matrix_func( diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 31290aa1..7de7aeb2 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -142,9 +142,21 @@ def test_SWAP_permutation(d, highp): @pytest.mark.parametrize("d", [2, 3, 5]) def test_RZZ_diagonal(d, highp): theta = 0.37 - RZZ = _rzz_matrix_func(d, theta) + RZZ = _rzz_matrix_func(d, theta, j1=0, k1=1, j2=0, k2=1) assert is_unitary(RZZ) - np.testing.assert_allclose(RZZ, np.diag(np.diag(RZZ)), atol=1e-5) # 对角阵 + + D = d * d + I = np.eye(D, dtype=np.complex128) + + idx_a = 0 * d + 0 + idx_b = 1 * d + 1 + + for t in range(D): + if t not in (idx_a, idx_b): + np.testing.assert_allclose(RZZ[t], I[t], atol=1e-5) + + np.testing.assert_allclose(RZZ[idx_a, idx_a], np.exp(-1j * theta / 2), atol=1e-5) + np.testing.assert_allclose(RZZ[idx_b, idx_b], np.exp(+1j * theta / 2), atol=1e-5) def test_RXX_selected_block(highp): From 92d98a3d352814e9290ca600ca7fbe1b4228fcc4 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:29:05 +0800 Subject: [PATCH 39/64] np -> backend to ensure the auto-differential --- tensorcircuit/quditgates.py | 92 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 6d6dfd10..3a1eaee5 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -3,11 +3,10 @@ import numpy as np -from .cons import npdtype +from .cons import backend, dtypestr Tensor = Any - SINGLE_BUILDERS = { "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), @@ -121,7 +120,7 @@ def _i_matrix_func(d: int) -> Tensor: :return: ``(d, d)`` identity matrix. :rtype: Tensor """ - return np.eye(d, dtype=npdtype) + return backend.eye(d, dtype=dtypestr) def _x_matrix_func(d: int) -> Tensor: @@ -135,10 +134,10 @@ def _x_matrix_func(d: int) -> Tensor: :return: ``(d, d)`` matrix for :math:`X_d`. :rtype: Tensor """ - matrix = np.zeros((d, d), dtype=npdtype) + matrix = backend.zeros((d, d), dtype=dtypestr) for j in range(d): matrix[(j + 1) % d, j] = 1.0 - return matrix + return backend.cast(matrix, dtype=dtypestr) def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -155,7 +154,8 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega - return np.diag([omega**j for j in range(d)]).astype(npdtype) + m = backend.convert_to_tensor(np.diag([omega**j for j in range(d)])) + return backend.cast(m, dtype=dtypestr) # def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -188,11 +188,12 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :rtype: Tensor """ omega = np.exp(2j * np.pi / d) if omega is None else omega - matrix = np.zeros((d, d), dtype=npdtype) + matrix = backend.zeros((d, d), dtype=dtypestr) + inv_sqrt_d = 1.0 / np.sqrt(d) for j in range(d): for k in range(d): - matrix[k, j] = omega ** (j * k) / np.sqrt(d) - return matrix + matrix[k, j] = (omega ** (j * k)) * inv_sqrt_d + return backend.cast(matrix, dtype=dtypestr) def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -210,11 +211,11 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: """ omega = np.exp(2j * np.pi / d) if omega is None else omega _pd = 0 if d % 2 == 0 else 1 - matrix = np.zeros((d, d), dtype=complex) + matrix = backend.zeros((d, d), dtype=dtypestr) for j in range(d): phase_exp = (j * (j + _pd)) / 2 matrix[j, j] = omega**phase_exp - return matrix + return backend.cast(matrix, dtype=dtypestr) def _check_rotation(d: int, j: int, k: int) -> None: @@ -253,13 +254,14 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - matrix = np.eye(d, dtype=npdtype) - c, s = np.cos(theta / 2.0), np.sin(theta / 2.0) + matrix = backend.eye(d, dtype=dtypestr) + c = backend.cos(theta / 2.0) + s = backend.sin(theta / 2.0) matrix[j, j] = c matrix[k, k] = c matrix[j, k] = -1j * s matrix[k, j] = -1j * s - return matrix + return backend.cast(matrix, dtype=dtypestr) def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: @@ -278,13 +280,14 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - matrix = np.eye(d, dtype=npdtype) - c, s = np.cos(theta / 2.0), np.sin(theta / 2.0) + matrix = backend.eye(d, dtype=dtypestr) + c = backend.cos(theta / 2.0) + s = backend.sin(theta / 2.0) matrix[j, j] = c matrix[k, k] = c matrix[j, k] = -s matrix[k, j] = s - return matrix + return backend.cast(matrix, dtype=dtypestr) def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: @@ -303,9 +306,9 @@ def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: :return: ``(d, d)`` diagonal matrix implementing :math:`RZ(\theta)` on level ``j``. :rtype: Tensor """ - matrix = np.eye(d, dtype=npdtype) - matrix[j, j] = np.exp(1j * theta) - return matrix + matrix = backend.eye(d, dtype=dtypestr) + matrix[j, j] = backend.exp(1j * theta) + return backend.cast(matrix, dtype=dtypestr) def _swap_matrix_func(d: int) -> Tensor: @@ -320,13 +323,13 @@ def _swap_matrix_func(d: int) -> Tensor: :rtype: Tensor """ D = d * d - matrix = np.zeros((D, D), dtype=npdtype) + matrix = backend.zeros((D, D), dtype=dtypestr) for i in range(d): for j in range(d): idx_in = i * d + j idx_out = j * d + i matrix[idx_out, idx_in] = 1.0 - return matrix + return backend.cast(matrix, dtype=dtypestr) def _rzz_matrix_func( @@ -362,18 +365,18 @@ def _rzz_matrix_func( raise ValueError("Selected basis states must be different: (j1,j2) ≠ (k1,k2).") D = d * d - M = np.eye(D, dtype=npdtype) + M = backend.eye(D, dtype=dtypestr) idx_a = j1 * d + j2 idx_b = k1 * d + k2 - phase_minus = np.exp(-1j * theta / 2.0) - phase_plus = np.exp(+1j * theta / 2.0) + phase_minus = backend.exp(-1j * theta / 2.0) + phase_plus = backend.exp(+1j * theta / 2.0) M[idx_a, idx_a] = phase_minus M[idx_b, idx_b] = phase_plus - return M + return backend.cast(M, dtype=dtypestr) def _rxx_matrix_func( @@ -400,22 +403,20 @@ def _rxx_matrix_func( :rtype: Tensor """ D = d * d - M = np.eye(D, dtype=npdtype) + M = backend.eye(D, dtype=dtypestr) - # flatten basis index: |a,b> ↦ a*d + b idx_a = j1 * d + j2 idx_b = k1 * d + k2 - c = np.cos(theta / 2.0) - s = np.sin(theta / 2.0) + c = backend.cos(theta / 2.0) + s = backend.sin(theta / 2.0) - # Overwrite the chosen 2x2 block M[idx_a, idx_a] = c M[idx_b, idx_b] = c M[idx_a, idx_b] = -1j * s M[idx_b, idx_a] = -1j * s - return M + return backend.cast(M, dtype=dtypestr) def _u8_matrix_func( @@ -480,7 +481,8 @@ def _u8_matrix_func( ) omega = np.exp(2j * np.pi / d) if omega is None else omega - return np.diag([omega ** vks[j] for j in range(d)]).astype(npdtype) + m = backend.convert_to_tensor(np.diag([omega ** vks[j] for j in range(d)])) + return backend.cast(m, dtype=dtypestr) def _cphase_matrix_func( @@ -512,24 +514,24 @@ def _cphase_matrix_func( z_matrix = _z_matrix_func(d=d, omega=omega) if cv is None: - z_pows = [np.eye(d, dtype=npdtype)] + z_pows = [backend.eye(d, dtype=dtypestr)] for _ in range(1, d): - z_pows.append(z_pows[-1] @ z_matrix) + z_pows.append(backend.matmul(z_pows[-1], z_matrix)) - matrix = np.zeros((size, size), dtype=npdtype) + matrix = backend.zeros((size, size), dtype=dtypestr) for a in range(d): rs = a * d matrix[rs : rs + d, rs : rs + d] = z_pows[a] - return matrix + return backend.cast(matrix, dtype=dtypestr) if not (0 <= cv < d): raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") - matrix = np.eye(size, dtype=npdtype) + matrix = backend.eye(size, dtype=dtypestr) rs = cv * d matrix[rs : rs + d, rs : rs + d] = z_matrix - return matrix + return backend.cast(matrix, dtype=dtypestr) def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: @@ -556,20 +558,20 @@ def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: x_matrix = _x_matrix_func(d=d) if cv is None: - x_pows = [np.eye(d, dtype=npdtype)] + x_pows = [backend.eye(d, dtype=dtypestr)] for _ in range(1, d): - x_pows.append(x_pows[-1] @ x_matrix) + x_pows.append(backend.matmul(x_pows[-1], x_matrix)) - matrix = np.zeros((size, size), dtype=npdtype) + matrix = backend.zeros((size, size), dtype=dtypestr) for a in range(d): rs = a * d matrix[rs : rs + d, rs : rs + d] = x_pows[a] - return matrix + return backend.cast(matrix, dtype=dtypestr) if not (0 <= cv < d): raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") - matrix = np.eye(size, dtype=npdtype) + matrix = backend.eye(size, dtype=dtypestr) rs = cv * d matrix[rs : rs + d, rs : rs + d] = x_matrix - return matrix + return backend.cast(matrix, dtype=dtypestr) From ee8b617ebf3e312ec266aec0731eb2da1aba852f Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:31:16 +0800 Subject: [PATCH 40/64] fixed rzz_gate --- tensorcircuit/quditcircuit.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index dd46662b..73b213de 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -267,16 +267,32 @@ def rxx( """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) - def rzz(self, *indices: int, theta: float) -> None: + def rzz( + self, + *indices: int, + theta: float, + j1: int = 0, + k1: int = 1, + j2: int = 0, + k2: int = 1, + ) -> None: """ - Apply a two-qudit RZZ interaction on the given indices. + Apply a two-qudit RZZ-type interaction on the given indices. :param indices: Two qudit indices. :type indices: int - :param theta: Interaction angle. + :param theta: Interaction strength/angle. :type theta: float + :param j1: Source level of the first qudit subspace. + :type j1: int + :param k1: Target level of the first qudit subspace. + :type k1: int + :param j2: Source level of the second qudit subspace. + :type j2: int + :param k2: Target level of the second qudit subspace. + :type k2: int """ - self._apply_gate(*indices, name="RZZ", theta=theta) + self._apply_gate(*indices, name="RZZ", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) def cphase(self, *indices: int, cv: Optional[int] = None) -> None: """ From 9cceefad026ea57a69a1cc25a9c8e54f5747223d Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:37:34 +0800 Subject: [PATCH 41/64] fixed a docsing bug --- tensorcircuit/basecircuit.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/tensorcircuit/basecircuit.py b/tensorcircuit/basecircuit.py index f996cf08..ffa50498 100644 --- a/tensorcircuit/basecircuit.py +++ b/tensorcircuit/basecircuit.py @@ -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] From 56902a58d952de8aa515e395eb333138ba048f1b Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:39:25 +0800 Subject: [PATCH 42/64] Add expectation_before(), amplitude_before() functions --- tensorcircuit/quditcircuit.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 73b213de..0f4e467d 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -563,3 +563,35 @@ def replace_mps_inputs(self, mps_inputs: QuOperator) -> None: :type mps_inputs: Tuple[Sequence[Gate], Sequence[Edge]] """ return self._circ.replace_mps_inputs(mps_inputs) + + def expectation_before( + self, + *ops: Tuple[tn.Node, List[int]], + reuse: bool = True, + **kws: Any, + ) -> List[tn.Node]: + """ + Get the tensor network in the form of a list of nodes + for the expectation calculation before the real contraction + + :param reuse: _description_, defaults to True + :type reuse: bool, optional + :raises ValueError: _description_ + :return: _description_ + :rtype: List[tn.Node] + """ + return self._circ.expectation_before(*ops, reuse=reuse, **kws) + + def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]: + r""" + 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. + + :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] + """ + return self._circ.amplitude_before(l) \ No newline at end of file From d396ba2a7b51fcff569e10a2ceedec5d3a4523f2 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:39:39 +0800 Subject: [PATCH 43/64] add Gate --- tensorcircuit/quditcircuit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 0f4e467d..fb7491ee 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -10,6 +10,7 @@ import numpy as np import tensornetwork as tn +from .gates import Gate from .utils import arg_alias from .basecircuit import BaseCircuit from .circuit import Circuit From 8b224f06c0405442404691dad72614597952d31e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 09:47:12 +0800 Subject: [PATCH 44/64] Provide a corresponding uppercase entry for the doors. --- tensorcircuit/quditcircuit.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index fb7491ee..dd37edbb 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -145,6 +145,8 @@ def i(self, index: int) -> None: """ self._apply_gate(index, name="I") + I = i + def x(self, index: int) -> None: """ Apply the X gate on the given qudit index. @@ -154,6 +156,8 @@ def x(self, index: int) -> None: """ self._apply_gate(index, name="X") + X = x + # def y(self, index: int) -> None: # """ # Apply the Y gate on the given qudit index. @@ -172,6 +176,8 @@ def z(self, index: int) -> None: """ self._apply_gate(index, name="Z") + Z = z + def h(self, index: int) -> None: """ Apply the Hadamard-like (H) gate on the given qudit index. @@ -181,6 +187,8 @@ def h(self, index: int) -> None: """ self._apply_gate(index, name="H") + H = h + def u8( self, index: int, gamma: float = 2.0, z: float = 1.0, eps: float = 0.0 ) -> None: @@ -198,6 +206,8 @@ def u8( """ self._apply_gate(index, name="U8", extra=(gamma, z, eps)) + U8 = u8 + def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ Apply the single-qudit RX rotation on ``index``. @@ -213,6 +223,8 @@ def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ self._apply_gate(index, name="RX", theta=theta, j=j, k=k) + RX = rx + def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ Apply the single-qudit RY rotation on ``index``. @@ -228,6 +240,8 @@ def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ self._apply_gate(index, name="RY", theta=theta, j=j, k=k) + RY = ry + def rz(self, index: int, theta: float, j: int = 0) -> None: """ Apply the single-qudit RZ rotation on ``index``. @@ -241,6 +255,8 @@ def rz(self, index: int, theta: float, j: int = 0) -> None: """ self._apply_gate(index, name="RZ", theta=theta, j=j) + RZ = rz + def rxx( self, *indices: int, @@ -268,6 +284,8 @@ def rxx( """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) + RXX = rxx + def rzz( self, *indices: int, @@ -295,6 +313,8 @@ def rzz( """ self._apply_gate(*indices, name="RZZ", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) + RZZ = rzz + def cphase(self, *indices: int, cv: Optional[int] = None) -> None: """ Apply a controlled phase (CPHASE) gate. @@ -306,6 +326,8 @@ def cphase(self, *indices: int, cv: Optional[int] = None) -> None: """ self._apply_gate(*indices, name="CPHASE", cv=cv) + CPHASE = cphase + def csum(self, *indices: int, cv: Optional[int] = None) -> None: """ Apply a controlled-sum (CSUM) gate. @@ -317,7 +339,7 @@ def csum(self, *indices: int, cv: Optional[int] = None) -> None: """ self._apply_gate(*indices, name="CSUM", cv=cv) - cnot = csum + cnot, CSUM, CNOT = csum, csum, csum # Functional def wavefunction(self, form: str = "default") -> tn.Node.tensor: @@ -595,4 +617,4 @@ def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]: :return: The tensornetwork nodes for the amplitude of the circuit. :rtype: List[Gate] """ - return self._circ.amplitude_before(l) \ No newline at end of file + return self._circ.amplitude_before(l) From 0944fb565fe0c695ab21c8bb22b5c14fc20c5efb Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 15:34:32 +0800 Subject: [PATCH 45/64] np -> backend, and this should solve auto-differential. --- tensorcircuit/quditgates.py | 351 ++++++++++++++++++++++-------------- 1 file changed, 212 insertions(+), 139 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 3a1eaee5..6c74ccfc 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -1,16 +1,16 @@ from functools import lru_cache -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, List, cast import numpy as np from .cons import backend, dtypestr +from .gates import num_to_tensor Tensor = Any SINGLE_BUILDERS = { "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), - # "Y": (("none",), lambda d, omega, **kw: _y_matrix_func(d, omega)), "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), "RX": ( @@ -47,6 +47,42 @@ @lru_cache(maxsize=None) +def _cached_matrix_cached( + kind: str, + name: str, + d: int, + omega: Optional[float] = None, + key: tuple[Any, ...] = (), +) -> Tensor: + r""" + Cached builder for gate matrices. + + This function is decorated with ``functools.lru_cache`` and should not be + called directly. It is invoked internally by :func:`_cached_matrix` once + the cache key (including backend identity, dtype, and optional parameters) + has been normalized. + + :param kind: Builder category, either ``"single"`` or ``"two"``. + :type kind: str + :param name: Gate name in the chosen builder dictionary. + :type name: str + :param d: Dimension of the qudit system. + :type d: int + :param omega: Optional scalar parameter used by certain gates. + :type omega: Optional[float or complex] + :param key: Normalized cache key including builder parameters and metadata. + :type key: tuple + :return: Gate matrix constructed by the corresponding builder. + :rtype: Tensor + """ + builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS + sig, builder = builders[name] + extras = () if key is None else key + extras_for_kwargs = extras[: len(sig)] + kwargs = {k: v for k, v in zip(sig, extras_for_kwargs)} + return builder(d, omega, **kwargs) + + def _cached_matrix( kind: str, name: str, @@ -54,36 +90,72 @@ def _cached_matrix( omega: Optional[float] = None, key: Optional[tuple[Any, ...]] = (), ) -> Tensor: - """ - Build and cache a matrix using a registered builder function. + r""" + Build (and optionally cache) a gate matrix with backend-/dtype-aware keys. - Looks up a builder in ``SINGLE_BUILDERS`` (for single–qudit gates) or - ``TWO_BUILDERS`` (for two–qudit gates) according to ``kind``, constructs the - matrix, and caches the result via ``functools.lru_cache``. + If ``key`` and ``omega`` can be normalized into hashable Python/NumPy scalars, + the result is cached using a key augmented with backend identity, dtype string, + and normalized ``omega``. Otherwise, caching is bypassed. - :param kind: Either ``"single"`` (use ``SINGLE_BUILDERS``) or ``"two"`` (use ``TWO_BUILDERS``). + :param kind: Builder category, either ``"single"`` or ``"two"``. :type kind: str - :param name: Builder name to look up in the chosen dictionary. + :param name: Gate name in the chosen builder dictionary. :type name: str - :param d: Dimension of the (sub)system. + :param d: Dimension of the qudit system. :type d: int - :param omega: Optional frequency/scaling parameter passed to the builder. - :type omega: Optional[float] - :param key: Tuple of extra parameters matched positionally to the builder's signature. - :type key: Optional[tuple[Any, ...]] - :return: Matrix built by the selected builder. + :param omega: Optional scalar parameter used by certain gates. + :type omega: Optional[float or complex] + :param key: Extra parameters matched positionally to the builder’s signature. + :type key: tuple + :return: Gate matrix constructed by the corresponding builder. :rtype: Tensor - :raises KeyError: If the builder ``name`` is not found. - :raises TypeError: If ``key`` does not match the builder’s expected parameters. - :raises ValueError: If ``key`` does not match the builder’s expected parameters. """ - builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS - try: - sig, builder = builders[name] - except KeyError as e: - raise KeyError(f"Unknown builder '{name}' for kind '{kind}'") from e + # Normalize `key` to a hashable tuple of Python scalars; else disable caching. + norm_key: Optional[Tuple[Any, ...]] + if key is None: + norm_key = cast(Tuple[Any, ...], ()) + else: + norm_list: List[Any] = [] + for v in key: + if isinstance(v, (int, float, complex, str, bool, type(None))): + norm_list.append(v) + elif isinstance(v, np.generic): + norm_list.append(v.item()) + else: + norm_key = None + break + else: + norm_key = tuple(norm_list) + + # Normalize omega to a scalar tag for the cache key; else disable caching. + omega_tag = None + if norm_key is not None: + if isinstance(omega, (int, float, complex, type(None))): + omega_tag = omega + elif isinstance(omega, np.generic): + omega_tag = omega.item() + else: + norm_key = None # non-scalar omega → no caching + + # Cached path + if norm_key is not None: + backend_id = getattr(backend, "name", None) + if backend_id is None: + backend_id = f"{backend.__class__.__module__}.{backend.__class__.__name__}" + augmented_key = norm_key + ( + "__backend__", + backend_id, + "__dtype__", + dtypestr, + "__omega__", + omega_tag, + ) + return _cached_matrix_cached(kind, name, d, omega, augmented_key) - extras: Tuple[Any, ...] = () if key is None else key # normalized & typed + # Non-cached fallback + builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS + sig, builder = builders[name] + extras = () if key is None else key kwargs = {k: v for k, v in zip(sig, extras)} return builder(d, omega, **kwargs) @@ -134,10 +206,11 @@ def _x_matrix_func(d: int) -> Tensor: :return: ``(d, d)`` matrix for :math:`X_d`. :rtype: Tensor """ - matrix = backend.zeros((d, d), dtype=dtypestr) - for j in range(d): - matrix[(j + 1) % d, j] = 1.0 - return backend.cast(matrix, dtype=dtypestr) + I = backend.eye(d, dtype=dtypestr) + X = backend.zeros((d, d), dtype=dtypestr) + for t in range(d): + X += backend.outer_product(I[:, (t + 1) % d], I[:, t]) + return X def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -153,25 +226,11 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` matrix for :math:`Z_d`. :rtype: Tensor """ - omega = np.exp(2j * np.pi / d) if omega is None else omega - m = backend.convert_to_tensor(np.diag([omega**j for j in range(d)])) - return backend.cast(m, dtype=dtypestr) - - -# def _y_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: -# r""" -# Generalized Pauli-Y (Y) gate for qudits. -# -# Defined (up to a global phase) via :math:`Y \propto Z\,X`. -# -# :param d: Qudit dimension. -# :type d: int -# :param omega: Optional primitive ``d``-th root of unity used by ``Z``. -# :type omega: Optional[float] -# :return: ``(d, d)`` matrix for :math:`Y`. -# :rtype: Tensor -# """ -# return np.matmul(_z_matrix_func(d, omega=omega), _x_matrix_func(d)) / 1j + omega = num_to_tensor( + np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr + ) + j = backend.cast(backend.arange(d), dtype=dtypestr) + return backend.diagflat(backend.power(omega, j)) def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -187,13 +246,13 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` matrix for :math:`H_d`. :rtype: Tensor """ - omega = np.exp(2j * np.pi / d) if omega is None else omega - matrix = backend.zeros((d, d), dtype=dtypestr) - inv_sqrt_d = 1.0 / np.sqrt(d) - for j in range(d): - for k in range(d): - matrix[k, j] = (omega ** (j * k)) * inv_sqrt_d - return backend.cast(matrix, dtype=dtypestr) + omega = num_to_tensor( + np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr + ) + j, k = backend.cast(backend.arange(d), dtype=dtypestr), backend.cast( + backend.arange(d), dtype=dtypestr + ) + return omega ** backend.outer_product(k, j) / backend.sqrt(num_to_tensor(d)) def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -209,13 +268,12 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` diagonal matrix for :math:`S_d`. :rtype: Tensor """ - omega = np.exp(2j * np.pi / d) if omega is None else omega - _pd = 0 if d % 2 == 0 else 1 - matrix = backend.zeros((d, d), dtype=dtypestr) - for j in range(d): - phase_exp = (j * (j + _pd)) / 2 - matrix[j, j] = omega**phase_exp - return backend.cast(matrix, dtype=dtypestr) + omega = num_to_tensor( + np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr + ) + j = backend.arange(d) + pd = 0 if d % 2 == 0 else 1 + return backend.diagflat(omega ** ((j * (j + pd)) / 2)) def _check_rotation(d: int, j: int, k: int) -> None: @@ -236,6 +294,22 @@ def _check_rotation(d: int, j: int, k: int) -> None: raise ValueError("R- rotation requires two distinct levels j != k.") +@lru_cache(maxsize=None) +def _basis_single(d: int, j: int, k: Optional[int] = None) -> Tuple[Tensor, ...]: + I = backend.eye(d, dtype=dtypestr) + ej = I[:, j] + Pjj = backend.outer_product(ej, ej) + + if k is None: + return I, Pjj + + ek = I[:, k] + Pkk = backend.outer_product(ek, ek) + Pjk = backend.outer_product(ej, ek) + Pkj = backend.outer_product(ek, ej) + return I, Pjj, Pkk, Pjk, Pkj + + def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: r""" Rotation-X (``RX``) gate on a selected two-level subspace of a qudit. @@ -254,14 +328,11 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - matrix = backend.eye(d, dtype=dtypestr) + I, Pjj, Pkk, Pjk, Pkj = _basis_single(d, j, k) + theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) s = backend.sin(theta / 2.0) - matrix[j, j] = c - matrix[k, k] = c - matrix[j, k] = -1j * s - matrix[k, j] = -1j * s - return backend.cast(matrix, dtype=dtypestr) + return I + (c - 1.0) * (Pjj + Pkk) + (-1j * s) * (Pjk + Pkj) def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: @@ -280,14 +351,11 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - matrix = backend.eye(d, dtype=dtypestr) + I, Pjj, Pkk, Pjk, Pkj = _basis_single(d, j, k) + theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) s = backend.sin(theta / 2.0) - matrix[j, j] = c - matrix[k, k] = c - matrix[j, k] = -s - matrix[k, j] = s - return backend.cast(matrix, dtype=dtypestr) + return I + (c - 1.0) * (Pjj + Pkk) - s * Pjk + s * Pkj def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: @@ -306,9 +374,10 @@ def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: :return: ``(d, d)`` diagonal matrix implementing :math:`RZ(\theta)` on level ``j``. :rtype: Tensor """ - matrix = backend.eye(d, dtype=dtypestr) - matrix[j, j] = backend.exp(1j * theta) - return backend.cast(matrix, dtype=dtypestr) + I, Pjj = _basis_single(d, j, k=None) + theta = num_to_tensor(theta) + phase = backend.exp(1j * theta) + return I + (phase - 1.0) * Pjj def _swap_matrix_func(d: int) -> Tensor: @@ -323,25 +392,20 @@ def _swap_matrix_func(d: int) -> Tensor: :rtype: Tensor """ D = d * d - matrix = backend.zeros((D, D), dtype=dtypestr) - for i in range(d): - for j in range(d): - idx_in = i * d + j - idx_out = j * d + i - matrix[idx_out, idx_in] = 1.0 - return backend.cast(matrix, dtype=dtypestr) + I = backend.eye(D, dtype=dtypestr) + return I.reshape(d, d, d, d).transpose(1, 0, 2, 3).reshape(D, D) def _rzz_matrix_func( d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 ) -> Tensor: r""" - Two-qudit ``RZZ(θ)`` on a selected two-state subspace. + Two-qudit ``RZZ(\theta)`` on a selected two-state subspace. - Acts like a qubit :math:`RZZ(θ)=\exp(-i\,\tfrac{θ}{2}\,\sigma_z)` on the + Acts like a qubit :math:`RZZ(\theta)=\exp(-i\,\tfrac{\theta}{2}\,\sigma_z)` on the two-dimensional subspace spanned by ``|j1, j2⟩`` and ``|k1, k2⟩``, and as identity elsewhere. The resulting block is diagonal with phases - :math:`\mathrm{diag}(e^{-iθ/2},\, e^{+iθ/2})`. + :math:`\mathrm{diag}(e^{-i\theta/2},\, e^{+i\theta/2})`. :param d: Dimension of each qudit (assumed equal). :type d: int @@ -355,28 +419,29 @@ def _rzz_matrix_func( :type j2: int :param k2: Level on qudit-2 for the second basis state. :type k2: int - :return: ``(d*d, d*d)`` matrix representing subspace :math:`RZZ(θ)`. + :return: ``(d*d, d*d)`` matrix representing subspace :math:`RZZ(\theta)`. :rtype: Tensor :raises ValueError: If indices are out of range or select the same basis state. """ if not (0 <= j1 < d and 0 <= k1 < d and 0 <= j2 < d and 0 <= k2 < d): - raise ValueError("Indices j1,k1,j2,k2 must be in [0, d-1].") + raise ValueError("Indices j1, k1, j2, k2 must be in [0, d-1].") if j1 == k1 and j2 == k2: - raise ValueError("Selected basis states must be different: (j1,j2) ≠ (k1,k2).") - - D = d * d - M = backend.eye(D, dtype=dtypestr) + raise ValueError( + "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." + ) idx_a = j1 * d + j2 idx_b = k1 * d + k2 - + theta = num_to_tensor(theta) phase_minus = backend.exp(-1j * theta / 2.0) phase_plus = backend.exp(+1j * theta / 2.0) - M[idx_a, idx_a] = phase_minus - M[idx_b, idx_b] = phase_plus - - return backend.cast(M, dtype=dtypestr) + I = backend.eye(d * d, dtype=dtypestr) + ea = I[:, idx_a] + eb = I[:, idx_b] + Paa = backend.outer_product(ea, ea) + Pbb = backend.outer_product(eb, eb) + return I + (phase_minus - 1.0) * Paa + (phase_plus - 1.0) * Pbb def _rxx_matrix_func( @@ -402,21 +467,27 @@ def _rxx_matrix_func( :return: ``(d*d, d*d)`` matrix representing :math:`RXX(\theta)` on the selected subspace. :rtype: Tensor """ - D = d * d - M = backend.eye(D, dtype=dtypestr) + if not (0 <= j1 < d and 0 <= k1 < d and 0 <= j2 < d and 0 <= k2 < d): + raise ValueError("Indices j1, k1, j2, k2 must be in [0, d-1].") + if j1 == k1 and j2 == k2: + raise ValueError( + "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." + ) idx_a = j1 * d + j2 idx_b = k1 * d + k2 - + theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) s = backend.sin(theta / 2.0) - M[idx_a, idx_a] = c - M[idx_b, idx_b] = c - M[idx_a, idx_b] = -1j * s - M[idx_b, idx_a] = -1j * s - - return backend.cast(M, dtype=dtypestr) + I = backend.eye(d * d, dtype=dtypestr) + ea = I[:, idx_a] + eb = I[:, idx_b] + Paa = backend.outer_product(ea, ea) + Pbb = backend.outer_product(eb, eb) + Pab = backend.outer_product(ea, eb) + Pba = backend.outer_product(eb, ea) + return I + (c - 1.0) * (Paa + Pbb) + (-1j * s) * (Pab + Pba) def _u8_matrix_func( @@ -480,9 +551,11 @@ def _u8_matrix_func( f"Sum of v_k's is not 0 mod {d}. Got {sum(vks) % d}. Check parameters." ) - omega = np.exp(2j * np.pi / d) if omega is None else omega - m = backend.convert_to_tensor(np.diag([omega ** vks[j] for j in range(d)])) - return backend.cast(m, dtype=dtypestr) + omega = num_to_tensor( + np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr + ) + vks_arr = backend.cast(backend.convert_to_tensor(np.array(vks)), dtype=dtypestr) + return backend.diagflat(backend.power(omega, vks_arr)) def _cphase_matrix_func( @@ -509,29 +582,28 @@ def _cphase_matrix_func( :rtype: Tensor :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ - omega = np.exp(2j * np.pi / d) if omega is None else omega - size = d**2 - z_matrix = _z_matrix_func(d=d, omega=omega) + omega = num_to_tensor( + np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr + ) + j = backend.arange(d) + I = backend.eye(d, dtype=dtypestr) if cv is None: - z_pows = [backend.eye(d, dtype=dtypestr)] - for _ in range(1, d): - z_pows.append(backend.matmul(z_pows[-1], z_matrix)) - - matrix = backend.zeros((size, size), dtype=dtypestr) + m = backend.zeros((d * d, d * d), dtype=dtypestr) for a in range(d): - rs = a * d - matrix[rs : rs + d, rs : rs + d] = z_pows[a] - return backend.cast(matrix, dtype=dtypestr) + Pa = backend.outer_product(I[:, a], I[:, a]) + Z_a = backend.diagflat(omega ** (a * j)) + m += backend.kron(Pa, Z_a) + return m if not (0 <= cv < d): raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") - matrix = backend.eye(size, dtype=dtypestr) - rs = cv * d - matrix[rs : rs + d, rs : rs + d] = z_matrix - - return backend.cast(matrix, dtype=dtypestr) + Z = backend.diagflat(omega**j) + m = backend.kron(I, I) + backend.kron( + backend.outer_product(I[:, cv], I[:, cv]), (Z - I) + ) + return m def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: @@ -554,24 +626,25 @@ def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: :rtype: Tensor :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ - size = d**2 - x_matrix = _x_matrix_func(d=d) + I = backend.eye(d, dtype=dtypestr) if cv is None: - x_pows = [backend.eye(d, dtype=dtypestr)] - for _ in range(1, d): - x_pows.append(backend.matmul(x_pows[-1], x_matrix)) - - matrix = backend.zeros((size, size), dtype=dtypestr) + m = backend.zeros((d * d, d * d), dtype=dtypestr) for a in range(d): - rs = a * d - matrix[rs : rs + d, rs : rs + d] = x_pows[a] - return backend.cast(matrix, dtype=dtypestr) + Pa = backend.outer_product(I[:, a], I[:, a]) + Xa = backend.zeros((d, d), dtype=dtypestr) + for t in range(d): + Xa += backend.outer_product(I[:, (t + a) % d], I[:, t]) + m += backend.kron(Pa, Xa) + return m if not (0 <= cv < d): raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") - matrix = backend.eye(size, dtype=dtypestr) - rs = cv * d - matrix[rs : rs + d, rs : rs + d] = x_matrix - return backend.cast(matrix, dtype=dtypestr) + X = backend.zeros((d, d), dtype=dtypestr) + for t in range(d): + X += backend.outer_product(I[:, (t + 1) % d], I[:, t]) + + return backend.kron(I, I) + backend.kron( + backend.outer_product(I[:, cv], I[:, cv]), (X - I) + ) From 3dd499b5c5a4882a185271fe1d2848cbedbaa05e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 15:53:37 +0800 Subject: [PATCH 46/64] Fixed a bug that could lead to backend pollution. --- tensorcircuit/quditgates.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 6c74ccfc..bbd6b98f 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -294,8 +294,23 @@ def _check_rotation(d: int, j: int, k: int) -> None: raise ValueError("R- rotation requires two distinct levels j != k.") -@lru_cache(maxsize=None) -def _basis_single(d: int, j: int, k: Optional[int] = None) -> Tuple[Tensor, ...]: +def _two_level_projectors( + d: int, j: int, k: Optional[int] = None +) -> Tuple[Tensor, ...]: + r""" + Construct projectors for single- or two-level subspaces in a ``d``-level qudit. + + :param d: Qudit dimension. + :type d: int + :param j: First level index. + :type j: int + :param k: Optional second level index. If None, only projectors for ``j`` are returned. + :type k: Optional[int] + :return: + - If ``k is None``: ``(I, Pjj)`` + - Else: ``(I, Pjj, Pkk, Pjk, Pkj)`` + :rtype: Tuple[Tensor, ...] + """ I = backend.eye(d, dtype=dtypestr) ej = I[:, j] Pjj = backend.outer_product(ej, ej) @@ -328,7 +343,7 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - I, Pjj, Pkk, Pjk, Pkj = _basis_single(d, j, k) + I, Pjj, Pkk, Pjk, Pkj = _two_level_projectors(d, j, k) theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) s = backend.sin(theta / 2.0) @@ -351,7 +366,7 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :rtype: Tensor """ _check_rotation(d, j, k) - I, Pjj, Pkk, Pjk, Pkj = _basis_single(d, j, k) + I, Pjj, Pkk, Pjk, Pkj = _two_level_projectors(d, j, k) theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) s = backend.sin(theta / 2.0) @@ -374,7 +389,7 @@ def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: :return: ``(d, d)`` diagonal matrix implementing :math:`RZ(\theta)` on level ``j``. :rtype: Tensor """ - I, Pjj = _basis_single(d, j, k=None) + I, Pjj = _two_level_projectors(d, j, k=None) theta = num_to_tensor(theta) phase = backend.exp(1j * theta) return I + (phase - 1.0) * Pjj From 5c3a082c767bbb8ef6f633630fd561172577f1e5 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 15:54:25 +0800 Subject: [PATCH 47/64] add test_quditvqe.py for testing vqe, jit and vmap in QUDIT Interface. --- tests/test_quditvqe.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_quditvqe.py diff --git a/tests/test_quditvqe.py b/tests/test_quditvqe.py new file mode 100644 index 00000000..c23e6e4a --- /dev/null +++ b/tests/test_quditvqe.py @@ -0,0 +1,72 @@ +# tests/test_quditvqe.py +# pylint: disable=invalid-name + +import os +import sys + +import numpy as np +import pytest +from pytest_lazyfixture import lazy_fixture as lf + +thisfile = os.path.abspath(__file__) +modulepath = os.path.dirname(os.path.dirname(thisfile)) +sys.path.insert(0, modulepath) + +import tensorcircuit as tc + + +def vqe_cost(params, d: int = 3): + c = tc.QuditCircuit(2, dim=d) + c.rx(0, params[0], j=0, k=1) + c.ry(1, params[1], j=0, k=1) + c.csum(0, 1) + + z0 = tc.backend.cast( + tc.backend.convert_to_tensor(np.diag([1, -1, 0])), + dtype=tc.cons.dtypestr, + ) + z1 = tc.backend.cast( + tc.backend.convert_to_tensor(np.diag([1, -1, 0])), + dtype=tc.cons.dtypestr, + ) + op = tc.backend.kron(z0, z1) + + wf = c.wavefunction() + energy = tc.backend.real( + tc.backend.einsum("i,ij,j->", tc.backend.adjoint(wf), op, wf) + ) + return energy + + +@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) +def test_autodiff(backend): + params = tc.backend.cast( + tc.backend.convert_to_tensor(np.array([0.1, 0.2])), dtype=tc.cons.rdtypestr + ) + grad_fn = tc.backend.grad(lambda p: vqe_cost(p)) + g = grad_fn(params) + assert g is not None + assert tc.backend.shape_tuple(g) == tc.backend.shape_tuple(params) + + +@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) +def test_jit(backend): + params = tc.backend.cast( + tc.backend.convert_to_tensor(np.array([0.1, 0.2])), dtype=tc.cons.rdtypestr + ) + f = lambda p: vqe_cost(p) + f_jit = tc.backend.jit(f) + e1 = f(params) + e2 = f_jit(params) + np.testing.assert_allclose(e1, e2, rtol=1e-5, atol=1e-6) + + +@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) +def test_vmap(backend): + params_batch = tc.backend.cast( + tc.backend.convert_to_tensor(np.array([[0.1, 0.2], [0.3, 0.4]])), + dtype=tc.cons.rdtypestr, + ) + f_batched = tc.backend.vmap(lambda p: vqe_cost(p)) + vals = f_batched(params_batch) + assert tc.backend.shape_tuple(vals) == (2,) From cdb1ee6534001240b0eaa3b8652867520d7bcc06 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 16:59:10 +0800 Subject: [PATCH 48/64] add tests for codecov test. --- tests/test_quditcircuit.py | 29 ++++++++++++++++++++++++ tests/test_quditgates.py | 46 +++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 877fe125..a9ace0f3 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -339,3 +339,32 @@ def test_sample_representation(): c.x(0) (result,) = c.sample(1, format="count_dict_bin").keys() assert result == _ALPHBET[i] + + +def test_quditcircuit_set_dim_validation(): + with pytest.raises(ValueError): + tc.QuditCircuit(1, 2) + with pytest.raises(ValueError): + tc.QuditCircuit(1, 2.5) # type: ignore[arg-type] + + +def test_quditcircuit_single_and_two_qudit_paths_and_wrappers(): + c = tc.QuditCircuit(2, 3) + c.x(0) + c.rzz( + 0, 1, theta=np.float64(0.2), j1=0, k1=1, j2=0, k2=1 + ) + c.cphase(0, 1, cv=1) + qo = c.get_quoperator() + assert qo is not None + _ = c.sample(allow_state=False, batch=1, format="count_dict_bin") + for bad in ["sample_int", "count_tuple", "count_dict_int", "count_vector"]: + with pytest.raises(NotImplementedError): + c.sample(allow_state=False, batch=1, format=bad) + + +def test_quditcircuit_amplitude_before_wrapper(): + c = tc.QuditCircuit(2, 3) + c.x(0) + nodes = c.amplitude_before("00") + assert isinstance(nodes, list) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 7de7aeb2..4a0edeb5 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -12,7 +12,6 @@ _i_matrix_func, _x_matrix_func, _z_matrix_func, - # _y_matrix_func, _h_matrix_func, _s_matrix_func, _rx_matrix_func, @@ -25,6 +24,7 @@ _cphase_matrix_func, _csum_matrix_func, _cached_matrix, + _is_prime, SINGLE_BUILDERS, TWO_BUILDERS, ) @@ -300,3 +300,47 @@ def test_builders_smoke(highp): key = tuple(defaults[s] for s in sig) M = _cached_matrix("two", name, d, None, key) assert M.shape == (d * d, d * d) + + +def test_cached_matrix_np_scalar_and_non_scalar_omega(): + d = 3 + if "RX" in SINGLE_BUILDERS: + name = "RX" + sig, _ = SINGLE_BUILDERS[name] + defaults = {"theta": np.float64(0.314159265), "j": 0, "k": 1} + key = tuple(defaults.get(s, 0) for s in sig if s != "none") + M = _cached_matrix("single", name, d, None, key) + assert M.shape == (d, d) + + name = "I" if "I" in SINGLE_BUILDERS else next(iter(SINGLE_BUILDERS.keys())) + sig, _ = SINGLE_BUILDERS[name] + key = tuple(0 for s in sig if s != "none") + M2 = _cached_matrix("single", name, d, np.array([0.5]), key) + assert M2.shape == (d, d) + + +def test__is_prime_edge_and_composites(): + assert _is_prime(2) is True + assert _is_prime(3) is True + assert _is_prime(4) is False + assert _is_prime(5) is True + assert _is_prime(25) is False + assert _is_prime(29) is True + + +def test_two_qudit_builders_index_validation(): + d = 3 + theta = 0.1 + with pytest.raises(ValueError): + _rzz_matrix_func(d, theta, j1=0, k1=1, j2=0, k2=3) + with pytest.raises(ValueError): + _rxx_matrix_func(d, theta, j1=0, k1=1, j2=3, k2=0) + with pytest.raises(ValueError): + _rzz_matrix_func(d, theta, j1=0, k1=0, j2=1, k2=1) + with pytest.raises(ValueError): + _rxx_matrix_func(d, theta, j1=2, k1=2, j2=0, k2=0) + + +def test_u8_requires_prime_dimension(): + with pytest.raises(ValueError): + _u8_matrix_func(d=9, gamma=1.0, z=0.0, eps=0.0) From 90646eeff4ba1426244a38ecef297110935ae4f7 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 17:04:39 +0800 Subject: [PATCH 49/64] black . --- tests/test_quditcircuit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index a9ace0f3..76283bca 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -351,9 +351,7 @@ def test_quditcircuit_set_dim_validation(): def test_quditcircuit_single_and_two_qudit_paths_and_wrappers(): c = tc.QuditCircuit(2, 3) c.x(0) - c.rzz( - 0, 1, theta=np.float64(0.2), j1=0, k1=1, j2=0, k2=1 - ) + c.rzz(0, 1, theta=np.float64(0.2), j1=0, k1=1, j2=0, k2=1) c.cphase(0, 1, cv=1) qo = c.get_quoperator() assert qo is not None From 0e253e167a523bd3c83e16b6f52a928d07d59044 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 17:40:35 +0800 Subject: [PATCH 50/64] extend check_rotation() --- tensorcircuit/quditgates.py | 50 +++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index bbd6b98f..9468fb3a 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -276,22 +276,30 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: return backend.diagflat(omega ** ((j * (j + pd)) / 2)) -def _check_rotation(d: int, j: int, k: int) -> None: +def _check_rotation_indices(d: int, *indices: int, distinct_pairs: bool = False) -> None: """ - Validate rotation subspace indices for a ``d``-level system. + Validate that indices are within [0, d-1] and optionally form distinct pairs. :param d: Qudit dimension. :type d: int - :param j: First level index. - :type j: int - :param k: Second level index. - :type k: int - :raises ValueError: If indices are out of range or if ``j == k``. + :param indices: Indices to validate. + :type indices: int + :param distinct_pairs: If True, enforce that (indices[0], indices[1]) + ≠ (indices[2], indices[3]) for 4 indices. + :type distinct_pairs: bool + :raises ValueError: If indices are invalid. """ - if not (0 <= j < d) or not (0 <= k < d): - raise ValueError(f"Indices j={j}, k={k} must satisfy 0 <= j,k < d (d={d}).") - if j == k: - raise ValueError("R- rotation requires two distinct levels j != k.") + for idx in indices: + if not (0 <= idx < d): + raise ValueError(f"Index {idx} must satisfy 0 <= index < d (d={d}).") + + if len(indices) == 2 and indices[0] == indices[1]: + raise ValueError("Rotation requires two distinct levels: j != k.") + + if distinct_pairs and len(indices) == 4: + j1, k1, j2, k2 = indices + if j1 == k1 and j2 == k2: + raise ValueError("Selected basis states must be different: (j1, j2) ≠ (k1, k2).") def _two_level_projectors( @@ -342,7 +350,7 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :return: ``(d, d)`` matrix for :math:`RX(\theta)` on the ``j,k`` subspace. :rtype: Tensor """ - _check_rotation(d, j, k) + _check_rotation_indices(d, j, k) I, Pjj, Pkk, Pjk, Pkj = _two_level_projectors(d, j, k) theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) @@ -365,7 +373,7 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :return: ``(d, d)`` matrix for :math:`RY(\theta)` on the ``j,k`` subspace. :rtype: Tensor """ - _check_rotation(d, j, k) + _check_rotation_indices(d, j, k) I, Pjj, Pkk, Pjk, Pkj = _two_level_projectors(d, j, k) theta = num_to_tensor(theta) c = backend.cos(theta / 2.0) @@ -438,13 +446,7 @@ def _rzz_matrix_func( :rtype: Tensor :raises ValueError: If indices are out of range or select the same basis state. """ - if not (0 <= j1 < d and 0 <= k1 < d and 0 <= j2 < d and 0 <= k2 < d): - raise ValueError("Indices j1, k1, j2, k2 must be in [0, d-1].") - if j1 == k1 and j2 == k2: - raise ValueError( - "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." - ) - + _check_rotation_indices(d, j1, k1, j2, k2, distinct_pairs=True) idx_a = j1 * d + j2 idx_b = k1 * d + k2 theta = num_to_tensor(theta) @@ -482,13 +484,7 @@ def _rxx_matrix_func( :return: ``(d*d, d*d)`` matrix representing :math:`RXX(\theta)` on the selected subspace. :rtype: Tensor """ - if not (0 <= j1 < d and 0 <= k1 < d and 0 <= j2 < d and 0 <= k2 < d): - raise ValueError("Indices j1, k1, j2, k2 must be in [0, d-1].") - if j1 == k1 and j2 == k2: - raise ValueError( - "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." - ) - + _check_rotation_indices(d, j1, k1, j2, k2, distinct_pairs=True) idx_a = j1 * d + j2 idx_b = k1 * d + k2 theta = num_to_tensor(theta) From 5a9a4ea6d9f5a5496de0bc56d87c129593c91ccf Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 17:51:20 +0800 Subject: [PATCH 51/64] remove qudit gates lru_cache --- tensorcircuit/quditcircuit.py | 22 +++---- tensorcircuit/quditgates.py | 116 +--------------------------------- tests/test_quditgates.py | 51 --------------- 3 files changed, 11 insertions(+), 178 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index dd37edbb..9ae0f013 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -15,7 +15,7 @@ from .basecircuit import BaseCircuit from .circuit import Circuit from .quantum import QuOperator, QuVector -from .quditgates import SINGLE_BUILDERS, TWO_BUILDERS, _cached_matrix +from .quditgates import SINGLE_BUILDERS, TWO_BUILDERS Tensor = Any @@ -88,7 +88,7 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: Apply a quantum gate (unitary) to one or two qudits in the circuit. The gate matrix is looked up by name in either ``SINGLE_BUILDERS`` (for single-qudit gates) - or ``TWO_BUILDERS`` (for two-qudit gates). The matrix is built (and cached) via ``_cached_matrix``, + or ``TWO_BUILDERS`` (for two-qudit gates). The matrix is built by the registered builder, then applied to the circuit at the given indices. :param indices: The qudit indices the gate should act on. @@ -103,18 +103,16 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: or if the number of indices does not match the gate type (single vs two). """ if len(indices) == 1 and name in SINGLE_BUILDERS: - sig, _ = SINGLE_BUILDERS[name] - key = tuple(kwargs.get(k) for k in sig if k != "none") - mat = _cached_matrix( - kind="single", name=name, d=self._d, omega=self._omega, key=key - ) + sig, builder = SINGLE_BUILDERS[name] + extras = tuple(kwargs.get(k) for k in sig if k != "none") + builder_kwargs = {k: v for k, v in zip(sig, extras)} + mat = builder(self._d, self._omega, **builder_kwargs) self._circ.unitary(*indices, unitary=mat, name=name, dim=self._d) # type: ignore elif len(indices) == 2 and name in TWO_BUILDERS: - sig, _ = TWO_BUILDERS[name] - key = tuple(kwargs.get(k) for k in sig if k != "none") - mat = _cached_matrix( - kind="two", name=name, d=self._d, omega=self._omega, key=key - ) + sig, builder = TWO_BUILDERS[name] + extras = tuple(kwargs.get(k) for k in sig if k != "none") + builder_kwargs = {k: v for k, v in zip(sig, extras)} + mat = builder(self._d, self._omega, **builder_kwargs) self._circ.unitary( # type: ignore *indices, unitary=mat, name=name, dim=self._d ) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 9468fb3a..1c26eeec 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -1,5 +1,4 @@ -from functools import lru_cache -from typing import Any, Optional, Tuple, List, cast +from typing import Any, Optional, Tuple import numpy as np @@ -46,119 +45,6 @@ } -@lru_cache(maxsize=None) -def _cached_matrix_cached( - kind: str, - name: str, - d: int, - omega: Optional[float] = None, - key: tuple[Any, ...] = (), -) -> Tensor: - r""" - Cached builder for gate matrices. - - This function is decorated with ``functools.lru_cache`` and should not be - called directly. It is invoked internally by :func:`_cached_matrix` once - the cache key (including backend identity, dtype, and optional parameters) - has been normalized. - - :param kind: Builder category, either ``"single"`` or ``"two"``. - :type kind: str - :param name: Gate name in the chosen builder dictionary. - :type name: str - :param d: Dimension of the qudit system. - :type d: int - :param omega: Optional scalar parameter used by certain gates. - :type omega: Optional[float or complex] - :param key: Normalized cache key including builder parameters and metadata. - :type key: tuple - :return: Gate matrix constructed by the corresponding builder. - :rtype: Tensor - """ - builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS - sig, builder = builders[name] - extras = () if key is None else key - extras_for_kwargs = extras[: len(sig)] - kwargs = {k: v for k, v in zip(sig, extras_for_kwargs)} - return builder(d, omega, **kwargs) - - -def _cached_matrix( - kind: str, - name: str, - d: int, - omega: Optional[float] = None, - key: Optional[tuple[Any, ...]] = (), -) -> Tensor: - r""" - Build (and optionally cache) a gate matrix with backend-/dtype-aware keys. - - If ``key`` and ``omega`` can be normalized into hashable Python/NumPy scalars, - the result is cached using a key augmented with backend identity, dtype string, - and normalized ``omega``. Otherwise, caching is bypassed. - - :param kind: Builder category, either ``"single"`` or ``"two"``. - :type kind: str - :param name: Gate name in the chosen builder dictionary. - :type name: str - :param d: Dimension of the qudit system. - :type d: int - :param omega: Optional scalar parameter used by certain gates. - :type omega: Optional[float or complex] - :param key: Extra parameters matched positionally to the builder’s signature. - :type key: tuple - :return: Gate matrix constructed by the corresponding builder. - :rtype: Tensor - """ - # Normalize `key` to a hashable tuple of Python scalars; else disable caching. - norm_key: Optional[Tuple[Any, ...]] - if key is None: - norm_key = cast(Tuple[Any, ...], ()) - else: - norm_list: List[Any] = [] - for v in key: - if isinstance(v, (int, float, complex, str, bool, type(None))): - norm_list.append(v) - elif isinstance(v, np.generic): - norm_list.append(v.item()) - else: - norm_key = None - break - else: - norm_key = tuple(norm_list) - - # Normalize omega to a scalar tag for the cache key; else disable caching. - omega_tag = None - if norm_key is not None: - if isinstance(omega, (int, float, complex, type(None))): - omega_tag = omega - elif isinstance(omega, np.generic): - omega_tag = omega.item() - else: - norm_key = None # non-scalar omega → no caching - - # Cached path - if norm_key is not None: - backend_id = getattr(backend, "name", None) - if backend_id is None: - backend_id = f"{backend.__class__.__module__}.{backend.__class__.__name__}" - augmented_key = norm_key + ( - "__backend__", - backend_id, - "__dtype__", - dtypestr, - "__omega__", - omega_tag, - ) - return _cached_matrix_cached(kind, name, d, omega, augmented_key) - - # Non-cached fallback - builders = SINGLE_BUILDERS if kind == "single" else TWO_BUILDERS - sig, builder = builders[name] - extras = () if key is None else key - kwargs = {k: v for k, v in zip(sig, extras)} - return builder(d, omega, **kwargs) - def _is_prime(n: int) -> bool: """ diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 4a0edeb5..0361567c 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -23,7 +23,6 @@ _u8_matrix_func, _cphase_matrix_func, _csum_matrix_func, - _cached_matrix, _is_prime, SINGLE_BUILDERS, TWO_BUILDERS, @@ -269,56 +268,6 @@ def test_CPHASE_CSUM_cv_range(highp): _csum_matrix_func(d, cv=d) -def test_cached_matrix_identity_and_x(highp): - A1 = _cached_matrix("single", "I", d=3, omega=None, key=()) - A2 = _cached_matrix("single", "I", d=3, omega=None, key=()) - assert A1 is A2 - - X1 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.1, 0, 1)) - X2 = _cached_matrix("single", "RX", d=3, omega=None, key=(0.2, 0, 1)) - assert X1 is not X2 - assert X1.shape == (3, 3) and X2.shape == (3, 3) - - -def test_builders_smoke(highp): - d = 3 - for name, (sig, _) in SINGLE_BUILDERS.items(): - defaults = { - "theta": 0.1, - "gamma": 0.1, - "z": 0.1, - "eps": 0.1, - "j": 0, - "k": 1, # ensure j != k when present - } - key = tuple(defaults.get(s, 0) for s in sig) - M = _cached_matrix("single", name, d, None, key) - assert M.shape == (d, d) - - for name, (sig, _) in TWO_BUILDERS.items(): - defaults = {"theta": 0.1, "j1": 0, "k1": 1, "j2": 0, "k2": 1, "cv": None} - key = tuple(defaults[s] for s in sig) - M = _cached_matrix("two", name, d, None, key) - assert M.shape == (d * d, d * d) - - -def test_cached_matrix_np_scalar_and_non_scalar_omega(): - d = 3 - if "RX" in SINGLE_BUILDERS: - name = "RX" - sig, _ = SINGLE_BUILDERS[name] - defaults = {"theta": np.float64(0.314159265), "j": 0, "k": 1} - key = tuple(defaults.get(s, 0) for s in sig if s != "none") - M = _cached_matrix("single", name, d, None, key) - assert M.shape == (d, d) - - name = "I" if "I" in SINGLE_BUILDERS else next(iter(SINGLE_BUILDERS.keys())) - sig, _ = SINGLE_BUILDERS[name] - key = tuple(0 for s in sig if s != "none") - M2 = _cached_matrix("single", name, d, np.array([0.5]), key) - assert M2.shape == (d, d) - - def test__is_prime_edge_and_composites(): assert _is_prime(2) is True assert _is_prime(3) is True From 507c3ab3ef31250c85e71fd159224fdc982e4811 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 19:28:36 +0800 Subject: [PATCH 52/64] optimized codes --- tensorcircuit/quditgates.py | 111 +++++++++++++++--------------------- 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 1c26eeec..47e8d402 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -45,7 +45,6 @@ } - def _is_prime(n: int) -> bool: """ Check whether a number is prime. @@ -92,11 +91,8 @@ def _x_matrix_func(d: int) -> Tensor: :return: ``(d, d)`` matrix for :math:`X_d`. :rtype: Tensor """ - I = backend.eye(d, dtype=dtypestr) - X = backend.zeros((d, d), dtype=dtypestr) - for t in range(d): - X += backend.outer_product(I[:, (t + 1) % d], I[:, t]) - return X + m = np.roll(np.eye(d), shift=1, axis=0) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -112,11 +108,9 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` matrix for :math:`Z_d`. :rtype: Tensor """ - omega = num_to_tensor( - np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr - ) - j = backend.cast(backend.arange(d), dtype=dtypestr) - return backend.diagflat(backend.power(omega, j)) + omega = np.exp(2j * np.pi / d) if omega is None else omega + m = np.diag(omega ** np.arange(d)) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -132,13 +126,10 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` matrix for :math:`H_d`. :rtype: Tensor """ - omega = num_to_tensor( - np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr - ) - j, k = backend.cast(backend.arange(d), dtype=dtypestr), backend.cast( - backend.arange(d), dtype=dtypestr - ) - return omega ** backend.outer_product(k, j) / backend.sqrt(num_to_tensor(d)) + omega = np.exp(2j * np.pi / d) if omega is None else omega + j, k = np.arange(d), np.arange(d) + m = omega ** np.outer(j, k) / np.sqrt(d) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: @@ -154,15 +145,17 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :return: ``(d, d)`` diagonal matrix for :math:`S_d`. :rtype: Tensor """ - omega = num_to_tensor( - np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr - ) - j = backend.arange(d) + omega = np.exp(2j * np.pi / d) if omega is None else omega pd = 0 if d % 2 == 0 else 1 - return backend.diagflat(omega ** ((j * (j + pd)) / 2)) + j = np.arange(d) + m = np.diag(omega ** ((j * (j + pd)) / 2)) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _check_rotation_indices(d: int, *indices: int, distinct_pairs: bool = False) -> None: + +def _check_rotation_indices( + d: int, *indices: int, distinct_pairs: bool = False +) -> None: """ Validate that indices are within [0, d-1] and optionally form distinct pairs. @@ -185,7 +178,9 @@ def _check_rotation_indices(d: int, *indices: int, distinct_pairs: bool = False) if distinct_pairs and len(indices) == 4: j1, k1, j2, k2 = indices if j1 == k1 and j2 == k2: - raise ValueError("Selected basis states must be different: (j1, j2) ≠ (k1, k2).") + raise ValueError( + "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." + ) def _two_level_projectors( @@ -448,11 +443,9 @@ def _u8_matrix_func( f"Sum of v_k's is not 0 mod {d}. Got {sum(vks) % d}. Check parameters." ) - omega = num_to_tensor( - np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr - ) - vks_arr = backend.cast(backend.convert_to_tensor(np.array(vks)), dtype=dtypestr) - return backend.diagflat(backend.power(omega, vks_arr)) + omega = np.exp(2j * np.pi / d) if omega is None else omega + m = np.diag([omega ** vks[j] for j in range(d)]) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _cphase_matrix_func( @@ -479,28 +472,20 @@ def _cphase_matrix_func( :rtype: Tensor :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ - omega = num_to_tensor( - np.exp(2j * np.pi / d) if omega is None else omega, dtype=dtypestr - ) - j = backend.arange(d) - I = backend.eye(d, dtype=dtypestr) + omega = np.exp(2j * np.pi / d) if omega is None else omega + r = np.arange(d).reshape(-1, 1) + s = np.arange(d).reshape(1, -1) if cv is None: - m = backend.zeros((d * d, d * d), dtype=dtypestr) - for a in range(d): - Pa = backend.outer_product(I[:, a], I[:, a]) - Z_a = backend.diagflat(omega ** (a * j)) - m += backend.kron(Pa, Z_a) - return m - - if not (0 <= cv < d): - raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") + phase = omega ** (r * s) + else: + if not (0 <= cv < d): + raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") + phase = 1 + (r == cv) * (omega**s - 1) - Z = backend.diagflat(omega**j) - m = backend.kron(I, I) + backend.kron( - backend.outer_product(I[:, cv], I[:, cv]), (Z - I) - ) - return m + diag = np.ravel(phase) + m = np.diag(diag) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: @@ -523,25 +508,21 @@ def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: :rtype: Tensor :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. """ - I = backend.eye(d, dtype=dtypestr) + I = np.eye(d) if cv is None: - m = backend.zeros((d * d, d * d), dtype=dtypestr) - for a in range(d): - Pa = backend.outer_product(I[:, a], I[:, a]) - Xa = backend.zeros((d, d), dtype=dtypestr) - for t in range(d): - Xa += backend.outer_product(I[:, (t + a) % d], I[:, t]) - m += backend.kron(Pa, Xa) - return m + blocks = [np.roll(I, shift=r, axis=0) for r in range(d)] + m = np.block( + [ + [blocks[r] if r == c else np.zeros((d, d)) for c in range(d)] + for r in range(d) + ] + ) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) if not (0 <= cv < d): raise ValueError(f"cv must be in [0, {d - 1}], got {cv}") - X = backend.zeros((d, d), dtype=dtypestr) - for t in range(d): - X += backend.outer_product(I[:, (t + 1) % d], I[:, t]) - - return backend.kron(I, I) + backend.kron( - backend.outer_product(I[:, cv], I[:, cv]), (X - I) - ) + X = np.roll(I, shift=1, axis=0) + m = np.kron(I, I) + np.kron(np.outer(I[:, cv], I[:, cv]), (X - I)) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) From ee11fd218dc7b5a00cf614398b4e36d003728069 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 19:29:06 +0800 Subject: [PATCH 53/64] remove un-used import. --- tests/test_quditgates.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 0361567c..4874d471 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -24,8 +24,6 @@ _cphase_matrix_func, _csum_matrix_func, _is_prime, - SINGLE_BUILDERS, - TWO_BUILDERS, ) From 9849ee09588e1d1e0ee9f7c77a2893747b20847e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 20:03:03 +0800 Subject: [PATCH 54/64] fixed a bug of qudit swap --- tensorcircuit/quditgates.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 47e8d402..9927ebd8 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -296,8 +296,9 @@ def _swap_matrix_func(d: int) -> Tensor: :rtype: Tensor """ D = d * d - I = backend.eye(D, dtype=dtypestr) - return I.reshape(d, d, d, d).transpose(1, 0, 2, 3).reshape(D, D) + I = np.eye(D, dtype=dtypestr) + m = I.reshape(d, d, d, d).transpose(1, 0, 2, 3).reshape(D, D) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _rzz_matrix_func( From a5246ae777fad7fbdc48c4541dbe378f427ee0b3 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 20:04:51 +0800 Subject: [PATCH 55/64] add backend as para for tests/ --- tests/test_quditgates.py | 63 +++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index 4874d471..fc377544 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -1,13 +1,17 @@ import sys import os -import pytest import numpy as np +import pytest +from pytest_lazyfixture import lazy_fixture as lf + thisfile = os.path.abspath(__file__) modulepath = os.path.dirname(os.path.dirname(thisfile)) sys.path.insert(0, modulepath) +import tensorcircuit as tc + from tensorcircuit.quditgates import ( _i_matrix_func, _x_matrix_func, @@ -28,6 +32,7 @@ def is_unitary(M): + M = tc.backend.numpy(M) Mc = M.astype(np.complex128, copy=False) I = np.eye(M.shape[0], dtype=np.complex128) return np.allclose(Mc.conj().T @ Mc, I, atol=1e-5, rtol=1e-5) and np.allclose( @@ -36,19 +41,22 @@ def is_unitary(M): @pytest.mark.parametrize("d", [2, 3, 4, 5]) -def test_I_X_Z_shapes_and_unitarity(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_I_X_Z_shapes_and_unitarity(d, backend, highp): I = _i_matrix_func(d) X = _x_matrix_func(d) Z = _z_matrix_func(d) assert I.shape == (d, d) and X.shape == (d, d) and Z.shape == (d, d) assert is_unitary(X) assert is_unitary(Z) - np.testing.assert_allclose(I, np.eye(d), atol=1e-5) + np.testing.assert_allclose(tc.backend.numpy(I), np.eye(d), atol=1e-5) @pytest.mark.parametrize("d", [2, 3, 4]) -def test_X_is_right_cyclic_shift(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_X_is_right_cyclic_shift(d, backend, highp): X = _x_matrix_func(d) + X = tc.backend.numpy(X) for j in range(d): v = np.zeros(d) v[j] = 1 @@ -59,10 +67,13 @@ def test_X_is_right_cyclic_shift(d, highp): @pytest.mark.parametrize("d", [2, 3, 5]) -def test_Z_diagonal_and_value(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_Z_diagonal_and_value(d, backend, highp): omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) - np.testing.assert_allclose(Z, np.diag([omega**j for j in range(d)]), atol=1e-5) + np.testing.assert_allclose( + tc.backend.numpy(Z), np.diag([omega**j for j in range(d)]), atol=1e-5 + ) assert is_unitary(Z) @@ -75,7 +86,8 @@ def test_Z_diagonal_and_value(d, highp): @pytest.mark.parametrize("d", [2, 3, 5]) -def test_H_is_fourier_like_and_unitary(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_H_is_fourier_like_and_unitary(d, backend, highp): H = _h_matrix_func(d) assert H.shape == (d, d) assert is_unitary(H) @@ -83,24 +95,25 @@ def test_H_is_fourier_like_and_unitary(d, highp): F = (1 / np.sqrt(d)) * np.array( [[omega ** (j * k) for k in range(d)] for j in range(d)] ).T - np.testing.assert_allclose( - H.astype(np.complex128), F.astype(np.complex128), atol=1e-5, rtol=1e-5 - ) + np.testing.assert_allclose(tc.backend.numpy(H), F, atol=1e-5, rtol=1e-5) @pytest.mark.parametrize("d", [2, 3, 5]) -def test_S_is_diagonal(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_S_is_diagonal(d, backend, highp): S = _s_matrix_func(d) np.testing.assert_allclose(S, np.diag(np.diag(S)), atol=1e-5) @pytest.mark.parametrize("d", [3, 5]) -def test_RX_RY_only_affect_subspace(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_RX_RY_only_affect_subspace(d, backend, highp): theta = 0.7 j, k = 0, 1 RX = _rx_matrix_func(d, theta, j, k) RY = _ry_matrix_func(d, theta, j, k) assert is_unitary(RX) and is_unitary(RY) + RX, RY = tc.backend.numpy(RX), tc.backend.numpy(RY) for t in range(d): if t not in (j, k): e = np.zeros(d) @@ -111,7 +124,8 @@ def test_RX_RY_only_affect_subspace(d, highp): np.testing.assert_allclose(outy, e, atol=1e-5) -def test_RZ_phase_on_single_level(highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_RZ_phase_on_single_level(backend, highp): d, theta, j = 5, 1.234, 2 RZ = _rz_matrix_func(d, theta, j) assert is_unitary(RZ) @@ -121,11 +135,13 @@ def test_RZ_phase_on_single_level(highp): @pytest.mark.parametrize("d", [2, 3, 5]) -def test_SWAP_permutation(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_SWAP_permutation(d, backend, highp): SW = _swap_matrix_func(d) D = d * d assert SW.shape == (D, D) assert is_unitary(SW) + SW = tc.backend.numpy(SW) for i in range(min(d, 3)): for j in range(min(d, 3)): v = np.zeros(D) @@ -137,7 +153,8 @@ def test_SWAP_permutation(d, highp): @pytest.mark.parametrize("d", [2, 3, 5]) -def test_RZZ_diagonal(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_RZZ_diagonal(d, backend, highp): theta = 0.37 RZZ = _rzz_matrix_func(d, theta, j1=0, k1=1, j2=0, k2=1) assert is_unitary(RZZ) @@ -156,7 +173,8 @@ def test_RZZ_diagonal(d, highp): np.testing.assert_allclose(RZZ[idx_b, idx_b], np.exp(+1j * theta / 2), atol=1e-5) -def test_RXX_selected_block(highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_RXX_selected_block(backend, highp): d = 4 theta = 0.81 j1, k1 = 0, 2 @@ -175,7 +193,8 @@ def test_RXX_selected_block(highp): @pytest.mark.parametrize("d", [3, 5]) -def test_CPHASE_blocks(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_CPHASE_blocks(d, backend, highp): omega = np.exp(2j * np.pi / d) Z = _z_matrix_func(d, omega) M = _cphase_matrix_func(d, cv=None, omega=omega) @@ -198,7 +217,8 @@ def test_CPHASE_blocks(d, highp): @pytest.mark.parametrize("d", [3, 5]) -def test_CSUM_blocks(d, highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_CSUM_blocks(d, backend, highp): X = _x_matrix_func(d) M = _csum_matrix_func(d, cv=None) for a in range(d): @@ -219,9 +239,11 @@ def test_CSUM_blocks(d, highp): np.testing.assert_allclose(block, np.eye(d), atol=1e-5) -def test_CSUM_mapping_small_d(highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_CSUM_mapping_small_d(backend, highp): d = 3 M = _csum_matrix_func(d) + M = tc.backend.numpy(M) for r in range(d): for s in range(d): v = np.zeros(d * d) @@ -242,7 +264,8 @@ def test_rotation_index_errors(highp): _rx_matrix_func(d, 0.1, j=2, k=2) -def test_U8_errors_and_values(highp): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_U8_errors_and_values(backend, highp): with pytest.raises(ValueError): _u8_matrix_func(d=4) with pytest.raises(ValueError): From 7a8759e474fa6a8a6ca2aec951bf5fed202799b7 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 20:58:01 +0800 Subject: [PATCH 56/64] add ref to u8 gate and fixed logical bug of this --- tensorcircuit/quditgates.py | 115 ++++++++++++++++++++++-------------- tests/test_quditgates.py | 85 +++++++++++++++++++++----- 2 files changed, 142 insertions(+), 58 deletions(-) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 9927ebd8..a4e17ed8 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -95,7 +95,7 @@ def _x_matrix_func(d: int) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: +def _z_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Generalized Pauli-Z on a ``d``-level system. @@ -104,7 +104,7 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :param d: Qudit dimension. :type d: int :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. - :type omega: Optional[float] + :type omega: Optional[complex] :return: ``(d, d)`` matrix for :math:`Z_d`. :rtype: Tensor """ @@ -113,7 +113,7 @@ def _z_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: +def _h_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Discrete Fourier transform (Hadamard-like) on ``d`` levels. @@ -122,7 +122,7 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :param d: Qudit dimension. :type d: int :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. - :type omega: Optional[float] + :type omega: Optional[complex] :return: ``(d, d)`` matrix for :math:`H_d`. :rtype: Tensor """ @@ -132,7 +132,7 @@ def _h_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: +def _s_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Diagonal phase gate ``S_d`` on ``d`` levels. @@ -141,7 +141,7 @@ def _s_matrix_func(d: int, omega: Optional[float] = None) -> Tensor: :param d: Qudit dimension. :type d: int :param omega: Optional primitive ``d``-th root of unity. Defaults to :math:`e^{2\pi i/d}`. - :type omega: Optional[float] + :type omega: Optional[complex] :return: ``(d, d)`` diagonal matrix for :math:`S_d`. :rtype: Tensor """ @@ -385,59 +385,88 @@ def _rxx_matrix_func( def _u8_matrix_func( d: int, - gamma: float = 2.0, - z: float = 1.0, - eps: float = 0.0, - omega: Optional[float] = None, + gamma: int = 2, + z: int = 1, + eps: int = 0, + omega: Optional[complex] = None, ) -> Tensor: r""" ``U8`` diagonal single-qudit gate for prime dimensions. - This gate represents a canonical nontrivial diagonal Clifford element - in prime-dimensional qudit systems. Together with generalized Pauli - operators, it generates the full single-qudit Clifford group. In the - qubit case (``d=2``), it reduces to the well-known π/8 gate. For higher - prime dimensions, the phases are defined through modular polynomials - depending on :math:`\gamma, z, \epsilon`. Its explicit inclusion ensures - coverage of the complete Clifford generating set across prime qudit - dimensions. + See ref: Howard, Mark, and Jiri Vala. + "Qudit versions of the qubit π/8 gate." Physical Review A 86, no. 2 (2012): 022316. + https://doi.org/10.1103/PhysRevA.86.022316 + + This gate is the qudit analogue of the qubit :math:`\pi/8` gate, defined in + *Howard & Campbell, Phys. Rev. A 86, 022316 (2012)*. It is diagonal in the + computational basis with exponents determined by modular polynomials in the + parameters :math:`\gamma, z, \epsilon`. These gates, together with Pauli and + Clifford operations, generate the full single-qudit Clifford hierarchy. + + - For :math:`d=2`, this reduces (up to global phase) to the standard qubit + :math:`\pi/8` gate. + - For :math:`d=3`, the exponents live in :math:`\mathbb{Z}_9` and the + primitive ninth root :math:`\zeta = e^{2\pi i/9}` is used. + - For prime :math:`d>3`, the construction uses the modular inverse of 12 in + :math:`\mathbb{Z}_d`. :param d: Qudit dimension (must be prime). :type d: int - :param gamma: Gate parameter (must be non-zero). - :type gamma: float - :param z: Gate parameter. - :type z: float - :param eps: Gate parameter. - :type eps: float - :param omega: Optional primitive :math:`d`-th root of unity. Defaults to :math:`e^{2\pi i/d}`. - :type omega: Optional[float] + :param gamma: Shear parameter :math:`\gamma' \in \mathbb{Z}_d`. + If ``gamma = 0``, the gate is a diagonal Clifford. + If ``gamma ≠ 0``, the gate is a genuine non-Clifford (analogue of :math:`\pi/8`). + :type gamma: int + :param z: Displacement parameter :math:`z' \in \mathbb{Z}_d`, + which sets the symplectic part of the associated Clifford. + :type z: int + :param eps: Phase offset parameter :math:`\epsilon' \in \mathbb{Z}_d`. + It only contributes a global phase factor :math:`\omega^{\epsilon'}`. + :type eps: int + :param omega: Optional primitive :math:`d`-th root of unity (complex). + Defaults to :math:`e^{2\pi i/d}` for d>3, and :math:`e^{2\pi i/9}` for d=3. + :type omega: Optional[complex] :return: ``(d, d)`` diagonal matrix of dtype ``npdtype``. :rtype: Tensor - :raises ValueError: If ``d`` is not prime; if ``gamma==0``; if 12 has no modular - inverse mod ``d``; or if the computed exponents do not sum to 0 mod ``d``. + :raises ValueError: If ``d`` is not prime; if 12 has no modular inverse + mod ``d`` (for ``d>3``); or if the computed exponents do not sum to + 0 mod ``d`` (or 0 mod 3 for ``d=3``). """ if not _is_prime(d): raise ValueError( f"Dimension d={d} is not prime, U8 gate requires a prime dimension." ) - if gamma == 0.0: - raise ValueError("gamma must be non-zero") - vks = [0] * d + omega = np.exp(2j * np.pi / d) if omega is None else omega + + gamma = int(gamma) % d + z = int(z) % d + eps = int(eps) % d + if d == 3: - vks = [0, 1, 8] - else: - try: - inv_12 = pow(12, -1, d) - except ValueError: + vks = [0, (6 * z + 2 * gamma + 3 * eps) % 9, (6 * z + 1 * gamma + 6 * eps) % 9] + if sum(vks) % 3 != 0: raise ValueError( - f"Inverse of 12 mod {d} does not exist. Choose a prime d that does not divide 12." + f"Sum of v_k's is not 0 mod 3. Got {sum(vks) % 3}. Check parameters." ) - for i in range(1, d): - a = inv_12 * i * (gamma + i * (6 * z + (2 * i - 3) * gamma)) + eps * i - vks[i] = int(a) % d + zeta = np.exp(2j * np.pi / 9) + m = np.diag([zeta**v for v in vks]) + return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) + + try: + inv_12 = pow(12, -1, d) + except ValueError: + raise ValueError( + f"Inverse of 12 mod {d} does not exist. Choose a prime d that does not divide 12." + ) + + vks = [0] * d + for k in range(1, d): + term_inner = ((6 * z) % d + ((2 * k - 3) % d) * gamma) % d + term = (gamma + (k * term_inner) % d) % d + vk = ((inv_12 * (k % d)) % d) * term % d + vk = (vk + (eps * (k % d)) % d) % d + vks[k] = vk if sum(vks) % d != 0: raise ValueError( @@ -445,12 +474,12 @@ def _u8_matrix_func( ) omega = np.exp(2j * np.pi / d) if omega is None else omega - m = np.diag([omega ** vks[j] for j in range(d)]) + m = np.diag([omega**v for v in vks]) return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) def _cphase_matrix_func( - d: int, cv: Optional[int] = None, omega: Optional[float] = None + d: int, cv: Optional[int] = None, omega: Optional[complex] = None ) -> Tensor: r""" Qudit controlled-phase (``CPHASE``) gate. @@ -468,7 +497,7 @@ def _cphase_matrix_func( :param cv: Optional control value in ``[0, d-1]``. If ``None``, builds the full SUMZ block-diagonal. :type cv: Optional[int] :param omega: Optional primitive ``d``-th root of unity for ``Z_d``. - :type omega: Optional[float] + :type omega: Optional[complex] :return: ``(d*d, d*d)`` matrix representing the controlled-phase. :rtype: Tensor :raises ValueError: If ``cv`` is provided and is outside ``[0, d-1]``. diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index fc377544..a0612cdb 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -264,19 +264,6 @@ def test_rotation_index_errors(highp): _rx_matrix_func(d, 0.1, j=2, k=2) -@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) -def test_U8_errors_and_values(backend, highp): - with pytest.raises(ValueError): - _u8_matrix_func(d=4) - with pytest.raises(ValueError): - _u8_matrix_func(d=3, gamma=0.0) - d = 3 - U = _u8_matrix_func(d, gamma=2.0, z=1.0, eps=0.0) - omega = np.exp(2j * np.pi / d) - expected = np.diag([omega**0, omega**1, omega**8]) - assert np.allclose(U, expected, atol=1e-5) - - def test_CPHASE_CSUM_cv_range(highp): d = 5 with pytest.raises(ValueError): @@ -311,6 +298,74 @@ def test_two_qudit_builders_index_validation(): _rxx_matrix_func(d, theta, j1=2, k1=2, j2=0, k2=0) -def test_u8_requires_prime_dimension(): +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_u8_prime_dimension_and_qubit_case(backend): + with pytest.raises(ValueError): + _u8_matrix_func(d=4) with pytest.raises(ValueError): - _u8_matrix_func(d=9, gamma=1.0, z=0.0, eps=0.0) + _u8_matrix_func(d=9, gamma=1, z=0, eps=0) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_u8_qutrit_correct_phases_and_gamma_zero_allowed(backend): + d = 3 + U3 = _u8_matrix_func(d=d, gamma=2, z=1, eps=0) + zeta = np.exp(2j * np.pi / 9) + expected3 = np.diag([zeta**0, zeta**1, zeta**8]) + assert np.allclose(tc.backend.numpy(U3), expected3, atol=1e-12) + + U3_g0 = _u8_matrix_func(d=d, gamma=0, z=1, eps=2) + U3_g0 = tc.backend.numpy(U3_g0) + assert U3_g0.shape == (3, 3) + assert np.allclose(U3_g0, np.diag(np.diag(U3_g0)), atol=1e-12) + assert np.allclose(U3_g0.conj().T @ U3_g0, np.eye(3), atol=1e-12) + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_u8_p_greater_than_3_matches_closed_form(backend): + d = 5 + gamma, z, eps = 2, 1, 3 + + inv_12 = pow(12, -1, d) # 12 \equiv 2 (mod 5), inverse = 3 + vks = [0] * d + for k in range(1, d): + k_ = k % d + term_inner = ((6 * z) % d + ((2 * k_ - 3) % d) * gamma % d) % d + term = (gamma + (k_ * term_inner) % d) % d + vk = ((inv_12 * k_) % d) * term % d + vk = (vk + (eps * k_) % d) % d + vks[k] = vk + + omega = np.exp(2j * np.pi / d) + expected5 = np.diag([omega**v for v in vks]) + + U5 = _u8_matrix_func(d=d, gamma=gamma, z=z, eps=eps) + assert np.allclose(tc.backend.numpy(U5), expected5, atol=1e-12) + assert sum(vks) % d == 0 + + +@pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) +def test_u8_parameter_normalization_and_custom_omega(backend): + d = 5 + U_modded = _u8_matrix_func(d=d, gamma=2, z=1, eps=3) + U_unnormalized = _u8_matrix_func( + d=d, gamma=7, z=-4, eps=13 + ) # 7\equiv 2, -4\equiv 1, 13\equiv 3 (mod 5) + assert np.allclose(U_modded, U_unnormalized, atol=1e-12) + + d = 7 + gamma, z, eps = 3, 2, 1 + inv_12 = pow(12, -1, d) + vks = [0] * d + for k in range(1, d): + k_ = k % d + term_inner = ((6 * z) % d + ((2 * k_ - 3) % d) * gamma % d) % d + term = (gamma + (k_ * term_inner) % d) % d + vk = ((inv_12 * k_) % d) * term % d + vk = (vk + (eps * k_) % d) % d + vks[k] = vk + + omega_custom = np.exp(2j * np.pi / d) * np.exp(0j) + U7_custom = _u8_matrix_func(d=d, gamma=gamma, z=z, eps=eps, omega=omega_custom) + expected7_custom = np.diag([omega_custom**v for v in vks]) + assert np.allclose(tc.backend.numpy(U7_custom), expected7_custom, atol=1e-12) From 80b04af58a3b8ea70a9148f8a61c6d23d4420610 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 21:23:32 +0800 Subject: [PATCH 57/64] move jit, autograd, vmap tests to test_quditcircuit.py --- tests/test_quditcircuit.py | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 76283bca..179827d5 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -348,6 +348,84 @@ def test_quditcircuit_set_dim_validation(): tc.QuditCircuit(1, 2.5) # type: ignore[arg-type] +@pytest.mark.parametrize("backend", [lf("jaxb"), lf("tfb"), lf("torchb")]) +def test_qudit_minimal_ad_qudit(backend): + """Minimal AD test on a single-qudit (d=3) circuit. + We differentiate the expectation ⟨Z⟩ w.r.t. a single RY parameter and + compare to a finite-difference estimate. + """ + + dim = 3 + + def energy(theta): + c = tc.QuditCircuit(1, dim) + # rotate on the (0,1) subspace so that the observable is sensitive to theta + c.ry(0, theta=theta, j=0, k=1) + # measure Z on site 0 (qudit Z for d=3) + E = c.expectation((tc.quditgates._z_matrix_func(dim), [0])) + return tc.backend.real(E) + + # backend autodiff gradient + grad_energy = tc.backend.grad(energy) + + theta0 = tc.num_to_tensor(0.37) + g = grad_energy(theta0) + + # finite-difference check + eps = 1e-3 + num = (energy(theta0 + eps) - energy(theta0 - eps)) / (2 * eps) + + np.testing.assert_allclose( + tc.backend.numpy(g), tc.backend.numpy(num), rtol=1e-2, atol=1e-3 + ) + + +@pytest.mark.parametrize("backend", [lf("jaxb"), lf("tfb"), lf("torchb")]) +def test_qudit_minimal_jit_qudit(backend): + """Minimal JIT test: jit-compiled energy matches eager energy.""" + + dim = 3 + + def energy(theta): + c = tc.QuditCircuit(1, dim) + c.ry(0, theta=theta, j=0, k=1) + E = c.expectation((tc.quditgates._z_matrix_func(dim), [0])) + return tc.backend.real(E) + + jit_energy = tc.backend.jit(energy) + + theta0 = tc.num_to_tensor(-0.91) + e_eager = energy(theta0) + e_jit = jit_energy(theta0) + + np.testing.assert_allclose( + tc.backend.numpy(e_jit), tc.backend.numpy(e_eager), rtol=1e-6, atol=1e-6 + ) + + +@pytest.mark.parametrize("backend", [lf("jaxb"), lf("tfb"), lf("torchb")]) +def test_qudit_minimal_vmap_qudit(backend): + """Minimal VMAP test: vectorized energies equal per-element eager results.""" + + dim = 3 + + def energy(theta): + c = tc.QuditCircuit(1, dim) + c.ry(0, theta=theta, j=0, k=1) + E = c.expectation((tc.quditgates._z_matrix_func(dim), [0])) + return tc.backend.real(E) + + venergy = tc.backend.vmap(energy) + + thetas = tc.array_to_tensor(np.linspace(-1.0, 1.0, 7)) + vvals = venergy(thetas) + eager_vals = np.array([energy(t) for t in thetas]) + + np.testing.assert_allclose( + tc.backend.numpy(vvals), eager_vals, rtol=1e-6, atol=1e-6 + ) + + def test_quditcircuit_single_and_two_qudit_paths_and_wrappers(): c = tc.QuditCircuit(2, 3) c.x(0) From 8ff69811c1bea17191d99380d069b418e04e5b9f Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 22:04:54 +0800 Subject: [PATCH 58/64] Add an example for qudit vqe. --- examples/vqe_qudit_example.py | 277 ++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 examples/vqe_qudit_example.py diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py new file mode 100644 index 00000000..d95ff3e3 --- /dev/null +++ b/examples/vqe_qudit_example.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +vqe_qudit_example.py + +A clean, backend-explicit VQE example for qudits using tensorcircuit. +You must set the backend explicitly via --backend {jax,tensorflow,torch}. +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)⊗X_sym(1) + Z_sym(0)⊗Z_sym(1) ] + where N = diag(0,1,...,d-1), X_sym = (X + X^†)/2, Z_sym = (Z + Z^†)/2. +""" + +import os + +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])] + 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( + 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) + + 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 + 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() From 768b003660e489bb232f7fb03a53c24b34ce1b88 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Fri, 5 Sep 2025 22:08:52 +0800 Subject: [PATCH 59/64] format --- examples/vqe_qudit_example.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index d95ff3e3..4604af16 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -1,10 +1,5 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -vqe_qudit_example.py - -A clean, backend-explicit VQE example for qudits using tensorcircuit. -You must set the backend explicitly via --backend {jax,tensorflow,torch}. +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. @@ -17,12 +12,10 @@ - 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)⊗X_sym(1) + Z_sym(0)⊗Z_sym(1) ] - where N = diag(0,1,...,d-1), X_sym = (X + X^†)/2, Z_sym = (Z + Z^†)/2. + 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 os - import argparse import math import sys @@ -257,6 +250,7 @@ def obj_np(theta_np): 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) From 68bb073897fa6e22e86a998bfd06e1b945379699 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Sat, 6 Sep 2025 12:16:50 +0800 Subject: [PATCH 60/64] remove quditvqe test. --- tests/test_quditvqe.py | 72 ------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 tests/test_quditvqe.py diff --git a/tests/test_quditvqe.py b/tests/test_quditvqe.py deleted file mode 100644 index c23e6e4a..00000000 --- a/tests/test_quditvqe.py +++ /dev/null @@ -1,72 +0,0 @@ -# tests/test_quditvqe.py -# pylint: disable=invalid-name - -import os -import sys - -import numpy as np -import pytest -from pytest_lazyfixture import lazy_fixture as lf - -thisfile = os.path.abspath(__file__) -modulepath = os.path.dirname(os.path.dirname(thisfile)) -sys.path.insert(0, modulepath) - -import tensorcircuit as tc - - -def vqe_cost(params, d: int = 3): - c = tc.QuditCircuit(2, dim=d) - c.rx(0, params[0], j=0, k=1) - c.ry(1, params[1], j=0, k=1) - c.csum(0, 1) - - z0 = tc.backend.cast( - tc.backend.convert_to_tensor(np.diag([1, -1, 0])), - dtype=tc.cons.dtypestr, - ) - z1 = tc.backend.cast( - tc.backend.convert_to_tensor(np.diag([1, -1, 0])), - dtype=tc.cons.dtypestr, - ) - op = tc.backend.kron(z0, z1) - - wf = c.wavefunction() - energy = tc.backend.real( - tc.backend.einsum("i,ij,j->", tc.backend.adjoint(wf), op, wf) - ) - return energy - - -@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) -def test_autodiff(backend): - params = tc.backend.cast( - tc.backend.convert_to_tensor(np.array([0.1, 0.2])), dtype=tc.cons.rdtypestr - ) - grad_fn = tc.backend.grad(lambda p: vqe_cost(p)) - g = grad_fn(params) - assert g is not None - assert tc.backend.shape_tuple(g) == tc.backend.shape_tuple(params) - - -@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) -def test_jit(backend): - params = tc.backend.cast( - tc.backend.convert_to_tensor(np.array([0.1, 0.2])), dtype=tc.cons.rdtypestr - ) - f = lambda p: vqe_cost(p) - f_jit = tc.backend.jit(f) - e1 = f(params) - e2 = f_jit(params) - np.testing.assert_allclose(e1, e2, rtol=1e-5, atol=1e-6) - - -@pytest.mark.parametrize("backend", [lf("tfb"), lf("jaxb"), lf("torchb")]) -def test_vmap(backend): - params_batch = tc.backend.cast( - tc.backend.convert_to_tensor(np.array([[0.1, 0.2], [0.3, 0.4]])), - dtype=tc.cons.rdtypestr, - ) - f_batched = tc.backend.vmap(lambda p: vqe_cost(p)) - vals = f_batched(params_batch) - assert tc.backend.shape_tuple(vals) == (2,) From 8aa269559bd13d9fad44b15ae4cdde61276a4d11 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Sat, 6 Sep 2025 12:20:46 +0800 Subject: [PATCH 61/64] change function name --- tests/test_quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 179827d5..3ba18592 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -426,7 +426,7 @@ def energy(theta): ) -def test_quditcircuit_single_and_two_qudit_paths_and_wrappers(): +def test_qudit_paths_and_sampling_wrappers(): c = tc.QuditCircuit(2, 3) c.x(0) c.rzz(0, 1, theta=np.float64(0.2), j1=0, k1=1, j2=0, k2=1) From 53669a242db1f8012a45b6df5f9fa8baf87e022e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Sat, 6 Sep 2025 12:21:09 +0800 Subject: [PATCH 62/64] add test for len(nodes) in amplitude_before_test --- tests/test_quditcircuit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 3ba18592..054389d3 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -444,3 +444,4 @@ def test_quditcircuit_amplitude_before_wrapper(): c.x(0) nodes = c.amplitude_before("00") assert isinstance(nodes, list) + assert len(nodes) == 5 # one gate (X on qudit 0) -> single node in the traced path From 52f9804510b3dde1ad93a27e211e698c64f7c38f Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Sat, 6 Sep 2025 13:08:28 +0800 Subject: [PATCH 63/64] optimized examples/vqe_qudit_example.py according to comments. --- examples/vqe_qudit_example.py | 94 ++++++++++++++--------------------- 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index 4604af16..deae1752 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -19,8 +19,7 @@ import argparse import math import sys -from dataclasses import dataclass -from typing import List, Sequence, Tuple +from typing import Sequence, Tuple import numpy as np import tensorcircuit as tc @@ -28,57 +27,19 @@ # ---------- 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)) +def build_2site_hamiltonian( + d: int, J: float +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]: + N = np.diag(np.arange(d)) + Xsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._x_matrix_func(d))) + Zsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._z_matrix_func(d))) H0 = N.copy() H1 = N.copy() - HXX = kron2(Xsym, Xsym) - HZZ = kron2(Zsym, Zsym) - H01 = J * (HXX + HZZ) - return Hamiltonian2Qudit(H0, H1, H01) + return H0, H1, Xsym, Zsym, J # ---------- Ansatz ---------- @@ -127,7 +88,12 @@ def build_ansatz(nlayers: int, d: int, params: Sequence) -> QuditCircuit: # ---------- Energy ---------- -def energy_expectation_backend(params_b, d: int, nlayers: int, ham: Hamiltonian2Qudit): +def energy_expectation_backend( + params_b, + d: int, + nlayers: int, + ham: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float], +): """ params_b: 1D backend tensor (jax/tf) of shape [nparams]. Returns backend scalar. @@ -137,18 +103,30 @@ def energy_expectation_backend(params_b, d: int, nlayers: int, ham: Hamiltonian2 plist = [params_b[i] for i in range(params_b.shape[0])] 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))) + H0, H1, Xsym, Zsym, J = ham + # Local number operators + E = E + c.expectation((tc.gates.Gate(H0), [0])) + E = E + c.expectation((tc.gates.Gate(H1), [1])) + # Coupling terms as products on separate sites (avoids 9x9 reshaping issues) + E = E + J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1])) + E = E + J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1])) return bk.real(E) def energy_expectation_numpy( - params_np: np.ndarray, d: int, nlayers: int, ham: Hamiltonian2Qudit + params_np: np.ndarray, + d: int, + nlayers: int, + ham: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float], ) -> 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))) + H0, H1, Xsym, Zsym, J = ham + E += c.expectation((tc.gates.Gate(H0), [0])) + E += c.expectation((tc.gates.Gate(H1), [1])) + # Coupling terms as products on separate sites (avoids 9x9 reshaping issues) + E += J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1])) + E += J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1])) return float(np.real(E)) @@ -159,7 +137,7 @@ 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) + x = rng.uniform(-math.pi, math.pi, size=x0_shape) y = fun_numpy(x) if y < best_y: best_x, best_y = x, y @@ -172,11 +150,11 @@ def gradient_descent_ad(energy_bk, x0_np: np.ndarray, steps=200, lr=0.1, jit=Fal Simple gradient descent in numpy space with backend-gradients. """ bk = tc.backend - if jit and hasattr(bk, "jit"): + if jit: energy_bk = bk.jit(energy_bk) grad_f = bk.grad(energy_bk) - x_np = x0_np.astype(np.float32).copy() + x_np = x0_np.copy() best_x, best_y = x_np.copy(), float("inf") def to_np(x): @@ -221,7 +199,7 @@ def main(): ap.add_argument( "--jit", action="store_true", - help="enable backend JIT for energy/grad if available", + help="enable backend JIT (all backends implement .jit; numpy backend no-ops)", ) args = ap.parse_args() @@ -255,7 +233,7 @@ 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) + x0 = rng.uniform(-math.pi, math.pi, size=(nparams,)) x, y = gradient_descent_ad( obj_bk, x0_np=x0, steps=args.steps, lr=args.lr, jit=args.jit ) From d4d3d8dd12901baeb2ff98d48c23eecc3086bead Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Tue, 9 Sep 2025 14:22:24 +0800 Subject: [PATCH 64/64] refresh the whole file, making no complicated conversion --- examples/vqe_qudit_example.py | 360 +++++++++++++--------------------- 1 file changed, 141 insertions(+), 219 deletions(-) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index deae1752..88179557 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -1,248 +1,170 @@ -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. """ +VQE on QuditCircuits. -import argparse -import math -import sys -from typing import Sequence, Tuple +This example shows how to run a simple VQE on a qudit system using +`tensorcircuit.QuditCircuit`. We build a compact ansatz using single-qudit +rotations in selected two-level subspaces and RXX-type entanglers, then +optimize the energy of a Hermitian "clock–shift" Hamiltonian: -import numpy as np -import tensorcircuit as tc -from tensorcircuit.quditcircuit import QuditCircuit + H(d) = - J * (X_c \otimes X_c) - h * (Z_c \otimes I + I \otimes Z_c) +where, for local dimension `d`, +- Z_c = (Z + Z^\dagger)/2 is the Hermitian "clock" observable with Z = diag(1, \omega, \omega^2, ..., \omega^{d-1}) +- X_c = (S + S^\dagger)/2 is the Hermitian "shift" observable with S the cyclic shift +- \omega = exp(2\pi i/d) -# ---------- Hamiltonian helpers ---------- -def symmetrize_hermitian(U: np.ndarray) -> np.ndarray: - return 0.5 * (U + U.conj().T) +The code defaults to a 2-qutrit (d=3) problem but can be changed via CLI flags. +""" +# import os, sys +# +# base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +# if base_dir not in sys.path: +# sys.path.insert(0, base_dir) -def build_2site_hamiltonian( - d: int, J: float -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]: - N = np.diag(np.arange(d)) - Xsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._x_matrix_func(d))) - Zsym = symmetrize_hermitian(tc.backend.numpy(tc.quditgates._z_matrix_func(d))) - H0 = N.copy() - H1 = N.copy() - return H0, H1, Xsym, Zsym, J +import time +import argparse +import tensorcircuit as tc +tc.set_backend("jax") +tc.set_dtype("complex128") -# ---------- Ansatz ---------- +def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float): + """Build a QuditCircuit ansatz and compute ⟨H⟩. -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). + Ansatz: + [ for L in 1...nlayers ] + - On each site q: + RX(q; θ_Lq^(01)) ∘ RY(q; θ_Lq^(12)) ∘ RZ(q; φ_Lq^(0)) + (subspace indices shown as superscripts) + - Entangle neighboring pairs with RXX on subspaces (0,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: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float], -): - """ - 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])] - c = build_ansatz(nlayers, d, plist) - E = 0.0 + 0.0j - H0, H1, Xsym, Zsym, J = ham - # Local number operators - E = E + c.expectation((tc.gates.Gate(H0), [0])) - E = E + c.expectation((tc.gates.Gate(H1), [1])) - # Coupling terms as products on separate sites (avoids 9x9 reshaping issues) - E = E + J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1])) - E = E + J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1])) - return bk.real(E) - - -def energy_expectation_numpy( - params_np: np.ndarray, - d: int, - nlayers: int, - ham: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float], -) -> float: - c = build_ansatz(nlayers, d, params_np.tolist()) - E = 0.0 + 0.0j - H0, H1, Xsym, Zsym, J = ham - E += c.expectation((tc.gates.Gate(H0), [0])) - E += c.expectation((tc.gates.Gate(H1), [1])) - # Coupling terms as products on separate sites (avoids 9x9 reshaping issues) - E += J * c.expectation((tc.gates.Gate(Xsym), [0]), (tc.gates.Gate(Xsym), [1])) - E += J * c.expectation((tc.gates.Gate(Zsym), [0]), (tc.gates.Gate(Zsym), [1])) - 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) - 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: - energy_bk = bk.jit(energy_bk) - grad_f = bk.grad(energy_bk) + if d < 3: + raise ValueError("This example assblumes d >= 3 (qutrit or higher).") + + S = tc.quditgates._x_matrix_func(d) + Z = tc.quditgates._z_matrix_func(d) + Sdag = tc.backend.adjoint(S) + Zdag = tc.backend.adjoint(Z) + + c = tc.QuditCircuit(nqudits, dim=d) - x_np = x0_np.copy() - best_x, best_y = x_np.copy(), float("inf") + pairs = [(i, i + 1) for i in range(nqudits - 1)] - def to_np(x): - return x if isinstance(x, np.ndarray) else bk.numpy(x) + it = iter(param) - 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 - 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) + for _ in range(nlayers): + for q in range(nqudits): + c.rx(q, theta=next(it), j=0, k=1) + c.ry(q, theta=next(it), j=1, k=2) + c.rz(q, theta=next(it), j=0) + + for i, j in pairs: + c.rxx(i, j, theta=next(it), j1=0, k1=1, j2=0, k2=1) + + # H = -J * 1/2 (S_i S_j^\dagger + S_i^\dagger S_j) - h * 1/2 (Z + Z^\dagger) + energy = 0.0 + for i, j in pairs: + e_ij = 0.5 * ( + c.expectation((S, [i]), (Sdag, [j])) + c.expectation((Sdag, [i]), (S, [j])) + ) + energy += -J * tc.backend.real(e_ij) + for q in range(nqudits): + zq = 0.5 * (c.expectation((Z, [q])) + c.expectation((Zdag, [q]))) + energy += -h * tc.backend.real(zq) + return tc.backend.real(energy) -# ---------- CLI ---------- +def build_param_shape(nqudits: int, d: int, nlayers: int): + # Per layer per qudit: RX^(01), RY^(12) (or dummy), RZ^(0) = 3 params + # Per layer entanglers: len(pairs) parameters + pairs = nqudits - 1 + per_layer = 3 * nqudits + pairs + return (nlayers * per_layer,) def main(): - ap = argparse.ArgumentParser(description="Qudit VQE (explicit backend)") - ap.add_argument( - "--backend", - required=True, - choices=["jax", "tensorflow"], - help="tensorcircuit backend", + parser = argparse.ArgumentParser( + description="VQE on QuditCircuit (clock–shift model)" ) - 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", + parser.add_argument( + "--d", type=int, default=3, help="Local dimension per site (>=3)" ) - 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 (all backends implement .jit; numpy backend no-ops)", + parser.add_argument("--nqudits", type=int, default=2, help="Number of sites") + parser.add_argument("--nlayers", type=int, default=3, help="Ansatz depth (layers)") + parser.add_argument( + "--J", type=float, default=1.0, help="Coupling strength for XcXc term" ) - 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}" + parser.add_argument( + "--h", type=float, default=0.6, help="Field strength for Zc terms" ) - - 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 + parser.add_argument("--steps", type=int, default=200, help="Optimization steps") + parser.add_argument("--lr", type=float, default=0.05, help="Learning rate") + args = parser.parse_args() + + assert args.d >= 3, "d must be >= 3" + + shape = build_param_shape(args.nqudits, args.d, args.nlayers) + param = tc.backend.random_uniform(shape, boundaries=(-0.1, 0.1), seed=42) + + try: + import optax + + optimizer = optax.adam(args.lr) + vgf = tc.backend.jit( + tc.backend.value_and_grad( + lambda p: vqe_forward( + p, + nqudits=args.nqudits, + d=args.d, + nlayers=args.nlayers, + J=args.J, + h=args.h, + ) + ) ) - 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,)) - x, y = gradient_descent_ad( - obj_bk, x0_np=x0, steps=args.steps, lr=args.lr, jit=args.jit + opt_state = optimizer.init(param) + + @tc.backend.jit + def train_step(p, opt_state): + loss, grads = vgf(p) + updates, opt_state = optimizer.update(grads, opt_state, p) + p = optax.apply_updates(p, updates) + return p, opt_state, loss + + print("Starting VQE optimization (optax/adam)...") + loss = None + for i in range(args.steps): + t0 = time.time() + param, opt_state, loss = train_step(param, opt_state) + # ensure sync for accurate timing + _ = float(loss) + if i % 20 == 0: + dt = time.time() - t0 + print(f"Step {i:4d} loss={loss:.6f} dt/step={dt:.4f}s") + print("Final loss:", float(loss) if loss is not None else "n/a") + + except ModuleNotFoundError: + print("Optax not available; using naive gradient descent.") + value_and_grad = tc.backend.value_and_grad( + lambda p: vqe_forward( + p, + nqudits=args.nqudits, + d=args.d, + nlayers=args.nlayers, + J=args.J, + h=args.h, + ) ) - - 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)]) + lr = args.lr + loss = None + for i in range(args.steps): + loss, grads = value_and_grad(param) + param = param - lr * grads + if i % 20 == 0: + print(f"Step {i:4d} loss={float(loss):.6f}") + print("Final loss:", float(loss) if loss is not None else "n/a") if __name__ == "__main__":