From 1ad9c76c156eb9b4403666dc46547a17e5787cbd Mon Sep 17 00:00:00 2001 From: Sankalp Sanand Date: Wed, 27 Sep 2023 19:46:48 -0400 Subject: [PATCH] added qelectron tests --- tests/qelectron_tests/.gitignore | 2 + tests/qelectron_tests/README.md | 35 ++ tests/qelectron_tests/__init__.py | 19 + .../pennylane_tests/conftest.py | 332 ++++++++++++++++++ .../qelectron_tests/pennylane_tests/setup.sh | 28 ++ tests/qelectron_tests/test_braket_plugin.py | 148 ++++++++ tests/qelectron_tests/test_decorator.py | 203 +++++++++++ tests/qelectron_tests/test_qelectron_db.py | 83 +++++ tests/qelectron_tests/test_qiskit_plugin.py | 163 +++++++++ .../test_qiskit_plugin_runtime.py | 208 +++++++++++ tests/qelectron_tests/test_run_later.py | 96 +++++ tests/qelectron_tests/utils.py | 125 +++++++ 12 files changed, 1442 insertions(+) create mode 100644 tests/qelectron_tests/.gitignore create mode 100644 tests/qelectron_tests/README.md create mode 100644 tests/qelectron_tests/__init__.py create mode 100644 tests/qelectron_tests/pennylane_tests/conftest.py create mode 100755 tests/qelectron_tests/pennylane_tests/setup.sh create mode 100644 tests/qelectron_tests/test_braket_plugin.py create mode 100644 tests/qelectron_tests/test_decorator.py create mode 100644 tests/qelectron_tests/test_qelectron_db.py create mode 100644 tests/qelectron_tests/test_qiskit_plugin.py create mode 100644 tests/qelectron_tests/test_qiskit_plugin_runtime.py create mode 100644 tests/qelectron_tests/test_run_later.py create mode 100644 tests/qelectron_tests/utils.py diff --git a/tests/qelectron_tests/.gitignore b/tests/qelectron_tests/.gitignore new file mode 100644 index 000000000..5f834c486 --- /dev/null +++ b/tests/qelectron_tests/.gitignore @@ -0,0 +1,2 @@ +pennylane_tests-* +.pytest_cache/ diff --git a/tests/qelectron_tests/README.md b/tests/qelectron_tests/README.md new file mode 100644 index 000000000..e3f8a7b71 --- /dev/null +++ b/tests/qelectron_tests/README.md @@ -0,0 +1,35 @@ +# QElectron Tests + +This tests package is a work in progress. + +It is designed to run the Pennylane test suite by patching the `QNode` class to +call a `QNodeQE` instance (*i.e.* a QElectron). + +## Cloning Pennylane tests + +One can clone the Pennylane test suite using the scripts provided `scripts/`: + +``` +cd scripts +bash clone_qml_tests.sh v0.30.0 +``` + +This above will create a folder `qelectron_tests/pennylane_test-v0.30.0/` that contains +the Pennylane test suite. + +## Running Pennylane tests on QElectrons + +One must also point to the configuration file (`qelectron_tests/conftest.py`) +to apply the required patches and fixtures. An example is given below: + +``` +cd covalent-os-private/tests +pytest -c qelectron_tests/conftest.py qelectron_tests/pennylane_tests-v0.30.0/test_return_types_qnode.py +``` + +To run the *entire* test suite, do the following: + +``` +cd covalent-os-private/tests +pytest -c qelectron_tests/conftest.py qelectron_tests/pennylane_tests-v0.30.0/ +``` diff --git a/tests/qelectron_tests/__init__.py b/tests/qelectron_tests/__init__.py new file mode 100644 index 000000000..523f77622 --- /dev/null +++ b/tests/qelectron_tests/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. diff --git a/tests/qelectron_tests/pennylane_tests/conftest.py b/tests/qelectron_tests/pennylane_tests/conftest.py new file mode 100644 index 000000000..97ebd7830 --- /dev/null +++ b/tests/qelectron_tests/pennylane_tests/conftest.py @@ -0,0 +1,332 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member +# pylint: disable=missing-function-docstring +# pylint: disable=invalid-name + +""" +Configuration that enables easy adoption of Pennylane tests to QElectrons. + +NOTE: ONLY USE this configuration file with Pennylane tests. +""" + +import inspect +import re +from typing import List +from unittest.mock import patch + +import pennylane as qml +import pytest + +import covalent as ct +from covalent._shared_files.utils import get_original_shots +from covalent.quantum.qcluster.simulator import SIMULATOR_DEVICES + +SKIP_RETURN_TYPES = ["qml.apply", "qml.vn_entropy", "qml.mutual_info"] + +SKIP_DEVICES = [ + "default.qutrit", + "default.mixed", + "default.gaussian", # TODO: allow for Simulator +] + +# XFAIL NOTES LEGEND +# (1) configuration issue; test passes manually +# (2) incompatible; requires manual test +# (3) not yet determined +# ----------------- +XFAIL_TEST_NAMES = [ + # "test_array_multiple" # NOTE: produces array with inhomogeneous shape + # Case 0 # + # fails and support not planned + "test_qutrit_device::test_device_executions", + # Case 1 # + # configuration issue, test passes manually + "test_qaoa::test_partial_cycle_mixer", + "test_qaoa::test_self_loop_raises_error", + "test_qaoa::test_inner_out_flow_constraint_hamiltonian_non_complete", + "test_qaoa::test_inner_net_flow_constraint_hamiltonian_non_complete", + # Case 2 # + # incompatible test, needs manual equivalent + "test_qnode::test_diff_method", + "test_qnode::test_jacobian", + # Case 3 # + "TestHamiltonian::test_hamiltonian_iadd", + "TestHamiltonian::test_hamiltonian_imul", + "TestHamiltonian::test_hamiltonian_isub", +] + +XFAIL_TEST_NAMES_CONDITIONAL = { + # NOTE: mocker.spy(qml.QubitDevice, "probability") working incorrectly for Braket executor. + "test_numerical_analytic_diff_agree": lambda item: ( + item.callspec.params.get("get_executors") is _init_LocalBraketQubitExecutor + or item.callspec.params.get("get_executors") is _init_LocalBraketQubitExecutor_cluster + ), + "test_lightning_qubit.py::test_integration": lambda item: ( + item.callspec.params.get("get_executors") is _init_QiskitExecutor_local_sampler + or item.callspec.params.get("get_executors") is _init_QiskitExecutor_local_sampler_cluster + ), +} + +SKIP_FOR_RUN_LATER = [ + # NOTE: calls qml.jacobian(qe_circuit.run_later(input).result()) + "test_numerical_analytic_diff_agree", + # Similar to previous. + "test_hamiltonian.py::TestHamiltonianDifferentiation::test_trainable_coeffs_paramshift", + "test_hamiltonian.py::TestHamiltonianDifferentiation::test_nontrainable_coeffs_paramshift", + "test_hamiltonian.py::TestHamiltonianDifferentiation::test_trainable_coeffs_autograd", + "test_hamiltonian.py::TestHamiltonianDifferentiation::test_nontrainable_coeffs_autograd", + "test_hamiltonian.py::TestHamiltonianDifferentiation::test_trainable_coeffs_jax", + "test_lightning_qubit.py::test_integration", + "test_iqp_emb.py::TestInterfaces::test_jax", +] + + +# VALIDATION FUNCTIONS +# ------------------------------------------------------------------------------ + + +def _check_return_type(executors, func): + """ + Checks whether a function returns a type that is not supported by QElectrons. + """ + + func_lines = inspect.getsourcelines(func)[0] + reached_return = False + for line in func_lines: + if line.strip().startswith("return"): + reached_return = True + + if reached_return: + for ret_typ in SKIP_RETURN_TYPES: + if ret_typ in line or ret_typ.split(".", maxsplit=1)[-1] in line: + pytest.skip(f"QElectrons don't support `{ret_typ}` measurements.") + + +def _check_device_type(executors, device): + """ + Checks whether a device is supported by QElectrons. + """ + + if not isinstance(executors, list): + # Always handle as list. + executors = [executors] + + if device.short_name in SKIP_DEVICES: + simulator_in_execs = any(isinstance(ex, ct.executor.Simulator) for ex in executors) + if not (simulator_in_execs and device.short_name == "default.gaussian"): + pytest.skip(f"QElectrons do not support the '{device.short_name}' device.") + + # Simulator + if any(hasattr(ex.shots, "__len__") and ex.name != "Simulator" for ex in executors): + pytest.skip("Only the Simulator QExecutor currently supports shot vectors.") + + if ( + any(isinstance(ex, ct.executor.Simulator) for ex in executors) + and device.short_name not in SIMULATOR_DEVICES + ): + pytest.skip(f"Simulator does not support the '{device.short_name}' device.") + + +def _check_qnode(qnode): + """ + Checks whether QNode settings are supported by QElectrons. + """ + # General + if qnode.diff_method in {"backprop", "adjoint"}: + pytest.skip(f"QElectron devices don't support the '{qnode.diff_method}' diff method.") + + +# UTILITIES +# ------------------------------------------------------------------------------ + + +def _init_Simulator(shots): + return ct.executor.Simulator(parallel="thread", shots=shots) + + +def _init_Simulator_cluster(shots): + return [ + ct.executor.Simulator(parallel="thread", shots=shots), + ct.executor.Simulator(parallel="thread", shots=shots), + ] + + +def _init_QiskitExecutor_local_sampler(shots): + return ct.executor.QiskitExecutor( + device="local_sampler", + shots=shots, + ) + + +def _init_QiskitExecutor_local_sampler_cluster(shots): + return [ + ct.executor.QiskitExecutor( + device="local_sampler", + shots=shots, + ), + ct.executor.QiskitExecutor( + device="local_sampler", + shots=shots, + ), + ] + + +def _init_LocalBraketQubitExecutor(shots): + return ct.executor.LocalBraketQubitExecutor(shots=shots) + + +def _init_LocalBraketQubitExecutor_cluster(shots): + return [ + ct.executor.LocalBraketQubitExecutor(shots=shots), + ct.executor.LocalBraketQubitExecutor(shots=shots), + ] + + +# QNODE PATCH THAT SUBSTITUTES IN QELECTRON +# ------------------------------------------------------------------------------ + + +def _get_wrapped_QNode(use_run_later, get_executors): # pylint: disable=invalid-name + """ + Patches `qml.QNode` to return a QElectron instead. + """ + + class _PatchedQNode(qml.QNode): + # pylint: disable=too-few-public-methods + + """ + This class replaces `qml.QNode` + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + shots = get_original_shots(self.device) + executors = get_executors(shots) + qnode = self + shots = get_original_shots(qnode.device) + executors = get_executors(shots=shots) + + _check_qnode(self) + _check_return_type(executors, qnode.func) + _check_device_type(executors, qnode.device) + + # QElectron that wraps the normal QNode + self.qelectron = ct.qelectron( + qnode=qnode, executors=get_executors(shots=get_original_shots(self.device)) + ) + + def __call__(self, *args, **kwargs): + if use_run_later: + return self.qelectron.run_later(*args, **kwargs).result() + return self.qelectron(*args, **kwargs) + + return _PatchedQNode + + +# TEST UTILITIES +# ------------------------------------------------------------------------------ + + +def _get_test_name(item: str): + """ + Returns the name of a test. + """ + return ( + re.findall(r"(test_[\w|\d]+.py::test_.*)\[", item.nodeid) + or re.findall(r"(test_[\w|\d]+.py::Test_.*)\[", item.nodeid) + ).pop() + + +# HOOKS +# ------------------------------------------------------------------------------ + + +def pytest_collection_modifyitems( + config: pytest.Config, items: List[pytest.Item] +): # pylint: disable=unused-argument + """ + Using Pytest hook to xfail selected tests. + """ + for item in items: + # XFail tests expected to fail in general. + if any(name in item.nodeid for name in XFAIL_TEST_NAMES): + item.add_marker(pytest.mark.xfail(reason="XFailing test also failed by normal QNode.")) + + # XFail tests expected to fail with `QElectron.run_later` + if ( + "use_run_later" in item.fixturenames + and item.callspec.params.get("use_run_later") + and any(name in item.nodeid for name in SKIP_FOR_RUN_LATER) + ): + item.add_marker( + pytest.mark.skip( + reason=f"{item.nodeid} expected to fail with `QElectron.run_later`" + ) + ) + + # XFail tests expected to fail in certain conditions. + if any(name in item.nodeid for name in XFAIL_TEST_NAMES_CONDITIONAL): + condition = XFAIL_TEST_NAMES_CONDITIONAL[_get_test_name(item)] + if condition(item): + item.add_marker(pytest.mark.xfail(reason="XFailing conditional case.")) + + +# FIXTURES +# ------------------------------------------------------------------------------ + + +@pytest.fixture(params=[True, False]) +def use_run_later(request): + """ + Determines whether QElectron is called normally or through `run_later`. + """ + return request.param + + +QEXECUTORS = [ + _init_Simulator, + _init_Simulator_cluster, + _init_QiskitExecutor_local_sampler, + _init_QiskitExecutor_local_sampler_cluster, + _init_LocalBraketQubitExecutor, + _init_LocalBraketQubitExecutor_cluster, +] + + +@pytest.fixture(params=QEXECUTORS) +def get_executors(request): + """ + Determines the QExecutor that is used. + """ + return request.param + + +@pytest.fixture(autouse=True) +def patch_qnode_creation(use_run_later, get_executors): + """ + Wraps the `pennylane.QNode` class such that the `qml.qnode()` decorator + instead creates QElectrons that wrap a QNode. + """ + patched_cls = _get_wrapped_QNode(use_run_later, get_executors) + with patch("pennylane.QNode", new=patched_cls): + yield diff --git a/tests/qelectron_tests/pennylane_tests/setup.sh b/tests/qelectron_tests/pennylane_tests/setup.sh new file mode 100755 index 000000000..b568eea9a --- /dev/null +++ b/tests/qelectron_tests/pennylane_tests/setup.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# This script clones the test suite from a specific Pennylane branch/tag + +PENNYLANE_VERSION_TAG=$1 # for example, "v0.30.0" +if [[ -z $PENNYLANE_VERSION_TAG ]]; then + echo "missing Pennylane version tag" + exit 1 +fi + +DIR=$(pwd) +CREATE_DIR="${DIR}/pennylane_tests-${PENNYLANE_VERSION_TAG}" + + +# Clone repo to temp. +git clone --quiet "https://github.com/PennyLaneAI/pennylane" /tmp/pennylane + +# Grab specific version. +cd /tmp/pennylane +git checkout --quiet $PENNYLANE_VERSION_TAG + +# Copy back to this `pwd`. +echo "creating: ${CREATE_DIR}" +cp -r ./tests/ "${CREATE_DIR}" +cd $DIR + +# Clean up. +rm -rf /tmp/pennylane diff --git a/tests/qelectron_tests/test_braket_plugin.py b/tests/qelectron_tests/test_braket_plugin.py new file mode 100644 index 000000000..d2675556a --- /dev/null +++ b/tests/qelectron_tests/test_braket_plugin.py @@ -0,0 +1,148 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +import pytest + + +def test_init_local_executor(): + """Test that the local Braket executor can be initialized.""" + + import covalent as ct + + ct.executor.LocalBraketQubitExecutor() + + +def test_init_executor(): + """Test that the Braket executor can be initialized.""" + + import covalent as ct + + ct.executor.BraketQubitExecutor() + + +def test_decorator_path(): + """Test that `ct.qelectron` is the QElectron decorator""" + from typing import Callable + + import covalent as ct + + assert isinstance(ct.qelectron, Callable), f"`ct.qelectron` is a {type(ct.qelectron).__name__}" + + +def test_circuit_call_single(): + """Test calling a QNode vs. QElectron with a scalar argument.""" + + import pennylane as qml + from pennylane import numpy as np + + import covalent as ct + + executors = [ + ct.executor.LocalBraketQubitExecutor(shots=None, max_jobs=19), + ct.executor.LocalBraketQubitExecutor(shots=10_000, max_jobs=1), + ] + + @qml.qnode(qml.device("default.qubit", wires=2)) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + qe_circuit = ct.qelectron(circuit, executors=executors, selector="cyclic") + + x = np.array(0.5) + + # Ensure every QCluster member is used at least once. + for _ in range(5): + res_1 = circuit(x) + res_2 = qe_circuit(x) + + assert isinstance(res_1, type(res_2)) + assert np.isclose(res_1, res_2, rtol=0.1) + + +def test_circuit_call_vector(): + """Test calling a QNode vs. QElectron with a vector argument.""" + + import pennylane as qml + from pennylane import numpy as np + + import covalent as ct + + executors = [ + ct.executor.LocalBraketQubitExecutor(shots=None, max_jobs=19), + ct.executor.LocalBraketQubitExecutor(shots=10_000, max_jobs=1), + ] + + @qml.qnode(qml.device("default.qubit", wires=2)) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + qe_circuit = ct.qelectron(circuit, executors=executors, selector="cyclic") + + X = np.random.rand(13) * 2 * np.pi + + # Ensure every QCluster member is used at least once. + for _ in range(5): + res_1 = circuit(X) + res_2 = qe_circuit(X) + + assert isinstance(res_1, type(res_2)) + assert np.isclose(res_1, res_2, atol=0.2).all() + + +def test_grad_basic(): + """Test calling gradients QNode vs. QElectron.""" + + import pennylane as qml + from pennylane import numpy as np + + import covalent as ct + + executors = [ + ct.executor.LocalBraketQubitExecutor(shots=10_000, max_jobs=1), + ct.executor.LocalBraketQubitExecutor(shots=None, max_jobs=19), + ] + + @qml.qnode(qml.device("default.qubit", wires=2)) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + qe_circuit = ct.qelectron(circuit, executors=executors, selector="cyclic") + + x = np.array(0.5, requires_grad=True) + + # Ensure every QCluster member is used at least once. + for _ in range(5): + res_1 = qml.grad(circuit)(x) + res_2 = qml.grad(qe_circuit)(x) + + with pytest.raises(AssertionError): + # NOTE: expected to fail due to QElectron fast-gradients trickery. + # NOTE: return types are QML tensor vs NumPy array, respectively. + assert isinstance(res_1, type(res_2)) + + assert np.isclose(res_1, res_2, rtol=0.1) diff --git a/tests/qelectron_tests/test_decorator.py b/tests/qelectron_tests/test_decorator.py new file mode 100644 index 000000000..ff29c6969 --- /dev/null +++ b/tests/qelectron_tests/test_decorator.py @@ -0,0 +1,203 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +import copy + +import pennylane as qml +import pytest +from numpy import isclose + +import covalent as ct + +EXECUTORS = [ + ct.executor.QiskitExecutor(device="local_sampler", shots=10_000), +] + + +@pytest.mark.parametrize("executor", EXECUTORS) +def test_decorator_vs_explicit_wrapper(executor): + """ + Test that `ct.qelectron` works as decorator and as explicit wrapper. + """ + + results = [] + + # Initialize qelectron by decorating a qnode. + dev = qml.device("default.qubit", wires=2) + + @ct.qelectron(executors=executor) + @qml.qnode(device=dev) + def simple_circuit_1(param): + """ + A tiny, reusable Pennylane circuit. + """ + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + qelectron = simple_circuit_1 + results.append(qelectron(0.5)) + + # Initialize qelectron by passing a qnode. + @qml.qnode(device=dev) + def simple_circuit_2(param): + """ + A tiny, reusable Pennylane circuit. + """ + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + qelectron = ct.qelectron(simple_circuit_2, executors=executor) + results.append(qelectron(0.5)) + + assert isinstance(results[0], type(results[1])), f"Results {results!r} are not the same type" + assert isclose(results[0], results[1], 0.1), f"Results {results!r} are not close" + + +class TestDecoratorArguments: + """ + Test that the `ct.qelectron` decorator accepts and correctly processes various + types of `executors` arguments. + + Specifically, the following types should be supported and the corresponding + behavior observed: + + (1) `executors=executor_1` + -> a single executor is used for all circuits + + (2) `executors=[executor_1, ..., executor_N]` + -> a `QCluster` is created from the two or more executor instances + + (3) `executors=qcluster_1` + -> the given `QCluster` is used for all circuits + """ + + @pytest.mark.parametrize("executor", EXECUTORS) + def test_single_executor(self, executor): + """ + Test that the `ct.qelectron` decorator accepts a single executor (case 1). + """ + + dev = qml.device("default.qubit", wires=2) + + # QElectron definition. + @ct.qelectron(executors=executor) + @qml.qnode(device=dev) + def qelectron_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + # Equivalent QNode definition (for comparison). + @qml.qnode(device=dev) + def normal_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + res = normal_circuit(0.5) + qres = qelectron_circuit(0.5) + + assert isinstance(qres, type(res)), f"Results {res!r} and {qres!r} are not the same type" + assert isclose(qres, res, 0.1), f"Results {res!r} and {qres!r} are not close" + + @pytest.mark.parametrize("executor", EXECUTORS) + def test_list_of_executors(self, executor): + """ + Test that the `ct.qelectron` decorator accepts a list of executors (case 2). + """ + + dev = qml.device("default.qubit", wires=2) + + # Create a list of executors. + executors_list = [executor, copy.deepcopy(executor)] + + # QElectron definition. + @ct.qelectron(executors=executors_list) + @qml.qnode(device=dev) + def qelectron_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + # Check that the executor is a `QCluster`. + assert len(qelectron_circuit.device.executors) == 1 + assert isinstance(qelectron_circuit.device.executors[0], ct.executor.QCluster) + + # Equivalent QNode definition (for comparison). + @qml.qnode(device=dev) + def normal_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + res = normal_circuit(0.5) + qres = qelectron_circuit(0.5) + + assert isinstance(qres, type(res)), f"Results {res!r} and {qres!r} are not the same type" + assert isclose(qres, res, 0.1), f"Results {res!r} and {qres!r} are not close" + + @pytest.mark.parametrize("executor", EXECUTORS) + def test_explicit_qcluster(self, executor): + """ + Test that the `ct.qelectron` decorator accepts a `QCluster` instance (case 3). + """ + + dev = qml.device("default.qubit", wires=2) + + # Create a `QCluster` explicitly. + executors_list = [executor, copy.deepcopy(executor)] + qcluster = ct.executor.QCluster(executors=executors_list) + + # QElectron definition. + @ct.qelectron(executors=qcluster) + @qml.qnode(device=dev) + def qelectron_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + # Check that the executor is a `QCluster`. + assert len(qelectron_circuit.device.executors) == 1 + assert isinstance(qelectron_circuit.device.executors[0], ct.executor.QCluster) + + # Equivalent QNode definition (for comparison). + @qml.qnode(device=dev) + def normal_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + res = normal_circuit(0.5) + qres = qelectron_circuit(0.5) + + assert isinstance(qres, type(res)), f"Results {res!r} and {qres!r} are not the same type" + assert isclose(qres, res, 0.1), f"Results {res!r} and {qres!r} are not close" diff --git a/tests/qelectron_tests/test_qelectron_db.py b/tests/qelectron_tests/test_qelectron_db.py new file mode 100644 index 000000000..35c5e21b4 --- /dev/null +++ b/tests/qelectron_tests/test_qelectron_db.py @@ -0,0 +1,83 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +import pennylane as qml + +import covalent as ct + + +def test_db_exposed_in_result(): + """ + Check that the QElectron database is correctly exposed in the result object. + """ + + # Define a QElectron circuit. + qexecutor = ct.executor.QiskitExecutor(device="local_sampler") # pylint: disable=no-member + + @ct.qelectron(executors=qexecutor) + @qml.qnode(qml.device("default.qubit", wires=1)) + def circuit(param): + qml.RZ(param, wires=0) + return qml.expval(qml.PauliZ(0)) + + # Define workflow electrons and lattice. + @ct.electron + def task_0_qe(param): + # Run the QElectron circuit and return the result. + return circuit(param) + + @ct.electron + def task_1_qe(param): + # Run the QElectron (10x) circuit and return the result. + params = [param * (1 + i / 10) for i in range(10)] + return [circuit(_param) for _param in params] + + @ct.electron + def task_2(): + # Returns a non-QElectron result. + return 46 + 2 + + @ct.lattice + def workflow(param): + return task_0_qe(param), task_1_qe(param), task_2() + + # Define expected number of entries in the QElectron database. + num_entries = { + "task_0_qe": 1, + "task_1_qe": 10, + "task_2": 0, + } + + # Dispatch workflow. + dispatch_id = ct.dispatch(workflow)(0.5) + result_obj = ct.get_result(dispatch_id, wait=True) + + # Check results. + for result_dict in result_obj.get_all_node_results(): + if (node_name := result_dict["node_name"]) in num_entries: + if node_name == "task_2": + # Non-QElectron task should have no qelectron data. + assert result_dict["qelectron"] is None + else: + # QElectron tasks should have qelectron data. + assert result_dict["qelectron"] is not None + + # Number of entries should match number of executions. + assert len(result_dict["qelectron"]) == num_entries[node_name] diff --git a/tests/qelectron_tests/test_qiskit_plugin.py b/tests/qelectron_tests/test_qiskit_plugin.py new file mode 100644 index 000000000..831009c6d --- /dev/null +++ b/tests/qelectron_tests/test_qiskit_plugin.py @@ -0,0 +1,163 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +import numpy as np +import pennylane as qml +import pytest + +import covalent as ct +from covalent._shared_files.config import get_config + +from .utils import arg_vector, simple_circuit, weight_vector + +EXECUTOR_CLASSES = [ + ct.executor.QiskitExecutor, + ct.executor.IBMQExecutor, +] + + +@pytest.mark.parametrize("executor_class", EXECUTOR_CLASSES) +def test_defaults_copied_from_config(executor_class): + """ + Check that instances get default values from the covalent config file. + """ + + # Initialize a minimal executor. + qexecutor = executor_class(device="local_sampler") + + # Get executor as a dictionary. + exec_config = qexecutor.dict() + + # Retrieve default values from config file. + name = executor_class.__name__ + default_config = get_config("qelectron")[name] + + # Test equivalence. + if hasattr(qexecutor, "options"): + config_without_options = default_config.copy() + config_options = config_without_options.pop("options") + + for k, val in config_without_options.items(): + assert exec_config[k] == val + + for k, val in config_options.items(): + exec_options = dict(exec_config["options"]) + assert exec_options[k] == val + else: + for k, val in default_config.items(): + assert exec_config[k] == val + + +def test_qiskit_exec_shots_is_none(): + """ + Check that a warning is raised if shots is None. + """ + + dev = qml.device("default.qubit", wires=2, shots=None) + qnode = qml.QNode(simple_circuit, device=dev) + + qexecutor = ct.executor.QiskitExecutor(device="local_sampler", shots=None) + qelectron = ct.qelectron(qnode, executors=qexecutor) + + val_1 = qnode(0.5) + with pytest.warns(UserWarning, match="The number of shots can not be None."): + val_2 = qelectron(0.5) + + assert isinstance(val_2, type(val_1)) + + +def test_default_return_type(): + """ + Test that a QElectron with the default QNode interface returns the correct type. + """ + + executor = ct.executor.QiskitExecutor(device="local_sampler", shots=1024) + + dev = qml.device("default.qubit", wires=2) + + # QElectron definition. + @ct.qelectron(executors=executor) + @qml.qnode(device=dev) + def qelectron_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + # Equivalent QNode definition (for comparison). + @qml.qnode(device=dev) + def normal_circuit(param): + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + res = normal_circuit(0.5) + qres = qelectron_circuit(0.5) + + assert isinstance(qres, type(res)), f"Results {res!r} and {qres!r} are not the same type" + + +_TEMPLATES = [ + (qml.AngleEmbedding, (arg_vector(6),), {"wires": range(6)}), + (qml.IQPEmbedding, (arg_vector(6),), {"wires": range(6)}), + (qml.QAOAEmbedding, (arg_vector(6),), {"wires": range(6), "weights": weight_vector(6)}), + (qml.DoubleExcitation, (arg_vector(4),), {"wires": range(4)}), + (qml.SingleExcitation, (arg_vector(2),), {"wires": range(2)}), +] + + +@pytest.mark.parametrize("executor_class", EXECUTOR_CLASSES[:1]) +@pytest.mark.parametrize("template", _TEMPLATES) +def test_template_circuits(template, executor_class): + """ + Check that above Pennylane templates are working. + """ + + _template, args, kwargs = template + num_wires = len(list(kwargs["wires"])) + + retval = _template(*args, **kwargs) + + # Define a circuit that uses the template. Also call the adjoint if allowed. + dev = qml.device("default.qubit", wires=num_wires, shots=10_000) + + @qml.qnode(dev, interface="numpy") + def _template_circuit(): + _template(*args, **kwargs) + + for i in range(num_wires): + # Do this so later adjoint does not invert. + qml.Hadamard(wires=i) + + if not isinstance(retval, qml.DoubleExcitation): + qml.adjoint(_template)(*args, **kwargs) + return qml.probs(wires=range(num_wires)) + + qexecutor = executor_class(device="local_sampler") # QiskitExecutor + qelectron = ct.qelectron(_template_circuit, executors=qexecutor) + + val_1 = _template_circuit() + val_2 = qelectron() + + assert isinstance(val_2, type(val_1)) + assert np.isclose(val_1, val_2, atol=0.1).all() diff --git a/tests/qelectron_tests/test_qiskit_plugin_runtime.py b/tests/qelectron_tests/test_qiskit_plugin_runtime.py new file mode 100644 index 000000000..19be0db1f --- /dev/null +++ b/tests/qelectron_tests/test_qiskit_plugin_runtime.py @@ -0,0 +1,208 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +import itertools + +import pennylane as qml +import pytest +from pennylane import numpy as np + +import covalent as ct +from covalent._shared_files.config import get_config + +from .utils import arg_vector, cyclic_selector, get_hamiltonian_circuit, weight_vector + +EXECUTOR_CLASSES = [ + ct.executor.QiskitExecutor, +] + +QISKIT_RUNTIME_BACKENDS = [ + "ibmq_qasm_simulator", + "simulator_statevector", + "simulator_mps", +] + +SHOTS = 10_000 + + +@pytest.fixture(autouse=True, scope="module") +def ensure_ibmqx_token(): + """ + Ensure that the IBMQX token is set in the config file. + """ + token_name = "ibmqx_token" + tokens = {} + qelectron_config = get_config("qelectron") + for k, val in qelectron_config.items(): + # Exit if a global `ibmqx_token` is set. + if k == "ibmqx_token" and val: + return + + # Here, `k` is the name of an executor class. + # Check if executors class config includes `"ibmqx_token"`. + if isinstance(val, dict) and token_name in val: + tokens[k] = val[token_name] + + for cls in EXECUTOR_CLASSES: + k = cls.__name__ + if not tokens[k]: + pytest.skip(f"Missing '{token_name}' for {k} in covalent config.") + + +@pytest.mark.parametrize("single_job", [True, False]) +@pytest.mark.parametrize("executor_class", EXECUTOR_CLASSES) +@pytest.mark.parametrize("backend", QISKIT_RUNTIME_BACKENDS) +def test_qiskit_runtime_hamiltonian(backend, executor_class, single_job): + """ + Check correctness of runtime executor result against normal QNode. + """ + ham_circuit, doubles, num_qubits = get_hamiltonian_circuit() + + dev = qml.device("default.qubit", wires=num_qubits, shots=SHOTS) + + # TODO: Has to be done this way for correctness. Why can't use `qml.QNode`? + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(params): + return ham_circuit(params) + + qexecutor = executor_class(device="sampler", single_job=single_job, backend=backend) + qelectron = ct.qelectron(circuit, executors=qexecutor) + + params = np.random.uniform(low=-np.pi / 2, high=np.pi / 2, size=len(doubles)) + + # Compute expectation values. + val_1 = circuit(params) + val_2 = qelectron(params) + + # Assert type agreement. + assert isinstance(val_2, type(val_1)) + + # Assert value agreement. + # Assert value agreement. + msg = ( + f"QElectron output ({val_2!r}) differs from " + f"QNode output ({val_1!r}) by >10% (shots={SHOTS})." + ) + assert np.isclose(val_1, val_2, rtol=0.10), msg + + +SINGLE_JOB_TRIPLETS = list(itertools.product([True, False], repeat=3)) + + +@pytest.mark.parametrize("single_job_triplet", SINGLE_JOB_TRIPLETS) +def test_qiskit_runtime_hamiltonian_cluster(single_job_triplet): + # pylint: disable=too-many-locals + """ + Check correctness of runtime CLUSTER executor result against normal QNode. + """ + ham_circuit, doubles, num_qubits = get_hamiltonian_circuit() + + dev = qml.device("default.qubit", wires=num_qubits, shots=SHOTS) + + # TODO: Has to be done this way for correctness. Why can't use `qml.QNode`? + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(params): + return ham_circuit(params) + + # Set function attribute. + cyclic_selector.i = 0 + + # Define the quantum executors cluster. + p_1, p_2, p_3 = single_job_triplet + qcluster = ct.executor.QCluster( + executors=[ + ct.executor.QiskitExecutor( + device="sampler", single_job=p_1, backend="ibmq_qasm_simulator" + ), + ct.executor.QiskitExecutor( + device="sampler", single_job=p_2, backend="simulator_statevector" + ), + ct.executor.QiskitExecutor(device="sampler", single_job=p_3, backend="simulator_mps"), + ], + selector=cyclic_selector, + ) + + # Define a QElectron that uses the executor cluster. + qelectron = ct.qelectron(circuit, executors=qcluster) + + params = np.random.uniform(low=-np.pi / 2, high=np.pi / 2, size=len(doubles)) + + # Compute expectation values. + val_1 = circuit(params) + val_2 = qelectron(params) + + # Assert type agreement. + assert isinstance(val_2, type(val_1)) + + # Assert value agreement. + msg = ( + f"QElectron output ({val_2!r}) differs from " + f"QNode output ({val_1!r}) by >10% (shots={SHOTS})." + ) + assert np.isclose(val_1, val_2, rtol=0.1), msg + + +TEMPLATES = [ + (qml.AngleEmbedding, (arg_vector(6),), {"wires": range(6)}), + (qml.IQPEmbedding, (arg_vector(6),), {"wires": range(6)}), + (qml.QAOAEmbedding, (arg_vector(6),), {"wires": range(6), "weights": weight_vector(6)}), + (qml.DoubleExcitation, (arg_vector(4),), {"wires": range(4)}), + (qml.SingleExcitation, (arg_vector(2),), {"wires": range(2)}), +] + + +@pytest.mark.parametrize("single_job", [True, False]) +@pytest.mark.parametrize("executor_class", EXECUTOR_CLASSES[:1]) +@pytest.mark.parametrize("template", TEMPLATES) +def test_template_circuits(template, executor_class, single_job): + """ + Check that above Pennylane templates are working. + """ + + _template, args, kwargs = template + num_wires = len(list(kwargs["wires"])) + + retval = _template(*args, **kwargs) + + # Define a circuit that uses the template. Also call the adjoint if allowed. + dev = qml.device("default.qubit", wires=num_wires, shots=10_000) + + @qml.qnode(dev, interface="numpy") + def _template_circuit(): + _template(*args, **kwargs) + + for i in range(num_wires): + # Do this so later adjoint does not invert. + qml.Hadamard(wires=i) + + if not isinstance(retval, qml.DoubleExcitation): + qml.adjoint(_template)(*args, **kwargs) + return qml.probs(wires=range(num_wires)) + + qexecutor = executor_class(device="sampler", single_job=single_job) # QiskitExecutor + qelectron = ct.qelectron(_template_circuit, executors=qexecutor) + + val_1 = _template_circuit() + val_2 = qelectron() + + assert isinstance(val_2, type(val_1)) + assert np.isclose(val_1, val_2, atol=0.1).all() diff --git a/tests/qelectron_tests/test_run_later.py b/tests/qelectron_tests/test_run_later.py new file mode 100644 index 000000000..511868b18 --- /dev/null +++ b/tests/qelectron_tests/test_run_later.py @@ -0,0 +1,96 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +import pennylane as qml +import pytest +from numpy import isclose + +import covalent as ct + +EXECUTORS = [ + ct.executor.QiskitExecutor(device="local_sampler", shots=10_000), + ct.executor.Simulator(), +] + + +@pytest.mark.parametrize("executor", EXECUTORS) +def test_qaoa(executor): + """ + Test that `run_later` produces the same result as the normal call. + """ + from networkx import Graph + from pennylane import qaoa + + wires = range(10) + graph = Graph([(0, 1), (1, 2), (2, 0)]) + cost_h, mixer_h = qaoa.maxcut(graph) + + def qaoa_layer(gamma, alpha): + qaoa.cost_layer(gamma, cost_h) + qaoa.mixer_layer(alpha, mixer_h) + + @ct.qelectron(executors=executor) + @qml.qnode(qml.device("lightning.qubit", wires=len(wires))) + def circuit(params): + for w in wires: + qml.Hadamard(wires=w) + qml.layer(qaoa_layer, 2, params[0], params[1]) + return qml.expval(cost_h) + + inputs = [[1, 1.0], [1.2, 1]] + output_1 = circuit(inputs.copy()) + output_2 = circuit.run_later(inputs.copy()).result() + + assert isclose(output_1, output_2, rtol=0.1), "Call and run later results are different" + + +@pytest.mark.parametrize("executor", EXECUTORS) +def test_multi_return_async(executor): + """ + Test that `run_later` produces the same result as the normal call. + """ + + @qml.qnode(qml.device("default.qubit", wires=2, shots=4096)) + def circuit(theta): + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + qml.RY(theta, wires=0) + + return [ + qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), + qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), + qml.expval(qml.PauliX(0) @ qml.PauliZ(1)), + qml.expval(qml.PauliX(0) @ qml.PauliX(1)), + ] + + qe_circuit = ct.qelectron(circuit, executors=executor) + + thetas = [0.1, 0.9] + + output_1 = [circuit(theta) for theta in thetas] + + futures = [qe_circuit.run_later(theta) for theta in thetas] + output_2 = [future.result() for future in futures] + + msg = "Call and run later results are different" + for o1, o2 in zip(output_1, output_2): + assert isclose(o1, o2, atol=0.1).all(), msg diff --git a/tests/qelectron_tests/utils.py b/tests/qelectron_tests/utils.py new file mode 100644 index 000000000..8455b9a2e --- /dev/null +++ b/tests/qelectron_tests/utils.py @@ -0,0 +1,125 @@ +# Copyright 2021 Agnostiq Inc. +# +# This file is part of Covalent. +# +# Licensed under the GNU Affero General Public License 3.0 (the "License"). +# A copy of the License may be obtained with this software package or at +# +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# Use of this file is prohibited except in compliance with the License. Any +# modifications or derivative works of this file must retain this copyright +# notice, and modified files must contain a notice indicating that they have +# been altered from the originals. +# +# Covalent is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details. +# +# Relief from the License may be granted by purchasing a commercial license. + +# pylint: disable=no-member + +from typing import Callable, List, Tuple + +import pennylane as qml +from pennylane import numpy as np + + +def simple_circuit(param): + """ + A tiny, reusable Pennylane circuit. + """ + qml.RX(param, wires=0) + qml.Hadamard(wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.operation.Tensor(*(qml.PauliY(0), qml.PauliX(1)))) + + +def get_hamiltonian_circuit() -> Tuple[Callable, List[int], int]: + """ + A Pennylane circuit that returns the `expval` of a Hamiltonian. + """ + + symbols = ["H", "H"] + coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614]) + H, num_qubits = qml.qchem.molecular_hamiltonian(symbols, coordinates) + + n_electrons = 2 + hf_state = qml.qchem.hf_state(n_electrons, num_qubits) + _, doubles = qml.qchem.excitations(n_electrons, num_qubits) + + def manual_double_excitation(phi, wires): + """ + Manually implement decomposition of `qml.DoubleExcitation` gate. + + TODO: replace with `qml.DoubleExcitation` call once template support is merged. + """ + qml.CNOT(wires=[wires[2], wires[3]]) + qml.CNOT(wires=[wires[0], wires[2]]) + qml.Hadamard(wires=wires[3]) + qml.Hadamard(wires=wires[0]) + qml.CNOT(wires=[wires[2], wires[3]]) + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(phi / 8, wires=wires[1]) + qml.RY(-phi / 8, wires=wires[0]) + qml.CNOT(wires=[wires[0], wires[3]]) + qml.Hadamard(wires=wires[3]) + qml.CNOT(wires=[wires[3], wires[1]]) + qml.RY(phi / 8, wires=wires[1]) + qml.RY(-phi / 8, wires=wires[0]) + qml.CNOT(wires=[wires[2], wires[1]]) + qml.CNOT(wires=[wires[2], wires[0]]) + qml.RY(-phi / 8, wires=wires[1]) + qml.RY(phi / 8, wires=wires[0]) + qml.CNOT(wires=[wires[3], wires[1]]) + qml.Hadamard(wires=wires[3]) + qml.CNOT(wires=[wires[0], wires[3]]) + qml.RY(-phi / 8, wires=wires[1]) + qml.RY(phi / 8, wires=wires[0]) + qml.CNOT(wires=[wires[0], wires[1]]) + qml.CNOT(wires=[wires[2], wires[0]]) + qml.Hadamard(wires=wires[0]) + qml.Hadamard(wires=wires[3]) + qml.CNOT(wires=[wires[0], wires[2]]) + qml.CNOT(wires=[wires[2], wires[3]]) + + def circuit(params): + """ + Applies circuit operations. + """ + for i, occ in enumerate(hf_state): + if occ == 1: + qml.PauliX(wires=i) + for param in params: + manual_double_excitation(param, wires=list(range(num_qubits))) + return qml.expval(H) + + return circuit, doubles, num_qubits + + +def cyclic_selector(qscript, executors): + """ + A QCluster selector that cycle through sub-executors in a cyclic fashion. + + NOTE: set the `i` attribute to 0 before using this selector. + + TODO: remove once default cluster selectors are implemented. + """ + ex = executors[cyclic_selector.i % len(executors)] + cyclic_selector.i += 1 + return ex + + +def arg_vector(size): + """ + A random `tensor` of size `(size,)` with values in `[0, 2 * pi]`. + """ + return qml.numpy.random.uniform(0, 2 * np.pi, size=(size,)) + + +def weight_vector(size): + """ + A QAOA weights vector that matches args from `arg_vector(size)`. + """ + return [arg_vector(2 * size) for _ in range(size)]