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

enable partially entangling ms gates #101

Merged
merged 15 commits into from
Apr 30, 2024
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### New features since last release

* Added an optional `theta` parameter to the MS gate to enable partially entangling gates, providing more control over qubit entanglement. This feature allows for finer adjustments in quantum algorithms without affecting existing implementations. [#101](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/101)

### Improvements 🛠

### Breaking changes 💔
Expand All @@ -10,12 +12,16 @@

### Documentation 📝

* Updated the docstring for the MS operation to include the new `theta` parameter, explaining its use and effects on the gate operation.

### Bug fixes 🐛

### Contributors ✍️

This release contains contributions from (in alphabetical order):

Spencer Churchill

---
# Release 0.34.0

Expand Down
4 changes: 3 additions & 1 deletion pennylane_ionq/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ def _apply_operation(self, operation):

if self.gateset == "native":
if len(par) > 1:
gate["phases"] = [float(v) for v in par]
gate["phases"] = [float(v) for v in par[:2]]
if len(par) > 2:
gate["angle"] = float(par[2])
else:
gate["phase"] = float(par[0])
elif par:
Expand Down
55 changes: 45 additions & 10 deletions pennylane_ionq/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""
Custom operations
"""
import numpy as np
from pennylane.operation import Operation


Expand Down Expand Up @@ -61,28 +62,62 @@ class GPI2(Operation): # pylint: disable=too-few-public-methods


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

.. math::

MS(\phi_{0}, \phi_{1}) =
MS(\phi_{0}, \phi_{1}, \theta) =
\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
\cos(\theta / 2) & 0 & 0 & -i e^{-2 \pi i(\phi_{0}+\phi_{1})} \\
0 & \cos(\theta / 2) & -i e^{-2 \pi i (\phi_{0}-\phi_{1})} & 0 \\
0 & -i e^{2 \pi i(\phi_{0}-\phi_{1})} & \cos(\theta / 2) & 0 \\
-i e^{2 \pi i(\phi_{0}+\phi_{1})} & 0 & 0 & \cos(\theta / 2)
\end{pmatrix}

Args:
phi0 (float): phase of the first qubit :math:`\phi`
phi1 (float): phase of the second qubit :math:`\phi`
phi0 (float): phase of the first qubit :math:`\phi_0`
phi1 (float): phase of the second qubit :math:`\phi_1`
theta (float): entanglement ratio of the qubits :math:`\theta \in [0, 0.25]`, defaults to 0.25
wires (Sequence[int]): the subsystems the operation acts on
"""

num_params = 2
num_params = 3
num_wires = 2
grad_method = None

def __init__(self, phi0, phi1, theta=0.25, wires=None):
super().__init__(phi0, phi1, theta, wires=wires)

@staticmethod
def compute_matrix(phi0, phi1, theta): # pylint: disable=arguments-differ
r"""Representation of the operator as a canonical matrix in the computational basis.

Args:
phi0 (float): phase of the first qubit :math:`\phi_0`
phi1 (float): phase of the second qubit :math:`\phi_1`
theta (float): entanglement ratio :math:`\theta`

Returns:
np.ndarray: canonical matrix
"""
cos = np.cos(theta / 2)
exp = np.exp
pi = np.pi
i = 1j
return (
1
/ np.sqrt(2)
* np.array(
[
[cos, 0, 0, -i * exp(-2 * pi * i * (phi0 + phi1))],
[0, cos, -i * exp(-2 * pi * i * (phi0 - phi1)), 0],
[0, -i * exp(2 * pi * i * (phi0 - phi1)), cos, 0],
[-i * exp(2 * pi * i * (phi0 + phi1)), 0, 0, cos],
]
)
)


# Custom operations for the QIS Gateset below

Expand Down
89 changes: 78 additions & 11 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ def test_generate_samples_qpu_device(self, wires, histogram):

unique_outcomes1 = np.unique(sample1, axis=0)
unique_outcomes2 = np.unique(sample2, axis=0)
assert np.all(unique_outcomes1 == unique_outcomes2) # possible outcomes are the same
assert np.all(
unique_outcomes1 == unique_outcomes2
) # possible outcomes are the same

sorted_outcomes1 = np.sort(sample1, axis=0)
sorted_outcomes2 = np.sort(sample2, axis=0)
assert np.all(sorted_outcomes1 == sorted_outcomes2) # set of outcomes is the same
assert np.all(
sorted_outcomes1 == sorted_outcomes2
) # set of outcomes is the same


class TestDeviceIntegration:
Expand Down Expand Up @@ -96,7 +100,9 @@ def test_failedcircuit(self, monkeypatch):
monkeypatch.setattr(
requests, "post", lambda url, timeout, data, headers: (url, data, headers)
)
monkeypatch.setattr(ResourceManager, "handle_response", lambda self, response: None)
monkeypatch.setattr(
ResourceManager, "handle_response", lambda self, response: None
)
monkeypatch.setattr(Job, "is_complete", False)
monkeypatch.setattr(Job, "is_failed", True)

Expand All @@ -111,13 +117,17 @@ def test_shots(self, shots, monkeypatch, mocker, tol):
monkeypatch.setattr(
requests, "post", lambda url, timeout, data, headers: (url, data, headers)
)
monkeypatch.setattr(ResourceManager, "handle_response", lambda self, response: None)
monkeypatch.setattr(
ResourceManager, "handle_response", lambda self, response: None
)
monkeypatch.setattr(Job, "is_complete", True)

def fake_response(self, resource_id=None, params=None):
"""Return fake response data"""
fake_json = {"0": 1}
setattr(self.resource, "data", type("data", tuple(), {"value": fake_json})())
setattr(
self.resource, "data", type("data", tuple(), {"value": fake_json})()
)

monkeypatch.setattr(ResourceManager, "get", fake_response)

Expand All @@ -133,20 +143,26 @@ def circuit():
circuit()
assert json.loads(spy.call_args[1]["data"])["shots"] == shots

@pytest.mark.parametrize("error_mitigation", [None, {"debias": True}, {"debias": False}])
@pytest.mark.parametrize(
"error_mitigation", [None, {"debias": True}, {"debias": False}]
)
def test_error_mitigation(self, error_mitigation, monkeypatch, mocker):
"""Test that shots are correctly specified when submitting a job to the API."""

monkeypatch.setattr(
requests, "post", lambda url, timeout, data, headers: (url, data, headers)
)
monkeypatch.setattr(ResourceManager, "handle_response", lambda self, response: None)
monkeypatch.setattr(
ResourceManager, "handle_response", lambda self, response: None
)
monkeypatch.setattr(Job, "is_complete", True)

def fake_response(self, resource_id=None, params=None):
"""Return fake response data"""
fake_json = {"0": 1}
setattr(self.resource, "data", type("data", tuple(), {"value": fake_json})())
setattr(
self.resource, "data", type("data", tuple(), {"value": fake_json})()
)

monkeypatch.setattr(ResourceManager, "get", fake_response)

Expand All @@ -167,7 +183,10 @@ def circuit():
spy = mocker.spy(requests, "post")
circuit()
if error_mitigation is not None:
assert json.loads(spy.call_args[1]["data"])["error_mitigation"] == error_mitigation
assert (
json.loads(spy.call_args[1]["data"])["error_mitigation"]
== error_mitigation
)
else:
with pytest.raises(KeyError, match="error_mitigation"):
json.loads(spy.call_args[1]["data"])["error_mitigation"]
Expand Down Expand Up @@ -224,7 +243,9 @@ def test_probability(self):
mock_prob.return_value = uniform_prob
assert np.array_equal(dev.probability(), uniform_prob)

@pytest.mark.parametrize("backend", ["harmony", "aria-1", "aria-2", "forte-1", None])
@pytest.mark.parametrize(
"backend", ["harmony", "aria-1", "aria-2", "forte-1", None]
)
def test_backend_initialization(self, backend):
"""Test that the device initializes with the correct backend."""
dev = qml.device(
Expand Down Expand Up @@ -305,14 +326,15 @@ def mock_submit_job(*args):
GPI(0.1, wires=[0])
GPI2(0.2, wires=[1])
MS(0.2, 0.3, wires=[1, 2])
MS(0.4, 0.5, 0.1, wires=[1, 2])

dev.apply(tape.operations)

assert dev.job["input"]["format"] == "ionq.circuit.v0"
assert dev.job["input"]["gateset"] == "native"
assert dev.job["input"]["qubits"] == 3

assert len(dev.job["input"]["circuit"]) == 3
assert len(dev.job["input"]["circuit"]) == 4
assert dev.job["input"]["circuit"][0] == {
"gate": "gpi",
"target": 0,
Expand All @@ -327,4 +349,49 @@ def mock_submit_job(*args):
"gate": "ms",
"targets": [1, 2],
"phases": [0.2, 0.3],
"angle": 0.25,
}
assert dev.job["input"]["circuit"][3] == {
"gate": "ms",
"targets": [1, 2],
"phases": [0.4, 0.5],
"angle": 0.1,
}

@pytest.mark.parametrize(
"phi0, phi1, theta",
[
(0.1, 0.2, 0.25), # Default fully entangling case
(0, 0.3, 0.1), # Partially entangling case
(1.5, 2.7, 0), # No entanglement case
],
)
def test_ms_gate_theta_variation(self, phi0, phi1, theta, tol=1e-6):
"""Test MS gate with different theta values to ensure correct entanglement behavior."""
ms_gate = MS(phi0, phi1, theta, wires=[0, 1])

# Compute the matrix representation of the gate
computed_matrix = ms_gate.compute_matrix(*ms_gate.data)

# Expected matrix
cos = np.cos(theta / 2)
exp = np.exp
pi = np.pi
i = 1j
expected_matrix = (
1
/ np.sqrt(2)
* np.array(
[
[cos, 0, 0, -i * exp(-2 * pi * i * (phi0 + phi1))],
[0, cos, -i * exp(-2 * pi * i * (phi0 - phi1)), 0],
[0, -i * exp(2 * pi * i * (phi0 - phi1)), cos, 0],
[-i * exp(2 * pi * i * (phi0 + phi1)), 0, 0, cos],
]
)
)

assert list(ms_gate.data) == [phi0, phi1, theta]
assert np.allclose(
computed_matrix, expected_matrix, atol=tol
), "Computed matrix does not match the expected matrix"
Loading