diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c34de..adcc5cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,24 @@ ### New features since last release +* Application of debiasing and sharpening for error mitigation is made available, with parameters set on device initialization. Error mitigation strategies that + need to be set at runtime are defined in the `error_mitigation` dictionary (currently a single strategy, `debias`, is available). Whether or not to + apply sharpening to the returned results is set via the parameter `sharpen`. A device using debiasing and sharpening to mitigate errors can be initialized as: + + ```python + import pennylane as qml + + dev = qml.device("ionq.qpu", wires=2, error_mitigation={"debias": True}, sharpen=True) + ``` + + For more details, see the [IonQ Guide on sharpening and debiasing](https://ionq.com/resources/debiasing-and-sharpening), or refer to the publication + [(#75)](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/75) + ### Improvements 🛠 +* The IonQ API version accessed via the plugin is updated from 0.1 to 0.3 + [(#75)](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/75) + * Use new `backend` field to specify `qpu`. [(#81)](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/81) @@ -20,6 +36,7 @@ This release contains contributions from (in alphabetical order): Spencer Churchill +Lillian Frederiksen --- # Release 0.32.0 diff --git a/doc/devices.rst b/doc/devices.rst index 0d3cb28..0cda9f5 100644 --- a/doc/devices.rst +++ b/doc/devices.rst @@ -4,8 +4,8 @@ IonQ Devices The PennyLane-IonQ plugin provides the ability for PennyLane to access devices available via IonQ's online API. -Currently, access is available to two remote devices: an ideal and -a noisy trapped-ion simulator. +Currently, access is available to two remote devices: one to access an ideal +trapped-ion simulator and another to access to IonQ's trapped-ion QPUs. .. raw::html
@@ -13,9 +13,8 @@ a noisy trapped-ion simulator. Ideal trapped-ion simulator ------------------------ -This device provides an ideal noiseless trapped-ion simulation. -Once the plugin has been installed, you can use this device -directly in PennyLane by specifying ``"ionq.simulator"``: +The :class:`~.pennylane_ionq.SimulatorDevice` provides an ideal noiseless trapped-ion simulation. +Once the plugin has been installed, you can use this device directly in PennyLane by specifying ``"ionq.simulator"``: .. code-block:: python @@ -38,9 +37,8 @@ directly in PennyLane by specifying ``"ionq.simulator"``: Trapped-Ion QPU --------------- -This device provides access to IonQ's trapped-ion QPUs. -Once the plugin has been installed, you can use this device -directly in PennyLane by specifying ``"ionq.qpu"`` with a +The :class:`~.pennylane_ionq.QPUDevice` provides access to IonQ's trapped-ion QPUs. Once the plugin has been +installed, you can use this device directly in PennyLane by specifying ``"ionq.qpu"`` with a ``"backend"`` from `available backends `_: .. code-block:: python diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index 08f9365..4f6b19d 100644 --- a/pennylane_ionq/api_client.py +++ b/pennylane_ionq/api_client.py @@ -112,12 +112,16 @@ class APIClient: api_key (str): IonQ cloud platform API key """ - USER_AGENT = "pennylane-ionq-api-client/0.1" - HOSTNAME = "api.ionq.co/v0.1" + USER_AGENT = "pennylane-ionq-api-client/0.3" + HOSTNAME = "api.ionq.co/v0.3" BASE_URL = "https://{}".format(HOSTNAME) def __init__(self, **kwargs): - self.AUTHENTICATION_TOKEN = os.getenv("IONQ_API_KEY") or kwargs.get("api_key", None) + self.AUTHENTICATION_TOKEN = ( + kwargs.get("api_key", None) + or os.getenv("PENNYLANE_IONQ_API_KEY") + or os.getenv("IONQ_API_KEY") + ) self.DEBUG = False if "IONQ_DEBUG" in os.environ: @@ -196,17 +200,18 @@ def request(self, method, **params): return response - def get(self, path): + def get(self, path, params=None): """ Sends a GET request to the provided path. Returns a response object. Args: path (str): path to send the GET request to + params (dict): parameters to include in the request Returns: requests.Response: A response object, or None if no response could be fetched """ - return self.request(requests.get, url=self.join_path(path)) + return self.request(requests.get, url=self.join_path(path), params=params) def post(self, path, payload): """ @@ -249,7 +254,7 @@ def join_path(self, path): """ return join_path(self.resource.PATH, path) - def get(self, resource_id=None): + def get(self, resource_id=None, params=None): """ Attempts to retrieve a particular record by sending a GET request to the appropriate endpoint. If successful, the resource @@ -262,9 +267,9 @@ def get(self, resource_id=None): raise MethodNotSupportedException("GET method on this resource is not supported") if resource_id is not None: - response = self.client.get(self.join_path(str(resource_id))) + response = self.client.get(self.join_path(str(resource_id)), params=params) else: - response = self.client.get(self.resource.PATH) + response = self.client.get(self.resource.PATH, params=params) self.handle_response(response) def create(self, **params): diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 76e6f97..026f699 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -14,7 +14,7 @@ """ This module contains the device class for constructing IonQ devices for PennyLane. """ -import os, warnings +import warnings from time import sleep import numpy as np @@ -63,16 +63,27 @@ class IonQDevice(QubitDevice): r"""IonQ device for PennyLane. Args: - target (str): the target device, either ``"simulator"`` or ``"qpu"`` 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"``. + + Kwargs: + target (str): the target device, either ``"simulator"`` or ``"qpu"``. Defaults to ``simulator``. + gateset (str): the target gateset, either ``"qis"`` or ``"native"``. Defaults to ``qis``. shots (int, list[int]): Number of circuit evaluations/random samples used to estimate - expectation values of observables. + expectation values of observables. Defaults to 1024. If a list of integers is passed, the circuit evaluations are batched over the list of shots. api_key (str): The IonQ API key. If not provided, the environment variable ``IONQ_API_KEY`` is used. + error_mitigation (dict): settings for error mitigation when creating a job. Defaults to None. + Not available on all backends. Set by default on some hardware systems. See + `IonQ API Job Creation `_ and + `IonQ Debiasing and Sharpening `_ for details. + Valid keys include: ``debias`` (bool). + sharpen (bool): whether to use sharpening when accessing the results of an executed job. Defaults to None + (no value passed at job retrieval). Will generally return more accurate results if your expected output + distribution has peaks. See `IonQ Debiasing and Sharpening + `_ for details. """ # pylint: disable=too-many-instance-attributes name = "IonQ PennyLane plugin" @@ -91,7 +102,17 @@ class IonQDevice(QubitDevice): # and therefore does not support the Hermitian observable. observables = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Identity"} - def __init__(self, wires, *, target="simulator", gateset="qis", shots=1024, api_key=None): + def __init__( + self, + wires, + *, + target="simulator", + gateset="qis", + shots=1024, + api_key=None, + error_mitigation=None, + sharpen=False, + ): if shots is None: raise ValueError("The ionq device does not support analytic expectation values.") @@ -99,6 +120,8 @@ def __init__(self, wires, *, target="simulator", gateset="qis", shots=1024, api_ self.target = target self.api_key = api_key self.gateset = gateset + self.error_mitigation = error_mitigation + self.sharpen = sharpen self._operation_map = _GATESET_OPS[gateset] self.reset() @@ -107,16 +130,25 @@ def reset(self): self._prob_array = None self.histogram = None self.circuit = { + "format": "ionq.circuit.v0", "qubits": self.num_wires, "circuit": [], "gateset": self.gateset, } self.job = { - "lang": "json", - "body": self.circuit, + "input": self.circuit, "target": self.target, "shots": self.shots, } + if self.error_mitigation is not None: + self.job["error_mitigation"] = self.error_mitigation + if self.job["target"] == "qpu": + self.job["target"] = "qpu.harmony" + warnings.warn( + "The ionq_qpu backend is deprecated. Defaulting to ionq_qpu.harmony.", + UserWarning, + stacklevel=2, + ) @property def operations(self): @@ -190,7 +222,9 @@ def _submit_job(self): if job.is_failed: raise JobExecutionError("Job failed") - job.manager.get(job.id.value) + params = {} if self.sharpen is None else {"sharpen": self.sharpen} + + job.manager.get(resource_id=job.id.value, params=params) # The returned job histogram is of the form # dict[str, float], and maps the computational basis @@ -242,19 +276,26 @@ 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 + gateset (str): the target gateset, either ``"qis"`` or ``"native"``. Defaults to ``qis``. + shots (int, list[int], None): 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. If a list of integers is passed, the circuit evaluations are batched over the list of shots. + Defaults to 1024. api_key (str): The IonQ API key. If not provided, the environment variable ``IONQ_API_KEY`` is used. """ name = "IonQ Simulator PennyLane plugin" short_name = "ionq.simulator" - 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 __init__(self, wires, *, gateset="qis", shots=1024, api_key=None): + super().__init__( + wires=wires, + target="simulator", + gateset=gateset, + shots=shots, + api_key=api_key, + ) def generate_samples(self): """Generates samples by random sampling with the probabilities returned by the simulator.""" @@ -270,14 +311,23 @@ 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"``. + gateset (str): the target gateset, either ``"qis"`` or ``"native"``. Defaults to ``qis``. backend (str): Optional specifier for an IonQ backend. Can be ``"harmony"``, ``"aria-1"``, etc. + Default to ``harmony``. 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. - If a list of integers is passed, the circuit evaluations are batched over the list of shots. + expectation values of observables. Defaults to 1024. If a list of integers is passed, the + circuit evaluations are batched over the list of shots. api_key (str): The IonQ API key. If not provided, the environment variable ``IONQ_API_KEY`` is used. + error_mitigation (dict): settings for error mitigation when creating a job. Defaults to None. + Not available on all backends. Set by default on some hardware systems. See + `IonQ API Job Creation `_ and + `IonQ Debiasing and Sharpening `_ for details. + Valid keys include: ``debias`` (bool). + sharpen (bool): whether to use sharpening when accessing the results of an executed job. + Defaults to None (no value passed at job retrieval). Will generally return more accurate results if + your expected output distribution has peaks. See `IonQ Debiasing and Sharpening + `_ for details. """ name = "IonQ QPU PennyLane plugin" short_name = "ionq.qpu" @@ -286,12 +336,14 @@ def __init__( self, wires, *, - target="qpu", - backend=None, gateset="qis", shots=1024, + backend="harmony", + error_mitigation=None, + sharpen=None, api_key=None, ): + target = "qpu" self.backend = backend if self.backend is not None: target += "." + self.backend @@ -301,6 +353,8 @@ def __init__( gateset=gateset, shots=shots, api_key=api_key, + error_mitigation=error_mitigation, + sharpen=sharpen, ) def generate_samples(self): diff --git a/tests/conftest.py b/tests/conftest.py index b59ea8d..2e46b6a 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,9 +36,7 @@ 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]]) # ========================================================== diff --git a/tests/test_api_client.py b/tests/test_api_client.py index e30d8e8..e95e720 100755 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -22,6 +22,8 @@ from pennylane_ionq.api_client import ( requests, Job, + Resource, + Field, ResourceManager, ObjectAlreadyCreatedException, MethodNotSupportedException, @@ -122,17 +124,13 @@ 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: @@ -169,7 +167,9 @@ def test_get_unsupported(self): with pytest.raises(MethodNotSupportedException): manager.get(1) - def test_get(self, monkeypatch): + @pytest.mark.parametrize("resource_id", [1, None]) + @pytest.mark.parametrize("params", [{}, {"sharpen": True}, {"sharpen": False}]) + def test_get(self, monkeypatch, resource_id, params): """ Test a successful GET request. Tests that manager.handle_response is being called with the correct Response object. @@ -184,7 +184,7 @@ def test_get(self, monkeypatch): manager = ResourceManager(mock_resource, mock_client) monkeypatch.setattr(manager, "handle_response", MagicMock()) - manager.get(1) + manager.get(resource_id=resource_id, params=params) # TODO test that this is called with correct path mock_client.get.assert_called_once() @@ -248,13 +248,9 @@ 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() @@ -303,12 +299,14 @@ def mock_raise(exception): mock_get_response = MockGETResponse(200) monkeypatch.setattr( - requests, "get", lambda url, timeout, headers: mock_get_response + requests, + "get", + lambda url, params=None, timeout=None, headers=None: mock_get_response, ) monkeypatch.setattr( requests, "post", - lambda url, timeout, headers, data: mock_raise(MockException), + lambda url, data=None, timeout=None, headers=None: mock_raise(MockException), ) client = api_client.APIClient(debug=True, api_key="test") @@ -327,7 +325,40 @@ def mock_raise(exception): assert len(client.errors) == 1 -class TestJob: +class TestResource: + + def test_resource_reloaading(self, monkeypatch): + """Test that ID must be set on resource types when reloading.""" + + class NoID(Resource): + """Dummy API resource without ID set.""" + + def __init__(self, client=None, api_key=None): + """Dummy init.""" + self.fields = (Field("foo", str),) + super().__init__(client=client, api_key=api_key) + + class WithID(Resource): + """Dummy API resource without ID set.""" + + def __init__(self, client=None, api_key=None): + """Dummy init.""" + self.fields = (Field("foo", str), Field("id", str)) + super().__init__(client=client, api_key=api_key) + + monkeypatch.setattr( + requests, "post", lambda url, timeout, headers, data: MockPOSTResponse(201) + ) + + res = NoID(api_key="test") + + with pytest.raises(TypeError, match="Resource does not have an ID"): + res.reload() + + res = WithID(api_key="test") + res.reload() + + def test_create_created(self, monkeypatch): """ Tests a successful Job creatioin with a mock POST response. Asserts that all fields on @@ -338,6 +369,8 @@ def test_create_created(self, monkeypatch): ) job = Job(api_key="test") job.manager.create(params={}) + assert not job.is_complete + assert not job.is_failed keys_to_check = SAMPLE_JOB_CREATE_RESPONSE.keys() for key in keys_to_check: diff --git a/tests/test_device.py b/tests/test_device.py index e50ef4c..847a768 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -17,6 +17,7 @@ import pennylane as qml import pytest import requests +from unittest.mock import PropertyMock, patch from conftest import shortnames from pennylane_ionq.api_client import JobExecutionError, ResourceManager, Job, Field @@ -51,15 +52,11 @@ 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: @@ -99,9 +96,7 @@ 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) @@ -116,17 +111,13 @@ 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): + def fake_response(self, resource_id=None, params=None): """Return fake response data""" fake_json = {"histogram": {"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) @@ -142,6 +133,45 @@ def circuit(): circuit() assert json.loads(spy.call_args[1]["data"])["shots"] == shots + @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(Job, "is_complete", True) + + def fake_response(self, resource_id=None, params=None): + """Return fake response data""" + fake_json = {"histogram": {"0": 1}} + setattr(self.resource, "data", type("data", tuple(), {"value": fake_json})()) + + monkeypatch.setattr(ResourceManager, "get", fake_response) + + dev = qml.device( + "ionq.qpu", + wires=1, + shots=5000, + api_key="test", + error_mitigation=error_mitigation, + ) + + @qml.qnode(dev) + def circuit(): + """Reference QNode""" + qml.PauliX(wires=0) + return qml.expval(qml.PauliZ(0)) + + spy = mocker.spy(requests, "post") + circuit() + if error_mitigation is not None: + 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"] + @pytest.mark.parametrize("shots", [8192]) def test_one_qubit_circuit(self, shots, requires_api, tol): """Test that devices provide correct result for a simple circuit""" @@ -181,22 +211,28 @@ def test_prob_no_results(self, d): dev = qml.device(d, wires=1, shots=1) assert dev.prob is None - @pytest.mark.parametrize( - "backend", ["harmony", "aria-1", "aria-2", "forte-1", None] - ) + def test_probability(self): + """Test that device.probability works.""" + dev = qml.device("ionq.simulator", wires=2) + dev._samples = np.array([[1, 1], [1, 1], [0, 0], [0, 0]]) + assert np.array_equal(dev.probability(shot_range=(0, 2)), [0, 0, 0, 1]) + + uniform_prob = [0.25] * 4 + with patch("pennylane_ionq.device.SimulatorDevice.prob", new_callable=PropertyMock) as mock_prob: + 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]) def test_backend_initialization(self, backend): """Test that the device initializes with the correct backend.""" - if backend: - dev = qml.device( - "ionq.qpu", - wires=2, - shots=1000, - backend=backend, - ) - assert dev.backend == backend - else: - dev = qml.device("ionq.qpu", wires=2, shots=1000) - assert dev.backend == None + dev = qml.device( + "ionq.qpu", + wires=2, + shots=1000, + backend=backend, + ) + assert dev.backend == backend class TestJobAttribute: @@ -216,13 +252,13 @@ def mock_submit_job(*args): dev.apply(tape.operations) - assert dev.job["lang"] == "json" - assert dev.job["body"]["gateset"] == "qis" + assert dev.job["input"]["format"] == "ionq.circuit.v0" + assert dev.job["input"]["gateset"] == "qis" assert dev.job["target"] == "foo" - assert dev.job["body"]["qubits"] == 1 + assert dev.job["input"]["qubits"] == 1 - assert len(dev.job["body"]["circuit"]) == 1 - assert dev.job["body"]["circuit"][0] == {"gate": "x", "target": 0} + assert len(dev.job["input"]["circuit"]) == 1 + assert dev.job["input"]["circuit"][0] == {"gate": "x", "target": 0} def test_parameterized_op(self, mocker): """Tests job attribute several parameterized operations.""" @@ -239,17 +275,17 @@ def mock_submit_job(*args): dev.apply(tape.operations) - assert dev.job["lang"] == "json" - assert dev.job["body"]["gateset"] == "qis" - assert dev.job["body"]["qubits"] == 1 + assert dev.job["input"]["format"] == "ionq.circuit.v0" + assert dev.job["input"]["gateset"] == "qis" + assert dev.job["input"]["qubits"] == 1 - assert len(dev.job["body"]["circuit"]) == 2 - assert dev.job["body"]["circuit"][0] == { + assert len(dev.job["input"]["circuit"]) == 2 + assert dev.job["input"]["circuit"][0] == { "gate": "rx", "target": 0, "rotation": 1.2345, } - assert dev.job["body"]["circuit"][1] == { + assert dev.job["input"]["circuit"][1] == { "gate": "ry", "target": 0, "rotation": 2.3456, @@ -269,24 +305,24 @@ def mock_submit_job(*args): GPI2(0.2, wires=[1]) MS(0.2, 0.3, wires=[1, 2]) - assert dev.job["lang"] == "json" - assert dev.job["body"]["gateset"] == "native" - assert dev.job["body"]["qubits"] == 3 - dev.apply(tape.operations) - assert len(dev.job["body"]["circuit"]) == 3 - assert dev.job["body"]["circuit"][0] == { + 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 dev.job["input"]["circuit"][0] == { "gate": "gpi", "target": 0, "phase": 0.1, } - assert dev.job["body"]["circuit"][1] == { + assert dev.job["input"]["circuit"][1] == { "gate": "gpi2", "target": 1, "phase": 0.2, } - assert dev.job["body"]["circuit"][2] == { + assert dev.job["input"]["circuit"][2] == { "gate": "ms", "targets": [1, 2], "phases": [0.2, 0.3],