From ac5dd0b6aefd9b19e62dd64968f50f5c119b9c4f Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Mon, 1 May 2023 15:11:33 -0400 Subject: [PATCH 01/11] add pytest.ini to fail on deprecation warnings --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..34596fce3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + error::DeprecationWarning From cecb6daff12e69eb03b863b9a4a8656760971ec5 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Tue, 2 May 2023 09:21:56 -0400 Subject: [PATCH 02/11] fix deprecation warnings from import while running tests --- pennylane_qiskit/ibmq.py | 63 +++++++++++------------------ pennylane_qiskit/runtime_devices.py | 2 +- requirements.txt | 2 +- 3 files changed, 26 insertions(+), 41 deletions(-) diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index 1d1ad9b43..695ba07f7 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -18,8 +18,9 @@ """ import os -from qiskit import IBMQ -from qiskit.providers.ibmq.exceptions import IBMQAccountError +from qiskit_ibm_provider import IBMProvider +from qiskit_ibm_provider.exceptions import IBMAccountError +from qiskit_ibm_provider.accounts.exceptions import AccountsError from .qiskit_device import QiskitDevice @@ -41,7 +42,7 @@ class IBMQDevice(QiskitDevice): or strings (``['ancilla', 'q1', 'q2']``). Note that for some backends, the number of wires has to match the number of qubits accessible. provider (Provider): The IBM Q provider you wish to use. If not provided, - then the default provider returned by ``IBMQ.get_provider()`` is used. + then the default provider returned by ``IBMProvider()`` is used. backend (str): the desired provider backend shots (int): number of circuit evaluations/random samples used to estimate expectation values and variances of observables @@ -67,9 +68,10 @@ def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=10 hub = kwargs.get("hub", "ibm-q") group = kwargs.get("group", "open") project = kwargs.get("project", "main") + instance = "/".join([hub, group, project]) # get a provider - p = provider or IBMQ.get_provider(hub=hub, group=group, project=project) + p = provider or IBMProvider(instance=instance) super().__init__(wires=wires, provider=p, backend=backend, shots=shots, **kwargs) @@ -101,41 +103,24 @@ def connect(kwargs): Args: kwargs(dict): dictionary that contains the token and the url""" + hub = kwargs.get("hub", "ibm-q") + group = kwargs.get("group", "open") + project = kwargs.get("project", "main") + instance = "/".join([hub, group, project]) + 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: # 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 + saved_accounts = IBMProvider.saved_accounts() + if not token: + if not saved_accounts: + raise IBMAccountError("No active IBM Q account, and no IBM Q token provided.") + try: + IBMProvider(url=url, instance=instance) + except AccountsError as e: + raise AccountsError(f"Accounts were found ({set(saved_accounts)}), but all failed to load.") from e + return + for account in saved_accounts.values(): + if account["token"] == token: + return + IBMProvider(token=token, url=url, instance=instance).save_account(token=token) diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py index 226cfb736..b007a07c5 100644 --- a/pennylane_qiskit/runtime_devices.py +++ b/pennylane_qiskit/runtime_devices.py @@ -18,7 +18,7 @@ import numpy as np -from qiskit.providers.ibmq import RunnerResult +from qiskit_ibm_runtime.constants import RunnerResult from pennylane_qiskit.ibmq import IBMQDevice diff --git a/requirements.txt b/requirements.txt index 46aa4c964..0be648d5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ python-dateutil==2.8.2 qiskit==0.42.1 qiskit-aer==0.10.1 qiskit-ibm-runtime==0.9.3 -qiskit-ibmq-provider==0.18.3 +qiskit-ibm-provider==0.5.2 qiskit-ignis==0.7.1 qiskit-terra==0.23.3 requests==2.27.1 From 23a551dcb759f29c3e9b859324e7579105f4fec9 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Tue, 2 May 2023 14:58:02 -0400 Subject: [PATCH 03/11] use the new runtime service --- pennylane_qiskit/aer.py | 4 +- pennylane_qiskit/ibmq.py | 4 +- pennylane_qiskit/qiskit_device.py | 8 +- pennylane_qiskit/runtime_devices.py | 30 +++--- pennylane_qiskit/vqe_runtime_runner.py | 2 +- requirements.txt | 2 +- tests/conftest.py | 13 ++- tests/test_qiskit_device.py | 15 ++- tests/test_runtime.py | 132 ++++++++++--------------- 9 files changed, 100 insertions(+), 110 deletions(-) diff --git a/pennylane_qiskit/aer.py b/pennylane_qiskit/aer.py index dbc6a2e10..ae1f06549 100644 --- a/pennylane_qiskit/aer.py +++ b/pennylane_qiskit/aer.py @@ -16,7 +16,7 @@ evaluation and differentiation of Qiskit Aer's C++ simulator using PennyLane. """ -import qiskit +import qiskit_aer from .qiskit_device import QiskitDevice @@ -60,4 +60,4 @@ def __init__(self, wires, shots=1024, backend="aer_simulator", method="automatic if method != "automatic": backend += "_" + method - super().__init__(wires, provider=qiskit.Aer, backend=backend, shots=shots, **kwargs) + super().__init__(wires, provider=qiskit_aer.Aer, backend=backend, shots=shots, **kwargs) diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index 695ba07f7..82044234f 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -118,7 +118,9 @@ def connect(kwargs): try: IBMProvider(url=url, instance=instance) except AccountsError as e: - raise AccountsError(f"Accounts were found ({set(saved_accounts)}), but all failed to load.") from e + raise AccountsError( + f"Accounts were found ({set(saved_accounts)}), but all failed to load." + ) from e return for account in saved_accounts.values(): if account["token"] == token: diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index 5c26fe2a2..95cbde7af 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -158,7 +158,13 @@ def __init__(self, wires, provider, backend, shots=1024, **kwargs): self.provider = provider self.backend_name = backend - self._capabilities["backend"] = [b.name() for b in self.provider.backends()] + + def _get_backend_name(name): + return name if isinstance(name, str) else name() + + self._capabilities["backend"] = [ + _get_backend_name(b.name) for b in self.provider.backends() + ] self._capabilities["returns_state"] = backend in self._state_backends # Check that the backend exists diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py index b007a07c5..199b12464 100644 --- a/pennylane_qiskit/runtime_devices.py +++ b/pennylane_qiskit/runtime_devices.py @@ -18,6 +18,9 @@ import numpy as np +from qiskit.primitives import Sampler +from qiskit_ibm_provider import IBMProvider +from qiskit_ibm_runtime import Options, QiskitRuntimeService from qiskit_ibm_runtime.constants import RunnerResult from pennylane_qiskit.ibmq import IBMQDevice @@ -55,6 +58,7 @@ class IBMQCircuitRunnerDevice(IBMQDevice): 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) + self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") def batch_execute(self, circuits): compiled_circuits = self.compile_circuits(circuits) @@ -65,10 +69,10 @@ def batch_execute(self, circuits): program_inputs[kwarg] = self.kwargs.get(kwarg) # Specify the backend. - options = {"backend_name": self.backend.name()} + options = {"backend": self.backend.name} # Send circuits to the cloud for execution by the circuit-runner program. - job = self.provider.runtime.run( + job = self.runtime_service.run( program_id="circuit-runner", options=options, inputs=program_inputs ) self._current_job = job.result(decoder=RunnerResult) @@ -140,6 +144,7 @@ class IBMQSamplerDevice(IBMQDevice): 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) + self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") def batch_execute(self, circuits): compiled_circuits = self.compile_circuits(circuits) @@ -147,13 +152,13 @@ def batch_execute(self, circuits): program_inputs = {"circuits": compiled_circuits} if "circuits_indices" not in self.kwargs: - circuit_indices = list(range(0, len(compiled_circuits))) + circuit_indices = list(range(len(compiled_circuits))) program_inputs["circuit_indices"] = circuit_indices else: circuit_indices = self.kwargs.get("circuit_indices") if "run_options" in self.kwargs: - if not "shots" in self.kwargs["run_options"]: + if "shots" not in self.kwargs["run_options"]: self.kwargs["run_options"]["shots"] = self.shots else: self.kwargs["run_options"] = {"shots": self.shots} @@ -162,11 +167,9 @@ def batch_execute(self, circuits): program_inputs[kwarg] = self.kwargs.get(kwarg) # Specify the backend. - options = {"backend_name": self.backend.name()} + options = {"backend": 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 - ) + job = self.runtime_service.run(program_id="sampler", options=options, inputs=program_inputs) self._current_job = job.result() results = [] @@ -199,14 +202,13 @@ def generate_samples(self, circuit_id=None): Returns: array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` """ - counts = self._current_job.get("quasi_dists")[circuit_id] + counts = self._current_job.quasi_dists[circuit_id] keys = list(counts.keys()) - number_of_states = 2 ** len(keys[0]) - - # Convert state to int - for i, elem in enumerate(keys): - keys[i] = int(elem, 2) + if isinstance(keys[0], str): + for i, elem in enumerate(keys): + keys[i] = int(elem, 2) + number_of_states = len(keys) values = list(counts.values()) states, probs = zip(*sorted(zip(keys, values))) diff --git a/pennylane_qiskit/vqe_runtime_runner.py b/pennylane_qiskit/vqe_runtime_runner.py index ace1a221e..96ab387aa 100644 --- a/pennylane_qiskit/vqe_runtime_runner.py +++ b/pennylane_qiskit/vqe_runtime_runner.py @@ -193,7 +193,7 @@ def vqe_runner( project = kwargs.get("project", "main") instance = "/".join([hub, group, project]) - options = {"backend_name": backend, "instance": instance} + options = {"backend": backend, "instance": instance} service = QiskitRuntimeService(channel="ibm_quantum", token=os.getenv("IBMQX_TOKEN")) rt_job = RuntimeJobWrapper() diff --git a/requirements.txt b/requirements.txt index 0be648d5f..453126de0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ pycparser==2.21 python-constraint==1.4.0 python-dateutil==2.8.2 qiskit==0.42.1 -qiskit-aer==0.10.1 +qiskit-aer==0.12.0 qiskit-ibm-runtime==0.9.3 qiskit-ibm-provider==0.5.2 qiskit-ignis==0.7.1 diff --git a/tests/conftest.py b/tests/conftest.py index 2e3a92b15..792c27d4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,8 @@ import pennylane as qml from qiskit import IBMQ +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime.accounts.exceptions import AccountNotFoundError from pennylane_qiskit import AerDevice, BasicAerDevice np.random.seed(42) @@ -52,8 +54,15 @@ def token(): if t is None: pytest.skip("Skipping test, no IBMQ token available") - yield t - IBMQ.disable_account() + return t + + +@pytest.fixture +def skip_if_no_account(): + try: + QiskitRuntimeService() + except AccountNotFoundError: + pytest.skip("Skipping test, no IBMQ account available") @pytest.fixture diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 630ce7c9e..1f48b6f03 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -1,5 +1,4 @@ import numpy as np -import pennylane import pytest import pennylane as qml @@ -63,14 +62,14 @@ def test_warning_raised_for_hardware_backend_analytic_expval(self, hardware_back assert len(record) == 1 # check that the message matches assert ( - record[0].message.args[0] == "The analytic calculation of " - "expectations, variances and probabilities is only supported on " - "statevector backends, not on the {}. Such statistics obtained from this " - "device are estimates based on samples.".format(dev.backend) + record[0].message.args[0] == "The analytic calculation of " + "expectations, variances and probabilities is only supported on " + "statevector backends, not on the {}. Such statistics obtained from this " + "device are estimates based on samples.".format(dev.backend) ) def test_no_warning_raised_for_software_backend_analytic_expval( - self, statevector_backend, recorder, recwarn + self, statevector_backend, recorder, recwarn ): """Tests that no warning is raised if the analytic attribute is true on statevector simulators when calculating the expectation""" @@ -144,7 +143,7 @@ def test_calls_to_reset(self, n_tapes, mocker, device): def test_result_legacy(self, device, tol): """Tests that the result has the correct shape and entry types.""" # TODO: remove once the legacy return system is removed. - pennylane.disable_return() + qml.disable_return() dev = device(2) tapes = [self.tape1, self.tape2] res = dev.batch_execute(tapes) @@ -164,7 +163,7 @@ def test_result_legacy(self, device, tol): assert isinstance(res[1], np.ndarray) assert np.allclose(res[1], tape2_expected, atol=0) - pennylane.enable_return() + qml.enable_return() def test_result(self, device, tol): """Tests that the result has the correct shape and entry types.""" diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 19dec8561..6f77d8e0a 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -24,26 +24,21 @@ from pennylane_qiskit import IBMQCircuitRunnerDevice, IBMQSamplerDevice from pennylane_qiskit.vqe_runtime_runner import vqe_runner, hamiltonian_to_list_string +pytest.mark.usefixtures("skip_if_no_account") + 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): + def test_short_name(self): """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() + assert isinstance(dev, IBMQCircuitRunnerDevice) @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, token, tol, shots): + def test_simple_circuit(self, 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) @@ -78,9 +73,9 @@ def circuit(theta, phi): } ], ) - def test_kwargs_circuit(self, token, tol, shots, kwargs): + def test_kwargs_circuit(self, 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) @@ -98,9 +93,9 @@ def circuit(theta, phi): assert np.allclose(res, expected, **tol) @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, token, tol, shots): + def test_batch_circuits(self, 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 @@ -120,10 +115,9 @@ def circuit(x, y, z): assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - def test_track_circuit_runner(self, token): + def test_track_circuit_runner(self): """Test that the tracker works.""" - IBMQ.enable_account(token) dev = IBMQCircuitRunnerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) dev.tracker.active = True @@ -143,21 +137,14 @@ def circuit(): 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) + def test_short_name(self): dev = qml.device("qiskit.ibmq.sampler", wires=1) - return dev.provider.credentials.is_ibmq() + assert isinstance(dev, IBMQSamplerDevice) @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, token, tol, shots): + def test_simple_circuit(self, 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) @@ -185,9 +172,9 @@ def circuit(theta, phi): } ], ) - def test_kwargs_circuit(self, token, tol, shots, kwargs): + def test_kwargs_circuit(self, 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) @@ -205,9 +192,9 @@ def circuit(theta, phi): assert np.allclose(res, expected, **tol) @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, token, tol, shots): + def test_batch_circuits(self, tol, shots): """Test executing batched circuits submitted to IBMQ using the Sampler device.""" - IBMQ.enable_account(token) + dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=shots) # Batch the input parameters @@ -227,10 +214,9 @@ def circuit(x, y, z): assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - def test_track_sampler(self, token): + def test_track_sampler(self): """Test that the tracker works.""" - IBMQ.enable_account(token) dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) dev.tracker.active = True @@ -258,9 +244,9 @@ def test_hamiltonian_to_list_string(self): assert [("XIX", 1), ("YZI", 1)] == result @pytest.mark.parametrize("shots", [8000]) - def test_simple_hamiltonian(self, token, tol, shots): + def test_simple_hamiltonian(self, 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): @@ -291,9 +277,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_ansatz_qiskit(self, token, tol, shots): + def test_ansatz_qiskit(self, 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)] @@ -316,9 +301,8 @@ def test_ansatz_qiskit(self, token, tol, shots): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_ansatz_qiskit_invalid(self, token, tol, shots): + def test_ansatz_qiskit_invalid(self, 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)] @@ -342,9 +326,8 @@ def test_ansatz_qiskit_invalid(self, token, tol, shots): ) @pytest.mark.parametrize("shots", [8000]) - def test_qnode(self, token, tol, shots): + def test_qnode(self, 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) @@ -374,9 +357,8 @@ def test_qnode(self, token, tol, shots): ) @pytest.mark.parametrize("shots", [8000]) - def test_tape(self, token, tol, shots): + def test_tape(self, shots): """Test that we cannot pass a tape as ansatz circuit.""" - IBMQ.enable_account(token) dev = qml.device("default.qubit", wires=1) @@ -409,9 +391,8 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_wrong_input(self, token, tol, shots): + def test_wrong_input(self, 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) @@ -439,9 +420,8 @@ def vqe_circuit(params, wire): ) @pytest.mark.parametrize("shots", [8000]) - def test_wrong_number_input_param(self, token, tol, shots): + def test_wrong_number_input_param(self, shots): """Test that we need a certain number of parameters.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.RX(params[0], wires=0) @@ -470,9 +450,8 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_one_param(self, token, tol, shots): + def test_one_param(self, 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) @@ -499,9 +478,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_too_many_param(self, token, tol, shots): + def test_too_many_param(self, 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) @@ -512,7 +490,10 @@ def vqe_circuit(params): hamiltonian = qml.Hamiltonian(coeffs, obs) - with pytest.warns(UserWarning, match="In order to match the tape expansion, the number of parameters has been changed."): + with pytest.warns( + UserWarning, + match="In order to match the tape expansion, the number of parameters has been changed.", + ): job = vqe_runner( backend="ibmq_qasm_simulator", hamiltonian=hamiltonian, @@ -534,9 +515,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_more_qubits_in_circuit_than_hamiltonian(self, token, tol, shots): + def test_more_qubits_in_circuit_than_hamiltonian(self, 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) @@ -564,9 +544,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_qubitunitary(self, token, tol, shots): + def test_qubitunitary(self, 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) @@ -595,9 +574,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_inverse(self, token, tol, shots): + def test_inverse(self, shots): """Test that we can handle inverse operations.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.adjoint(qml.RX(params[0], wires=0)) @@ -625,9 +603,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_hamiltonian_format(self, token, tol, shots): + def test_hamiltonian_format(self, shots): """Test that a PennyLane Hamiltonian is required.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.RX(params[0], wires=0) @@ -653,9 +630,8 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_hamiltonian_tensor(self, token, tol, shots): + def test_hamiltonian_tensor(self, shots): """Test that we can handle tensor Hamiltonians.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.RX(params[0], wires=0) @@ -681,11 +657,12 @@ def vqe_circuit(params): assert "function" in job.intermediate_results assert "step" in job.intermediate_results - @pytest.mark.parametrize("bad_op", [qml.Hermitian(np.array([[1, 0], [0, -1]]), wires=0), qml.Hadamard(1)]) + @pytest.mark.parametrize( + "bad_op", [qml.Hermitian(np.array([[1, 0], [0, -1]]), wires=0), qml.Hadamard(1)] + ) @pytest.mark.parametrize("shots", [8000]) - def test_not_auth_operation_hamiltonian(self, token, tol, shots, bad_op): + def test_not_auth_operation_hamiltonian(self, shots, bad_op): """Test the observables in the Hamiltonian are I, X, Y, or Z.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.RX(params[0], wires=0) @@ -712,9 +689,8 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_not_auth_operation_hamiltonian_tensor(self, token, tol, shots): + def test_not_auth_operation_hamiltonian_tensor(self, shots): """Test the observables in the tensor Hamiltonian are I, X, Y, or Z.""" - IBMQ.enable_account(token) def vqe_circuit(params): qml.RX(params[0], wires=0) @@ -741,9 +717,9 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_scipy_optimizer(self, token, tol, shots): + def test_scipy_optimizer(self, tol, shots): """Test we can run a VQE problem with a SciPy optimizer.""" - IBMQ.enable_account(token) + tol = 1e-1 def vqe_circuit(params): @@ -772,10 +748,8 @@ def vqe_circuit(params): assert "parameters" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_scipy_optimizer(self, token, tol, shots): + def test_scipy_optimizer(self, 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) @@ -802,9 +776,9 @@ def vqe_circuit(params): assert "parameters" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_simple_hamiltonian_with_untrainable_parameters(self, token, tol, shots): + def test_simple_hamiltonian_with_untrainable_parameters(self, tol, shots): """Test a simple VQE problem with untrainable parameters.""" - IBMQ.enable_account(token) + tol = 1e-1 def vqe_circuit(params): @@ -837,9 +811,8 @@ def vqe_circuit(params): assert "step" in job.intermediate_results @pytest.mark.parametrize("shots", [8000]) - def test_invalid_function(self, token, tol, shots): + def test_invalid_function(self, shots): """Test that an invalid function cannot be passed.""" - IBMQ.enable_account(token) def vqe_circuit(params): c = params[0] + params[1] @@ -867,9 +840,8 @@ def vqe_circuit(params): ) @pytest.mark.parametrize("shots", [8000]) - def test_invalid_ansatz(self, token, tol, shots): + def test_invalid_ansatz(self, shots): """Test that an invalid ansatz cannot be passed.""" - IBMQ.enable_account(token) coeffs = [1, 1] obs = [qml.PauliX(0), qml.PauliZ(0)] @@ -904,4 +876,4 @@ def test_qnspsa_disabled(self): hamiltonian=hamiltonian, x0=[3.97507603, 3.00854038], optimizer="QNSPSA", - ) \ No newline at end of file + ) From a208766d499dda17a0b4078df58967f812b5fcac Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 15:35:44 -0400 Subject: [PATCH 04/11] fix up ibmq tests --- doc/requirements.txt | 1 + pennylane_qiskit/ibmq.py | 2 +- pennylane_qiskit/runtime_devices.py | 4 +- pytest.ini | 3 - setup.py | 1 + tests/conftest.py | 24 +- tests/test_ibmq.py | 464 +++++++++++++--------------- tests/test_integration.py | 5 +- tests/test_qiskit_device.py | 2 +- tests/test_runtime.py | 83 ++--- 10 files changed, 268 insertions(+), 321 deletions(-) delete mode 100644 pytest.ini diff --git a/doc/requirements.txt b/doc/requirements.txt index 4f7986776..bb494bbb3 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -10,6 +10,7 @@ pygments==2.7.4 pygments-github-lexers==0.0.5 qiskit==0.42.1 qiskit-ibm-runtime==0.9.3 +qiskit-ibm-provider==0.5.2 sphinxcontrib-bibtex==2.5.0 sphinx-automodapi==0.14.1 pennylane-sphinx-theme diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index 82044234f..b912bd655 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -125,4 +125,4 @@ def connect(kwargs): for account in saved_accounts.values(): if account["token"] == token: return - IBMProvider(token=token, url=url, instance=instance).save_account(token=token) + IBMProvider.save_account(token=token, url=url, instance=instance) diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py index 199b12464..205093918 100644 --- a/pennylane_qiskit/runtime_devices.py +++ b/pennylane_qiskit/runtime_devices.py @@ -18,9 +18,7 @@ import numpy as np -from qiskit.primitives import Sampler -from qiskit_ibm_provider import IBMProvider -from qiskit_ibm_runtime import Options, QiskitRuntimeService +from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_ibm_runtime.constants import RunnerResult from pennylane_qiskit.ibmq import IBMQDevice diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 34596fce3..000000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -filterwarnings = - error::DeprecationWarning diff --git a/setup.py b/setup.py index 3ada398db..f171de323 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ requirements = [ "qiskit>=0.32", "qiskit-ibm-runtime", + "qiskit-ibm-provider", "mthree>=0.17", "pennylane>=0.30", "numpy", diff --git a/tests/conftest.py b/tests/conftest.py index 792c27d4e..5c8d41f05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,9 +20,7 @@ import numpy as np import pennylane as qml -from qiskit import IBMQ -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime.accounts.exceptions import AccountNotFoundError +from qiskit_ibm_provider import IBMProvider from pennylane_qiskit import AerDevice, BasicAerDevice np.random.seed(42) @@ -48,21 +46,19 @@ @pytest.fixture -def token(): +def skip_if_no_account(): t = os.getenv("IBMQX_TOKEN", None) - - if t is None: - pytest.skip("Skipping test, no IBMQ token available") - - return t + try: + IBMProvider(token=t) + except Exception: + missing = "token" if t else "account" + pytest.skip(f"Skipping test, no IBMQ {missing} available") @pytest.fixture -def skip_if_no_account(): - try: - QiskitRuntimeService() - except AccountNotFoundError: - pytest.skip("Skipping test, no IBMQ account available") +def skip_if_account_saved(): + if IBMProvider.saved_accounts(): + pytest.skip("Skipping test, IBMQ will load an account successfully") @pytest.fixture diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py index 77fc70ab2..2524982c1 100644 --- a/tests/test_ibmq.py +++ b/tests/test_ibmq.py @@ -18,99 +18,65 @@ import pennylane as qml import pytest -from qiskit import IBMQ -from qiskit.providers.ibmq.exceptions import IBMQAccountError +from qiskit_ibm_provider.exceptions import IBMAccountError +from qiskit_ibm_provider import IBMProvider from pennylane_qiskit import IBMQDevice -from pennylane_qiskit import ibmq as ibmq +from pennylane_qiskit import ibmq -def test_load_from_env(token, monkeypatch): - """test loading an IBMQ device from - an env variable""" - monkeypatch.setenv("IBMQX_TOKEN", token) - dev = IBMQDevice(wires=1) - assert dev.provider.credentials.is_ibmq() - - -def test_load_from_env_multiple_device(token, monkeypatch): - """Test creating multiple IBMQ devices when the environment variable - for the IBMQ token was set.""" - monkeypatch.setenv("IBMQX_TOKEN", token) - dev1 = IBMQDevice(wires=1) - dev2 = IBMQDevice(wires=1) - - assert dev1.provider.credentials.is_ibmq() - assert dev2.provider.credentials.is_ibmq() +class MockQiskitDeviceInit: + """A mocked version of the QiskitDevice __init__ method which + is called on by the IBMQDevice""" + def mocked_init(self, wires, provider, backend, shots, **kwargs): + """Stores the provider which QiskitDevice.__init__ was + called with.""" + self.provider = provider -def test_load_from_env_multiple_device_and_token(monkeypatch): - """Test creating multiple devices when the different tokens are defined - using an environment variable.""" - mock_provider = "MockProvider" - mock_qiskit_device = MockQiskitDeviceInit() +def test_multi_load_changing_token(monkeypatch): + """Test multiple account loads with changing tokens.""" with monkeypatch.context() as m: - m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) + # unrelated mock + m.setattr(ibmq.QiskitDevice, "__init__", lambda self, *a, **k: None) - creds = [] + # mock save_account to save the token, saved_accounts lists those tokens + tokens = [] - def enable_account(new_creds): - creds.append(new_creds) + def saved_accounts(): + return {f"account-{i}": {"token": t} for i, t in enumerate(tokens)} - def active_account(): - if len(creds) != 0: - return {"token": creds[-1]} + def save_account(token=None, **kwargs): + tokens.append(token) - m.setattr(ibmq.IBMQ, "enable_account", enable_account) - m.setattr(ibmq.IBMQ, "disable_account", lambda: None) - m.setattr(ibmq.IBMQ, "active_account", active_account) + m.setattr(IBMProvider, "saved_accounts", saved_accounts) + m.setattr(IBMProvider, "save_account", save_account) + m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) - m.setenv("IBMQX_TOKEN", "TOKEN1") - dev1 = IBMQDevice(wires=1, provider=mock_provider) - # first login - assert creds == ["TOKEN1"] - dev1 = IBMQDevice(wires=1, provider=mock_provider) - # same token, login is elided - assert creds == ["TOKEN1"] + m.setenv("IBMQX_TOKEN", "T1") + IBMQDevice(wires=1) + assert tokens == ["T1"] + IBMQDevice(wires=1) + assert tokens == ["T1"] - m.setenv("IBMQX_TOKEN", "TOKEN2") - dev2 = IBMQDevice(wires=1, provider=mock_provider) - # second login - assert creds == ["TOKEN1", "TOKEN2"] + m.setenv("IBMQX_TOKEN", "T2") + IBMQDevice(wires=1) + assert tokens == ["T1", "T2"] -def test_load_kwargs_takes_precedence(token, monkeypatch): +def test_load_kwargs_takes_precedence(monkeypatch, mocker): """Test that with a potentially valid token stored as an environment variable, passing the token as a keyword argument takes precedence.""" monkeypatch.setenv("IBMQX_TOKEN", "SomePotentiallyValidToken") - dev = IBMQDevice(wires=1, ibmqx_token=token) - assert dev.provider.credentials.is_ibmq() - + mock = mocker.patch("qiskit_ibm_provider.IBMProvider.save_account") -def test_load_env_empty_string_has_short_error(monkeypatch): - """Test that the empty string is treated as a missing token.""" - monkeypatch.setenv("IBMQX_TOKEN", "") - with pytest.raises(IBMQAccountError, match="No active IBM Q account"): - IBMQDevice(wires=1) - - -def test_account_already_loaded(token): - """Test loading an IBMQ device using - an already loaded account""" - IBMQ.enable_account(token) - dev = IBMQDevice(wires=1) - assert dev.provider.credentials.is_ibmq() - - -class MockQiskitDeviceInit: - """A mocked version of the QiskitDevice __init__ method which - is called on by the IBMQDevice""" + with monkeypatch.context() as m: + m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) + m.setattr(ibmq.QiskitDevice, "__init__", lambda self, *a, **k: None) + IBMQDevice(wires=1, ibmqx_token="TheTrueToken") - def mocked_init(self, wires, provider, backend, shots, **kwargs): - """Stores the provider which QiskitDevice.__init__ was - called with.""" - self.provider = provider + mock.assert_called_with(token="TheTrueToken", url=None, instance="ibm-q/open/main") def test_custom_provider(monkeypatch): @@ -121,222 +87,210 @@ def test_custom_provider(monkeypatch): monkeypatch.setenv("IBMQX_TOKEN", "1") with monkeypatch.context() as m: + m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) - m.setattr(ibmq.IBMQ, "enable_account", lambda *args, **kwargs: None) - - # Here mocking to a value such that it is not None - m.setattr(ibmq.IBMQ, "active_account", lambda *args, **kwargs: {"token": "1"}) - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", provider=mock_provider) + m.setattr(IBMProvider, "saved_accounts", lambda: {"my-account": {"token": "1"}}) + IBMQDevice(wires=2, backend="ibmq_qasm_simulator", provider=mock_provider) assert mock_qiskit_device.provider == mock_provider -def mock_get_provider(*args, **kwargs): - """A mock function for the get_provider Qiskit function to record the - arguments which it was called with.""" - return (args, kwargs) - - def test_default_provider(monkeypatch): """Tests that the default provider is used when no custom provider was specified.""" mock_qiskit_device = MockQiskitDeviceInit() monkeypatch.setenv("IBMQX_TOKEN", "1") + def provider_init(self, instance=None): + self.instance = instance + with monkeypatch.context() as m: m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) - m.setattr(ibmq.IBMQ, "get_provider", mock_get_provider) - m.setattr(ibmq.IBMQ, "enable_account", lambda *args, **kwargs: None) - - # Here mocking to a value such that it is not None - m.setattr(ibmq.IBMQ, "active_account", lambda *args, **kwargs: {"token": "1"}) - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator") + m.setattr(IBMProvider, "__init__", provider_init) + m.setattr(IBMProvider, "saved_accounts", lambda: {"my-account": {"token": "1"}}) + IBMQDevice(wires=2, backend="ibmq_qasm_simulator") - assert mock_qiskit_device.provider[0] == () - assert mock_qiskit_device.provider[1] == {"hub": "ibm-q", "group": "open", "project": "main"} + assert isinstance(mock_qiskit_device.provider, IBMProvider) + assert mock_qiskit_device.provider.instance == "ibm-q/open/main" -def test_custom_provider_hub_group_project(monkeypatch): +def test_custom_provider_hub_group_project_url(monkeypatch, mocker): """Tests that the custom arguments passed during device instantiation are - used when calling get_provider.""" - mock_qiskit_device = MockQiskitDeviceInit() + used when calling IBMProvider.save_account""" monkeypatch.setenv("IBMQX_TOKEN", "1") + mock = mocker.patch("qiskit_ibm_provider.IBMProvider.save_account") custom_hub = "SomeHub" custom_group = "SomeGroup" custom_project = "SomeProject" + instance = f"{custom_hub}/{custom_group}/{custom_project}" with monkeypatch.context() as m: - m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) - m.setattr(ibmq.IBMQ, "get_provider", mock_get_provider) - m.setattr(ibmq.IBMQ, "enable_account", lambda *args, **kwargs: None) - - # Here mocking to a value such that it is not None - m.setattr(ibmq.IBMQ, "active_account", lambda *args, **kwargs: {"token": "1"}) - dev = IBMQDevice( + m.setattr(ibmq.QiskitDevice, "__init__", lambda *a, **k: None) + m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) + IBMQDevice( wires=2, backend="ibmq_qasm_simulator", hub=custom_hub, group=custom_group, project=custom_project, + ibmqx_url="example.com", ) - assert mock_qiskit_device.provider[0] == () - assert mock_qiskit_device.provider[1] == { - "hub": custom_hub, - "group": custom_group, - "project": custom_project, - } + mock.assert_called_with(token="1", url="example.com", instance=instance) -def test_load_from_disk(token): - """Test loading the account credentials and the device from disk.""" - IBMQ.save_account(token) - dev = IBMQDevice(wires=1) - assert dev.provider.credentials.is_ibmq() - IBMQ.delete_account() +@pytest.mark.usefixtures("skip_if_account_saved") +class TestMustNotHaveAccount: + """Tests that require the user _not_ have an IBMQ account loaded.""" - -def test_account_error(monkeypatch): - """Test that an error is raised if there is no active IBMQ account.""" - - # Token is passed such that the test is skipped if no token was provided - with pytest.raises(IBMQAccountError, match="No active IBM Q account"): - with monkeypatch.context() as m: - m.delenv("IBMQX_TOKEN", raising=False) + def test_load_env_empty_string_has_short_error(self, monkeypatch): + """Test that the empty string is treated as a missing token.""" + monkeypatch.setenv("IBMQX_TOKEN", "") + with pytest.raises(IBMAccountError, match="No active IBM Q account"): IBMQDevice(wires=1) - -@pytest.mark.parametrize("shots", [1000]) -def test_simple_circuit(token, tol, shots): - """Test executing a simple circuit submitted to IBMQ.""" - IBMQ.enable_account(token) - dev = IBMQDevice(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", [1000]) -def test_simple_circuit_with_batch_params(token, tol, shots, mocker): - """Test that executing a simple circuit with batched parameters is - submitted to IBMQ once.""" - IBMQ.enable_account(token) - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.batch_params(all_operations=True) - @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)) - - # Check that we run only once - spy1 = mocker.spy(dev, "batch_execute") - spy2 = mocker.spy(dev.backend, "run") - - # Batch the input parameters - batch_dim = 3 - theta = np.linspace(0, 0.543, batch_dim) - phi = np.linspace(0, 0.123, batch_dim) - - res = circuit(theta, phi) - assert np.allclose(res[0], np.cos(theta), **tol) - assert np.allclose(res[1], np.cos(theta) * np.cos(phi), **tol) - - # Check that IBMQBackend.run was called once - assert spy1.call_count == 1 - assert spy2.call_count == 1 - - -@pytest.mark.parametrize("shots", [1000]) -def test_batch_execute_parameter_shift(token, tol, shots, mocker): - """Test that devices provide correct result computing the gradient of a - circuit using the parameter-shift rule and the batch execution pipeline.""" - IBMQ.enable_account(token) - dev = IBMQDevice(wires=3, backend="ibmq_qasm_simulator", shots=shots) - - spy1 = mocker.spy(dev, "batch_execute") - spy2 = mocker.spy(dev.backend, "run") - - @qml.qnode(dev, diff_method="parameter-shift") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2)) - - x = qml.numpy.array(0.543, requires_grad=True) - y = qml.numpy.array(0.123, requires_grad=True) - - res = qml.grad(circuit)(x, y) - expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) - assert np.allclose(res, expected, **tol) - - # Check that QiskitDevice.batch_execute was called once - assert spy1.call_count == 2 - - # Check that run was called twice: for the partial derivatives and for - # running the circuit - assert spy2.call_count == 2 - - -@pytest.mark.parametrize("shots", [1000]) -def test_probability(token, tol, shots): - """Test that the probs function works.""" - IBMQ.enable_account(token) - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - dev_analytic = qml.device("default.qubit", wires=2, shots=None) - - x = [0.2, 0.5] - - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[0, 1]) - - prob = qml.QNode(circuit, dev) - prob_analytic = qml.QNode(circuit, dev_analytic) - - # Calling the hardware only once - hw_prob = prob(x) - - assert np.isclose(hw_prob.sum(), 1, **tol) - assert np.allclose(prob_analytic(x), hw_prob, **tol) - assert not np.array_equal(prob_analytic(x), hw_prob) - - -def test_track(token): - """Test that the tracker works.""" - - IBMQ.enable_account(token) - dev = IBMQDevice(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 "creating" in dev.tracker.history["job_time"][0] - assert "validating" in dev.tracker.history["job_time"][0] - assert "queued" in dev.tracker.history["job_time"][0] - assert "running" in dev.tracker.history["job_time"][0] - assert len(dev.tracker.history["job_time"][0]) == 4 + def test_account_error(self, monkeypatch): + """Test that an error is raised if there is no active IBMQ account.""" + + # Token is passed such that the test is skipped if no token was provided + with pytest.raises(IBMAccountError, match="No active IBM Q account"): + with monkeypatch.context() as m: + m.delenv("IBMQX_TOKEN", raising=False) + IBMQDevice(wires=1) + + +@pytest.mark.usefixtures("skip_if_no_account") +class TestIBMQWithRealAccount: + """Tests that require an active IBMQ account.""" + + def test_load_from_env_multiple_device(self): + """Test creating multiple IBMQ devices when the environment variable + for the IBMQ token was set.""" + dev1 = IBMQDevice(wires=1) + dev2 = IBMQDevice(wires=1) + assert dev1 is not dev2 + + @pytest.mark.parametrize("shots", [1000]) + def test_simple_circuit(self, tol, shots): + """Test executing a simple circuit submitted to IBMQ.""" + dev = IBMQDevice(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", [1000]) + def test_simple_circuit_with_batch_params(self, tol, shots, mocker): + """Test that executing a simple circuit with batched parameters is + submitted to IBMQ once.""" + dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + + @qml.batch_params(all_operations=True) + @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)) + + # Check that we run only once + spy1 = mocker.spy(dev, "batch_execute") + spy2 = mocker.spy(dev.backend, "run") + + # Batch the input parameters + batch_dim = 3 + theta = np.linspace(0, 0.543, batch_dim) + phi = np.linspace(0, 0.123, batch_dim) + + res = circuit(theta, phi) + assert np.allclose(res[0], np.cos(theta), **tol) + assert np.allclose(res[1], np.cos(theta) * np.cos(phi), **tol) + + # Check that IBMQBackend.run was called once + assert spy1.call_count == 1 + assert spy2.call_count == 1 + + @pytest.mark.parametrize("shots", [1000]) + def test_batch_execute_parameter_shift(self, tol, shots, mocker): + """Test that devices provide correct result computing the gradient of a + circuit using the parameter-shift rule and the batch execution pipeline.""" + dev = IBMQDevice(wires=3, backend="ibmq_qasm_simulator", shots=shots) + + spy1 = mocker.spy(dev, "batch_execute") + spy2 = mocker.spy(dev.backend, "run") + + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(x, y): + qml.RX(x, wires=[0]) + qml.RY(y, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2)) + + x = qml.numpy.array(0.543, requires_grad=True) + y = qml.numpy.array(0.123, requires_grad=True) + + res = qml.grad(circuit)(x, y) + expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) + assert np.allclose(res, expected, **tol) + + # Check that QiskitDevice.batch_execute was called once + assert spy1.call_count == 2 + + # Check that run was called twice: for the partial derivatives and for + # running the circuit + assert spy2.call_count == 2 + + @pytest.mark.parametrize("shots", [1000]) + def test_probability(self, tol, shots): + """Test that the probs function works.""" + dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) + dev_analytic = qml.device("default.qubit", wires=2, shots=None) + + x = [0.2, 0.5] + + def circuit(x): + qml.RX(x[0], wires=0) + qml.RY(x[1], wires=0) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[0, 1]) + + prob = qml.QNode(circuit, dev) + prob_analytic = qml.QNode(circuit, dev_analytic) + + # Calling the hardware only once + hw_prob = prob(x) + + assert np.isclose(hw_prob.sum(), 1, **tol) + assert np.allclose(prob_analytic(x), hw_prob, **tol) + assert not np.array_equal(prob_analytic(x), hw_prob) + + def _test_track(self): # TODO: fix tracker + """Test that the tracker works.""" + dev = IBMQDevice(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 "creating" in dev.tracker.history["job_time"][0] + assert "validating" in dev.tracker.history["job_time"][0] + assert "queued" in dev.tracker.history["job_time"][0] + assert "running" in dev.tracker.history["job_time"][0] + assert len(dev.tracker.history["job_time"][0]) == 4 diff --git a/tests/test_integration.py b/tests/test_integration.py index 9b6f4947e..2bcb798ce 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -5,14 +5,13 @@ from pennylane.numpy import tensor import pytest import qiskit -import qiskit.providers.aer as aer +import qiskit_aer as aer -from pennylane_qiskit import AerDevice, BasicAerDevice from pennylane_qiskit.qiskit_device import QiskitDevice from conftest import state_backends -pldevices = [("qiskit.aer", qiskit.Aer), ("qiskit.basicaer", qiskit.BasicAer)] +pldevices = [("qiskit.aer", aer.Aer), ("qiskit.basicaer", qiskit.BasicAer)] class TestDeviceIntegration: diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 1f48b6f03..e865a8a47 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -4,7 +4,7 @@ import pennylane as qml from pennylane_qiskit import AerDevice from pennylane_qiskit.qiskit_device import QiskitDevice -import qiskit.providers.aer.noise as noise +from qiskit_aer import noise test_transpile_options = [ {}, diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 6f77d8e0a..b9eefa1ef 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -19,14 +19,11 @@ import pennylane as qml import pytest -from qiskit import IBMQ - from pennylane_qiskit import IBMQCircuitRunnerDevice, IBMQSamplerDevice from pennylane_qiskit.vqe_runtime_runner import vqe_runner, hamiltonian_to_list_string -pytest.mark.usefixtures("skip_if_no_account") - +@pytest.mark.usefixtures("skip_if_no_account") class TestCircuitRunner: """Test class for the circuit runner IBMQ runtime device.""" @@ -134,6 +131,7 @@ def circuit(): assert len(dev.tracker.history["job_time"][0]) == 1 +@pytest.mark.usefixtures("skip_if_no_account") class TestSampler: """Test class for the sampler IBMQ runtime device.""" @@ -230,19 +228,10 @@ def circuit(): assert len(dev.tracker.history) == 2 +@pytest.mark.usefixtures("skip_if_no_account") 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.PauliY(0) @ qml.PauliZ(1)] - - hamiltonian = qml.Hamiltonian(coeffs, obs) - result = hamiltonian_to_list_string(hamiltonian, hamiltonian.wires) - - assert [("XIX", 1), ("YZI", 1)] == result - @pytest.mark.parametrize("shots", [8000]) def test_simple_hamiltonian(self, tol, shots): """Test a simple VQE problem with Hamiltonian and a circuit from PennyLane""" @@ -602,33 +591,6 @@ def vqe_circuit(params): assert "function" in job.intermediate_results assert "step" in job.intermediate_results - @pytest.mark.parametrize("shots", [8000]) - def test_hamiltonian_format(self, shots): - """Test that a PennyLane Hamiltonian is required.""" - - def vqe_circuit(params): - qml.RX(params[0], wires=0) - qml.RX(params[1], wires=1) - - hamiltonian = qml.PauliZ(wires=0) - - with pytest.raises( - qml.QuantumFunctionError, match="A PennyLane Hamiltonian object is required." - ): - vqe_runner( - 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", - }, - ) - @pytest.mark.parametrize("shots", [8000]) def test_hamiltonian_tensor(self, shots): """Test that we can handle tensor Hamiltonians.""" @@ -877,3 +839,42 @@ def test_qnspsa_disabled(self): x0=[3.97507603, 3.00854038], optimizer="QNSPSA", ) + + +def test_hamiltonian_to_list_string(): + """Test the function that transforms a PennyLane Hamiltonian to a list string Hamiltonian.""" + coeffs = [1, 1] + obs = [qml.PauliX(0) @ qml.PauliX(2), qml.PauliY(0) @ qml.PauliZ(1)] + + hamiltonian = qml.Hamiltonian(coeffs, obs) + result = hamiltonian_to_list_string(hamiltonian, hamiltonian.wires) + + assert [("XIX", 1), ("YZI", 1)] == result + + +@pytest.mark.parametrize("shots", [8000]) +def test_hamiltonian_format(shots): + """Test that a PennyLane Hamiltonian is required.""" + + def vqe_circuit(params): + qml.RX(params[0], wires=0) + qml.RX(params[1], wires=1) + + hamiltonian = qml.PauliZ(wires=0) + + with pytest.raises( + qml.QuantumFunctionError, match="A PennyLane Hamiltonian object is required." + ): + vqe_runner( + 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", + }, + ) From e8f58ef54c281c9bfe87bfa3d67f461fd377d524 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 16:22:11 -0400 Subject: [PATCH 05/11] update docs --- README.rst | 5 ++++- doc/devices/aer.rst | 4 ++-- doc/devices/ibmq.rst | 15 ++++++++++----- doc/devices/runtime.rst | 3 +-- pennylane_qiskit/aer.py | 6 +++--- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index a2105ea94..61a043b0f 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,10 @@ in the source folder. Tests restricted to a specific provider can be run by exec only be run if a ``ibmqx_token`` for the `IBM Q experience `_ is configured in the `PennyLane configuration file - `_. + `_, if the token is + exported in your environment under the name ``IBMQX_TOKEN``, or if you have previously saved your + account credentials using the + `new IBMProvider `_ If this is the case, running ``make test`` also executes tests on the ``ibmq`` device. By default tests on the ``ibmq`` device run with ``ibmq_qasm_simulator`` backend diff --git a/doc/devices/aer.rst b/doc/devices/aer.rst index 4bd6217fb..6f65153d6 100644 --- a/doc/devices/aer.rst +++ b/doc/devices/aer.rst @@ -45,7 +45,7 @@ or, alternatively, .. code-block:: python - from qiskit import Aer + from qiskit_aer import Aer Aer.backends() .. note:: @@ -103,7 +103,7 @@ which you can instantiate and apply to the device as follows import pennylane as qml import qiskit - import qiskit.providers.aer.noise as noise + from qiskit_aer import noise # Error probabilities prob_1 = 0.001 # 1-qubit gate diff --git a/doc/devices/ibmq.rst b/doc/devices/ibmq.rst index 62d2ebcbb..880828e54 100644 --- a/doc/devices/ibmq.rst +++ b/doc/devices/ibmq.rst @@ -15,8 +15,8 @@ IBM Q account. If the device finds no account it will raise an error: 'No active IBM Q account, and no IBM Q token provided. -You can use the ``qiskit.IBMQ.save_account("")`` function to permanently store an account, -and the ``qiskit.IBMQ.load_account()`` function to load the stored account in a given session. +You can use the ``qiskit_ibm_provider.IBMProvider.save_account("")`` function to permanently +store an account, and the account will be automatically used from then onward. Alternatively, you can specify the token with PennyLane via the `PennyLane configuration file `__ by adding the section @@ -34,6 +34,11 @@ You may also directly pass your IBM Q API token to the device: dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', ibmqx_token="XXX") +You may also save your token as an environment variable by running the following in a terminal: + +.. code:: + + export IBMQX_TOKEN= .. warning:: Never publish code containing your token online. @@ -58,8 +63,8 @@ Custom providers can be passed as arguments when a ``qiskit.ibmq`` device is cre .. code-block:: python - from qiskit import IBMQ - provider = IBMQ.enable_account('XYZ') + from qiskit_ibm_provider import IBMProvider + provider = IBMProvider("XYZ") import pennylane as qml dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', provider=provider) @@ -76,4 +81,4 @@ Custom provider options can also be passed as keyword arguments when creating a ibmqx_token='XXX', hub='MYHUB', group='MYGROUP', project='MYPROJECT') More details on Qiskit providers can be found -in the `IBMQ provider documentation `_. +in the `IBMQ provider documentation `_. diff --git a/doc/devices/runtime.rst b/doc/devices/runtime.rst index 64cfc007c..da9197d4d 100644 --- a/doc/devices/runtime.rst +++ b/doc/devices/runtime.rst @@ -29,8 +29,6 @@ We created a wrapper to use PennyLane objects while solving VQE problems on IBM from pennylane_qiskit import vqe_runner - IBMQ.enable_account(token) - def vqe_circuit(params): qml.RX(params[0], wires=0) qml.RY(params[1], wires=0) @@ -38,6 +36,7 @@ We created a wrapper to use PennyLane objects while solving VQE problems on IBM coeffs = [1, 1] obs = [qml.PauliX(0), qml.PauliZ(0)] hamiltonian = qml.Hamiltonian(coeffs, obs) + shots = 8000 job = vqe_runner( backend="ibmq_qasm_simulator", diff --git a/pennylane_qiskit/aer.py b/pennylane_qiskit/aer.py index ae1f06549..6f36d7a9e 100644 --- a/pennylane_qiskit/aer.py +++ b/pennylane_qiskit/aer.py @@ -31,7 +31,7 @@ class AerDevice(QiskitDevice): a range of transpile options can be given as kwargs. For more information on backends, please visit the - `Aer provider documentation `_. + `qiskit_aer documentation `_. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, @@ -39,8 +39,8 @@ class AerDevice(QiskitDevice): or strings (``['ancilla', 'q1', 'q2']``). backend (str): the desired backend method (str): The desired simulation method. A list of supported simulation - methods can be returned using ``qiskit.Aer.available_methods()``, or by referring - to the ``AerSimulator`` `documentation `__. + methods can be returned using ``qiskit_aer.AerSimulator().available_methods()``, or by referring + to the ``AerSimulator`` `documentation `__. shots (int or None): number of circuit evaluations/random samples used to estimate expectation values and variances of observables. For statevector backends, setting to ``None`` results in computing statistics like expectation values and variances analytically. From 111ccbddbecec850fabf97e8eddff2c84c29f650 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 16:33:35 -0400 Subject: [PATCH 06/11] replace remaining qiskit.providers.aer stuff from docs --- doc/devices/aer.rst | 2 +- doc/index.rst | 2 +- pennylane_qiskit/aer.py | 2 +- pennylane_qiskit/ibmq.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/devices/aer.rst b/doc/devices/aer.rst index 6f65153d6..b9ad981eb 100644 --- a/doc/devices/aer.rst +++ b/doc/devices/aer.rst @@ -84,7 +84,7 @@ The options are set via additional keyword arguments: ) For more information on available methods and their options, please visit the `AerSimulator -documentation `_. +documentation `_. .. warning:: diff --git a/doc/index.rst b/doc/index.rst index 4dc3de87a..87441c8e5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -67,7 +67,7 @@ follows: PennyLane chooses the ``qasm_simulator`` as the default backend if no backend is specified. For more details on the ``qasm_simulator``, including available backend options, see -`Qiskit Qasm Simulator documentation `_. +`Qiskit Qasm Simulator documentation `_. Tutorials ~~~~~~~~~ diff --git a/pennylane_qiskit/aer.py b/pennylane_qiskit/aer.py index 6f36d7a9e..24e1a3536 100644 --- a/pennylane_qiskit/aer.py +++ b/pennylane_qiskit/aer.py @@ -49,7 +49,7 @@ class AerDevice(QiskitDevice): name (str): The name of the circuit. Default ``'circuit'``. compile_backend (BaseBackend): The backend used for compilation. If you wish to simulate a device compliant circuit, you can specify a backend here. - noise_model (NoiseModel): NoiseModel Object from ``qiskit.providers.aer.noise`` + noise_model (NoiseModel): NoiseModel Object from ``qiskit_aer.noise`` """ # pylint: disable=too-many-arguments diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index b912bd655..c9c2cc582 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -52,7 +52,7 @@ class IBMQDevice(QiskitDevice): variable ``IBMQX_TOKEN`` is used. ibmqx_url (str): The IBM Q URL. If not provided, the environment variable ``IBMQX_URL`` is used, followed by the default URL. - noise_model (NoiseModel): NoiseModel Object from ``qiskit.providers.aer.noise``. + noise_model (NoiseModel): NoiseModel Object from ``qiskit_aer.noise``. Only applicable for simulator backends. hub (str): Name of the provider hub. group (str): Name of the provider group. From 1e88d45988358240cbe3c0aadbb94d4dace9a1b4 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 16:40:29 -0400 Subject: [PATCH 07/11] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25236d390..0df48db6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ now been removed. The `upload_vqe_runner` and `delete_vqe_runner` functions have also been removed. [(#298)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/298) +### Improvements + +* Updated many small things across the plugin to match re-works and deprecations in `qiskit`. The plugin + can still be used in the same way as before. However, we suggest you authenticate with + `qiskit_ibm_provider.IBMProvider` instead of `qiskit.IBMQ` from now on, as the latter is deprecated. + [(#301)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/301) + ### Contributors This release contains contributions from (in alphabetical order): From 700521512d9700b7cf494060f97a624cd78827c6 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 17:44:22 -0400 Subject: [PATCH 08/11] fix tracker and tests; ensure correct meta-keys are received --- pennylane_qiskit/ibmq.py | 17 +++++++++++------ tests/test_ibmq.py | 29 +++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index c9c2cc582..6ccd38744 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -21,6 +21,7 @@ from qiskit_ibm_provider import IBMProvider from qiskit_ibm_provider.exceptions import IBMAccountError from qiskit_ibm_provider.accounts.exceptions import AccountsError +from qiskit_ibm_provider.job import IBMJobError from .qiskit_device import QiskitDevice @@ -84,14 +85,18 @@ def batch_execute(self, circuits): # pragma: no cover def _track_run(self): # pragma: no cover """Provide runtime information.""" + expected_keys = {"created", "running", "finished"} time_per_step = self._current_job.time_per_step() + if not set(time_per_step).issuperset(expected_keys): + self._current_job.wait_for_final_state(timeout=60) # seconds + self._current_job.refresh() + time_per_step = self._current_job.time_per_step() + if not set(time_per_step).issuperset(expected_keys): + raise IBMJobError(f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}") + job_time = { - "creating": (time_per_step["CREATED"] - time_per_step["CREATING"]).total_seconds(), - "validating": ( - time_per_step["VALIDATED"] - time_per_step["VALIDATING"] - ).total_seconds(), - "queued": (time_per_step["RUNNING"] - time_per_step["QUEUED"]).total_seconds(), - "running": (time_per_step["COMPLETED"] - time_per_step["RUNNING"]).total_seconds(), + "queued": (time_per_step["running"] - time_per_step["created"]).total_seconds(), + "running": (time_per_step["finished"] - time_per_step["running"]).total_seconds(), } self.tracker.update(job_time=job_time) self.tracker.record() diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py index 2524982c1..6dc63d25a 100644 --- a/tests/test_ibmq.py +++ b/tests/test_ibmq.py @@ -17,9 +17,11 @@ import numpy as np import pennylane as qml import pytest +from unittest.mock import patch -from qiskit_ibm_provider.exceptions import IBMAccountError from qiskit_ibm_provider import IBMProvider +from qiskit_ibm_provider.exceptions import IBMAccountError +from qiskit_ibm_provider.job import IBMJobError from pennylane_qiskit import IBMQDevice from pennylane_qiskit import ibmq @@ -275,7 +277,7 @@ def circuit(x): assert np.allclose(prob_analytic(x), hw_prob, **tol) assert not np.array_equal(prob_analytic(x), hw_prob) - def _test_track(self): # TODO: fix tracker + def test_track(self): """Test that the tracker works.""" dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) dev.tracker.active = True @@ -288,9 +290,20 @@ def circuit(): circuit() assert "job_time" in dev.tracker.history - if "job_time" in dev.tracker.history: - assert "creating" in dev.tracker.history["job_time"][0] - assert "validating" in dev.tracker.history["job_time"][0] - assert "queued" in dev.tracker.history["job_time"][0] - assert "running" in dev.tracker.history["job_time"][0] - assert len(dev.tracker.history["job_time"][0]) == 4 + assert set(dev.tracker.history["job_time"][0]) == {"queued", "running"} + + @patch("qiskit_ibm_provider.job.ibm_circuit_job.IBMCircuitJob.time_per_step", return_value={"CREATING": "1683149330"}) + def test_track_fails_with_unexpected_metadata(self, mock_time_per_step): + """Tests that the tracker fails when it doesn't get the required metadata.""" + dev = IBMQDevice(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) + + with pytest.raises(IBMJobError, match="time_per_step had keys"): + circuit() + + assert mock_time_per_step.call_count == 2 From 65134206f92e346f5154f3068924b0c8854ba9b6 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Wed, 3 May 2023 17:45:36 -0400 Subject: [PATCH 09/11] black --- pennylane_qiskit/ibmq.py | 4 +++- tests/test_ibmq.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index 6ccd38744..f3686ae9e 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -92,7 +92,9 @@ def _track_run(self): # pragma: no cover self._current_job.refresh() time_per_step = self._current_job.time_per_step() if not set(time_per_step).issuperset(expected_keys): - raise IBMJobError(f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}") + raise IBMJobError( + f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}" + ) job_time = { "queued": (time_per_step["running"] - time_per_step["created"]).total_seconds(), diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py index 6dc63d25a..2655f9200 100644 --- a/tests/test_ibmq.py +++ b/tests/test_ibmq.py @@ -292,7 +292,10 @@ def circuit(): assert "job_time" in dev.tracker.history assert set(dev.tracker.history["job_time"][0]) == {"queued", "running"} - @patch("qiskit_ibm_provider.job.ibm_circuit_job.IBMCircuitJob.time_per_step", return_value={"CREATING": "1683149330"}) + @patch( + "qiskit_ibm_provider.job.ibm_circuit_job.IBMCircuitJob.time_per_step", + return_value={"CREATING": "1683149330"}, + ) def test_track_fails_with_unexpected_metadata(self, mock_time_per_step): """Tests that the tracker fails when it doesn't get the required metadata.""" dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) From 3f44b8427e1c95538c3072457657cf9e5308089f Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Thu, 4 May 2023 15:21:47 -0400 Subject: [PATCH 10/11] add runtime_secs to IBMQDevice --- pennylane_qiskit/ibmq.py | 24 +++++++++++++++++++----- pennylane_qiskit/qiskit_device.py | 4 ++-- tests/test_ibmq.py | 12 +++++++++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py index f3686ae9e..8f088abf7 100644 --- a/pennylane_qiskit/ibmq.py +++ b/pennylane_qiskit/ibmq.py @@ -47,6 +47,8 @@ class IBMQDevice(QiskitDevice): backend (str): the desired provider backend shots (int): number of circuit evaluations/random samples used to estimate expectation values and variances of observables + timeout_secs (int): A timeout value in seconds to wait for job results from an IBMQ backend. + The default value of ``None`` means no timeout Keyword Args: ibmqx_token (str): The IBM Q API token. If not provided, the environment @@ -62,7 +64,15 @@ class IBMQDevice(QiskitDevice): short_name = "qiskit.ibmq" - def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): + def __init__( + self, + wires, + provider=None, + backend="ibmq_qasm_simulator", + shots=1024, + timeout_secs=None, + **kwargs, + ): # pylint:disable=too-many-arguments # Connection to IBMQ connect(kwargs) @@ -75,9 +85,10 @@ def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=10 p = provider or IBMProvider(instance=instance) super().__init__(wires=wires, provider=p, backend=backend, shots=shots, **kwargs) + self.timeout_secs = timeout_secs - def batch_execute(self, circuits): # pragma: no cover - res = super().batch_execute(circuits) + def batch_execute(self, circuits): # pragma: no cover, pylint:disable=arguments-differ + res = super().batch_execute(circuits, timeout=self.timeout_secs) if self.tracker.active: self._track_run() return res @@ -88,12 +99,15 @@ def _track_run(self): # pragma: no cover expected_keys = {"created", "running", "finished"} time_per_step = self._current_job.time_per_step() if not set(time_per_step).issuperset(expected_keys): - self._current_job.wait_for_final_state(timeout=60) # seconds + # self._current_job.result() should have already run by now + # tests see a race condition, so this is ample time for that case + timeout_secs = self.timeout_secs or 60 + self._current_job.wait_for_final_state(timeout=timeout_secs) self._current_job.refresh() time_per_step = self._current_job.time_per_step() if not set(time_per_step).issuperset(expected_keys): raise IBMJobError( - f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}" + f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}. If your program takes a long time, you may want to configure the device with a higher `timeout_secs`" ) job_time = { diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index 95cbde7af..2b559e246 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -459,14 +459,14 @@ def compile_circuits(self, circuits): return compiled_circuits - def batch_execute(self, circuits): + def batch_execute(self, circuits, timeout: int = None): # 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() + result = self._current_job.result(timeout=timeout) # increment counter for number of executions of qubit device self._num_executions += 1 diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py index 2655f9200..ec7880e54 100644 --- a/tests/test_ibmq.py +++ b/tests/test_ibmq.py @@ -21,7 +21,7 @@ from qiskit_ibm_provider import IBMProvider from qiskit_ibm_provider.exceptions import IBMAccountError -from qiskit_ibm_provider.job import IBMJobError +from qiskit_ibm_provider.job import IBMJobError, IBMCircuitJob from pennylane_qiskit import IBMQDevice from pennylane_qiskit import ibmq @@ -296,9 +296,13 @@ def circuit(): "qiskit_ibm_provider.job.ibm_circuit_job.IBMCircuitJob.time_per_step", return_value={"CREATING": "1683149330"}, ) - def test_track_fails_with_unexpected_metadata(self, mock_time_per_step): + @pytest.mark.parametrize("timeout", [None, 120]) + def test_track_fails_with_unexpected_metadata(self, mock_time_per_step, timeout, mocker): """Tests that the tracker fails when it doesn't get the required metadata.""" - dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) + batch_execute_spy = mocker.spy(ibmq.QiskitDevice, "batch_execute") + wait_spy = mocker.spy(IBMCircuitJob, "wait_for_final_state") + + dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1, timeout_secs=timeout) dev.tracker.active = True @qml.qnode(dev) @@ -310,3 +314,5 @@ def circuit(): circuit() assert mock_time_per_step.call_count == 2 + batch_execute_spy.assert_called_with(dev, mocker.ANY, timeout=timeout) + wait_spy.assert_called_with(mocker.ANY, timeout=timeout or 60) From f9b3d9129fe9df9987f030590d81ab881b29f639 Mon Sep 17 00:00:00 2001 From: Matthew Silverman Date: Thu, 4 May 2023 15:22:05 -0400 Subject: [PATCH 11/11] fix quasi_dists in Sampler.generate_samples --- pennylane_qiskit/runtime_devices.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py index 205093918..4fdff3d73 100644 --- a/pennylane_qiskit/runtime_devices.py +++ b/pennylane_qiskit/runtime_devices.py @@ -200,13 +200,14 @@ def generate_samples(self, circuit_id=None): Returns: array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` """ - counts = self._current_job.quasi_dists[circuit_id] + counts = self._current_job.quasi_dists[circuit_id].binary_probabilities() keys = list(counts.keys()) - if isinstance(keys[0], str): - for i, elem in enumerate(keys): - keys[i] = int(elem, 2) - number_of_states = len(keys) + number_of_states = 2 ** len(keys[0]) + + # Convert state to int + for i, elem in enumerate(keys): + keys[i] = int(elem, 2) values = list(counts.values()) states, probs = zip(*sorted(zip(keys, values)))