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

Add IonQ native gateset support #55

Merged
merged 6 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pennylane_ionq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
PennyLane IonQ overview
=======================
"""
from .ops import GPI, GPI2, MS, XX, YY, ZZ
from .device import SimulatorDevice, QPUDevice
from ._version import __version__
91 changes: 59 additions & 32 deletions pennylane_ionq/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,40 @@
from .api_client import Job, JobExecutionError
from ._version import __version__

_qis_operation_map = {
# native PennyLane operations also native to IonQ
"PauliX": "x",
"PauliY": "y",
"PauliZ": "z",
"Hadamard": "h",
"CNOT": "cnot",
"SWAP": "swap",
"RX": "rx",
"RY": "ry",
"RZ": "rz",
"S": "s",
"S.inv": "si",
"T": "t",
"T.inv": "ti",
"SX": "v",
"SX.inv": "vi",
# additional operations not native to PennyLane but present in IonQ
"XX": "xx",
"YY": "yy",
"ZZ": "zz",
}

_native_operation_map = {
"GPI": "gpi",
"GPI2": "gpi2",
"MS": "ms",
}

_GATESET_OPS = {
"native": _native_operation_map,
"qis": _qis_operation_map,
}


class IonQDevice(QubitDevice):
r"""IonQ device for PennyLane.
Expand All @@ -35,6 +69,7 @@ class IonQDevice(QubitDevice):
wires (int or Iterable[Number, str]]): Number of wires to initialize the device with,
or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``)
or strings (``['ancilla', 'q1', 'q2']``).
gateset (str): the target gateset, either ``"qis"`` or ``"native"``.
shots (int, list[int]): Number of circuit evaluations/random samples used to estimate
expectation values of observables.
If a list of integers is passed, the circuit evaluations are batched over the list of shots.
Expand All @@ -54,47 +89,30 @@ class IonQDevice(QubitDevice):
"inverse_operations": True,
}

_operation_map = {
# native PennyLane operations also native to IonQ
"PauliX": "x",
"PauliY": "y",
"PauliZ": "z",
"Hadamard": "h",
"CNOT": "cnot",
"SWAP": "swap",
"RX": "rx",
"RY": "ry",
"RZ": "rz",
"S": "s",
"S.inv": "si",
"T": "t",
"T.inv": "ti",
"SX": "v",
"SX.inv": "vi",
# additional operations not native to PennyLane but present in IonQ
"XX": "xx",
"YY": "yy",
"ZZ": "zz",
}

# Note: unlike QubitDevice, IonQ does not support QubitUnitary,
# and therefore does not support the Hermitian observable.
observables = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Identity"}

def __init__(self, wires, *, target="simulator", shots=1024, api_key=None):
def __init__(self, wires, *, target="simulator", gateset="qis", shots=1024, api_key=None):
Cynocracy marked this conversation as resolved.
Show resolved Hide resolved
if shots is None:
raise ValueError("The ionq device does not support analytic expectation values.")

super().__init__(wires=wires, shots=shots)
self.target = target
self.api_key = api_key
self.gateset = gateset
self._operation_map = _GATESET_OPS[gateset]
self.reset()

def reset(self):
"""Reset the device"""
self._prob_array = None
self.histogram = None
self.circuit = {"qubits": self.num_wires, "circuit": []}
self.circuit = {
"qubits": self.num_wires,
"circuit": [],
"gateset": self.gateset,
}
self.job = {
"lang": "json",
"body": self.circuit,
Expand Down Expand Up @@ -138,7 +156,7 @@ def _apply_operation(self, operation):
par = operation.parameters

if len(wires) == 2:
if name in {"SWAP", "XX", "YY", "ZZ"}:
if name in {"SWAP", "XX", "YY", "ZZ", "MS"}:
# these gates takes two targets
gate["targets"] = wires
else:
Expand All @@ -147,7 +165,12 @@ def _apply_operation(self, operation):
else:
gate["target"] = wires[0]

if par:
if self.gateset == "native":
if len(par) > 1:
gate["phases"] = [float(v) for v in par]
else:
gate["phase"] = float(par[0])
elif par:
gate["rotation"] = float(par[0])

self.circuit["circuit"].append(gate)
Expand Down Expand Up @@ -217,6 +240,7 @@ class SimulatorDevice(IonQDevice):
wires (int or Iterable[Number, str]]): Number of wires to initialize the device with,
or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``)
or strings (``['ancilla', 'q1', 'q2']``).
gateset (str): the target gateset, either ``"qis"`` or ``"native"``.
shots (int, list[int]): Number of circuit evaluations/random samples used to estimate
expectation values of observables. If ``None``, the device calculates probability, expectation values,
and variances analytically. If an integer, it specifies the number of samples to estimate these quantities.
Expand All @@ -227,8 +251,8 @@ class SimulatorDevice(IonQDevice):
name = "IonQ Simulator PennyLane plugin"
short_name = "ionq.simulator"

