diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed88df..68758bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 💔 @@ -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 diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 4f46216..6f7cd37 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -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: diff --git a/pennylane_ionq/ops.py b/pennylane_ionq/ops.py index d826d58..057172a 100644 --- a/pennylane_ionq/ops.py +++ b/pennylane_ionq/ops.py @@ -14,6 +14,7 @@ """ Custom operations """ +import numpy as np from pennylane.operation import Operation @@ -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 diff --git a/tests/test_device.py b/tests/test_device.py index f50f782..c8842fd 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -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: @@ -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) @@ -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) @@ -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) @@ -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"] @@ -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( @@ -305,6 +326,7 @@ 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) @@ -312,7 +334,7 @@ def mock_submit_job(*args): 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, @@ -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"