Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeKitchen committed Jan 2, 2025
1 parent 3444eae commit 1867711
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 134 deletions.
98 changes: 33 additions & 65 deletions pennylane/ops/qubit/non_parametric_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:**
Expand All @@ -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)

Check notice on line 1248 in pennylane/ops/qubit/non_parametric_ops.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/ops/qubit/non_parametric_ops.py#L1248

Attribute '_inverse' defined outside __init__ (attribute-defined-outside-init)
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):
Expand Down
114 changes: 45 additions & 69 deletions tests/ops/qubit/test_non_parametric_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)"),
Expand All @@ -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"])
Expand Down Expand Up @@ -943,17 +941,36 @@ 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),
qml.Hadamard("a"),
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)),
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

0 comments on commit 1867711

Please sign in to comment.