def __init__(self, wires, *, target="simulator", shots=1024, api_key=None):
super().__init__(wires=wires, target=target, shots=shots, api_key=api_key)
def __init__(self, wires, *, target="simulator", gateset="qis", shots=1024, api_key=None):
super().__init__(wires=wires, target=target, gateset=gateset, shots=shots, api_key=api_key)

def generate_samples(self):
"""Generates samples by random sampling with the probabilities returned by the simulator."""
Expand All @@ -244,6 +268,7 @@ class QPUDevice(IonQDevice):
wires (int or Iterable[Number, str]]): Number of wires to initialize the device with,
or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``)
or strings (``['ancilla', 'q1', 'q2']``).
gateset (str): the target gateset, either ``"qis"`` or ``"native"``.
shots (int, list[int]): Number of circuit evaluations/random samples used to estimate
expectation values of observables. If ``None``, the device calculates probability, expectation values,
and variances analytically. If an integer, it specifies the number of samples to estimate these quantities.
Expand All @@ -254,8 +279,8 @@ class QPUDevice(IonQDevice):
name = "IonQ QPU PennyLane plugin"
short_name = "ionq.qpu"

def __init__(self, wires, *, target="qpu", shots=1024, api_key=None):
super().__init__(wires=wires, target=target, shots=shots, api_key=api_key)
def __init__(self, wires, *, target="qpu", gateset="qis", shots=1024, api_key=None):
super().__init__(wires=wires, target=target, gateset=gateset, shots=shots, api_key=api_key)

def generate_samples(self):
"""Generates samples from the qpu.
Expand All @@ -266,7 +291,9 @@ def generate_samples(self):
"""
number_of_states = 2**self.num_wires
counts = np.rint(
self.prob * self.shots, out=np.zeros(number_of_states, dtype=int), casting="unsafe"
self.prob * self.shots,
out=np.zeros(number_of_states, dtype=int),
casting="unsafe",
)
samples = np.repeat(np.arange(number_of_states), counts)
np.random.shuffle(samples)
Expand Down
69 changes: 66 additions & 3 deletions pennylane_ionq/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,72 @@
"""
from pennylane.operation import Operation

# Custom operations for the native gateset below.
class GPI(Operation): # pylint: disable=too-few-public-methods
r"""GPI(phi, wires)
Single-qubit GPI gate.

.. math::

GPI(\phi) =
\begin{pmatrix}
0 & e^{-i 2 \pi \phi} \\
e^{i 2 \pi \phi} & 0
\end{pmatrix}
Args:
phi (float): phase :math:`\phi`
wires (Sequence[int]): the subsystems the operation acts on
"""
num_params = 1
num_wires = 1
grad_method = None


class GPI2(Operation): # pylint: disable=too-few-public-methods
r"""GPI2(phi, wires)
Single-qubit GPI2 gate.

.. math::

GPI2(\phi) =
\begin{pmatrix}
1 & -i e^{-2 \pi i \phi} \\
-i e^{2 \pi i \phi} & 1
\end{pmatrix}
Args:
phi (float): phase :math:`\phi`
wires (Sequence[int]): the subsystems the operation acts on
"""
num_params = 1
num_wires = 1
grad_method = None


class MS(Operation): # pylint: disable=too-few-public-methods
r"""MS(phi0, phi1, wires)
2-qubit entanlging MS gate.

.. math::

