From ef7dfa58ca5fc0c1c68f63bde8973d14b4360d24 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 28 Jan 2022 19:28:58 -0500 Subject: [PATCH] Add circuit-runner, sampler and custom VQE programs from Qiskit-Runtime (#157) * Small change. * First commit * Working circuit runner. * Sampler * Sampler * VQE is working. * Headers. * Update for batches. * Update docstrigs. * CodeFactor. * Add tests for runtime. * Tests. * Test. * Test circuit runner. * Test Sampler. * Correct generate_samples * Add entry points. * More shots. * Add token arg. * Change import * Change Sampler sample. * Circuit runner kwargs test. * Shots kwargs * Change import. * Add kwargs Sampler test. * Import. * Impport again. * Import change * Add test tracker. * Update track tests. * Update tracker runtime. * Update tracker. * Update. * Codfactor changes. * Update. * Update codefactor * More codefactor. * Add callback * Update * Move vqe * Update. * Add test VQE. * Update. * delete * Update VQE test. * Add shots. * Update test * Update delete * Unused import. * More iteration. * m * Change tol. * More VQE tests. * Black * Update. * Update. * Update * Update pennylane_qiskit/vqe/__init__.py Co-authored-by: antalszava * Update from review. * Update tests. * Typo. * Update from tests. * Runtime updated. * Runtime updated. * Update vqe * Update * Update from Antal review. * Update * Black. * More test. * Update parameters * Codefactor update. * Update. * Update. * Change order jac. * Typo. * Params. * Update tests. * ValueError. * print * Update. * Tol. * Update review. * Add doc. * Unused variable. * Update tests. * Update params. * Update review. * Readd mthree * Typo. * Path. * Black * Change path. * Update runtime_programs/vqe_runtime_program.py * Update doc/devices/runtime.rst Co-authored-by: antalszava * Update review. * coverarc * coverarc omit + skip IBMQ and Runtime tests * Changelog Co-authored-by: antalszava --- .coveragerc | 5 + .github/workflows/tests.yml | 4 +- CHANGELOG.md | 5 +- doc/devices/runtime.rst | 57 ++ doc/index.rst | 11 + pennylane_qiskit/__init__.py | 3 + pennylane_qiskit/ibmq.py | 90 ++- pennylane_qiskit/qiskit_device.py | 20 +- pennylane_qiskit/runtime_devices.py | 204 +++++ pennylane_qiskit/vqe_runtime_runner.py | 502 ++++++++++++ requirements.txt | 4 +- runtime_programs/vqe_runtime_program.py | 186 +++++ setup.py | 3 + tests/conftest.py | 32 +- tests/test_ibmq.py | 32 +- tests/test_runtime.py | 967 ++++++++++++++++++++++++ 16 files changed, 2057 insertions(+), 68 deletions(-) create mode 100644 doc/devices/runtime.rst create mode 100644 pennylane_qiskit/runtime_devices.py create mode 100644 pennylane_qiskit/vqe_runtime_runner.py create mode 100644 runtime_programs/vqe_runtime_program.py create mode 100644 tests/test_runtime.py diff --git a/.coveragerc b/.coveragerc index b23a8ec6c..431dd9838 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,11 @@ # .coveragerc to control coverage.py [run] source = pennylane_qiskit +omit = + # omit IBMQ files + pennylane_qiskit/ibmq.py + # omit Runtime files + pennylane_qiskit/*runtime* [report] # Regexes for lines to exclude from consideration diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a978d604..dae4e8d14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,9 @@ jobs: pip install dist/PennyLane*.whl - name: Run tests - run: python -m pytest tests --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native + # Skip IBMQ and Runtime tests as they depend on IBMQ's availability and + # easily result in timeouts + run: python -m pytest tests -k 'not test_ibmq.py and not test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native env: IBMQX_TOKEN_TEST: ${{ secrets.IBMQX_TOKEN_TEST }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8bb0249..b248fc4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ### New features since last release +* Add two devices for runtime programs and one VQE runtime program solver. + [(#157)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/157) + ### Breaking changes ### Improvements @@ -18,7 +21,7 @@ This release contains contributions from (in alphabetical order): -Tanner Rogalsky +Guillermo Alonso-Linaje, Romain Moyard, Tanner Rogalsky, Antal Száva --- diff --git a/doc/devices/runtime.rst b/doc/devices/runtime.rst new file mode 100644 index 000000000..72c4dd5aa --- /dev/null +++ b/doc/devices/runtime.rst @@ -0,0 +1,57 @@ +Qiskit Runtime Programs +======================= + +PennyLane-Qiskit supports running PennyLane on IBM Q hardware via the Qiskit runtime programs ``circuit-runner`` +and ``sampler``. You can choose between those two runtime programs and also have the possibility to choose the +backend on which the circuits will be run. Those two devices inherit directly from the ``IBMQ`` device and work the +the same way, you can refer to the corresponding documentation for details about token and providers +`IBMQ documentation for PennyLane `_. + +You can use the ``circuit_runner`` and ``sampler`` devices by using their short names, for example: + +.. code-block:: python + + dev = qml.device('qiskit.ibmq.circuit_runner', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) + + +.. code-block:: python + + dev = qml.device('qiskit.ibmq.sampler', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) + + +Custom Runtime Programs +~~~~~~~~~~~~~~~~~~~~~~~ + +Not all Qiskit runtime programs correspond to complete devices, some solve specific problems (VQE, QAOA, etc...). +We created a custom Qiskit runtime program for solving VQE problems in PennyLane ``runtime_programs\vqe_runtime_program.py``. +In order to use this program you need to upload on IBMQ (only once), get the program ID and use the VQE runner. + +.. code-block:: python + + from pennylane_qiskit import upload_vqe_runner, vqe_runner + + IBMQ.enable_account(token) + + program_id = upload_vqe_runner(hub="ibm-q", group="open", project="main") + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={"hub": "ibm-q", "group": "open", "project": "main"}, + ) + +More details on Qiskit runtime programs in the `IBMQ runtime documentation `_. diff --git a/doc/index.rst b/doc/index.rst index e93d3516e..489fc0dd9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,6 +31,16 @@ Currently, there are three different devices available: :description: Allows integration with qiskit's hardware backends, and hardware-specific simulators. :link: devices/ibmq.html +.. devicegalleryitem:: + :name: 'qiskit.ibmq.circuit_runner' + :description: Allows integration with qiskit's circuit runner runtime program. + :link: devices/runtime.html + +.. devicegalleryitem:: + :name: 'qiskit.ibmq.sampler' + :description: Allows integration with qiskit's sampler runtime program. + :link: devices/runtime.html + .. raw:: html
@@ -112,6 +122,7 @@ hardware access. devices/aer devices/basicaer devices/ibmq + devices/runtime .. toctree:: :maxdepth: 1 diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index d99adbee7..7276f65a0 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -18,3 +18,6 @@ from .basic_aer import BasicAerDevice from .converter import load, load_qasm, load_qasm_from_file from .ibmq import IBMQDevice +from .runtime_devices import IBMQCircuitRunnerDevice +from .runtime_devices import IBMQSamplerDevice +from .vqe_runtime_runner import vqe_runner, upload_vqe_runner, delete_vqe_runner diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index 7878e3c5b..7790b0854 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -58,52 +58,14 @@ class IBMQDevice(QiskitDevice): short_name = "qiskit.ibmq" def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): - token = kwargs.get("ibmqx_token", None) or os.getenv("IBMQX_TOKEN") - url = kwargs.get("ibmqx_url", None) or os.getenv("IBMQX_URL") - # Specify a single hub, group and project + # Connection to IBMQ + connect(kwargs) + hub = kwargs.get("hub", "ibm-q") group = kwargs.get("group", "open") project = kwargs.get("project", "main") - # TODO: remove "no cover" when #173 is resolved - if token is not None: # pragma: no cover - # token was provided by the user, so attempt to enable an - # IBM Q account manually - def login(): - ibmq_kwargs = {"url": url} if url is not None else {} - IBMQ.enable_account(token, **ibmq_kwargs) - - active_account = IBMQ.active_account() - if active_account is None: - login() - else: - # There is already an active account: - # If the token is the same, do nothing. - # If the token is different, authenticate with the new account. - if active_account["token"] != token: - IBMQ.disable_account() - login() - else: - # check if an IBM Q account is already active. - # - # * IBMQ v2 credentials stored in active_account(). - # If no accounts are active, it returns None. - - if IBMQ.active_account() is None: - # no active account - try: - # attempt to load a v2 account stored on disk - IBMQ.load_account() - except IBMQAccountError: - # attempt to enable an account manually using - # a provided token - raise IBMQAccountError( - "No active IBM Q account, and no IBM Q token provided." - ) from None - - # IBM Q account is now enabled - # get a provider p = provider or IBMQ.get_provider(hub=hub, group=group, project=project) @@ -129,3 +91,49 @@ def _track_run(self): # pragma: no cover } self.tracker.update(job_time=job_time) self.tracker.record() + + +def connect(kwargs): + """Function that allows connection to IBMQ. + + Args: + kwargs(dict): dictionary that contains the token and the url""" + + token = kwargs.get("ibmqx_token", None) or os.getenv("IBMQX_TOKEN") + url = kwargs.get("ibmqx_url", None) or os.getenv("IBMQX_URL") + + # TODO: remove "no cover" when #173 is resolved + if token is not None: # pragma: no cover + # token was provided by the user, so attempt to enable an + # IBM Q account manually + def login(): + ibmq_kwargs = {"url": url} if url is not None else {} + IBMQ.enable_account(token, **ibmq_kwargs) + + active_account = IBMQ.active_account() + if active_account is None: + login() + else: + # There is already an active account: + # If the token is the same, do nothing. + # If the token is different, authenticate with the new account. + if active_account["token"] != token: + IBMQ.disable_account() + login() + else: + # check if an IBM Q account is already active. + # + # * IBMQ v2 credentials stored in active_account(). + # If no accounts are active, it returns None. + + if IBMQ.active_account() is None: + # no active account + try: + # attempt to load a v2 account stored on disk + IBMQ.load_account() + except IBMQAccountError: + # attempt to enable an account manually using + # a provided token + raise IBMQAccountError( + "No active IBM Q account, and no IBM Q token provided." + ) from None diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index 2e071e4de..7a361c507 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -234,7 +234,6 @@ def create_circuit_object(self, operations, **kwargs): self._circuit.save_state() def apply(self, operations, **kwargs): - self.create_circuit_object(operations, **kwargs) # These operations need to run for all devices @@ -377,13 +376,19 @@ def analytic_probability(self, wires=None): prob = self.marginal_prob(np.abs(self._state) ** 2, wires) return prob - def batch_execute(self, circuits): + def compile_circuits(self, circuits): + r"""Compiles multiple circuits one after the other. - compiled_circuits = [] + Args: + circuits (list[.tapes.QuantumTape]): the circuits to be compiled + Returns: + list[QuantumCircuit]: the list of compiled circuits + """ # Compile each circuit object - for circuit in circuits: + compiled_circuits = [] + for circuit in circuits: # We need to reset the device here, else it will # not start the next computation in the zero state self.reset() @@ -393,6 +398,13 @@ def batch_execute(self, circuits): compiled_circ.name = f"circ{len(compiled_circuits)}" compiled_circuits.append(compiled_circ) + return compiled_circuits + + def batch_execute(self, circuits): + # pylint: disable=missing-function-docstring + + compiled_circuits = self.compile_circuits(circuits) + # Send the batch of circuit objects using backend.run self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args) result = self._current_job.result() diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py new file mode 100644 index 000000000..44bf2f93c --- /dev/null +++ b/pennylane_qiskit/runtime_devices.py @@ -0,0 +1,204 @@ +# Copyright 2021-2022 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains classes for constructing Qiskit runtime devices for PennyLane. +""" +# pylint: disable=attribute-defined-outside-init, protected-access, arguments-renamed + +import numpy as np + +import qiskit.result.postprocess +from qiskit.providers.ibmq import RunnerResult +from pennylane_qiskit.ibmq import IBMQDevice + + +class IBMQCircuitRunnerDevice(IBMQDevice): + r"""Class for a Qiskit runtime circuit-runner program device in PennyLane. Circuit runner is a + runtime program that takes one or more circuits, compiles them, executes them, and optionally + applies measurement error mitigation. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['ancilla', 'q1', 'q2']``). + provider (Provider): The Qiskit simulation provider + backend (str): the desired backend + shots (int): Number of circuit evaluations/random samples used to estimate expectation values and variances of + observables. Default=1024. + + Keyword Args: + initial_layout (array[int]): Initial position of virtual qubits on physical qubits. + layout_method (string): Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre') + routing_method (string): Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre'). + translation_method (string): Name of translation pass ('unroller', 'translator', 'synthesis'). + seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. + optimization_level (int): How much optimization to perform on the circuits (0-3). Higher levels generate more + optimized circuits. Default is 1. + init_qubits (bool): Whether to reset the qubits to the ground state for each shot. + rep_delay (float): Delay between programs in seconds. + transpiler_options (dict): Additional compilation options. + measurement_error_mitigation (bool): Whether to apply measurement error mitigation. Default is False. + """ + + short_name = "qiskit.ibmq.circuit_runner" + + def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): + self.kwargs = kwargs + super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) + + def batch_execute(self, circuits): + + compiled_circuits = self.compile_circuits(circuits) + + program_inputs = {"circuits": compiled_circuits, "shots": self.shots} + + for kwarg in self.kwargs: + program_inputs[kwarg] = self.kwargs.get(kwarg) + + # Specify the backend. + options = {"backend_name": self.backend.name()} + + # Send circuits to the cloud for execution by the circuit-runner program. + job = self.provider.runtime.run( + program_id="circuit-runner", options=options, inputs=program_inputs + ) + self._current_job = job.result(decoder=RunnerResult) + + results = [] + + for index, circuit in enumerate(circuits): + self._samples = self.generate_samples(index) + res = self.statistics(circuit.observables) + results.append(res) + + if self.tracker.active: + job_time = { + "total_time": self._current_job._metadata.get("time_taken"), + } + self.tracker.update(batches=1, batch_len=len(circuits), job_time=job_time) + self.tracker.record() + + return results + + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. + + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. + + Args: + circuit (int): position of the circuit in the batch. + + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + counts = self._current_job.get_counts() + + # Batch of circuits + if not isinstance(counts, dict): + counts = self._current_job.get_counts()[circuit] + + samples = [] + for key, value in counts.items(): + for _ in range(0, value): + samples.append(key) + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) + + +class IBMQSamplerDevice(IBMQDevice): + r"""Class for a Qiskit runtime sampler program device in PennyLane. Sampler is a Qiskit runtime program + that samples distributions generated by given circuits executed on the target backend. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['ancilla', 'q1', 'q2']``). + provider (Provider): the Qiskit simulation provider + backend (str): the desired backend + shots (int or None): Number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. Default=1024. + + Keyword Args: + return_mitigation_overhead (bool): Return mitigation overhead factor. Default is False. + run_config (dict): A collection of kwargs passed to backend.run, if shots are given here it will take + precedence over the shots arg. + skip_transpilation (bool): Skip circuit transpilation. Default is False. + transpile_config (dict): A collection of kwargs passed to transpile. + use_measurement_mitigation (bool): Use measurement mitigation to improve results. Default is False. + use_dynamical_decoupling (bool): Use dynamical decoupling to improve fidelities. + """ + + short_name = "qiskit.ibmq.sampler" + + def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): + self.kwargs = kwargs + super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) + + def batch_execute(self, circuits): + + compiled_circuits = self.compile_circuits(circuits) + + program_inputs = {"circuits": compiled_circuits} + + if "run_config" in self.kwargs: + if not "shots" in self.kwargs["run_config"]: + self.kwargs["run_config"]["shots"] = self.shots + else: + self.kwargs["run_config"] = {"shots": self.shots} + + for kwarg in self.kwargs: + program_inputs[kwarg] = self.kwargs.get(kwarg) + + # Specify the backend. + options = {"backend_name": self.backend.name()} + # Send circuits to the cloud for execution by the sampler program. + job = self.provider.runtime.run( + program_id="sampler", options=options, inputs=program_inputs + ) + self._current_job = job.result() + results = [] + + for index, circuit in enumerate(circuits): + self._samples = self.generate_samples(index) + res = self.statistics(circuit.observables) + results.append(res) + + if self.tracker.active: + self.tracker.update(batches=1, batch_len=len(circuits)) + self.tracker.record() + + return results + + def generate_samples(self, circuit_id=None): + r"""Returns the computational basis samples generated for all wires. + + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. + + Args: + circuit_id (int): position of the circuit in the batch. + + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + counts = self._current_job.get("counts")[circuit_id] + counts_formatted = qiskit.result.postprocess.format_counts( + counts, {"memory_slots": self._circuit.num_qubits} + ) + + samples = [] + for key, value in counts_formatted.items(): + for _ in range(0, value): + samples.append(key) + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) diff --git a/pennylane_qiskit/vqe_runtime_runner.py b/pennylane_qiskit/vqe_runtime_runner.py new file mode 100644 index 000000000..01487af9f --- /dev/null +++ b/pennylane_qiskit/vqe_runtime_runner.py @@ -0,0 +1,502 @@ +# Copyright 2021-2022 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains a function to run aa custom PennyLane VQE problem on qiskit runtime. +""" +# pylint: disable=too-few-public-methods,protected-access,too-many-arguments,too-many-branches,too-many-statements + +import os +import warnings +import inspect +from collections import OrderedDict + +import pennylane.numpy as np +import pennylane as qml + +from pennylane_qiskit.qiskit_device import QiskitDevice +from pennylane_qiskit.ibmq import connect +import qiskit.circuit.library.n_local as lib_local +from qiskit.providers.ibmq.runtime import ResultDecoder +from qiskit.circuit import ParameterVector, QuantumCircuit, QuantumRegister +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit import IBMQ + +from scipy.optimize import OptimizeResult + + +class VQEResultDecoder(ResultDecoder): + """The class is used to decode the result from the runtime problem and return it as a + Scipy Optimizer result. + """ + + @classmethod + def decode(cls, data): + """Decode the data from the VQE program.""" + data = super().decode(data) + return OptimizeResult(data) + + +class RuntimeJobWrapper: + """A simple Job wrapper that attaches intermediate results directly to the job object itself + in the `intermediate_results attribute` via the `_callback` function. + """ + + def __init__(self): + self._job = None + self._decoder = VQEResultDecoder + self.intermediate_results = { + "nfev": [], + "parameters": [], + "function": [], + "step": [], + "accepted": [], + } + + def _callback(self, *args): + """The callback function that attaches intermediate results to the wrapper: + + Args: + nfev (int): Number of evaluation. + xk (array_like): A list or NumPy array to attach. + fk (float): Value of the function. + step (float): Value of the step. + accepted (bool): True if the loss function value has improved, False otherwise. + """ + _, (nfev, xk, fk, step, accepted) = args + self.intermediate_results["nfev"].append(nfev) + self.intermediate_results["parameters"].append(xk) + self.intermediate_results["function"].append(fk) + self.intermediate_results["step"].append(step) + self.intermediate_results["accepted"].append(accepted) + + def _scipy_callback(self, *args): + """The callback function that attaches intermediate results to the wrapper: + + Args: + xk (array_like): A list or NumPy array to attach. + """ + _, xk = args + self.intermediate_results["parameters"].append(xk) + + def result(self): + """Get the result of the job as a SciPy OptimizerResult object. + + This method blocks until the job is done, cancelled, or raises an error. + + Returns: + OptimizerResult: An optimizer result object. + """ + return self._job.result(decoder=self._decoder) + + +def upload_vqe_runner(hub="ibm-q", group="open", project="main", **kwargs): + """Upload the custom VQE runner to the IBMQ cloud. + + Args: + hub (str): Ibmq provider hub. + group (str): Ibmq provider group. + project (str): Ibmq provider project. + + Returns: + str: Program id that can be used to run the program. + """ + + connect(kwargs) + + # Specify a single hub, group and project + hub = kwargs.get("hub", "ibm-q") + group = kwargs.get("group", "open") + project = kwargs.get("project", "main") + + meta = { + "name": "vqe-runtime", + "description": "A sample VQE program.", + "max_execution_time": 100000, + "spec": {}, + } + + meta["spec"]["parameters"] = { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "hamiltonian": { + "description": "Hamiltonian whose ground state we want to find.", + "type": "array", + }, + "x0": { + "description": "Initial vector of parameters for the quantum circuit.", + "type": "array", + }, + "ansatz": { + "description": "Qiskit circuit or name of ansatz quantum circuit to use, default='EfficientSU2'", + "type": "[QuantumCircuit,string]", + "default": "EfficientSU2", + }, + "ansatz_config": { + "description": "Configuration parameters for the ansatz circuit.", + "type": "dict", + }, + "optimizer": { + "description": "Classical optimizer to use, default='SPSA'.", + "type": "string", + "default": "SPSA", + }, + "optimizer_config": { + "description": "Configuration parameters for the optimizer.", + "type": "dict", + }, + "shots": { + "description": "The number of shots used for each circuit evaluation.", + "type": "integer", + }, + "use_measurement_mitigation": { + "description": "Use measurement mitigation, default=False.", + "type": "boolean", + "default": False, + }, + }, + "required": ["hamiltonian", "x0"], + } + + meta["spec"]["return_values"] = { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "Final result in SciPy optimizer format", + "type": "object", + } + + meta["spec"]["intermadiate_results"] = { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "Dictionnary containing: " + "The number of evaluation at current optimization step." + "Parameter vector at current optimization step." + "Function value at the current optimization step." + "The size of the step.", + "type": "dict", + } + + provider = IBMQ.get_provider(hub=hub, group=group, project=project) + ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + PROG_DIR = "runtime_programs" + PROG_FILE = "vqe_runtime_program.py" + prog_path = os.path.join(PROG_DIR, PROG_FILE) + program_path = os.path.join(ROOT_DIR, prog_path) + + program_id = provider.runtime.upload_program(data=program_path, metadata=meta) + return program_id + + +def delete_vqe_runner(provider, program_id): + """Delete the desired program on the IBMQ platform. + Args: + provider (object): IBMQ provider. + program_id (str): Id of the Qiskit runtime to be deleted. + """ + provider.runtime.delete_program(program_id) + + +def vqe_runner( + backend, + hamiltonian, + x0, + program_id, + ansatz="EfficientSU2", + ansatz_config=None, + optimizer="SPSA", + optimizer_config=None, + shots=8192, + use_measurement_mitigation=False, + **kwargs, +): + """Routine that executes a given VQE problem via the sample-vqe program on the target backend. + + Args: + backend (ProgramBackend): Qiskit backend instance. + hamiltonian (qml.Hamiltonian): Hamiltonian whose ground state we want to find. + x0 (array_like): Initial vector of parameters. + program_id(str): Id of the program, it has to be generated by using the upload_vqe_runner function. + Once the program is uploaded, you can find the id in your program list online. + ansatz (Quantum function or str): Optional, a PennyLane quantum function or the name of the Qiskit + ansatz quantum circuit to use. Default='EfficientSU2' + ansatz_config (dict): Optional, configuration parameters for the ansatz circuit if from Qiskit library. + optimizer (str): Optional, string specifying classical optimizer. Default='SPSA'. + optimizer_config (dict): Optional, configuration parameters for the optimizer. + shots (int): Optional, number of shots to take per circuit. Default=1024. + use_measurement_mitigation (bool): Optional, use measurement mitigation. Default=False. + + Returns: + OptimizeResult: The result in SciPy optimization format. + """ + # Init the dictionnaries + if ansatz_config is None: + ansatz_config = {} + + if optimizer_config is None: + optimizer_config = {"maxiter": 100} + + if not isinstance(hamiltonian, qml.Hamiltonian): + raise qml.QuantumFunctionError("A PennyLane Hamiltonian object is required.") + + connect(kwargs) + + options = {"backend_name": backend} + + inputs = {} + + # Validate circuit ansatz and number of qubits + if not isinstance(ansatz, str): + inputs["x0"], inputs["ansatz"], num_qubits, wires = _pennylane_to_qiskit_ansatz( + ansatz, x0, hamiltonian + ) + + # The circuit will be taken from the Qiskit library as a str was passed. + else: + wires = hamiltonian.wires + num_qubits = len(wires) + + ansatz_circ = getattr(lib_local, ansatz, None) + if ansatz_circ is None: + raise ValueError(f"Ansatz {ansatz} not in n_local circuit library.") + + inputs["ansatz"] = ansatz + inputs["ansatz_config"] = ansatz_config + + # If given x0, validate its length against num_params in ansatz + x0 = np.asarray(x0) + ansatz_circ = ansatz_circ(num_qubits, **ansatz_config) + num_params = ansatz_circ.num_parameters + + if x0.shape[0] != num_params: + warnings.warn( + "The shape of parameters array is not correct, a random initialization has been applied." + ) + x0 = 2 * np.pi * np.random.rand(num_params) + + inputs["x0"] = x0 + + # Transform the PennyLane hamilonian to a suitable form + hamiltonian = hamiltonian_to_list_string(hamiltonian, wires) + + inputs["hamiltonian"] = hamiltonian + + # Set the rest of the inputs + inputs["optimizer"] = optimizer + inputs["optimizer_config"] = optimizer_config + inputs["shots"] = shots + inputs["use_measurement_mitigation"] = use_measurement_mitigation + + # Specify a single hub, group and project + hub = kwargs.get("hub", "ibm-q") + group = kwargs.get("group", "open") + project = kwargs.get("project", "main") + + provider = IBMQ.get_provider(hub=hub, group=group, project=project) + + rt_job = RuntimeJobWrapper() + + # Callbacks functions are different between optimizers. + if optimizer in ["SPSA", "QNSPSA"]: + job = provider.runtime.run( + program_id, options=options, inputs=inputs, callback=rt_job._callback + ) + else: + job = provider.runtime.run( + program_id, options=options, inputs=inputs, callback=rt_job._scipy_callback + ) + rt_job._job = job + + return rt_job + + +def _pennylane_to_qiskit_ansatz(ansatz, x0, hamiltonian): + r"""Convert an ansatz from PennyLane to a circuit in Qiskit. + + Args: + ansatz (Quantum Function): A PennyLane quantum function that represents the circuit. + x0 (array_like): The array of parameters. + num_qubits_h (int): Number of qubits evaluated from the Hamiltonian. + + Returns: + list[tuple[float,str]]: Hamiltonian in a format for the runtime program. + """ + + if isinstance(ansatz, (qml.QNode, qml.tape.QuantumTape)): + raise qml.QuantumFunctionError("The ansatz must be a callable quantum function.") + + if callable(ansatz): + if len(inspect.getfullargspec(ansatz).args) != 1: + raise qml.QuantumFunctionError("Param should be a single vector.") + try: + tape_param = x0[0] if len(x0) == 1 else x0 + tape = qml.transforms.make_tape(ansatz)(np.array(tape_param)).expand( + depth=5, stop_at=lambda obj: obj.name in QiskitDevice._operation_map + ) + except IndexError as e: + raise qml.QuantumFunctionError("Not enough parameters in X0.") from e + + # Raise exception if there are no operations + if len(tape.operations) == 0: + raise qml.QuantumFunctionError("Function contains no quantum operations.") + + params = tape.get_parameters() + trainable_params = [] + + for p in params: + if qml.math.requires_grad(p): + trainable_params.append(p) + + num_params = len(trainable_params) + + if len(x0) != num_params: + warnings.warn("Due to the tape expansion, the number of parameters has increased.") + x0 = 2 * np.pi * np.random.rand(num_params) + + wires_circuit = tape.wires + wires_hamiltonian = hamiltonian.wires + all_wires = wires_circuit + wires_hamiltonian + + # Set the number of qubits + num_qubits = len(all_wires) + + circuit_ansatz = _qiskit_ansatz(num_params, num_qubits, all_wires, tape) + + else: + raise ValueError("Input ansatz is not a quantum function or a string.") + + return x0, circuit_ansatz, num_qubits, all_wires + + +def _qiskit_ansatz(num_params, num_qubits, wires, tape): + """Transform a quantum tape from PennyLane to a Qiskit circuit. + + Args: + num_params (int): Number of parameters. + num_qubits (int): Number of qubits. + wires (qml.wire.Wires): Wires used in the tape and Hamiltonian. + tape (qml.tape.QuantumTape): The quantum tape of the circuit ansatz in PennyLane. + + Returns: + QuantumCircuit: Qiskit quantum circuit. + + """ + consecutive_wires = qml.wires.Wires(range(num_qubits)) + wires_map = OrderedDict(zip(wires, consecutive_wires)) + # From here: Create the Qiskit ansatz circuit + params_vector = ParameterVector("p", num_params) + + reg = QuantumRegister(num_qubits, "q") + circuit_ansatz = QuantumCircuit(reg, name="vqe") + + circuits = [] + + j = 0 + for operation in tape.operations: + wires = operation.wires.map(wires_map) + par = operation.parameters + operation = operation.name + mapped_operation = QiskitDevice._operation_map[operation] + + qregs = [reg[i] for i in wires.labels] + + if operation.split(".inv")[0] in ("QubitUnitary", "QubitStateVector"): + # Need to revert the order of the quantum registers used in + # Qiskit such that it matches the PennyLane ordering + qregs = list(reversed(qregs)) + + dag = circuit_to_dag(QuantumCircuit(reg, name="")) + + if operation in ("QubitUnitary", "QubitStateVector"): + # Parameters are matrices + gate = mapped_operation(par[0]) + else: + # Parameters for the operation + if par and qml.math.requires_grad(par[0]): + op_num_params = len(par) + par = [] + for num in range(op_num_params): + par.append(params_vector[j + num]) + j += op_num_params + + gate = mapped_operation(*par) + + if operation.endswith(".inv"): + gate = gate.inverse() + + dag.apply_operation_back(gate, qargs=qregs) + circuit = dag_to_circuit(dag) + circuits.append(circuit) + + for circuit in circuits: + circuit_ansatz &= circuit + + return circuit_ansatz + + +def hamiltonian_to_list_string(hamiltonian, wires): + r"""Convert a Hamiltonian object from PennyLane to a list of pairs representing each coefficient and + term in the Hamiltonian. + + Args: + hamiltonian (qml.Hamiltonian): A Hamiltonian from PennyLane. + wires (qml.wires.Wires): A list of qubits from PennyLane. + + Returns: + list[tuple[float,str]]: Hamiltonian in a format for the runtime program. + """ + + num_qubits = len(wires) + + consecutive_wires = qml.wires.Wires(range(num_qubits)) + wires_map = OrderedDict(zip(wires, consecutive_wires)) + + coeff, observables = hamiltonian.terms + + authorized_obs = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Identity"} + + for obs in observables: + obs_names = obs.name if isinstance(obs.name, list) else [obs.name] + if any(ob not in authorized_obs for ob in obs_names): + raise qml.QuantumFunctionError("Observable is not accepted.") + + # Create string Hamiltonian + obs_str = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Hadamard": "H", "Identity": "I"} + + obs_org = [] + # Map the PennyLane hamiltonian to a list PauliY(1) @ PauliY(0) -> [[[0,'Y'], [1,'Y']]] + for obs in observables: + # Tensors + if isinstance(obs.name, list): + internal = [] + for i, j in zip(obs.wires.map(wires_map).tolist(), obs.name): + internal.append([i, obs_str[j]]) + internal.sort() + obs_org.append(internal) + else: + obs_org.append([[obs.wires.map(wires_map).tolist()[0], obs_str[obs.name]]]) + + # Create the hamiltonian terms as lists of strings [[[0,'Y'], [1,'Y']]] -> [['YI'], ['IY']] + obs_list = [] + for elem in obs_org: + empty_obs = ["I"] * num_qubits + for el in elem: + wire = el[0] + observable = el[1] + empty_obs[wire] = observable + obs_list.append(empty_obs) + + # Create the list of tuples with coeffs and Hamiltonian terms as strings [['YI'], ['IY']] -> [(1, 'YI'), (1, 'IY')] + hamiltonian = [] + for i, elem in enumerate(obs_list): + result = "".join(elem) + hamiltonian.append((coeff[i], result)) + + return hamiltonian diff --git a/requirements.txt b/requirements.txt index b90f95ea8..09d0c62c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ +qiskit>=0.32 +mthree>=0.17.2 +git+https://github.com/PennyLaneAI/pennylane.git qiskit>=0.25 -git+https://github.com/PennyLaneAI/pennylane numpy sympy networkx>=2.2;python_version>'3.5' diff --git a/runtime_programs/vqe_runtime_program.py b/runtime_programs/vqe_runtime_program.py new file mode 100644 index 000000000..4cd2aae90 --- /dev/null +++ b/runtime_programs/vqe_runtime_program.py @@ -0,0 +1,186 @@ +# Copyright 2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +This module contains a custom VQE runtime program that can be uploaded to IBMQ. +""" +# pylint: disable=too-many-arguments,too-many-branches,too-many-statements +import numpy as np +import scipy.optimize as opt +from scipy.optimize import OptimizeResult +import mthree + +import qiskit.circuit.library.n_local as lib_local +from qiskit.algorithms.optimizers import SPSA, QNSPSA +from qiskit import QuantumCircuit, transpile + + +def opstr_to_meas_circ(op_str): + """Takes a list of operator strings and creates a Qiskit circuit with the correct pre-measurement rotations. + + Args: + op_str (list): List of strings representing the operators needed for measurements. + + Returns: + list: List of circuits for rotations before measurement. + """ + num_qubits = len(op_str[0]) + circs = [] + for op in op_str: + qc = QuantumCircuit(num_qubits) + for idx, item in enumerate(op): + if item == "X": + qc.h(idx) + elif item == "Y": + qc.sdg(idx) + qc.h(idx) + elif item == "H": + qc.ry(-np.pi / 4, idx) + circs.append(qc) + return circs + + +def main( + backend, + user_messenger, + hamiltonian, + x0, + ansatz="EfficientSU2", + ansatz_config=None, + optimizer="SPSA", + optimizer_config=None, + shots=8192, + use_measurement_mitigation=False, +): + """ + The main sample VQE program. + + Args: + backend (qiskit.providers.ibmq.runtime.ProgramBackend): Qiskit backend instance. + user_messenger (qiskit.providers.ibmq.runtime.UserMessenger): Used to communicate with the program user. + hamiltonian (list): Hamiltonian whose ground state we want to find. e.g. [(1, XY),(1, IH)]. + x0 (array_like): Initial vector of parameters. + ansatz (str): Optional, QuantumCircuit or the name of ansatz quantum circuit to use, default='EfficientSU2'. + ansatz_config (dict): Optional, configuration parameters for the ansatz circuit. + optimizer (str): Optional, string specifying classical optimizer, default='SPSA'. + optimizer_config (dict): Optional, configuration parameters for the optimizer. + shots (int): Optional, number of shots to take per circuit. + use_measurement_mitigation (bool): Optional, use measurement mitigation, default=False. + + Returns: + OptimizeResult: The result in SciPy optimization format. + """ + + if ansatz_config is None: + ansatz_config = {} + + if optimizer_config is None: + optimizer_config = {"maxiter": 100} + + coeffs = np.array([item[0] for item in hamiltonian], dtype=complex) + op_strings = [item[1] for item in hamiltonian] + + num_qubits = len(op_strings[0]) + + # Get the Qiskit circuit from the library if a str was given + if isinstance(ansatz, str): + ansatz_instance = getattr(lib_local, ansatz) + ansatz_circuit = ansatz_instance(num_qubits, **ansatz_config) + else: + ansatz_circuit = ansatz + + meas_circs = opstr_to_meas_circ(op_strings) + + meas_strings = [ + string.replace("X", "Z").replace("Y", "Z").replace("H", "Z") for string in op_strings + ] + + # Take the ansatz circuits and add measurements + full_circs = [ansatz_circuit.compose(mcirc).measure_all(inplace=False) for mcirc in meas_circs] + + num_params = ansatz_circuit.num_parameters + + # Check initial state + if x0 is not None: + x0 = np.asarray(x0, dtype=float) + if x0.shape[0] != num_params: + shape = x0.shape[0] + raise ValueError( + f"Number of params in x0 ({shape}) does not match number \ + of ansatz parameters ({num_params})." + ) + else: + x0 = 2 * np.pi * np.random.rand(num_params) + + # Transpile the circuits + trans_dict = {} + if not backend.configuration().simulator: + trans_dict = {"layout_method": "sabre", "routing_method": "sabre"} + trans_circs = transpile(full_circs, backend, optimization_level=3, **trans_dict) + + # Measurement mitigation + if use_measurement_mitigation: + maps = mthree.utils.final_measurement_mapping(trans_circs) + mit = mthree.M3Mitigation(backend) + mit.cals_from_system(maps) + + def callback(*args): + user_messenger.publish(args) + + def vqe_func(params): + # Binds parameters to the transpiled circuits. + bound_circs = [circ.bind_parameters(params) for circ in trans_circs] + + # Submit the job and get the counts + counts = backend.run(bound_circs, shots=shots).result().get_counts() + + if use_measurement_mitigation: + quasi_collection = mit.apply_correction(counts, maps) + expvals = quasi_collection.expval(meas_strings) + else: + expvals = mthree.utils.expval(counts, meas_strings) + + energy = np.sum(coeffs * expvals).real + return energy + + # SPSA and QNSPSA are taken from Qiskit and not SciPy + if optimizer == "SPSA": + spsa = SPSA(**optimizer_config, callback=callback) + x, loss, nfev = spsa.optimize(num_params, vqe_func, initial_point=x0) + res = OptimizeResult( + fun=loss, + x=x, + nit=optimizer_config["maxiter"], + nfev=nfev, + message="Optimization terminated successfully.", + success=True, + ) + elif optimizer == "QNSPSA": + fidelity = QNSPSA.get_fidelity(ansatz_circuit) + spsa = QNSPSA(fidelity, **optimizer_config, callback=callback) + x, loss, nfev = spsa.optimize(num_params, vqe_func, initial_point=x0) + res = OptimizeResult( + fun=loss, + x=x, + nit=optimizer_config["maxiter"], + nfev=nfev, + message="Optimization terminated successfully.", + success=True, + ) + # SciPy optimizers + else: + res = opt.minimize( + vqe_func, x0, method=optimizer, options=optimizer_config, callback=callback + ) + + return res diff --git a/setup.py b/setup.py index f7fed6025..8b7864da6 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ requirements = [ "qiskit>=0.25", + "mthree>=0.17", "pennylane @ git+https://github.com/PennyLaneAI/pennylane.git", "numpy", "networkx>=2.2", @@ -43,6 +44,8 @@ 'qiskit.aer = pennylane_qiskit:AerDevice', 'qiskit.basicaer = pennylane_qiskit:BasicAerDevice', 'qiskit.ibmq = pennylane_qiskit:IBMQDevice', + 'qiskit.ibmq.circuit_runner = pennylane_qiskit:IBMQCircuitRunnerDevice', + 'qiskit.ibmq.sampler = pennylane_qiskit:IBMQSamplerDevice' ], 'pennylane.io': [ 'qiskit = pennylane_qiskit:load', diff --git a/tests/conftest.py b/tests/conftest.py index b573324c6..9e176316c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,28 @@ +# Copyright 2021-2022 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +This module contains tests for PennyLane runtime programs. +""" + +import os import pytest import numpy as np import pennylane as qml +from qiskit import IBMQ from pennylane_qiskit import AerDevice, BasicAerDevice -import contextlib -import io - - np.random.seed(42) U = np.array( @@ -29,6 +44,15 @@ ] hw_backends = ["qasm_simulator", "aer_simulator"] +@pytest.fixture +def token(): + t = os.getenv("IBMQX_TOKEN_TEST", None) + + if t is None: + pytest.skip("Skipping test, no IBMQ token available") + + yield t + IBMQ.disable_account() @pytest.fixture def tol(shots): diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py index a19c9c5a9..d0f8802c0 100644 --- a/tests/test_ibmq.py +++ b/tests/test_ibmq.py @@ -1,5 +1,19 @@ -import os - +# Copyright 2021-2022 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +This module contains tests for PennyLane IBMQ devices. +""" import numpy as np import pennylane as qml import pytest @@ -9,20 +23,6 @@ from pennylane_qiskit import IBMQDevice from pennylane_qiskit import ibmq as ibmq -from pennylane_qiskit import qiskit_device as qiskit_device - - -@pytest.fixture -def token(): - """A fixture loading the IBMQ token from the IBMQX_TOKEN_TEST environment - variable.""" - t = os.getenv("IBMQX_TOKEN_TEST", None) - - if t is None or t == '': - pytest.skip("Skipping test, no IBMQ token available") - - yield t - IBMQ.disable_account() def test_load_from_env(token, monkeypatch): diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 000000000..7cbb9bd37 --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,967 @@ +# Copyright 2021-2022 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +This module contains tests for PennyLane runtime programs. +""" + +import numpy as np +import pennylane as qml +import pytest + +from qiskit import IBMQ + + +from pennylane_qiskit import IBMQCircuitRunnerDevice, IBMQSamplerDevice +from runtime_programs.vqe_runtime_program import opstr_to_meas_circ +from pennylane_qiskit.vqe_runtime_runner import vqe_runner, upload_vqe_runner, delete_vqe_runner, hamiltonian_to_list_string + +class TestCircuitRunner: + """Test class for the circuit runner IBMQ runtime device.""" + def test_load_from_env(self, token, monkeypatch): + """Test loading an IBMQ Circuit Runner Qiskit runtime device from an env variable.""" + monkeypatch.setenv("IBMQX_TOKEN", token) + dev = IBMQCircuitRunnerDevice(wires=1) + assert dev.provider.credentials.is_ibmq() + + def test_short_name(self, token): + """Test that we can call the circuit runner using its shortname.""" + IBMQ.enable_account(token) + dev = qml.device("qiskit.ibmq.circuit_runner", wires=1) + return dev.provider.credentials.is_ibmq() + + @pytest.mark.parametrize("shots", [8000]) + def test_simple_circuit(self, token, tol, shots): + """Test executing a simple circuit submitted to IBMQ circuit runner runtime program.""" + IBMQ.enable_account(token) + dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + theta = 0.432 + phi = 0.123 + + res = circuit(theta, phi) + expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) + assert np.allclose(res, expected, **tol) + + @pytest.mark.parametrize("shots", [8000]) + @pytest.mark.parametrize( + "kwargs", + [ + { + "initial_layout": [0, 1], + "layout_method": "trivial", + "routing_method": "basic", + "translation_method": "unroller", + "seed_transpiler": 42, + "optimization_level": 2, + "init_qubits": True, + "rep_delay": 0.01, + "transpiler_options": {"approximation_degree": 1.0}, + "measurement_error_mmitigation": True, + } + ], + ) + def test_kwargs_circuit(self, token, tol, shots, kwargs): + """Test executing a simple circuit submitted to IBMQ circuit runner runtime program with kwargs.""" + IBMQ.enable_account(token) + dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + theta = 0.432 + phi = 0.123 + + res = circuit(theta, phi) + expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) + assert np.allclose(res, expected, **tol) + + @pytest.mark.parametrize("shots", [8000]) + def test_batch_circuits(self, token, tol, shots): + """Test that we can send batched circuits to the circuit runner runtime program.""" + IBMQ.enable_account(token) + dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + + # Batch the input parameters + batch_dim = 3 + a = np.linspace(0, 0.543, batch_dim) + b = np.linspace(0, 0.123, batch_dim) + c = np.linspace(0, 0.987, batch_dim) + + @qml.batch_params(all_operations=True) + @qml.qnode(dev) + def circuit(x, y, z): + """Reference QNode""" + qml.PauliX(0) + qml.Hadamard(wires=0) + qml.Rot(x, y, z, wires=0) + return qml.expval(qml.PauliZ(0)) + + assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) + + def test_track_circuit_runner(self, token): + """Test that the tracker works.""" + + IBMQ.enable_account(token) + dev = IBMQCircuitRunnerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) + dev.tracker.active = True + + @qml.qnode(dev) + def circuit(): + qml.PauliX(wires=0) + return qml.probs(wires=0) + + circuit() + + assert "job_time" in dev.tracker.history + if "job_time" in dev.tracker.history: + assert "total_time" in dev.tracker.history["job_time"][0] + assert len(dev.tracker.history["job_time"][0]) == 1 + + +class TestSampler: + """Test class for the sampler IBMQ runtime device.""" + def test_load_from_env(self, token, monkeypatch): + """Test loading an IBMQ Sampler Qiskit runtime device from an env variable.""" + monkeypatch.setenv("IBMQX_TOKEN", token) + dev = IBMQSamplerDevice(wires=1) + assert dev.provider.credentials.is_ibmq() + + def test_short_name(self, token): + IBMQ.enable_account(token) + dev = qml.device("qiskit.ibmq.sampler", wires=1) + return dev.provider.credentials.is_ibmq() + + @pytest.mark.parametrize("shots", [8000]) + def test_simple_circuit(self, token, tol, shots): + """Test executing a simple circuit submitted to IBMQ using the Sampler device.""" + IBMQ.enable_account(token) + dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + theta = 0.432 + phi = 0.123 + + res = circuit(theta, phi) + expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) + assert np.allclose(res, expected, **tol) + + @pytest.mark.parametrize("shots", [8000]) + @pytest.mark.parametrize( + "kwargs", + [ + { + "return_mitigation_overhead": True, + "run_config": {"seed_simulator": 42}, + "skip_transpilation": False, + "transpile_config": {"approximation_degree": 1.0}, + "use_measurement_mitigation": True, + } + ], + ) + def test_kwargs_circuit(self, token, tol, shots, kwargs): + """Test executing a simple circuit submitted to IBMQ using the Sampler device with kwargs.""" + IBMQ.enable_account(token) + dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) + + @qml.qnode(dev) + def circuit(theta, phi): + qml.RX(theta, wires=0) + qml.RX(phi, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + theta = 0.432 + phi = 0.123 + + res = circuit(theta, phi) + expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) + assert np.allclose(res, expected, **tol) + + @pytest.mark.parametrize("shots", [8000]) + def test_batch_circuits(self, token, tol, shots): + """Test executing batched circuits submitted to IBMQ using the Sampler device.""" + IBMQ.enable_account(token) + dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + + # Batch the input parameters + batch_dim = 3 + a = np.linspace(0, 0.543, batch_dim) + b = np.linspace(0, 0.123, batch_dim) + c = np.linspace(0, 0.987, batch_dim) + + @qml.batch_params(all_operations=True) + @qml.qnode(dev) + def circuit(x, y, z): + """Reference QNode""" + qml.PauliX(0) + qml.Hadamard(wires=0) + qml.Rot(x, y, z, wires=0) + return qml.expval(qml.PauliZ(0)) + + assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) + + def test_track_sampler(self, token): + """Test that the tracker works.""" + + IBMQ.enable_account(token) + dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) + dev.tracker.active = True + + @qml.qnode(dev) + def circuit(): + qml.PauliX(wires=0) + return qml.probs(wires=0) + + circuit() + + assert len(dev.tracker.history) == 2 + + +class TestCustomVQE: + """Class to test the custom VQE program.""" + + def test_hamiltonian_to_list_string(self): + """Test the function that transforms a PennyLane Hamiltonian to a list string Hamiltonian.""" + coeffs = [1, 1] + obs = [qml.PauliX(0) @ qml.PauliX(2), qml.Hadamard(0) @ qml.PauliZ(1)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + result = hamiltonian_to_list_string(hamiltonian, hamiltonian.wires) + + assert [(1, 'XIX'), (1, 'HZI')] == result + + def test_op_str_measurement_circ(self): + """Test that the opstr_to_meas_circ function finds the necessary rotations before measurements in the + circuit. """ + circ = opstr_to_meas_circ('HIHXZ') + results = [] + for c in circ: + if c: + results.append((c.data[0][0].name, c.data[0][0].params)) + else: + results.append(()) + assert [('ry', [-0.7853981633974483]), (), ('ry', [-0.7853981633974483]), ('h', []), ()] == results + + @pytest.mark.parametrize("shots", [8000]) + def test_simple_hamiltonian(self, token, tol, shots): + """Test a simple VQE problem with Hamiltonian and a circuit from PennyLane""" + IBMQ.enable_account(token) + tol = 1e-1 + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert np.allclose(job.result()["fun"], -1.43, tol) + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_ansatz_qiskit(self, token, tol, shots): + """Test a simple VQE problem with an ansatz from Qiskit library.""" + IBMQ.enable_account(token) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz="EfficientSU2", + x0=[3.97507603, 3.00854038], + shots=shots, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_ansatz_qiskit_invalid(self, token, tol, shots): + """Test a simple VQE problem with an invalid ansatz from Qiskit library.""" + IBMQ.enable_account(token) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(ValueError, match="Ansatz InEfficientSU2 not in n_local circuit library."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz="InEfficientSU2", + x0=[3.97507603, 3.00854038], + shots=shots, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_qnode(self, token, tol, shots): + """Test that we cannot pass a QNode as ansatz circuit.""" + IBMQ.enable_account(token) + + with qml.tape.QuantumTape() as vqe_tape: + qml.RX(3.97507603, wires=0) + qml.RY(3.00854038, wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="The ansatz must be a callable quantum function."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_tape, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_tape(self, token, tol, shots): + """Test that we cannot pass a tape as ansatz circuit.""" + IBMQ.enable_account(token) + + dev = qml.device("default.qubit", wires=1) + + @qml.qnode(dev) + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="The ansatz must be a callable quantum function."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_wrong_input(self, token, tol, shots): + """Test that we can only give a single vector parameter to the ansatz circuit.""" + IBMQ.enable_account(token) + + def vqe_circuit(params, wire): + qml.RX(params[0], wires=wire) + qml.RY(params[1], wires=wire) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="Param should be a single vector."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_wrong_number_input_param(self, token, tol, shots): + """Test that we need a certain number of parameters.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + qml.RY(params[2], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="Not enough parameters in X0."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[0, 0], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_one_param(self, token, tol, shots): + """Test that we can only give a single vector parameter to the ansatz circuit.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params, wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[0.0], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_too_many_param(self, token, tol, shots): + """Test that we handle the case where too many parameters were given.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038, 3.55637849], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_more_qubits_in_circuit_than_hamiltonian(self, token, tol, shots): + """Test that we handle the case where there are more qubits in the circuit than the hamiltonian.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_qubitunitary(self, token, tol, shots): + """Test that we can handle a QubitUnitary operation.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.QubitUnitary(np.array([[1, 0], [0, 1]]), wires=0) + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_inverse(self, token, tol, shots): + """Test that we can handle inverse operations.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0).inv() + qml.RX(params[1], wires=1) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_hamiltonian_format(self, token, tol, shots): + """Test that a PennyLane Hamiltonian is required.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + hamiltonian = qml.PauliZ(wires=0) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="A PennyLane Hamiltonian object is required."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_hamiltonian_tensor(self, token, tol, shots): + """Test that we can handle tensor Hamiltonians.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + coeffs = [0.2, -0.543] + obs = [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(0) @ qml.Hadamard(1)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_not_auth_operation_hamiltonian(self, token, tol, shots): + """Test the observables in the Hamiltonian are I, X, Y, Z or H.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=0) + + H = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.Hermitian(H, wires=0)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="Observable is not accepted."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_not_auth_operation_hamiltonian_tensor(self, token, tol, shots): + """Test the observables in the tensor Hamiltonian are I, X, Y, Z or H.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + H = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) + coeffs = [1, 1] + obs = [qml.PauliX(0) @ qml.Hermitian(H, wires=1), qml.PauliZ(wires=1)] + hamiltonian = qml.Hamiltonian(coeffs, obs) + + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="Observable is not accepted."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + @pytest.mark.parametrize("shots", [8000]) + def test_scipy_optimizer(self, token, tol, shots): + """Test we can run a VQE problem with a SciPy optimizer.""" + IBMQ.enable_account(token) + tol = 1e-1 + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="Powell", + optimizer_config={"maxiter": 10}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + result = job.result()["fun"] + + assert np.allclose(result, -1.43, tol) + assert "parameters" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_simple_hamiltonian_with_untrainable_parameters(self, token, tol, shots): + """Test a simple VQE problem with untrainable parameters.""" + IBMQ.enable_account(token) + tol = 1e-1 + + def vqe_circuit(params): + qml.RZ(0.1, wires=0) + qml.RX(params[0], wires=0) + qml.RY(params[1], wires=0) + qml.RZ(0.2, wires=0) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + job = vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer="SPSA", + optimizer_config={"maxiter": 40}, + kwargs={"hub": "ibm-q-startup", "group": "ibm-q-startup", "project": "reservations"}, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + assert np.allclose(job.result()["fun"], -1.43, tol) + assert isinstance(job.intermediate_results, dict) + assert "nfev" in job.intermediate_results + assert "parameters" in job.intermediate_results + assert "function" in job.intermediate_results + assert "step" in job.intermediate_results + assert "accepted" in job.intermediate_results + + @pytest.mark.parametrize("shots", [8000]) + def test_invalid_function(self, token, tol, shots): + """Test that an invalid function cannot be passed.""" + IBMQ.enable_account(token) + + def vqe_circuit(params): + c = params[0] + params[1] + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(qml.QuantumFunctionError, match="Function contains no quantum operations."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=vqe_circuit, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id) + + + @pytest.mark.parametrize("shots", [8000]) + def test_invalid_ansatz(self, token, tol, shots): + """Test that an invalid ansatz cannot be passed.""" + IBMQ.enable_account(token) + + coeffs = [1, 1] + obs = [qml.PauliX(0), qml.PauliZ(0)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + program_id = upload_vqe_runner(hub="ibm-q-startup", group="xanadu", project="reservations") + + with pytest.raises(ValueError, match="Input ansatz is not a quantum function or a string."): + vqe_runner( + program_id=program_id, + backend="ibmq_qasm_simulator", + hamiltonian=hamiltonian, + ansatz=10, + x0=[3.97507603, 3.00854038], + shots=shots, + optimizer_config={"maxiter": 10}, + kwargs={ + "hub": "ibm-q-startup", + "group": "ibm-q-startup", + "project": "reservations", + }, + ) + + provider = IBMQ.get_provider(hub="ibm-q-startup", group="xanadu", project="reservations") + delete_vqe_runner(provider=provider, program_id=program_id)