From fb07c8a756e2d57191785205e27284c9c4a1f0e1 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Sun, 23 Jul 2023 22:54:13 -0700 Subject: [PATCH 01/28] create job with debias --- pennylane_ionq/api_client.py | 22 ++++++++---- pennylane_ionq/device.py | 69 ++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index 08f9365..891cbd8 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: @@ -220,7 +224,9 @@ def post(self, path, payload): Returns: requests.Response: A response object, or None if no response could be fetched """ - return self.request(requests.post, url=self.join_path(path), data=json.dumps(payload)) + return self.request( + requests.post, url=self.join_path(path), data=json.dumps(payload) + ) class ResourceManager: @@ -259,7 +265,9 @@ def get(self, resource_id=None): resource_id (int): the ID of an object to be retrieved """ if "GET" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException("GET method on this resource is not supported") + 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))) @@ -276,7 +284,9 @@ def create(self, **params): **params: arbitrary parameters to be passed on to the POST request """ if "POST" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException("POST method on this resource is not supported") + raise MethodNotSupportedException( + "POST method on this resource is not supported" + ) if self.resource.id: raise ObjectAlreadyCreatedException("ID must be None when calling create") diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index fed1378..1906260 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -93,14 +93,26 @@ 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, + error_mitigation=None, + api_key=None, + ): if shots is None: - raise ValueError("The ionq device does not support analytic expectation values.") + 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.error_mitigation = error_mitigation self._operation_map = _GATESET_OPS[gateset] self.reset() @@ -109,16 +121,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.", + DeprecationWarning, + stacklevel=2, + ) @property def operations(self): @@ -132,9 +153,12 @@ def operations(self): def apply(self, operations, **kwargs): self.reset() rotations = kwargs.pop("rotations", []) + self.error_mitigation = kwargs.pop("error_mitigation", {}) if len(operations) == 0 and len(rotations) == 0: - warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.") + warnings.warn( + "Circuit is empty. Empty circuits return failures. Submitting anyway." + ) for i, operation in enumerate(operations): if i > 0 and operation.name in {"BasisState", "QubitStateVector"}: @@ -210,7 +234,8 @@ def prob(self): # Here, we rearrange the states to match the big-endian ordering # expected by PennyLane. basis_states = ( - int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in self.histogram + int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) + for k in self.histogram ) idx = np.fromiter(basis_states, dtype=int) @@ -230,7 +255,9 @@ def probability(self, wires=None, shot_range=None, bin_size=None): if shot_range is None and bin_size is None: return self.marginal_prob(self.prob, wires) - return self.estimate_probability(wires=wires, shot_range=shot_range, bin_size=bin_size) + return self.estimate_probability( + wires=wires, shot_range=shot_range, bin_size=bin_size + ) class SimulatorDevice(IonQDevice): @@ -251,8 +278,12 @@ class SimulatorDevice(IonQDevice): 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, *, 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.""" @@ -279,8 +310,24 @@ class QPUDevice(IonQDevice): name = "IonQ QPU PennyLane plugin" short_name = "ionq.qpu" - 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 __init__( + self, + wires, + *, + target="qpu", + gateset="qis", + shots=1024, + error_mitigation=None, + api_key=None, + ): + super().__init__( + wires=wires, + target=target, + gateset=gateset, + shots=shots, + error_mitigation=error_mitigation, + api_key=api_key, + ) def generate_samples(self): """Generates samples from the qpu. From 0d01740eca99001edde29b5dbce1400c7a6a910c Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Sun, 23 Jul 2023 23:08:06 -0700 Subject: [PATCH 02/28] get sharpened results --- pennylane_ionq/api_client.py | 11 ++++++----- pennylane_ionq/device.py | 9 ++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index 891cbd8..d2c383f 100644 --- a/pennylane_ionq/api_client.py +++ b/pennylane_ionq/api_client.py @@ -200,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): """ @@ -255,7 +256,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 @@ -270,9 +271,9 @@ def get(self, resource_id=None): ) 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 1906260..191e73d 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -101,6 +101,7 @@ def __init__( gateset="qis", shots=1024, error_mitigation=None, + sharpen=None, api_key=None, ): if shots is None: @@ -113,6 +114,7 @@ def __init__( self.api_key = api_key self.gateset = gateset self.error_mitigation = error_mitigation + self.sharpen = sharpen self._operation_map = _GATESET_OPS[gateset] self.reset() @@ -212,7 +214,10 @@ def _submit_job(self): if job.is_failed: raise JobExecutionError("Job failed") - job.manager.get(job.id.value) + if self.sharpen is not None: + params["sharpen"] = 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 @@ -318,6 +323,7 @@ def __init__( gateset="qis", shots=1024, error_mitigation=None, + sharpen=None, api_key=None, ): super().__init__( @@ -326,6 +332,7 @@ def __init__( gateset=gateset, shots=shots, error_mitigation=error_mitigation, + sharpen=sharpen, api_key=api_key, ) From ac9d407fa1239b2178393774cebb3a60ee78fb39 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 08:33:59 -0700 Subject: [PATCH 03/28] add error_mitigation and sharpen to the end Co-authored-by: Matthew Silverman --- pennylane_ionq/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 191e73d..0dfabfb 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -100,9 +100,9 @@ def __init__( target="simulator", gateset="qis", shots=1024, + api_key=None, error_mitigation=None, sharpen=None, - api_key=None, ): if shots is None: raise ValueError( From d54f87142c1d1971a5a8a21255c24d0d06c67e4f Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 08:39:42 -0700 Subject: [PATCH 04/28] use userwarning Co-authored-by: Matthew Silverman --- pennylane_ionq/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 0dfabfb..4b499fc 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -139,7 +139,7 @@ def reset(self): self.job["target"] = "qpu.harmony" warnings.warn( "The ionq_qpu backend is deprecated. Defaulting to ionq_qpu.harmony.", - DeprecationWarning, + UserWarning, stacklevel=2, ) From 543c885c1cdc8f58b62388e1cb76677903175526 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 08:45:07 -0700 Subject: [PATCH 05/28] fix undefined params and sharpen Co-authored-by: Matthew Silverman --- pennylane_ionq/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 4b499fc..33a99c9 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -214,8 +214,7 @@ def _submit_job(self): if job.is_failed: raise JobExecutionError("Job failed") - if self.sharpen is not None: - params["sharpen"] = sharpen + params = {} if self.sharpen is None else {"sharpen": self.sharpen} job.manager.get(resource_id=job.id.value, params=params) From 23862908504c7fc079e250577645e645f86065f9 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 08:45:22 -0700 Subject: [PATCH 06/28] reorder args Co-authored-by: Matthew Silverman --- pennylane_ionq/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 33a99c9..516142c 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -321,9 +321,9 @@ def __init__( target="qpu", gateset="qis", shots=1024, + api_key=None, error_mitigation=None, sharpen=None, - api_key=None, ): super().__init__( wires=wires, From bdfa9042ffeb143cb5b2dd03e5d92bb401e11621 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 08:45:38 -0700 Subject: [PATCH 07/28] final arg reorder Co-authored-by: Matthew Silverman --- pennylane_ionq/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 516142c..6c27d46 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -330,9 +330,9 @@ def __init__( target=target, gateset=gateset, shots=shots, + api_key=api_key, error_mitigation=error_mitigation, sharpen=sharpen, - api_key=api_key, ) def generate_samples(self): From f5503b222d97dca370dadd64d9a25fe8570843e2 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 24 Jul 2023 11:42:06 -0700 Subject: [PATCH 08/28] remove unnecessary kwargs pop --- pennylane_ionq/device.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 6c27d46..deaf0d1 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -155,7 +155,6 @@ def operations(self): def apply(self, operations, **kwargs): self.reset() rotations = kwargs.pop("rotations", []) - self.error_mitigation = kwargs.pop("error_mitigation", {}) if len(operations) == 0 and len(rotations) == 0: warnings.warn( From a0a77a7027824e9e71ea645390a4aacb906538bf Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 3 Aug 2023 15:48:41 -0700 Subject: [PATCH 09/28] use new fields for tests --- tests/test_device.py | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index df5f236..328924b 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -96,7 +96,6 @@ def mock_submit_job(*args): dev.apply([]) def test_failedcircuit(self, monkeypatch): - monkeypatch.setattr( requests, "post", lambda url, timeout, data, headers: (url, data, headers) ) @@ -200,13 +199,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.""" @@ -223,17 +222,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, @@ -246,35 +245,32 @@ def mock_submit_job(*args): pass mocker.patch("pennylane_ionq.device.IonQDevice._submit_job", mock_submit_job) - dev = IonQDevice(wires=(0,1,2), gateset="native") + dev = IonQDevice(wires=(0, 1, 2), gateset="native") with qml.tape.QuantumTape() as tape: GPI(0.1, wires=[0]) 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], } - - - From e8ad8379a083519ac74295d0a705f5f2325f15b6 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 7 Aug 2023 08:46:12 -0700 Subject: [PATCH 10/28] add to contributors --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef9426..7caf57b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This release contains contributions from (in alphabetical order): +Spencer Churchill + --- # Release 0.28.0 From 1efeb0dd73af6f65bb29f3d017f2fba8b167cb77 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 19 Oct 2023 09:09:33 -0700 Subject: [PATCH 11/28] format code + add params --- CHANGELOG.md | 6 ++++++ pennylane_ionq/api_client.py | 12 +++--------- pennylane_ionq/device.py | 23 ++++++----------------- tests/test_api_client.py | 8 ++++++-- tests/test_device.py | 2 +- 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7caf57b..fbda263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ ### Improvements +* adds a new `error_mitigation` parameter on job submission so users can + configure strategies +* adds a new `aggregation` parameter on results to allow getting results + aggregated under the two different methods described in + + ### Documentation ### Bug fixes diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index d2c383f..4f6b19d 100644 --- a/pennylane_ionq/api_client.py +++ b/pennylane_ionq/api_client.py @@ -225,9 +225,7 @@ def post(self, path, payload): Returns: requests.Response: A response object, or None if no response could be fetched """ - return self.request( - requests.post, url=self.join_path(path), data=json.dumps(payload) - ) + return self.request(requests.post, url=self.join_path(path), data=json.dumps(payload)) class ResourceManager: @@ -266,9 +264,7 @@ def get(self, resource_id=None, params=None): resource_id (int): the ID of an object to be retrieved """ if "GET" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException( - "GET method on this resource is not supported" - ) + 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)), params=params) @@ -285,9 +281,7 @@ def create(self, **params): **params: arbitrary parameters to be passed on to the POST request """ if "POST" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException( - "POST method on this resource is not supported" - ) + raise MethodNotSupportedException("POST method on this resource is not supported") if self.resource.id: raise ObjectAlreadyCreatedException("ID must be None when calling create") diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index deaf0d1..a481756 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -105,9 +105,7 @@ def __init__( sharpen=None, ): if shots is None: - raise ValueError( - "The ionq device does not support analytic expectation values." - ) + raise ValueError("The ionq device does not support analytic expectation values.") super().__init__(wires=wires, shots=shots) self.target = target @@ -157,9 +155,7 @@ def apply(self, operations, **kwargs): rotations = kwargs.pop("rotations", []) if len(operations) == 0 and len(rotations) == 0: - warnings.warn( - "Circuit is empty. Empty circuits return failures. Submitting anyway." - ) + warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.") for i, operation in enumerate(operations): if i > 0 and operation.name in {"BasisState", "QubitStateVector"}: @@ -237,8 +233,7 @@ def prob(self): # Here, we rearrange the states to match the big-endian ordering # expected by PennyLane. basis_states = ( - int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) - for k in self.histogram + int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in self.histogram ) idx = np.fromiter(basis_states, dtype=int) @@ -258,9 +253,7 @@ def probability(self, wires=None, shot_range=None, bin_size=None): if shot_range is None and bin_size is None: return self.marginal_prob(self.prob, wires) - return self.estimate_probability( - wires=wires, shot_range=shot_range, bin_size=bin_size - ) + return self.estimate_probability(wires=wires, shot_range=shot_range, bin_size=bin_size) class SimulatorDevice(IonQDevice): @@ -281,12 +274,8 @@ class SimulatorDevice(IonQDevice): 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, *, 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.""" diff --git a/tests/test_api_client.py b/tests/test_api_client.py index e30d8e8..3880b15 100755 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -303,12 +303,16 @@ 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") diff --git a/tests/test_device.py b/tests/test_device.py index 328924b..051c038 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -121,7 +121,7 @@ def test_shots(self, shots, monkeypatch, mocker, tol): ) 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( From 10116dbe4d3d6f57f84276abab30cda20b409f4f Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:57:57 -0500 Subject: [PATCH 12/28] Update CHANGELOG.md Co-authored-by: Matthew Silverman --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a85bd..d8bf40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ * adds a new `error_mitigation` parameter on IonQ job submission so users can configure strategies + [(#75)](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/75) + * adds a new `aggregation` parameter on IonQ results to allow getting results aggregated under the two different methods described in + [(#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) From f28f2a95ea1ab815caa42a683e48ac02b767e491 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 13:28:57 -0500 Subject: [PATCH 13/28] increase test coverage --- tests/test_api_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 3880b15..1a20cc4 100755 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -169,7 +169,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 +186,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() From ba6dd7ec4265ae698af98075170cfccd9b285401 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 14:46:18 -0500 Subject: [PATCH 14/28] update changelog --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bf40e..b9b29e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,18 @@ ### Improvements 🛠 -* adds a new `error_mitigation` parameter on IonQ job submission so users can - configure strategies +* 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) -* adds a new `aggregation` parameter on IonQ results to allow getting results +* Adds a new `error_mitigation` parameter on IonQ job submission so users can + configure error mitigation strategies + [(#75)](https://github.com/PennyLaneAI/PennyLane-IonQ/pull/75) + +* Adds a new `aggregation` parameter on IonQ results to allow getting results aggregated under the two different methods described in [(#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) @@ -28,6 +32,7 @@ This release contains contributions from (in alphabetical order): Spencer Churchill +Lillian Frederiksen --- # Release 0.32.0 From 01f6042aa57269a9f30b4f64369701c97ce19d62 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:21:43 -0500 Subject: [PATCH 15/28] add error_mitigation testing --- tests/test_device.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_device.py b/tests/test_device.py index d19e069..9bb85c3 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -142,6 +142,44 @@ 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""" From c9750f1113bbf0c148e6cf358ed78eb7961dabfe Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:33:27 -0500 Subject: [PATCH 16/28] expand on docstrings --- pennylane_ionq/device.py | 51 +++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 4b06207..7633d49 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: ``debiasing`` (bool). + sharpen (bool): whether to use debiasing 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" @@ -100,7 +111,7 @@ def __init__( shots=1024, api_key=None, error_mitigation=None, - sharpen=None, + sharpen=False, ): if shots is None: raise ValueError("The ionq device does not support analytic expectation values.") @@ -265,19 +276,22 @@ 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 + + Kwargs: + 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.""" @@ -293,14 +307,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"``. + Kwargs: + 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. 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: ``debiasing`` (bool). + sharpen (bool): whether to use debiasing 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" @@ -309,7 +332,6 @@ def __init__( self, wires, *, - target="qpu", gateset="qis", shots=1024, backend=None, @@ -317,6 +339,7 @@ def __init__( sharpen=None, api_key=None, ): + target = "qpu" self.backend = backend if self.backend is not None: target += "." + self.backend From 6c288f7bca62433e4ac945e48279a601c17b56c7 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:40:08 -0500 Subject: [PATCH 17/28] black formatting --- pennylane_ionq/api_client.py | 12 +++++++++--- pennylane_ionq/device.py | 23 ++++++++++++++++++----- tests/test_device.py | 18 ++++++++++++++---- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index 4f6b19d..d2c383f 100644 --- a/pennylane_ionq/api_client.py +++ b/pennylane_ionq/api_client.py @@ -225,7 +225,9 @@ def post(self, path, payload): Returns: requests.Response: A response object, or None if no response could be fetched """ - return self.request(requests.post, url=self.join_path(path), data=json.dumps(payload)) + return self.request( + requests.post, url=self.join_path(path), data=json.dumps(payload) + ) class ResourceManager: @@ -264,7 +266,9 @@ def get(self, resource_id=None, params=None): resource_id (int): the ID of an object to be retrieved """ if "GET" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException("GET method on this resource is not supported") + 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)), params=params) @@ -281,7 +285,9 @@ def create(self, **params): **params: arbitrary parameters to be passed on to the POST request """ if "POST" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException("POST method on this resource is not supported") + raise MethodNotSupportedException( + "POST method on this resource is not supported" + ) if self.resource.id: raise ObjectAlreadyCreatedException("ID must be None when calling create") diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 7633d49..2799d57 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -114,7 +114,9 @@ def __init__( sharpen=False, ): if shots is None: - raise ValueError("The ionq device does not support analytic expectation values.") + raise ValueError( + "The ionq device does not support analytic expectation values." + ) super().__init__(wires=wires, shots=shots) self.target = target @@ -164,7 +166,9 @@ def apply(self, operations, **kwargs): rotations = kwargs.pop("rotations", []) if len(operations) == 0 and len(rotations) == 0: - warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.") + warnings.warn( + "Circuit is empty. Empty circuits return failures. Submitting anyway." + ) for i, operation in enumerate(operations): if i > 0 and operation.name in { @@ -246,7 +250,8 @@ def prob(self): # Here, we rearrange the states to match the big-endian ordering # expected by PennyLane. basis_states = ( - int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in self.histogram + int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) + for k in self.histogram ) idx = np.fromiter(basis_states, dtype=int) @@ -266,7 +271,9 @@ def probability(self, wires=None, shot_range=None, bin_size=None): if shot_range is None and bin_size is None: return self.marginal_prob(self.prob, wires) - return self.estimate_probability(wires=wires, shot_range=shot_range, bin_size=bin_size) + return self.estimate_probability( + wires=wires, shot_range=shot_range, bin_size=bin_size + ) class SimulatorDevice(IonQDevice): @@ -291,7 +298,13 @@ class SimulatorDevice(IonQDevice): short_name = "ionq.simulator" 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) + 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.""" diff --git a/tests/test_device.py b/tests/test_device.py index 9bb85c3..c16968e 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -142,7 +142,9 @@ 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.""" @@ -163,7 +165,13 @@ def fake_response(self, resource_id=None, params=None): monkeypatch.setattr(ResourceManager, "get", fake_response) - dev = qml.device("ionq.qpu", wires=1, shots=5000, api_key="test", error_mitigation=error_mitigation) + dev = qml.device( + "ionq.qpu", + wires=1, + shots=5000, + api_key="test", + error_mitigation=error_mitigation, + ) @qml.qnode(dev) def circuit(): @@ -174,12 +182,14 @@ 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"] - @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""" From b3c6f61aa4328383b44c449c05a44ab5b5937d6d Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:47:04 -0500 Subject: [PATCH 18/28] update overview doc to refer to docstrings for details --- doc/devices.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/devices.rst b/doc/devices.rst index 0d3cb28..5695b4a 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,8 +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 +The `simulator 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"``: .. code-block:: python @@ -38,8 +38,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 +The `QPU 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 ``"backend"`` from `available backends `_: From 100d6bfcff718c2ded78f835043b7092777ae8c1 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:48:31 -0500 Subject: [PATCH 19/28] more black formatting --- pennylane_ionq/api_client.py | 12 +++--------- pennylane_ionq/device.py | 15 ++++----------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pennylane_ionq/api_client.py b/pennylane_ionq/api_client.py index d2c383f..4f6b19d 100644 --- a/pennylane_ionq/api_client.py +++ b/pennylane_ionq/api_client.py @@ -225,9 +225,7 @@ def post(self, path, payload): Returns: requests.Response: A response object, or None if no response could be fetched """ - return self.request( - requests.post, url=self.join_path(path), data=json.dumps(payload) - ) + return self.request(requests.post, url=self.join_path(path), data=json.dumps(payload)) class ResourceManager: @@ -266,9 +264,7 @@ def get(self, resource_id=None, params=None): resource_id (int): the ID of an object to be retrieved """ if "GET" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException( - "GET method on this resource is not supported" - ) + 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)), params=params) @@ -285,9 +281,7 @@ def create(self, **params): **params: arbitrary parameters to be passed on to the POST request """ if "POST" not in self.resource.SUPPORTED_METHODS: - raise MethodNotSupportedException( - "POST method on this resource is not supported" - ) + raise MethodNotSupportedException("POST method on this resource is not supported") if self.resource.id: raise ObjectAlreadyCreatedException("ID must be None when calling create") diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 2799d57..c58053c 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -114,9 +114,7 @@ def __init__( sharpen=False, ): if shots is None: - raise ValueError( - "The ionq device does not support analytic expectation values." - ) + raise ValueError("The ionq device does not support analytic expectation values.") super().__init__(wires=wires, shots=shots) self.target = target @@ -166,9 +164,7 @@ def apply(self, operations, **kwargs): rotations = kwargs.pop("rotations", []) if len(operations) == 0 and len(rotations) == 0: - warnings.warn( - "Circuit is empty. Empty circuits return failures. Submitting anyway." - ) + warnings.warn("Circuit is empty. Empty circuits return failures. Submitting anyway.") for i, operation in enumerate(operations): if i > 0 and operation.name in { @@ -250,8 +246,7 @@ def prob(self): # Here, we rearrange the states to match the big-endian ordering # expected by PennyLane. basis_states = ( - int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) - for k in self.histogram + int(bin(int(k))[2:].rjust(self.num_wires, "0")[::-1], 2) for k in self.histogram ) idx = np.fromiter(basis_states, dtype=int) @@ -271,9 +266,7 @@ def probability(self, wires=None, shot_range=None, bin_size=None): if shot_range is None and bin_size is None: return self.marginal_prob(self.prob, wires) - return self.estimate_probability( - wires=wires, shot_range=shot_range, bin_size=bin_size - ) + return self.estimate_probability(wires=wires, shot_range=shot_range, bin_size=bin_size) class SimulatorDevice(IonQDevice): From c2a97935ab45b32091fca898918891e4f8d33c58 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:53:35 -0500 Subject: [PATCH 20/28] update hyperlink --- doc/devices.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/devices.rst b/doc/devices.rst index 5695b4a..bc64192 100644 --- a/doc/devices.rst +++ b/doc/devices.rst @@ -13,9 +13,8 @@ trapped-ion simulator and another to access to IonQ's trapped-ion QPUs. Ideal trapped-ion simulator ------------------------ -The `simulator 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 From 0bfef6728521046bce5d9b3942229e605d8bece9 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 15:59:04 -0500 Subject: [PATCH 21/28] troubleshoot docstring formatting problem --- doc/devices.rst | 5 ++--- pennylane_ionq/device.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/devices.rst b/doc/devices.rst index bc64192..0cda9f5 100644 --- a/doc/devices.rst +++ b/doc/devices.rst @@ -37,9 +37,8 @@ Once the plugin has been installed, you can use this device directly in PennyLan Trapped-Ion QPU --------------- -The `QPU 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/device.py b/pennylane_ionq/device.py index c58053c..2f917cd 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -314,7 +314,7 @@ class QPUDevice(IonQDevice): or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). Kwargs: - gateset (str): the target gateset, either ``"qis"`` or ``"native"``. Defaults to ``qis``. + 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. shots (int, list[int]): Number of circuit evaluations/random samples used to estimate expectation values of observables. Defaults to 1024. If a list of integers is passed, the From bed8239cffd5e409676b5777b958af62dd721506 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 16:09:01 -0500 Subject: [PATCH 22/28] troubleshoot docstring formatting problem 2 --- pennylane_ionq/device.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 2f917cd..eaaec67 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -275,9 +275,7 @@ class SimulatorDevice(IonQDevice): Args: 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']``). - - Kwargs: + or strings (``['ancilla', 'q1', 'q2']``). 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, @@ -313,7 +311,6 @@ 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']``). - Kwargs: 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. shots (int, list[int]): Number of circuit evaluations/random samples used to estimate From 27c332f29c0169ac117ad95a5ce71177b7ce48a4 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 16:41:47 -0500 Subject: [PATCH 23/28] update changelog and docstrings --- CHANGELOG.md | 20 ++++++++++++-------- pennylane_ionq/device.py | 14 +++++++------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b29e5..b74dcaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,22 @@ ### New features since last release -### Improvements 🛠 +* 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 only a single strategy, `debiasing`, 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 -* 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) + dev = qml.device("ionq.qpu", wires=2, error_mitigation={"debias": True}, sharpen=True) + ``` -* Adds a new `error_mitigation` parameter on IonQ job submission so users can - configure error mitigation strategies + 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) -* Adds a new `aggregation` parameter on IonQ results to allow getting results - aggregated under the two different methods described in - +### 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`. diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index eaaec67..f665302 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -79,8 +79,8 @@ class IonQDevice(QubitDevice): 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: ``debiasing`` (bool). - sharpen (bool): whether to use debiasing when accessing the results of an executed job. Defaults to None + 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. @@ -275,7 +275,7 @@ class SimulatorDevice(IonQDevice): Args: 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']``). + or strings (``['ancilla', 'q1', 'q2']``). 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, @@ -322,10 +322,10 @@ class QPUDevice(IonQDevice): 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: ``debiasing`` (bool). - sharpen (bool): whether to use debiasing 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 + 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" From b37d18e11f8ee613f520efa7711ab204697f0224 Mon Sep 17 00:00:00 2001 From: lillian542 Date: Mon, 18 Dec 2023 16:42:16 -0500 Subject: [PATCH 24/28] black formatting for tests --- tests/conftest.py | 4 +--- tests/test_api_client.py | 20 +++++--------------- tests/test_device.py | 41 ++++++++++------------------------------ 3 files changed, 16 insertions(+), 49 deletions(-) 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 1a20cc4..5320278 100755 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -122,17 +122,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: @@ -250,13 +246,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() @@ -312,9 +304,7 @@ def mock_raise(exception): monkeypatch.setattr( requests, "post", - lambda url, data=None, timeout=None, headers=None: mock_raise( - MockException - ), + lambda url, data=None, timeout=None, headers=None: mock_raise(MockException), ) client = api_client.APIClient(debug=True, api_key="test") diff --git a/tests/test_device.py b/tests/test_device.py index c16968e..7a0ffd6 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -51,15 +51,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 +95,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 +110,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, 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,26 +132,20 @@ 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 = {"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) @@ -182,10 +166,7 @@ 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"] @@ -229,9 +210,7 @@ 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] - ) + @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: From 33ab2da2f7953a5b58173f5ffa9971f72645d664 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:53:55 -0500 Subject: [PATCH 25/28] correct wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b74dcaf..adcc5cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### 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 only a single strategy, `debiasing`, is available). Whether or not to + 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 From a4712a6dc6da4a80bcb877a5b63241f063e2c1a6 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Tue, 2 Jan 2024 15:49:51 -0500 Subject: [PATCH 26/28] default backend is harmony --- pennylane_ionq/device.py | 7 +++---- tests/test_device.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index f665302..7746693 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -313,6 +313,7 @@ class QPUDevice(IonQDevice): or strings (``['ancilla', 'q1', 'q2']``). 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. Defaults to 1024. If a list of integers is passed, the circuit evaluations are batched over the list of shots. @@ -337,15 +338,13 @@ def __init__( *, gateset="qis", shots=1024, - backend=None, + backend="harmony", error_mitigation=None, sharpen=None, api_key=None, ): - target = "qpu" + target = f"qpu.{backend}" self.backend = backend - if self.backend is not None: - target += "." + self.backend super().__init__( wires=wires, target=target, diff --git a/tests/test_device.py b/tests/test_device.py index 7a0ffd6..0abe551 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -223,7 +223,7 @@ def test_backend_initialization(self, backend): assert dev.backend == backend else: dev = qml.device("ionq.qpu", wires=2, shots=1000) - assert dev.backend == None + assert dev.backend == "harmony" # default value class TestJobAttribute: From 7bd4a44e5e5a77205df1dc02212cc1205b5de6bf Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Tue, 2 Jan 2024 16:05:46 -0500 Subject: [PATCH 27/28] try to please codecov --- tests/test_api_client.py | 39 ++++++++++++++++++++++++++++++++++++++- tests/test_device.py | 13 +++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 5320278..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, @@ -323,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 @@ -334,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 0abe551..bb855ef 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 @@ -210,6 +211,18 @@ def test_prob_no_results(self, d): dev = qml.device(d, wires=1, shots=1) assert dev.prob is 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.""" From 36b8c960f1839634839d9794a7895ec1d1658fd6 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Tue, 2 Jan 2024 16:27:06 -0500 Subject: [PATCH 28/28] revert and simplify default target --- pennylane_ionq/device.py | 4 +++- tests/test_device.py | 18 +++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pennylane_ionq/device.py b/pennylane_ionq/device.py index 7746693..026f699 100644 --- a/pennylane_ionq/device.py +++ b/pennylane_ionq/device.py @@ -343,8 +343,10 @@ def __init__( sharpen=None, api_key=None, ): - target = f"qpu.{backend}" + target = "qpu" self.backend = backend + if self.backend is not None: + target += "." + self.backend super().__init__( wires=wires, target=target, diff --git a/tests/test_device.py b/tests/test_device.py index bb855ef..847a768 100755 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -226,17 +226,13 @@ def test_probability(self): @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 == "harmony" # default value + dev = qml.device( + "ionq.qpu", + wires=2, + shots=1000, + backend=backend, + ) + assert dev.backend == backend class TestJobAttribute: