Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix gpi diff 1 #18

Draft
wants to merge 9 commits into
base: rc-0.3
Choose a base branch
from
40 changes: 39 additions & 1 deletion ionizer/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
"""
Native gates for IonQ hardware as PennyLane operations.
"""
import functools
import inspect
import numpy as np

import pennylane as qml
from pennylane.operation import Operation


stack_last = functools.partial(qml.math.stack, axis=-1)


class GPI(Operation):
r"""
The single-qubit GPI rotation
Expand All @@ -32,6 +37,8 @@ class GPI(Operation):
e^{i\phi} & 0
\end{bmatrix}.

This operation does not work with adjoint differentiation, it will result in an incorrect output.

Args:
phi (float): rotation angle :math:`\phi`
wires (Sequence[int] or int): the wire the operation acts on
Expand All @@ -43,6 +50,30 @@ class GPI(Operation):
num_wires = 1
num_params = 1
ndim_params = (0,)
grad_method = "A"
parameter_frequencies = [(1,)]

grad_recipe = ([[1, 1, np.pi / 4], [-1, 1, -np.pi / 4]],)

def generator(self):
r"""The Generator for GPI gate.

.. math::
\hat(G) = \frac{\pi}{2}\begin{bmatrix}
1 & e^{i\phi} \\
e^{-i\phi} & 1
\end{bmatrix} = -\frac{\pi}{2}(I-\cos(\phi)X-\sin(\phi)Y).
"""
phi = self.data[0]
c = (np.pi / 2) / phi
coeffs = np.array([c, -c * qml.math.cos(phi), -c * qml.math.sin(phi)])
observables = [
qml.Identity(wires=self.wires),
qml.PauliX(wires=self.wires),
qml.PauliY(wires=self.wires),
]

return qml.Hamiltonian(coeffs, observables)

# Note: disable pylint complaint about redefined built-in, since the id
# value itself is coming from the class definition of Operators in PennyLane proper.
Expand All @@ -65,7 +96,14 @@ def compute_matrix(phi): # pylint: disable=arguments-differ
array([[0. +0.j , 0.95533649-0.29552021j],
[0.95533649+0.29552021j, 0. +0.j ]])
"""
return qml.math.stack([[0, qml.math.exp(-1j * phi)], [qml.math.exp(1j * phi), 0]])
if qml.math.get_interface(phi) == "tensorflow":
phi = qml.math.cast_like(phi, 1j)

a = 0 + 0j
b = qml.math.exp((0 - 1j) * phi)
return qml.math.stack(
qml.math.stack([stack_last([a, b]), stack_last([qml.math.conj(b), a])])
)

def adjoint(self):
# The GPI gate is its own adjoint.
Expand Down
101 changes: 101 additions & 0 deletions tests/test_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Unit tests for GPI2 operation and its utility functions.
"""

import pytest

import pennylane as qml
from pennylane import numpy as np
import pennylane.math as math
from ionizer.ops import GPI

diff_methods = ["backprop", "parameter-shift", "finite-diff"]
two_pi = 2 * np.pi


def get_GPI_matrix(phi):
"""Gets the GPI expected GPI matrix"""
return np.array([[0, math.exp(-1j * phi)], [math.exp(1j * phi), 0]])


class State:
@staticmethod
def set_state():
"""Quantum function to set a known starting state to use for testing."""
qml.RX(0.2, wires=0)
qml.RY(1.1, wires=0)
qml.RX(0.3, wires=0)

@classmethod
@qml.qnode(qml.device("default.qubit", wires=1))
def get_state(cls):
"""Gets the predefined starting state"""
cls.set_state()
return qml.state()

def __init__(self):
"""Creates an object for an intial quantum state."""
self.state = self.get_state()
self.a_conj_b = self.state[0] * np.conj(self.state[1])
self.b_conj_a = self.state[1] * np.conj(self.state[0])


@pytest.mark.parametrize("phi", [0, 0.37 * two_pi, 1.23 * two_pi, two_pi])
class TestGPI:
@staticmethod
def circuit(phi):
"""Circuit that applies the GPI gate to a known starting state
returning the expectation value of Pauli Y."""
State.set_state()
GPI(phi, wires=0)
return qml.expval(qml.PauliY(wires=0))

@pytest.fixture(autouse=True)
def state(self):
"""Starting state to be used in computation"""
self._state = State()

def test_GPI_compute_matrix(self, phi):
"""Tests wheter the right matrix is returned depending on phi."""
gpi_matrix = GPI.compute_matrix(phi)
check_matrix = get_GPI_matrix(phi)

assert math.allclose(gpi_matrix, check_matrix)

def test_GPI_circuit(self, phi):
"""Tests whether the expected value is returned for a circuit using GPI"""
phi_GPI = np.array(phi)
dev = qml.device("default.qubit", wires=1)

qnode_GPI = qml.QNode(self.circuit, dev)
val_GPI = qnode_GPI(phi_GPI)

expected_inner_product_1 = 1j * self._state.b_conj_a * np.exp(-2j * phi)
expected_inner_product_2 = -1j * self._state.a_conj_b * np.exp(2j * phi)
expected_val = expected_inner_product_1 + expected_inner_product_2

assert np.isclose(
val_GPI, expected_val, atol=1e-07
), f"Given val: {val_GPI}; Expected val: {expected_val}"

def get_circuit_grad(self, phi):
"""Gets the expected gradient of the basic circuit using GPI's output."""
expected_inner_product_1 = 1j * self._state.b_conj_a * np.exp(-2j * phi) * (-2j)
expected_inner_product_2 = -1j * self._state.a_conj_b * np.exp(2j * phi) * 2j
return np.real(expected_inner_product_1 + expected_inner_product_2)

@pytest.mark.parametrize("diff_method", diff_methods)
def test_GPI_grad(self, diff_method, phi):
"""Tests whether the correct gradient for a circuit using GPI
using different gradient computation methods."""

phi_GPI = np.array(phi)
dev = qml.device("default.qubit", wires=1)

qnode_GPI = qml.QNode(self.circuit, dev, diff_method=diff_method)
grad_GPI = qml.grad(qnode_GPI, argnum=0)(phi_GPI)

expected_grad = self.get_circuit_grad(phi)
assert np.isclose(
grad_GPI, expected_grad
), f"Given grad: {grad_GPI}; Expected grad: {expected_grad}"
Loading