MS(\phi_{0}, \phi_{1}) =
\frac{1}{\sqrt{2}}\begin{pmatrix}
1 & 0 & 0 & -i e^{-2 \pi i(\phi_{0}+\phi_{1})} \\
0 & 1 & -i e^{-2 \pi i (\phi_{0}-\phi_{1})} & 0 \\
0 & -i e^{2 \pi i(\phi_{0}-\phi_{1})} & 1 & 0 \\
-i e^{2 \pi i(\phi_{0}+\phi_{1})} & 0 & 0 & 1
\end{pmatrix}
Args:
phi0 (float): phase of the first qubit :math:`\phi`
phi1 (float): phase of the second qubit :math:`\phi`
wires (Sequence[int]): the subsystems the operation acts on
"""
num_params = 2
num_wires = 2
grad_method = None


# Custom operations for the QIS Gateset below


class XX(Operation):
r"""XX(phi, wires)
Expand All @@ -36,7 +102,6 @@ class XX(Operation):
"""
num_params = 1
num_wires = 2
Cynocracy marked this conversation as resolved.
Show resolved Hide resolved
par_domain = "R"
grad_method = "A"


Expand All @@ -59,7 +124,6 @@ class YY(Operation):
"""
num_params = 1
num_wires = 2
par_domain = "R"
grad_method = "A"


Expand All @@ -82,5 +146,4 @@ class ZZ(Operation):
"""
num_params = 1
num_wires = 2
par_domain = "R"
grad_method = "A"
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
U2 = np.array([[0, 1, 1, 1], [1, 0, 1, -1], [1, -1, 0, 1], [1, 1, -1, 0]]) / np.sqrt(3)

# single qubit Hermitian observable
A = np.array([[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]])
A = np.array(
[[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]]
)


# ==========================================================
Expand Down
29 changes: 22 additions & 7 deletions tests/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ def client():
return api_client.APIClient(api_key="test")


SAMPLE_JOB_CREATE_RESPONSE = {"id": "a6a146d0-d64f-42f4-8b17-ec761fbab7fd", "status": "ready"}
SAMPLE_JOB_CREATE_RESPONSE = {
"id": "a6a146d0-d64f-42f4-8b17-ec761fbab7fd",
"status": "ready",
}

SAMPLE_JOB_RESPONSE = {
"id": "617a1f8b-59d4-435d-aa33-695433d7155e",
Expand Down Expand Up @@ -119,13 +122,17 @@ def test_set_authorization_header(self):

authentication_token = MagicMock()
client.set_authorization_header(authentication_token)
assert client.HEADERS["Authorization"] == "apiKey {}".format(authentication_token)
assert client.HEADERS["Authorization"] == "apiKey {}".format(
authentication_token
)

def test_join_path(self, client):
"""
Test that two paths can be joined and separated by a forward slash.
"""
assert client.join_path("jobs") == "{client.BASE_URL}/jobs".format(client=client)
assert client.join_path("jobs") == "{client.BASE_URL}/jobs".format(
client=client
)


class TestResourceManager:
Expand Down Expand Up @@ -241,9 +248,13 @@ def test_handle_response(self, monkeypatch):

manager = ResourceManager(mock_resource, mock_client)

monkeypatch.setattr(manager, "handle_success_response", mock_handle_success_response)
monkeypatch.setattr(
manager, "handle_success_response", mock_handle_success_response
)

monkeypatch.setattr(manager, "handle_error_response", mock_handle_error_response)
monkeypatch.setattr(
manager, "handle_error_response", mock_handle_error_response
)

manager.handle_response(mock_response)
assert manager.http_response_data == mock_response.json()
Expand Down Expand Up @@ -291,9 +302,13 @@ def mock_raise(exception):

mock_get_response = MockGETResponse(200)

monkeypatch.setattr(requests, "get", lambda url, timeout, headers: mock_get_response)
monkeypatch.setattr(
requests, "post", lambda url, timeout, headers, data: mock_raise(MockException)
requests, "get", lambda url, timeout, headers: mock_get_response
)
monkeypatch.setattr(
requests,
"post",
lambda url, timeout, headers, data: mock_raise(MockException),
)

client = api_client.APIClient(debug=True, api_key="test")
Expand Down
Loading