diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 14b940afb69..9f99d03b517 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -25,7 +25,7 @@ from scipy import sparse import pennylane as qml -from pennylane.operation import Observable, Operation +from pennylane.operation import Observable, Operation, expand_matrix from pennylane.typing import TensorLike from pennylane.wires import Wires, WiresLike @@ -1205,9 +1205,10 @@ def single_qubit_rot_angles(self) -> list[TensorLike]: class V(Operation): r"""V(wires) - The V operator (square root of NOT) + The V operator, sometimes called the fourth root of X gate, satisfies V^2 = X and V^4 = I. - .. math:: V = \frac{1+i}{2} \begin{bmatrix} 1 & 1\\ 1 & 1\end{bmatrix}. + V is unitary but not Hermitian. Its matrix definition (with no extra phase) is chosen + so that V^2 exactly matches the PauliX operator, and V^\dagger * V = I. **Details:** @@ -1217,82 +1218,49 @@ class V(Operation): Args: wires (Sequence[int] or int): the wire the operation acts on """ + num_wires = 1 num_params = 0 - basis = "X" - - @property - def pauli_rep(self): - if self._pauli_rep is None: - self._pauli_rep = qml.pauli.PauliSentence( - { - qml.pauli.PauliWord({self.wires[0]: "I"}): (0.5 - 0.5j), - qml.pauli.PauliWord({self.wires[0]: "X"}): (0.5 + 0.5j), - } - ) - return self._pauli_rep - - def __repr__(self) -> str: - """String representation.""" - wire = self.wires[0] - if isinstance(wire, str): - return f"V('{wire}')" - return f"V({wire})" + _queue_category = "_ops" @staticmethod @lru_cache() - def compute_matrix() -> np.ndarray: # pylint: disable=arguments-differ - r"""Representation of the operator as a canonical matrix in the computational basis. + def compute_matrix() -> np.ndarray: + return 0.5 * np.array( + [ + [1.0 + 1.0j, 1.0 - 1.0j], + [1.0 - 1.0j, 1.0 + 1.0j], + ], + dtype=complex, + ) - Returns: - ndarray: matrix representation - """ - return 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=complex) + def matrix(self, wire_order=None): + """Return the matrix of V. If self._inverse is True, return V† = (V)⁻¹.""" + mat = self.compute_matrix() - @staticmethod - def compute_eigvals() -> np.ndarray: - r"""Eigenvalues of the operator in the computational basis. + if getattr(self, "_inverse", False): + mat = mat.conj().T - Returns: - array: eigenvalues - """ - return np.array([1, 1j]) + return expand_matrix(mat, wires=self.wires, wire_order=wire_order) def adjoint(self) -> "V": - r"""The adjoint operator is V^3 since V^4 = I.""" - return self.pow(3)[0] + new_op = V(self.wires) + new_op._inverse = not getattr(self, "_inverse", False) + return new_op - def pow(self, z: Union[int, float]): - r"""Implement the power operation for the V gate.""" - if not isinstance(z, int): + def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: + """Only integer powers are well-defined for V so that powers cycle with period 4.""" + if not float(z).is_integer(): raise qml.operation.PowUndefinedError(self, z) - z_mod4 = z % 4 - if z_mod4 == 0 or z_mod4 == 2: # For even powers (0 or 2 mod 4) + + z_int = int(z) % 4 + if z_int == 0: return [] - elif z_mod4 == 1: + if z_int == 1: return [copy(self)] - else: # z_mod4 == 3 - # This is the adjoint V^† - # Create a new V gate and modify its matrix to be V^† - adj = copy(self) - adj._matrix = 0.5 * np.array([[1 - 1j, 1 + 1j], [1 + 1j, 1 - 1j]], dtype=complex) - return [adj] - - @staticmethod - def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: - r"""Decomposition of V gate into basic gates. - - V = RZ(-π/2)RY(π/2)RZ(π/2) - """ - return [ - qml.RZ(np.pi/2, wires=wires), - qml.RY(np.pi/2, wires=wires), - qml.RZ(-np.pi/2, wires=wires), - ] - - def single_qubit_rot_angles(self) -> list[TensorLike]: - # V = RZ(-\pi/2) RY(\pi/2) RZ(\pi/2) - return [np.pi/2, np.pi/2, -np.pi/2] + if z_int == 2: + return [qml.PauliX(wires=self.wires)] + return [qml.PauliX(wires=self.wires), copy(self)] class G(Operation): diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 51a309c5f05..126bd187d9f 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -57,8 +57,6 @@ (qml.S, S), (qml.T, T), (qml.ECR, ECR), - (qml.V, 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]])), - (qml.G, 1/np.sqrt(2) * np.array([[1, -1], [1, 1]])), # Controlled operations (qml.CNOT, CNOT), (qml.CH, CH), @@ -82,7 +80,7 @@ (qml.SISWAP(["qubit0", "qubit1"]), SISWAP), ] -STRING_REPR = ( +STRING_REPR = [ (qml.Identity(0), "I(0)"), (qml.Hadamard(0), "H(0)"), (qml.PauliX(0), "X(0)"), @@ -108,8 +106,8 @@ (qml.Z(3), "Z(3)"), (qml.T(0), "T(0)"), (qml.S(0), "S(0)"), - (qml.SX(0), "SX(0)"), -) + (qml.SX(0), "SX(0)") +] @pytest.mark.parametrize("wire", [0, "a", "a"]) @@ -943,8 +941,29 @@ def test_repr(self): op_repr = qml.MultiControlledX(wires=wires, control_values=control_values).__repr__() assert op_repr == f"MultiControlledX(wires={wires}, control_values={control_values})" + def test_v_gate_adjoint(self, tol): + """Test that the adjoint of the V gate is correctly implemented as V^†.""" + v = qml.V(0) + v_adj = v.adjoint() + + # V^† should be the conjugate transpose of V + v_matrix = v.matrix() + v_adj_matrix = v_adj.matrix() + + # Check that V^† * V = I + product = v_adj_matrix @ v_matrix + assert np.allclose(product, np.eye(2), atol=tol) + + # Check that V^2 = X + v2 = v_matrix @ v_matrix + assert np.allclose(v2, qml.X(0).matrix(), atol=tol) + + # Check that V^4 = I + v4 = v2 @ v2 + assert np.allclose(v4, np.eye(2), atol=tol) + -period_two_ops = ( +period_two_ops = [ qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0), @@ -952,8 +971,6 @@ def test_repr(self): qml.SWAP(wires=(0, 1)), qml.ISWAP(wires=(0, 1)), qml.ECR(wires=(0, 1)), - qml.V(wires=0), - qml.G(wires=0), # Controlled operations qml.CNOT(wires=(0, 1)), qml.CY(wires=(0, 1)), @@ -963,7 +980,7 @@ def test_repr(self): qml.CSWAP(wires=(0, 1, 2)), qml.Toffoli(wires=(0, 1, 2)), qml.MultiControlledX(wires=(0, 1, 2, 3)) -) +] class TestPowMethod: @@ -1280,12 +1297,12 @@ def test_involution_operators(op): assert adj_op.name == op.name -op_pauli_rep = ( +op_pauli_rep = [ (qml.PauliX(wires=0), qml.pauli.PauliSentence({qml.pauli.PauliWord({0: "X"}): 1})), (qml.PauliY(wires="a"), qml.pauli.PauliSentence({qml.pauli.PauliWord({"a": "Y"}): 1})), (qml.PauliZ(wires=4), qml.pauli.PauliSentence({qml.pauli.PauliWord({4: "Z"}): 1})), - (qml.Identity(wires="target"), qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1})), -) + (qml.Identity(wires="target"), qml.pauli.PauliSentence({qml.pauli.PauliWord({}): 1})) +] @pytest.mark.parametrize("op, rep", op_pauli_rep) @@ -1359,64 +1376,23 @@ def test_matrix_and_pauli_rep_equivalence(self, op, rep): assert np.allclose(op.matrix(), qml.matrix(op.pauli_rep, wire_order=op.wires)) assert np.allclose(rep, qml.matrix(op.pauli_rep, wire_order=op.wires)) - def test_v_gate_matrix(self, tol): - """Test that the V gate has the correct matrix representation.""" - op = qml.V(wires=0) - res = op.matrix() - expected = 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]]) - assert np.allclose(res, expected, atol=tol) - - def test_v_gate_decomposition(self, tol): - """Test that the V gate decomposition is correct.""" - op = qml.V(wires=0) - decomp = op.decomposition() + def test_v_gate_adjoint(self, tol): + """Test that the adjoint of the V gate is correctly implemented as V^†.""" + v = qml.V(0) + v_adj = v.adjoint() - # V = RZ(-π/2)RY(π/2)RZ(π/2) - assert len(decomp) == 3 - assert isinstance(decomp[0], qml.RZ) - assert isinstance(decomp[1], qml.RY) - assert isinstance(decomp[2], qml.RZ) - assert np.allclose(decomp[0].data[0], np.pi/2, atol=tol) - assert np.allclose(decomp[1].data[0], np.pi/2, atol=tol) - assert np.allclose(decomp[2].data[0], -np.pi/2, atol=tol) - - def test_g_gate_matrix(self, tol): - """Test that the G gate has the correct matrix representation.""" - op = qml.G(wires=0) - res = op.matrix() - expected = 1/np.sqrt(2) * np.array([[1, -1], [1, 1]]) - assert np.allclose(res, expected, atol=tol) - - def test_g_gate_decomposition(self, tol): - """Test that the G gate decomposition is correct.""" - op = qml.G(wires=0) - decomp = op.decomposition() + # V^† should be the conjugate transpose of V + v_matrix = v.matrix() + v_adj_matrix = v_adj.matrix() - # G = RZ(π)RY(-π/4)RZ(0) - assert len(decomp) == 3 - assert isinstance(decomp[0], qml.RZ) - assert isinstance(decomp[1], qml.RY) - assert isinstance(decomp[2], qml.RZ) - assert np.allclose(decomp[0].data[0], 0, atol=tol) - assert np.allclose(decomp[1].data[0], -np.pi/4, atol=tol) - assert np.allclose(decomp[2].data[0], np.pi, atol=tol) - - def test_v_gate_adjoint(self, tol): - """Test that the adjoint of the V gate is correct.""" - op = qml.V(wires=0) - op_adj = op.adjoint() + # Check that V^† * V = I + product = v_adj_matrix @ v_matrix + assert np.allclose(product, np.eye(2), atol=tol) - # V^† = V^3 (since V^4 = I) - res = op_adj.matrix() - expected = np.linalg.matrix_power(op.matrix(), 3) - assert np.allclose(res, expected, atol=tol) - - def test_g_gate_adjoint(self, tol): - """Test that the adjoint of the G gate is correct.""" - op = qml.G(wires=0) - op_adj = op.adjoint() + # Check that V^2 = X + v2 = v_matrix @ v_matrix + assert np.allclose(v2, qml.X(0).matrix(), atol=tol) - # G^† = G (since G^2 = I) - res = op_adj.matrix() - expected = op.matrix() - assert np.allclose(res, expected, atol=tol) + # Check that V^4 = I + v4 = v2 @ v2 + assert np.allclose(v4, np.eye(2), atol=tol)