From b7e7514b7811569880cdab8e165e12ddcded2d52 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Tue, 9 Sep 2025 21:33:21 +0800 Subject: [PATCH 01/10] making quditgate functions into pulic (by removing _), this fix the bug in docsing. --- examples/vqe_qudit_example.py | 4 +- tensorcircuit/quditgates.py | 112 +++++++++++++++++++++------------- tests/test_quditcircuit.py | 22 +++---- tests/test_quditgates.py | 106 ++++++++++++++++---------------- 4 files changed, 136 insertions(+), 108 deletions(-) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index 6cfa8129..6d18fb37 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -43,8 +43,8 @@ def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float if d < 3: raise ValueError("This example assumes d >= 3 (qutrit or higher).") - S = tc.quditgates._x_matrix_func(d) - Z = tc.quditgates._z_matrix_func(d) + S = tc.quditgates.x_matrix_func(d) + Z = tc.quditgates.z_matrix_func(d) Sdag = tc.backend.adjoint(S) Zdag = tc.backend.adjoint(Z) diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index a4e17ed8..40df6d77 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -1,3 +1,7 @@ +""" +Single-qudit and two-qudit gates and their corresponding matrix. +""" + from typing import Any, Optional, Tuple import numpy as np @@ -8,25 +12,25 @@ Tensor = Any SINGLE_BUILDERS = { - "I": (("none",), lambda d, omega, **kw: _i_matrix_func(d)), - "X": (("none",), lambda d, omega, **kw: _x_matrix_func(d)), - "Z": (("none",), lambda d, omega, **kw: _z_matrix_func(d, omega)), - "H": (("none",), lambda d, omega, **kw: _h_matrix_func(d, omega)), + "I": (("none",), lambda d, omega, **kw: i_matrix_func(d)), + "X": (("none",), lambda d, omega, **kw: x_matrix_func(d)), + "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"]), + 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"]), + 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"]), + lambda d, omega, **kw: rz_matrix_func(d, kw["theta"], kw["j"]), ), "U8": ( ("gamma", "z", "eps"), - lambda d, omega, **kw: _u8_matrix_func( + lambda d, omega, **kw: u8_matrix_func( d, kw["gamma"], kw["z"], kw["eps"], omega ), ), @@ -35,13 +39,13 @@ TWO_BUILDERS = { "RXX": ( ("theta", "j1", "k1", "j2", "k2"), - lambda d, omega, **kw: _rxx_matrix_func( + 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"])), + "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"])), } @@ -68,7 +72,7 @@ def _is_prime(n: int) -> bool: return True -def _i_matrix_func(d: int) -> Tensor: +def i_matrix_func(d: int) -> Tensor: """ Identity matrix of size ``d``. @@ -80,7 +84,7 @@ def _i_matrix_func(d: int) -> Tensor: return backend.eye(d, dtype=dtypestr) -def _x_matrix_func(d: int) -> Tensor: +def x_matrix_func(d: int) -> Tensor: r""" Generalized Pauli-X on a ``d``-level system. @@ -95,7 +99,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[complex] = None) -> Tensor: +def z_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Generalized Pauli-Z on a ``d``-level system. @@ -113,7 +117,7 @@ def _z_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _h_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: +def h_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Discrete Fourier transform (Hadamard-like) on ``d`` levels. @@ -132,7 +136,7 @@ def _h_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _s_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: +def s_matrix_func(d: int, omega: Optional[complex] = None) -> Tensor: r""" Diagonal phase gate ``S_d`` on ``d`` levels. @@ -214,7 +218,7 @@ def _two_level_projectors( return I, Pjj, Pkk, Pjk, Pkj -def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: +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. @@ -239,7 +243,7 @@ def _rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: 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: +def ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: r""" Rotation-Y (``RY``) gate on a selected two-level subspace of a qudit. @@ -262,7 +266,7 @@ def _ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: return I + (c - 1.0) * (Pjj + Pkk) - s * Pjk + s * Pkj -def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: +def rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: r""" Rotation-Z (``RZ``) gate for qudits. @@ -284,7 +288,7 @@ def _rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: return I + (phase - 1.0) * Pjj -def _swap_matrix_func(d: int) -> Tensor: +def swap_matrix_func(d: int) -> Tensor: """ SWAP gate for two qudits of dimension ``d``. @@ -301,7 +305,7 @@ def _swap_matrix_func(d: int) -> Tensor: return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _rzz_matrix_func( +def rzz_matrix_func( d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 ) -> Tensor: r""" @@ -343,7 +347,7 @@ def _rzz_matrix_func( return I + (phase_minus - 1.0) * Paa + (phase_plus - 1.0) * Pbb -def _rxx_matrix_func( +def rxx_matrix_func( d: int, theta: float, j1: int = 0, k1: int = 1, j2: int = 0, k2: int = 1 ) -> Tensor: r""" @@ -383,7 +387,7 @@ def _rxx_matrix_func( return I + (c - 1.0) * (Paa + Pbb) + (-1j * s) * (Pab + Pba) -def _u8_matrix_func( +def u8_matrix_func( d: int, gamma: int = 2, z: int = 1, @@ -478,19 +482,31 @@ def _u8_matrix_func( return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _cphase_matrix_func( +def cphase_matrix_func( d: int, cv: Optional[int] = None, omega: Optional[complex] = None ) -> Tensor: r""" 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 │ - SUMZ_d = │ 0 0 Z_d^2 ... 0 │ - │ . . . . . │ - │ 0 0 0 ... Z_d^{d-1} │ - └ ─┘ + + Logical definition: + + .. math:: + + \lvert r \rangle \lvert s \rangle \;\longmapsto\; + \omega^{rs} \lvert r \rangle \lvert s \rangle + + Matrix form: + + .. math:: + + \mathrm{SUMZ}_d = + \begin{bmatrix} + I_d & 0 & 0 & \cdots & 0 \\ + 0 & Z_d & 0 & \cdots & 0 \\ + 0 & 0 & Z_d^2 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & \cdots & Z_d^{d-1} + \end{bmatrix} :param d: Qudit dimension (for each register). :type d: int @@ -518,17 +534,29 @@ def _cphase_matrix_func( return backend.cast(backend.convert_to_tensor(m), dtype=dtypestr) -def _csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: +def csum_matrix_func(d: int, cv: Optional[int] = None) -> Tensor: r""" 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 │ - SUMX_d = │ 0 0 X_d^2 ... 0 │ - │ . . . . . │ - │ 0 0 0 ... X_d^{d-1} │ - └ ─┘ + + Logical definition: + + .. math:: + + \lvert r \rangle \lvert s \rangle \;\longmapsto\; + \lvert r \rangle \lvert r+s \pmod d \rangle + + Matrix form: + + .. math:: + + \mathrm{SUMX}_d = + \begin{bmatrix} + I_d & 0 & 0 & \cdots & 0 \\ + 0 & X_d & 0 & \cdots & 0 \\ + 0 & 0 & X_d^2 & \cdots & 0 \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 0 & 0 & 0 & \cdots & X_d^{d-1} + \end{bmatrix} :param d: Qudit dimension (for each register). :type d: int diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index 054389d3..cdb8d29d 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -76,7 +76,7 @@ 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]))), + tc.backend.numpy(c.expectation((tc.quditgates.z_matrix_func(3), [0]))), 0, atol=1e-7, ) @@ -88,7 +88,7 @@ def test_complex128(highp, tfb): c.rx(0, theta=1j) c.wavefunction() np.testing.assert_allclose( - c.expectation((tc.quditgates._z_matrix_func(3), [1])), 0, atol=1e-15 + c.expectation((tc.quditgates.z_matrix_func(3), [1])), 0, atol=1e-15 ) @@ -112,10 +112,10 @@ def test_single_qubit(): @pytest.mark.parametrize("backend", [lf("npb"), lf("cpb")]) def test_expectation_between_two_states_qudit(backend): dim = 3 - X3 = tc.quditgates._x_matrix_func(dim) + 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) + 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) @@ -165,8 +165,8 @@ def test_expectation_between_two_states_qudit(backend): @pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb"), lf("cpb")]) def test_any_inputs_state_qudit_true_gates(backend): dim = 3 - Xd = tc.quditgates._x_matrix_func(dim) - Zd = tc.quditgates._z_matrix_func(dim) + 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): @@ -219,7 +219,7 @@ def test_unitary(backend): 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.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-5 @@ -362,7 +362,7 @@ def energy(theta): # 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])) + E = c.expectation((tc.quditgates.z_matrix_func(dim), [0])) return tc.backend.real(E) # backend autodiff gradient @@ -389,7 +389,7 @@ def test_qudit_minimal_jit_qudit(backend): 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])) + E = c.expectation((tc.quditgates.z_matrix_func(dim), [0])) return tc.backend.real(E) jit_energy = tc.backend.jit(energy) @@ -412,7 +412,7 @@ def test_qudit_minimal_vmap_qudit(backend): 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])) + E = c.expectation((tc.quditgates.z_matrix_func(dim), [0])) return tc.backend.real(E) venergy = tc.backend.vmap(energy) diff --git a/tests/test_quditgates.py b/tests/test_quditgates.py index a0612cdb..72012d67 100644 --- a/tests/test_quditgates.py +++ b/tests/test_quditgates.py @@ -13,20 +13,20 @@ import tensorcircuit as tc from tensorcircuit.quditgates import ( - _i_matrix_func, - _x_matrix_func, - _z_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, + i_matrix_func, + x_matrix_func, + z_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, _is_prime, ) @@ -43,9 +43,9 @@ def is_unitary(M): @pytest.mark.parametrize("d", [2, 3, 4, 5]) @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) + 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) @@ -55,7 +55,7 @@ def test_I_X_Z_shapes_and_unitarity(d, backend, highp): @pytest.mark.parametrize("d", [2, 3, 4]) @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 = x_matrix_func(d) X = tc.backend.numpy(X) for j in range(d): v = np.zeros(d) @@ -70,7 +70,7 @@ def test_X_is_right_cyclic_shift(d, backend, 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) + Z = z_matrix_func(d, omega) np.testing.assert_allclose( tc.backend.numpy(Z), np.diag([omega**j for j in range(d)]), atol=1e-5 ) @@ -88,7 +88,7 @@ def test_Z_diagonal_and_value(d, backend, highp): @pytest.mark.parametrize("d", [2, 3, 5]) @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) + H = h_matrix_func(d) assert H.shape == (d, d) assert is_unitary(H) omega = np.exp(2j * np.pi / d) @@ -101,7 +101,7 @@ def test_H_is_fourier_like_and_unitary(d, backend, highp): @pytest.mark.parametrize("d", [2, 3, 5]) @pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) def test_S_is_diagonal(d, backend, highp): - S = _s_matrix_func(d) + S = s_matrix_func(d) np.testing.assert_allclose(S, np.diag(np.diag(S)), atol=1e-5) @@ -110,8 +110,8 @@ def test_S_is_diagonal(d, backend, highp): 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) + 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): @@ -127,7 +127,7 @@ def test_RX_RY_only_affect_subspace(d, backend, 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) + RZ = rz_matrix_func(d, theta, j) assert is_unitary(RZ) diag = np.ones(d, dtype=np.complex64) diag[j] = np.exp(1j * theta) @@ -137,7 +137,7 @@ def test_RZ_phase_on_single_level(backend, highp): @pytest.mark.parametrize("d", [2, 3, 5]) @pytest.mark.parametrize("backend", [lf("npb"), lf("tfb"), lf("jaxb")]) def test_SWAP_permutation(d, backend, highp): - SW = _swap_matrix_func(d) + SW = swap_matrix_func(d) D = d * d assert SW.shape == (D, D) assert is_unitary(SW) @@ -156,7 +156,7 @@ def test_SWAP_permutation(d, backend, 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) + RZZ = rzz_matrix_func(d, theta, j1=0, k1=1, j2=0, k2=1) assert is_unitary(RZZ) D = d * d @@ -179,7 +179,7 @@ def test_RXX_selected_block(backend, highp): theta = 0.81 j1, k1 = 0, 2 j2, k2 = 1, 3 - RXX = _rxx_matrix_func(d, theta, j1, k1, j2, k2) + RXX = rxx_matrix_func(d, theta, j1, k1, j2, k2) assert is_unitary(RXX) D = d * d I = np.eye(D) @@ -196,8 +196,8 @@ def test_RXX_selected_block(backend, 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) + 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] @@ -206,7 +206,7 @@ def test_CPHASE_blocks(d, backend, highp): assert is_unitary(M) cv = 1 - M2 = _cphase_matrix_func(d, cv=cv, omega=omega) + 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] @@ -219,8 +219,8 @@ def test_CPHASE_blocks(d, backend, highp): @pytest.mark.parametrize("d", [3, 5]) @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) + 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] @@ -229,7 +229,7 @@ def test_CSUM_blocks(d, backend, highp): assert is_unitary(M) cv = 2 % d - M2 = _csum_matrix_func(d, cv=cv) + M2 = csum_matrix_func(d, cv=cv) for a in range(d): rs = a * d block = M2[rs : rs + d, rs : rs + d] @@ -242,7 +242,7 @@ def test_CSUM_blocks(d, backend, 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 = csum_matrix_func(d) M = tc.backend.numpy(M) for r in range(d): for s in range(d): @@ -257,23 +257,23 @@ def test_CSUM_mapping_small_d(backend, highp): def test_rotation_index_errors(highp): d = 4 with pytest.raises(ValueError): - _rx_matrix_func(d, 0.1, j=-1, k=1) + rx_matrix_func(d, 0.1, j=-1, k=1) with pytest.raises(ValueError): - _ry_matrix_func(d, 0.1, j=0, k=4) + ry_matrix_func(d, 0.1, j=0, k=4) with pytest.raises(ValueError): - _rx_matrix_func(d, 0.1, j=2, k=2) + rx_matrix_func(d, 0.1, j=2, k=2) def test_CPHASE_CSUM_cv_range(highp): d = 5 with pytest.raises(ValueError): - _cphase_matrix_func(d, cv=-1) + cphase_matrix_func(d, cv=-1) with pytest.raises(ValueError): - _cphase_matrix_func(d, cv=d) + cphase_matrix_func(d, cv=d) with pytest.raises(ValueError): - _csum_matrix_func(d, cv=-1) + csum_matrix_func(d, cv=-1) with pytest.raises(ValueError): - _csum_matrix_func(d, cv=d) + csum_matrix_func(d, cv=d) def test__is_prime_edge_and_composites(): @@ -289,32 +289,32 @@ 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) + 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) + 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) + 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) + rxx_matrix_func(d, theta, j1=2, k1=2, j2=0, k2=0) @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) + u8_matrix_func(d=4) with pytest.raises(ValueError): - _u8_matrix_func(d=9, gamma=1, z=0, eps=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) + 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 = 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) @@ -339,7 +339,7 @@ def test_u8_p_greater_than_3_matches_closed_form(backend): 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) + 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 @@ -347,8 +347,8 @@ def test_u8_p_greater_than_3_matches_closed_form(backend): @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( + 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) @@ -366,6 +366,6 @@ def test_u8_parameter_normalization_and_custom_omega(backend): 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) + 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 17603332922e3661a1d8e40b87c8a2fedbca33a4 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Tue, 9 Sep 2025 21:59:49 +0800 Subject: [PATCH 02/10] qubit -> qudit --- tensorcircuit/quditcircuit.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 9ae0f013..7e7d48b8 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -27,31 +27,30 @@ class QuditCircuit: ``QuditCircuit`` class. Qudit quick example (d=3): - .. code-block:: python - - c = tc.QuditCircuit(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, ...). + + >>> c = tc.QuditCircuit(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, + nqudits: 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._nqudits = nqudits self._circ = Circuit( - nqubits=nqubits, + nqudits=nqudits, dim=dim, inputs=inputs, mps_inputs=mps_inputs, @@ -64,7 +63,7 @@ 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." + f"You passed dim={dim}. For qudits, please use `Circuit` instead." ) # Require integer d>=2; current string-encoded IO supports d<=36 (0–9A–Z digits). if dim > 36: @@ -79,9 +78,9 @@ def dim(self) -> int: return self._d @property - def nqubits(self) -> int: + def nqudits(self) -> int: """qudit number of the circuit""" - return self._nqubits + return self._nqudits def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: """ @@ -483,7 +482,7 @@ def sample( :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 + and shape [batch, nqudits] 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 @@ -538,7 +537,7 @@ def mid_measurement(self, index: int, keep: int = 0) -> Tensor: 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. + :param index: The index of qudit 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 From d8a1ba898d8253e17460fd5915abc9355e36b138 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 09:18:16 +0800 Subject: [PATCH 03/10] fix a tiny bug --- tensorcircuit/quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 7e7d48b8..fb86c2be 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -50,7 +50,7 @@ def __init__( self._nqudits = nqudits self._circ = Circuit( - nqudits=nqudits, + nqubits=nqudits, dim=dim, inputs=inputs, mps_inputs=mps_inputs, From 2b08ee9fb2b4d9f8f3a1eb831f5cfb4be1b0247e Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 11:09:02 +0800 Subject: [PATCH 04/10] fix a typo in circuit.py --- tensorcircuit/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/circuit.py b/tensorcircuit/circuit.py index 2347bdca..0802b6d9 100644 --- a/tensorcircuit/circuit.py +++ b/tensorcircuit/circuit.py @@ -874,7 +874,7 @@ def expectation( :param nmc: repetition time for Monte Carlo sampling for noisfy calculation, defaults to 1000 :type nmc: int, optional :param status: external randomness given by tensor uniformly from [0, 1], defaults to None, - used for noisfy circuit sampling + used for noisy circuit sampling :type status: Optional[Tensor], optional :raises ValueError: "Cannot measure two operators in one index" :return: Tensor with one element From 06a3c146f0cdc9b5a587d6312a0f3054e58c5c1c Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 11:53:49 +0800 Subject: [PATCH 05/10] optimized docsing in quditcircuit.py and quditgates.py. --- tensorcircuit/quditcircuit.py | 279 ++++++++++++++++++++-------------- tensorcircuit/quditgates.py | 71 ++++++--- 2 files changed, 217 insertions(+), 133 deletions(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index fb86c2be..0373a8c5 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -1,7 +1,13 @@ """ -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. +Quantum circuit: state simulator for **qudits** (d-level systems). + +This module provides a high-level `QuditCircuit` API that mirrors `tensorcircuit.circuit.Circuit` +but targets qudits with dimension `3 <= d <= 36`. +For string-encoded samples/counts, digits use `0–9A–Z` where `A=10, …, Z=35`. + +.. note:: + For qubits (`d=2`) please use :class:`tensorcircuit.circuit.Circuit`. + """ from functools import partial @@ -24,16 +30,39 @@ class QuditCircuit: r""" - ``QuditCircuit`` class. + The `QuditCircuit` class provides a d-dimensional state-vector simulator and a thin wrapper + around :class:`tensorcircuit.circuit.Circuit`, exposing a qudit-friendly API and docstrings. - Qudit quick example (d=3): + **Quick example (d=3):** - >>> c = tc.QuditCircuit(2, d=3) + >>> c = tc.QuditCircuit(2, dim=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, ...). + + .. note:: + For `3 <= d <= 36`, string samples and count keys use base-`d` characters `0–9A–Z` + (`A=10, …, Z=35`). + + :param nqudits: Number of qudits (wires) in the circuit. + :type nqudits: int + :param dim: Qudit local dimension `d`. Must satisfy `3 <= d <= 36`. + :type dim: int + :param inputs: Optional initial state as a wavefunction. + :type inputs: Optional[Tensor] + :param mps_inputs: Optional initial state in MPS/MPO-like form. + :type mps_inputs: Optional[QuOperator] + :param split: Internal contraction/splitting configuration passed through to + :class:`~tensorcircuit.circuit.Circuit`. + :type split: Optional[Dict[str, Any]] + + :var is_dm: Whether the simulator is a density-matrix simulator (`False` here). + :vartype is_dm: bool + :var dim: Property for the local dimension `d`. + :vartype dim: int + :var nqudits: Property for the number of qudits. + :vartype nqudits: int """ is_dm = False @@ -84,22 +113,19 @@ def nqudits(self) -> int: def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: """ - Apply a quantum gate (unitary) to one or two qudits in the circuit. + Apply a single- or two-qudit unitary by name. - 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 by the registered builder, - then applied to the circuit at the given indices. + The gate matrix is looked up by name in either :data:`SINGLE_BUILDERS` (single-qudit) + or :data:`TWO_BUILDERS` (two-qudit). The registered builder is called with `(d, omega, **kwargs)` + to produce the unitary, which is then applied at the given indices. - :param indices: The qudit indices the gate should act on. - - One index → single-qudit gate. - - Two indices → two-qudit gate. + :param indices: Qudit indices the gate acts 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). + :param name: Gate name registered in the corresponding builder set. :type name: str - :param kwargs: Extra parameters for the gate matched against the builder signature. + :param kwargs: Extra parameters forwarded to the builder (matched by keyword). :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 the name is unknown or the arity does not match the number of indices. """ if len(indices) == 1 and name in SINGLE_BUILDERS: sig, builder = SINGLE_BUILDERS[name] @@ -120,13 +146,13 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: """ - Apply a quantum gate (unitary) to one or two qudits in the circuit. + Apply an arbitrary unitary on one or two qudits. - :param indices: The qudit indices the gate should act on. + :param indices: Target qudit indices. :type indices: int - :param unitary: The unitary matrix to apply to the qudit(s). + :param unitary: Unitary matrix acting on the specified qudit(s), with shape `(d, d)` or `(d^2, d^2)`. :type unitary: Tensor - :param name: The name to record for this gate. + :param name: Optional label stored in the circuit history. :type name: str """ self._circ.unitary(*indices, unitary=unitary, name=name, dim=self._d) # type: ignore @@ -135,9 +161,9 @@ def any(self, *indices: int, unitary: Tensor, name: str = "any") -> None: def i(self, index: int) -> None: """ - Apply the identity (I) gate on the given qudit index. + Apply the generalized identity gate `I` on the given qudit. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int """ self._apply_gate(index, name="I") @@ -146,9 +172,9 @@ def i(self, index: int) -> None: def x(self, index: int) -> None: """ - Apply the X gate on the given qudit index. + Apply the generalized shift gate `X` on the given qudit (adds `+1 mod d`). - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int """ self._apply_gate(index, name="X") @@ -166,9 +192,9 @@ def x(self, index: int) -> None: def z(self, index: int) -> None: """ - Apply the Z gate on the given qudit index. + Apply the generalized phase gate `Z` on the given qudit (multiplies by `omega^k`). - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int """ self._apply_gate(index, name="Z") @@ -177,9 +203,9 @@ def z(self, index: int) -> None: def h(self, index: int) -> None: """ - Apply the Hadamard-like (H) gate on the given qudit index. + Apply the generalized Hadamard-like gate `H` (DFT on `d` levels) on the given qudit. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int """ self._apply_gate(index, name="H") @@ -190,15 +216,15 @@ 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. + Apply the parameterized single-qudit gate `U8`. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int - :param gamma: Gate parameter ``gamma``. + :param gamma: Gate parameter `gamma`. :type gamma: float - :param z: Gate parameter ``z``. + :param z: Gate parameter `z`. :type z: float - :param eps: Gate parameter ``eps``. + :param eps: Gate parameter `eps`. :type eps: float """ self._apply_gate(index, name="U8", extra=(gamma, z, eps)) @@ -207,16 +233,17 @@ def u8( def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ - Apply the single-qudit RX rotation on ``index``. + Single-qudit rotation `RX` on a selected two-level subspace `(j, k)`. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int :param theta: Rotation angle. :type theta: float - :param j: Source level of the rotation subspace. + :param j: Source level of the rotation subspace (0-based). :type j: int - :param k: Target level of the rotation subspace. + :param k: Target level of the rotation subspace (0-based). :type k: int + :raises ValueError: If `j == k` or indices are outside `[0, d-1]`. """ self._apply_gate(index, name="RX", theta=theta, j=j, k=k) @@ -224,16 +251,17 @@ def rx(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: """ - Apply the single-qudit RY rotation on ``index``. + Single-qudit rotation `RY` on a selected two-level subspace `(j, k)`. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int :param theta: Rotation angle. :type theta: float - :param j: Source level of the rotation subspace. + :param j: Source level of the rotation subspace (0-based). :type j: int - :param k: Target level of the rotation subspace. + :param k: Target level of the rotation subspace (0-based). :type k: int + :raises ValueError: If `j == k` or indices are outside `[0, d-1]`. """ self._apply_gate(index, name="RY", theta=theta, j=j, k=k) @@ -241,14 +269,15 @@ def ry(self, index: int, theta: float, j: int = 0, k: int = 1) -> None: def rz(self, index: int, theta: float, j: int = 0) -> None: """ - Apply the single-qudit RZ rotation on ``index``. + Single-qudit phase rotation `RZ` applied on level `j`. - :param index: Qudit index to apply the gate on. + :param index: Qudit index. :type index: int - :param theta: Rotation angle around Z. + :param theta: Phase rotation angle around Z. :type theta: float - :param j: Level where the phase rotation is applied. + :param j: Level where the phase is applied (0-based). :type j: int + :raises ValueError: If `j` is outside `[0, d-1]`. """ self._apply_gate(index, name="RZ", theta=theta, j=j) @@ -264,11 +293,11 @@ def rxx( k2: int = 1, ) -> None: """ - Apply a two-qudit RXX-type interaction on the given indices. + Two-qudit interaction `RXX` acting on subspaces `(j1, k1)` and `(j2, k2)`. - :param indices: Two qudit indices. + :param indices: Two qudit indices `(q1, q2)`. :type indices: int - :param theta: Interaction strength/angle. + :param theta: Interaction angle. :type theta: float :param j1: Source level of the first qudit subspace. :type j1: int @@ -278,6 +307,7 @@ def rxx( :type j2: int :param k2: Target level of the second qudit subspace. :type k2: int + :raises ValueError: If levels are invalid or the arity is not two. """ self._apply_gate(*indices, name="RXX", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) @@ -293,11 +323,11 @@ def rzz( k2: int = 1, ) -> None: """ - Apply a two-qudit RZZ-type interaction on the given indices. + Two-qudit interaction `RZZ` acting on subspaces `(j1, k1)` and `(j2, k2)`. - :param indices: Two qudit indices. + :param indices: Two qudit indices `(q1, q2)`. :type indices: int - :param theta: Interaction strength/angle. + :param theta: Interaction angle. :type theta: float :param j1: Source level of the first qudit subspace. :type j1: int @@ -307,6 +337,7 @@ def rzz( :type j2: int :param k2: Target level of the second qudit subspace. :type k2: int + :raises ValueError: If levels are invalid or the arity is not two. """ self._apply_gate(*indices, name="RZZ", theta=theta, j1=j1, k1=k1, j2=j2, k2=k2) @@ -314,12 +345,13 @@ def rzz( def cphase(self, *indices: int, cv: Optional[int] = None) -> None: """ - Apply a controlled phase (CPHASE) gate. + Apply a controlled-phase gate `CPHASE`. - :param indices: Two qudit indices (control, target). + :param indices: Two qudit indices `(control, target)`. :type indices: int - :param cv: Optional control value. If ``None``, defaults to ``1``. + :param cv: Optional control value. If `None`, defaults to `1`. :type cv: Optional[int] + :raises ValueError: If arity is not two. """ self._apply_gate(*indices, name="CPHASE", cv=cv) @@ -327,12 +359,13 @@ def cphase(self, *indices: int, cv: Optional[int] = None) -> None: def csum(self, *indices: int, cv: Optional[int] = None) -> None: """ - Apply a controlled-sum (CSUM) gate. + Apply a generalized controlled-sum gate `CSUM` (a.k.a. qudit CNOT). - :param indices: Two qudit indices (control, target). + :param indices: Two qudit indices `(control, target)`. :type indices: int - :param cv: Optional control value. If ``None``, defaults to ``1``. + :param cv: Optional control value. If `None`, defaults to `1`. :type cv: Optional[int] + :raises ValueError: If arity is not two. """ self._apply_gate(*indices, name="CSUM", cv=cv) @@ -340,6 +373,15 @@ def csum(self, *indices: int, cv: Optional[int] = None) -> None: # Functional def wavefunction(self, form: str = "default") -> tn.Node.tensor: + """ + Compute the output wavefunction from the circuit. + + :param form: The str indicating the form of the output wavefunction. + "default": [-1], "ket": [-1, 1], "bra": [1, -1] + :type form: str, optional + :return: Tensor with the corresponding shape. + :rtype: Tensor + """ return self._circ.wavefunction(form) state = wavefunction @@ -378,16 +420,30 @@ def expectation( reuse: bool = True, enable_lightcone: bool = False, nmc: int = 1000, - status: Optional[Tensor] = None, **kws: Any, ) -> Tensor: + """ + Compute expectation(s) of local operators. + + :param ops: Pairs of `(operator_node, [sites])` specifying where each operator acts. + :type ops: Tuple[tn.Node, List[int]] + :param reuse: If True, then the wavefunction tensor is cached for further expectation evaluation, + defaults to be true. + :type reuse: bool, optional + :param enable_lightcone: whether enable light cone simplification, defaults to False + :type enable_lightcone: bool, optional + :param nmc: repetition time for Monte Carlo sampling for noisfy calculation, defaults to 1000 + :type nmc: int, optional + :raises ValueError: "Cannot measure two operators in one index" + :return: Tensor with one element + :rtype: Tensor + """ return self._circ.expectation( *ops, reuse=reuse, enable_lightcone=enable_lightcone, noise_conf=None, nmc=nmc, - status=status, **kws, ) @@ -413,12 +469,13 @@ def 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. + Return the amplitude for a given base-`d` string `l`. - :Example: + For state simulators, this computes :math:`\langle l \vert \psi \rangle`. + For density-matrix simulators, it would compute :math:`\operatorname{Tr}(\rho \vert l \rangle \langle l \vert)` + (note the square in magnitude differs between the two formalisms). + + **Example** >>> c = tc.QuditCircuit(2, dim=3) >>> c.x(0) @@ -429,18 +486,18 @@ def amplitude(self, l: Union[str, Tensor]) -> Tensor: >>> c.amplitude("21") array(1.+0.j, dtype=complex64) - :param l: The bitstring of base-d characters. + :param l: Bitstring in base-`d` using `0–9A–Z`. :type l: Union[str, Tensor] - :return: The amplitude of the circuit. - :rtype: tn.Node.tensor + :return: Complex amplitude. + :rtype: Tensor """ return self._circ.amplitude(l) def probability(self) -> Tensor: """ - Get the ``d^n`` length probability vector over the computational basis. + Get the length-`d^n` 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() @@ -457,20 +514,16 @@ def sample( jittable: bool = True, ) -> Any: 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: + Batched sampling from the circuit or final state. - "sample_bin": # [np.array([1, 0]), np.array([1, 0])] + :param batch: Number of samples. If `None`, returns a single draw. + :type batch: Optional[int] + :param allow_state: If `True`, sample from the final state (when memory allows). Prefer `True` for speed. + :type allow_state: bool + :param readout_error: Optional readout error model. + :type readout_error: Optional[Sequence[Any]] + :param format: Output format. See :py:meth:`tensorcircuit.quantum.measurement_results`. + Supported formats for qudits include: "count_vector": # np.array([2, 0, 0, 0]) @@ -490,6 +543,7 @@ def sample( :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 + :raises NotImplementedError: For integer-based output formats not suitable for qudits. """ if format in ["sample_int", "count_tuple", "count_dict_int", "count_vector"]: raise NotImplementedError( @@ -507,14 +561,14 @@ 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`` + remaining wavefunction or density matrix on sites in `left`, while fixing the other + sites to given digits as indicated by `traceout`. - :param traceout: can be jitted + :param traceout: A tensor encoding digits (0..d-1) for sites to be fixed; jittable. :type traceout: Tensor - :param left: cannot be jitted - :type left: Tuple - :return: _description_ + :param left: Tuple of site indices to keep (non-jittable argument). + :type left: Tuple[int, ...] + :return: Remaining wavefunction or density matrix on the kept sites. :rtype: Tensor """ return self._circ.projected_subsystem( @@ -533,14 +587,16 @@ def replace_inputs(self, inputs: Tensor) -> None: 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. + Mid-circuit Z-basis post-selection. + + The returned state is **not normalized**; normalize manually if needed. - :param index: The index of qudit that the Z direction postselection applied on. + :param index: Qudit index where post-selection is applied. :type index: int - :param keep: the post-selected digit in {0, ..., d-1}, defaults to be 0. - :type keep: int, optional + :param keep: Post-selected digit in `{0, …, d-1}`. + :type keep: int + :return: Unnormalized post-selected state. + :rtype: Tensor """ return self._circ.mid_measurement(index, keep=keep) @@ -562,22 +618,19 @@ def get_quvector(self) -> QuVector: def replace_mps_inputs(self, mps_inputs: QuOperator) -> None: """ - Replace the input state in MPS representation while keep the circuit structure unchanged. + Replace the input state (keeps circuit structure) using an MPS/MPO-like representation. + + **Example** - :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) - >>> + >>> c2.x(0); c2.wavefunction() + array([...], 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) + >>> c3.replace_mps_inputs(c.quvector()); c3.wavefunction() + array([...], dtype=complex64) :param mps_inputs: (Nodes, dangling Edges) for a MPS like initial wavefunction. :type mps_inputs: Tuple[Sequence[Gate], Sequence[Edge]] @@ -591,8 +644,7 @@ def expectation_before( **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 + Build (but do not contract) the tensor network for expectation evaluation. :param reuse: _description_, defaults to True :type reuse: bool, optional @@ -604,12 +656,13 @@ def expectation_before( 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. + Return the (uncontracted) tensor network nodes for the amplitude of configuration `l`. + + For state simulators, this corresponds to :math:`\langle l \vert \psi \rangle`. + For density-matrix simulators, + it would correspond to :math:`\operatorname{Tr}(\rho \vert l \rangle \langle l \vert)`. - :param l: The bitstring of 0 and 1s. + :param l: Base-`d` string using `0–9A–Z` or an equivalent tensor index. :type l: Union[str, Tensor] :return: The tensornetwork nodes for the amplitude of the circuit. :rtype: List[Gate] diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 40df6d77..0173b9ca 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -1,5 +1,14 @@ -""" -Single-qudit and two-qudit gates and their corresponding matrix. +r""" +Single-qudit and two-qudit gates and their matrix representations. + +This module implements gates for **qudits** (d-level systems), providing d-dimensional +analogues of familiar qubit gates as well as qudit-specific primitives. + +**Registry** +Single-qudit builders: ``I``, ``X``, ``Z``, ``H``, ``RX``, ``RY``, ``RZ``, ``U8``. +Two-qudit builders: ``RXX``, ``RZZ``, ``CPHASE``, ``CSUM``. + +These names are used by higher-level APIs (e.g. :class:`tensorcircuit.quditcircuit.QuditCircuit`). """ from typing import Any, Optional, Tuple @@ -161,16 +170,19 @@ 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. + Validate subspace indices for rotations and interactions. + + Ensures every index lies in ``[0, d-1]`` and (optionally) that two selected + basis pairs are distinct. :param d: Qudit dimension. :type d: int - :param indices: Indices to validate. + :param indices: Indices to validate (e.g., ``j, k`` or ``j1, k1, j2, k2``). :type indices: int - :param distinct_pairs: If True, enforce that (indices[0], indices[1]) - ≠ (indices[2], indices[3]) for 4 indices. + :param distinct_pairs: If ``True`` and four indices are provided, enforce that + ``(j1, k1)`` and ``(j2, k2)`` do not denote the same basis state. :type distinct_pairs: bool - :raises ValueError: If indices are invalid. + :raises ValueError: If any index is out of range or if the distinctness constraint fails. """ for idx in indices: if not (0 <= idx < d): @@ -220,9 +232,15 @@ def _two_level_projectors( 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. + Rotation-X (``RX``) on a selected two-level subspace of a qudit. + + Acts like the qubit :math:`RX(\theta)` on levels :math:`j` and :math:`k`, identity elsewhere. On the + :math:`\{\lvert j\rangle,\lvert k\rangle\}` subspace the matrix equals - Acts like the qubit :math:`RX(\theta)` on levels ``j`` and ``k``, identity elsewhere. + .. math:: + RX(\theta) = \cos\tfrac{\theta}{2}\,(\lvert j\rangle\!\langle j\rvert+\lvert k\rangle\!\langle k\rvert) + - i\,\sin\tfrac{\theta}{2}\,(\lvert j\rangle\!\langle k\rvert + \lvert k\rangle\!\langle j\rvert) + + I. :param d: Qudit dimension. :type d: int @@ -232,7 +250,7 @@ def rx_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :type j: int :param k: Second level index. :type k: int - :return: ``(d, d)`` matrix for :math:`RX(\theta)` on the ``j,k`` subspace. + :return: ``(d, d)`` matrix for :math:`RX(\theta)` on the :math:`j,k` subspace. :rtype: Tensor """ _check_rotation_indices(d, j, k) @@ -245,7 +263,15 @@ 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 on a selected two-level subspace of a qudit. + Rotation-Y (``RY``) on a selected two-level subspace of a qudit. + + Acts like the qubit :math:`RY(\theta)` on levels :math:`j` and :math:`k`, identity elsewhere. On the + :math:`\{\lvert j\rangle,\lvert k\rangle\}` subspace the matrix equals + + .. math:: + RY(\theta) = \cos\tfrac{\theta}{2}\,(\lvert j\rangle\!\langle j\rvert+\lvert k\rangle\!\langle k\rvert) + + \sin\tfrac{\theta}{2}\,(\lvert k\rangle\!\langle j\rvert - \lvert j\rangle\!\langle k\rvert) + + I. :param d: Qudit dimension. :type d: int @@ -255,7 +281,7 @@ def ry_matrix_func(d: int, theta: float, j: int = 0, k: int = 1) -> Tensor: :type j: int :param k: Second level index. :type k: int - :return: ``(d, d)`` matrix for :math:`RY(\theta)` on the ``j,k`` subspace. + :return: ``(d, d)`` matrix for :math:`RY(\theta)` on the :math:`j,k` subspace. :rtype: Tensor """ _check_rotation_indices(d, j, k) @@ -268,10 +294,15 @@ 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. + Rotation-Z (``RZ``) on a selected level of a qudit. - 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. + Acts like the qubit :math:`RZ(\theta)` but applies a phase only to level :math:`j`. On the computational + basis it equals + + .. math:: + RZ(\theta) = I + (e^{i\theta}-1)\,\lvert j\rangle\!\langle j\rvert, + + i.e. :math:`\lvert j\rangle \mapsto e^{i\theta}\,\lvert j\rangle` and all other levels unchanged. :param d: Qudit dimension. :type d: int @@ -279,7 +310,7 @@ def rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: :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``. + :return: ``(d, d)`` diagonal matrix implementing :math:`RZ(\theta)` on level :math:`j`. :rtype: Tensor """ I, Pjj = _two_level_projectors(d, j, k=None) @@ -289,14 +320,14 @@ def rz_matrix_func(d: int, theta: float, j: int = 0) -> Tensor: def swap_matrix_func(d: int) -> Tensor: - """ - SWAP gate for two qudits of dimension ``d``. + r""" + SWAP gate for two qudits of equal dimension :math:`d`. - Exchanges basis states ``|i⟩|j⟩ → |j⟩|i⟩``. + Exchanges basis states :math:`\lvert i\rangle\lvert j\rangle \to \lvert j\rangle\lvert i\rangle`. :param d: Qudit dimension (for each register). :type d: int - :return: ``(d*d, d*d)`` matrix representing SWAP. + :return: ``(d^2, d^2)`` permutation matrix implementing SWAP. :rtype: Tensor """ D = d * d From e3ecca432b70bbc264f1d7870cbab69e2735e23c Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 13:00:06 +0800 Subject: [PATCH 06/10] adjust some unicode and doc fixing. --- examples/time_evolution_comparison.py | 6 +++++- examples/vqe_qudit_example.py | 8 ++++---- tensorcircuit/basecircuit.py | 6 +++--- tensorcircuit/circuit.py | 4 ++-- tensorcircuit/quditcircuit.py | 16 ++++++++-------- tensorcircuit/quditgates.py | 13 +++++++------ tests/test_quditcircuit.py | 4 ++-- 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/examples/time_evolution_comparison.py b/examples/time_evolution_comparison.py index 2865ec2b..ea660051 100644 --- a/examples/time_evolution_comparison.py +++ b/examples/time_evolution_comparison.py @@ -27,7 +27,11 @@ def create_heisenberg_hamiltonian(n, sparse=True): def create_initial_state(n): - """Create initial Neel state |↑↓↑↓↑↓↑↓> for n sites.""" + r""" + Create initial Neel state + :math:`\vert \uparrow\downarrow\uparrow\downarrow\uparrow\downarrow\uparrow\downarrow\rangle` + for n sites. + """ c = tc.Circuit(n) # Apply X gates to odd sites to create Neel state c.x([i for i in range(1, n, 2)]) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index 6d18fb37..077d7715 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -4,7 +4,7 @@ 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: +optimize the energy of a Hermitian "clock-shift" Hamiltonian: H(d) = - J * (X_c \otimes X_c) - h * (Z_c \otimes I + I \otimes Z_c) @@ -31,12 +31,12 @@ def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float): - """Build a QuditCircuit ansatz and compute ⟨H⟩. + r"""Build a QuditCircuit ansatz and compute :math:`\langle H\rangle`. Ansatz: [ for L in 1...nlayers ] - On each site q: - RX(q; θ_Lq^(01)) ∘ RY(q; θ_Lq^(12)) ∘ RZ(q; φ_Lq^(0)) + :math:`RX(q; \theta_L q^(01)) RY(q; \theta_L q^(12)) RZ(q; \phi_L q^(0))` (subspace indices shown as superscripts) - Entangle neighboring pairs with RXX on subspaces (0,1) """ @@ -86,7 +86,7 @@ def build_param_shape(nqudits: int, d: int, nlayers: int): def main(): parser = argparse.ArgumentParser( - description="VQE on QuditCircuit (clock–shift model)" + description="VQE on QuditCircuit (clock-shift model)" ) parser.add_argument( "--d", type=int, default=3, help="Local dimension per site (>=3)" diff --git a/tensorcircuit/basecircuit.py b/tensorcircuit/basecircuit.py index ffa50498..3ba149af 100644 --- a/tensorcircuit/basecircuit.py +++ b/tensorcircuit/basecircuit.py @@ -3,7 +3,7 @@ Note: - Supports qubit (d = 2) and qudit (d >= 2) systems. - - For string-encoded samples/counts when d <= 36, digits use base-d characters 0–9A–Z (A = 10, …, Z = 35). + - For string-encoded samples/counts when d <= 36, digits use base-d characters 0-9A-Z (A = 10, ..., Z = 35). """ # pylint: disable=invalid-name @@ -362,7 +362,7 @@ def to_qir(self) -> List[Dict[str, Any]]: def perfect_sampling(self, status: Optional[Tensor] = None) -> Tuple[str, float]: """ - Sampling base-d strings (0–9A–Z when d <= 36) from the circuit output based on quantum amplitudes. + Sampling base-d strings (0-9A-Z when d <= 36) from the circuit output based on quantum amplitudes. Reference: arXiv:1201.3974. :param status: external randomness, with shape [nqubits], defaults to None @@ -604,7 +604,7 @@ def sample( "count_tuple": # (np.array([0]), np.array([2])) "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); + for cases d\in [11, 36], use 0-9A-Z digits (e.g., 'A' -> 10, ..., 'Z' -> 35); "count_dict_int": # {0: 2, 1: 0, 2: 0, 3: 0} diff --git a/tensorcircuit/circuit.py b/tensorcircuit/circuit.py index 0802b6d9..7ce1c63e 100644 --- a/tensorcircuit/circuit.py +++ b/tensorcircuit/circuit.py @@ -1,7 +1,7 @@ """ Quantum circuit: the state simulator. Supports qubit (dim=2) and qudit (3 <= dim <= 36) systems. -For string-encoded samples/counts, digits use 0–9A–Z where A=10, …, Z=35. +For string-encoded samples/counts, digits use 0-9A-Z where A=10, ..., Z=35. """ # pylint: disable=invalid-name @@ -768,7 +768,7 @@ def measure_reference( Take measurement on the given quantum lines by ``index``. Return format: - - For d <= 36, the sample is a base-d string using 0–9A–Z (A=10,…). + - For d <= 36, the sample is a base-d string using 0-9A-Z (A=10,...). :Example: diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 0373a8c5..be424aeb 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -3,7 +3,7 @@ This module provides a high-level `QuditCircuit` API that mirrors `tensorcircuit.circuit.Circuit` but targets qudits with dimension `3 <= d <= 36`. -For string-encoded samples/counts, digits use `0–9A–Z` where `A=10, …, Z=35`. +For string-encoded samples/counts, digits use `0-9A-Z` where `A=10, ..., Z=35`. .. note:: For qubits (`d=2`) please use :class:`tensorcircuit.circuit.Circuit`. @@ -42,8 +42,8 @@ class QuditCircuit: >>> c.sample(1024, format="count_dict_bin") .. note:: - For `3 <= d <= 36`, string samples and count keys use base-`d` characters `0–9A–Z` - (`A=10, …, Z=35`). + For `3 <= d <= 36`, string samples and count keys use base-`d` characters `0-9A-Z` + (`A=10, ..., Z=35`). :param nqudits: Number of qudits (wires) in the circuit. :type nqudits: int @@ -94,7 +94,7 @@ def _set_dim(self, dim: int) -> None: f"QuditCircuit is only for qudits (dim>=3). " f"You passed dim={dim}. For qudits, please use `Circuit` instead." ) - # Require integer d>=2; current string-encoded IO supports d<=36 (0–9A–Z digits). + # 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." @@ -486,7 +486,7 @@ def amplitude(self, l: Union[str, Tensor]) -> Tensor: >>> c.amplitude("21") array(1.+0.j, dtype=complex64) - :param l: Bitstring in base-`d` using `0–9A–Z`. + :param l: Bitstring in base-`d` using `0-9A-Z`. :type l: Union[str, Tensor] :return: Complex amplitude. :rtype: Tensor @@ -528,7 +528,7 @@ def sample( "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); + for cases :math:`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 @@ -593,7 +593,7 @@ def mid_measurement(self, index: int, keep: int = 0) -> Tensor: :param index: Qudit index where post-selection is applied. :type index: int - :param keep: Post-selected digit in `{0, …, d-1}`. + :param keep: Post-selected digit in `{0, ..., d-1}`. :type keep: int :return: Unnormalized post-selected state. :rtype: Tensor @@ -662,7 +662,7 @@ def amplitude_before(self, l: Union[str, Tensor]) -> List[Gate]: For density-matrix simulators, it would correspond to :math:`\operatorname{Tr}(\rho \vert l \rangle \langle l \vert)`. - :param l: Base-`d` string using `0–9A–Z` or an equivalent tensor index. + :param l: Base-`d` string using `0-9A-Z` or an equivalent tensor index. :type l: Union[str, Tensor] :return: The tensornetwork nodes for the amplitude of the circuit. :rtype: List[Gate] diff --git a/tensorcircuit/quditgates.py b/tensorcircuit/quditgates.py index 0173b9ca..c514a09d 100644 --- a/tensorcircuit/quditgates.py +++ b/tensorcircuit/quditgates.py @@ -195,7 +195,7 @@ def _check_rotation_indices( j1, k1, j2, k2 = indices if j1 == k1 and j2 == k2: raise ValueError( - "Selected basis states must be different: (j1, j2) ≠ (k1, k2)." + "Selected basis states must be different: (j1, j2) != (k1, k2)." ) @@ -343,7 +343,7 @@ def rzz_matrix_func( Two-qudit ``RZZ(\theta)`` on a selected two-state subspace. 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⟩``, + two-dimensional subspace spanned by :math:`\lvert j1, j2\rangle` and :math:`\lvert k1, k2\rangle`, and as identity elsewhere. The resulting block is diagonal with phases :math:`\mathrm{diag}(e^{-i\theta/2},\, e^{+i\theta/2})`. @@ -384,7 +384,8 @@ def rxx_matrix_func( r""" 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⟩``. + Acts like a qubit :math:`RXX` on the subspace spanned by + :math:`\lvert j1, j2\rangle` and :math:`\lvert k1, k2\rangle`. :param d: Dimension of each qudit (assumed equal). :type d: int @@ -429,7 +430,7 @@ def u8_matrix_func( ``U8`` diagonal single-qudit gate for prime dimensions. See ref: Howard, Mark, and Jiri Vala. - "Qudit versions of the qubit π/8 gate." Physical Review A 86, no. 2 (2012): 022316. + "Qudit versions of the qubit \pi/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 @@ -448,8 +449,8 @@ def u8_matrix_func( :param d: Qudit dimension (must be prime). :type d: int :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`). + If :math:`gamma = 0`, the gate is a diagonal Clifford. + If :math:`gamma \neq 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. diff --git a/tests/test_quditcircuit.py b/tests/test_quditcircuit.py index cdb8d29d..492071b5 100644 --- a/tests/test_quditcircuit.py +++ b/tests/test_quditcircuit.py @@ -350,8 +350,8 @@ def test_quditcircuit_set_dim_validation(): @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 + r"""Minimal AD test on a single-qudit (d=3) circuit. + We differentiate the expectation :math:`\langle Z\rangle` w.r.t. a single RY parameter and compare to a finite-difference estimate. """ From a5c2e5b3e020e9cf3f20a9a5e616d78c2e15ba42 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 13:04:00 +0800 Subject: [PATCH 07/10] typo --- tensorcircuit/quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index be424aeb..3433f8cb 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -92,7 +92,7 @@ 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 qudits, please use `Circuit` instead." + 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: From a0fb4f3df9f30dc7b2483831071b3e40903e4012 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 13:06:55 +0800 Subject: [PATCH 08/10] unicode --- tensorcircuit/quditcircuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorcircuit/quditcircuit.py b/tensorcircuit/quditcircuit.py index 3433f8cb..3b235efe 100644 --- a/tensorcircuit/quditcircuit.py +++ b/tensorcircuit/quditcircuit.py @@ -119,7 +119,7 @@ def _apply_gate(self, *indices: int, name: str, **kwargs: Any) -> None: or :data:`TWO_BUILDERS` (two-qudit). The registered builder is called with `(d, omega, **kwargs)` to produce the unitary, which is then applied at the given indices. - :param indices: Qudit indices the gate acts on. One index → single-qudit gate; two indices → two-qudit gate. + :param indices: Qudit indices the gate acts on. One index -> single-qudit gate; two indices -> two-qudit gate. :type indices: int :param name: Gate name registered in the corresponding builder set. :type name: str From 88900ace3a610ce68d025a012015291f8660df49 Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 13:18:08 +0800 Subject: [PATCH 09/10] fix unicode --- tensorcircuit/applications/layers.py | 2 +- tensorcircuit/backends/abstract_backend.py | 4 ++-- tensorcircuit/densitymatrix.py | 4 ++-- tensorcircuit/quantum.py | 20 ++++++++++---------- tensorcircuit/results/counts.py | 2 +- tensorcircuit/shadows.py | 2 +- tensorcircuit/templates/hamiltonians.py | 20 ++++++++++++++------ tensorcircuit/templates/lattice.py | 2 +- tensorcircuit/timeevol.py | 4 ++-- 9 files changed, 34 insertions(+), 26 deletions(-) diff --git a/tensorcircuit/applications/layers.py b/tensorcircuit/applications/layers.py index 64c36bca..fa31549a 100644 --- a/tensorcircuit/applications/layers.py +++ b/tensorcircuit/applications/layers.py @@ -35,7 +35,7 @@ def _resolve(symbol: Union[Symbol, Tensor], i: int = 0) -> Tensor: """ - Make sure the layer is compatible with both multi-param and single param requirements。 + Make sure the layer is compatible with both multi-param and single param requirements What could be the input: list/tuple of sympy.symbol, tf.tensor with 1D or 0D shape """ diff --git a/tensorcircuit/backends/abstract_backend.py b/tensorcircuit/backends/abstract_backend.py index ac3f3d9f..e6e05e2c 100644 --- a/tensorcircuit/backends/abstract_backend.py +++ b/tensorcircuit/backends/abstract_backend.py @@ -1039,8 +1039,8 @@ def searchsorted(self: Any, a: Tensor, v: Tensor, side: str = "left") -> Tensor: :type a: Tensor :param v: value to inserted :type v: Tensor - :param side: If ‘left’, the index of the first suitable location found is given. - If ‘right’, return the last such index. + :param side: If `left`, the index of the first suitable location found is given. + If `right`, return the last such index. If there is no suitable index, return either 0 or N (where N is the length of a), defaults to "left" :type side: str, optional diff --git a/tensorcircuit/densitymatrix.py b/tensorcircuit/densitymatrix.py index 5fbfa420..af0081db 100644 --- a/tensorcircuit/densitymatrix.py +++ b/tensorcircuit/densitymatrix.py @@ -179,9 +179,9 @@ def _contract(self) -> None: @staticmethod def check_kraus(kraus: Sequence[Gate]) -> bool: - """ + r""" Check if Kraus operators satisfy the completeness relation: - sum_i K_i^† K_i = I + :math:`\sum_i K_i^\dagger K_i = I` :param kraus: Sequence of Kraus operators :type kraus: Sequence[Gate] diff --git a/tensorcircuit/quantum.py b/tensorcircuit/quantum.py index 1755a7bb..98b3966f 100644 --- a/tensorcircuit/quantum.py +++ b/tensorcircuit/quantum.py @@ -79,7 +79,7 @@ def _decode_basis_label(label: str, n: int, dim: int) -> List[int]: """ Decode a string basis label into a list of integer digits. - The label is interpreted in base-``dim`` using characters ``0–9A–Z``. + The label is interpreted in base-``dim`` using characters ``0-9A-Z``. Only dimensions up to 36 are supported. :param label: basis label string, e.g. "010" or "A9F" @@ -97,7 +97,7 @@ def _decode_basis_label(label: str, n: int, dim: int) -> List[int]: """ if dim > 36: raise NotImplementedError( - f"String basis label supports d<=36 (0–9A–Z). Got dim={dim}. " + f"String basis label supports d<=36 (0-9A-Z). Got dim={dim}. " "Use an integer array/tensor of length n instead." ) s = label.upper() @@ -107,7 +107,7 @@ def _decode_basis_label(label: str, n: int, dim: int) -> List[int]: for ch in s: if ch not in _ALPHABET: raise ValueError( - f"Invalid character '{ch}' in basis label (allowed 0–9A–Z)." + f"Invalid character '{ch}' in basis label (allowed 0-9A-Z)." ) v = _ALPHABET.index(ch) if v >= dim: @@ -751,7 +751,7 @@ def tensor_product(self, other: "QuOperator") -> "QuOperator": """ Tensor product with another operator. Given two operators `A` and `B`, produces a new operator `AB` representing - :math:`A ⊗ B`. The `out_edges` (`in_edges`) of `AB` is simply the + :math:`A \otimes B`. The `out_edges` (`in_edges`) of `AB` is simply the concatenation of the `out_edges` (`in_edges`) of `A.copy()` with that of `B.copy()`: `new_out_edges = [*out_edges_A_copy, *out_edges_B_copy]` @@ -2403,13 +2403,13 @@ def free_energy( def renyi_entropy(rho: Union[Tensor, QuOperator], k: int = 2) -> Tensor: """ - Compute the Rényi entropy of order :math:`k` by given density matrix. + Compute the Renyi entropy of order :math:`k` by given density matrix. :param rho: The density matrix in form of Tensor or QuOperator. :type rho: Union[Tensor, QuOperator] - :param k: The order of Rényi entropy, default is 2. + :param k: The order of Renyi entropy, default is 2. :type k: int, optional - :return: The :math:`k` th order of Rényi entropy. + :return: The :math:`k` th order of Renyi entropy. :rtype: Tensor """ s = 1 / (1 - k) * backend.real(backend.log(trace_product(*[rho for _ in range(k)]))) @@ -2423,7 +2423,7 @@ def renyi_free_energy( k: int = 2, ) -> Tensor: """ - Compute the Rényi free energy of the corresponding density matrix and Hamiltonian. + Compute the Renyi free energy of the corresponding density matrix and Hamiltonian. :Example: @@ -2440,9 +2440,9 @@ def renyi_free_energy( :type h: Union[Tensor, QuOperator] :param beta: Constant for the optimization, default is 1. :type beta: float, optional - :param k: The order of Rényi entropy, default is 2. + :param k: The order of Renyi entropy, default is 2. :type k: int, optional - :return: The :math:`k` th order of Rényi entropy. + :return: The :math:`k` th order of Renyi entropy. :rtype: Tensor """ energy = backend.real(trace_product(rho, h)) diff --git a/tensorcircuit/results/counts.py b/tensorcircuit/results/counts.py index 662da8d9..2807592d 100644 --- a/tensorcircuit/results/counts.py +++ b/tensorcircuit/results/counts.py @@ -140,7 +140,7 @@ def count2vec( def vec2count(vec: Tensor, prune: bool = False, dim: Optional[int] = None) -> ct: """ Map a count/probability vector of length D to a dictionary with base-d string keys (0-9A-Z). - Only generate string keys when d ≤ 36; if d is inferred to be > 36, raise a NotImplementedError. + Only generate string keys when d <= 36; if d is inferred to be > 36, raise a NotImplementedError. :param vec: A one-dimensional vector of length D = d**n :param prune: Whether to prune near-zero elements (threshold 1e-8) diff --git a/tensorcircuit/shadows.py b/tensorcircuit/shadows.py index 63cad7f6..b253cf1d 100644 --- a/tensorcircuit/shadows.py +++ b/tensorcircuit/shadows.py @@ -334,7 +334,7 @@ def entropy_shadow( def renyi_entropy_2(snapshots: Tensor, sub: Optional[Sequence[int]] = None) -> Tensor: r"""To calculate the second order Renyi entropy of a subsystem from snapshot, please refer to - Brydges, T. et al. Science 364, 260–263 (2019). This function is not jitable. + Brydges, T. et al. Science 364, 260-263 (2019). This function is not jitable. :param snapshots: shape = (ns, repeat, nq) :type: Tensor diff --git a/tensorcircuit/templates/hamiltonians.py b/tensorcircuit/templates/hamiltonians.py index b7e0cc64..692d0454 100644 --- a/tensorcircuit/templates/hamiltonians.py +++ b/tensorcircuit/templates/hamiltonians.py @@ -19,11 +19,11 @@ def heisenberg_hamiltonian( j_coupling: Union[float, List[float], Tuple[float, ...]] = 1.0, interaction_scope: str = "neighbors", ) -> Any: - """ + r""" Generates the sparse matrix of the Heisenberg Hamiltonian for a given lattice. The Heisenberg Hamiltonian is defined as: - H = J * Σ_{i,j} (X_i X_j + Y_i Y_j + Z_i Z_j) + :math:`H = J\sum_{i,j} (X_i X_j + Y_i Y_j + Z_i Z_j)` where the sum is over a specified set of interacting pairs {i,j}. :param lattice: An instance of a class derived from AbstractLattice, @@ -86,13 +86,21 @@ def heisenberg_hamiltonian( def rydberg_hamiltonian( lattice: AbstractLattice, omega: float, delta: float, c6: float ) -> Any: - """ + r""" Generates the sparse matrix of the Rydberg atom array Hamiltonian. The Hamiltonian is defined as: - H = Σ_i (Ω/2)X_i - Σ_i δ(1 - Z_i)/2 + Σ_{i None: This method supports two modes: 1. KDTree mode (use_kdtree=True): Fast, O(N log N) performance for large lattices but breaks differentiability due to scipy dependency - 2. Distance matrix mode (use_kdtree=False): Slower O(N²) but fully differentiable + 2. Distance matrix mode (use_kdtree=False): Slower O(N^2) but fully differentiable and backend-agnostic :param max_k: Maximum number of neighbor shells to compute diff --git a/tensorcircuit/timeevol.py b/tensorcircuit/timeevol.py index 9f13cf5a..ce956fd2 100644 --- a/tensorcircuit/timeevol.py +++ b/tensorcircuit/timeevol.py @@ -387,7 +387,7 @@ def hamiltonian_evol( ... [0.0, 2.0, -1.0, 0.0], ... [0.0, 0.0, 0.0, 1.0] ... ]) - >>> # Initial state |00⟩ + >>> # Initial state |00> >>> psi0 = tc.array_to_tensor([1.0, 0.0, 0.0, 0.0]) >>> # Evolution times >>> times = tc.array_to_tensor([0.0, 0.5, 1.0]) @@ -843,7 +843,7 @@ def estimate_spectral_bounds( :type n_iter: int :param psi0: Optional initial state. :type psi0: Optional[Any] - :return: (E_max, E_min)。 + :return: (E_max, E_min) """ shape = h.shape D = shape[-1] From e4726404ca21af303a1ab5db86772d071bf83cfa Mon Sep 17 00:00:00 2001 From: Weiguo Ma Date: Wed, 10 Sep 2025 13:21:48 +0800 Subject: [PATCH 10/10] typo --- examples/vqe_qudit_example.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/vqe_qudit_example.py b/examples/vqe_qudit_example.py index 077d7715..46c416a4 100644 --- a/examples/vqe_qudit_example.py +++ b/examples/vqe_qudit_example.py @@ -36,7 +36,7 @@ def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float Ansatz: [ for L in 1...nlayers ] - On each site q: - :math:`RX(q; \theta_L q^(01)) RY(q; \theta_L q^(12)) RZ(q; \phi_L q^(0))` + :math:`RX(q; \theta_L q^{(01)}) RY(q; \theta_L q^{(12)}) RZ(q; \phi_L q^{(0)})` (subspace indices shown as superscripts) - Entangle neighboring pairs with RXX on subspaces (0,1) """ @@ -76,8 +76,8 @@ def vqe_forward(param, *, nqudits: int, d: int, nlayers: int, J: float, h: float return tc.backend.real(energy) -def build_param_shape(nqudits: int, d: int, nlayers: int): - # Per layer per qudit: RX^(01), RY^(12) (or dummy), RZ^(0) = 3 params +def build_param_shape(nqudits: 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