From 852783a9e093c4f43e286cd15b434fae2b654455 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:21:19 -0400 Subject: [PATCH 01/47] New Qiskit device prototype (#350) * initial prototype device * add use_primitives kwarg * reorganize circuit conversion part 1 * move circuit translation out of device * estimator execution * some small improvements for codefactor * allow circuits with mixed MP types * move translation functions * add support for broadcast_expand and session * add kwargs for options * tidy up options and session * warn if using non-primitive measurements * remove context manager for device session * refactor handling of Options and kwargs * cleaning up * fix 'c register already exists' hardware error * change Options update interface * don't allow options to override shots * don't use classical reg in estimator circuits * update docstring * add conversion tests * add observable conversion test * fix bug in PauliOps converter * add more conversion test functions * remove Adjoint from supported ops and tidy up * tests and little fixes * add tests and black formatting * fix wire order bug for Estimator returns * add tests * remove error if device doesn't initialize * try to get CI to run * more CI stuff * try a thing * black formatting * fix typo * add missing skip-if-no-account * update converter tests * add mockers to allow tests to run in CI * temporarily comment out integration tests * get service from backend * add mock service to mock backend * mock calls to Session in unit testing * uncomment the other tests again * black formatting * newer black formatting * add mocked tests for main execute method * add MockSession to mocked execution tests * pylint * add backend to MockSession calls * mock tests for _execute methods * black formatting * mock transpile for execute_runtime_service * add name to MockedBackend * Apply suggestions from code review Co-authored-by: Matthew Silverman Co-authored-by: Astral Cai * Add barrier to ops list * apply suggestion from code review Co-authored-by: Astral Cai * revert adding barrier for now * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Small performance change * Revert * pin qiskit for now * Pin qiskit-ibm-runtime * Move function in init to update_kwargs * Delete print statement lol * fix for shot information * name property change * fixes * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Edited docstrings * Function signature * Shots are now with context manager * black reformat * black reformat * test changes * tests * fixed conflicts * Change mp_to_pauli to accept mps that affect more than 1 qubit * Revert to fix CI tests * black reformat * Update setup.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update requirements-ci.txt Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * tests * Fixed a test * maybe this works? * placeholder settings * fixed CI tests, bandaid fix * gitignore for 1.0 qiskit versioning * delete venv1 * tests, shots information, shot vector case, ibm_run_time compatibility * woops * docstring for operation_to_qiskit * black * appease codecov * mock test * Deleted comment * remove some notebooks * Update .gitignore Co-authored-by: Astral Cai * tests and docstrings --------- Co-authored-by: Matthew Silverman Co-authored-by: Astral Cai Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Austin Huang Co-authored-by: Astral Cai --- .DS_Store | Bin 0 -> 8196 bytes pennylane_qiskit/converter.py | 127 +++- pennylane_qiskit/qiskit_device2.py | 628 ++++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- tests/test_base_device.py | 1076 ++++++++++++++++++++++++++++ tests/test_converter.py | 218 +++++- 7 files changed, 2045 insertions(+), 8 deletions(-) create mode 100644 .DS_Store create mode 100644 pennylane_qiskit/qiskit_device2.py create mode 100644 tests/test_base_device.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0622312c937da06b2686d5d54f1dbed3a15b3700 GIT binary patch literal 8196 zcmeHM%We}f6g_TJ5zz`of<*u!W(kW#>K~*f0V0)$2>X)9P^#ud9xdIV+4B$l0^i39 z&b_uOduAepDxpf)k$onfd#W1gOy&K1jN(^J^yq}0UvE?wcXz4JP4&zofu0k=o zI(dnj!?Y~MF)9!hC@LVe`wj+J;}qkdewPo@0ekb8Hl^<28D4VsHC~{DBfKFtF)8qV z58wT}IMj96fY$)<1LBRlGtUXGrl;_n3zE zTPHdDUE0eIyLiqOdU($ro?;rZRGIIdGBwP(!j!K|>ewfyIL2q)^%=+9&LIl<{QG!& z#Xl7znG3wsHhL-NQ{#YN 0: + angle += 0.3 + res = circuit(angle)[0] + """ + # Code to acquire session: + existing_session = device._session + session = Session(backend=device.backend) + device._session = session + try: + yield session + finally: + # Code to release session: + session.close() + device._session = existing_session + + +def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether or not a measurement is accepted when sampling.""" + + return isinstance( + m, + ( + qml.measurements.SampleMeasurement, + qml.measurements.ClassicalShadowMP, + qml.measurements.ShadowExpvalMP, + ), + ) + + +@transform +def split_measurement_types( + tape: qml.tape.QuantumTape, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Split into separate tapes based on measurement type. Counts will use the + Qiskit Sampler, ExpectationValue and Variance will use the Estimator, and other + strictly sample-based measurements will use the standard backend.run function""" + + estimator = [] + sampler = [] + no_prim = [] + + for i, mp in enumerate(tape.measurements): + if isinstance(mp, (ExpectationMP, VarianceMP)): + estimator.append((mp, i)) + elif isinstance(mp, ProbabilityMP): + sampler.append((mp, i)) + else: + no_prim.append((mp, i)) + + order_indices = [[i for mp, i in group] for group in [estimator, sampler, no_prim]] + + tapes = [] + if estimator: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in estimator], + shots=tape.shots, + ) + ] + ) + if sampler: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in sampler], + shots=tape.shots, + ) + ] + ) + if no_prim: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in no_prim], + shots=tape.shots, + ) + ] + ) + + def reorder_fn(res): + """re-order the output to the original shape and order""" + + flattened_indices = [i for group in order_indices for i in group] + flattened_results = [r for group in res for r in group] + + result = dict(zip(flattened_indices, flattened_results)) + + return tuple(result[i] for i in sorted(result.keys())) + + return tapes, reorder_fn + + +def qiskit_options_to_flat_dict(options): + """Create a dictionary from a Qiskit Options object""" + # this will break (or at least overwrite potentially relevant information) + # if they name things in some categories on Options the same as things in + # other categories, but at that point they've really departed from the kwarg API + options_dict = {} + for key, val in vars(options).items(): + if hasattr(val, "__dict__"): + options_dict.update(qiskit_options_to_flat_dict(val)) + elif val is not None: + options_dict[key] = val + return options_dict + + +class QiskitDevice2(Device): + r"""Hardware/simulator Qiskit device for PennyLane. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['aux_wire', 'q1', 'q2']``). + backend (Backend): the initialized Qiskit backend + + Keyword Args: + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. + use_primitives (bool): whether or not to use Qiskit Primitives. Defaults to False. If True, + getting expectation values and variance from the backend will use a Qiskit Estimator, + and getting probabilities will use a Qiskit Sampler. Other measurement types will continue + to return results from the backend without using a Primitive. + options (Options): a Qiskit Options object for specifying handling the Qiskit task + (transpiliation, error mitigation, execution, etc). Defaults to None. See Qiskit documentation + for more details. + session (Session): a Qiskit Session to use for device execution. If none is provided, a session will + be created at each device execution. + **kwargs: transpilation and runtime kwargs to be used for measurements without Qiskit Primitives. + If any values are defined both in ``options`` and in the remaining ``kwargs``, the value + provided in ``options`` will take precedence. These kwargs will be ignored for all Primitive-based + measurements on the device. + """ + + operations = set(QISKIT_OPERATION_MAP.keys()) + observables = { + "PauliX", + "PauliY", + "PauliZ", + "Identity", + "Hadamard", + "Hermitian", + "Projector", + } + + # pylint:disable = too-many-arguments + def __init__( + self, + wires, + backend, + shots=1024, + use_primitives=False, + options=None, + session=None, + **kwargs, + ): + if shots is None: + warnings.warn( + "Expected an integer number of shots, but received shots=None. Defaulting " + "to 1024 shots. The analytic calculation of results is not supported on " + "this device. All statistics obtained from this device are estimates based " + "on samples.", + UserWarning, + ) + + shots = 1024 + + self.options = options or Options() + if self.options.execution.shots == 4000: ## 4000 is default value in Qiskit. + self.options.execution.shots = shots + + super().__init__(wires=wires, shots=shots) + + self._backend = backend + + # ToDo: possibly things fail if this is not a QiskitRuntimeService - confirm and decide how to handle (SC 55725) + self._service = backend._service + self._use_primitives = use_primitives + self._session = session + + # initial kwargs are saved and referenced every time the kwargs used for transpilation and execution + self._init_kwargs = kwargs + # _kwargs are used instead of the Options for performing raw sample based measurements (using old Qiskit API) + # the _kwargs are a combination of information from Options and _init_kwargs + self._kwargs = None + if self.options.simulator.noise_model: + self.backend.set_options(noise_model=self.options.simulator.noise_model) + + # Perform validation against backend + b = self.backend + if len(self.wires) > int(b.configuration().n_qubits): + raise ValueError( + f"Backend '{backend}' supports maximum {b.configuration().n_qubits} wires" + ) + + self.reset() + self._update_kwargs() + + @property + def backend(self): + """The Qiskit backend object. + + Returns: + qiskit.providers.Backend: Qiskit backend object. + """ + return self._backend + + @property + def service(self): + """The QiskitRuntimeService service. + + Returns: + qiskit.qiskit_ibm_runtime.QiskitRuntimeService + """ + return self._service + + @property + def session(self): + """The QiskitRuntimeService session. + + Returns: + qiskit.qiskit_ibm_runtime.Session + """ + return self._session + + @property + def num_wires(self): + return len(self.wires) + + def update_session(self, session): + self._session = session + + def reset(self): + self._current_job = None + + def stopping_condition(self, op: qml.operation.Operator) -> bool: + """Specifies whether or not an Operator is accepted by QiskitDevice2.""" + return op.name in self.operations + + def observable_stopping_condition(self, obs: qml.operation.Operator) -> bool: + """Specifies whether or not an observable is accepted by QiskitDevice2.""" + return obs.name in self.observables + + def preprocess( + self, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Tuple[TransformProgram, ExecutionConfig]: + """This function defines the device transform program to be applied and an updated device configuration. + + Args: + execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the + parameters needed to fully describe the execution. + + Returns: + TransformProgram, ExecutionConfig: A transform program that when called returns QuantumTapes that the device + can natively execute as well as a postprocessing function to be called after execution, and a configuration with + unset specifications filled in. + + This device: + + * Supports any operations with explicit PennyLane to Qiskit gate conversions defined in the plugin + * Does not intrinsically support parameter broadcasting + + """ + config = execution_config + config.use_device_gradient = False + + transform_program = TransformProgram() + + transform_program.add_transform(validate_device_wires, self.wires, name=self.name) + transform_program.add_transform( + decompose, + stopping_condition=self.stopping_condition, + name=self.name, + skip_initial_state_prep=False, + ) + transform_program.add_transform( + validate_measurements, + sample_measurements=accepted_sample_measurement, + name=self.name, + ) + transform_program.add_transform( + validate_observables, + stopping_condition=self.observable_stopping_condition, + name=self.name, + ) + + transform_program.add_transform(broadcast_expand) + # missing: split non-commuting, sum_expand, etc. + + if self._use_primitives: + transform_program.add_transform(split_measurement_types) + + return transform_program, config + + def _update_kwargs(self): + """Combine the settings defined in options and the settings passed as kwargs, with + the definition in options taking precedence if there is conflicting information""" + option_kwargs = qiskit_options_to_flat_dict(self.options) + + overlapping_kwargs = set(self._init_kwargs).intersection(set(option_kwargs)) + if overlapping_kwargs: + warnings.warn( + f"The keyword argument(s) {overlapping_kwargs} passed to the device are also " + f"defined in the device Options. The definition in Options will be used." + ) + if option_kwargs["shots"] != self.shots.total_shots: + warnings.warn( + f"Setting shots via the Options is not supported on PennyLane devices. The shots {self.shots} " + f"passed to the device will be used." + ) + self.options.execution.shots = self.shots.total_shots + + option_kwargs.pop("shots") + kwargs = self._init_kwargs.copy() + kwargs.update(option_kwargs) + + self._kwargs = kwargs + + @staticmethod + def get_transpile_args(kwargs): + """The transpile argument setter. + + Keyword Args: + kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the + `Qiskit transpiler documentation `_ + """ + + transpile_sig = inspect.signature(transpile).parameters + + transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} + transpile_args.pop("circuits", None) + transpile_args.pop("backend", None) + + return transpile_args + + def compile_circuits(self, circuits): + r"""Compiles multiple circuits one after the other. + + Args: + circuits (list[QuantumCircuit]): the circuits to be compiled + + Returns: + list[QuantumCircuit]: the list of compiled circuits + """ + # Compile each circuit object + compiled_circuits = [] + transpile_args = self.get_transpile_args(self._kwargs) + + for i, circuit in enumerate(circuits): + compiled_circ = transpile(circuit, backend=self.backend, **transpile_args) + compiled_circ.name = f"circ{i}" + compiled_circuits.append(compiled_circ) + + return compiled_circuits + + # pylint: disable=unused-argument + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + session = self._session or Session(backend=self.backend) + + if not self._use_primitives: + results = self._execute_runtime_service(circuits, session=session) + return results + + results = [] + + if isinstance(circuits, QuantumScript): + circuits = [circuits] + + @contextmanager + def execute_circuits(session): + try: + for circ in circuits: + if circ.shots and len(circ.shots.shot_vector) > 1: + warnings.warn( + f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." + f"The circuit will be run once with {circ.shots.total_shots} shots instead." + ) + if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)): + execute_fn = self._execute_estimator + elif isinstance(circ.measurements[0], ProbabilityMP): + execute_fn = self._execute_sampler + else: + execute_fn = self._execute_runtime_service + results.append(execute_fn(circ, session)) + yield results + finally: + session.close() + + with execute_circuits(session) as results: + return results + + def _execute_runtime_service(self, circuits, session): + """Execution using old runtime_service (can't use runtime sessions)""" + # update kwargs in case Options has been modified since last execution + self._update_kwargs() + + # in case a single circuit is passed + if isinstance(circuits, QuantumScript): + circuits = [circuits] + + qcirc = [ + circuit_to_qiskit(circ, self.num_wires, diagonalize=True, measure=True) + for circ in circuits + ] + compiled_circuits = self.compile_circuits(qcirc) + + program_inputs = { + "circuits": compiled_circuits, + "shots": circuits[0].shots.total_shots or self.shots.total_shots, + } + + for kwarg, value in self._kwargs.items(): + program_inputs[kwarg] = value + + options = { + "backend": self.backend.name, + "log_level": self.options.environment.log_level, + "job_tags": self.options.environment.job_tags, + "max_execution_time": self.options.max_execution_time, + } + + # Send circuits to the cloud for execution by the circuit-runner program. + job = self.service.run( + program_id="circuit-runner", + options=options, + inputs=program_inputs, + session_id=session.session_id, + ) + self._current_job = job.result(decoder=RunnerResult) + + results = [] + + for index, circuit in enumerate(circuits): + self._samples = self.generate_samples(index) + res = [ + mp.process_samples(self._samples, wire_order=self.wires) + for mp in circuit.measurements + ] + single_measurement = len(circuit.measurements) == 1 + res = res[0] if single_measurement else tuple(res) + results.append(res) + + return tuple(results) + + def _execute_sampler(self, circuit, session): + """Execution for the Sampler primitive""" + + qcirc = circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True) + if circuit.shots: + self.options.execution.shots = circuit.shots.total_shots + sampler = Sampler(session=session, options=self.options) + + result = sampler.run(qcirc).result() + self._current_job = result + + # needs processing function to convert to the correct format for states, and + # also handle instances where wires were specified in probs, and for multiple probs measurements + # single_measurement = len(circuit.measurements) == 1 + # res = (res[0], ) if single_measurement else tuple(res) + + return (result.quasi_dists[0],) + + def _execute_estimator(self, circuit, session): + # the Estimator primitive takes care of diagonalization and measurements itself, + # so diagonalizing gates and measurements are not included in the circuit + qcirc = circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False) + if circuit.shots: + self.options.execution.shots = circuit.shots.total_shots + estimator = Estimator(session=session, options=self.options) + + # split into one call per measurement + # could technically be more efficient if there are some observables where we ask + # for expectation value and variance on the same observable, but spending time on + # that right now feels excessive + + # ToDo: need to sort differently for cases where the observable is not + # compatible with a SparsePauliOp representation + pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] + result = estimator.run([qcirc] * len(pauli_observables), pauli_observables).result() + self._current_job = result + result = self._process_estimator_job(circuit.measurements, result) + + return result + + @staticmethod + def _process_estimator_job(measurements, job_result): + """Estimator returns both expectation value and variance for each observable measured, + along with some metadata. Extract the relevant number for each measurement process and + return the requested results from the Estimator executions.""" + + expvals = job_result.values + variances = [res["variance"] for res in job_result.metadata] + + result = [] + for i, mp in enumerate(measurements): + if isinstance(mp, ExpectationMP): + result.append(expvals[i]) + elif isinstance(mp, VarianceMP): + result.append(variances[i]) + + single_measurement = len(measurements) == 1 + result = (result[0],) if single_measurement else tuple(result) + + return result + + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. + + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. + + Args: + circuit (int): position of the circuit in the batch. + + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + counts = self._current_job.get_counts() + # Batch of circuits + if not isinstance(counts, dict): + counts = self._current_job.get_counts()[circuit] + + samples = [] + for key, value in counts.items(): + samples.extend([key] * value) + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) diff --git a/requirements.txt b/requirements.txt index 7e9803fce..c2ee3b9f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ qiskit-aer==0.13.3 qiskit-ibm-runtime==0.20.0 qiskit-ibm-provider==0.10.0 qiskit-ignis==0.7.1 -qiskit-terra==0.46.0 +qiskit-terra==0.45.3 requests==2.31.0 requests-ntlm==1.2.0 retworkx==0.14.1 diff --git a/setup.py b/setup.py index 024750016..2ad85947b 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,8 @@ requirements = [ "qiskit>=0.32", "qiskit-aer", - "qiskit-ibm-provider", "qiskit-ibm-runtime", + "qiskit-ibm-provider", "pennylane>=0.30", "numpy", "networkx>=2.2", diff --git a/tests/test_base_device.py b/tests/test_base_device.py new file mode 100644 index 000000000..77ee617f1 --- /dev/null +++ b/tests/test_base_device.py @@ -0,0 +1,1076 @@ +# Copyright 2021-2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains tests for the base Qiskit device for the new PennyLane device API +""" + +import numpy as np +import pytest +import inspect +from unittest.mock import patch, Mock +from semantic_version import Version +import qiskit_ibm_runtime + +import pennylane as qml +from pennylane.tape.qscript import QuantumScript + +from pennylane_qiskit import AerDevice +from pennylane_qiskit.qiskit_device2 import ( + QiskitDevice2, + qiskit_session, + accepted_sample_measurement, + split_measurement_types, + qiskit_options_to_flat_dict, +) +from pennylane_qiskit.converter import ( + circuit_to_qiskit, + mp_to_pauli, + QISKIT_OPERATION_MAP, +) + +from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator +from qiskit_ibm_runtime.options import Options +from qiskit_ibm_runtime.constants import RunnerResult + +# do not import Estimator (imported above) from qiskit.primitives - the identically +# named Estimator object has a different call signature than the remote device Estimator, +# and only runs local simulations. We need the Estimator from qiskit_ibm_runtime. They +# both use this EstimatorResults, however: +from qiskit.primitives import EstimatorResult + +from qiskit import QuantumCircuit + +from qiskit_aer.noise import NoiseModel + + +class Configuration: + def __init__(self, n_qubits): + self.n_qubits = n_qubits + self.noise_model = None + + +class MockedBackend: + def __init__(self, num_qubits=10): + self._configuration = Configuration(num_qubits) + self.options = self._configuration + self._service = "SomeServiceProvider" + self.name = "mocked_backend" + + def configuration(self): + return self._configuration + + def set_options(self, noise_model): + self.options.noise_model = noise_model + + +class MockSession: + def __init__(self, backend, max_time=None): + self.backend = backend + self.max_time = max_time + self.session_id = "123" + + def close(self): # This is just to appease a test + pass + + +try: + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.backend("ibmq_qasm_simulator") +except: + backend = MockedBackend() + +test_dev = QiskitDevice2(wires=5, backend=backend) + + +def options_for_testing(): + """Creates an Options object with defined values in multiple sub-categories""" + options = Options() + options.environment.job_tags = ["getting angle"] + options.resilience.noise_amplifier = "LocalFoldingAmplifier" + options.optimization_level = 2 + options.resilience_level = 1 + # options.simulator.noise_model = None + return options + + +class TestDeviceInitialization: + @pytest.mark.parametrize("use_primitives", [True, False]) + def test_use_primitives_kwarg(self, use_primitives): + """Test the _use_primitives attribute is set on initialization""" + dev = QiskitDevice2(wires=2, backend=backend, use_primitives=use_primitives) + assert dev._use_primitives == use_primitives + + def test_no_shots_warns_and_defaults(self): + """Test that initializing with shots=None raises a warning indicating that + the device is sample based and will default to 1024 shots""" + + with pytest.warns( + UserWarning, + match="Expected an integer number of shots, but received shots=None", + ): + dev = QiskitDevice2(wires=2, backend=backend, shots=None) + + assert dev.shots.total_shots == 1024 + assert dev.options.execution.shots == 1024 + + def test_kwargs_on_initialization(self, mocker): + """Test that update_kwargs is called on intialization and combines the Options + and kwargs as self._kwargs""" + + options = Options() + options.environment.job_tags = ["my_tag"] + + spy = mocker.spy(QiskitDevice2, "_update_kwargs") + + dev = QiskitDevice2( + wires=2, + backend=backend, + options=options, + random_kwarg1=True, + random_kwarg2="a", + ) + + spy.assert_called_once() + + # kwargs are updated to a combination of the information from the Options and kwargs + assert dev._kwargs == { + "random_kwarg1": True, + "random_kwarg2": "a", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + "job_tags": ["my_tag"], + } + + # initial kwargs are saved without modification + assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} + + def test_backend_wire_validation(self): + """Test that the an error is raised if the number of device wires exceeds + the number of wires available on the backend""" + + with pytest.raises(ValueError, match="supports maximum"): + dev = QiskitDevice2(wires=500, backend=backend) + + def test_setting_simulator_noise_model(self): + """Test that the simulator noise model saved on a passed Options + object is used to set the backend noise model""" + + options = Options() + options.simulator.noise_model = {"placeholder": 1} + + new_backend = MockedBackend() + dev1 = QiskitDevice2(wires=3, backend=backend) + dev2 = QiskitDevice2(wires=3, backend=new_backend, options=options) + + assert dev1.backend.options.noise_model == None + assert dev2.backend.options.noise_model == {"placeholder": 1} + + +class TestQiskitSessionManagement: + """Test using Qiskit sessions with the device""" + + def test_default_no_session_on_initialization(self): + """Test that the default behaviour is no session at initialization""" + + dev = QiskitDevice2(wires=2, backend=backend) + assert dev._session == None + + def test_initializing_with_session(self): + """Test that you can initialize a device with an existing Qiskit session""" + + session = MockSession(backend=backend, max_time="1m") + dev = QiskitDevice2(wires=2, backend=backend, session=session) + assert dev._session == session + + @patch("pennylane_qiskit.qiskit_device2.Session") + @pytest.mark.parametrize("initial_session", [None, MockSession(backend)]) + def test_using_session_context(self, mock_session, initial_session): + """Test that you can add a session within a context manager""" + + dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + + assert dev._session == initial_session + + with qiskit_session(dev) as session: + assert dev._session == session + assert dev._session != initial_session + + assert dev._session == initial_session + + @pytest.mark.parametrize("initial_session", [None, MockSession(backend)]) + def test_update_session(self, initial_session): + """Test that you can update the session stored on the device""" + + dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + assert dev._session == initial_session + + new_session = MockSession(backend=backend, max_time="1m") + dev.update_session(new_session) + + assert dev._session != initial_session + assert dev._session == new_session + + +class TestDevicePreprocessing: + """Tests the device preprocessing functions""" + + @pytest.mark.parametrize( + "measurements, expectation", + [ + ( + [ + qml.expval(qml.PauliZ(1)), + qml.counts(), + qml.var(qml.PauliY(0)), + qml.probs(wires=[2]), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + [qml.probs(wires=[2])], + [qml.counts()], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.expval(qml.PauliX(2)), + qml.var(qml.PauliY(0)), + qml.probs(wires=[2]), + ], + [ + [ + qml.expval(qml.PauliZ(1)), + qml.expval(qml.PauliX(2)), + qml.var(qml.PauliY(0)), + ], + [qml.probs(wires=[2])], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.counts(), + qml.var(qml.PauliY(0)), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + [qml.counts()], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.var(qml.PauliY(0)), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + ], + ), + ( + [qml.counts(), qml.sample(wires=[1, 0])], + [[qml.counts(), qml.sample(wires=[1, 0])]], + ), + ( + [qml.probs(wires=[2])], + [[qml.probs(wires=[2])]], + ), + ], + ) + def test_split_measurement_types(self, measurements, expectation): + """Test that the split_measurement_types transform splits measurements into Estimator-based + (expval, var), Sampler-based (probs) and raw-sample based (everything else)""" + + operations = [qml.PauliX(0), qml.PauliY(1), qml.Hadamard(2), qml.CNOT([2, 1])] + qs = QuantumScript(operations, measurements=measurements) + tapes, reorder_fn = split_measurement_types(qs) + + # operations not modified + assert np.all([tape.operations == operations for tape in tapes]) + + # measurements split as expected + assert [tape.measurements for tape in tapes] == expectation + + # reorder_fn puts them back + assert reorder_fn([tape.measurements for tape in tapes]) == tuple(qs.measurements) + + @pytest.mark.parametrize( + "op, expected", + [ + (qml.PauliX(0), True), + (qml.CRX(0.1, wires=[0, 1]), True), + (qml.sum(qml.PauliY(1), qml.PauliZ(0)), False), + (qml.pow(qml.RX(1.1, 0), 3), False), + (qml.adjoint(qml.S(0)), True), + (qml.adjoint(qml.RX(1.2, 0)), False), + ], + ) + def test_stopping_conditions(self, op, expected): + """Test that stopping_condition works""" + res = test_dev.stopping_condition(op) + assert res == expected + + @pytest.mark.parametrize( + "obs, expected", + [ + (qml.PauliX(0), True), + (qml.Hadamard(3), True), + (qml.prod(qml.PauliY(1), qml.PauliZ(0)), False), + ], + ) + def test_observable_stopping_condition(self, obs, expected): + """Test that observable_stopping_condition works""" + res = test_dev.observable_stopping_condition(obs) + assert res == expected + + @pytest.mark.parametrize( + "measurements,num_types", + [ + ([qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])], 2), + ([qml.expval(qml.PauliZ(0)), qml.sample(wires=[0, 1])], 2), + ([qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], 2), + ([qml.var(qml.PauliZ(0)), qml.expval(qml.PauliX(1))], 1), + ([qml.probs(wires=[0]), qml.counts(), qml.var(qml.PauliY(2))], 3), + ], + ) + def test_preprocess_splits_incompatible_primitive_measurements(self, measurements, num_types): + """Test that the default behaviour for preprocess it to split the tapes based + on meausrement type. Expval and Variance are one type (Estimator), Probs another (Sampler), + and everything else a third (raw sample-based measurements).""" + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) + + program, _ = dev.preprocess() + tapes, _ = program([qs]) + + # measurements that are incompatible are split when use_primtives=True + assert len(tapes) == num_types + + @pytest.mark.parametrize( + "measurements", + [ + [qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])], + [qml.expval(qml.PauliZ(0)), qml.sample(wires=[0, 1])], + [qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], + ], + ) + def test_preprocess_measurements_without_primitives(self, measurements): + """Test if Primitives are not being used that the preprocess does not split + the tapes based on measurement type""" + + qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=False) + program, _ = dev.preprocess() + + tapes, _ = program([qs]) + + # measurements that are incompatible on the primitive-based device + # are not split when use_primtives=False + assert len(tapes) == 1 + + def test_preprocess_decomposes_unsupported_operator(self): + """Test that the device preprocess decomposes operators that + aren't on the list of Qiskit-supported operators""" + qs = QuantumScript( + [qml.CosineWindow(wires=range(2))], measurements=[qml.expval(qml.PauliZ(0))] + ) + + # tape contains unsupported operations + assert not np.all([op in QISKIT_OPERATION_MAP for op in qs.operations]) + + program, _ = test_dev.preprocess() + tapes, _ = program([qs]) + + # tape no longer contained unsupporrted operations + assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) + + def test_intial_state_prep_also_decomposes(self): + """Test that the device preprocess decomposes + unsupported operator even if they are state prep operators""" + + qs = QuantumScript( + [qml.AmplitudeEmbedding(features=[0.5, 0.5, 0.5, 0.5], wires=range(2))], + measurements=[qml.expval(qml.PauliZ(0))], + ) + + program, _ = test_dev.preprocess() + tapes, _ = program([qs]) + + assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) + + +class TestOptionsHandling: + def test_qiskit_options_to_flat_dict(self): + """Test that a Qiskit Options object is converted to an un-nested python dictionary""" + + options = options_for_testing() + + options_dict = qiskit_options_to_flat_dict(options) + + assert isinstance(options_dict, dict) + # the values in the dict are not themselves dictionaries or convertable to dictionaries + for val in options_dict.values(): + assert not hasattr(val, "__dict__") + assert not isinstance(val, dict) + + @pytest.mark.parametrize("options", [None, options_for_testing()]) + def test_shots_kwarg_updates_default_options(self, options): + """Check that the shots passed to the device are set on the device + as well as updated on the Options object""" + + dev = QiskitDevice2(wires=2, backend=backend, shots=23, options=options) + + assert dev.shots.total_shots == 23 + assert dev.options.execution.shots == 23 + + def test_warning_if_shots(self): + """Test that a warning is raised if the user attempt to specify shots on + Options instead of as a kwarg, and sets shots to the shots passed (defaults + to 1024).""" + + options = options_for_testing() + options.execution.shots = 1000 + + with pytest.warns( + UserWarning, + match="Setting shots via the Options is not supported on PennyLane devices", + ): + dev = QiskitDevice2(wires=2, backend=backend, options=options) + + assert dev.shots.total_shots == 1024 + assert dev.options.execution.shots == 1024 + + with pytest.warns( + UserWarning, + match="Setting shots via the Options is not supported on PennyLane devices", + ): + dev = QiskitDevice2(wires=2, backend=backend, shots=200, options=options) + + assert dev.shots.total_shots == 200 + assert dev.options.execution.shots == 200 + + def test_update_kwargs_no_overlapping_options_passed(self): + """Test that if there is no overlap between options defined as device kwargs and on Options, + _update_kwargs creates a combined dictionary""" + + dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True, random_kwarg2="a") + + assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} + if Version(qiskit_ibm_runtime.__version__) < Version("0.21.0"): + assert dev._kwargs == { + "random_kwarg1": True, + "random_kwarg2": "a", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + "job_tags": [], + } + else: + assert dev._kwargs == { + "random_kwarg1": True, + "random_kwarg2": "a", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + } + + dev.options.environment.job_tags = ["my_tag"] + dev.options.max_execution_time = "1m" + + dev._update_kwargs() + + # _init_kwargs are unchanged, _kwargs are updated + assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} + assert dev._kwargs == { + "random_kwarg1": True, + "random_kwarg2": "a", + "max_execution_time": "1m", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + "job_tags": ["my_tag"], + } + + def test_update_kwargs_with_overlapping_options(self): + """Test that if there is overlap between options defined as device kwargs and on Options, + _update_kwargs creates a combined dictionary with Options taking precedence, and raises a + warning""" + + dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True, max_execution_time="1m") + + assert dev._init_kwargs == {"random_kwarg1": True, "max_execution_time": "1m"} + if Version(qiskit_ibm_runtime.__version__) < Version("0.21.0"): + assert dev._kwargs == { + "random_kwarg1": True, + "max_execution_time": "1m", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + "job_tags": [], + } + else: + assert dev._kwargs == { + "random_kwarg1": True, + "max_execution_time": "1m", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + } + + dev.options.environment.job_tags = ["my_tag"] + dev.options.max_execution_time = "30m" + + with pytest.warns( + UserWarning, + match="also defined in the device Options. The definition in Options will be used.", + ): + dev._update_kwargs() + + # _init_kwargs are unchanged, _kwargs are updated + assert dev._init_kwargs == {"random_kwarg1": True, "max_execution_time": "1m"} + assert dev._kwargs == { + "random_kwarg1": True, + "max_execution_time": "30m", # definition from Options is used + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + "job_tags": ["my_tag"], + } + + def test_update_kwargs_with_shots_set_on_options(self): + """Test that if shots have been defined on Options, _update_kwargs raises a warning + and ignores the shots as defined on Options""" + + dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True) + + start_init_kwargs = dev._init_kwargs + start_kwargs = dev._kwargs + + dev.options.execution.shots = 500 + + with pytest.warns( + UserWarning, + match="Setting shots via the Options is not supported on PennyLane devices", + ): + assert dev.options.execution.shots == 500 + dev._update_kwargs() + + # _init_kwargs and _kwargs are unchanged, shots was ignored + assert dev._init_kwargs == start_init_kwargs + assert dev._kwargs == start_kwargs + + # the shots on the Options have been reset to the device shots + assert dev.options.execution.shots == dev.shots.total_shots + + +class TestDeviceProperties: + def test_name_property(self): + """Test the backend property""" + assert test_dev.name == "QiskitDevice2" + + def test_backend_property(self): + """Test the backend property""" + assert test_dev.backend == test_dev._backend + assert test_dev.backend == backend + + def test_service_property(self): + """Test the service property""" + assert test_dev.service == test_dev._service + + def test_session_property(self): + """Test the session property""" + + session = MockSession(backend=backend) + dev = QiskitDevice2(wires=2, backend=backend, session=session) + assert dev.session == dev._session + assert dev.session == session + + def test_num_wires_property(self): + """Test the num_wires property""" + + wires = [1, 2, 3] + dev = QiskitDevice2(wires=wires, backend=backend) + assert dev.num_wires == len(wires) + + +class TestMockedExecution: + def test_get_transpile_args(self): + """Test that get_transpile_args works as expected by filtering out + kwargs that don't match the Qiskit transpile signature""" + kwargs = {"random_kwarg": 3, "optimization_level": 3, "circuits": []} + assert QiskitDevice2.get_transpile_args(kwargs) == {"optimization_level": 3} + + @patch("pennylane_qiskit.qiskit_device2.transpile") + def test_compile_circuits(self, transpile_mock): + """Tests compile_circuits with a mocked transpile function to avoid calling + a remote backend. This renders it fairly useless as a test, but makes CodeCov + pass.""" + + transpile_mock.return_value = QuantumCircuit(2) + + # technically this doesn't matter due to the mock, but this is the correct input format for the function + circuits = [ + QuantumScript([qml.PauliX(0)], measurements=[qml.expval(qml.PauliZ(0))]), + QuantumScript([qml.PauliX(0)], measurements=[qml.probs(wires=[0])]), + QuantumScript([qml.PauliX(0), qml.PauliZ(1)], measurements=[qml.counts()]), + ] + input_circuits = [circuit_to_qiskit(c, register_size=2) for c in circuits] + + compiled_circuits = test_dev.compile_circuits(input_circuits) + + assert len(compiled_circuits) == len(input_circuits) + for i, circuit in enumerate(compiled_circuits): + assert isinstance(circuit, QuantumCircuit) + + @pytest.mark.parametrize( + "measurements, expectation", + [ + ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))], (1, 0)), + ([qml.var(qml.PauliX(0))], (1)), + ( + [ + qml.expval(qml.PauliX(0)), + qml.expval(qml.PauliZ(0)), + qml.var(qml.PauliX(0)), + ], + (0, 1, 1), + ), + ], + ) + def test_process_estimator_job_mocked(self, measurements, expectation): + """Test the process_estimator_job function with constructed return for + Estimator (integration test that runs with a Token is below)""" + + values = np.array([np.random.ranf() for i in range(len(measurements))]) + metadata = [{"variance": np.random.ranf(), "shots": 4000} for i in range(len(measurements))] + + result = EstimatorResult(values, metadata) + processed_result = QiskitDevice2._process_estimator_job(measurements, result) + + assert isinstance(processed_result, tuple) + assert len(processed_result) == len(measurements) + + @pytest.mark.parametrize( + "results, index", + [ + ({"00": 125, "10": 500, "01": 250, "11": 125}, None), + ([{}, {"00": 125, "10": 500, "01": 250, "11": 125}], 1), + ([{}, {}, {"00": 125, "10": 500, "01": 250, "11": 125}], 2), + ], + ) + def test_generate_samples_mocked_single_result(self, results, index): + """Test generate_samples with a Mocked return for the job result + (integration test that runs with a Token is below)""" + + # create mocked Job with results dict + def get_counts(): + return results + + mock_job = Mock() + mock_job.configure_mock(get_counts=get_counts) + test_dev._current_job = mock_job + + samples = test_dev.generate_samples(circuit=index) + results_dict = results if index is None else results[index] + + assert len(samples) == sum(results_dict.values()) + assert len(samples[0]) == 2 + + assert len(np.argwhere([np.allclose(s, [0, 0]) for s in samples])) == results_dict["00"] + assert len(np.argwhere([np.allclose(s, [1, 1]) for s in samples])) == results_dict["11"] + + # order of samples is swapped compared to keys (Qiskit wire order convention is reverse of PennyLane) + assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == results_dict["10"] + assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == results_dict["01"] + + def test_execute_pipeline_no_primitives_mocked(self, mocker): + """Test that a device **not** using Primitives only calls the _execute_runtime_service + to execute, regardless of measurement type""" + + dev = QiskitDevice2( + wires=5, backend=backend, use_primitives=False, session=MockSession(backend) + ) + + initial_session = dev._session + + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + qs = QuantumScript( + [qml.PauliX(0), qml.PauliY(1)], + measurements=[ + qml.expval(qml.PauliZ(0)), + qml.probs(wires=[0, 1]), + qml.counts(), + qml.sample(), + ], + ) + + with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + res = dev.execute(qs) + + runtime_service_execute.assert_called_once() + sampler_execute.assert_not_called() + estimator_execute.assert_not_called() + + assert res == "runtime_execute_res" + assert initial_session == dev._session # session is not changed + + @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._execute_estimator") + def test_execute_pipeline_primitives_no_session(self, mocker): + """Test that a Primitives-based device initialized with no Session creates one for the + execution, and then returns the device session to None.""" + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True, session=None) + + assert dev._session is None + + qs = QuantumScript([qml.PauliX(0), qml.PauliY(1)], measurements=[qml.expval(qml.PauliZ(0))]) + + with patch("pennylane_qiskit.qiskit_device2.Session") as mock_session: + res = dev.execute(qs) + mock_session.assert_called_once() # a session was created + + assert dev._session is None # the device session is still None + + def test_execute_pipeline_with_all_execute_types_mocked(self, mocker): + """Test that a device that **is** using Primitives calls the _execute_runtime_service + to execute measurements that require raw samples, and the relevant primitive measurements + on the other measurements""" + + dev = QiskitDevice2( + wires=5, backend=backend, use_primitives=True, session=MockSession(backend) + ) + + qs = QuantumScript( + [qml.PauliX(0), qml.PauliY(1)], + measurements=[ + qml.expval(qml.PauliZ(0)), + qml.probs(wires=[0, 1]), + qml.counts(), + qml.sample(), + ], + ) + tapes, reorder_fn = split_measurement_types(qs) + + with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): + with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): + with patch.object(dev, "_execute_estimator", return_value="estimator_execute_res"): + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + res = dev.execute(tapes) + + runtime_service_execute.assert_called_once() + sampler_execute.assert_called_once() + estimator_execute.assert_called_once() + + assert res == [ + "estimator_execute_res", + "sampler_execute_res", + "runtime_execute_res", + ] + + @patch("pennylane_qiskit.qiskit_device2.Estimator") + @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._process_estimator_job") + @pytest.mark.parametrize("session", [None, MockSession(backend)]) + def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, session): + """Test the _execute_estimator function using a mocked version of Estimator + that returns a meaningless result.""" + + qs = QuantumScript( + [qml.PauliX(0)], + measurements=[qml.expval(qml.PauliY(0)), qml.var(qml.PauliX(0))], + shots=100, + ) + result = test_dev._execute_estimator(qs, session) + + # to emphasize, this did nothing except appease CodeCov + assert isinstance(result, Mock) + + @patch("pennylane_qiskit.qiskit_device2.Sampler") + @pytest.mark.parametrize("session", [None, MockSession(backend)]) + def test_execute_sampler_mocked(self, mocked_sampler, session): + """Test the _execute_sampler function using a mocked version of Sampler + that returns a meaningless result.""" + + qs = QuantumScript([qml.PauliX(0)], measurements=[qml.counts()], shots=100) + result = test_dev._execute_sampler(qs, session) + + # to emphasize, this did nothing except appease CodeCov + assert isinstance(result[0], Mock) + + @patch("pennylane_qiskit.qiskit_device2.transpile") + def test_execute_runtime_service_mocked(self, mocked_transpile): + """Test the _execute_sampler function using a mocked version of Sampler + that returns a meaningless result.""" + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + + mock_counts = {"00": 125, "10": 500, "01": 250, "11": 125} + mock_result = Mock() + mock_job = Mock() + mock_service = Mock() + + mock_result.get_counts = Mock(return_value=mock_counts) + mock_job.result = Mock(return_value=mock_result) + mock_service.run = Mock(return_value=mock_job) + + dev._service = mock_service + + qs = QuantumScript([qml.PauliX(0)], measurements=[qml.sample()]) + result = dev._execute_runtime_service(qs, MockSession(backend)) + + samples = result[0] + + assert len(samples) == sum(mock_counts.values()) + assert len(samples[0]) == 2 + + assert len(np.argwhere([np.allclose(s, [0, 0]) for s in samples])) == mock_counts["00"] + assert len(np.argwhere([np.allclose(s, [1, 1]) for s in samples])) == mock_counts["11"] + + # order of samples is swapped compared to keys (Qiskit wire order convention is reverse of PennyLane) + assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == mock_counts["10"] + assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == mock_counts["01"] + + def test_shot_vector_warning_mocked(self): + """Test that a device that executes a circuit with an array of shots raises the appropriate warning""" + + dev = QiskitDevice2( + wires=5, backend=backend, use_primitives=True, session=MockSession(backend) + ) + qs = QuantumScript( + measurements=[ + qml.expval(qml.PauliX(0)), + ], + shots=[5, 10, 2], + ) + + with patch.object(dev, "_execute_estimator"): + with pytest.warns( + UserWarning, + match="Setting shot vector", + ): + dev.execute(qs) + + +@pytest.mark.usefixtures("skip_if_no_account") +class TestExecution: + @pytest.mark.parametrize("wire", [0, 1]) + @pytest.mark.parametrize( + "angle,op,expectation", + [ + (np.pi / 2, qml.RX, [0, -1, 0, 1, 0, 1]), + (np.pi, qml.RX, [0, 0, -1, 1, 1, 0]), + (np.pi / 2, qml.RY, [1, 0, 0, 0, 1, 1]), + (np.pi, qml.RY, [0, 0, -1, 1, 1, 0]), + (np.pi / 2, qml.RZ, [0, 0, 1, 1, 1, 0]), + ], + ) + def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expectation): + """Test that the Estimator with various observables returns expected results. + Essentially testing that the conversion to PauliOps in _execute_estimator behaves as + expected. Iterating over wires ensures that the wire operated on and the wire measured + correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) + """ + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + qs = QuantumScript( + [op(angle, wire)], + measurements=[ + qml.expval(qml.PauliX(wire)), + qml.expval(qml.PauliY(wire)), + qml.expval(qml.PauliZ(wire)), + qml.var(qml.PauliX(wire)), + qml.var(qml.PauliY(wire)), + qml.var(qml.PauliZ(wire)), + ], + ) + + res = dev.execute(qs) + + runtime_service_execute.assert_not_called() + sampler_execute.assert_not_called() + estimator_execute.assert_called_once() + + assert np.allclose(res, expectation, atol=0.1) + + @pytest.mark.parametrize( + "measurements, expectation", + [ + ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))], (1, 0)), + ([qml.var(qml.PauliX(0))], (1)), + ( + [ + qml.expval(qml.PauliX(0)), + qml.expval(qml.PauliZ(0)), + qml.var(qml.PauliX(0)), + ], + (0, 1, 1), + ), + ], + ) + def test_process_estimator_job(self, measurements, expectation): + """for variance and for expval and for a combination""" + + # make PennyLane circuit + qs = QuantumScript([], measurements=measurements) + + # convert to Qiskit circuit information + qcirc = circuit_to_qiskit(qs, register_size=qs.num_wires, diagonalize=False, measure=False) + pauli_observables = [mp_to_pauli(mp, qs.num_wires) for mp in qs.measurements] + + # run on simulator via Estimator + estimator = Estimator(backend=backend) + result = estimator.run([qcirc] * len(pauli_observables), pauli_observables).result() + + # confirm that the result is as expected - if the test fails at this point, its because the + # Qiskit result format has changed + assert isinstance(result, EstimatorResult) + + assert isinstance(result.values, np.ndarray) + assert result.values.size == len(qs.measurements) + + assert isinstance(result.metadata, list) + assert len(result.metadata) == len(qs.measurements) + + for data in result.metadata: + assert isinstance(data, dict) + assert list(data.keys()) == ["variance", "shots"] + + processed_result = QiskitDevice2._process_estimator_job(qs.measurements, result) + assert isinstance(processed_result, tuple) + assert np.allclose(processed_result, expectation, atol=0.05) + + @pytest.mark.parametrize("num_wires", [1, 3, 5]) + @pytest.mark.parametrize("num_shots", [50, 100]) + def test_generate_samples(self, num_wires, num_shots): + qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) + + qcirc = circuit_to_qiskit(qs, register_size=num_wires, diagonalize=True, measure=True) + compiled_circuits = test_dev.compile_circuits([qcirc]) + + # Send circuits to the cloud for execution by the circuit-runner program + job = test_dev.service.run( + program_id="circuit-runner", + options={"backend": backend.name}, + inputs={"circuits": compiled_circuits, "shots": num_shots}, + ) + + test_dev._current_job = job.result(decoder=RunnerResult) + + samples = test_dev.generate_samples() + + assert len(samples) == num_shots + assert len(samples[0]) == num_wires + + # we expect the samples to be orderd such that q0 has a 50% chance + # of being excited, and everything else is in the ground state + exp_res0 = np.zeros(num_wires) + exp_res1 = np.zeros(num_wires) + exp_res1[0] = 1 + + # the two expected results are in samples + assert exp_res1 in samples + assert exp_res0 in samples + + # nothing else is in samples + assert [s for s in samples if not s in np.array([exp_res0, exp_res1])] == [] + + def test_tape_shots_used_runtime_service(self, mocker): + """Tests that device uses tape shots rather than device shots for _execute_runtime_service""" + dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + + @qml.qnode(dev) + def circuit(): + return qml.sample() + + res = circuit(shots=[5]) + + runtime_service_execute.assert_called_once() + + assert len(res[0]) == 5 + + # Should reset to device shots if circuit ran again without shots defined + res = circuit() + assert len(res[0]) == 2 + + def test_tape_shots_used_for_estimator(self, mocker): + """Tests that device uses tape shots rather than device shots for estimator""" + dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + + estimator_execute = mocker.spy(dev, "_execute_estimator") + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + circuit(shots=[5]) + + estimator_execute.assert_called_once() + assert dev._current_job.metadata[0]["shots"] == 5 + + # Should reset to device shots if circuit ran again without shots defined + circuit() + assert dev._current_job.metadata[0]["shots"] == 2 + + def test_tape_shots_used_for_sampler(self, mocker): + """Tests that device uses tape shots rather than device shots for sampler""" + dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + + sampler_execute = mocker.spy(dev, "_execute_sampler") + + @qml.qnode(dev) + def circuit(): + qml.PauliX(0) + return qml.probs(wires=[0, 1]) + + circuit(shots=[5]) + + sampler_execute.assert_called_once() + assert dev._current_job.metadata[0]["shots"] == 5 + + # Should reset to device shots if circuit ran again without shots defined + circuit() + assert dev._current_job.metadata[0]["shots"] == 2 + + def test_warning_for_shot_vector(self): + """Tests that a warning is raised if a shot vector is passed and total shots of tape is used instead.""" + dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + with pytest.warns( + UserWarning, + match="Setting shot vector", + ): + circuit(shots=[5, 10, 2]) + assert dev._current_job.metadata[0]["shots"] == 17 + + # Should reset to device shots if circuit ran again without shots defined + circuit() + assert dev._current_job.metadata[0]["shots"] == 2 diff --git a/tests/test_converter.py b/tests/test_converter.py index f9825b23f..8da6c762a 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -9,7 +9,7 @@ from qiskit.circuit.library import DraperQFTAdder from qiskit.circuit.parametervector import ParameterVectorElement from qiskit.exceptions import QiskitError -from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info import Operator, SparsePauliOp import pennylane as qml from pennylane import numpy as np @@ -19,10 +19,14 @@ load_qasm, load_qasm_from_file, map_wires, + circuit_to_qiskit, + operation_to_qiskit, + mp_to_pauli, _format_params_dict, _check_parameter_bound, ) from pennylane.wires import Wires +from pennylane.tape.qscript import QuantumScript THETA = np.linspace(0.11, 3, 5) @@ -30,7 +34,7 @@ VARPHI = np.linspace(0.02, 3, 5) -class TestConverter: +class TestConverterQiskitToPennyLane: """Tests the converter function that allows converting QuantumCircuit objects to Pennylane templates.""" @@ -403,7 +407,7 @@ def test_wires_pass_different_wires_than_for_circuit(self, recorder): assert recorder.queue[0].wires == Wires(three_wires) -class TestConverterGates: +class TestConverterGatesQiskitToPennyLane: """Tests over specific gate related tests""" @pytest.mark.parametrize( @@ -1564,6 +1568,214 @@ def test_diff_meas_circuit(self): assert qtemp()[0] != qtemp2()[0] and qtemp2()[0] == qml.expval(qml.PauliZ(0)) +class TestConverterPennyLaneCircuitToQiskit: + + def test_circuit_to_qiskit(self): + """Test that a simple PennyLane circuit is converted to the expected Qiskit circuit""" + + qscript = QuantumScript([qml.Hadamard(1), qml.CNOT([1, 0])]) + qc = circuit_to_qiskit(qscript, len(qscript.wires), diagonalize=False, measure=False) + + operation_names = [instruction.operation.name for instruction in qc.data] + + assert operation_names == ["h", "cx"] + + def test_circuit_to_qiskit_with_parameterized_gate(self): + """Test that a simple PennyLane circuit is converted to the expected Qiskit circuit""" + angle = 1.2 + + qscript = QuantumScript([qml.Hadamard(1), qml.CNOT([1, 0]), qml.RX(angle, 2)]) + qc = circuit_to_qiskit(qscript, len(qscript.wires), diagonalize=False, measure=False) + + operation_names = [instruction.operation.name for instruction in qc.data] + operation_params = [instruction.operation.params for instruction in qc.data] + + assert operation_names == ["h", "cx", "rx"] + assert operation_params == [[], [], [angle]] + + @pytest.mark.parametrize("operations", [[], [qml.PauliX(0), qml.PauliY(1)], [qml.Hadamard(0)]]) + @pytest.mark.parametrize("register_size", [2, 5]) + def test_circuit_to_qiskit_register_size(self, operations, register_size): + """Test that the regsiter_size determines the shape of the Qiskit + QuantumCircuit register""" + + qc = circuit_to_qiskit(QuantumScript(operations), register_size) + + # there is a single classical and a single quantum register + assert len(qc.cregs) == len(qc.qregs) == 1 + + # the register contains qubits equal to the register size + assert len(qc.qubits) == register_size + + @pytest.mark.parametrize( + "operations, final_op_name", + [([qml.PauliX(0), qml.PauliY(1)], "y"), ([[qml.CNOT([0, 1]), qml.Hadamard(1)], "h"])], + ) + @pytest.mark.parametrize("measure", [True, False]) + def test_circuit_to_qiskit_measure_kwarg(self, operations, final_op_name, measure): + """Test that measurements are added to the circuit if and only if measure=True""" + + qc = circuit_to_qiskit(QuantumScript(operations), 2, measure=measure) + final_instruction = qc.data[-1] + + if measure: + assert final_instruction.operation.name == "measure" + else: + final_instruction.operation.name == final_op_name + + @pytest.mark.parametrize("diagonalize", [True, False]) + def test_circuit_to_qiskit_diagonalize_kwarg(self, diagonalize): + """Test that diagonalizing gates are included in the circuit if diagonalize=True""" + + qscript = QuantumScript( + [qml.Hadamard(1), qml.CNOT([1, 0])], measurements=[qml.expval(qml.PauliY(1))] + ) + assert qscript.diagonalizing_gates == [qml.PauliZ(1), qml.S(1), qml.Hadamard(1)] + + qc = circuit_to_qiskit(qscript, 2, diagonalize=diagonalize, measure=True) + + # get list of instruction names up to the barrier (played right before measurements) + instructions = [] + for instruction in qc.data: + if instruction.operation.name == "barrier": + break + instructions.append(instruction.operation.name) + + # check length of instructions matches length of expected gates + expected_gates = qscript.operations + if diagonalize: + expected_gates += qscript.diagonalizing_gates + + assert len(instructions) == len(expected_gates) + + +class TestConverterGatePennyLaneToQiskit: + + def test_non_parameteric_operation_to_qiskit(self): + """Test that a non-parameteric operation is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.PauliX(0) + + qc = operation_to_qiskit(op, QuantumRegister(1)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + wires = [qc.find_bit(q).index for q in qubits] + + assert ops == ["x"] + assert wires == [0] + + def test_parameteric_operation_to_qiskit(self): + """Test that a parameteric operation is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.RX(1.23, 2) + + qc = operation_to_qiskit(op, QuantumRegister(3)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + wires = [qc.find_bit(q).index for q in qubits] + params = [instruction.operation.params for instruction in qc.data] + + assert ops == ["rx"] + assert wires == [2] + assert params == [[1.23]] + + # ToDo: add custom wire label support? Or have we already mapped to integers here? Story #55168 + @pytest.mark.parametrize("op_wires", ([0, 1], [2, 4])) + def test_multi_wire_operation_to_qiskit(self, op_wires): + """Test that an operation with multiple wires is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.CNOT(op_wires) + + qc = operation_to_qiskit(op, QuantumRegister(5)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + qc_wires = [qc.find_bit(q).index for q in qubits] + + assert ops == ["cx"] + assert qc_wires == op_wires + + @pytest.mark.parametrize( + "op", + [ + qml.QubitUnitary( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], wires=[0, 1] + ), + qml.StatePrep(np.array([1, 0, 0, 0]), wires=[0, 1]), + qml.QubitStateVector(np.array([1, 0, 0, 0]), wires=[0, 1]), + ], + ) + def test_state_prep_ops_have_reversed_register(self, op): + """Tests that the wire order is reversed when applying matrix-based operators from PennyLane, + because the Qiskit convention for inferring wire order for matrices is the reverse of the + PennyLane convention""" + + qc = operation_to_qiskit(op, reg=QuantumRegister(3)) + qubits = qc[0].qubits + wires = [qc.find_bit(q).index for q in qubits] + + # wires on the qiskit circuit are the PL wires reversed + assert Wires(wires) == op.wires[::-1] + + def test_with_predefined_creg(self): + """Test that it also works if passing in an already existing classical register""" + + creg = ClassicalRegister(3) + + op = qml.RX(1.23, 2) + + qc1 = operation_to_qiskit(op, QuantumRegister(3), creg=creg) + qc2 = operation_to_qiskit(op, QuantumRegister(3), creg=None) + + ops1 = [instruction.operation.name for instruction in qc1.data] + params1 = [instruction.operation.params for instruction in qc1.data] + ops2 = [instruction.operation.name for instruction in qc2.data] + params2 = [instruction.operation.params for instruction in qc2.data] + + qubits1 = [instruction.qubits for instruction in qc1.data][0] + wires1 = [qc1.find_bit(q).index for q in qubits1] + qubits2 = [instruction.qubits for instruction in qc2.data][0] + wires2 = [qc2.find_bit(q).index for q in qubits2] + + assert ops1 == ops2 == ["rx"] + assert wires1 == wires2 == [2] + assert params1 == params2 == [[1.23]] + + +class TestConverterUtilsPennyLaneToQiskit: + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "observable, obs_string", + [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], + ) + @pytest.mark.parametrize("wire", [0, 1, 2]) + @pytest.mark.parametrize("register_size", [3, 5]) + def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, register_size): + """Tests that a SparsePauliOp is created from a Pauli observable, and that + it has the expected format""" + + obs = measurement_type(observable(wire)) + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + + # the wire the observable acts on is correctly labelled + # wire order reversed in Qiskit, so we put it back to use PL wire as an index + pauli_op_list.reverse() + assert pauli_op_list.pop(wire) == obs_string + + # remaining wires are all Identity + assert np.all([op == "I" for op in pauli_op_list]) + + class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" From c83f8c88466e4ca06aeedaab0cb93e0b14371e55 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:49:28 -0400 Subject: [PATCH 02/47] Add compile_backend kwarg (#398) Adds the compile_backend kwarg to the Qiskit device. It is useful when you want to do circuit transpilation when using the old Qiskit API (e.g. use_primitives = False). * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh --------- Co-authored-by: Austin Huang Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 15 ++++++- tests/test_base_device.py | 64 +++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 3e8c6872e..67d8a2c0d 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -223,6 +223,9 @@ class QiskitDevice2(Device): for more details. session (Session): a Qiskit Session to use for device execution. If none is provided, a session will be created at each device execution. + compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be + sent to the backend device, to be set if the backend desired for compliation differs from the + backend used for execution. Defaults to ``None``, which means the primary backend will be used. **kwargs: transpilation and runtime kwargs to be used for measurements without Qiskit Primitives. If any values are defined both in ``options`` and in the remaining ``kwargs``, the value provided in ``options`` will take precedence. These kwargs will be ignored for all Primitive-based @@ -249,6 +252,7 @@ def __init__( use_primitives=False, options=None, session=None, + compile_backend=None, **kwargs, ): if shots is None: @@ -269,6 +273,7 @@ def __init__( super().__init__(wires=wires, shots=shots) self._backend = backend + self._compile_backend = compile_backend if compile_backend else backend # ToDo: possibly things fail if this is not a QiskitRuntimeService - confirm and decide how to handle (SC 55725) self._service = backend._service @@ -302,6 +307,14 @@ def backend(self): """ return self._backend + @property + def compile_backend(self): + """The ``compile_backend`` is a Qiskit backend object to be used for transpilation. + Returns: + qiskit.providers.backend: Qiskit backend object. + """ + return self._compile_backend + @property def service(self): """The QiskitRuntimeService service. @@ -445,7 +458,7 @@ def compile_circuits(self, circuits): transpile_args = self.get_transpile_args(self._kwargs) for i, circuit in enumerate(circuits): - compiled_circ = transpile(circuit, backend=self.backend, **transpile_args) + compiled_circ = transpile(circuit, backend=self.compile_backend, **transpile_args) compiled_circ.name = f"circ{i}" compiled_circuits.append(compiled_circ) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 77ee617f1..244ea5947 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -61,11 +61,11 @@ def __init__(self, n_qubits): class MockedBackend: - def __init__(self, num_qubits=10): + def __init__(self, num_qubits=10, name="mocked_backend"): self._configuration = Configuration(num_qubits) self.options = self._configuration self._service = "SomeServiceProvider" - self.name = "mocked_backend" + self.name = name def configuration(self): return self._configuration @@ -105,6 +105,21 @@ def options_for_testing(): class TestDeviceInitialization: + def test_compile_backend_kwarg(self): + """Test that the compile_backend is set correctly if passed, and the main + backend is used otherwise""" + + compile_backend = MockedBackend(name="compile_backend") + main_backend = MockedBackend(name="main_backend") + + dev1 = QiskitDevice2(wires=5, backend=main_backend) + dev2 = QiskitDevice2(wires=5, backend=main_backend, compile_backend=compile_backend) + + assert dev1._compile_backend == dev1._backend == main_backend + + assert dev2._compile_backend != dev2._backend + assert dev2._compile_backend == compile_backend + @pytest.mark.parametrize("use_primitives", [True, False]) def test_use_primitives_kwarg(self, use_primitives): """Test the _use_primitives attribute is set on initialization""" @@ -586,6 +601,15 @@ def test_backend_property(self): assert test_dev.backend == test_dev._backend assert test_dev.backend == backend + def test_compile_backend_property(self): + """Test the compile_backend property""" + + compile_backend = MockedBackend(name="compile_backend") + dev = QiskitDevice2(wires=5, backend=backend, compile_backend=compile_backend) + + assert dev.compile_backend == dev._compile_backend + assert dev.compile_backend == compile_backend + def test_service_property(self): """Test the service property""" assert test_dev.service == test_dev._service @@ -610,14 +634,37 @@ class TestMockedExecution: def test_get_transpile_args(self): """Test that get_transpile_args works as expected by filtering out kwargs that don't match the Qiskit transpile signature""" + + # independently kwargs = {"random_kwarg": 3, "optimization_level": 3, "circuits": []} assert QiskitDevice2.get_transpile_args(kwargs) == {"optimization_level": 3} + # on a device + transpile_args = { + "random_kwarg": 3, + "seed_transpiler": 42, + "optimization_level": 3, + "circuits": [], + } + compile_backend = MockedBackend(name="compile_backend") + dev = QiskitDevice2( + wires=5, backend=backend, compile_backend=compile_backend, **transpile_args + ) + assert dev.get_transpile_args(dev._kwargs) == { + "optimization_level": 3, + "seed_transpiler": 42, + } + @patch("pennylane_qiskit.qiskit_device2.transpile") - def test_compile_circuits(self, transpile_mock): + @pytest.mark.parametrize("compile_backend", [None, MockedBackend(name="compile_backend")]) + def test_compile_circuits(self, transpile_mock, compile_backend): """Tests compile_circuits with a mocked transpile function to avoid calling - a remote backend. This renders it fairly useless as a test, but makes CodeCov - pass.""" + a remote backend. Confirm compile_backend and transpile_args are used.""" + + transpile_args = {"seed_transpiler": 42, "optimization_level": 2} + dev = QiskitDevice2( + wires=5, backend=backend, compile_backend=compile_backend, **transpile_args + ) transpile_mock.return_value = QuantumCircuit(2) @@ -629,7 +676,12 @@ def test_compile_circuits(self, transpile_mock): ] input_circuits = [circuit_to_qiskit(c, register_size=2) for c in circuits] - compiled_circuits = test_dev.compile_circuits(input_circuits) + with patch.object(dev, "get_transpile_args", return_value=transpile_args): + compiled_circuits = dev.compile_circuits(input_circuits) + + transpile_mock.assert_called_with( + input_circuits[2], backend=dev.compile_backend, **transpile_args + ) assert len(compiled_circuits) == len(input_circuits) for i, circuit in enumerate(compiled_circuits): From 41c73b0bfef3fdcaece5758a6fe766dc44948f84 Mon Sep 17 00:00:00 2001 From: lillian542 <38584660+lillian542@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:47:05 -0400 Subject: [PATCH 03/47] Support both V1 and V2 (#399) * add name to MockedBackend * support both V1 and V2 syntax for retrieving backend name and num_qubits * test relevant methods with both V1 and V2 MockBackends * tests updated for old device api as per other PR * tests * Update tests/test_base_device.py Co-authored-by: Utkarsh * Apply suggestions from code review Co-authored-by: Utkarsh * merge conf --------- Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Austin Huang Co-authored-by: Utkarsh --- pennylane_qiskit/converter.py | 4 +- pennylane_qiskit/qiskit_device2.py | 23 +++++-- tests/test_base_device.py | 96 ++++++++++++++++++++++++++---- tests/test_qiskit_device.py | 4 +- 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index a56636e6f..76e4d5744 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -21,6 +21,7 @@ import numpy as np from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister +from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit @@ -34,9 +35,6 @@ import pennylane as qml import pennylane.ops as pennylane_ops -from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP - -# pylint: disable=too-many-instance-attributes inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 67d8a2c0d..88778d7c4 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -26,6 +26,8 @@ import numpy as np import pennylane as qml from qiskit.compiler import transpile +from qiskit.providers import BackendV2 + from qiskit_ibm_runtime import Session, Sampler, Estimator from qiskit_ibm_runtime.constants import RunnerResult from qiskit_ibm_runtime.options import Options @@ -255,6 +257,7 @@ def __init__( compile_backend=None, **kwargs, ): + if shots is None: warnings.warn( "Expected an integer number of shots, but received shots=None. Defaulting " @@ -289,11 +292,13 @@ def __init__( self.backend.set_options(noise_model=self.options.simulator.noise_model) # Perform validation against backend - b = self.backend - if len(self.wires) > int(b.configuration().n_qubits): - raise ValueError( - f"Backend '{backend}' supports maximum {b.configuration().n_qubits} wires" - ) + available_qubits = ( + backend.num_qubits + if isinstance(backend, BackendV2) + else backend.configuration().n_qubits + ) + if len(self.wires) > int(available_qubits): + raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") self.reset() self._update_kwargs() @@ -527,8 +532,14 @@ def _execute_runtime_service(self, circuits, session): for kwarg, value in self._kwargs.items(): program_inputs[kwarg] = value + backend_name = ( + self.backend.name + if isinstance(self.backend, BackendV2) + else self.backend.configuration().backend_name + ) + options = { - "backend": self.backend.name, + "backend": backend_name, "log_level": self.options.environment.log_level, "job_tags": self.options.environment.job_tags, "max_execution_time": self.options.max_execution_time, diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 244ea5947..9e486de3c 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -42,12 +42,14 @@ from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator from qiskit_ibm_runtime.options import Options from qiskit_ibm_runtime.constants import RunnerResult +from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 # do not import Estimator (imported above) from qiskit.primitives - the identically # named Estimator object has a different call signature than the remote device Estimator, # and only runs local simulations. We need the Estimator from qiskit_ibm_runtime. They # both use this EstimatorResults, however: from qiskit.primitives import EstimatorResult +from qiskit.providers import BackendV1, BackendV2 from qiskit import QuantumCircuit @@ -55,24 +57,54 @@ class Configuration: - def __init__(self, n_qubits): + def __init__(self, n_qubits, backend_name): self.n_qubits = n_qubits + self.backend_name = backend_name self.noise_model = None -class MockedBackend: +class MockedBackend(BackendV2): def __init__(self, num_qubits=10, name="mocked_backend"): - self._configuration = Configuration(num_qubits) - self.options = self._configuration + self._options = Configuration(num_qubits, name) self._service = "SomeServiceProvider" self.name = name + self._target = Mock() + self._target.num_qubits = num_qubits + + def set_options(self, noise_model): + self.options.noise_model = noise_model + + def _default_options(self): + return {} + + def max_circuits(self): + return 10 + + def run(self, *args, **kwargs): + return None + + @property + def target(self): + return self._target + +class MockedBackendLegacy(BackendV1): + def __init__(self, num_qubits=10, name="mocked_backend_legacy"): + self._configuration = Configuration(num_qubits, backend_name=name) + self._service = "SomeServiceProvider" + self._options = self._default_options() def configuration(self): return self._configuration - def set_options(self, noise_model): - self.options.noise_model = noise_model + def _default_options(self): + return {} + + def run(self, *args, **kwargs): + return None + @property + def options(self): + return self._options class MockSession: def __init__(self, backend, max_time=None): @@ -90,6 +122,7 @@ def close(self): # This is just to appease a test except: backend = MockedBackend() +legacy_backend = MockedBackendLegacy() test_dev = QiskitDevice2(wires=5, backend=backend) @@ -100,9 +133,45 @@ def options_for_testing(): options.resilience.noise_amplifier = "LocalFoldingAmplifier" options.optimization_level = 2 options.resilience_level = 1 - # options.simulator.noise_model = None return options +class TestSupportForV1andV2: + """Tests compatibility with BackendV1 and BackendV2""" + + @pytest.mark.parametrize( + "backend", + [ + legacy_backend, + backend, + ], + ) + def test_v1_and_v2_mocked(self, backend): + """Test that device initializes with no error mocked""" + dev = QiskitDevice2(wires=10, backend=backend, use_primitives=True) + assert dev._backend == backend + + + @pytest.mark.skip(reason="Fake backends do not have attribute _service, should address in (SC 55725)") + @pytest.mark.parametrize( + "backend", + [ + FakeManila(), + FakeManilaV2(), + ] + ) + def test_v1_and_v2_manila(self, backend): + """Test that device initializes with no error with V1 and V2 backends by Qiskit""" + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample(qml.PauliZ(0)) + + res = circuit(np.pi/2) + assert isinstance(res, np.ndarray) + assert np.shape(res) == (1024,) class TestDeviceInitialization: def test_compile_backend_kwarg(self): @@ -171,9 +240,10 @@ def test_kwargs_on_initialization(self, mocker): # initial kwargs are saved without modification assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} - def test_backend_wire_validation(self): - """Test that the an error is raised if the number of device wires exceeds - the number of wires available on the backend""" + @pytest.mark.parametrize("backend", [backend, legacy_backend]) + def test_backend_wire_validation(self, backend): + """Test that an error is raised if the number of device wires exceeds + the number of wires available on the backend, for both backend versions""" with pytest.raises(ValueError, match="supports maximum"): dev = QiskitDevice2(wires=500, backend=backend) @@ -748,7 +818,8 @@ def get_counts(): assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == results_dict["10"] assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == results_dict["01"] - def test_execute_pipeline_no_primitives_mocked(self, mocker): + @pytest.mark.parametrize("backend", [backend, legacy_backend]) + def test_execute_pipeline_no_primitives_mocked(self, mocker, backend): """Test that a device **not** using Primitives only calls the _execute_runtime_service to execute, regardless of measurement type""" @@ -799,7 +870,8 @@ def test_execute_pipeline_primitives_no_session(self, mocker): assert dev._session is None # the device session is still None - def test_execute_pipeline_with_all_execute_types_mocked(self, mocker): + @pytest.mark.parametrize("backend", [backend, legacy_backend]) + def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): """Test that a device that **is** using Primitives calls the _execute_runtime_service to execute measurements that require raw samples, and the relevant primitive measurements on the other measurements""" diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 3a721f32c..5aec93c91 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -111,8 +111,8 @@ def circuit(x): return qml.sample(qml.PauliZ(0)) res = circuit(np.pi/2) - assert(isinstance(res, np.ndarray)) - assert(np.shape(res) == (1024,)) + assert isinstance(res, np.ndarray) + assert np.shape(res) == (1024,) class TestProbabilities: From 0ba56a9cc23a66a7c593a319d98d3aadd1b48c07 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 23 Apr 2024 14:48:33 -0400 Subject: [PATCH 04/47] Scalar tests passing --- pennylane_qiskit/converter.py | 46 ++++++++++++++++++++++++++++++---- tests/test_converter.py | 47 ++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 76e4d5744..acda7c025 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -710,16 +710,52 @@ def mp_to_pauli(mp, register_size): # ToDo: I believe this could be extended to cover expectation values of Hamiltonians observables = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Identity": "I"} + pauli_strings = [] + coeffs = None - pauli_string = ["I"] * register_size - pauli_string[mp.wires[0]] = observables[mp.obs.name] + if hasattr(mp.obs, "coeffs"): + coeffs = mp.obs.coeffs + + if isinstance(mp.obs, qml.Hamiltonian): + mp.obs = mp.obs.ops + + if not isinstance(mp.obs, list): + mp.obs = [mp.obs] + + for obs in mp.obs: + pauli_string = ["I"] * register_size + for i in range(len(obs.wires)): + if isinstance(obs.name, str): + pauli_string[obs.wires[i]] = observables[obs.name] + else: + pauli_string[obs.wires[i]] = observables[obs.name[i]] + + pauli_string.reverse() + pauli_string = ("").join(pauli_string) + pauli_strings.append(pauli_string) + + return SparsePauliOp(data = pauli_strings, coeffs = coeffs) + + # if len(mp.wires) == 1: + # pauli_string[mp.wires[0]] = observables[mp.obs.name] + # elif isinstance(mp.obs, qml.Hamiltonian): + # for observable in mp.obs.ops: + # print(observable) + # for i in range(len(observable.wires)): + # if isinstance(observable.name, str): + # pauli_string[observable.wires[i]] = observables[observable.name] + # else: + # pauli_string[observable.wires[i]] = observables[observable.name[i]] + + + # for observable in mp.obs: + # pauli_string[observable.wires[0]] = observables[observable.name] # Qiskit orders wires in the opposite direction compared to PL - pauli_string.reverse() + #pauli_string.reverse() - pauli_string = ("").join(pauli_string) + #pauli_string = ("").join(pauli_string) - return SparsePauliOp(pauli_string) def load_pauli_op( diff --git a/tests/test_converter.py b/tests/test_converter.py index 8da6c762a..b7b8160df 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1745,7 +1745,6 @@ def test_with_predefined_creg(self): class TestConverterUtilsPennyLaneToQiskit: - @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) @pytest.mark.parametrize( "observable, obs_string", @@ -1775,6 +1774,52 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize("register_size", [3, 5]) + @pytest.mark.parametrize("wire", [0, 1, 2]) + @pytest.mark.parametrize("coeff", [2, 3, -2]) + @pytest.mark.parametrize( + "observable, obs_string", + [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], + ) + def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): + """Tests that a SparsePauliOp is created from a Hamiltonian, and that + it has the expected format""" + op = coeff * observable(wire) + obs = measurement_type(op) + data = "" + for i in range(register_size - 1, -1, -1): + if i == wire: + data += obs_string + else: + data += "I" + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) + + + @pytest.mark.parametrize("measurement_type", [qml.expval]) + @pytest.mark.parametrize("register_size", [3]) + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): + """Tests that a SparsePauliOp is created from a Hamiltonian, and that + it has the expected format""" + hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0) @ qml.Z(1), qml.Z(2) @ qml.Y(1)]) ## 1 * X(0) @ Z (1) + 2 * Z(2) @ Y (1) + obs = measurement_type(hamiltonian) + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" From a1ff60638f19d302a802f46a4d586a616e0be95c Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 23 Apr 2024 17:21:01 -0400 Subject: [PATCH 05/47] Change mp_to_pauli to be more general --- pennylane_qiskit/converter.py | 44 ++++++----------------------------- tests/test_converter.py | 44 +++-------------------------------- 2 files changed, 10 insertions(+), 78 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index acda7c025..076e412dc 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -709,53 +709,23 @@ def mp_to_pauli(mp, register_size): # ToDo: I believe this could be extended to cover expectation values of Hamiltonians - observables = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Identity": "I"} pauli_strings = [] - coeffs = None + coeffs = [] + op = mp.obs - if hasattr(mp.obs, "coeffs"): - coeffs = mp.obs.coeffs - - if isinstance(mp.obs, qml.Hamiltonian): - mp.obs = mp.obs.ops - - if not isinstance(mp.obs, list): - mp.obs = [mp.obs] + pauli_strings = [] - for obs in mp.obs: + for pauli_term, coeff in op.pauli_rep.items(): pauli_string = ["I"] * register_size - for i in range(len(obs.wires)): - if isinstance(obs.name, str): - pauli_string[obs.wires[i]] = observables[obs.name] - else: - pauli_string[obs.wires[i]] = observables[obs.name[i]] - + coeffs.append(coeff) + if len(pauli_term.wires) == 1: + pauli_string[pauli_term.wires[0]] = pauli_term[pauli_term.wires[0]] pauli_string.reverse() pauli_string = ("").join(pauli_string) pauli_strings.append(pauli_string) return SparsePauliOp(data = pauli_strings, coeffs = coeffs) - # if len(mp.wires) == 1: - # pauli_string[mp.wires[0]] = observables[mp.obs.name] - # elif isinstance(mp.obs, qml.Hamiltonian): - # for observable in mp.obs.ops: - # print(observable) - # for i in range(len(observable.wires)): - # if isinstance(observable.name, str): - # pauli_string[observable.wires[i]] = observables[observable.name] - # else: - # pauli_string[observable.wires[i]] = observables[observable.name[i]] - - - # for observable in mp.obs: - # pauli_string[observable.wires[0]] = observables[observable.name] - - # Qiskit orders wires in the opposite direction compared to PL - #pauli_string.reverse() - - #pauli_string = ("").join(pauli_string) - def load_pauli_op( diff --git a/tests/test_converter.py b/tests/test_converter.py index b7b8160df..b6ad6f1c5 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -10,6 +10,7 @@ from qiskit.circuit.parametervector import ParameterVectorElement from qiskit.exceptions import QiskitError from qiskit.quantum_info import Operator, SparsePauliOp +from semantic_version import Version import pennylane as qml from pennylane import numpy as np @@ -1774,51 +1775,12 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) - @pytest.mark.parametrize("register_size", [3, 5]) - @pytest.mark.parametrize("wire", [0, 1, 2]) - @pytest.mark.parametrize("coeff", [2, 3, -2]) - @pytest.mark.parametrize( - "observable, obs_string", - [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], - ) - def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): + def test_mp_to_pauli_for_hamiltonian(self): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - op = coeff * observable(wire) - obs = measurement_type(op) - data = "" - for i in range(register_size - 1, -1, -1): - if i == wire: - data += obs_string - else: - data += "I" + assert True - pauli_op = mp_to_pauli(obs, register_size) - assert isinstance(pauli_op, SparsePauliOp) - - pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - # all qubits in register are accounted for - assert len(pauli_op_list) == register_size - assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) - - - @pytest.mark.parametrize("measurement_type", [qml.expval]) - @pytest.mark.parametrize("register_size", [3]) - def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): - """Tests that a SparsePauliOp is created from a Hamiltonian, and that - it has the expected format""" - hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0) @ qml.Z(1), qml.Z(2) @ qml.Y(1)]) ## 1 * X(0) @ Z (1) + 2 * Z(2) @ Y (1) - obs = measurement_type(hamiltonian) - - pauli_op = mp_to_pauli(obs, register_size) - assert isinstance(pauli_op, SparsePauliOp) - - pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - - # all qubits in register are accounted for - assert len(pauli_op_list) == register_size class TestControlOpIntegration: From 17c3e674235709a7c5e1baf037ebf37b42f3efbf Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 23 Apr 2024 17:42:52 -0400 Subject: [PATCH 06/47] Hamiltonian testing --- tests/test_converter.py | 52 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index b6ad6f1c5..c21b91ab7 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1775,12 +1775,60 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - def test_mp_to_pauli_for_hamiltonian(self): + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize("register_size", [3, 5, 7]) + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - assert True + terms = [qml.X(0), qml.X(1)] + data = [] + for term in terms: + pauli_string = ["I"] * register_size + pauli_string[term.wires[0]] = term.basis + pauli_string.reverse() + pauli_string = ("").join(pauli_string) + data.append(pauli_string) + + hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]) + obs = measurement_type(hamiltonian) + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op == SparsePauliOp(data = data, coeffs = [1, 2]) + + # @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + # @pytest.mark.parametrize("register_size", [3, 5]) + # @pytest.mark.parametrize("wire", [0, 1, 2]) + # @pytest.mark.parametrize("coeff", [2, 3, -2]) + # @pytest.mark.parametrize( + # "observable, obs_string", + # [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], + # ) + # def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): + # """Tests that a SparsePauliOp is created from a Hamiltonian, and that + # it has the expected format""" + # op = coeff * observable(wire) + # obs = measurement_type(op) + # data = "" + # for i in range(register_size - 1, -1, -1): + # if i == wire: + # data += obs_string + # else: + # data += "I" + + # pauli_op = mp_to_pauli(obs, register_size) + # assert isinstance(pauli_op, SparsePauliOp) + + # pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + + # # all qubits in register are accounted for + # assert len(pauli_op_list) == register_size + # assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) class TestControlOpIntegration: From 4f21e7ddf354cc36efee55c5d50e4f978c3d2729 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Apr 2024 13:04:06 -0400 Subject: [PATCH 07/47] tests --- tests/test_converter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index c21b91ab7..743c7c182 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1777,19 +1777,19 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) @pytest.mark.parametrize("register_size", [3, 5, 7]) - def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): + @pytest.mark.parametrize("observables", [[qml.X(0), qml.X(1)],[qml.X(1), qml.Y(2)]]) + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size, observables): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - terms = [qml.X(0), qml.X(1)] data = [] - for term in terms: + for obs in observables: pauli_string = ["I"] * register_size - pauli_string[term.wires[0]] = term.basis + pauli_string[obs.wires[0]] = obs.basis pauli_string.reverse() pauli_string = ("").join(pauli_string) data.append(pauli_string) - hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]) + hamiltonian = qml.Hamiltonian([1, 2], observables) obs = measurement_type(hamiltonian) From 54c7483792547fd9b8a600c45d467606f26b2b56 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Apr 2024 13:12:02 -0400 Subject: [PATCH 08/47] Revert "tests" This reverts commit 4f21e7ddf354cc36efee55c5d50e4f978c3d2729. --- tests/test_converter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 743c7c182..c21b91ab7 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1777,19 +1777,19 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) @pytest.mark.parametrize("register_size", [3, 5, 7]) - @pytest.mark.parametrize("observables", [[qml.X(0), qml.X(1)],[qml.X(1), qml.Y(2)]]) - def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size, observables): + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" + terms = [qml.X(0), qml.X(1)] data = [] - for obs in observables: + for term in terms: pauli_string = ["I"] * register_size - pauli_string[obs.wires[0]] = obs.basis + pauli_string[term.wires[0]] = term.basis pauli_string.reverse() pauli_string = ("").join(pauli_string) data.append(pauli_string) - hamiltonian = qml.Hamiltonian([1, 2], observables) + hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]) obs = measurement_type(hamiltonian) From 160b14c5111388a5d9a8f57e5e74b4421a915e09 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Apr 2024 13:13:11 -0400 Subject: [PATCH 09/47] Revert "Hamiltonian testing" This reverts commit 17c3e674235709a7c5e1baf037ebf37b42f3efbf. --- tests/test_converter.py | 52 ++--------------------------------------- 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index c21b91ab7..b6ad6f1c5 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1775,60 +1775,12 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) - @pytest.mark.parametrize("register_size", [3, 5, 7]) - def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): + def test_mp_to_pauli_for_hamiltonian(self): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - terms = [qml.X(0), qml.X(1)] - data = [] - for term in terms: - pauli_string = ["I"] * register_size - pauli_string[term.wires[0]] = term.basis - pauli_string.reverse() - pauli_string = ("").join(pauli_string) - data.append(pauli_string) - - hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]) + assert True - obs = measurement_type(hamiltonian) - - pauli_op = mp_to_pauli(obs, register_size) - assert isinstance(pauli_op, SparsePauliOp) - pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - # all qubits in register are accounted for - assert len(pauli_op_list) == register_size - assert pauli_op == SparsePauliOp(data = data, coeffs = [1, 2]) - - # @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) - # @pytest.mark.parametrize("register_size", [3, 5]) - # @pytest.mark.parametrize("wire", [0, 1, 2]) - # @pytest.mark.parametrize("coeff", [2, 3, -2]) - # @pytest.mark.parametrize( - # "observable, obs_string", - # [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], - # ) - # def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): - # """Tests that a SparsePauliOp is created from a Hamiltonian, and that - # it has the expected format""" - # op = coeff * observable(wire) - # obs = measurement_type(op) - # data = "" - # for i in range(register_size - 1, -1, -1): - # if i == wire: - # data += obs_string - # else: - # data += "I" - - # pauli_op = mp_to_pauli(obs, register_size) - # assert isinstance(pauli_op, SparsePauliOp) - - # pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - - # # all qubits in register are accounted for - # assert len(pauli_op_list) == register_size - # assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) class TestControlOpIntegration: From 4386d445b3c9834b6ee030d610bd8140f5e2d263 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Apr 2024 13:13:23 -0400 Subject: [PATCH 10/47] Revert "Change mp_to_pauli to be more general" This reverts commit a1ff60638f19d302a802f46a4d586a616e0be95c. --- pennylane_qiskit/converter.py | 44 +++++++++++++++++++++++++++++------ tests/test_converter.py | 44 ++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 076e412dc..acda7c025 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -709,23 +709,53 @@ def mp_to_pauli(mp, register_size): # ToDo: I believe this could be extended to cover expectation values of Hamiltonians + observables = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Identity": "I"} pauli_strings = [] - coeffs = [] - op = mp.obs + coeffs = None - pauli_strings = [] + if hasattr(mp.obs, "coeffs"): + coeffs = mp.obs.coeffs + + if isinstance(mp.obs, qml.Hamiltonian): + mp.obs = mp.obs.ops + + if not isinstance(mp.obs, list): + mp.obs = [mp.obs] - for pauli_term, coeff in op.pauli_rep.items(): + for obs in mp.obs: pauli_string = ["I"] * register_size - coeffs.append(coeff) - if len(pauli_term.wires) == 1: - pauli_string[pauli_term.wires[0]] = pauli_term[pauli_term.wires[0]] + for i in range(len(obs.wires)): + if isinstance(obs.name, str): + pauli_string[obs.wires[i]] = observables[obs.name] + else: + pauli_string[obs.wires[i]] = observables[obs.name[i]] + pauli_string.reverse() pauli_string = ("").join(pauli_string) pauli_strings.append(pauli_string) return SparsePauliOp(data = pauli_strings, coeffs = coeffs) + # if len(mp.wires) == 1: + # pauli_string[mp.wires[0]] = observables[mp.obs.name] + # elif isinstance(mp.obs, qml.Hamiltonian): + # for observable in mp.obs.ops: + # print(observable) + # for i in range(len(observable.wires)): + # if isinstance(observable.name, str): + # pauli_string[observable.wires[i]] = observables[observable.name] + # else: + # pauli_string[observable.wires[i]] = observables[observable.name[i]] + + + # for observable in mp.obs: + # pauli_string[observable.wires[0]] = observables[observable.name] + + # Qiskit orders wires in the opposite direction compared to PL + #pauli_string.reverse() + + #pauli_string = ("").join(pauli_string) + def load_pauli_op( diff --git a/tests/test_converter.py b/tests/test_converter.py index b6ad6f1c5..b7b8160df 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -10,7 +10,6 @@ from qiskit.circuit.parametervector import ParameterVectorElement from qiskit.exceptions import QiskitError from qiskit.quantum_info import Operator, SparsePauliOp -from semantic_version import Version import pennylane as qml from pennylane import numpy as np @@ -1775,12 +1774,51 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - def test_mp_to_pauli_for_hamiltonian(self): + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize("register_size", [3, 5]) + @pytest.mark.parametrize("wire", [0, 1, 2]) + @pytest.mark.parametrize("coeff", [2, 3, -2]) + @pytest.mark.parametrize( + "observable, obs_string", + [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], + ) + def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - assert True + op = coeff * observable(wire) + obs = measurement_type(op) + data = "" + for i in range(register_size - 1, -1, -1): + if i == wire: + data += obs_string + else: + data += "I" + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) + + + @pytest.mark.parametrize("measurement_type", [qml.expval]) + @pytest.mark.parametrize("register_size", [3]) + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): + """Tests that a SparsePauliOp is created from a Hamiltonian, and that + it has the expected format""" + hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0) @ qml.Z(1), qml.Z(2) @ qml.Y(1)]) ## 1 * X(0) @ Z (1) + 2 * Z(2) @ Y (1) + obs = measurement_type(hamiltonian) + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size class TestControlOpIntegration: From f71c1d495274db9beaff69a13922f42d04269368 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Apr 2024 13:14:41 -0400 Subject: [PATCH 11/47] revert --- pennylane_qiskit/converter.py | 46 ++++------------------------------ tests/test_converter.py | 47 +---------------------------------- 2 files changed, 6 insertions(+), 87 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index acda7c025..76e4d5744 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -710,52 +710,16 @@ def mp_to_pauli(mp, register_size): # ToDo: I believe this could be extended to cover expectation values of Hamiltonians observables = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Identity": "I"} - pauli_strings = [] - coeffs = None - if hasattr(mp.obs, "coeffs"): - coeffs = mp.obs.coeffs - - if isinstance(mp.obs, qml.Hamiltonian): - mp.obs = mp.obs.ops - - if not isinstance(mp.obs, list): - mp.obs = [mp.obs] - - for obs in mp.obs: - pauli_string = ["I"] * register_size - for i in range(len(obs.wires)): - if isinstance(obs.name, str): - pauli_string[obs.wires[i]] = observables[obs.name] - else: - pauli_string[obs.wires[i]] = observables[obs.name[i]] - - pauli_string.reverse() - pauli_string = ("").join(pauli_string) - pauli_strings.append(pauli_string) - - return SparsePauliOp(data = pauli_strings, coeffs = coeffs) - - # if len(mp.wires) == 1: - # pauli_string[mp.wires[0]] = observables[mp.obs.name] - # elif isinstance(mp.obs, qml.Hamiltonian): - # for observable in mp.obs.ops: - # print(observable) - # for i in range(len(observable.wires)): - # if isinstance(observable.name, str): - # pauli_string[observable.wires[i]] = observables[observable.name] - # else: - # pauli_string[observable.wires[i]] = observables[observable.name[i]] - - - # for observable in mp.obs: - # pauli_string[observable.wires[0]] = observables[observable.name] + pauli_string = ["I"] * register_size + pauli_string[mp.wires[0]] = observables[mp.obs.name] # Qiskit orders wires in the opposite direction compared to PL - #pauli_string.reverse() + pauli_string.reverse() - #pauli_string = ("").join(pauli_string) + pauli_string = ("").join(pauli_string) + return SparsePauliOp(pauli_string) def load_pauli_op( diff --git a/tests/test_converter.py b/tests/test_converter.py index b7b8160df..8da6c762a 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1745,6 +1745,7 @@ def test_with_predefined_creg(self): class TestConverterUtilsPennyLaneToQiskit: + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) @pytest.mark.parametrize( "observable, obs_string", @@ -1774,52 +1775,6 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) - @pytest.mark.parametrize("register_size", [3, 5]) - @pytest.mark.parametrize("wire", [0, 1, 2]) - @pytest.mark.parametrize("coeff", [2, 3, -2]) - @pytest.mark.parametrize( - "observable, obs_string", - [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], - ) - def test_mp_to_pauli_for_scalar(self, measurement_type, register_size, wire, coeff, observable, obs_string): - """Tests that a SparsePauliOp is created from a Hamiltonian, and that - it has the expected format""" - op = coeff * observable(wire) - obs = measurement_type(op) - data = "" - for i in range(register_size - 1, -1, -1): - if i == wire: - data += obs_string - else: - data += "I" - - pauli_op = mp_to_pauli(obs, register_size) - assert isinstance(pauli_op, SparsePauliOp) - - pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - - # all qubits in register are accounted for - assert len(pauli_op_list) == register_size - assert pauli_op == SparsePauliOp(data=data, coeffs=[coeff]) - - - @pytest.mark.parametrize("measurement_type", [qml.expval]) - @pytest.mark.parametrize("register_size", [3]) - def test_mp_to_pauli_for_hamiltonian(self, measurement_type, register_size): - """Tests that a SparsePauliOp is created from a Hamiltonian, and that - it has the expected format""" - hamiltonian = qml.Hamiltonian([1, 2], [qml.X(0) @ qml.Z(1), qml.Z(2) @ qml.Y(1)]) ## 1 * X(0) @ Z (1) + 2 * Z(2) @ Y (1) - obs = measurement_type(hamiltonian) - - pauli_op = mp_to_pauli(obs, register_size) - assert isinstance(pauli_op, SparsePauliOp) - - pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - - # all qubits in register are accounted for - assert len(pauli_op_list) == register_size - class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" From 214cc32f2a3611a6a07f8606af4be2942efcb048 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:23:21 -0400 Subject: [PATCH 12/47] Install PL dev intead of PL stable on CI (#516) (#518) * install PL dev intead of PL stable on CI * trigger ci * also a couple of other places Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- .github/workflows/tests.yml | 4 ++++ .github/workflows/tests_qiskit_1.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c5d43a89a..3703508a6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install git+https://github.com/PennyLaneAI/pennylane.git pip install -r requirements-ci-legacy.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze @@ -70,13 +71,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install git+https://github.com/PennyLaneAI/pennylane.git pip install -r requirements-ci-legacy.txt pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade + pip freeze - name: Install Plugin run: | python setup.py bdist_wheel pip install dist/PennyLane*.whl + pip freeze - name: Run tests run: | diff --git a/.github/workflows/tests_qiskit_1.yml b/.github/workflows/tests_qiskit_1.yml index 103abf4ef..1ad3c3b53 100644 --- a/.github/workflows/tests_qiskit_1.yml +++ b/.github/workflows/tests_qiskit_1.yml @@ -29,6 +29,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install git+https://github.com/PennyLaneAI/pennylane.git pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze @@ -67,6 +68,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install git+https://github.com/PennyLaneAI/pennylane.git pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade From 15931507ea3de081f6165ff41937ef78cae4dbce Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Thu, 25 Apr 2024 14:48:58 -0400 Subject: [PATCH 13/47] Delete ds store --- .DS_Store | Bin 8196 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 0622312c937da06b2686d5d54f1dbed3a15b3700..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM%We}f6g_TJ5zz`of<*u!W(kW#>K~*f0V0)$2>X)9P^#ud9xdIV+4B$l0^i39 z&b_uOduAepDxpf)k$onfd#W1gOy&K1jN(^J^yq}0UvE?wcXz4JP4&zofu0k=o zI(dnj!?Y~MF)9!hC@LVe`wj+J;}qkdewPo@0ekb8Hl^<28D4VsHC~{DBfKFtF)8qV z58wT}IMj96fY$)<1LBRlGtUXGrl;_n3zE zTPHdDUE0eIyLiqOdU($ro?;rZRGIIdGBwP(!j!K|>ewfyIL2q)^%=+9&LIl<{QG!& z#Xl7znG3wsHhL-NQ{#YN Date: Thu, 25 Apr 2024 15:33:34 -0400 Subject: [PATCH 14/47] import change --- pennylane_qiskit/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 76e4d5744..ff0d816f8 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -21,7 +21,6 @@ import numpy as np from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit @@ -35,6 +34,7 @@ import pennylane as qml import pennylane.ops as pennylane_ops +from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} From f0c6dc94d30abb377161f045a6f3b59829bac7a4 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Thu, 25 Apr 2024 17:54:35 -0400 Subject: [PATCH 15/47] black --- tests/test_base_device.py | 27 ++++++++++++++++----------- tests/test_qiskit_device.py | 20 ++++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 9e486de3c..a66461b49 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -86,7 +86,8 @@ def run(self, *args, **kwargs): @property def target(self): return self._target - + + class MockedBackendLegacy(BackendV1): def __init__(self, num_qubits=10, name="mocked_backend_legacy"): self._configuration = Configuration(num_qubits, backend_name=name) @@ -106,6 +107,7 @@ def run(self, *args, **kwargs): def options(self): return self._options + class MockSession: def __init__(self, backend, max_time=None): self.backend = backend @@ -135,6 +137,7 @@ def options_for_testing(): options.resilience_level = 1 return options + class TestSupportForV1andV2: """Tests compatibility with BackendV1 and BackendV2""" @@ -149,30 +152,32 @@ def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" dev = QiskitDevice2(wires=10, backend=backend, use_primitives=True) assert dev._backend == backend - - @pytest.mark.skip(reason="Fake backends do not have attribute _service, should address in (SC 55725)") + @pytest.mark.skip( + reason="Fake backends do not have attribute _service, should address in (SC 55725)" + ) @pytest.mark.parametrize( - "backend", - [ - FakeManila(), - FakeManilaV2(), - ] + "backend", + [ + FakeManila(), + FakeManilaV2(), + ], ) def test_v1_and_v2_manila(self, backend): """Test that device initializes with no error with V1 and V2 backends by Qiskit""" dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) - + @qml.qnode(dev) def circuit(x): qml.RX(x, wires=[0]) qml.CNOT(wires=[0, 1]) return qml.sample(qml.PauliZ(0)) - - res = circuit(np.pi/2) + + res = circuit(np.pi / 2) assert isinstance(res, np.ndarray) assert np.shape(res) == (1024,) + class TestDeviceInitialization: def test_compile_backend_kwarg(self): """Test that the compile_backend is set correctly if passed, and the main diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 5aec93c91..5f136e00a 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -5,7 +5,7 @@ from pennylane_qiskit import AerDevice from pennylane_qiskit.qiskit_device import QiskitDevice from qiskit_aer import noise -from qiskit.providers import BackendV1, BackendV2 +from qiskit.providers import BackendV1, BackendV2 from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 from unittest.mock import Mock from qiskit_ibm_runtime.options import Options @@ -92,25 +92,25 @@ def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" dev = qml.device("qiskit.remote", wires=10, backend=backend, use_primitives=True) assert dev._backend == backend - + @pytest.mark.parametrize( - "backend", - [ - FakeManila(), - FakeManilaV2(), - ] + "backend", + [ + FakeManila(), + FakeManilaV2(), + ], ) def test_v1_and_v2_manila(self, backend): """Test that device initializes with no error with V1 and V2 backends by Qiskit""" dev = qml.device("qiskit.remote", wires=5, backend=backend, use_primitives=True) - + @qml.qnode(dev) def circuit(x): qml.RX(x, wires=[0]) qml.CNOT(wires=[0, 1]) return qml.sample(qml.PauliZ(0)) - - res = circuit(np.pi/2) + + res = circuit(np.pi / 2) assert isinstance(res, np.ndarray) assert np.shape(res) == (1024,) From 1b85a3acf1c004e14c7191fbd8337cc2f3e29bd5 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 30 Apr 2024 14:33:37 -0400 Subject: [PATCH 16/47] some changes --- tests/test_base_device.py | 49 ++++++++++++++++++------------------- tests/test_converter.py | 3 +-- tests/test_qiskit_device.py | 5 ---- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index a66461b49..f49601577 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -15,31 +15,15 @@ This module contains tests for the base Qiskit device for the new PennyLane device API """ +from unittest.mock import patch, Mock import numpy as np import pytest -import inspect -from unittest.mock import patch, Mock from semantic_version import Version import qiskit_ibm_runtime import pennylane as qml from pennylane.tape.qscript import QuantumScript - -from pennylane_qiskit import AerDevice -from pennylane_qiskit.qiskit_device2 import ( - QiskitDevice2, - qiskit_session, - accepted_sample_measurement, - split_measurement_types, - qiskit_options_to_flat_dict, -) -from pennylane_qiskit.converter import ( - circuit_to_qiskit, - mp_to_pauli, - QISKIT_OPERATION_MAP, -) - -from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator +from qiskit_ibm_runtime import QiskitRuntimeService, Estimator from qiskit_ibm_runtime.options import Options from qiskit_ibm_runtime.constants import RunnerResult from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 @@ -53,9 +37,22 @@ from qiskit import QuantumCircuit -from qiskit_aer.noise import NoiseModel +from pennylane_qiskit.qiskit_device2 import ( + QiskitDevice2, + qiskit_session, + split_measurement_types, + qiskit_options_to_flat_dict, +) +from pennylane_qiskit.converter import ( + circuit_to_qiskit, + mp_to_pauli, + QISKIT_OPERATION_MAP, +) +# pylint: disable=protected-access, unused-argument, too-many-arguments, redefined-outer-name + +# pylint: disable=too-few-public-methods class Configuration: def __init__(self, n_qubits, backend_name): self.n_qubits = n_qubits @@ -108,6 +105,7 @@ def options(self): return self._options +# pylint: disable=too-few-public-methods class MockSession: def __init__(self, backend, max_time=None): self.backend = backend @@ -118,10 +116,11 @@ def close(self): # This is just to appease a test pass +# pylint: disable=broad-except try: service = QiskitRuntimeService(channel="ibm_quantum") backend = service.backend("ibmq_qasm_simulator") -except: +except Exception: backend = MockedBackend() legacy_backend = MockedBackendLegacy() @@ -251,7 +250,7 @@ def test_backend_wire_validation(self, backend): the number of wires available on the backend, for both backend versions""" with pytest.raises(ValueError, match="supports maximum"): - dev = QiskitDevice2(wires=500, backend=backend) + QiskitDevice2(wires=500, backend=backend) def test_setting_simulator_noise_model(self): """Test that the simulator noise model saved on a passed Options @@ -264,7 +263,7 @@ def test_setting_simulator_noise_model(self): dev1 = QiskitDevice2(wires=3, backend=backend) dev2 = QiskitDevice2(wires=3, backend=new_backend, options=options) - assert dev1.backend.options.noise_model == None + assert dev1.backend.options.noise_model is None assert dev2.backend.options.noise_model == {"placeholder": 1} @@ -275,7 +274,7 @@ def test_default_no_session_on_initialization(self): """Test that the default behaviour is no session at initialization""" dev = QiskitDevice2(wires=2, backend=backend) - assert dev._session == None + assert dev._session is None def test_initializing_with_session(self): """Test that you can initialize a device with an existing Qiskit session""" @@ -759,7 +758,7 @@ def test_compile_circuits(self, transpile_mock, compile_backend): ) assert len(compiled_circuits) == len(input_circuits) - for i, circuit in enumerate(compiled_circuits): + for _, circuit in enumerate(compiled_circuits): assert isinstance(circuit, QuantumCircuit) @pytest.mark.parametrize( @@ -894,7 +893,7 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): qml.sample(), ], ) - tapes, reorder_fn = split_measurement_types(qs) + tapes, _ = split_measurement_types(qs) with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): diff --git a/tests/test_converter.py b/tests/test_converter.py index 708b82659..3b0c6256f 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -28,6 +28,7 @@ import pennylane as qml from pennylane import numpy as np +from pennylane.tape.qscript import QuantumScript from pennylane.wires import Wires from pennylane_qiskit.converter import ( load, @@ -42,8 +43,6 @@ _check_parameter_bound, ) -from pennylane.tape.qscript import QuantumScript - # pylint: disable=protected-access, unused-argument, too-many-arguments diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index a722293ef..57b4d2aa9 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -25,11 +25,6 @@ import pennylane as qml from pennylane_qiskit import AerDevice from pennylane_qiskit.qiskit_device import QiskitDevice -from qiskit_aer import noise -from qiskit.providers import BackendV1, BackendV2 -from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 -from unittest.mock import Mock -from qiskit_ibm_runtime.options import Options # pylint: disable=protected-access, unused-argument, too-few-public-methods From d21adf8a05afbb75463d46b83654e97d84ae77cb Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 30 Apr 2024 14:37:57 -0400 Subject: [PATCH 17/47] linters --- pennylane_qiskit/qiskit_device2.py | 2 +- tests/test_base_device.py | 2 +- tests/test_converter.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 88778d7c4..4ff7637f0 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -469,7 +469,7 @@ def compile_circuits(self, circuits): return compiled_circuits - # pylint: disable=unused-argument + # pylint: disable=unused-argument, no-member def execute( self, circuits: QuantumTape_or_Batch, diff --git a/tests/test_base_device.py b/tests/test_base_device.py index f49601577..fbc8e90f6 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -869,7 +869,7 @@ def test_execute_pipeline_primitives_no_session(self, mocker): qs = QuantumScript([qml.PauliX(0), qml.PauliY(1)], measurements=[qml.expval(qml.PauliZ(0))]) with patch("pennylane_qiskit.qiskit_device2.Session") as mock_session: - res = dev.execute(qs) + dev.execute(qs) mock_session.assert_called_once() # a session was created assert dev._session is None # the device session is still None diff --git a/tests/test_converter.py b/tests/test_converter.py index 3b0c6256f..5f124dc29 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1789,7 +1789,7 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) - +# pylint:disable=not-context-manager class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" @@ -2005,6 +2005,7 @@ def circuit_native_pennylane(angle): assert np.allclose(qnode(0.543), circuit_native_pennylane(0.543)) + # pylint:disable=unused-variable def test_mid_circuit_as_terminal(self): """Test the control workflows where mid-circuit measurements disguise as terminal ones""" From 009aa763fa4888d09b2e1465eba1c46bf7b17737 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 30 Apr 2024 14:39:19 -0400 Subject: [PATCH 18/47] black --- tests/test_converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_converter.py b/tests/test_converter.py index 5f124dc29..206a17462 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1789,6 +1789,7 @@ def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, regis # remaining wires are all Identity assert np.all([op == "I" for op in pauli_op_list]) + # pylint:disable=not-context-manager class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" From 0943af7355d837fdde610c89633a6cb2375d961c Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 30 Apr 2024 14:46:19 -0400 Subject: [PATCH 19/47] pylinting --- tests/test_converter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 206a17462..f88ac9bb8 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1634,8 +1634,6 @@ def test_circuit_to_qiskit_measure_kwarg(self, operations, final_op_name, measur if measure: assert final_instruction.operation.name == "measure" - else: - final_instruction.operation.name == final_op_name @pytest.mark.parametrize("diagonalize", [True, False]) def test_circuit_to_qiskit_diagonalize_kwarg(self, diagonalize): @@ -1757,7 +1755,7 @@ def test_with_predefined_creg(self): assert wires1 == wires2 == [2] assert params1 == params2 == [[1.23]] - +# pylint:disable=too-few-public-methods class TestConverterUtilsPennyLaneToQiskit: @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) From 98a30179834f59abea20c3f76b3877937fe4cbf9 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 30 Apr 2024 14:48:58 -0400 Subject: [PATCH 20/47] black --- tests/test_converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_converter.py b/tests/test_converter.py index f88ac9bb8..90ae2ef1b 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1755,6 +1755,7 @@ def test_with_predefined_creg(self): assert wires1 == wires2 == [2] assert params1 == params2 == [[1.23]] + # pylint:disable=too-few-public-methods class TestConverterUtilsPennyLaneToQiskit: From cbc23a367c1b6409ae8a9a2fc6c367050c43e99d Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 1 May 2024 09:12:42 -0400 Subject: [PATCH 21/47] small change --- tests/test_base_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index fbc8e90f6..969adcf8c 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -116,11 +116,11 @@ def close(self): # This is just to appease a test pass -# pylint: disable=broad-except +# pylint: disable=bare-except try: service = QiskitRuntimeService(channel="ibm_quantum") backend = service.backend("ibmq_qasm_simulator") -except Exception: +except: backend = MockedBackend() legacy_backend = MockedBackendLegacy() From b25dda39a3f1655cf1b137143146d22c0ebe4745 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 1 May 2024 09:25:50 -0400 Subject: [PATCH 22/47] change conftest --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index dcc3fad91..e31650874 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,11 +59,12 @@ @pytest.fixture +# pylint: disable=broad-except def skip_if_no_account(): t = os.getenv("IBMQX_TOKEN", None) try: IBMProvider(token=t) - except IBMProviderValueError: + except Exception: missing = "token" if t else "account" pytest.skip(f"Skipping test, no IBMQ {missing} available") From 64e0a4edc2f3b6f56203b13c3a5519cb50dd1e01 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 1 May 2024 09:29:29 -0400 Subject: [PATCH 23/47] black linter --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e31650874..ee7b811e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ import pennylane as qml from semantic_version import Version -from qiskit_ibm_provider import IBMProvider, IBMProviderValueError +from qiskit_ibm_provider import IBMProvider from pennylane_qiskit import AerDevice, BasicAerDevice, BasicSimulatorDevice # pylint: disable=protected-access, unused-argument, redefined-outer-name @@ -58,8 +58,8 @@ ] -@pytest.fixture # pylint: disable=broad-except +@pytest.fixture def skip_if_no_account(): t = os.getenv("IBMQX_TOKEN", None) try: From f47e2852b54c90f2452901227ec1c91a16a7708c Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 1 May 2024 10:16:29 -0400 Subject: [PATCH 24/47] fix --- tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ee7b811e8..06fdb56ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,13 +58,12 @@ ] -# pylint: disable=broad-except @pytest.fixture def skip_if_no_account(): t = os.getenv("IBMQX_TOKEN", None) try: IBMProvider(token=t) - except Exception: + except: # pylint: disable=broad-except, bare-except missing = "token" if t else "account" pytest.skip(f"Skipping test, no IBMQ {missing} available") From 6c37e6cc4d5f77756e96a85b87c8c9d8fe88a5e1 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Wed, 1 May 2024 14:14:42 -0400 Subject: [PATCH 25/47] Generalize mp_to_pauli so that it can work with any observable that has a Pauli Rep (#517) * tests * converter * fixes to tests * refactor * delete print * black * pylint changes * deleted tests/pylintrc * one additional test * black * tests * redo commits * clean up * black * formatting * black * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * rewrite tests to all manual cases * [skip-ci] black * single qubit operations * value error for no pauli rep * list comprehension * removed one list comprehension * ehh honestly this is fine too * delete some useless lines * [skip-ci] refactor * [skip ci] accidentally deleted something...? * Update tests/test_converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * remove parametrize * [skip ci] black * [skip ci] docstring * [skip ci] added assert statement * [skip-ci] linear comb changes * additional more complicated integration tests * undo some formatting * formats * integration tests * tests * deleted unsupported observables * remove sparsehamiltonian for now * rollback pylint * revert observable stopping condition * black * just a commit * black * fm * Update tests/test_base_device.py Co-authored-by: Utkarsh * linter errors * linter * linter * linters * try finally block better? * revert * let's just do this... * trailing whitespace * linter * black * formats * black * black * clean up commits * reformatting fixed * clean up commit --------- Co-authored-by: Utkarsh Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- pennylane_qiskit/converter.py | 32 ++++--- tests/test_base_device.py | 76 ++++++++++++++- tests/test_converter.py | 171 ++++++++++++++++++++++++++++++---- 3 files changed, 243 insertions(+), 36 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 2c810bf6a..495c557a8 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1,4 +1,4 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. +# Copyright 2021-2024 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -356,7 +356,7 @@ def load(quantum_circuit: QuantumCircuit, measurements=None): function: The resulting PennyLane template. """ - # pylint:disable=too-many-branches, fixme, protected-access + # pylint:disable=too-many-branches, fixme, protected-access, too-many-nested-blocks def _function(*args, params: dict = None, wires: list = None, **kwargs): """Returns a PennyLane quantum function created based on the input QuantumCircuit. Warnings are created for each of the QuantumCircuit instructions that were @@ -707,20 +707,22 @@ def mp_to_pauli(mp, register_size): mp(Union[ExpectationMP, VarianceMP]): MeasurementProcess to be converted to a SparsePauliOp register_size(int): total size of the qubit register being measured """ + op = mp.obs + + if op.pauli_rep: + pauli_strings = [ + "".join( + ["I" if i not in pauli_term.wires else pauli_term[i] for i in range(register_size)][ + ::-1 + ] ## Qiskit follows opposite wire order convention + ) + for pauli_term in op.pauli_rep.keys() + ] + coeffs = list(op.pauli_rep.values()) + else: + raise ValueError(f"The operator {op} does not have a representation for SparsePauliOp") - # ToDo: I believe this could be extended to cover expectation values of Hamiltonians - - observables = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z", "Identity": "I"} - - pauli_string = ["I"] * register_size - pauli_string[mp.wires[0]] = observables[mp.obs.name] - - # Qiskit orders wires in the opposite direction compared to PL - pauli_string.reverse() - - pauli_string = ("").join(pauli_string) - - return SparsePauliOp(pauli_string) + return SparsePauliOp(data=pauli_strings, coeffs=coeffs).simplify() def load_pauli_op( diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 969adcf8c..33f868aaa 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -36,7 +36,6 @@ from qiskit.providers import BackendV1, BackendV2 from qiskit import QuantumCircuit - from pennylane_qiskit.qiskit_device2 import ( QiskitDevice2, qiskit_session, @@ -1043,6 +1042,64 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec assert np.allclose(res, expectation, atol=0.1) + @pytest.mark.parametrize("wire", [0, 1, 2, 3]) + @pytest.mark.parametrize( + "angle, op, multi_q_obs", + [ + ( + np.pi / 2, + qml.RX, + qml.ops.LinearCombination([1, 3], [qml.X(3) @ qml.Y(1), qml.Z(0) * 3]), + ), + ( + np.pi, + qml.RX, + qml.ops.LinearCombination([1, 3], [qml.X(3) @ qml.Y(1), qml.Z(0) * 3]) + - 4 * qml.X(2), + ), + (np.pi / 2, qml.RY, qml.sum(qml.PauliZ(0), qml.PauliX(1))), + (np.pi, qml.RY, qml.dot([2, 3], [qml.X(0), qml.Y(0)])), + ( + np.pi / 2, + qml.RZ, + qml.Hamiltonian([1], [qml.X(0) @ qml.Y(2)]) - 3 * qml.Z(3) @ qml.Z(1), + ), + ], + ) + def test_estimator_with_various_multi_qubit_pauli_obs( + self, mocker, wire, angle, op, multi_q_obs + ): + """Test that the Estimator with various multi-qubit observables returns expected results. + Essentially testing that the conversion to PauliOps in _execute_estimator behaves as + expected. Iterating over wires ensures that the wire operated on and the wire measured + correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) + """ + + pl_dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) + dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend, use_primitives=True) + + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + qs = QuantumScript( + [op(angle, wire)], + measurements=[ + qml.expval(multi_q_obs), + qml.var(multi_q_obs), + ], + shots=10000, + ) + + res = dev.execute(qs) + expectation = pl_dev.execute(qs) + + runtime_service_execute.assert_not_called() + sampler_execute.assert_not_called() + estimator_execute.assert_called_once() + + assert np.allclose(res, expectation, atol=0.3) ## atol is high due to high variance + @pytest.mark.parametrize( "measurements, expectation", [ @@ -1056,10 +1113,22 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec ], (0, 1, 1), ), + ([qml.expval(0.5 * qml.Y(0) + 0.5 * qml.Y(0) - 1.5 * qml.X(0) - 0.5 * qml.Y(0))], (0)), + ( + [ + qml.expval( + qml.ops.LinearCombination( + [1, 3, 4], [qml.X(3) @ qml.Y(2), qml.Y(4) - qml.X(2), qml.Z(2) * 3] + ) + + qml.X(4) + ) + ], + (16), + ), ], ) def test_process_estimator_job(self, measurements, expectation): - """for variance and for expval and for a combination""" + """Tests that the estimator returns expected and accurate results for an ``expval`` and ``var`` for a variety of multi-qubit observables""" # make PennyLane circuit qs = QuantumScript([], measurements=measurements) @@ -1085,10 +1154,9 @@ def test_process_estimator_job(self, measurements, expectation): for data in result.metadata: assert isinstance(data, dict) assert list(data.keys()) == ["variance", "shots"] - processed_result = QiskitDevice2._process_estimator_job(qs.measurements, result) assert isinstance(processed_result, tuple) - assert np.allclose(processed_result, expectation, atol=0.05) + assert np.allclose(processed_result, expectation, atol=0.1) @pytest.mark.parametrize("num_wires", [1, 3, 5]) @pytest.mark.parametrize("num_shots", [50, 100]) diff --git a/tests/test_converter.py b/tests/test_converter.py index 90ae2ef1b..deedda94e 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -27,6 +27,7 @@ from qiskit.quantum_info import SparsePauliOp import pennylane as qml +from pennylane import I, X, Y, Z from pennylane import numpy as np from pennylane.tape.qscript import QuantumScript from pennylane.wires import Wires @@ -43,7 +44,6 @@ _check_parameter_bound, ) - # pylint: disable=protected-access, unused-argument, too-many-arguments THETA = np.linspace(0.11, 3, 5) @@ -1583,7 +1583,6 @@ def test_diff_meas_circuit(self): class TestConverterPennyLaneCircuitToQiskit: - def test_circuit_to_qiskit(self): """Test that a simple PennyLane circuit is converted to the expected Qiskit circuit""" @@ -1662,7 +1661,6 @@ def test_circuit_to_qiskit_diagonalize_kwarg(self, diagonalize): class TestConverterGatePennyLaneToQiskit: - def test_non_parameteric_operation_to_qiskit(self): """Test that a non-parameteric operation is correctly converted to a Qiskit circuit with a single operation""" @@ -1758,35 +1756,174 @@ def test_with_predefined_creg(self): # pylint:disable=too-few-public-methods class TestConverterUtilsPennyLaneToQiskit: + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "operator, expected", + [ + (qml.X(0), SparsePauliOp("IIIIX")), + (qml.I(1), SparsePauliOp("IIIII")), + (Y(0), SparsePauliOp("IIIIY")), + (qml.PauliZ(0), SparsePauliOp("IIIIZ")), + ( + X(0) + I(0) + 2 * Y(1) + I(1), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIII") + + 2 * SparsePauliOp("IIIYI") + + SparsePauliOp("IIIII"), + ), + ( + qml.X(0) + qml.X(0) + qml.Y(1) + qml.Z(2), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIYI") + + SparsePauliOp("IIZII"), + ), + ( + qml.sum(X(0) + X(0) + Y(1) + Z(2)), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIYI") + + SparsePauliOp("IIZII"), + ), + ( + (qml.X(0) + 2 * qml.Y(1)), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIYI"), + ), + ( + qml.sum(X(0) + qml.s_prod(2, Y(1))), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIYI"), + ), + (qml.X(0) + qml.Y(0), SparsePauliOp("IIIIX") + SparsePauliOp("IIIIY")), + ( + 0.5 * X(0) + 3 * (X(2) + qml.PauliY(1)), + 0.5 * SparsePauliOp("IIIIX") + + 3 * (SparsePauliOp("IIXII") + SparsePauliOp("IIIYI")), + ), + ( + 0.5 * X(0) + 0.5 * qml.Y(0) - 1.5 * qml.X(0) - 0.5 * qml.Y(0), + 0.5 * SparsePauliOp("IIIIX") + + 0.5 * SparsePauliOp("IIIIY") + - 1.5 * SparsePauliOp("IIIIX") + - 0.5 * SparsePauliOp("IIIIY"), + ), + ( + qml.ops.LinearCombination( + [1, 3, 4], + [X(3) @ Y(2), Y(4) - X(2), Z(2) * 3], + ) + + qml.X(4), + 1 * SparsePauliOp("IXIII") @ SparsePauliOp("IIYII") + + 3 * (SparsePauliOp("YIIII") - SparsePauliOp("IIXII")) + + 3 * 4 * SparsePauliOp("IIZII") + + SparsePauliOp("XIIII"), + ), + ], + ) + def test_mp_to_pauli_for_general_operator(self, measurement_type, operator, expected): + """Tests that a SparsePauliOp is created given any general operator that has a Pauli representation, and that it has the expected format""" + obs = measurement_type(operator) + register_size = 5 + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "operator, expected", + [ + (X(0) @ Y(1), SparsePauliOp("IIX") @ (SparsePauliOp("IYI"))), + ( + (X(0) + Y(1)) @ Y(1), + (SparsePauliOp("IIX") + SparsePauliOp("IYI")) @ (SparsePauliOp("IYI")), + ), + ( + (X(0) + Y(1)) @ (Z(0) + Z(1)), + (SparsePauliOp("IIX") + SparsePauliOp("IYI")) + @ (SparsePauliOp("IIZ") + SparsePauliOp("IZI")), + ), + ( + 2 * (X(0) + Y(1)) @ ((Z(0) + Z(1)) @ Z(2)), + 2 + * (SparsePauliOp("IIX") + SparsePauliOp("IYI")) + @ (SparsePauliOp("IIZ") + SparsePauliOp("IZI")) + @ SparsePauliOp("ZII"), + ), + ( + 0.5 * (X(0) @ X(1)) + 0.7 * (X(1) @ X(2)) + 0.8 * (X(2) @ X(1)), + 0.5 * (SparsePauliOp("IIX") @ SparsePauliOp("IXI")) + + 0.7 * (SparsePauliOp("IXI") @ SparsePauliOp("XII")) + + 0.8 * (SparsePauliOp("XII") @ SparsePauliOp("IXI")), + ), + ], + ) + def test_mp_to_pauli_tensor_products(self, measurement_type, operator, expected): + """Tests that a SparsePauliOp is created given any general operator that has a Pauli representation, and that it is accurate""" + obs = measurement_type(operator) + register_size = 3 + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) @pytest.mark.parametrize( - "observable, obs_string", - [(qml.PauliX, "X"), (qml.PauliY, "Y"), (qml.PauliZ, "Z"), (qml.Identity, "I")], + "hamiltonian, expected", + [ + ( + qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]), + SparsePauliOp(["IIIIX", "IIIXI"], [1, 2]), + ), + ( + qml.Hamiltonian([3, -2], [qml.X(0), qml.X(0)]), + SparsePauliOp(["IIIIX", "IIIIX"], [3, -2]), + ), + ( + qml.Hamiltonian([-3, 3, 0.5, 5], [qml.X(0), qml.X(0), qml.Z(1), qml.Y(2)]), + SparsePauliOp(["IIIIX", "IIIIX", "IIIZI", "IIYII"], [-3, 3, 0.5, 5]), + ), + ( + qml.Hamiltonian([1], [qml.X(0)]) + 2 * qml.Z(0) @ qml.Z(1), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIIZ") @ SparsePauliOp("IIIZI"), + ), + ( + qml.Hamiltonian([1], [qml.X(0) @ Y(2)]) - 3 * qml.Z(4) @ qml.Z(1), + (SparsePauliOp("IIIIX") @ SparsePauliOp("IIYII")) + - 3 * SparsePauliOp("ZIIII") @ SparsePauliOp("IIIZI"), + ), + ], ) - @pytest.mark.parametrize("wire", [0, 1, 2]) - @pytest.mark.parametrize("register_size", [3, 5]) - def test_mp_to_pauli(self, measurement_type, observable, obs_string, wire, register_size): - """Tests that a SparsePauliOp is created from a Pauli observable, and that + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, hamiltonian, expected): + """Tests that a SparsePauliOp is created from a Hamiltonian, and that it has the expected format""" - obs = measurement_type(observable(wire)) + obs = measurement_type(hamiltonian) + register_size = 5 pauli_op = mp_to_pauli(obs, register_size) assert isinstance(pauli_op, SparsePauliOp) pauli_op_list = list(pauli_op.paulis.to_labels()[0]) - # all qubits in register are accounted for assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + def test_mp_to_pauli_error_for_no_pauli_rep(self, measurement_type): + """Tests that an error is raised when mp_to_pauli is given an operator that does not have a pauli representation""" - # the wire the observable acts on is correctly labelled - # wire order reversed in Qiskit, so we put it back to use PL wire as an index - pauli_op_list.reverse() - assert pauli_op_list.pop(wire) == obs_string + obs = measurement_type(qml.X(0) @ qml.Hadamard(2)) - # remaining wires are all Identity - assert np.all([op == "I" for op in pauli_op_list]) + assert not obs.obs.pauli_rep + with pytest.raises(ValueError, match="The operator"): + mp_to_pauli(obs, 5) # pylint:disable=not-context-manager From 3c5dec3dd6939d77a953bb061391fc00fb8a06ae Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Tue, 7 May 2024 12:41:54 -0400 Subject: [PATCH 26/47] Use old _execute_runtime_service for observables that are not compatible with SPO representation (#525) * Functionality * syntax error * additional test * additional test cases * edit comment * comment * additional comments * added a warning * added a warning * delete print statement * warning test * delete a test * black * Minor adjustment to sort observables. Modified tests to accomodate. Doc strings edit * Modified tests and warning * edit * fixes * moved test to mocked * mockedbackend update * black * changes * test * uncomment * docstring * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * black * fix --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 28 +++++++--- tests/test_base_device.py | 88 ++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 4ff7637f0..986142a94 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -124,10 +124,12 @@ def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: @transform -def split_measurement_types( +def split_execution_types( tape: qml.tape.QuantumTape, ) -> (Sequence[qml.tape.QuantumTape], Callable): - """Split into separate tapes based on measurement type. Counts will use the + """Split into separate tapes based on measurement type. However, for ``expval`` and ``var`` + measurements, if the measured observable does not have a ``pauli_rep``, it is split as a + separate tape and will use the standard backend.run function. Counts will use the Qiskit Sampler, ExpectationValue and Variance will use the Estimator, and other strictly sample-based measurements will use the standard backend.run function""" @@ -137,7 +139,15 @@ def split_measurement_types( for i, mp in enumerate(tape.measurements): if isinstance(mp, (ExpectationMP, VarianceMP)): - estimator.append((mp, i)) + if mp.obs.pauli_rep: + estimator.append((mp, i)) + else: + warnings.warn( + f"The observable measured {mp.obs} does not have a `pauli_rep` " + "and will be run without using the Estimator primitive. Instead, " + "the standard backend.run function will be used." + ) + no_prim.append((mp, i)) elif isinstance(mp, ProbabilityMP): sampler.append((mp, i)) else: @@ -401,10 +411,10 @@ def preprocess( ) transform_program.add_transform(broadcast_expand) - # missing: split non-commuting, sum_expand, etc. + # missing: split non-commuting, sum_expand, etc. [SC-62047] if self._use_primitives: - transform_program.add_transform(split_measurement_types) + transform_program.add_transform(split_execution_types) return transform_program, config @@ -495,7 +505,9 @@ def execute_circuits(session): f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." f"The circuit will be run once with {circ.shots.total_shots} shots instead." ) - if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)): + if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)) and getattr( + circ.measurements[0].obs, "pauli_rep", None + ): execute_fn = self._execute_estimator elif isinstance(circ.measurements[0], ProbabilityMP): execute_fn = self._execute_sampler @@ -556,6 +568,8 @@ def _execute_runtime_service(self, circuits, session): results = [] + ### Note: this assumes that the input values are valid. + ### Don't write tests cases that require transforms not yet implemented (not here). for index, circuit in enumerate(circuits): self._samples = self.generate_samples(index) res = [ @@ -599,8 +613,6 @@ def _execute_estimator(self, circuit, session): # for expectation value and variance on the same observable, but spending time on # that right now feels excessive - # ToDo: need to sort differently for cases where the observable is not - # compatible with a SparsePauliOp representation pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] result = estimator.run([qcirc] * len(pauli_observables), pauli_observables).result() self._current_job = result diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 33f868aaa..307e9c74e 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -39,7 +39,7 @@ from pennylane_qiskit.qiskit_device2 import ( QiskitDevice2, qiskit_session, - split_measurement_types, + split_execution_types, qiskit_options_to_flat_dict, ) from pennylane_qiskit.converter import ( @@ -374,15 +374,28 @@ class TestDevicePreprocessing: [qml.probs(wires=[2])], [[qml.probs(wires=[2])]], ), + ( + [ + qml.expval(qml.Hadamard(0)), + qml.expval(qml.PauliX(0)), + qml.var(qml.PauliZ(0)), + qml.counts(), + ], + [ + [qml.expval(qml.PauliX(0)), qml.var(qml.PauliZ(0))], + [qml.expval(qml.Hadamard(0)), qml.counts()], + ], + ), ], ) - def test_split_measurement_types(self, measurements, expectation): - """Test that the split_measurement_types transform splits measurements into Estimator-based + @pytest.mark.filterwarnings("ignore::UserWarning") + def test_split_execution_types(self, measurements, expectation): + """Test that the split_execution_types transform splits measurements into Estimator-based (expval, var), Sampler-based (probs) and raw-sample based (everything else)""" operations = [qml.PauliX(0), qml.PauliY(1), qml.Hadamard(2), qml.CNOT([2, 1])] qs = QuantumScript(operations, measurements=measurements) - tapes, reorder_fn = split_measurement_types(qs) + tapes, reorder_fn = split_execution_types(qs) # operations not modified assert np.all([tape.operations == operations for tape in tapes]) @@ -892,7 +905,7 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): qml.sample(), ], ) - tapes, _ = split_measurement_types(qs) + tapes, _ = split_execution_types(qs) with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): @@ -1270,3 +1283,68 @@ def circuit(): # Should reset to device shots if circuit ran again without shots defined circuit() assert dev._current_job.metadata[0]["shots"] == 2 + + @pytest.mark.parametrize( + "observable", + [ + [qml.Hadamard(0), qml.PauliX(1)], + [qml.PauliZ(0), qml.Hadamard(1)], + [qml.PauliZ(0), qml.Hadamard(0)], + ], + ) + @pytest.mark.filterwarnings("ignore::UserWarning") + def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): + """Test that the device uses _execute_runtime_service and _execute_estimator appropriately + and provides an accurate answer for measurements with observables that don't have a pauli_rep. + """ + + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + + pl_dev = qml.device("default.qubit", wires=5) + + runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") + estimator_execute = mocker.spy(dev, "_execute_estimator") + sampler_execute = mocker.spy(dev, "_execute_sampler") + + @qml.qnode(dev) + def circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(observable[0]), qml.expval(observable[1]) + + @qml.qnode(pl_dev) + def pl_circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(observable[0]), qml.expval(observable[1]) + + res = circuit() + pl_res = pl_circuit() + + runtime_service_execute.assert_called_once() + estimator_execute.assert_called_once() + sampler_execute.assert_not_called() + + assert np.allclose(res, pl_res, atol=0.1) + + def test_warning_for_split_execution_types_when_observable_no_pauli(self): + """Test that a warning is raised when device is passed a measurement on + an observable that does not have a pauli_rep.""" + + dev = QiskitDevice2( + wires=5, + backend=backend, + use_primitives=True, + ) + + @qml.qnode(dev) + def circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(qml.Hadamard(0)) + + with pytest.warns( + UserWarning, + match="The observable measured", + ): + circuit() From f78851a4681ae8f5fecf2b395db02032bc41349c Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 10 May 2024 09:47:39 -0400 Subject: [PATCH 27/47] Move off of "ibmq_qasm_simulator" and towards using AerSimulator for qiskit tests (#521) * import aersimulator * commit * black * added functionality for local simulators when use_primitves = False * [skip ci] changes to AerSim * removal of ibm service * tests * woops * temp fixes * unfinished changes * ignore tests due to version errors * ignore tests due to version errors * black * cleanup * delete comment * revert a change * replaced a test case * shouldn't need to skip if no acc anymore * added comments * delete comment * minor refactor * Added additional comments * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * [skip ci] delete comment * add comment --------- Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 42 ++++++++++++++----- tests/test_base_device.py | 66 +++++++++++++++--------------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 986142a94..a1082e8df 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -288,8 +288,7 @@ def __init__( self._backend = backend self._compile_backend = compile_backend if compile_backend else backend - # ToDo: possibly things fail if this is not a QiskitRuntimeService - confirm and decide how to handle (SC 55725) - self._service = backend._service + self._service = getattr(backend, "_service", None) self._use_primitives = use_primitives self._session = session @@ -523,6 +522,13 @@ def execute_circuits(session): def _execute_runtime_service(self, circuits, session): """Execution using old runtime_service (can't use runtime sessions)""" + + # The legacy ``backend.run()`` interface in Qiskit Runtime, which was used as the dedicated “direct hardware access” entry point, has been deprecated by Qiskit. + # The new SamplerV2 class now fulfills this role. Support for the backend.run() will be dropped on or around October 15, 2024. + # Please refer to the `migration guide `_ for instructions on how to migrate any existing code. + # This corresponds to the "circuit-runner" and "qasm3-runner" programs if you are invoking the REST API directly. + # ToDo: deprecate this by or around October 15, 2024. + # update kwargs in case Options has been modified since last execution self._update_kwargs() @@ -530,6 +536,8 @@ def _execute_runtime_service(self, circuits, session): if isinstance(circuits, QuantumScript): circuits = [circuits] + shots = circuits[0].shots.total_shots or self.shots.total_shots + qcirc = [ circuit_to_qiskit(circ, self.num_wires, diagonalize=True, measure=True) for circ in circuits @@ -538,7 +546,7 @@ def _execute_runtime_service(self, circuits, session): program_inputs = { "circuits": compiled_circuits, - "shots": circuits[0].shots.total_shots or self.shots.total_shots, + "shots": shots, } for kwarg, value in self._kwargs.items(): @@ -550,7 +558,7 @@ def _execute_runtime_service(self, circuits, session): else self.backend.configuration().backend_name ) - options = { + circuit_runner_options = { "backend": backend_name, "log_level": self.options.environment.log_level, "job_tags": self.options.environment.job_tags, @@ -558,13 +566,25 @@ def _execute_runtime_service(self, circuits, session): } # Send circuits to the cloud for execution by the circuit-runner program. - job = self.service.run( - program_id="circuit-runner", - options=options, - inputs=program_inputs, - session_id=session.session_id, - ) - self._current_job = job.result(decoder=RunnerResult) + # Cloud simulators will be deprecated on May 15th so this will be exclusively for real hardware devices. + if self.service: + job = self.service.run( + program_id="circuit-runner", + options=circuit_runner_options, + inputs=program_inputs, + session_id=session.session_id, + ) + self._current_job = job.result(decoder=RunnerResult) + else: # Uses local simulator instead. After May 15th, all simulations will use this logic instead. + # Does not support AerSimulator specific options e.g. choose a specific method + # refer to this https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.AerSimulator.html + # To support this would be confusing in terms of option setting (The options we currently support are runtime options) + # This here is just to capture and track shots information + self.backend.set_options(shots=shots) + job = self.backend.run( + compiled_circuits, + ) + self._current_job = job.result() results = [] diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 307e9c74e..c3d13b1eb 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -20,13 +20,15 @@ import pytest from semantic_version import Version import qiskit_ibm_runtime +import qiskit import pennylane as qml from pennylane.tape.qscript import QuantumScript -from qiskit_ibm_runtime import QiskitRuntimeService, Estimator +from qiskit_ibm_runtime import Estimator from qiskit_ibm_runtime.options import Options -from qiskit_ibm_runtime.constants import RunnerResult + from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 +from qiskit_aer import AerSimulator # do not import Estimator (imported above) from qiskit.primitives - the identically # named Estimator object has a different call signature than the remote device Estimator, @@ -115,14 +117,9 @@ def close(self): # This is just to appease a test pass -# pylint: disable=bare-except -try: - service = QiskitRuntimeService(channel="ibm_quantum") - backend = service.backend("ibmq_qasm_simulator") -except: - backend = MockedBackend() - +mocked_backend = MockedBackend() legacy_backend = MockedBackendLegacy() +backend = AerSimulator() test_dev = QiskitDevice2(wires=5, backend=backend) @@ -141,29 +138,30 @@ class TestSupportForV1andV2: @pytest.mark.parametrize( "backend", - [ - legacy_backend, - backend, - ], + [legacy_backend, backend, mocked_backend], ) def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" dev = QiskitDevice2(wires=10, backend=backend, use_primitives=True) assert dev._backend == backend - @pytest.mark.skip( - reason="Fake backends do not have attribute _service, should address in (SC 55725)" - ) @pytest.mark.parametrize( - "backend", + "backend, use_primitives, shape", [ - FakeManila(), - FakeManilaV2(), + (FakeManila(), True, (1, 1024)), + (FakeManila(), False, (1024,)), + (FakeManilaV2(), True, (1, 1024)), + (FakeManilaV2(), False, (1024,)), ], ) - def test_v1_and_v2_manila(self, backend): - """Test that device initializes with no error with V1 and V2 backends by Qiskit""" - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + @pytest.mark.skipif( + Version(qiskit.__version__) < Version("1.0.0"), + reason="Session initialization is not supported for local simulators for Qiskit version < 1.0/qiskit_ibm_runtime version < 0.22.0", + ## See https://docs.quantum.ibm.com/api/migration-guides/local-simulators for additional details + ) + def test_v1_and_v2_manila(self, backend, use_primitives, shape): + """Test that device initializes and runs without error with V1 and V2 backends by Qiskit""" + dev = QiskitDevice2(wires=5, backend=backend, use_primitives=use_primitives) @qml.qnode(dev) def circuit(x): @@ -172,8 +170,9 @@ def circuit(x): return qml.sample(qml.PauliZ(0)) res = circuit(np.pi / 2) - assert isinstance(res, np.ndarray) - assert np.shape(res) == (1024,) + + assert np.shape(res) == shape + assert dev._backend == backend class TestDeviceInitialization: @@ -1009,11 +1008,16 @@ def test_shot_vector_warning_mocked(self): dev.execute(qs) -@pytest.mark.usefixtures("skip_if_no_account") +@pytest.mark.skipif( + Version(qiskit.__version__) < Version("1.0.0"), + reason="Session initialization is not supported for local simulators for Qiskit version < 1.0/qiskit_ibm_runtime version < 0.22.0", + ## See https://docs.quantum.ibm.com/api/migration-guides/local-simulators for additional details +) class TestExecution: + @pytest.mark.parametrize("wire", [0, 1]) @pytest.mark.parametrize( - "angle,op,expectation", + "angle, op, expectation", [ (np.pi / 2, qml.RX, [0, -1, 0, 1, 0, 1]), (np.pi, qml.RX, [0, 0, -1, 1, 1, 0]), @@ -1166,7 +1170,6 @@ def test_process_estimator_job(self, measurements, expectation): for data in result.metadata: assert isinstance(data, dict) - assert list(data.keys()) == ["variance", "shots"] processed_result = QiskitDevice2._process_estimator_job(qs.measurements, result) assert isinstance(processed_result, tuple) assert np.allclose(processed_result, expectation, atol=0.1) @@ -1179,14 +1182,9 @@ def test_generate_samples(self, num_wires, num_shots): qcirc = circuit_to_qiskit(qs, register_size=num_wires, diagonalize=True, measure=True) compiled_circuits = test_dev.compile_circuits([qcirc]) - # Send circuits to the cloud for execution by the circuit-runner program - job = test_dev.service.run( - program_id="circuit-runner", - options={"backend": backend.name}, - inputs={"circuits": compiled_circuits, "shots": num_shots}, - ) + job = test_dev.backend.run(circuits=compiled_circuits, shots=num_shots) - test_dev._current_job = job.result(decoder=RunnerResult) + test_dev._current_job = job.result() samples = test_dev.generate_samples() From ae898be66ce8d064419db3d8a6edd4528b416f70 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Wed, 22 May 2024 17:44:19 -0400 Subject: [PATCH 28/47] Update reqs to 1.0 (#536) * removed legacy ci * Delete .github/workflows/tests.yml * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * removed devices * pylint * pylint * docs fix * setup.py changes * delete * revert * reqs change * setup * change to reqs to match ci * removed a test * pylint * put ibmq.rst back * delete ibmq * remove ibmq * deletion * codecov * lint * changes to tests * Revert "Remove devices that will not be supported in the new release" (#544) * changelogs --- .github/workflows/ibmq_tests.yml | 6 +- .github/workflows/ibmq_tests_1.yml | 44 -------------- .github/workflows/tests.yml | 17 ++---- .github/workflows/tests_qiskit_1.yml | 91 ---------------------------- CHANGELOG.md | 8 +++ requirements-ci-legacy.txt | 5 -- tests/test_qiskit_device.py | 14 +++-- 7 files changed, 25 insertions(+), 160 deletions(-) delete mode 100644 .github/workflows/ibmq_tests_1.yml delete mode 100644 .github/workflows/tests_qiskit_1.yml delete mode 100644 requirements-ci-legacy.txt diff --git a/.github/workflows/ibmq_tests.yml b/.github/workflows/ibmq_tests.yml index a094d1b0f..edbf3e7f8 100644 --- a/.github/workflows/ibmq_tests.yml +++ b/.github/workflows/ibmq_tests.yml @@ -1,7 +1,7 @@ -name: IBMQ integration tests +name: IBMQ integration tests with Qiskit 1.0 on: schedule: - - cron: '0 0 * * 0,4' # At 00:00 on Sunday and Thursday. + - cron: '1 0 * * 0,4' # At 01:00 on Sunday and Thursday. workflow_dispatch: jobs: @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze diff --git a/.github/workflows/ibmq_tests_1.yml b/.github/workflows/ibmq_tests_1.yml deleted file mode 100644 index edbf3e7f8..000000000 --- a/.github/workflows/ibmq_tests_1.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: IBMQ integration tests with Qiskit 1.0 -on: - schedule: - - cron: '1 0 * * 0,4' # At 01:00 on Sunday and Thursday. - workflow_dispatch: - -jobs: - tests: - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.9] - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock flaky --upgrade - pip freeze - - - name: Install Plugin - run: | - pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} - pip freeze - - - name: Run tests - # Only run IBMQ and Runtime tests (skipped otherwise) - run: python -m pytest tests -k 'test_ibmq.py or test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - env: - IBMQX_TOKEN: ${{ secrets.IBMQX_TOKEN_TEST }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3703508a6..1ad3c3b53 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Tests for 1.0 on: push: branches: @@ -30,7 +30,7 @@ jobs: run: | python -m pip install --upgrade pip pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze @@ -39,12 +39,9 @@ jobs: pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} pip freeze - - name: Run tests - # Skip IBMQ and Runtime tests as they depend on IBMQ's availability and - # easily result in timeouts + - name: Run standard Qiskit plugin tests + # Run the standard tests with the most recent version of Qiskit run: python -m pytest tests -k 'not test_ibmq.py and not test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - env: - IBMQX_TOKEN: ${{ secrets.IBMQX_TOKEN_TEST }} - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -72,19 +69,17 @@ jobs: run: | python -m pip install --upgrade pip pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade - pip freeze - name: Install Plugin run: | python setup.py bdist_wheel pip install dist/PennyLane*.whl - pip freeze - name: Run tests run: | - pl-device-test --device=qiskit.basicaer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator + pl-device-test --device=qiskit.basicsim --tb=short --skip-ops --shots=20000 --device-kwargs backend=basic_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=statevector_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=unitary_simulator diff --git a/.github/workflows/tests_qiskit_1.yml b/.github/workflows/tests_qiskit_1.yml deleted file mode 100644 index 1ad3c3b53..000000000 --- a/.github/workflows/tests_qiskit_1.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Tests for 1.0 -on: - push: - branches: - - master - pull_request: - -jobs: - tests: - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.9, '3.10', '3.11'] - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock flaky --upgrade - pip freeze - - - name: Install Plugin - run: | - pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} - pip freeze - - - name: Run standard Qiskit plugin tests - # Run the standard tests with the most recent version of Qiskit - run: python -m pytest tests -k 'not test_ibmq.py and not test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.codecov_token }} - file: ./coverage.xml - - integration-tests: - runs-on: ubuntu-latest - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade - - - name: Install Plugin - run: | - python setup.py bdist_wheel - pip install dist/PennyLane*.whl - - - name: Run tests - run: | - pl-device-test --device=qiskit.basicsim --tb=short --skip-ops --shots=20000 --device-kwargs backend=basic_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=statevector_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=unitary_simulator - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.codecov_token }} - file: ./coverage.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 624711b2d..8de4f43dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ ### Improvements 🛠 ### Breaking changes 💔 +* Support has been removed for Qiskit versions below 0.46. The minimum required version for Qiskit is now 1.0. + If you want to continue to use older versions of Qiskit with the plugin, please use version 0.36 of + the Pennylane-Qiskit plugin. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) + +* The test suite no longer runs for Qiskit versions below 0.46. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) ### Deprecations 👋 @@ -15,6 +22,7 @@ ### Contributors ✍️ This release contains contributions from (in alphabetical order): +Austin Huang --- # Release 0.36.0 diff --git a/requirements-ci-legacy.txt b/requirements-ci-legacy.txt deleted file mode 100644 index f33b309b7..000000000 --- a/requirements-ci-legacy.txt +++ /dev/null @@ -1,5 +0,0 @@ -pennylane>=0.32 -qiskit<0.46 -qiskit-ibm-runtime<0.21 -numpy -sympy diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 57b4d2aa9..34b958b1f 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -174,16 +174,16 @@ def test_warning_raised_for_hardware_backend_analytic_expval(self, recorder): with pytest.warns(UserWarning) as record: dev = qml.device("qiskit.aer", backend="aer_simulator", wires=2, shots=None) - # check that only one warning was raised - assert len(record) == 1 - # check that the message matches assert ( - record[0].message.args[0] == "The analytic calculation of " + record[1].message.args[0] == "The analytic calculation of " "expectations, variances and probabilities is only supported on " f"statevector backends, not on the {dev.backend.name}. Such statistics obtained from this " "device are estimates based on samples." ) + # Two warnings are being raised: one about analytic calculations and another about deprecation. + assert len(record) == 2 + @pytest.mark.parametrize("method", ["unitary", "statevector"]) def test_no_warning_raised_for_software_backend_analytic_expval( self, method, recorder, recwarn @@ -193,8 +193,10 @@ def test_no_warning_raised_for_software_backend_analytic_expval( _ = qml.device("qiskit.aer", backend="aer_simulator", method=method, wires=2, shots=None) - # check that no warnings were raised - assert len(recwarn) == 0 + # These simulators are being deprecated. Warning is raised in Qiskit 1.0 + # Migrate to AerSimulator with AerSimulator(method=method) and append + # run circuits with the `save_state` instruction. + assert len(recwarn) == 1 class TestAerBackendOptions: From 902713100cfe07b6151f5af68b93921371a12989 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Thu, 23 May 2024 09:22:18 -0400 Subject: [PATCH 29/47] Remove basicaer (#546) * removed legacy ci * Delete .github/workflows/tests.yml * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * removed devices * pylint * pylint * docs fix * setup.py changes * delete * revert * reqs change * setup * change to reqs to match ci * removed a test * pylint * put ibmq.rst back * delete ibmq * remove ibmq * deletion * codecov * lint * changes to tests * Revert "Remove devices that will not be supported in the new release" (#544) * docs * remove basicaer * reqs to build docs * docs * pylint * tests fix * path change * Changelog and doc * changelogs * Update CHANGELOG.md * deleted a test file * removed error * basic sim pylint * Update CHANGELOG.md Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * remove ifelse block * pylint --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- CHANGELOG.md | 3 + doc/devices/basicaer.rst | 32 ---------- doc/index.rst | 11 +--- doc/requirements.txt | 8 +-- pennylane_qiskit/__init__.py | 2 +- pennylane_qiskit/basic_aer.py | 111 ---------------------------------- pennylane_qiskit/basic_sim.py | 49 +++++++++++++++ requirements.txt | 48 ++------------- tests/conftest.py | 28 +++------ tests/test_new_qiskit_temp.py | 57 ----------------- 10 files changed, 73 insertions(+), 276 deletions(-) delete mode 100644 doc/devices/basicaer.rst delete mode 100644 pennylane_qiskit/basic_aer.py create mode 100644 pennylane_qiskit/basic_sim.py delete mode 100644 tests/test_new_qiskit_temp.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de4f43dc..6c7f05fcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ * The test suite no longer runs for Qiskit versions below 0.46. [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) +* The ``qiskit.basicaer`` device has been removed because it is not supported for versions of Qiskit above 0.46. + [(#546)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/546) + ### Deprecations 👋 ### Documentation 📝 diff --git a/doc/devices/basicaer.rst b/doc/devices/basicaer.rst deleted file mode 100644 index e8746b2c8..000000000 --- a/doc/devices/basicaer.rst +++ /dev/null @@ -1,32 +0,0 @@ -The BasicAer device -=================== - -.. note:: - - Qiskit discontinued their ``BasicAer`` device in the 1.0 release, so this device - is only available for lower versions of Qiskit. For a simple Python simulator - compatible with Qiskit 1.0, use the :ref:`BasicSim device ` instead. - -While the ``'qiskit.aer'`` device is the standard go-to simulator that is provided along -the Qiskit main package installation, there exists a natively included python simulator -that is slower but will work usually without the need to install other dependencies -(C++, BLAS, and so on). This simulator can be used through the device ``'qiskit.basicaer'``: - -.. code-block:: python - - import pennylane as qml - dev = qml.device('qiskit.basicaer', wires=2) - -As with the ``'qiskit.aer'`` device, there are different backends available, which you can find -by calling - -.. code-block:: python - - dev.capabilities()['backend'] - -.. note:: - - Currently, PennyLane does not support the ``'pulse_simulator'`` backend. - -The backends are used in the same manner as specified for the ``'qiskit.aer'`` device. -The ``'qiskit.basicaer'`` device, however, does not support the simulation of noise models. diff --git a/doc/index.rst b/doc/index.rst index 372bf84d8..7699ea78e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -21,10 +21,6 @@ The following devices are available: :description: Qiskit's staple simulator with great features such as noise models. :link: devices/aer.html -.. title-card:: - :name: 'qiskit.basicaer' - :description: A simplified version of the Aer device, which requires fewer dependencies. - :link: devices/basicaer.html .. title-card:: :name: 'qiskit.basicsim' @@ -76,9 +72,9 @@ follows: dev = qml.device('qiskit.aer', wires=2, backend='unitary_simulator') -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 `_. +PennyLane chooses the ``aer_simulator`` as the default backend if no backend is specified. +For more details on the ``aer_simulator``, including available backend options, see +`Qiskit Aer Simulator documentation `_. Tutorials ~~~~~~~~~ @@ -138,7 +134,6 @@ hardware access. :hidden: devices/aer - devices/basicaer devices/basicsim devices/ibmq devices/runtime diff --git a/doc/requirements.txt b/doc/requirements.txt index e66880f32..512ea9984 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,10 +7,10 @@ pennylane==0.34 pybind11==2.11.1 pygments==2.17.2 pygments-github-lexers==0.0.5 -qiskit==0.45.3 -qiskit-aer==0.13.3 -qiskit-ibm-runtime==0.20.0 -qiskit-ibm-provider==0.10.0 +qiskit==1.0.2 +qiskit-aer==0.14.1 +qiskit-ibm-runtime==0.23.0 +qiskit-ibm-provider==0.11.0 sphinxcontrib-bibtex==2.6.2 sphinx-automodapi==0.17.0 pennylane-sphinx-theme diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 991b73d7c..1298cceed 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -15,7 +15,7 @@ from ._version import __version__ from .aer import AerDevice -from .basic_aer import BasicAerDevice, BasicSimulatorDevice +from .basic_sim import BasicSimulatorDevice from .ibmq import IBMQDevice from .remote import RemoteDevice from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file diff --git a/pennylane_qiskit/basic_aer.py b/pennylane_qiskit/basic_aer.py deleted file mode 100644 index ecf26fce8..000000000 --- a/pennylane_qiskit/basic_aer.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the :class:`~.BasicAerDevice` class, a PennyLane device that allows -evaluation and differentiation of Qiskit Terra's BasicAer simulator -using PennyLane. -""" -import qiskit - -from semantic_version import Version - -from .qiskit_device import QiskitDevice - -if Version(qiskit.__version__) >= Version("1.0.0"): - from qiskit.providers.basic_provider import BasicProvider - - -class BasicAerDevice(QiskitDevice): - """A PennyLane device for the native Python Qiskit simulator BasicAer. - - Please see the `Qiskit documentations `_ - further information on the backend options and transpile options. - - A range of :code:`backend_options` that will be passed to the simulator and - a range of transpile options can be given as kwargs. - - For more information on backends, please visit the - `Basic Aer provider documentation `_. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['aux_wire', 'q1', 'q2']``). - backend (str): the desired backend - 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. - - Keyword Args: - 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. - """ - - short_name = "qiskit.basicaer" - - def __init__(self, wires, shots=1024, backend="qasm_simulator", **kwargs): - - max_ver = Version("0.46", partial=True) - - if Version(qiskit.__version__) > max_ver: - raise RuntimeError( - f"Qiskit has discontinued the BasicAer device, so it can only be used in" - f"versions of Qiskit below 1.0. You have version {qiskit.__version__} " - f"installed. For a Python simulator, use the 'qiskit.basicsim' device " - f"instead. Alternatively, you can downgrade Qiskit to use the " - f"'qiskit.basicaer' device." - ) - - super().__init__(wires, provider=qiskit.BasicAer, backend=backend, shots=shots, **kwargs) - - -class BasicSimulatorDevice(QiskitDevice): - """A PennyLane device for the native Python Qiskit simulator. - - For more information on the ``BasicSimulator`` backend options and transpile options, please visit the - `BasicProvider documentation `_. - These options can be passed to this plugin device as keyword arguments. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['aux_wire', 'q1', 'q2']``). - backend (str): the desired backend - 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. - """ - - short_name = "qiskit.basicsim" - - analytic_warning_message = ( - "The plugin does not currently support analytic calculation of expectations, variances " - "and probabilities with the BasicProvider backend {}. Such statistics obtained from this " - "device are estimates based on samples." - ) - - def __init__(self, wires, shots=1024, backend="basic_simulator", **kwargs): - - min_version = Version("1.0.0") - - if Version(qiskit.__version__) < min_version: - raise RuntimeError( - f"The 'qiskit.simulator' device is not compatible with version of Qiskit prior " - f"to 1.0. You have version {qiskit.__version__} installed. For a Python simulator, " - f"use the 'qiskit.basicaer' device instead. Alternatively, upgrade Qiskit " - f"(see https://docs.quantum.ibm.com/start/install) to use the 'qiskit.basicsim' device." - ) - - super().__init__(wires, provider=BasicProvider(), backend=backend, shots=shots, **kwargs) diff --git a/pennylane_qiskit/basic_sim.py b/pennylane_qiskit/basic_sim.py new file mode 100644 index 000000000..0b51be915 --- /dev/null +++ b/pennylane_qiskit/basic_sim.py @@ -0,0 +1,49 @@ +# Copyright 2019 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the :class:`~.BasicAerDevice` class, a PennyLane device that allows +evaluation and differentiation of Qiskit Terra's BasicAer simulator +using PennyLane. +""" +from qiskit.providers.basic_provider import BasicProvider +from .qiskit_device import QiskitDevice + + +class BasicSimulatorDevice(QiskitDevice): + """A PennyLane device for the native Python Qiskit simulator. + + For more information on the ``BasicSimulator`` backend options and transpile options, please visit the + `BasicProvider documentation `_. + These options can be passed to this plugin device as keyword arguments. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['aux_wire', 'q1', 'q2']``). + backend (str): the desired backend + 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. + """ + + short_name = "qiskit.basicsim" + + analytic_warning_message = ( + "The plugin does not currently support analytic calculation of expectations, variances " + "and probabilities with the BasicProvider backend {}. Such statistics obtained from this " + "device are estimates based on samples." + ) + + def __init__(self, wires, shots=1024, backend="basic_simulator", **kwargs): + super().__init__(wires, provider=BasicProvider(), backend=backend, shots=shots, **kwargs) diff --git a/requirements.txt b/requirements.txt index c2ee3b9f9..ad1fe23f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,44 +1,4 @@ -appdirs==1.4.4 -autograd==1.6.2 -autoray==0.6.8 -cachetools==5.3.3 -certifi==2024.2.2 -cffi==1.16.0 -charset-normalizer==3.3.2 -cryptography==42.0.5 -Cython==3.0.8 -dill==0.3.8 -future==1.0.0 -idna==3.6 -mpmath==1.3.0 -networkx==3.2.1 -ninja==1.11.1.1 -ntlm-auth==1.5.0 -numpy==1.26.4 -orjson==3.9.15 -pbr==6.0.0 -pennylane==0.34 -PennyLane-Lightning==0.34 -ply==3.11 -psutil==5.9.8 -pycparser==2.21 -python-constraint==1.4.0 -python-dateutil==2.8.2 -qiskit==0.45.3 -qiskit-aer==0.13.3 -qiskit-ibm-runtime==0.20.0 -qiskit-ibm-provider==0.10.0 -qiskit-ignis==0.7.1 -qiskit-terra==0.45.3 -requests==2.31.0 -requests-ntlm==1.2.0 -retworkx==0.14.1 -scipy==1.12.0 -semantic-version==2.10.0 -six==1.16.0 -stevedore==5.2.0 -symengine==0.11.0 -sympy==1.12 -toml==0.10.2 -urllib3==2.2.1 -websocket-client==1.7.0 +pennylane>=0.32 +qiskit +numpy +sympy diff --git a/tests/conftest.py b/tests/conftest.py index 06fdb56ee..7fcd26813 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,12 +18,10 @@ import os import pytest import numpy as np -import qiskit import pennylane as qml -from semantic_version import Version from qiskit_ibm_provider import IBMProvider -from pennylane_qiskit import AerDevice, BasicAerDevice, BasicSimulatorDevice +from pennylane_qiskit import AerDevice, BasicSimulatorDevice # pylint: disable=protected-access, unused-argument, redefined-outer-name @@ -40,22 +38,14 @@ A = np.array([[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]]) -if Version(qiskit.__version__) < Version("1.0.0"): - test_devices = [AerDevice, BasicAerDevice] - hw_backends = ["qasm_simulator", "aer_simulator"] - state_backends = [ - "statevector_simulator", - "unitary_simulator", - ] -else: - test_devices = [AerDevice, BasicSimulatorDevice] - hw_backends = ["qasm_simulator", "aer_simulator", "basic_simulator"] - state_backends = [ - "statevector_simulator", - "unitary_simulator", - "aer_simulator_statevector", - "aer_simulator_unitary", - ] +test_devices = [AerDevice, BasicSimulatorDevice] +hw_backends = ["qasm_simulator", "aer_simulator", "basic_simulator"] +state_backends = [ + "statevector_simulator", + "unitary_simulator", + "aer_simulator_statevector", + "aer_simulator_unitary", +] @pytest.fixture diff --git a/tests/test_new_qiskit_temp.py b/tests/test_new_qiskit_temp.py deleted file mode 100644 index 82c090373..000000000 --- a/tests/test_new_qiskit_temp.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -r""" -This module contains tests for testing backends and providers for PennyLane IBMQ devices. -""" -import pytest -import pennylane as qml -import qiskit - -from semantic_version import Version - -from pennylane_qiskit import BasicSimulatorDevice - -# pylint: disable= unused-argument - - -@pytest.mark.skipif( - Version(qiskit.__version__) < Version("1.0.0"), - reason="versions below 1.0 are compatible with BasicAer", -) -def test_error_is_raised_if_initalizing_basicaer_device(monkeypatch): - """Test that when Qiskit 1.0 is installed, an error is raised if you try - to initialize the 'qiskit.basicaer' device.""" - - # test that the correct error is actually raised in Qiskit 1.0 (rather than fx an import error) - with pytest.raises( - RuntimeError, - match="Qiskit has discontinued the BasicAer device", - ): - qml.device("qiskit.basicaer", wires=2) - - -@pytest.mark.skipif( - Version(qiskit.__version__) >= Version("1.0.0"), - reason="versions 1.0 and above are compatible with BasicSimulator", -) -def test_error_is_raised_if_initalizing_basic_simulator_device(monkeypatch): - """Test that when a version of Qiskit below 1.0 is installed, an error is raised if you try - to initialize the BasicSimulatorDevice.""" - - # test that the correct error is actually raised in Qiskit 1.0 (rather than fx an import error) - with pytest.raises( - RuntimeError, - match="device is not compatible with version of Qiskit prior to 1.0", - ): - BasicSimulatorDevice(wires=2) From 83c9483010d034f8aac024641c4b60604e131258 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Tue, 28 May 2024 13:47:56 -0400 Subject: [PATCH 30/47] Remove use primitives and everything that depends on it (#538) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 132 ++--------------- tests/test_base_device.py | 219 +++++------------------------ 2 files changed, 49 insertions(+), 302 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index a1082e8df..c24da8978 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -29,7 +29,6 @@ from qiskit.providers import BackendV2 from qiskit_ibm_runtime import Session, Sampler, Estimator -from qiskit_ibm_runtime.constants import RunnerResult from qiskit_ibm_runtime.options import Options from pennylane import transform @@ -45,7 +44,7 @@ validate_measurements, validate_device_wires, ) -from pennylane.measurements import ProbabilityMP, ExpectationMP, VarianceMP +from pennylane.measurements import ExpectationMP, VarianceMP from ._version import __version__ from .converter import QISKIT_OPERATION_MAP, circuit_to_qiskit, mp_to_pauli @@ -127,15 +126,13 @@ def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: def split_execution_types( tape: qml.tape.QuantumTape, ) -> (Sequence[qml.tape.QuantumTape], Callable): - """Split into separate tapes based on measurement type. However, for ``expval`` and ``var`` - measurements, if the measured observable does not have a ``pauli_rep``, it is split as a - separate tape and will use the standard backend.run function. Counts will use the - Qiskit Sampler, ExpectationValue and Variance will use the Estimator, and other - strictly sample-based measurements will use the standard backend.run function""" + """Split into separate tapes based on measurement type. Counts and sample-based measurements + will use the Qiskit Sampler. ExpectationValue and Variance will use the Estimator, except + when the measured observable does not have a `pauli_rep`. In that case, the Sampler will be + used, and the raw samples will be processed to give an expectation value.""" estimator = [] sampler = [] - no_prim = [] for i, mp in enumerate(tape.measurements): if isinstance(mp, (ExpectationMP, VarianceMP)): @@ -145,15 +142,13 @@ def split_execution_types( warnings.warn( f"The observable measured {mp.obs} does not have a `pauli_rep` " "and will be run without using the Estimator primitive. Instead, " - "the standard backend.run function will be used." + "raw samples from the Sampler will be used." ) - no_prim.append((mp, i)) - elif isinstance(mp, ProbabilityMP): - sampler.append((mp, i)) + sampler.append((mp, i)) else: - no_prim.append((mp, i)) + sampler.append((mp, i)) - order_indices = [[i for mp, i in group] for group in [estimator, sampler, no_prim]] + order_indices = [[i for mp, i in group] for group in [estimator, sampler]] tapes = [] if estimator: @@ -176,16 +171,6 @@ def split_execution_types( ) ] ) - if no_prim: - tapes.extend( - [ - qml.tape.QuantumScript( - tape.operations, - measurements=[mp for mp, i in no_prim], - shots=tape.shots, - ) - ] - ) def reorder_fn(res): """re-order the output to the original shape and order""" @@ -226,10 +211,6 @@ class QiskitDevice2(Device): Keyword Args: shots (int or None): number of circuit evaluations/random samples used to estimate expectation values and variances of observables. - use_primitives (bool): whether or not to use Qiskit Primitives. Defaults to False. If True, - getting expectation values and variance from the backend will use a Qiskit Estimator, - and getting probabilities will use a Qiskit Sampler. Other measurement types will continue - to return results from the backend without using a Primitive. options (Options): a Qiskit Options object for specifying handling the Qiskit task (transpiliation, error mitigation, execution, etc). Defaults to None. See Qiskit documentation for more details. @@ -261,7 +242,6 @@ def __init__( wires, backend, shots=1024, - use_primitives=False, options=None, session=None, compile_backend=None, @@ -289,7 +269,6 @@ def __init__( self._compile_backend = compile_backend if compile_backend else backend self._service = getattr(backend, "_service", None) - self._use_primitives = use_primitives self._session = session # initial kwargs are saved and referenced every time the kwargs used for transpilation and execution @@ -412,8 +391,7 @@ def preprocess( transform_program.add_transform(broadcast_expand) # missing: split non-commuting, sum_expand, etc. [SC-62047] - if self._use_primitives: - transform_program.add_transform(split_execution_types) + transform_program.add_transform(split_execution_types) return transform_program, config @@ -486,10 +464,6 @@ def execute( ) -> Result_or_ResultBatch: session = self._session or Session(backend=self.backend) - if not self._use_primitives: - results = self._execute_runtime_service(circuits, session=session) - return results - results = [] if isinstance(circuits, QuantumScript): @@ -508,10 +482,8 @@ def execute_circuits(session): circ.measurements[0].obs, "pauli_rep", None ): execute_fn = self._execute_estimator - elif isinstance(circ.measurements[0], ProbabilityMP): - execute_fn = self._execute_sampler else: - execute_fn = self._execute_runtime_service + execute_fn = self._execute_sampler results.append(execute_fn(circ, session)) yield results finally: @@ -520,88 +492,6 @@ def execute_circuits(session): with execute_circuits(session) as results: return results - def _execute_runtime_service(self, circuits, session): - """Execution using old runtime_service (can't use runtime sessions)""" - - # The legacy ``backend.run()`` interface in Qiskit Runtime, which was used as the dedicated “direct hardware access” entry point, has been deprecated by Qiskit. - # The new SamplerV2 class now fulfills this role. Support for the backend.run() will be dropped on or around October 15, 2024. - # Please refer to the `migration guide `_ for instructions on how to migrate any existing code. - # This corresponds to the "circuit-runner" and "qasm3-runner" programs if you are invoking the REST API directly. - # ToDo: deprecate this by or around October 15, 2024. - - # update kwargs in case Options has been modified since last execution - self._update_kwargs() - - # in case a single circuit is passed - if isinstance(circuits, QuantumScript): - circuits = [circuits] - - shots = circuits[0].shots.total_shots or self.shots.total_shots - - qcirc = [ - circuit_to_qiskit(circ, self.num_wires, diagonalize=True, measure=True) - for circ in circuits - ] - compiled_circuits = self.compile_circuits(qcirc) - - program_inputs = { - "circuits": compiled_circuits, - "shots": shots, - } - - for kwarg, value in self._kwargs.items(): - program_inputs[kwarg] = value - - backend_name = ( - self.backend.name - if isinstance(self.backend, BackendV2) - else self.backend.configuration().backend_name - ) - - circuit_runner_options = { - "backend": backend_name, - "log_level": self.options.environment.log_level, - "job_tags": self.options.environment.job_tags, - "max_execution_time": self.options.max_execution_time, - } - - # Send circuits to the cloud for execution by the circuit-runner program. - # Cloud simulators will be deprecated on May 15th so this will be exclusively for real hardware devices. - if self.service: - job = self.service.run( - program_id="circuit-runner", - options=circuit_runner_options, - inputs=program_inputs, - session_id=session.session_id, - ) - self._current_job = job.result(decoder=RunnerResult) - else: # Uses local simulator instead. After May 15th, all simulations will use this logic instead. - # Does not support AerSimulator specific options e.g. choose a specific method - # refer to this https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.AerSimulator.html - # To support this would be confusing in terms of option setting (The options we currently support are runtime options) - # This here is just to capture and track shots information - self.backend.set_options(shots=shots) - job = self.backend.run( - compiled_circuits, - ) - self._current_job = job.result() - - results = [] - - ### Note: this assumes that the input values are valid. - ### Don't write tests cases that require transforms not yet implemented (not here). - for index, circuit in enumerate(circuits): - self._samples = self.generate_samples(index) - res = [ - mp.process_samples(self._samples, wire_order=self.wires) - for mp in circuit.measurements - ] - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - return tuple(results) - def _execute_sampler(self, circuit, session): """Execution for the Sampler primitive""" diff --git a/tests/test_base_device.py b/tests/test_base_device.py index c3d13b1eb..93e3a0148 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -20,7 +20,6 @@ import pytest from semantic_version import Version import qiskit_ibm_runtime -import qiskit import pennylane as qml from pennylane.tape.qscript import QuantumScript @@ -142,26 +141,22 @@ class TestSupportForV1andV2: ) def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" - dev = QiskitDevice2(wires=10, backend=backend, use_primitives=True) + dev = QiskitDevice2(wires=10, backend=backend) assert dev._backend == backend @pytest.mark.parametrize( - "backend, use_primitives, shape", + "backend, shape", [ - (FakeManila(), True, (1, 1024)), - (FakeManila(), False, (1024,)), - (FakeManilaV2(), True, (1, 1024)), - (FakeManilaV2(), False, (1024,)), + (FakeManila(), (1, 1024)), + (FakeManilaV2(), (1, 1024)), ], ) - @pytest.mark.skipif( - Version(qiskit.__version__) < Version("1.0.0"), - reason="Session initialization is not supported for local simulators for Qiskit version < 1.0/qiskit_ibm_runtime version < 0.22.0", - ## See https://docs.quantum.ibm.com/api/migration-guides/local-simulators for additional details + @pytest.mark.skip( + reason="The functionality of using sampler to get an accurate answer is not yet implemented" ) - def test_v1_and_v2_manila(self, backend, use_primitives, shape): + def test_v1_and_v2_manila(self, backend, shape): """Test that device initializes and runs without error with V1 and V2 backends by Qiskit""" - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=use_primitives) + dev = QiskitDevice2(wires=5, backend=backend) @qml.qnode(dev) def circuit(x): @@ -191,12 +186,6 @@ def test_compile_backend_kwarg(self): assert dev2._compile_backend != dev2._backend assert dev2._compile_backend == compile_backend - @pytest.mark.parametrize("use_primitives", [True, False]) - def test_use_primitives_kwarg(self, use_primitives): - """Test the _use_primitives attribute is set on initialization""" - dev = QiskitDevice2(wires=2, backend=backend, use_primitives=use_primitives) - assert dev._use_primitives == use_primitives - def test_no_shots_warns_and_defaults(self): """Test that initializing with shots=None raises a warning indicating that the device is sample based and will default to 1024 shots""" @@ -325,8 +314,7 @@ class TestDevicePreprocessing: ], [ [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], - [qml.probs(wires=[2])], - [qml.counts()], + [qml.counts(), qml.probs(wires=[2])], ], ), ( @@ -390,7 +378,7 @@ class TestDevicePreprocessing: @pytest.mark.filterwarnings("ignore::UserWarning") def test_split_execution_types(self, measurements, expectation): """Test that the split_execution_types transform splits measurements into Estimator-based - (expval, var), Sampler-based (probs) and raw-sample based (everything else)""" + (expval, var) and Sampler-based (everything else)""" operations = [qml.PauliX(0), qml.PauliY(1), qml.Hadamard(2), qml.CNOT([2, 1])] qs = QuantumScript(operations, measurements=measurements) @@ -439,17 +427,17 @@ def test_observable_stopping_condition(self, obs, expected): [ ([qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])], 2), ([qml.expval(qml.PauliZ(0)), qml.sample(wires=[0, 1])], 2), - ([qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], 2), + ([qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], 1), ([qml.var(qml.PauliZ(0)), qml.expval(qml.PauliX(1))], 1), - ([qml.probs(wires=[0]), qml.counts(), qml.var(qml.PauliY(2))], 3), + ([qml.probs(wires=[0]), qml.counts(), qml.var(qml.PauliY(2))], 2), ], ) def test_preprocess_splits_incompatible_primitive_measurements(self, measurements, num_types): """Test that the default behaviour for preprocess it to split the tapes based - on meausrement type. Expval and Variance are one type (Estimator), Probs another (Sampler), - and everything else a third (raw sample-based measurements).""" + on meausrement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements + are another type (Sampler).""" - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend) qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) program, _ = dev.preprocess() @@ -458,29 +446,6 @@ def test_preprocess_splits_incompatible_primitive_measurements(self, measurement # measurements that are incompatible are split when use_primtives=True assert len(tapes) == num_types - @pytest.mark.parametrize( - "measurements", - [ - [qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])], - [qml.expval(qml.PauliZ(0)), qml.sample(wires=[0, 1])], - [qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], - ], - ) - def test_preprocess_measurements_without_primitives(self, measurements): - """Test if Primitives are not being used that the preprocess does not split - the tapes based on measurement type""" - - qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) - - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=False) - program, _ = dev.preprocess() - - tapes, _ = program([qs]) - - # measurements that are incompatible on the primitive-based device - # are not split when use_primtives=False - assert len(tapes) == 1 - def test_preprocess_decomposes_unsupported_operator(self): """Test that the device preprocess decomposes operators that aren't on the list of Qiskit-supported operators""" @@ -833,47 +798,12 @@ def get_counts(): assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == results_dict["10"] assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == results_dict["01"] - @pytest.mark.parametrize("backend", [backend, legacy_backend]) - def test_execute_pipeline_no_primitives_mocked(self, mocker, backend): - """Test that a device **not** using Primitives only calls the _execute_runtime_service - to execute, regardless of measurement type""" - - dev = QiskitDevice2( - wires=5, backend=backend, use_primitives=False, session=MockSession(backend) - ) - - initial_session = dev._session - - sampler_execute = mocker.spy(dev, "_execute_sampler") - estimator_execute = mocker.spy(dev, "_execute_estimator") - - qs = QuantumScript( - [qml.PauliX(0), qml.PauliY(1)], - measurements=[ - qml.expval(qml.PauliZ(0)), - qml.probs(wires=[0, 1]), - qml.counts(), - qml.sample(), - ], - ) - - with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") - res = dev.execute(qs) - - runtime_service_execute.assert_called_once() - sampler_execute.assert_not_called() - estimator_execute.assert_not_called() - - assert res == "runtime_execute_res" - assert initial_session == dev._session # session is not changed - @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._execute_estimator") def test_execute_pipeline_primitives_no_session(self, mocker): """Test that a Primitives-based device initialized with no Session creates one for the execution, and then returns the device session to None.""" - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True, session=None) + dev = QiskitDevice2(wires=5, backend=backend, session=None) assert dev._session is None @@ -887,13 +817,10 @@ def test_execute_pipeline_primitives_no_session(self, mocker): @pytest.mark.parametrize("backend", [backend, legacy_backend]) def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): - """Test that a device that **is** using Primitives calls the _execute_runtime_service - to execute measurements that require raw samples, and the relevant primitive measurements - on the other measurements""" + """Test that a device executes measurements that require raw samples on the sampler, + and the relevant primitive measurements on the estimator""" - dev = QiskitDevice2( - wires=5, backend=backend, use_primitives=True, session=MockSession(backend) - ) + dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) qs = QuantumScript( [qml.PauliX(0), qml.PauliY(1)], @@ -906,23 +833,19 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): ) tapes, _ = split_execution_types(qs) - with patch.object(dev, "_execute_runtime_service", return_value="runtime_execute_res"): - with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): - with patch.object(dev, "_execute_estimator", return_value="estimator_execute_res"): - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") - sampler_execute = mocker.spy(dev, "_execute_sampler") - estimator_execute = mocker.spy(dev, "_execute_estimator") + with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): + with patch.object(dev, "_execute_estimator", return_value="estimator_execute_res"): + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") - res = dev.execute(tapes) + res = dev.execute(tapes) - runtime_service_execute.assert_called_once() sampler_execute.assert_called_once() estimator_execute.assert_called_once() assert res == [ "estimator_execute_res", "sampler_execute_res", - "runtime_execute_res", ] @patch("pennylane_qiskit.qiskit_device2.Estimator") @@ -954,45 +877,10 @@ def test_execute_sampler_mocked(self, mocked_sampler, session): # to emphasize, this did nothing except appease CodeCov assert isinstance(result[0], Mock) - @patch("pennylane_qiskit.qiskit_device2.transpile") - def test_execute_runtime_service_mocked(self, mocked_transpile): - """Test the _execute_sampler function using a mocked version of Sampler - that returns a meaningless result.""" - - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) - - mock_counts = {"00": 125, "10": 500, "01": 250, "11": 125} - mock_result = Mock() - mock_job = Mock() - mock_service = Mock() - - mock_result.get_counts = Mock(return_value=mock_counts) - mock_job.result = Mock(return_value=mock_result) - mock_service.run = Mock(return_value=mock_job) - - dev._service = mock_service - - qs = QuantumScript([qml.PauliX(0)], measurements=[qml.sample()]) - result = dev._execute_runtime_service(qs, MockSession(backend)) - - samples = result[0] - - assert len(samples) == sum(mock_counts.values()) - assert len(samples[0]) == 2 - - assert len(np.argwhere([np.allclose(s, [0, 0]) for s in samples])) == mock_counts["00"] - assert len(np.argwhere([np.allclose(s, [1, 1]) for s in samples])) == mock_counts["11"] - - # order of samples is swapped compared to keys (Qiskit wire order convention is reverse of PennyLane) - assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == mock_counts["10"] - assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == mock_counts["01"] - def test_shot_vector_warning_mocked(self): """Test that a device that executes a circuit with an array of shots raises the appropriate warning""" - dev = QiskitDevice2( - wires=5, backend=backend, use_primitives=True, session=MockSession(backend) - ) + dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) qs = QuantumScript( measurements=[ qml.expval(qml.PauliX(0)), @@ -1008,11 +896,6 @@ def test_shot_vector_warning_mocked(self): dev.execute(qs) -@pytest.mark.skipif( - Version(qiskit.__version__) < Version("1.0.0"), - reason="Session initialization is not supported for local simulators for Qiskit version < 1.0/qiskit_ibm_runtime version < 0.22.0", - ## See https://docs.quantum.ibm.com/api/migration-guides/local-simulators for additional details -) class TestExecution: @pytest.mark.parametrize("wire", [0, 1]) @@ -1033,9 +916,8 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) """ - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend) - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1053,7 +935,6 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec res = dev.execute(qs) - runtime_service_execute.assert_not_called() sampler_execute.assert_not_called() estimator_execute.assert_called_once() @@ -1093,9 +974,8 @@ def test_estimator_with_various_multi_qubit_pauli_obs( """ pl_dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) - dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend, use_primitives=True) + dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1111,7 +991,6 @@ def test_estimator_with_various_multi_qubit_pauli_obs( res = dev.execute(qs) expectation = pl_dev.execute(qs) - runtime_service_execute.assert_not_called() sampler_execute.assert_not_called() estimator_execute.assert_called_once() @@ -1176,6 +1055,7 @@ def test_process_estimator_job(self, measurements, expectation): @pytest.mark.parametrize("num_wires", [1, 3, 5]) @pytest.mark.parametrize("num_shots", [50, 100]) + @pytest.mark.skip(reason="Need to replace this with using SamplerV2.") def test_generate_samples(self, num_wires, num_shots): qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) @@ -1204,29 +1084,9 @@ def test_generate_samples(self, num_wires, num_shots): # nothing else is in samples assert [s for s in samples if not s in np.array([exp_res0, exp_res1])] == [] - def test_tape_shots_used_runtime_service(self, mocker): - """Tests that device uses tape shots rather than device shots for _execute_runtime_service""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) - - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") - - @qml.qnode(dev) - def circuit(): - return qml.sample() - - res = circuit(shots=[5]) - - runtime_service_execute.assert_called_once() - - assert len(res[0]) == 5 - - # Should reset to device shots if circuit ran again without shots defined - res = circuit() - assert len(res[0]) == 2 - def test_tape_shots_used_for_estimator(self, mocker): """Tests that device uses tape shots rather than device shots for estimator""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend, shots=2) estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1245,7 +1105,7 @@ def circuit(): def test_tape_shots_used_for_sampler(self, mocker): """Tests that device uses tape shots rather than device shots for sampler""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend, shots=2) sampler_execute = mocker.spy(dev, "_execute_sampler") @@ -1265,7 +1125,7 @@ def circuit(): def test_warning_for_shot_vector(self): """Tests that a warning is raised if a shot vector is passed and total shots of tape is used instead.""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend, shots=2) @qml.qnode(dev) def circuit(): @@ -1291,16 +1151,18 @@ def circuit(): ], ) @pytest.mark.filterwarnings("ignore::UserWarning") + @pytest.mark.skip( + reason="The functionality of using sampler to get the accurate answer is not yet implemented" + ) def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): - """Test that the device uses _execute_runtime_service and _execute_estimator appropriately + """Test that the device uses _sampler and _execute_estimator appropriately and provides an accurate answer for measurements with observables that don't have a pauli_rep. """ - dev = QiskitDevice2(wires=5, backend=backend, use_primitives=True) + dev = QiskitDevice2(wires=5, backend=backend) pl_dev = qml.device("default.qubit", wires=5) - runtime_service_execute = mocker.spy(dev, "_execute_runtime_service") estimator_execute = mocker.spy(dev, "_execute_estimator") sampler_execute = mocker.spy(dev, "_execute_sampler") @@ -1319,9 +1181,8 @@ def pl_circuit(): res = circuit() pl_res = pl_circuit() - runtime_service_execute.assert_called_once() estimator_execute.assert_called_once() - sampler_execute.assert_not_called() + sampler_execute.assert_called_once() assert np.allclose(res, pl_res, atol=0.1) @@ -1329,11 +1190,7 @@ def test_warning_for_split_execution_types_when_observable_no_pauli(self): """Test that a warning is raised when device is passed a measurement on an observable that does not have a pauli_rep.""" - dev = QiskitDevice2( - wires=5, - backend=backend, - use_primitives=True, - ) + dev = QiskitDevice2(wires=5, backend=backend) @qml.qnode(dev) def circuit(): From 9e5df684f38b487b1f60ce357c09c120fcc9fda0 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 31 May 2024 15:49:11 -0400 Subject: [PATCH 31/47] Remove ibmq devices (#550) * Removing ibmq devices from the docs and relevant files * missed something in docs * Changelog updates * Update CHANGELOG.md Co-authored-by: Utkarsh --------- Co-authored-by: Utkarsh --- CHANGELOG.md | 3 + doc/devices/ibmq.rst | 84 -------- doc/devices/runtime.rst | 21 -- doc/index.rst | 20 +- pennylane_qiskit/__init__.py | 3 - pennylane_qiskit/ibmq.py | 149 ------------- pennylane_qiskit/runtime_devices.py | 228 -------------------- setup.py | 3 - tests/test_ibmq.py | 323 ---------------------------- tests/test_runtime.py | 227 ------------------- tests/test_sample.py | 294 ------------------------- 11 files changed, 4 insertions(+), 1351 deletions(-) delete mode 100644 doc/devices/ibmq.rst delete mode 100644 doc/devices/runtime.rst delete mode 100644 pennylane_qiskit/ibmq.py delete mode 100644 pennylane_qiskit/runtime_devices.py delete mode 100644 tests/test_ibmq.py delete mode 100644 tests/test_runtime.py delete mode 100644 tests/test_sample.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7f05fcf..76814d925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ * The ``qiskit.basicaer`` device has been removed because it is not supported for versions of Qiskit above 0.46. [(#546)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/546) +* The IBM quantum devices, ``qiskit.ibmq``, ``qiskit.ibmq.circuit_runner`` and ``qiskit.ibmq.sampler``, have been removed due to deprecations of the IBMProvider and the cloud simulator "ibmq_qasm_simulator". + [(#550)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/550) + ### Deprecations 👋 ### Documentation 📝 diff --git a/doc/devices/ibmq.rst b/doc/devices/ibmq.rst deleted file mode 100644 index 880828e54..000000000 --- a/doc/devices/ibmq.rst +++ /dev/null @@ -1,84 +0,0 @@ -IBM Q Experience -================ - -PennyLane-Qiskit supports running PennyLane on IBM Q hardware via the ``qistkit.ibmq`` device. -You can choose between different backends - either simulators tailor-made to emulate the real hardware, -or the real hardware itself. - -Accounts and Tokens -~~~~~~~~~~~~~~~~~~~ - -By default, the ``qiskit.ibmq`` device will attempt to use an already active or stored -IBM Q account. If the device finds no account it will raise an error: - -.. code:: - - 'No active IBM Q account, and no IBM Q token provided. - -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 - -.. code:: - - [qiskit.global] - - [qiskit.ibmq] - ibmqx_token = "XXX" - -You may also directly pass your IBM Q API token to the device: - -.. code-block:: python - - 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. - -Backends -~~~~~~~~ - -By default, the ``qiskit.ibmq`` device uses the simulator backend -``ibmq_qasm_simulator``, but this may be changed to any of the real backends as returned by - -.. code-block:: python - - dev.capabilities()['backend'] - -Most of the backends of the ``qiskit.ibmq`` device, such as ``ibmq_london`` or ``ibmq_16_melbourne``, -are *hardware backends*. Running PennyLane with these backends means to send the circuit as a job to the actual quantum -computer and retrieve the results via the cloud. - -Specifying providers -~~~~~~~~~~~~~~~~~~~~ - -Custom providers can be passed as arguments when a ``qiskit.ibmq`` device is created: - -.. code-block:: python - - 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) - -If no provider is passed explicitly, then the official provider options are used, -``hub='ibm-q'``, ``group='open'`` and ``project='main'``. - -Custom provider options can also be passed as keyword arguments when creating a device: - -.. code-block:: python - - import pennylane as qml - dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', - ibmqx_token='XXX', hub='MYHUB', group='MYGROUP', project='MYPROJECT') - -More details on Qiskit providers can be found -in the `IBMQ provider documentation `_. diff --git a/doc/devices/runtime.rst b/doc/devices/runtime.rst deleted file mode 100644 index db94c90cc..000000000 --- a/doc/devices/runtime.rst +++ /dev/null @@ -1,21 +0,0 @@ -Qiskit Runtime Programs -======================= - -PennyLane-Qiskit supports running PennyLane on IBM Q hardware via the Qiskit runtime programs ``circuit-runner`` -and ``sampler``. You can choose between those two runtime programs and also have the possibility to choose the -backend on which the circuits will be run. Those two devices inherit directly from the ``IBMQ`` device and work the -the same way, you can refer to the corresponding documentation for details about token and providers -`IBMQ documentation for PennyLane `_. - -You can use the ``circuit_runner`` and ``sampler`` devices by using their short names, for example: - -.. code-block:: python - - dev = qml.device('qiskit.ibmq.circuit_runner', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) - - -.. code-block:: python - - dev = qml.device('qiskit.ibmq.sampler', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) - -More details on Qiskit runtime programs in the `IBMQ runtime documentation `_. diff --git a/doc/index.rst b/doc/index.rst index 7699ea78e..7f7237996 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -27,27 +27,11 @@ The following devices are available: :description: A simple local Python simulator running the Qiskit ``BasicSimulator``. :link: devices/basicsim.html -.. title-card:: - :name: 'qiskit.ibmq.circuit_runner' - :description: Allows integration with Qiskit's circuit runner runtime program. - :link: devices/runtime.html - -.. title-card:: - :name: 'qiskit.ibmq.sampler' - :description: Allows integration with Qiskit's sampler runtime program. - :link: devices/runtime.html - .. title-card:: :name: 'qiskit.remote' :description: Allows integration with any Qiskit backend. :link: devices/remote.html -.. title-card:: - :name: 'qiskit.ibmq' - :description: Allows integration with Qiskit's hardware backends, and hardware-specific simulators. - :link: devices/ibmq.html - - .. raw:: html
@@ -112,7 +96,7 @@ You can also try it out using any of the qubit based `demos from the PennyLane d `_, for example the tutorial on `qubit rotation `_. Simply replace ``'default.qubit'`` with any of the available Qiskit devices, -such as ``'qiskit.aer'``, or ``'qiskit.ibmq'`` if you have an API key for +such as ``'qiskit.aer'``, or ``'qiskit.remote'`` if you have an API key for hardware access. .. raw:: html @@ -135,8 +119,6 @@ hardware access. devices/aer devices/basicsim - devices/ibmq - devices/runtime devices/remote .. toctree:: diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 1298cceed..2a886b80e 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -16,8 +16,5 @@ from ._version import __version__ from .aer import AerDevice from .basic_sim import BasicSimulatorDevice -from .ibmq import IBMQDevice from .remote import RemoteDevice from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file -from .runtime_devices import IBMQCircuitRunnerDevice -from .runtime_devices import IBMQSamplerDevice diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py deleted file mode 100644 index 8f088abf7..000000000 --- a/pennylane_qiskit/ibmq.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains the :class:`~.IBMQDevice` class, a PennyLane device that allows -evaluation and differentiation of IBM Q's Quantum Processing Units (QPUs) -using PennyLane. -""" -import os - -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 - - -class IBMQDevice(QiskitDevice): - """A PennyLane device for the IBMQ API (remote) backend. - - For more details, see the `Qiskit documentation `_ - - You need to register at `IBMQ `_ in order to - recieve a token that is used for authentication using the API. - - As of the writing of this documentation, the API is free of charge, although - there is a credit system to limit access to the quantum devices. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). 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 ``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 - 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 - 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_aer.noise``. - Only applicable for simulator backends. - hub (str): Name of the provider hub. - group (str): Name of the provider group. - project (str): Name of the provider project. - """ - - short_name = "qiskit.ibmq" - - 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) - - 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 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, pylint:disable=arguments-differ - res = super().batch_execute(circuits, timeout=self.timeout_secs) - if self.tracker.active: - self._track_run() - return res - - 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.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}. If your program takes a long time, you may want to configure the device with a higher `timeout_secs`" - ) - - job_time = { - "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() - - -def connect(kwargs): - """Function that allows connection to IBMQ. - - 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") - - 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.save_account(token=token, url=url, instance=instance) diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py deleted file mode 100644 index 174fd6c85..000000000 --- a/pennylane_qiskit/runtime_devices.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright 2021-2022 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This module contains classes for constructing Qiskit runtime devices for PennyLane. -""" -# pylint: disable=attribute-defined-outside-init, protected-access, arguments-renamed - -import numpy as np - -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime.constants import RunnerResult -from pennylane_qiskit.ibmq import IBMQDevice - - -class IBMQCircuitRunnerDevice(IBMQDevice): - r"""Class for a Qiskit runtime circuit-runner program device in PennyLane. Circuit runner is a - runtime program that takes one or more circuits, compiles them, executes them, and optionally - applies measurement error mitigation. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider): The Qiskit simulation provider - backend (str): the desired backend - shots (int): Number of circuit evaluations/random samples used to estimate expectation values and variances of - observables. Default=1024. - - Keyword Args: - initial_layout (array[int]): Initial position of virtual qubits on physical qubits. - layout_method (string): Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre') - routing_method (string): Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre'). - translation_method (string): Name of translation pass ('unroller', 'translator', 'synthesis'). - seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. - optimization_level (int): How much optimization to perform on the circuits (0-3). Higher levels generate more - optimized circuits. Default is 1. - init_qubits (bool): Whether to reset the qubits to the ground state for each shot. - rep_delay (float): Delay between programs in seconds. - transpiler_options (dict): Additional compilation options. - measurement_error_mitigation (bool): Whether to apply measurement error mitigation. Default is False. - """ - - short_name = "qiskit.ibmq.circuit_runner" - - def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): - self.kwargs = kwargs - super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) - self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") - - def batch_execute(self, circuits): - compiled_circuits = self.compile_circuits(circuits) - - program_inputs = {"circuits": compiled_circuits, "shots": self.shots} - - for kwarg in self.kwargs: - program_inputs[kwarg] = self.kwargs.get(kwarg) - - # Specify the backend. - options = {"backend": self.backend.name, "job_tags": self.kwargs.get("job_tags")} - - session_id = self.kwargs.get("session_id") - - # Send circuits to the cloud for execution by the circuit-runner program. - job = self.runtime_service.run( - program_id="circuit-runner", - options=options, - inputs=program_inputs, - session_id=session_id, - ) - self._current_job = job.result(decoder=RunnerResult) - - results = [] - - for index, circuit in enumerate(circuits): - self._samples = self.generate_samples(index) - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - if self.tracker.active: - job_time = { - "total_time": self._current_job._metadata.get("time_taken"), - } - self.tracker.update(batches=1, batch_len=len(circuits), job_time=job_time) - self.tracker.record() - - return results - - def generate_samples(self, circuit=None): - r"""Returns the computational basis samples generated for all wires. - - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. - - Args: - circuit (int): position of the circuit in the batch. - - Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` - """ - counts = self._current_job.get_counts() - # Batch of circuits - if not isinstance(counts, dict): - counts = self._current_job.get_counts()[circuit] - - samples = [] - for key, value in counts.items(): - for _ in range(0, value): - samples.append(key) - return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) - - -class IBMQSamplerDevice(IBMQDevice): - r"""Class for a Qiskit runtime sampler program device in PennyLane. Sampler is a Qiskit runtime program - that samples distributions generated by given circuits executed on the target backend. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider): the Qiskit simulation provider - backend (str): the desired backend - shots (int or None): Number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. Default=1024. - - Keyword Args: - circuit_indices (bool): Indices of the circuits to evaluate. Default is ``range(0, len(circuits))``. - run_options (dict): A collection of kwargs passed to backend.run, if shots are given here it will take - precedence over the shots arg. - skip_transpilation (bool): Skip circuit transpilation. Default is False. - """ - - short_name = "qiskit.ibmq.sampler" - - def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): - self.kwargs = kwargs - super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) - self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") - - def batch_execute(self, circuits): - compiled_circuits = self.compile_circuits(circuits) - - program_inputs = {"circuits": compiled_circuits} - - if "circuits_indices" not in self.kwargs: - 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 "shots" not in self.kwargs["run_options"]: - self.kwargs["run_options"]["shots"] = self.shots - else: - self.kwargs["run_options"] = {"shots": self.shots} - - for kwarg in self.kwargs: - program_inputs[kwarg] = self.kwargs.get(kwarg) - - # Specify the backend. - options = {"backend": self.backend.name} - # Send circuits to the cloud for execution by the sampler program. - job = self.runtime_service.run(program_id="sampler", options=options, inputs=program_inputs) - self._current_job = job.result() - - results = [] - - counter = 0 - for index, circuit in enumerate(circuits): - if index in circuit_indices: - self._samples = self.generate_samples(counter) - counter += 1 - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - if self.tracker.active: - self.tracker.update(batches=1, batch_len=len(circuits)) - self.tracker.record() - - return results - - # pylint: disable=arguments-differ - def generate_samples(self, circuit_id=None): - r"""Returns the computational basis samples generated for all wires. - - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. - - Args: - circuit_id (int): position of the circuit in the batch. - - Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` - """ - # We get nearest probability distribution because the quasi-distribution may contain negative probabilities - counts = ( - self._current_job.quasi_dists[circuit_id] - .nearest_probability_distribution() - .binary_probabilities() - ) - # Since qiskit does not return padded string we need to recover the number of qubits with self.num_wires - number_of_states = 2**self.num_wires - # Initialize probabilities to 0 - probs = [0] * number_of_states - # Fill in probabilities from counts: (state, prob) (e.g. ('010', 0.5)) - for state, prob in counts.items(): - # Formatting all strings to the same lenght - while len(state) < self.num_wires: - state = "0" + state[:] - # Inverting the order to recover Pennylane convention - probs[int(state[::-1], 2)] = prob - return self.states_to_binary( - self.sample_basis_states(number_of_states, probs), self.num_wires - ) diff --git a/setup.py b/setup.py index 2ad85947b..a06e430cf 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,6 @@ 'qiskit.aer = pennylane_qiskit:AerDevice', 'qiskit.basicaer = pennylane_qiskit:BasicAerDevice', 'qiskit.basicsim = pennylane_qiskit:BasicSimulatorDevice', - 'qiskit.ibmq = pennylane_qiskit:IBMQDevice', - 'qiskit.ibmq.circuit_runner = pennylane_qiskit:IBMQCircuitRunnerDevice', - 'qiskit.ibmq.sampler = pennylane_qiskit:IBMQSamplerDevice' ], 'pennylane.io': [ 'qiskit = pennylane_qiskit:load', diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py deleted file mode 100644 index 36257b58d..000000000 --- a/tests/test_ibmq.py +++ /dev/null @@ -1,323 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -r""" -This module contains tests for PennyLane IBMQ devices. -""" -from unittest.mock import patch -from functools import partial - -import numpy as np -import pennylane as qml -import pytest - -from qiskit_ibm_provider import IBMProvider -from qiskit_ibm_provider.exceptions import IBMAccountError -from qiskit_ibm_provider.job import IBMJobError, IBMCircuitJob - -from pennylane_qiskit import IBMQDevice -from pennylane_qiskit import ibmq - -# pylint: disable=protected-access, unused-argument, too-few-public-methods - - -class MockQiskitDeviceInit: - """A mocked version of the QiskitDevice __init__ method which - is called on by the IBMQDevice""" - - # pylint: disable=attribute-defined-outside-init - def mocked_init(self, wires, provider, backend, shots, **kwargs): - """Stores the provider which QiskitDevice.__init__ was - called with.""" - self.provider = provider - - -def test_multi_load_changing_token(monkeypatch): - """Test multiple account loads with changing tokens.""" - with monkeypatch.context() as m: - # unrelated mock - m.setattr(ibmq.QiskitDevice, "__init__", lambda self, *a, **k: None) - - # mock save_account to save the token, saved_accounts lists those tokens - tokens = [] - - def saved_accounts(): - return {f"account-{i}": {"token": t} for i, t in enumerate(tokens)} - - def save_account(token=None, **kwargs): - tokens.append(token) - - 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", "T1") - IBMQDevice(wires=1) - assert tokens == ["T1"] - IBMQDevice(wires=1) - assert tokens == ["T1"] - - m.setenv("IBMQX_TOKEN", "T2") - IBMQDevice(wires=1) - assert tokens == ["T1", "T2"] - - -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") - mock = mocker.patch("qiskit_ibm_provider.IBMProvider.save_account") - - 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") - - mock.assert_called_with(token="TheTrueToken", url=None, instance="ibm-q/open/main") - - -def test_custom_provider(monkeypatch): - """Tests that a custom provider can be passed when creating an IBMQ - device.""" - mock_provider = "MockProvider" - mock_qiskit_device = MockQiskitDeviceInit() - 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(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 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(IBMProvider, "__init__", provider_init) - m.setattr(IBMProvider, "saved_accounts", lambda: {"my-account": {"token": "1"}}) - IBMQDevice(wires=2, backend="ibmq_qasm_simulator") - - assert isinstance(mock_qiskit_device.provider, IBMProvider) - assert mock_qiskit_device.provider.instance == "ibm-q/open/main" - - -def test_custom_provider_hub_group_project_url(monkeypatch, mocker): - """Tests that the custom arguments passed during device instantiation are - 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__", 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", - ) - - mock.assert_called_with(token="1", url="example.com", instance=instance) - - -@pytest.mark.usefixtures("skip_if_account_saved") -class TestMustNotHaveAccount: - """Tests that require the user _not_ have an IBMQ account loaded.""" - - 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) - - 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) - - @partial(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): - """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 - 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"}, - ) - @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.""" - 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) - 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 - batch_execute_spy.assert_called_with(dev, mocker.ANY, timeout=timeout) - wait_spy.assert_called_with(mocker.ANY, timeout=timeout or 60) diff --git a/tests/test_runtime.py b/tests/test_runtime.py deleted file mode 100644 index 23d83d18f..000000000 --- a/tests/test_runtime.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -r""" -This module contains tests for PennyLane runtime programs. -""" - -from functools import partial -import numpy as np -import pennylane as qml -import pytest - -from pennylane_qiskit import IBMQCircuitRunnerDevice, IBMQSamplerDevice - - -@pytest.mark.usefixtures("skip_if_no_account") -class TestCircuitRunner: - """Test class for the circuit runner IBMQ runtime device.""" - - def test_short_name(self): - """Test that we can call the circuit runner using its shortname.""" - dev = qml.device("qiskit.ibmq.circuit_runner", wires=1) - assert isinstance(dev, IBMQCircuitRunnerDevice) - - @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, tol, shots): - """Test executing a simple circuit submitted to IBMQ circuit runner runtime program.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - @pytest.mark.parametrize( - "kwargs", - [ - { - "layout_method": "trivial", - "routing_method": "basic", - "translation_method": "translator", - "seed_transpiler": 42, - "optimization_level": 2, - "init_qubits": True, - "rep_delay": 0.01, - "transpiler_options": {"approximation_degree": 1.0}, - "measurement_error_mmitigation": True, - } - ], - ) - def test_kwargs_circuit(self, tol, shots, kwargs): - """Test executing a simple circuit submitted to IBMQ circuit runner runtime program with kwargs.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, tol, shots): - """Test that we can send batched circuits to the circuit runner runtime program.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - # Batch the input parameters - batch_dim = 3 - a = np.linspace(0, 0.543, batch_dim) - b = np.linspace(0, 0.123, batch_dim) - c = np.linspace(0, 0.987, batch_dim) - - @partial(qml.batch_params, all_operations=True) - @qml.qnode(dev) - def circuit(x, y, z): - """Reference QNode""" - qml.PauliX(0) - qml.Hadamard(wires=0) - qml.Rot(x, y, z, wires=0) - return qml.expval(qml.PauliZ(0)) - - assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - - def test_track_circuit_runner(self): - """Test that the tracker works.""" - - dev = IBMQCircuitRunnerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - circuit() - - assert "job_time" in dev.tracker.history - if "job_time" in dev.tracker.history: - assert "total_time" in dev.tracker.history["job_time"][0] - assert len(dev.tracker.history["job_time"][0]) == 1 - - -@pytest.mark.usefixtures("skip_if_no_account") -class TestSampler: - """Test class for the sampler IBMQ runtime device.""" - - def test_short_name(self): - dev = qml.device("qiskit.ibmq.sampler", wires=1) - assert isinstance(dev, IBMQSamplerDevice) - - @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, tol, shots): - """Test executing a simple circuit submitted to IBMQ using the Sampler device.""" - - dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - @pytest.mark.parametrize( - "kwargs", - [ - { - "circuit_indices": [0], - "run_options": {"seed_simulator": 42}, - "skip_transpilation": False, - } - ], - ) - def test_kwargs_circuit(self, tol, shots, kwargs): - """Test executing a simple circuit submitted to IBMQ using the Sampler device with kwargs.""" - - dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, tol, shots): - """Test executing batched circuits submitted to IBMQ using the Sampler device.""" - - dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=shots) - - # Batch the input parameters - batch_dim = 3 - a = np.linspace(0, 0.543, batch_dim) - b = np.linspace(0, 0.123, batch_dim) - c = np.linspace(0, 0.987, batch_dim) - - @partial(qml.batch_params, all_operations=True) - @qml.qnode(dev) - def circuit(x, y, z): - """Reference QNode""" - qml.PauliX(0) - qml.Hadamard(wires=0) - qml.Rot(x, y, z, wires=0) - return qml.expval(qml.PauliZ(0)) - - assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - - def test_track_sampler(self): - """Test that the tracker works.""" - - dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - circuit() - - assert len(dev.tracker.history) == 2 diff --git a/tests/test_sample.py b/tests/test_sample.py deleted file mode 100644 index c5d620459..000000000 --- a/tests/test_sample.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -r""" -This module contains tests for sampling from PennyLane IBMQ devices. -""" -import pytest - -import numpy as np -from flaky import flaky -import pennylane as qml - -# pylint: disable=protected-access, unused-argument, too-many-arguments - -np.random.seed(42) - -THETA = np.linspace(0.11, 1, 3) -PHI = np.linspace(0.32, 1, 3) -VARPHI = np.linspace(0.02, 1, 3) - - -@pytest.mark.parametrize("shots", [8192]) -class TestSample: - """Tests for the sample return type""" - - def test_sample_values(self, device, shots, tol): - """Tests if the samples returned by sample have - the correct values - """ - dev = device(1) - par = 1.5708 - - observable = qml.PauliZ(wires=[0]) - - dev.apply( - [ - qml.RX(par, wires=[0]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - @pytest.mark.parametrize("theta", THETA) - def test_sample_values_hermitian(self, theta, device, shots, tol): - """Tests if the samples of a Hermitian observable returned by sample have - the correct values - """ - dev = device(1) - - A = np.array([[1, 2j], [-2j, 0]]) - - observable = qml.Hermitian(A, wires=[0]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix - eigvals = np.linalg.eigvalsh(A) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - # the analytic mean is 2*sin(theta)+0.5*cos(theta)+0.5 - assert np.allclose(np.mean(s1), 2 * np.sin(theta) + 0.5 * np.cos(theta) + 0.5, **tol) - - # the analytic variance is 0.25*(sin(theta)-4*cos(theta))^2 - assert np.allclose(np.var(s1), 0.25 * (np.sin(theta) - 4 * np.cos(theta)) ** 2, **tol) - - @pytest.mark.parametrize("theta", THETA) - def test_sample_values_hermitian_multi_qubit(self, theta, device, shots, tol): - """Tests if the samples of a multi-qubit Hermitian observable returned by sample have - the correct values - """ - dev = device(2) - - A = np.array( - [ - [1, 2j, 1 - 2j, 0.5j], - [-2j, 0, 3 + 4j, 1], - [1 + 2j, 3 - 4j, 0.75, 1.5 - 2j], - [-0.5j, 1, 1.5 + 2j, -1], - ] - ) - - observable = qml.Hermitian(A, wires=[0, 1]) - - dev.apply( - [qml.RX(theta, wires=[0]), qml.RY(2 * theta, wires=[1]), qml.CNOT(wires=[0, 1])], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix - eigvals = np.linalg.eigvalsh(A) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - # make sure the mean matches the analytic mean - expected = ( - 88 * np.sin(theta) - + 24 * np.sin(2 * theta) - - 40 * np.sin(3 * theta) - + 5 * np.cos(theta) - - 6 * np.cos(2 * theta) - + 27 * np.cos(3 * theta) - + 6 - ) / 32 - assert np.allclose(np.mean(s1), expected, **tol) - - -@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) -@pytest.mark.parametrize("shots", [8192]) -class TestTensorSample: - """Test tensor expectation values""" - - def test_paulix_pauliy(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving PauliX and PauliY works correctly""" - dev = device(3) - - observable = qml.PauliX(wires=[0]) @ qml.PauliY(wires=[2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - mean = np.mean(s1) - expected = np.sin(theta) * np.sin(phi) * np.sin(varphi) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 8 * np.sin(theta) ** 2 * np.cos(2 * varphi) * np.sin(phi) ** 2 - - np.cos(2 * (theta - phi)) - - np.cos(2 * (theta + phi)) - + 2 * np.cos(2 * theta) - + 2 * np.cos(2 * phi) - + 14 - ) / 16 - assert np.allclose(var, expected, **tol) - - def test_pauliz_hadamard_pauliy(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving PauliZ and PauliY and hadamard works correctly""" - dev = device(3) - - observable = qml.PauliZ(wires=[0]) @ qml.Hadamard(wires=[1]) @ qml.PauliY(wires=[2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - mean = np.mean(s1) - expected = -(np.cos(varphi) * np.sin(phi) + np.sin(varphi) * np.cos(theta)) / np.sqrt(2) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 3 - + np.cos(2 * phi) * np.cos(varphi) ** 2 - - np.cos(2 * theta) * np.sin(varphi) ** 2 - - 2 * np.cos(theta) * np.sin(phi) * np.sin(2 * varphi) - ) / 4 - assert np.allclose(var, expected, **tol) - - @flaky(max_runs=5, min_passes=3) - def test_hermitian(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving qml.Hermitian works correctly""" - dev = device(3) - - A = np.array( - [ - [-6, 2 + 1j, -3, -5 + 2j], - [2 - 1j, 0, 2 - 1j, -5 + 4j], - [-3, 2 + 1j, 0, -4 + 3j], - [-5 - 2j, -5 - 4j, -4 - 3j, -6], - ] - ) - observable = qml.PauliZ(wires=[0]) @ qml.Hermitian(A, wires=[1, 2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix tensor product Z - Z = np.diag([1, -1]) - eigvals = np.linalg.eigvalsh(np.kron(Z, A)) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - mean = np.mean(s1) - expected = 0.5 * ( - -6 * np.cos(theta) * (np.cos(varphi) + 1) - - 2 * np.sin(varphi) * (np.cos(theta) + np.sin(phi) - 2 * np.cos(phi)) - + 3 * np.cos(varphi) * np.sin(phi) - + np.sin(phi) - ) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 1057 - - np.cos(2 * phi) - + 12 * (27 + np.cos(2 * phi)) * np.cos(varphi) - - 2 * np.cos(2 * varphi) * np.sin(phi) * (16 * np.cos(phi) + 21 * np.sin(phi)) - + 16 * np.sin(2 * phi) - - 8 * (-17 + np.cos(2 * phi) + 2 * np.sin(2 * phi)) * np.sin(varphi) - - 8 * np.cos(2 * theta) * (3 + 3 * np.cos(varphi) + np.sin(varphi)) ** 2 - - 24 * np.cos(phi) * (np.cos(phi) + 2 * np.sin(phi)) * np.sin(2 * varphi) - - 8 - * np.cos(theta) - * ( - 4 - * np.cos(phi) - * ( - 4 - + 8 * np.cos(varphi) - + np.cos(2 * varphi) - - (1 + 6 * np.cos(varphi)) * np.sin(varphi) - ) - + np.sin(phi) - * ( - 15 - + 8 * np.cos(varphi) - - 11 * np.cos(2 * varphi) - + 42 * np.sin(varphi) - + 3 * np.sin(2 * varphi) - ) - ) - ) / 16 - assert np.allclose(var, expected, **tol) From 343e984ac059aad39578d511f02e457f695f187b Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:52:27 -0400 Subject: [PATCH 32/47] Migrate to v2 primitives (#539) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * deleting unused tests * line change * pylint * yay * docstring * refactoring of estimator and sampler * process_estimator_job tests * comment for clarity --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 109 +++++-------- tests/test_base_device.py | 245 ++++++++++++----------------- 2 files changed, 138 insertions(+), 216 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index c24da8978..8d269da3d 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -28,8 +28,7 @@ from qiskit.compiler import transpile from qiskit.providers import BackendV2 -from qiskit_ibm_runtime import Session, Sampler, Estimator -from qiskit_ibm_runtime.options import Options +from qiskit_ibm_runtime import Session, SamplerV2 as Sampler, EstimatorV2 as Estimator from pennylane import transform from pennylane.transforms.core import TransformProgram @@ -180,23 +179,11 @@ def reorder_fn(res): result = dict(zip(flattened_indices, flattened_results)) - return tuple(result[i] for i in sorted(result.keys())) - - return tapes, reorder_fn + result = tuple(result[i] for i in sorted(result.keys())) + return result[0] if len(result) == 1 else result -def qiskit_options_to_flat_dict(options): - """Create a dictionary from a Qiskit Options object""" - # this will break (or at least overwrite potentially relevant information) - # if they name things in some categories on Options the same as things in - # other categories, but at that point they've really departed from the kwarg API - options_dict = {} - for key, val in vars(options).items(): - if hasattr(val, "__dict__"): - options_dict.update(qiskit_options_to_flat_dict(val)) - elif val is not None: - options_dict[key] = val - return options_dict + return tapes, reorder_fn class QiskitDevice2(Device): @@ -211,9 +198,6 @@ class QiskitDevice2(Device): Keyword Args: shots (int or None): number of circuit evaluations/random samples used to estimate expectation values and variances of observables. - options (Options): a Qiskit Options object for specifying handling the Qiskit task - (transpiliation, error mitigation, execution, etc). Defaults to None. See Qiskit documentation - for more details. session (Session): a Qiskit Session to use for device execution. If none is provided, a session will be created at each device execution. compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be @@ -242,7 +226,6 @@ def __init__( wires, backend, shots=1024, - options=None, session=None, compile_backend=None, **kwargs, @@ -259,10 +242,6 @@ def __init__( shots = 1024 - self.options = options or Options() - if self.options.execution.shots == 4000: ## 4000 is default value in Qiskit. - self.options.execution.shots = shots - super().__init__(wires=wires, shots=shots) self._backend = backend @@ -275,9 +254,7 @@ def __init__( self._init_kwargs = kwargs # _kwargs are used instead of the Options for performing raw sample based measurements (using old Qiskit API) # the _kwargs are a combination of information from Options and _init_kwargs - self._kwargs = None - if self.options.simulator.noise_model: - self.backend.set_options(noise_model=self.options.simulator.noise_model) + self._kwargs = kwargs # Perform validation against backend available_qubits = ( @@ -289,7 +266,7 @@ def __init__( raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") self.reset() - self._update_kwargs() + # ToDo: process or update kwargs in some fashion here @property def backend(self): @@ -395,30 +372,6 @@ def preprocess( return transform_program, config - def _update_kwargs(self): - """Combine the settings defined in options and the settings passed as kwargs, with - the definition in options taking precedence if there is conflicting information""" - option_kwargs = qiskit_options_to_flat_dict(self.options) - - overlapping_kwargs = set(self._init_kwargs).intersection(set(option_kwargs)) - if overlapping_kwargs: - warnings.warn( - f"The keyword argument(s) {overlapping_kwargs} passed to the device are also " - f"defined in the device Options. The definition in Options will be used." - ) - if option_kwargs["shots"] != self.shots.total_shots: - warnings.warn( - f"Setting shots via the Options is not supported on PennyLane devices. The shots {self.shots} " - f"passed to the device will be used." - ) - self.options.execution.shots = self.shots.total_shots - - option_kwargs.pop("shots") - kwargs = self._init_kwargs.copy() - kwargs.update(option_kwargs) - - self._kwargs = kwargs - @staticmethod def get_transpile_args(kwargs): """The transpile argument setter. @@ -474,9 +427,9 @@ def execute_circuits(session): try: for circ in circuits: if circ.shots and len(circ.shots.shot_vector) > 1: - warnings.warn( + raise ValueError( f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." - f"The circuit will be run once with {circ.shots.total_shots} shots instead." + "Please use a single integer instead when specifying the number of shots." ) if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)) and getattr( circ.measurements[0].obs, "pauli_rep", None @@ -495,36 +448,46 @@ def execute_circuits(session): def _execute_sampler(self, circuit, session): """Execution for the Sampler primitive""" - qcirc = circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True) - if circuit.shots: - self.options.execution.shots = circuit.shots.total_shots - sampler = Sampler(session=session, options=self.options) + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True)] + sampler = Sampler(session=session) + compiled_circuits = self.compile_circuits(qcirc) - result = sampler.run(qcirc).result() - self._current_job = result + # len(compiled_circuits) is always 1 so the indexing does not matter. + result = sampler.run(compiled_circuits).result()[0] + classical_register_name = compiled_circuits[0].cregs[0].name + self._current_job = getattr(result.data, classical_register_name) + + results = [] # needs processing function to convert to the correct format for states, and # also handle instances where wires were specified in probs, and for multiple probs measurements # single_measurement = len(circuit.measurements) == 1 # res = (res[0], ) if single_measurement else tuple(res) - return (result.quasi_dists[0],) + self._samples = self.generate_samples(0) + res = [ + mp.process_samples(self._samples, wire_order=self.wires) for mp in circuit.measurements + ] + single_measurement = len(circuit.measurements) == 1 + res = res[0] if single_measurement else tuple(res) + results.append(res) + + return tuple(results) def _execute_estimator(self, circuit, session): # the Estimator primitive takes care of diagonalization and measurements itself, # so diagonalizing gates and measurements are not included in the circuit - qcirc = circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False) - if circuit.shots: - self.options.execution.shots = circuit.shots.total_shots - estimator = Estimator(session=session, options=self.options) + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False)] + estimator = Estimator(session=session) + pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] + compiled_circuits = self.compile_circuits(qcirc) # split into one call per measurement # could technically be more efficient if there are some observables where we ask # for expectation value and variance on the same observable, but spending time on # that right now feels excessive - - pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] - result = estimator.run([qcirc] * len(pauli_observables), pauli_observables).result() + circ_and_obs = [(compiled_circuits[0], pauli_observables)] + result = estimator.run(circ_and_obs).result() self._current_job = result result = self._process_estimator_job(circuit.measurements, result) @@ -536,8 +499,12 @@ def _process_estimator_job(measurements, job_result): along with some metadata. Extract the relevant number for each measurement process and return the requested results from the Estimator executions.""" - expvals = job_result.values - variances = [res["variance"] for res in job_result.metadata] + expvals = job_result[0].data.evs + variances = ( + job_result[0].data.stds ** 2 * 4096 + ) # this 4096 is the # of shots Qiskit uses by default. It is hard-coded here. + # ToDo: Track the # of shots and use that instead of hard-coding + # to calculate the variance. result = [] for i, mp in enumerate(measurements): diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 93e3a0148..0e8b17992 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -23,8 +23,7 @@ import pennylane as qml from pennylane.tape.qscript import QuantumScript -from qiskit_ibm_runtime import Estimator -from qiskit_ibm_runtime.options import Options +from qiskit_ibm_runtime import EstimatorV2 as Estimator from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 from qiskit_aer import AerSimulator @@ -33,20 +32,18 @@ # named Estimator object has a different call signature than the remote device Estimator, # and only runs local simulations. We need the Estimator from qiskit_ibm_runtime. They # both use this EstimatorResults, however: -from qiskit.primitives import EstimatorResult from qiskit.providers import BackendV1, BackendV2 -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, transpile from pennylane_qiskit.qiskit_device2 import ( QiskitDevice2, qiskit_session, split_execution_types, - qiskit_options_to_flat_dict, ) from pennylane_qiskit.converter import ( circuit_to_qiskit, - mp_to_pauli, QISKIT_OPERATION_MAP, + mp_to_pauli, ) # pylint: disable=protected-access, unused-argument, too-many-arguments, redefined-outer-name @@ -122,16 +119,6 @@ def close(self): # This is just to appease a test test_dev = QiskitDevice2(wires=5, backend=backend) -def options_for_testing(): - """Creates an Options object with defined values in multiple sub-categories""" - options = Options() - options.environment.job_tags = ["getting angle"] - options.resilience.noise_amplifier = "LocalFoldingAmplifier" - options.optimization_level = 2 - options.resilience_level = 1 - return options - - class TestSupportForV1andV2: """Tests compatibility with BackendV1 and BackendV2""" @@ -147,13 +134,10 @@ def test_v1_and_v2_mocked(self, backend): @pytest.mark.parametrize( "backend, shape", [ - (FakeManila(), (1, 1024)), - (FakeManilaV2(), (1, 1024)), + (FakeManila(), (1024,)), + (FakeManilaV2(), (1024,)), ], ) - @pytest.mark.skip( - reason="The functionality of using sampler to get an accurate answer is not yet implemented" - ) def test_v1_and_v2_manila(self, backend, shape): """Test that device initializes and runs without error with V1 and V2 backends by Qiskit""" dev = QiskitDevice2(wires=5, backend=backend) @@ -197,14 +181,13 @@ def test_no_shots_warns_and_defaults(self): dev = QiskitDevice2(wires=2, backend=backend, shots=None) assert dev.shots.total_shots == 1024 - assert dev.options.execution.shots == 1024 + @pytest.mark.skip(reason="Options handling not decided on yet") def test_kwargs_on_initialization(self, mocker): """Test that update_kwargs is called on intialization and combines the Options and kwargs as self._kwargs""" - options = Options() - options.environment.job_tags = ["my_tag"] + options = {"my_tag": 1} spy = mocker.spy(QiskitDevice2, "_update_kwargs") @@ -239,11 +222,12 @@ def test_backend_wire_validation(self, backend): with pytest.raises(ValueError, match="supports maximum"): QiskitDevice2(wires=500, backend=backend) + @pytest.mark.skip(reason="Options handling not decided on yet") def test_setting_simulator_noise_model(self): """Test that the simulator noise model saved on a passed Options object is used to set the backend noise model""" - options = Options() + options = {} options.simulator.noise_model = {"placeholder": 1} new_backend = MockedBackend() @@ -391,7 +375,11 @@ def test_split_execution_types(self, measurements, expectation): assert [tape.measurements for tape in tapes] == expectation # reorder_fn puts them back - assert reorder_fn([tape.measurements for tape in tapes]) == tuple(qs.measurements) + assert ( + reorder_fn([tape.measurements for tape in tapes]) == qs.measurements[0] + if len(qs.measurements) == 1 + else reorder_fn([tape.measurements for tape in tapes]) == tuple(qs.measurements) + ) @pytest.mark.parametrize( "op, expected", @@ -477,43 +465,18 @@ def test_intial_state_prep_also_decomposes(self): assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) +@pytest.mark.skip(reason="Options handling not decided on yet") class TestOptionsHandling: - def test_qiskit_options_to_flat_dict(self): - """Test that a Qiskit Options object is converted to an un-nested python dictionary""" - - options = options_for_testing() - - options_dict = qiskit_options_to_flat_dict(options) - - assert isinstance(options_dict, dict) - # the values in the dict are not themselves dictionaries or convertable to dictionaries - for val in options_dict.values(): - assert not hasattr(val, "__dict__") - assert not isinstance(val, dict) - - @pytest.mark.parametrize("options", [None, options_for_testing()]) - def test_shots_kwarg_updates_default_options(self, options): - """Check that the shots passed to the device are set on the device - as well as updated on the Options object""" - - dev = QiskitDevice2(wires=2, backend=backend, shots=23, options=options) - - assert dev.shots.total_shots == 23 - assert dev.options.execution.shots == 23 - def test_warning_if_shots(self): """Test that a warning is raised if the user attempt to specify shots on Options instead of as a kwarg, and sets shots to the shots passed (defaults to 1024).""" - options = options_for_testing() - options.execution.shots = 1000 - with pytest.warns( UserWarning, match="Setting shots via the Options is not supported on PennyLane devices", ): - dev = QiskitDevice2(wires=2, backend=backend, options=options) + dev = QiskitDevice2(wires=2, backend=backend) assert dev.shots.total_shots == 1024 assert dev.options.execution.shots == 1024 @@ -522,7 +485,7 @@ def test_warning_if_shots(self): UserWarning, match="Setting shots via the Options is not supported on PennyLane devices", ): - dev = QiskitDevice2(wires=2, backend=backend, shots=200, options=options) + dev = QiskitDevice2(wires=2, backend=backend, shots=200) assert dev.shots.total_shots == 200 assert dev.options.execution.shots == 200 @@ -534,23 +497,13 @@ def test_update_kwargs_no_overlapping_options_passed(self): dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True, random_kwarg2="a") assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} - if Version(qiskit_ibm_runtime.__version__) < Version("0.21.0"): - assert dev._kwargs == { - "random_kwarg1": True, - "random_kwarg2": "a", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - "job_tags": [], - } - else: - assert dev._kwargs == { - "random_kwarg1": True, - "random_kwarg2": "a", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - } + assert dev._kwargs == { + "random_kwarg1": True, + "random_kwarg2": "a", + "skip_transpilation": False, + "init_qubits": True, + "log_level": "WARNING", + } dev.options.environment.job_tags = ["my_tag"] dev.options.max_execution_time = "1m" @@ -737,34 +690,6 @@ def test_compile_circuits(self, transpile_mock, compile_backend): for _, circuit in enumerate(compiled_circuits): assert isinstance(circuit, QuantumCircuit) - @pytest.mark.parametrize( - "measurements, expectation", - [ - ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))], (1, 0)), - ([qml.var(qml.PauliX(0))], (1)), - ( - [ - qml.expval(qml.PauliX(0)), - qml.expval(qml.PauliZ(0)), - qml.var(qml.PauliX(0)), - ], - (0, 1, 1), - ), - ], - ) - def test_process_estimator_job_mocked(self, measurements, expectation): - """Test the process_estimator_job function with constructed return for - Estimator (integration test that runs with a Token is below)""" - - values = np.array([np.random.ranf() for i in range(len(measurements))]) - metadata = [{"variance": np.random.ranf(), "shots": 4000} for i in range(len(measurements))] - - result = EstimatorResult(values, metadata) - processed_result = QiskitDevice2._process_estimator_job(measurements, result) - - assert isinstance(processed_result, tuple) - assert len(processed_result) == len(measurements) - @pytest.mark.parametrize( "results, index", [ @@ -817,8 +742,8 @@ def test_execute_pipeline_primitives_no_session(self, mocker): @pytest.mark.parametrize("backend", [backend, legacy_backend]) def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): - """Test that a device executes measurements that require raw samples on the sampler, - and the relevant primitive measurements on the estimator""" + """Test that a device executes measurements that require raw samples via the sampler, + and the relevant primitive measurements via the estimator""" dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) @@ -865,20 +790,8 @@ def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, ses # to emphasize, this did nothing except appease CodeCov assert isinstance(result, Mock) - @patch("pennylane_qiskit.qiskit_device2.Sampler") - @pytest.mark.parametrize("session", [None, MockSession(backend)]) - def test_execute_sampler_mocked(self, mocked_sampler, session): - """Test the _execute_sampler function using a mocked version of Sampler - that returns a meaningless result.""" - - qs = QuantumScript([qml.PauliX(0)], measurements=[qml.counts()], shots=100) - result = test_dev._execute_sampler(qs, session) - - # to emphasize, this did nothing except appease CodeCov - assert isinstance(result[0], Mock) - - def test_shot_vector_warning_mocked(self): - """Test that a device that executes a circuit with an array of shots raises the appropriate warning""" + def test_shot_vector_error_mocked(self): + """Test that a device that executes a circuit with an array of shots raises the appropriate ValueError""" dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) qs = QuantumScript( @@ -889,10 +802,7 @@ def test_shot_vector_warning_mocked(self): ) with patch.object(dev, "_execute_estimator"): - with pytest.warns( - UserWarning, - match="Setting shot vector", - ): + with pytest.raises(ValueError, match="Setting shot vector"): dev.execute(qs) @@ -994,7 +904,7 @@ def test_estimator_with_various_multi_qubit_pauli_obs( sampler_execute.assert_not_called() estimator_execute.assert_called_once() - assert np.allclose(res, expectation, atol=0.3) ## atol is high due to high variance + assert np.allclose(res[0], expectation, atol=0.3) ## atol is high due to high variance @pytest.mark.parametrize( "measurements, expectation", @@ -1035,34 +945,31 @@ def test_process_estimator_job(self, measurements, expectation): # run on simulator via Estimator estimator = Estimator(backend=backend) - result = estimator.run([qcirc] * len(pauli_observables), pauli_observables).result() - - # confirm that the result is as expected - if the test fails at this point, its because the - # Qiskit result format has changed - assert isinstance(result, EstimatorResult) + compiled_circuits = [transpile(qcirc, backend=backend)] + circ_and_obs = [(compiled_circuits[0], pauli_observables)] + result = estimator.run(circ_and_obs).result() - assert isinstance(result.values, np.ndarray) - assert result.values.size == len(qs.measurements) + assert isinstance(result[0].data.evs, np.ndarray) + assert result[0].data.evs.size == len(qs.measurements) - assert isinstance(result.metadata, list) - assert len(result.metadata) == len(qs.measurements) + assert isinstance(result[0].metadata, dict) - for data in result.metadata: - assert isinstance(data, dict) processed_result = QiskitDevice2._process_estimator_job(qs.measurements, result) assert isinstance(processed_result, tuple) assert np.allclose(processed_result, expectation, atol=0.1) @pytest.mark.parametrize("num_wires", [1, 3, 5]) @pytest.mark.parametrize("num_shots", [50, 100]) - @pytest.mark.skip(reason="Need to replace this with using SamplerV2.") + @pytest.mark.skip( + reason="Need to replace this with using SamplerV2." + ) # Resolved in PR #547 https://github.com/PennyLaneAI/pennylane-qiskit/pull/547 def test_generate_samples(self, num_wires, num_shots): qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) qcirc = circuit_to_qiskit(qs, register_size=num_wires, diagonalize=True, measure=True) compiled_circuits = test_dev.compile_circuits([qcirc]) - job = test_dev.backend.run(circuits=compiled_circuits, shots=num_shots) + job = test_dev._execute_sampler(circuits=compiled_circuits, shots=num_shots) test_dev._current_job = job.result() @@ -1084,6 +991,9 @@ def test_generate_samples(self, num_wires, num_shots): # nothing else is in samples assert [s for s in samples if not s in np.array([exp_res0, exp_res1])] == [] + @pytest.mark.skip( + reason="Tracking shot information will be addressed in the PR about options handling" + ) def test_tape_shots_used_for_estimator(self, mocker): """Tests that device uses tape shots rather than device shots for estimator""" dev = QiskitDevice2(wires=5, backend=backend, shots=2) @@ -1103,6 +1013,9 @@ def circuit(): circuit() assert dev._current_job.metadata[0]["shots"] == 2 + @pytest.mark.skip( + reason="Tracking shot information will be addressed in the PR about options handling" + ) def test_tape_shots_used_for_sampler(self, mocker): """Tests that device uses tape shots rather than device shots for sampler""" dev = QiskitDevice2(wires=5, backend=backend, shots=2) @@ -1123,20 +1036,19 @@ def circuit(): circuit() assert dev._current_job.metadata[0]["shots"] == 2 - def test_warning_for_shot_vector(self): - """Tests that a warning is raised if a shot vector is passed and total shots of tape is used instead.""" + @pytest.mark.skip( + reason="Tracking shot information will be addressed in the PR about options handling" + ) + def test_error_for_shot_vector(self): + """Tests that a ValueError is raised if a shot vector is passed.""" dev = QiskitDevice2(wires=5, backend=backend, shots=2) @qml.qnode(dev) def circuit(): return qml.expval(qml.PauliX(0)) - with pytest.warns( - UserWarning, - match="Setting shot vector", - ): + with pytest.raises(ValueError, match="Setting shot vector"): circuit(shots=[5, 10, 2]) - assert dev._current_job.metadata[0]["shots"] == 17 # Should reset to device shots if circuit ran again without shots defined circuit() @@ -1151,12 +1063,9 @@ def circuit(): ], ) @pytest.mark.filterwarnings("ignore::UserWarning") - @pytest.mark.skip( - reason="The functionality of using sampler to get the accurate answer is not yet implemented" - ) def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): - """Test that the device uses _sampler and _execute_estimator appropriately - and provides an accurate answer for measurements with observables that don't have a pauli_rep. + """Test that the device uses _sampler and _execute_estimator appropriately and + provides an accurate answer for measurements with observables that don't have a pauli_rep. """ dev = QiskitDevice2(wires=5, backend=backend) @@ -1203,3 +1112,49 @@ def circuit(): match="The observable measured", ): circuit() + + def test_qiskit_probability_output_format(self): + """Test that the format and values of the Qiskit device's output for `qml.probs` is + the same as pennylane's.""" + + dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) + qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + return [qml.probs(wires=[0, 1])] + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.Hadamard(0) + return [qml.probs(wires=[0, 1])] + + res = circuit() + qiskit_res = qiskit_circuit() + + assert np.shape(res) == np.shape(qiskit_res) + assert np.allclose(res, qiskit_res, atol=0.03) + + def test_sampler_output_shape(self): + """Test that the shape of the results produced from the sampler for the Qiskit device + is consistent with Pennylane""" + dev = qml.device("default.qubit", wires=[0, 1, 2, 3], shots=1024) + qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample() + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample() + + res = circuit(np.pi / 2) + qiskit_res = qiskit_circuit(np.pi / 2) + + assert np.shape(res) == np.shape(qiskit_res) From db3197925f9c6284a64e8a91a36e41836914acd7 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Thu, 13 Jun 2024 09:45:43 -0400 Subject: [PATCH 33/47] pylint --- tests/test_qiskit_device.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 969742087..7eabcf9ca 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -15,12 +15,10 @@ This module contains tests qiskit devices for PennyLane IBMQ devices. """ from unittest.mock import Mock -from packaging.version import Version import numpy as np import pytest -import qiskit as qk from qiskit_aer import noise from qiskit.providers import BackendV1, BackendV2 from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 From f734b9335c19c7af9abccae65fcc4bfca12f0b5d Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:15:42 -0400 Subject: [PATCH 34/47] Process kwargs (#547) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * linter --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device2.py | 140 +++++++++--- tests/test_base_device.py | 342 +++++++++++++---------------- 2 files changed, 258 insertions(+), 224 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 8d269da3d..e14e98e8e 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -203,10 +203,11 @@ class QiskitDevice2(Device): compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be sent to the backend device, to be set if the backend desired for compliation differs from the backend used for execution. Defaults to ``None``, which means the primary backend will be used. - **kwargs: transpilation and runtime kwargs to be used for measurements without Qiskit Primitives. - If any values are defined both in ``options`` and in the remaining ``kwargs``, the value - provided in ``options`` will take precedence. These kwargs will be ignored for all Primitive-based - measurements on the device. + **kwargs: transpilation and runtime keyword arguments to be used for measurements with Primitives. + If an `options` dictionary is defined amongst the kwargs, and there are settings that overlap + with those in kwargs, the settings in `options` will take precedence over kwargs. Keyword + arguments accepted by both the transpiler and at runtime (e.g. ``optimization_level``) + will be passed to the transpiler rather than to the Primitive. """ operations = set(QISKIT_OPERATION_MAP.keys()) @@ -250,11 +251,7 @@ def __init__( self._service = getattr(backend, "_service", None) self._session = session - # initial kwargs are saved and referenced every time the kwargs used for transpilation and execution - self._init_kwargs = kwargs - # _kwargs are used instead of the Options for performing raw sample based measurements (using old Qiskit API) - # the _kwargs are a combination of information from Options and _init_kwargs - self._kwargs = kwargs + kwargs["shots"] = shots # Perform validation against backend available_qubits = ( @@ -266,7 +263,9 @@ def __init__( raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") self.reset() - # ToDo: process or update kwargs in some fashion here + self._kwargs, self._transpile_args = self._process_kwargs( + kwargs + ) # processes kwargs and separates transpilation arguments to dev._transpile_args @property def backend(self): @@ -372,22 +371,69 @@ def preprocess( return transform_program, config + def _process_kwargs(self, kwargs): + """Processes kwargs given and separates them into kwargs and transpile_args. If given + a keyword argument 'options' that is a dictionary, a common practice in + Qiskit, the options in said dictionary take precedence over any overlapping keyword + arguments defined in the kwargs. + + Keyword Args: + kwargs (dict): keyword arguments that set either runtime options or transpilation + options. + + Returns: + kwargs, transpile_args: keyword arguments for the runtime options and keyword + arguments for the transpiler + """ + + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model") + self.backend.set_options(noise_model=noise_model) + + if "options" in kwargs: + for key, val in kwargs.pop("options").items(): + if key in kwargs: + warnings.warn( + "An overlap between what was passed in via options and what was passed in via kwargs was found." + f"The value set in options {key}={val} will be used." + ) + kwargs[key] = val + + shots = kwargs.pop("shots") + + if "default_shots" in kwargs: + warnings.warn( + f"default_shots was found in the keyword arguments, but it is not supported by {self.name}" + "Please use the `shots` keyword argument instead. The number of shots " + f"{shots} will be used instead." + ) + kwargs["default_shots"] = shots + + kwargs, transpile_args = self.get_transpile_args(kwargs) + + return kwargs, transpile_args + @staticmethod def get_transpile_args(kwargs): - """The transpile argument setter. + """The transpile argument setter. This separates keyword arguments related to transpilation + from the rest of the keyword arguments and removes those keyword arguments from kwargs. Keyword Args: - kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the + kwargs (dict): combined keyword arguments to be parsed for the Qiskit transpiler. For more details, see the `Qiskit transpiler documentation `_ + + Returns: + kwargs (dict), transpile_args (dict): keyword arguments for the runtime options and keyword + arguments for the transpiler """ transpile_sig = inspect.signature(transpile).parameters - transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} + transpile_args = {arg: kwargs.pop(arg) for arg in transpile_sig if arg in kwargs} transpile_args.pop("circuits", None) transpile_args.pop("backend", None) - return transpile_args + return kwargs, transpile_args def compile_circuits(self, circuits): r"""Compiles multiple circuits one after the other. @@ -400,7 +446,7 @@ def compile_circuits(self, circuits): """ # Compile each circuit object compiled_circuits = [] - transpile_args = self.get_transpile_args(self._kwargs) + transpile_args = self._transpile_args for i, circuit in enumerate(circuits): compiled_circ = transpile(circuit, backend=self.compile_backend, **transpile_args) @@ -446,19 +492,30 @@ def execute_circuits(session): return results def _execute_sampler(self, circuit, session): - """Execution for the Sampler primitive""" + """Returns the result of the execution of the circuit using the SamplerV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.sample()` will return the raw samples. + Args: + circuits (list[QuantumCircuit]): the circuits to be executed via SamplerV2 + session (Session): the session that the execution will be performed with + + Returns: + result (tuple): the processed result from SamplerV2 + """ qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True)] sampler = Sampler(session=session) compiled_circuits = self.compile_circuits(qcirc) + sampler.options.update(**self._kwargs) # len(compiled_circuits) is always 1 so the indexing does not matter. - result = sampler.run(compiled_circuits).result()[0] + result = sampler.run( + compiled_circuits, + shots=circuit.shots.total_shots if circuit.shots.total_shots else None, + ).result()[0] classical_register_name = compiled_circuits[0].cregs[0].name self._current_job = getattr(result.data, classical_register_name) - results = [] - # needs processing function to convert to the correct format for states, and # also handle instances where wires were specified in probs, and for multiple probs measurements # single_measurement = len(circuit.measurements) == 1 @@ -468,13 +525,24 @@ def _execute_sampler(self, circuit, session): res = [ mp.process_samples(self._samples, wire_order=self.wires) for mp in circuit.measurements ] + single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) + res = (res[0],) if single_measurement else tuple(res) - return tuple(results) + return res def _execute_estimator(self, circuit, session): + """Returns the result of the execution of the circuit using the EstimatorV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.var` will return the variance. + + Args: + circuits (list[QuantumCircuit]): the circuits to be executed via EstimatorV2 + session (Session): the session that the execution will be performed with + + Returns: + result (tuple): the processed result from EstimatorV2 + """ # the Estimator primitive takes care of diagonalization and measurements itself, # so diagonalizing gates and measurements are not included in the circuit qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False)] @@ -482,12 +550,16 @@ def _execute_estimator(self, circuit, session): pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] compiled_circuits = self.compile_circuits(qcirc) + estimator.options.update(**self._kwargs) # split into one call per measurement # could technically be more efficient if there are some observables where we ask # for expectation value and variance on the same observable, but spending time on # that right now feels excessive circ_and_obs = [(compiled_circuits[0], pauli_observables)] - result = estimator.run(circ_and_obs).result() + result = estimator.run( + circ_and_obs, + precision=np.sqrt(1 / circuit.shots.total_shots) if circuit.shots else None, + ).result() self._current_job = result result = self._process_estimator_job(circuit.measurements, result) @@ -495,16 +567,22 @@ def _execute_estimator(self, circuit, session): @staticmethod def _process_estimator_job(measurements, job_result): - """Estimator returns both expectation value and variance for each observable measured, - along with some metadata. Extract the relevant number for each measurement process and - return the requested results from the Estimator executions.""" + """Estimator returns the expectation value and standard error for each observable measured, + along with some metadata that contains the precision. Extracts the relevant number for each + measurement process and return the requested results from the Estimator executions. + Note that for variance, we calculate the variance by using the standard error and the + precision value. + + Args: + measurements (list[MeasurementProcess]): the measurements in the circuit + job_result (Any): the result from EstimatorV2 + + Returns: + result (tuple): the processed result from EstimatorV2 + """ expvals = job_result[0].data.evs - variances = ( - job_result[0].data.stds ** 2 * 4096 - ) # this 4096 is the # of shots Qiskit uses by default. It is hard-coded here. - # ToDo: Track the # of shots and use that instead of hard-coding - # to calculate the variance. + variances = (job_result[0].data.stds / job_result[0].metadata["target_precision"]) ** 2 result = [] for i, mp in enumerate(measurements): diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 0e8b17992..8a36b310e 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -17,14 +17,13 @@ from unittest.mock import patch, Mock import numpy as np +from pydantic_core import ValidationError import pytest -from semantic_version import Version -import qiskit_ibm_runtime import pennylane as qml from pennylane.tape.qscript import QuantumScript -from qiskit_ibm_runtime import EstimatorV2 as Estimator +from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 from qiskit_aer import AerSimulator @@ -182,38 +181,6 @@ def test_no_shots_warns_and_defaults(self): assert dev.shots.total_shots == 1024 - @pytest.mark.skip(reason="Options handling not decided on yet") - def test_kwargs_on_initialization(self, mocker): - """Test that update_kwargs is called on intialization and combines the Options - and kwargs as self._kwargs""" - - options = {"my_tag": 1} - - spy = mocker.spy(QiskitDevice2, "_update_kwargs") - - dev = QiskitDevice2( - wires=2, - backend=backend, - options=options, - random_kwarg1=True, - random_kwarg2="a", - ) - - spy.assert_called_once() - - # kwargs are updated to a combination of the information from the Options and kwargs - assert dev._kwargs == { - "random_kwarg1": True, - "random_kwarg2": "a", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - "job_tags": ["my_tag"], - } - - # initial kwargs are saved without modification - assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} - @pytest.mark.parametrize("backend", [backend, legacy_backend]) def test_backend_wire_validation(self, backend): """Test that an error is raised if the number of device wires exceeds @@ -222,17 +189,13 @@ def test_backend_wire_validation(self, backend): with pytest.raises(ValueError, match="supports maximum"): QiskitDevice2(wires=500, backend=backend) - @pytest.mark.skip(reason="Options handling not decided on yet") def test_setting_simulator_noise_model(self): """Test that the simulator noise model saved on a passed Options object is used to set the backend noise model""" - options = {} - options.simulator.noise_model = {"placeholder": 1} - new_backend = MockedBackend() dev1 = QiskitDevice2(wires=3, backend=backend) - dev2 = QiskitDevice2(wires=3, backend=new_backend, options=options) + dev2 = QiskitDevice2(wires=3, backend=new_backend, noise_model={"placeholder": 1}) assert dev1.backend.options.noise_model is None assert dev2.backend.options.noise_model == {"placeholder": 1} @@ -422,7 +385,7 @@ def test_observable_stopping_condition(self, obs, expected): ) def test_preprocess_splits_incompatible_primitive_measurements(self, measurements, num_types): """Test that the default behaviour for preprocess it to split the tapes based - on meausrement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements + on measurement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements are another type (Sampler).""" dev = QiskitDevice2(wires=5, backend=backend) @@ -465,133 +428,110 @@ def test_intial_state_prep_also_decomposes(self): assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) -@pytest.mark.skip(reason="Options handling not decided on yet") -class TestOptionsHandling: +class TestKwargsHandling: def test_warning_if_shots(self): - """Test that a warning is raised if the user attempt to specify shots on - Options instead of as a kwarg, and sets shots to the shots passed (defaults - to 1024).""" + """Test that a warning is raised if the user attempts to specify shots by using + `default_shots`, and instead sets shots to the default amount of 1024.""" with pytest.warns( UserWarning, - match="Setting shots via the Options is not supported on PennyLane devices", + match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=backend) + dev = QiskitDevice2(wires=2, backend=backend, default_shots=333) - assert dev.shots.total_shots == 1024 - assert dev.options.execution.shots == 1024 + # Qiskit takes in `default_shots` to define the # of shots, therefore we use + # the kwarg "default_shots" rather than shots to pass it to Qiskit. + assert dev._kwargs["default_shots"] == 1024 + + dev = QiskitDevice2(wires=2, backend=backend, shots=200) + assert dev._kwargs["default_shots"] == 200 with pytest.warns( UserWarning, - match="Setting shots via the Options is not supported on PennyLane devices", + match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=backend, shots=200) + dev = QiskitDevice2(wires=2, backend=backend, options={"default_shots": 30}) - assert dev.shots.total_shots == 200 - assert dev.options.execution.shots == 200 + # resets to default since we reinitialize the device + assert dev._kwargs["default_shots"] == 1024 - def test_update_kwargs_no_overlapping_options_passed(self): - """Test that if there is no overlap between options defined as device kwargs and on Options, - _update_kwargs creates a combined dictionary""" + def test_warning_if_options_and_kwargs_overlap(self): + """Test that a warning is raised if the user has options that overlap with the kwargs""" - dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True, random_kwarg2="a") + with pytest.warns( + UserWarning, + match="An overlap between", + ): + dev = QiskitDevice2( + wires=2, + backend=backend, + options={"resilience_level": 1, "optimization_level": 1}, + resilience_level=2, + random_sauce="spaghetti", + ) - assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} - assert dev._kwargs == { - "random_kwarg1": True, - "random_kwarg2": "a", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - } + assert dev._kwargs["resilience_level"] == 1 + assert dev._transpile_args["optimization_level"] == 1 - dev.options.environment.job_tags = ["my_tag"] - dev.options.max_execution_time = "1m" - - dev._update_kwargs() - - # _init_kwargs are unchanged, _kwargs are updated - assert dev._init_kwargs == {"random_kwarg1": True, "random_kwarg2": "a"} - assert dev._kwargs == { - "random_kwarg1": True, - "random_kwarg2": "a", - "max_execution_time": "1m", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - "job_tags": ["my_tag"], - } + # You can initialize the device with any kwarg, but you'll get a ValidationError + # when you run the circuit + assert dev._kwargs["random_sauce"] == "spaghetti" - def test_update_kwargs_with_overlapping_options(self): - """Test that if there is overlap between options defined as device kwargs and on Options, - _update_kwargs creates a combined dictionary with Options taking precedence, and raises a - warning""" - - dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True, max_execution_time="1m") - - assert dev._init_kwargs == {"random_kwarg1": True, "max_execution_time": "1m"} - if Version(qiskit_ibm_runtime.__version__) < Version("0.21.0"): - assert dev._kwargs == { - "random_kwarg1": True, - "max_execution_time": "1m", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - "job_tags": [], - } - else: - assert dev._kwargs == { - "random_kwarg1": True, - "max_execution_time": "1m", - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - } - - dev.options.environment.job_tags = ["my_tag"] - dev.options.max_execution_time = "30m" + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) - with pytest.warns( - UserWarning, - match="also defined in the device Options. The definition in Options will be used.", - ): - dev._update_kwargs() - - # _init_kwargs are unchanged, _kwargs are updated - assert dev._init_kwargs == {"random_kwarg1": True, "max_execution_time": "1m"} - assert dev._kwargs == { - "random_kwarg1": True, - "max_execution_time": "30m", # definition from Options is used - "skip_transpilation": False, - "init_qubits": True, - "log_level": "WARNING", - "job_tags": ["my_tag"], - } + with pytest.raises(ValidationError, match="Object has no attribute"): + circuit() - def test_update_kwargs_with_shots_set_on_options(self): - """Test that if shots have been defined on Options, _update_kwargs raises a warning - and ignores the shots as defined on Options""" + def test_options_and_kwargs_combine_into_unified_kwargs(self): + """Test that options set via the keyword argument options and options set via kwargs + will combine into a single unified kwargs that is passed to the device""" - dev = QiskitDevice2(wires=2, backend=backend, random_kwarg1=True) + dev = QiskitDevice2( + wires=2, + backend=backend, + options={"resilience_level": 1}, + execution={"init_qubits": False}, + ) + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) - start_init_kwargs = dev._init_kwargs - start_kwargs = dev._kwargs + circuit() + assert dev._kwargs["resilience_level"] == 1 + assert dev._kwargs["execution"]["init_qubits"] is False - dev.options.execution.shots = 500 + circuit(shots=123) + assert dev._kwargs["resilience_level"] == 1 + assert dev._kwargs["execution"]["init_qubits"] is False - with pytest.warns( - UserWarning, - match="Setting shots via the Options is not supported on PennyLane devices", - ): - assert dev.options.execution.shots == 500 - dev._update_kwargs() + def test_no_error_is_raised_if_transpilation_options_are_passed(self): + """Tests that when transpilation options are passed in, they are properly + handled without error""" + + dev = QiskitDevice2( + wires=2, + backend=backend, + options={"resilience_level": 1, "optimization_level": 1}, + seed_transpiler=42, + ) + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) - # _init_kwargs and _kwargs are unchanged, shots was ignored - assert dev._init_kwargs == start_init_kwargs - assert dev._kwargs == start_kwargs + circuit() + assert dev._kwargs["resilience_level"] == 1 + assert not hasattr(dev._kwargs, "seed_transpiler") + assert dev._transpile_args["seed_transpiler"] == 42 - # the shots on the Options have been reset to the device shots - assert dev.options.execution.shots == dev.shots.total_shots + # Make sure that running the circuit again doesn't change the optios + circuit(shots=5) + assert dev._kwargs["resilience_level"] == 1 + assert not hasattr(dev._kwargs, "seed_transpiler") + assert dev._transpile_args["seed_transpiler"] == 42 class TestDeviceProperties: @@ -638,10 +578,6 @@ def test_get_transpile_args(self): """Test that get_transpile_args works as expected by filtering out kwargs that don't match the Qiskit transpile signature""" - # independently - kwargs = {"random_kwarg": 3, "optimization_level": 3, "circuits": []} - assert QiskitDevice2.get_transpile_args(kwargs) == {"optimization_level": 3} - # on a device transpile_args = { "random_kwarg": 3, @@ -653,7 +589,7 @@ def test_get_transpile_args(self): dev = QiskitDevice2( wires=5, backend=backend, compile_backend=compile_backend, **transpile_args ) - assert dev.get_transpile_args(dev._kwargs) == { + assert dev._transpile_args == { "optimization_level": 3, "seed_transpiler": 42, } @@ -683,7 +619,7 @@ def test_compile_circuits(self, transpile_mock, compile_backend): compiled_circuits = dev.compile_circuits(input_circuits) transpile_mock.assert_called_with( - input_circuits[2], backend=dev.compile_backend, **transpile_args + input_circuits[2], backend=dev.compile_backend, **dev._transpile_args ) assert len(compiled_circuits) == len(input_circuits) @@ -906,6 +842,25 @@ def test_estimator_with_various_multi_qubit_pauli_obs( assert np.allclose(res[0], expectation, atol=0.3) ## atol is high due to high variance + def test_tape_shots_used_for_estimator(self, mocker): + """Tests that device uses tape shots rather than device shots for estimator""" + dev = QiskitDevice2(wires=5, backend=backend, shots=2) + + estimator_execute = mocker.spy(dev, "_execute_estimator") + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + circuit(shots=[5]) + + estimator_execute.assert_called_once() + # calculates # of shots executed from precision + assert int(np.ceil(1 / dev._current_job[0].metadata["target_precision"] ** 2)) == 5 + + circuit() + assert int(np.ceil(1 / dev._current_job[0].metadata["target_precision"] ** 2)) == 2 + @pytest.mark.parametrize( "measurements, expectation", [ @@ -960,20 +915,12 @@ def test_process_estimator_job(self, measurements, expectation): @pytest.mark.parametrize("num_wires", [1, 3, 5]) @pytest.mark.parametrize("num_shots", [50, 100]) - @pytest.mark.skip( - reason="Need to replace this with using SamplerV2." - ) # Resolved in PR #547 https://github.com/PennyLaneAI/pennylane-qiskit/pull/547 def test_generate_samples(self, num_wires, num_shots): qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) + dev = QiskitDevice2(wires=num_wires, backend=backend, shots=num_shots) + dev._execute_sampler(circuit=qs, session=Session(backend=backend)) - qcirc = circuit_to_qiskit(qs, register_size=num_wires, diagonalize=True, measure=True) - compiled_circuits = test_dev.compile_circuits([qcirc]) - - job = test_dev._execute_sampler(circuits=compiled_circuits, shots=num_shots) - - test_dev._current_job = job.result() - - samples = test_dev.generate_samples() + samples = dev.generate_samples(0) assert len(samples) == num_shots assert len(samples[0]) == num_wires @@ -991,31 +938,6 @@ def test_generate_samples(self, num_wires, num_shots): # nothing else is in samples assert [s for s in samples if not s in np.array([exp_res0, exp_res1])] == [] - @pytest.mark.skip( - reason="Tracking shot information will be addressed in the PR about options handling" - ) - def test_tape_shots_used_for_estimator(self, mocker): - """Tests that device uses tape shots rather than device shots for estimator""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2) - - estimator_execute = mocker.spy(dev, "_execute_estimator") - - @qml.qnode(dev) - def circuit(): - return qml.expval(qml.PauliX(0)) - - circuit(shots=[5]) - - estimator_execute.assert_called_once() - assert dev._current_job.metadata[0]["shots"] == 5 - - # Should reset to device shots if circuit ran again without shots defined - circuit() - assert dev._current_job.metadata[0]["shots"] == 2 - - @pytest.mark.skip( - reason="Tracking shot information will be addressed in the PR about options handling" - ) def test_tape_shots_used_for_sampler(self, mocker): """Tests that device uses tape shots rather than device shots for sampler""" dev = QiskitDevice2(wires=5, backend=backend, shots=2) @@ -1030,29 +952,26 @@ def circuit(): circuit(shots=[5]) sampler_execute.assert_called_once() - assert dev._current_job.metadata[0]["shots"] == 5 + assert dev._current_job.num_shots == 5 # Should reset to device shots if circuit ran again without shots defined circuit() - assert dev._current_job.metadata[0]["shots"] == 2 + assert dev._current_job.num_shots == 2 - @pytest.mark.skip( - reason="Tracking shot information will be addressed in the PR about options handling" - ) def test_error_for_shot_vector(self): """Tests that a ValueError is raised if a shot vector is passed.""" dev = QiskitDevice2(wires=5, backend=backend, shots=2) @qml.qnode(dev) def circuit(): - return qml.expval(qml.PauliX(0)) + return qml.sample(qml.PauliX(0)) with pytest.raises(ValueError, match="Setting shot vector"): circuit(shots=[5, 10, 2]) # Should reset to device shots if circuit ran again without shots defined circuit() - assert dev._current_job.metadata[0]["shots"] == 2 + assert dev._current_job.num_shots == 2 @pytest.mark.parametrize( "observable", @@ -1158,3 +1077,40 @@ def qiskit_circuit(x): qiskit_res = qiskit_circuit(np.pi / 2) assert np.shape(res) == np.shape(qiskit_res) + + def test_sampler_output_shape_multi_measurements(self): + """Test that the shape of the results produced from the sampler for the Qiskit device + is consistent with Pennylane for circuits with multiple measurements""" + dev = qml.device("default.qubit", wires=[0, 1, 2, 3], shots=10) + qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend, shots=10) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return ( + qml.sample(), + qml.sample(qml.Y(0)), + qml.expval(qml.X(1)), + qml.var(qml.Y(0)), + qml.counts(), + ) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return ( + qml.sample(), + qml.sample(qml.Y(0)), + qml.expval(qml.X(1)), + qml.var(qml.Y(0)), + qml.counts(), + ) + + res = circuit(np.pi / 2) + qiskit_res = qiskit_circuit(np.pi / 2) + + assert np.shape(res[0]) == np.shape(qiskit_res[0]) + assert np.shape(res[1]) == np.shape(qiskit_res[1]) + assert len(res) == len(qiskit_res) From 6442fee749eb858530990a50031ccb23552e36db Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:38:20 -0400 Subject: [PATCH 35/47] Docstrings for converter functions and new qiskit device (#552) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * [skip ci] docstrings for converter functions * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * [skip ci] examples of QiskitDevice2 added to docstring * docstring changes * docstring * more docstrings * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * docstrings * docstrings * formatting * changes * some examples * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * examples and links * change inheritance for remote device * build docs * better docs * fix docs a little * remove redundant docstrings * revert * import fix * build sphinx * revert change to QiskitDev2 * Update pennylane_qiskit/remote.py Co-authored-by: Utkarsh * reformat to within 100 chars --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/converter.py | 6 ++ pennylane_qiskit/qiskit_device2.py | 5 +- pennylane_qiskit/remote.py | 121 ++++++++++++++++++++++++++--- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 9d4fa55a3..75c7e217b 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -631,6 +631,9 @@ def circuit_to_qiskit(circuit, register_size, diagonalize=True, measure=True): a full circuit is represented either as a Qiskit circuit with operations and measurements (measure=True), or a Qiskit circuit with only operations, paired with a Qiskit Estimator defining the measurement process. + + Returns: + QuantumCircuit: the qiskit equivalent of the given circuit """ reg = QuantumRegister(register_size) @@ -712,6 +715,9 @@ def mp_to_pauli(mp, register_size): Args: mp(Union[ExpectationMP, VarianceMP]): MeasurementProcess to be converted to a SparsePauliOp register_size(int): total size of the qubit register being measured + + Returns: + SparsePauliOp: the ``SparsePauliOp`` of the given Pauli observable """ op = mp.obs diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index e14e98e8e..5bb38289a 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -436,7 +436,7 @@ def get_transpile_args(kwargs): return kwargs, transpile_args def compile_circuits(self, circuits): - r"""Compiles multiple circuits one after the other. + """Compiles multiple circuits one after the other. Args: circuits (list[QuantumCircuit]): the circuits to be compiled @@ -518,8 +518,6 @@ def _execute_sampler(self, circuit, session): # needs processing function to convert to the correct format for states, and # also handle instances where wires were specified in probs, and for multiple probs measurements - # single_measurement = len(circuit.measurements) == 1 - # res = (res[0], ) if single_measurement else tuple(res) self._samples = self.generate_samples(0) res = [ @@ -570,6 +568,7 @@ def _process_estimator_job(measurements, job_result): """Estimator returns the expectation value and standard error for each observable measured, along with some metadata that contains the precision. Extracts the relevant number for each measurement process and return the requested results from the Estimator executions. + Note that for variance, we calculate the variance by using the standard error and the precision value. diff --git a/pennylane_qiskit/remote.py b/pennylane_qiskit/remote.py index 4be045527..251ca5887 100644 --- a/pennylane_qiskit/remote.py +++ b/pennylane_qiskit/remote.py @@ -24,17 +24,120 @@ class RemoteDevice(QiskitDevice): Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider | None): provider to lookup the backend on (ignored if a backend instance is passed). - backend (str | Backend): the desired backend. Either a name to look up on a provider, or a - BackendV1 or BackendV2 instance. - 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. + or iterable that contains unique labels for the subsystems as numbers + (i.e., ``[-1, 0, 2]``) or strings (``['aux_wire', 'q1', 'q2']``). + backend (Backend): the initialized Qiskit backend Keyword Args: - name (str): The name of the circuit. Default ``'circuit'``. + shots (Union[int, None]): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. + session (Session): a Qiskit Session to use for device execution. If none is provided, + a session will be created at each device execution. + compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit + that will be sent to the backend device, to be set if the backend desired for + compliation differs from the backend used for execution. Defaults to ``None``, + which means the primary backend will be used. + **kwargs: transpilation and runtime keyword arguments to be used for measurements with + Primitives. If an `options` dictionary is defined amongst the kwargs, and there are + settings that overlap with those in kwargs, the settings in `options` will take + precedence over kwargs. Keyword arguments accepted by both the transpiler and at + runtime (e.g. ``optimization_level``) will be passed to the transpiler rather + than to the Primitive. + + **Example:** + + .. code-block:: python + + import pennylane as qml + from qiskit_ibm_runtime import QiskitRuntimeService + + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(n_qubits=127, simulator=False, operational=True) + dev = qml.device("qiskit.remote", wires=127, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(1)) + + >>> circuit(np.pi/3, shots=1024) + 0.529296875 + + This device also supports the use of local simulators such as ``AerSimulator`` or + fake backends such as ``FakeManila``. + + .. code-block:: python + + import pennylane as qml + from qiskit_aer import AerSimulator + + backend = AerSimulator() + dev = qml.device("qiskit.remote", wires=5, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(1)) + + >>> circuit(np.pi/3, shots=1024) + 0.49755859375 + + We can also change the number of shots, either when initializing the device or when we execute + the circuit. Note that the shots number specified on circuit execution will override whatever + was set on device initialization. + + .. code-block:: python + + dev = qml.device("qiskit.remote", wires=5, backend=backend, shots=2) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample(qml.PauliZ(1)) + + >>> circuit(np.pi/3) # this will run with 2 shots + array([-1., 1.]) + + >>> circuit(np.pi/3, shots=5) # this will run with 5 shots + array([-1., -1., 1., 1., 1.]) + + >>> circuit(np.pi/3) # this will run with 2 shots + array([-1., 1.]) + + Internally, the device uses the `EstimatorV2 `_ + and the `SamplerV2 `_ + runtime primitives to execute the measurements. To set options for + `transpilation `_ or + `runtime `_, simply pass + the keyword arguments into the device. If you wish to change options other than ``shots``, + PennyLane requires you to re-initialize the device to do so. + + .. code-block:: python + + import pennylane as qml + from qiskit_ibm_runtime.fake_provider import FakeManilaV2 + + backend = FakeManilaV2() + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=1, + seed_transpiler=42, + ) + # to change options, re-initialize the device + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=2, + seed_transpiler=24, + ) """ short_name = "qiskit.remote" From 2bf41303d4a3518f02c9f5a7a147d227f0230cc6 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:33:30 -0400 Subject: [PATCH 36/47] Diagonalize gates (#558) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * [skip ci] import split_non_commuting * diagonalize tests for Hadamard * changed stopping condition to reflect reality of what's supported and added tests and changed tests to fit new stopping condition * [skip ci] added comment about qml.var not providing matching answers * some tests * [skip ci] linter * interesting changes * linter * split non commuting test cases * sprod * sampler tested as well * linter * black * docstrings and comments * docstrings and comments black * comment regarding magic number * todo * add np * more concise way of testing * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * fix black * diagonalize for edge case * linter * pylint * clean up * fix docstring --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/converter.py | 7 +- pennylane_qiskit/qiskit_device2.py | 12 +- tests/test_base_device.py | 227 ++++++++++++++++++++++++++++- tests/test_converter.py | 23 +++ 4 files changed, 261 insertions(+), 8 deletions(-) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 75c7e217b..064ab4c88 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -35,6 +35,7 @@ import pennylane as qml import pennylane.ops as pennylane_ops +from pennylane.tape.tape import rotations_and_diagonal_measurements from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} @@ -655,7 +656,11 @@ def circuit_to_qiskit(circuit, register_size, diagonalize=True, measure=True): # rotate the state for measurement in the computational basis # ToDo: check this in cases with multiple different bases if diagonalize: - rotations = circuit.diagonalizing_gates + rotations, measurements = rotations_and_diagonal_measurements(circuit) + for _, m in enumerate(measurements): + if m.obs is not None: + rotations.extend(m.obs.diagonalizing_gates()) + for rot in rotations: qc &= operation_to_qiskit(rot, reg, creg) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index 5bb38289a..da5bfe463 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -32,7 +32,7 @@ from pennylane import transform from pennylane.transforms.core import TransformProgram -from pennylane.transforms import broadcast_expand +from pennylane.transforms import broadcast_expand, split_non_commuting from pennylane.tape import QuantumTape, QuantumScript from pennylane.typing import Result, ResultBatch from pennylane.devices import Device @@ -129,7 +129,6 @@ def split_execution_types( will use the Qiskit Sampler. ExpectationValue and Variance will use the Estimator, except when the measured observable does not have a `pauli_rep`. In that case, the Sampler will be used, and the raw samples will be processed to give an expectation value.""" - estimator = [] sampler = [] @@ -219,6 +218,11 @@ class QiskitDevice2(Device): "Hadamard", "Hermitian", "Projector", + "Prod", + "Sum", + "LinearCombination", + "SProd", + # TODO Could support SparseHamiltonian } # pylint:disable = too-many-arguments @@ -365,7 +369,7 @@ def preprocess( ) transform_program.add_transform(broadcast_expand) - # missing: split non-commuting, sum_expand, etc. [SC-62047] + transform_program.add_transform(split_non_commuting) transform_program.add_transform(split_execution_types) @@ -579,10 +583,8 @@ def _process_estimator_job(measurements, job_result): Returns: result (tuple): the processed result from EstimatorV2 """ - expvals = job_result[0].data.evs variances = (job_result[0].data.stds / job_result[0].metadata["target_precision"]) ** 2 - result = [] for i, mp in enumerate(measurements): if isinstance(mp, ExpectationMP): diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 8a36b310e..505d49b17 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -365,7 +365,7 @@ def test_stopping_conditions(self, op, expected): [ (qml.PauliX(0), True), (qml.Hadamard(3), True), - (qml.prod(qml.PauliY(1), qml.PauliZ(0)), False), + (qml.prod(qml.PauliY(1), qml.PauliZ(0)), True), ], ) def test_observable_stopping_condition(self, obs, expected): @@ -373,6 +373,67 @@ def test_observable_stopping_condition(self, obs, expected): res = test_dev.observable_stopping_condition(obs) assert res == expected + @pytest.mark.parametrize( + "measurements, num_tapes", + [ + ( + [ + qml.expval(qml.X(0) + qml.Y(0) + qml.Z(0)), + ], + 3, + ), + ( + [qml.var(qml.X(0) + qml.Y(0) + qml.Z(0))], # Var should not split + 1, + ), + ( + [ + qml.expval(qml.X(0)), + qml.expval(qml.Y(1)), + qml.expval(qml.Z(0) @ qml.Z(1)), + qml.expval(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + ), + ( + [ + qml.expval( + qml.prod(qml.X(0), qml.Z(0), qml.Z(0)) + 0.35 * qml.X(0) - 0.21 * qml.Z(0) + ) + ], + 2, + ), + ( + [ + qml.counts(qml.X(0)), + qml.counts(qml.Y(1)), + qml.counts(qml.Z(0) @ qml.Z(1)), + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + ), + ( + [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + ), + ], + ) + def test_preprocess_split_non_commuting(self, measurements, num_tapes): + """Test that `split_non_commuting` works as expected in the preprocess function.""" + + dev = QiskitDevice2(wires=5, backend=backend) + qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) + + program, _ = dev.preprocess() + tapes, _ = program([qs]) + + assert len(tapes) == num_tapes + @pytest.mark.parametrize( "measurements,num_types", [ @@ -451,7 +512,6 @@ def test_warning_if_shots(self): match="default_shots was found in the keyword arguments", ): dev = QiskitDevice2(wires=2, backend=backend, options={"default_shots": 30}) - # resets to default since we reinitialize the device assert dev._kwargs["default_shots"] == 1024 @@ -1114,3 +1174,166 @@ def qiskit_circuit(x): assert np.shape(res[0]) == np.shape(qiskit_res[0]) assert np.shape(res[1]) == np.shape(qiskit_res[1]) assert len(res) == len(qiskit_res) + + @pytest.mark.parametrize( + "observable", + [ + lambda: [qml.expval(qml.Hadamard(0)), qml.expval(qml.Hadamard(0))], + lambda: [ + qml.var(qml.Hadamard(0)), + qml.var(qml.Hadamard(0)), + ], + lambda: [ + qml.expval(qml.X(0)), + qml.expval(qml.Y(1)), + qml.expval(0.5 * qml.Y(1)), + qml.expval(qml.Z(0) @ qml.Z(1)), + qml.expval(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.expval( + qml.ops.LinearCombination( + [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] + ) + ), + qml.expval( + qml.ops.LinearCombination( + [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + ) + ), + ], + lambda: [ + qml.expval( + qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) + ) + ], + lambda: [qml.expval(qml.X(0) @ qml.Z(1) + qml.Z(0))], + pytest.param( + [qml.var(qml.X(0) + qml.Z(0))], + marks=pytest.mark.xfail(reason="Qiskit itself is bugged when given Sum"), + ), + lambda: [ + qml.expval(qml.Hadamard(0)), + qml.expval(qml.Hadamard(1)), + qml.expval(qml.Hadamard(0) @ qml.Hadamard(1)), + qml.expval( + qml.Hadamard(0) @ qml.Hadamard(1) + 0.5 * qml.Hadamard(1) + qml.Hadamard(0) + ), + ], + ], + ) + def test_observables_that_need_split_non_commuting(self, observable): + """Tests that observables that have non-commuting measurements are + processed correctly when executed by the Estimator or, in the case of + qml.Hadamard, executed by the Sampler via expval() or var""" + qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=30000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert np.allclose(res, qiskit_res, atol=0.05) + + @pytest.mark.parametrize( + "observable", + [ + lambda: [qml.counts(qml.X(0) + qml.Y(0)), qml.counts(qml.X(0))], + lambda: [ + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.counts(0.5 * qml.Y(1)), + ], + ], + ) + def test_observables_that_need_split_non_commuting_counts(self, observable): + """Tests that observables that have non-commuting measurents are processed + correctly when executed by the Sampler via counts()""" + qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=30000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert len(qiskit_res) == len(res) + for res1, res2 in zip(qiskit_res, res): + assert all(res1[key] - res2.get(key, 0) < 300 for key in res1) + + @pytest.mark.parametrize( + "observable", + [ + lambda: [qml.sample(qml.X(0) + qml.Y(0)), qml.sample(qml.X(0))], + lambda: [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))], + lambda: [ + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample(0.5 * qml.Y(1)), + ], + lambda: [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(0.5 * qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample( + qml.ops.LinearCombination( + [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] + ) + ), + qml.sample( + qml.ops.LinearCombination( + [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + ) + ), + ], + lambda: [ + qml.sample( + qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) + ) + ], + ], + ) + def test_observables_that_need_split_non_commuting_samples(self, observable): + """Tests that observables that have non-commuting measurents are processed + correctly when executed by the Sampler via sample()""" + qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=30000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert np.allclose(np.mean(qiskit_res, axis=1), np.mean(res, axis=1), atol=0.05) diff --git a/tests/test_converter.py b/tests/test_converter.py index 2ebe1bd6b..ba61ab33d 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1723,6 +1723,29 @@ def test_circuit_to_qiskit_diagonalize_kwarg(self, diagonalize): assert len(instructions) == len(expected_gates) + def test_circuit_to_qiskit_measurements_with_overlapping_wires(self): + """Test that diagonalizing gates work for circuits with + measurements on overlapping wires""" + + measurements = [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] + tape = qml.tape.QuantumScript(measurements=measurements) + + qc = circuit_to_qiskit(tape, 2, diagonalize=True, measure=True) + + # get list of instruction names up to the barrier (played right before measurements) + instructions = [] + for instruction in qc.data: + if instruction.operation.name == "barrier": + break + instructions.append(instruction.operation.name) + + # manually diagonalized test case since Qiskit transpiles whatever we had before + # and that results is different from PL's diagonalization + expected_gates = ["ry", "rx"] + + assert len(instructions) == len(expected_gates) + assert instructions == expected_gates + class TestConverterGatePennyLaneToQiskit: def test_non_parameteric_operation_to_qiskit(self): From bd7a3cb881e0a9bd06205f60cbfcdb7e76d488a1 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:22:10 -0400 Subject: [PATCH 37/47] Qiskit session (#551) * functionality implemented * minor adjustments to tests * [skip-ci] Qiskit Sessions now test many warnings since you can set session options on device initialization and when using the session manager. We use the options in the device for things that are generally not updateable for the device e.g. backends; we use session options for everything else * [skip-ci] tests that we are passing on kwargs to Qiskit's session constructor, and verifying that an error is raised due to such behavior * [skip ci] pylint * small comments * Generalization of the session options * delicious docstrings * [skip ci] tests and clarification * [skip ci] better session options * comments for clarity * changes to the tests & the warning message * docstrings * type error changes * docstrings * add qiskit_session to docs * a little more consistency in comments * for docs * fix ci * black * revert * revert * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * Qiskit Session update * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstring update --------- Co-authored-by: Utkarsh Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- pennylane_qiskit/qiskit_device2.py | 61 +++++++++++++++++-- tests/test_base_device.py | 97 ++++++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 10 deletions(-) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py index da5bfe463..ed4cf6a1c 100644 --- a/pennylane_qiskit/qiskit_device2.py +++ b/pennylane_qiskit/qiskit_device2.py @@ -55,14 +55,21 @@ # pylint: disable=protected-access @contextmanager -def qiskit_session(device): +def qiskit_session(device, **kwargs): """A context manager that creates a Qiskit Session and sets it as a session on the device while the context manager is active. Using the context manager will ensure the Session closes properly and is removed from the device after - completing the tasks. + completing the tasks. Any Session that was initialized and passed into the + device will be overwritten by the Qiskit Session created by this context + manager. Args: device (QiskitDevice2): the device that will create remote tasks using the session + **kwargs: session keyword arguments to be used for settings for the Session. At the + time of writing, the only relevant keyword argument is "max_time", which lets you + set the maximum amount of time the sessin is open. For the most up to date information, + please refer to the Qiskit Session + `documentation `_. **Example:** @@ -87,18 +94,60 @@ def circuit(x): angle = 0.1 - with qiskit_session(dev) as session: - - res = circuit(angle)[0] # you queue for the first execution + with qiskit_session(dev, max_time=60) as session: + # queue for the first execution + res = circuit(angle)[0] # then this loop executes immediately after without queueing again while res > 0: angle += 0.3 res = circuit(angle)[0] + + Note that if you passed in a session to your device, that session will be overwritten + by `qiskit_session`. + + .. code-block:: python + + import pennylane as qml + from pennylane_qiskit import qiskit_session + from qiskit_ibm_runtime import QiskitRuntimeService, Session + + # get backend + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(simulator=False, operational=True) + + # initialize device + dev = qml.device('qiskit.remote', wires=2, backend=backend, session=Session(backend=backend, max_time=30)) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, 0) + qml.CNOT([0, 1]) + return qml.expval(qml.PauliZ(1)) + + angle = 0.1 + + # This session will have the Qiskit default settings max_time=900 + with qiskit_session(dev) as session: + res = circuit(angle)[0] + + while res > 0: + angle += 0.3 + res = circuit(angle)[0] """ # Code to acquire session: existing_session = device._session - session = Session(backend=device.backend) + + session_options = {"backend": device.backend, "service": device.service} + + for k, v in kwargs.items(): + # Options like service and backend should be tied to the settings set on device + if k in session_options: + warnings.warn(f"Using '{k}' set in device, {getattr(device, k)}", UserWarning) + else: + session_options[k] = v + + session = Session(**session_options) device._session = session try: yield session diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 505d49b17..793d6c2bb 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -22,8 +22,7 @@ import pennylane as qml from pennylane.tape.qscript import QuantumScript - -from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator +from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 from qiskit_aer import AerSimulator @@ -104,8 +103,10 @@ def options(self): # pylint: disable=too-few-public-methods class MockSession: def __init__(self, backend, max_time=None): - self.backend = backend - self.max_time = max_time + self._backend = backend + self._max_time = max_time + self._args = "random" # this is to satisfy a mock + self._kwargs = "random" # this is to satisfy a mock self.session_id = "123" def close(self): # This is just to appease a test @@ -232,6 +233,94 @@ def test_using_session_context(self, mock_session, initial_session): assert dev._session == initial_session + def test_using_session_context_options(self): + """Test that you can set session options using qiskit_session""" + dev = QiskitDevice2(wires=2, backend=backend) + + assert dev._session is None + + with qiskit_session(dev, max_time=30) as session: + assert dev._session == session + assert dev._session is not None + assert dev._session._max_time == 30 + + assert dev._session is None + + def test_error_when_passing_unexpected_kwarg(self): + """Test that we accept any keyword argument that the user wants to supply so that if + Qiskit allows for more customization we can automatically accomodate those needs. Right + now there are no such keyword arguments, so an error on Qiskit's side is raised.""" + + dev = QiskitDevice2(wires=2, backend=backend) + + assert dev._session is None + + with pytest.raises( + TypeError, # Type error for wrong keyword argument differs across python versions + ): + with qiskit_session(dev, any_kwarg=30) as session: + assert dev._session == session + assert dev._session is not None + + assert dev._session is None + + def test_no_warning_when_using_initial_session_options(self): + initial_session = Session(backend=backend, max_time=30) + dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + + assert dev._session == initial_session + + with qiskit_session(dev) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._max_time == session._max_time + assert dev._session._max_time != initial_session._max_time + + assert dev._session == initial_session + assert dev._session._max_time == initial_session._max_time + + def test_warnings_when_overriding_session_context_options(self, recorder): + """Test that warnings are raised when the session options try to override either the + device's `backend` or `service`. Also ensures that the session options, even the + default options, passed in from the `qiskit_session` take precedence, barring + `backend` or `service`""" + initial_session = Session(backend=backend) + dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + + assert dev._session == initial_session + + with pytest.warns( + UserWarning, + match="Using 'backend' set in device", + ): + with qiskit_session(dev, max_time=30, backend=FakeManilaV2()) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._backend.name == "aer_simulator" + + with pytest.warns( + UserWarning, + match="Using 'service' set in device", + ): + with qiskit_session(dev, max_time=30, service="placeholder") as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._service != "placeholder" + + # device session should be unchanged by qiskit_session + assert dev._session == initial_session + + max_time_session = Session(backend=backend, max_time=60) + dev = QiskitDevice2(wires=2, backend=backend, session=max_time_session) + with qiskit_session(dev, max_time=30) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._max_time == 30 + assert dev._session._max_time != 60 + + assert dev._session == max_time_session + assert dev._session._max_time == 60 + @pytest.mark.parametrize("initial_session", [None, MockSession(backend)]) def test_update_session(self, initial_session): """Test that you can update the session stored on the device""" From b239c669ecad3d1ae0023253e48184e314a8d9ac Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:38:53 -0400 Subject: [PATCH 38/47] Tests with fakehardware (#553) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * [skip ci] some tests with fakehardware * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * [skip ci] import split_non_commuting * diagonalize tests for Hadamard * changed stopping condition to reflect reality of what's supported and added tests and changed tests to fit new stopping condition * [skip ci] added comment about qml.var not providing matching answers * some tests * [skip ci] linter * interesting changes * linter * split non commuting test cases * sprod * sampler tested as well * linter * black * docstrings and comments * docstrings and comments black * comment regarding magic number * todo * add np * more concise way of testing * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * fix black * diagonalize for edge case * linter * pylint * clean up * fix docstring * flaky * flaky and fake * rename bakcend to aer_backend --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- tests/test_base_device.py | 158 +++++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 70 deletions(-) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 793d6c2bb..2fd3c2fe1 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -16,6 +16,7 @@ """ from unittest.mock import patch, Mock +from flaky import flaky import numpy as np from pydantic_core import ValidationError import pytest @@ -115,8 +116,8 @@ def close(self): # This is just to appease a test mocked_backend = MockedBackend() legacy_backend = MockedBackendLegacy() -backend = AerSimulator() -test_dev = QiskitDevice2(wires=5, backend=backend) +aer_backend = AerSimulator() +test_dev = QiskitDevice2(wires=5, backend=aer_backend) class TestSupportForV1andV2: @@ -124,7 +125,7 @@ class TestSupportForV1andV2: @pytest.mark.parametrize( "backend", - [legacy_backend, backend, mocked_backend], + [legacy_backend, aer_backend, mocked_backend], ) def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" @@ -178,11 +179,11 @@ def test_no_shots_warns_and_defaults(self): UserWarning, match="Expected an integer number of shots, but received shots=None", ): - dev = QiskitDevice2(wires=2, backend=backend, shots=None) + dev = QiskitDevice2(wires=2, backend=aer_backend, shots=None) assert dev.shots.total_shots == 1024 - @pytest.mark.parametrize("backend", [backend, legacy_backend]) + @pytest.mark.parametrize("backend", [aer_backend, legacy_backend]) def test_backend_wire_validation(self, backend): """Test that an error is raised if the number of device wires exceeds the number of wires available on the backend, for both backend versions""" @@ -195,7 +196,7 @@ def test_setting_simulator_noise_model(self): object is used to set the backend noise model""" new_backend = MockedBackend() - dev1 = QiskitDevice2(wires=3, backend=backend) + dev1 = QiskitDevice2(wires=3, backend=aer_backend) dev2 = QiskitDevice2(wires=3, backend=new_backend, noise_model={"placeholder": 1}) assert dev1.backend.options.noise_model is None @@ -205,13 +206,15 @@ def test_setting_simulator_noise_model(self): class TestQiskitSessionManagement: """Test using Qiskit sessions with the device""" - def test_default_no_session_on_initialization(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_default_no_session_on_initialization(self, backend): """Test that the default behaviour is no session at initialization""" dev = QiskitDevice2(wires=2, backend=backend) assert dev._session is None - def test_initializing_with_session(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_initializing_with_session(self, backend): """Test that you can initialize a device with an existing Qiskit session""" session = MockSession(backend=backend, max_time="1m") @@ -219,11 +222,11 @@ def test_initializing_with_session(self): assert dev._session == session @patch("pennylane_qiskit.qiskit_device2.Session") - @pytest.mark.parametrize("initial_session", [None, MockSession(backend)]) + @pytest.mark.parametrize("initial_session", [None, MockSession(aer_backend)]) def test_using_session_context(self, mock_session, initial_session): """Test that you can add a session within a context manager""" - dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -235,7 +238,7 @@ def test_using_session_context(self, mock_session, initial_session): def test_using_session_context_options(self): """Test that you can set session options using qiskit_session""" - dev = QiskitDevice2(wires=2, backend=backend) + dev = QiskitDevice2(wires=2, backend=aer_backend) assert dev._session is None @@ -251,7 +254,7 @@ def test_error_when_passing_unexpected_kwarg(self): Qiskit allows for more customization we can automatically accomodate those needs. Right now there are no such keyword arguments, so an error on Qiskit's side is raised.""" - dev = QiskitDevice2(wires=2, backend=backend) + dev = QiskitDevice2(wires=2, backend=aer_backend) assert dev._session is None @@ -265,8 +268,8 @@ def test_error_when_passing_unexpected_kwarg(self): assert dev._session is None def test_no_warning_when_using_initial_session_options(self): - initial_session = Session(backend=backend, max_time=30) - dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + initial_session = Session(backend=aer_backend, max_time=30) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -284,8 +287,8 @@ def test_warnings_when_overriding_session_context_options(self, recorder): device's `backend` or `service`. Also ensures that the session options, even the default options, passed in from the `qiskit_session` take precedence, barring `backend` or `service`""" - initial_session = Session(backend=backend) - dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + initial_session = Session(backend=aer_backend) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -310,8 +313,8 @@ def test_warnings_when_overriding_session_context_options(self, recorder): # device session should be unchanged by qiskit_session assert dev._session == initial_session - max_time_session = Session(backend=backend, max_time=60) - dev = QiskitDevice2(wires=2, backend=backend, session=max_time_session) + max_time_session = Session(backend=aer_backend, max_time=60) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=max_time_session) with qiskit_session(dev, max_time=30) as session: assert dev._session == session assert dev._session != initial_session @@ -321,14 +324,14 @@ def test_warnings_when_overriding_session_context_options(self, recorder): assert dev._session == max_time_session assert dev._session._max_time == 60 - @pytest.mark.parametrize("initial_session", [None, MockSession(backend)]) + @pytest.mark.parametrize("initial_session", [None, MockSession(aer_backend)]) def test_update_session(self, initial_session): """Test that you can update the session stored on the device""" - dev = QiskitDevice2(wires=2, backend=backend, session=initial_session) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session - new_session = MockSession(backend=backend, max_time="1m") + new_session = MockSession(backend=aer_backend, max_time="1m") dev.update_session(new_session) assert dev._session != initial_session @@ -455,6 +458,7 @@ def test_stopping_conditions(self, op, expected): (qml.PauliX(0), True), (qml.Hadamard(3), True), (qml.prod(qml.PauliY(1), qml.PauliZ(0)), True), + (qml.prod(qml.PauliY(1), qml.PauliZ(0)), True), ], ) def test_observable_stopping_condition(self, obs, expected): @@ -515,7 +519,7 @@ def test_observable_stopping_condition(self, obs, expected): def test_preprocess_split_non_commuting(self, measurements, num_tapes): """Test that `split_non_commuting` works as expected in the preprocess function.""" - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice2(wires=5, backend=aer_backend) qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) program, _ = dev.preprocess() @@ -538,7 +542,7 @@ def test_preprocess_splits_incompatible_primitive_measurements(self, measurement on measurement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements are another type (Sampler).""" - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice2(wires=5, backend=aer_backend) qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) program, _ = dev.preprocess() @@ -587,20 +591,20 @@ def test_warning_if_shots(self): UserWarning, match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=backend, default_shots=333) + dev = QiskitDevice2(wires=2, backend=aer_backend, default_shots=333) # Qiskit takes in `default_shots` to define the # of shots, therefore we use # the kwarg "default_shots" rather than shots to pass it to Qiskit. assert dev._kwargs["default_shots"] == 1024 - dev = QiskitDevice2(wires=2, backend=backend, shots=200) + dev = QiskitDevice2(wires=2, backend=aer_backend, shots=200) assert dev._kwargs["default_shots"] == 200 with pytest.warns( UserWarning, match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=backend, options={"default_shots": 30}) + dev = QiskitDevice2(wires=2, backend=aer_backend, options={"default_shots": 30}) # resets to default since we reinitialize the device assert dev._kwargs["default_shots"] == 1024 @@ -613,7 +617,7 @@ def test_warning_if_options_and_kwargs_overlap(self): ): dev = QiskitDevice2( wires=2, - backend=backend, + backend=aer_backend, options={"resilience_level": 1, "optimization_level": 1}, resilience_level=2, random_sauce="spaghetti", @@ -633,12 +637,13 @@ def circuit(): with pytest.raises(ValidationError, match="Object has no attribute"): circuit() - def test_options_and_kwargs_combine_into_unified_kwargs(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_options_and_kwargs_combine_into_unified_kwargs(self, backend): """Test that options set via the keyword argument options and options set via kwargs will combine into a single unified kwargs that is passed to the device""" dev = QiskitDevice2( - wires=2, + wires=5, backend=backend, options={"resilience_level": 1}, execution={"init_qubits": False}, @@ -656,12 +661,13 @@ def circuit(): assert dev._kwargs["resilience_level"] == 1 assert dev._kwargs["execution"]["init_qubits"] is False - def test_no_error_is_raised_if_transpilation_options_are_passed(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_no_error_is_raised_if_transpilation_options_are_passed(self, backend): """Tests that when transpilation options are passed in, they are properly handled without error""" dev = QiskitDevice2( - wires=2, + wires=5, backend=backend, options={"resilience_level": 1, "optimization_level": 1}, seed_transpiler=42, @@ -691,9 +697,10 @@ def test_name_property(self): def test_backend_property(self): """Test the backend property""" assert test_dev.backend == test_dev._backend - assert test_dev.backend == backend + assert test_dev.backend == aer_backend - def test_compile_backend_property(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_compile_backend_property(self, backend): """Test the compile_backend property""" compile_backend = MockedBackend(name="compile_backend") @@ -709,8 +716,8 @@ def test_service_property(self): def test_session_property(self): """Test the session property""" - session = MockSession(backend=backend) - dev = QiskitDevice2(wires=2, backend=backend, session=session) + session = MockSession(backend=aer_backend) + dev = QiskitDevice2(wires=2, backend=aer_backend, session=session) assert dev.session == dev._session assert dev.session == session @@ -718,7 +725,7 @@ def test_num_wires_property(self): """Test the num_wires property""" wires = [1, 2, 3] - dev = QiskitDevice2(wires=wires, backend=backend) + dev = QiskitDevice2(wires=wires, backend=aer_backend) assert dev.num_wires == len(wires) @@ -736,7 +743,7 @@ def test_get_transpile_args(self): } compile_backend = MockedBackend(name="compile_backend") dev = QiskitDevice2( - wires=5, backend=backend, compile_backend=compile_backend, **transpile_args + wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args ) assert dev._transpile_args == { "optimization_level": 3, @@ -751,7 +758,7 @@ def test_compile_circuits(self, transpile_mock, compile_backend): transpile_args = {"seed_transpiler": 42, "optimization_level": 2} dev = QiskitDevice2( - wires=5, backend=backend, compile_backend=compile_backend, **transpile_args + wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args ) transpile_mock.return_value = QuantumCircuit(2) @@ -813,7 +820,7 @@ def test_execute_pipeline_primitives_no_session(self, mocker): """Test that a Primitives-based device initialized with no Session creates one for the execution, and then returns the device session to None.""" - dev = QiskitDevice2(wires=5, backend=backend, session=None) + dev = QiskitDevice2(wires=5, backend=aer_backend, session=None) assert dev._session is None @@ -825,7 +832,7 @@ def test_execute_pipeline_primitives_no_session(self, mocker): assert dev._session is None # the device session is still None - @pytest.mark.parametrize("backend", [backend, legacy_backend]) + @pytest.mark.parametrize("backend", [aer_backend, legacy_backend, FakeManila(), FakeManilaV2()]) def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): """Test that a device executes measurements that require raw samples via the sampler, and the relevant primitive measurements via the estimator""" @@ -860,7 +867,7 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): @patch("pennylane_qiskit.qiskit_device2.Estimator") @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._process_estimator_job") - @pytest.mark.parametrize("session", [None, MockSession(backend)]) + @pytest.mark.parametrize("session", [None, MockSession(aer_backend)]) def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, session): """Test the _execute_estimator function using a mocked version of Estimator that returns a meaningless result.""" @@ -878,7 +885,7 @@ def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, ses def test_shot_vector_error_mocked(self): """Test that a device that executes a circuit with an array of shots raises the appropriate ValueError""" - dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) + dev = QiskitDevice2(wires=5, backend=aer_backend, session=MockSession(aer_backend)) qs = QuantumScript( measurements=[ qml.expval(qml.PauliX(0)), @@ -904,6 +911,7 @@ class TestExecution: (np.pi / 2, qml.RZ, [0, 0, 1, 1, 1, 0]), ], ) + @flaky(max_runs=10, min_passes=7) def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expectation): """Test that the Estimator with various observables returns expected results. Essentially testing that the conversion to PauliOps in _execute_estimator behaves as @@ -911,7 +919,7 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) """ - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice2(wires=5, backend=aer_backend) sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -959,6 +967,7 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec ), ], ) + @flaky(max_runs=10, min_passes=7) def test_estimator_with_various_multi_qubit_pauli_obs( self, mocker, wire, angle, op, multi_q_obs ): @@ -969,7 +978,7 @@ def test_estimator_with_various_multi_qubit_pauli_obs( """ pl_dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) - dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) + dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=aer_backend) sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -993,7 +1002,7 @@ def test_estimator_with_various_multi_qubit_pauli_obs( def test_tape_shots_used_for_estimator(self, mocker): """Tests that device uses tape shots rather than device shots for estimator""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2) + dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1037,6 +1046,7 @@ def circuit(): ), ], ) + @flaky(max_runs=10, min_passes=7) def test_process_estimator_job(self, measurements, expectation): """Tests that the estimator returns expected and accurate results for an ``expval`` and ``var`` for a variety of multi-qubit observables""" @@ -1048,8 +1058,8 @@ def test_process_estimator_job(self, measurements, expectation): pauli_observables = [mp_to_pauli(mp, qs.num_wires) for mp in qs.measurements] # run on simulator via Estimator - estimator = Estimator(backend=backend) - compiled_circuits = [transpile(qcirc, backend=backend)] + estimator = Estimator(backend=aer_backend) + compiled_circuits = [transpile(qcirc, backend=aer_backend)] circ_and_obs = [(compiled_circuits[0], pauli_observables)] result = estimator.run(circ_and_obs).result() @@ -1066,8 +1076,8 @@ def test_process_estimator_job(self, measurements, expectation): @pytest.mark.parametrize("num_shots", [50, 100]) def test_generate_samples(self, num_wires, num_shots): qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) - dev = QiskitDevice2(wires=num_wires, backend=backend, shots=num_shots) - dev._execute_sampler(circuit=qs, session=Session(backend=backend)) + dev = QiskitDevice2(wires=num_wires, backend=aer_backend, shots=num_shots) + dev._execute_sampler(circuit=qs, session=Session(backend=aer_backend)) samples = dev.generate_samples(0) @@ -1089,7 +1099,7 @@ def test_generate_samples(self, num_wires, num_shots): def test_tape_shots_used_for_sampler(self, mocker): """Tests that device uses tape shots rather than device shots for sampler""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2) + dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) sampler_execute = mocker.spy(dev, "_execute_sampler") @@ -1109,7 +1119,7 @@ def circuit(): def test_error_for_shot_vector(self): """Tests that a ValueError is raised if a shot vector is passed.""" - dev = QiskitDevice2(wires=5, backend=backend, shots=2) + dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) @qml.qnode(dev) def circuit(): @@ -1131,12 +1141,13 @@ def circuit(): ], ) @pytest.mark.filterwarnings("ignore::UserWarning") + @flaky(max_runs=10, min_passes=7) def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): """Test that the device uses _sampler and _execute_estimator appropriately and provides an accurate answer for measurements with observables that don't have a pauli_rep. """ - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice2(wires=5, backend=aer_backend) pl_dev = qml.device("default.qubit", wires=5) @@ -1167,7 +1178,7 @@ def test_warning_for_split_execution_types_when_observable_no_pauli(self): """Test that a warning is raised when device is passed a measurement on an observable that does not have a pauli_rep.""" - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice2(wires=5, backend=aer_backend) @qml.qnode(dev) def circuit(): @@ -1181,12 +1192,13 @@ def circuit(): ): circuit() - def test_qiskit_probability_output_format(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_qiskit_probability_output_format(self, backend): """Test that the format and values of the Qiskit device's output for `qml.probs` is the same as pennylane's.""" - dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) - qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) + dev = qml.device("default.qubit", wires=[0, 1, 2, 3, 4]) + qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3, 4], backend=backend) @qml.qnode(dev) def circuit(): @@ -1202,36 +1214,37 @@ def qiskit_circuit(): qiskit_res = qiskit_circuit() assert np.shape(res) == np.shape(qiskit_res) - assert np.allclose(res, qiskit_res, atol=0.03) - def test_sampler_output_shape(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_sampler_output_shape(self, backend): """Test that the shape of the results produced from the sampler for the Qiskit device is consistent with Pennylane""" - dev = qml.device("default.qubit", wires=[0, 1, 2, 3], shots=1024) - qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend) + dev = qml.device("default.qubit", wires=5, shots=1024) + qiskit_dev = QiskitDevice2(wires=5, backend=backend) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=[0]) qml.CNOT(wires=[0, 1]) - return qml.sample() + return [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] @qml.qnode(qiskit_dev) def qiskit_circuit(x): qml.RX(x, wires=[0]) qml.CNOT(wires=[0, 1]) - return qml.sample() + return [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] res = circuit(np.pi / 2) qiskit_res = qiskit_circuit(np.pi / 2) assert np.shape(res) == np.shape(qiskit_res) - def test_sampler_output_shape_multi_measurements(self): + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_sampler_output_shape_multi_measurements(self, backend): """Test that the shape of the results produced from the sampler for the Qiskit device is consistent with Pennylane for circuits with multiple measurements""" - dev = qml.device("default.qubit", wires=[0, 1, 2, 3], shots=10) - qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=backend, shots=10) + dev = qml.device("default.qubit", wires=5, shots=10) + qiskit_dev = QiskitDevice2(wires=5, backend=backend, shots=10) @qml.qnode(dev) def circuit(x): @@ -1309,11 +1322,12 @@ def qiskit_circuit(x): ], ], ) + @flaky(max_runs=10, min_passes=7) def test_observables_that_need_split_non_commuting(self, observable): """Tests that observables that have non-commuting measurements are processed correctly when executed by the Estimator or, in the case of qml.Hadamard, executed by the Sampler via expval() or var""" - qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=30000) @qml.qnode(qiskit_dev) def qiskit_circuit(): @@ -1344,10 +1358,11 @@ def circuit(): ], ], ) + @flaky(max_runs=10, min_passes=7) def test_observables_that_need_split_non_commuting_counts(self, observable): """Tests that observables that have non-commuting measurents are processed correctly when executed by the Sampler via counts()""" - qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=4000) @qml.qnode(qiskit_dev) def qiskit_circuit(): @@ -1355,7 +1370,7 @@ def qiskit_circuit(): qml.RZ(np.pi / 3, 0) return observable() - dev = qml.device("default.qubit", wires=3, shots=30000) + dev = qml.device("default.qubit", wires=3, shots=4000) @qml.qnode(dev) def circuit(): @@ -1390,6 +1405,8 @@ def circuit(): [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] ) ), + ], + lambda: [ qml.sample( qml.ops.LinearCombination( [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" @@ -1403,10 +1420,11 @@ def circuit(): ], ], ) + @flaky(max_runs=10, min_passes=7) def test_observables_that_need_split_non_commuting_samples(self, observable): """Tests that observables that have non-commuting measurents are processed correctly when executed by the Sampler via sample()""" - qiskit_dev = QiskitDevice2(wires=3, backend=backend, shots=30000) + qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=20000) @qml.qnode(qiskit_dev) def qiskit_circuit(): @@ -1414,7 +1432,7 @@ def qiskit_circuit(): qml.RZ(np.pi / 3, 0) return observable() - dev = qml.device("default.qubit", wires=3, shots=30000) + dev = qml.device("default.qubit", wires=3, shots=20000) @qml.qnode(dev) def circuit(): From ec6968b818fc1ee1a9a1bd6135a2858557fa4da5 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 9 Jul 2024 14:06:56 -0400 Subject: [PATCH 39/47] fix merge conf --- setup.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/setup.py b/setup.py index c3d73fd90..977301939 100644 --- a/setup.py +++ b/setup.py @@ -25,12 +25,7 @@ "qiskit>=0.32", "qiskit-aer", "qiskit-ibm-runtime", -<<<<<<< HEAD - "qiskit-ibm-provider", - "pennylane>=0.30", -======= "pennylane>=0.37", ->>>>>>> master "numpy", "sympy<1.13", "networkx>=2.2", From 30f675cc977d86cd2af7224574fe48d86f059e00 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 9 Jul 2024 14:08:48 -0400 Subject: [PATCH 40/47] fix setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 977301939..9eea781c8 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ "qiskit>=0.32", "qiskit-aer", "qiskit-ibm-runtime", + "qiskit-ibm-provider", "pennylane>=0.37", "numpy", "sympy<1.13", From 3d1ba4ed5cdbd6a8c96eeabd1ffc64f3bfd0084e Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 9 Jul 2024 16:08:40 -0400 Subject: [PATCH 41/47] reqs.txt --- requirements.txt | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0b6ea2540..ad1fe23f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,51 +1,4 @@ -<<<<<<< HEAD pennylane>=0.32 qiskit numpy sympy -======= -appdirs==1.4.4 -autograd==1.6.2 -autoray==0.6.11 -cachetools==5.3.3 -certifi==2024.7.4 -cffi==1.16.0 -charset-normalizer==3.3.2 -cryptography==42.0.5 -Cython==3.0.8 -dill==0.3.8 -future==1.0.0 -idna==3.6 -mpmath==1.3.0 -networkx==3.2.1 -ninja==1.11.1.1 -ntlm-auth==1.5.0 -numpy==1.26.4 -orjson==3.9.15 -pbr==6.0.0 -pennylane==0.37 -PennyLane-Lightning==0.37 -ply==3.11 -psutil==5.9.8 -pycparser==2.21 -python-constraint==1.4.0 -python-dateutil==2.8.2 -qiskit==0.45.3 -qiskit-aer==0.13.3 -qiskit-ibm-runtime==0.20.0 -qiskit-ibm-provider==0.10.0 -qiskit-ignis==0.7.1 -qiskit-terra==0.45.3 -requests==2.31.0 -requests-ntlm==1.2.0 -retworkx==0.14.1 -scipy==1.12.0 -semantic-version==2.10.0 -six==1.16.0 -stevedore==5.2.0 -symengine==0.11.0 -sympy==1.12 -toml==0.10.2 -urllib3==2.2.1 -websocket-client==1.7.0 ->>>>>>> master From 19586e388e13913344657a215fc18eb0ce930fd2 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 9 Jul 2024 16:17:09 -0400 Subject: [PATCH 42/47] changelog changes --- CHANGELOG.md | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea29fc9a4..7b10d7a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ ### Breaking changes 💔 +* Support has been removed for Qiskit versions below 0.46. The minimum required version for Qiskit is now 1.0. + If you want to continue to use older versions of Qiskit with the plugin, please use version 0.36 of + the Pennylane-Qiskit plugin. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) + +* The test suite no longer runs for Qiskit versions below 0.46. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) + +* The ``qiskit.basicaer`` device has been removed because it is not supported for versions of Qiskit above 0.46. + [(#546)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/546) + +* The IBM quantum devices, ``qiskit.ibmq``, ``qiskit.ibmq.circuit_runner`` and ``qiskit.ibmq.sampler``, have been removed due to deprecations of the IBMProvider and the cloud simulator "ibmq_qasm_simulator". + [(#550)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/550) + ### Deprecations 👋 ### Documentation 📝 @@ -27,30 +41,6 @@ This release contains contributions from (in alphabetical order): * Improvements have been made to load circuits with `SwitchCaseOp` gates with default case. [(#514)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/514) -<<<<<<< HEAD -### Breaking changes 💔 -* Support has been removed for Qiskit versions below 0.46. The minimum required version for Qiskit is now 1.0. - If you want to continue to use older versions of Qiskit with the plugin, please use version 0.36 of - the Pennylane-Qiskit plugin. - [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) - -* The test suite no longer runs for Qiskit versions below 0.46. - [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) - -* The ``qiskit.basicaer`` device has been removed because it is not supported for versions of Qiskit above 0.46. - [(#546)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/546) - -* The IBM quantum devices, ``qiskit.ibmq``, ``qiskit.ibmq.circuit_runner`` and ``qiskit.ibmq.sampler``, have been removed due to deprecations of the IBMProvider and the cloud simulator "ibmq_qasm_simulator". - [(#550)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/550) - -### Deprecations 👋 - -### Documentation 📝 - -### Bug fixes 🐛 - -======= ->>>>>>> master ### Contributors ✍️ This release contains contributions from (in alphabetical order): From b6c398911e04d3db95e74e50094d1b2f44034a1e Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:16:14 -0400 Subject: [PATCH 43/47] Use new qiskit device as the base for remote (#566) * import changes * Added TODOs for tests * changes * this should pass * pylint * circular import * observables update * remerge * import from qiskitdevice2 * Delete unnecessary tests and mocks * black/pylint * Add tests back in for codecov. * refactor tests * delete legacy device only functionality * change around imports * add assertion * fix * fix * fix setup * clean up * fix reqs.txt * fix * changelog changed * maybe this works? * a docstring? * a docstring? * reverts * does this break * revert * fix * fix * attempt a doc fix * Update tests/test_integration.py Co-authored-by: Utkarsh * some docstrings --------- Co-authored-by: obliviateandsurrender --- pennylane_qiskit/aer.py | 4 +- pennylane_qiskit/basic_sim.py | 4 +- pennylane_qiskit/converter.py | 46 +- pennylane_qiskit/qiskit_device.py | 936 +++++++++++++---------- pennylane_qiskit/qiskit_device2.py | 669 ---------------- pennylane_qiskit/qiskit_device_legacy.py | 491 ++++++++++++ tests/test_base_device.py | 116 +-- tests/test_integration.py | 84 +- tests/test_qiskit_device.py | 12 +- 9 files changed, 1172 insertions(+), 1190 deletions(-) delete mode 100644 pennylane_qiskit/qiskit_device2.py create mode 100644 pennylane_qiskit/qiskit_device_legacy.py diff --git a/pennylane_qiskit/aer.py b/pennylane_qiskit/aer.py index 24e1a3536..34d6c3bce 100644 --- a/pennylane_qiskit/aer.py +++ b/pennylane_qiskit/aer.py @@ -18,10 +18,10 @@ """ import qiskit_aer -from .qiskit_device import QiskitDevice +from .qiskit_device_legacy import QiskitDeviceLegacy -class AerDevice(QiskitDevice): +class AerDevice(QiskitDeviceLegacy): """A PennyLane device for the C++ Qiskit Aer simulator. Please refer to the `Qiskit documentation `_ for diff --git a/pennylane_qiskit/basic_sim.py b/pennylane_qiskit/basic_sim.py index 0b51be915..99f0a10eb 100644 --- a/pennylane_qiskit/basic_sim.py +++ b/pennylane_qiskit/basic_sim.py @@ -17,10 +17,10 @@ using PennyLane. """ from qiskit.providers.basic_provider import BasicProvider -from .qiskit_device import QiskitDevice +from .qiskit_device_legacy import QiskitDeviceLegacy -class BasicSimulatorDevice(QiskitDevice): +class BasicSimulatorDevice(QiskitDeviceLegacy): """A PennyLane device for the native Python Qiskit simulator. For more information on the ``BasicSimulator`` backend options and transpile options, please visit the diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 064ab4c88..0df4e492d 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -25,6 +25,7 @@ import qiskit.qasm2 from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit +from qiskit.circuit import library as lib from qiskit.circuit.classical import expr from qiskit.circuit.controlflow.switch_case import _DefaultCaseType from qiskit.circuit.library import GlobalPhaseGate @@ -36,7 +37,50 @@ import pennylane as qml import pennylane.ops as pennylane_ops from pennylane.tape.tape import rotations_and_diagonal_measurements -from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP + +QISKIT_OPERATION_MAP = { + # native PennyLane operations also native to qiskit + "PauliX": lib.XGate, + "PauliY": lib.YGate, + "PauliZ": lib.ZGate, + "Hadamard": lib.HGate, + "CNOT": lib.CXGate, + "CZ": lib.CZGate, + "SWAP": lib.SwapGate, + "ISWAP": lib.iSwapGate, + "RX": lib.RXGate, + "RY": lib.RYGate, + "RZ": lib.RZGate, + "Identity": lib.IGate, + "CSWAP": lib.CSwapGate, + "CRX": lib.CRXGate, + "CRY": lib.CRYGate, + "CRZ": lib.CRZGate, + "PhaseShift": lib.PhaseGate, + "QubitStateVector": lib.Initialize, + "StatePrep": lib.Initialize, + "Toffoli": lib.CCXGate, + "QubitUnitary": lib.UnitaryGate, + "U1": lib.U1Gate, + "U2": lib.U2Gate, + "U3": lib.U3Gate, + "IsingZZ": lib.RZZGate, + "IsingYY": lib.RYYGate, + "IsingXX": lib.RXXGate, + "S": lib.SGate, + "T": lib.TGate, + "SX": lib.SXGate, + "Adjoint(S)": lib.SdgGate, + "Adjoint(T)": lib.TdgGate, + "Adjoint(SX)": lib.SXdgGate, + "CY": lib.CYGate, + "CH": lib.CHGate, + "CPhase": lib.CPhaseGate, + "CCZ": lib.CCZGate, + "ECR": lib.ECRGate, + "Barrier": lib.Barrier, + "Adjoint(GlobalPhase)": lib.GlobalPhaseGate, +} inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index b03a96922..8df10cadf 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 Xanadu Quantum Technologies Inc. +# Copyright 2019-2024 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,124 +12,258 @@ # See the License for the specific language governing permissions and # limitations under the License. r""" -This module contains a base class for constructing Qiskit devices for PennyLane. +This module contains a prototype base class for constructing Qiskit devices +for PennyLane with the new device API. """ # pylint: disable=too-many-instance-attributes,attribute-defined-outside-init -import abc -import inspect import warnings +import inspect +from typing import Union, Callable, Tuple, Sequence +from contextlib import contextmanager import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.circuit import library as lib +import pennylane as qml from qiskit.compiler import transpile -from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.providers import Backend, BackendV2, QiskitBackendNotFoundError - -from pennylane import QubitDevice, DeviceError -from pennylane.measurements import SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP +from qiskit.providers import BackendV2 + +from qiskit_ibm_runtime import Session, SamplerV2 as Sampler, EstimatorV2 as Estimator + +from pennylane import transform +from pennylane.transforms.core import TransformProgram +from pennylane.transforms import broadcast_expand, split_non_commuting +from pennylane.tape import QuantumTape, QuantumScript +from pennylane.typing import Result, ResultBatch +from pennylane.devices import Device +from pennylane.devices.execution_config import ExecutionConfig, DefaultExecutionConfig +from pennylane.devices.preprocess import ( + decompose, + validate_observables, + validate_measurements, + validate_device_wires, +) +from pennylane.measurements import ExpectationMP, VarianceMP from ._version import __version__ +from .converter import QISKIT_OPERATION_MAP, circuit_to_qiskit, mp_to_pauli + +QuantumTapeBatch = Sequence[QuantumTape] +QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] +Result_or_ResultBatch = Union[Result, ResultBatch] + + +# pylint: disable=protected-access +@contextmanager +def qiskit_session(device, **kwargs): + """A context manager that creates a Qiskit Session and sets it as a session + on the device while the context manager is active. Using the context manager + will ensure the Session closes properly and is removed from the device after + completing the tasks. Any Session that was initialized and passed into the + device will be overwritten by the Qiskit Session created by this context + manager. + + Args: + device (QiskitDevice2): the device that will create remote tasks using the session + **kwargs: session keyword arguments to be used for settings for the Session. At the + time of writing, the only relevant keyword argument is "max_time", which lets you + set the maximum amount of time the sessin is open. For the most up to date information, + please refer to the Qiskit Session + `documentation `_. + + **Example:** + + .. code-block:: python + + import pennylane as qml + from pennylane_qiskit import qiskit_session + from qiskit_ibm_runtime import QiskitRuntimeService + + # get backend + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(simulator=False, operational=True) + + # initialize device + dev = qml.device('qiskit.remote', wires=2, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, 0) + qml.CNOT([0, 1]) + return qml.expval(qml.PauliZ(1)) + + angle = 0.1 + + with qiskit_session(dev, max_time=60) as session: + # queue for the first execution + res = circuit(angle)[0] + + # then this loop executes immediately after without queueing again + while res > 0: + angle += 0.3 + res = circuit(angle)[0] + + Note that if you passed in a session to your device, that session will be overwritten + by `qiskit_session`. + + .. code-block:: python + + import pennylane as qml + from pennylane_qiskit import qiskit_session + from qiskit_ibm_runtime import QiskitRuntimeService, Session -SAMPLE_TYPES = (SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP) - - -QISKIT_OPERATION_MAP = { - # native PennyLane operations also native to qiskit - "PauliX": lib.XGate, - "PauliY": lib.YGate, - "PauliZ": lib.ZGate, - "Hadamard": lib.HGate, - "CNOT": lib.CXGate, - "CZ": lib.CZGate, - "SWAP": lib.SwapGate, - "ISWAP": lib.iSwapGate, - "RX": lib.RXGate, - "RY": lib.RYGate, - "RZ": lib.RZGate, - "Identity": lib.IGate, - "CSWAP": lib.CSwapGate, - "CRX": lib.CRXGate, - "CRY": lib.CRYGate, - "CRZ": lib.CRZGate, - "PhaseShift": lib.PhaseGate, - "QubitStateVector": lib.Initialize, - "StatePrep": lib.Initialize, - "Toffoli": lib.CCXGate, - "QubitUnitary": lib.UnitaryGate, - "U1": lib.U1Gate, - "U2": lib.U2Gate, - "U3": lib.U3Gate, - "IsingZZ": lib.RZZGate, - "IsingYY": lib.RYYGate, - "IsingXX": lib.RXXGate, - "S": lib.SGate, - "T": lib.TGate, - "SX": lib.SXGate, - "Adjoint(S)": lib.SdgGate, - "Adjoint(T)": lib.TdgGate, - "Adjoint(SX)": lib.SXdgGate, - "CY": lib.CYGate, - "CH": lib.CHGate, - "CPhase": lib.CPhaseGate, - "CCZ": lib.CCZGate, - "ECR": lib.ECRGate, - "Barrier": lib.Barrier, - "Adjoint(GlobalPhase)": lib.GlobalPhaseGate, -} - - -def _get_backend_name(backend): + # get backend + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(simulator=False, operational=True) + + # initialize device + dev = qml.device('qiskit.remote', wires=2, backend=backend, session=Session(backend=backend, max_time=30)) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, 0) + qml.CNOT([0, 1]) + return qml.expval(qml.PauliZ(1)) + + angle = 0.1 + + # This session will have the Qiskit default settings max_time=900 + with qiskit_session(dev) as session: + res = circuit(angle)[0] + + while res > 0: + angle += 0.3 + res = circuit(angle)[0] + """ + # Code to acquire session: + existing_session = device._session + + session_options = {"backend": device.backend, "service": device.service} + + for k, v in kwargs.items(): + # Options like service and backend should be tied to the settings set on device + if k in session_options: + warnings.warn(f"Using '{k}' set in device, {getattr(device, k)}", UserWarning) + else: + session_options[k] = v + + session = Session(**session_options) + device._session = session try: - return backend.name() # BackendV1 - except TypeError: # pragma: no cover - return backend.name # BackendV2 + yield session + finally: + # Code to release session: + session.close() + device._session = existing_session + + +def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether or not a measurement is accepted when sampling.""" + + return isinstance( + m, + ( + qml.measurements.SampleMeasurement, + qml.measurements.ClassicalShadowMP, + qml.measurements.ShadowExpvalMP, + ), + ) + + +@transform +def split_execution_types( + tape: qml.tape.QuantumTape, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Split into separate tapes based on measurement type. Counts and sample-based measurements + will use the Qiskit Sampler. ExpectationValue and Variance will use the Estimator, except + when the measured observable does not have a `pauli_rep`. In that case, the Sampler will be + used, and the raw samples will be processed to give an expectation value.""" + estimator = [] + sampler = [] + + for i, mp in enumerate(tape.measurements): + if isinstance(mp, (ExpectationMP, VarianceMP)): + if mp.obs.pauli_rep: + estimator.append((mp, i)) + else: + warnings.warn( + f"The observable measured {mp.obs} does not have a `pauli_rep` " + "and will be run without using the Estimator primitive. Instead, " + "raw samples from the Sampler will be used." + ) + sampler.append((mp, i)) + else: + sampler.append((mp, i)) + + order_indices = [[i for mp, i in group] for group in [estimator, sampler]] + + tapes = [] + if estimator: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in estimator], + shots=tape.shots, + ) + ] + ) + if sampler: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in sampler], + shots=tape.shots, + ) + ] + ) + + def reorder_fn(res): + """re-order the output to the original shape and order""" + flattened_indices = [i for group in order_indices for i in group] + flattened_results = [r for group in res for r in group] -class QiskitDevice(QubitDevice, abc.ABC): - r"""Abstract Qiskit device for PennyLane. + if len(flattened_indices) != len(flattened_results): + raise ValueError( + "The lengths of flattened_indices and flattened_results do not match." + ) # pragma: no cover + + result = dict(zip(flattened_indices, flattened_results)) + + result = tuple(result[i] for i in sorted(result.keys())) + + return result[0] if len(result) == 1 else result + + return tapes, reorder_fn + + +class QiskitDevice(Device): + r"""Hardware/simulator Qiskit device for PennyLane. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider | None): The Qiskit backend provider. - backend (str | Backend): the desired backend. If a string, a provider must be given. - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. For state vector backends, - setting to ``None`` results in computing statistics like expectation values and variances analytically. + or strings (``['aux_wire', 'q1', 'q2']``). + backend (Backend): the initialized Qiskit backend Keyword Args: - 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. + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. + session (Session): a Qiskit Session to use for device execution. If none is provided, a session will + be created at each device execution. + compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be + sent to the backend device, to be set if the backend desired for compliation differs from the + backend used for execution. Defaults to ``None``, which means the primary backend will be used. + **kwargs: transpilation and runtime keyword arguments to be used for measurements with Primitives. + If an `options` dictionary is defined amongst the kwargs, and there are settings that overlap + with those in kwargs, the settings in `options` will take precedence over kwargs. Keyword + arguments accepted by both the transpiler and at runtime (e.g. ``optimization_level``) + will be passed to the transpiler rather than to the Primitive. """ - name = "Qiskit PennyLane plugin" - pennylane_requires = ">=0.37.0" - version = __version__ - plugin_version = __version__ - author = "Xanadu" - - _capabilities = { - "model": "qubit", - "tensor_observables": True, - "inverse_operations": True, - } - _operation_map = QISKIT_OPERATION_MAP - _state_backends = { - "statevector_simulator", - "simulator_statevector", - "unitary_simulator", - "aer_simulator_statevector", - "aer_simulator_unitary", - } - """set[str]: Set of backend names that define the backends - that support returning the underlying quantum statevector""" - - operations = set(_operation_map.keys()) + operations = set(QISKIT_OPERATION_MAP.keys()) observables = { "PauliX", "PauliY", @@ -138,399 +272,415 @@ class QiskitDevice(QubitDevice, abc.ABC): "Hadamard", "Hermitian", "Projector", + "Prod", + "Sum", + "LinearCombination", + "SProd", + # TODO Could support SparseHamiltonian } - analytic_warning_message = ( - "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." - ) - - _eigs = {} - - def __init__(self, wires, provider, backend, shots=1024, **kwargs): + # pylint:disable = too-many-arguments + def __init__( + self, + wires, + backend, + shots=1024, + session=None, + compile_backend=None, + **kwargs, + ): + + if shots is None: + warnings.warn( + "Expected an integer number of shots, but received shots=None. Defaulting " + "to 1024 shots. The analytic calculation of results is not supported on " + "this device. All statistics obtained from this device are estimates based " + "on samples.", + UserWarning, + ) + + shots = 1024 super().__init__(wires=wires, shots=shots) - self.provider = provider - - if isinstance(backend, Backend): - self._backend = backend - self.backend_name = _get_backend_name(backend) - elif provider is None: - raise ValueError("Must pass a provider if the backend is not a Backend instance.") - else: - try: - self._backend = provider.get_backend(backend) - except QiskitBackendNotFoundError as e: - available_backends = list(map(_get_backend_name, provider.backends())) - raise ValueError( - f"Backend '{backend}' does not exist. Available backends " - f"are:\n {available_backends}" - ) from e - - self.backend_name = _get_backend_name(self._backend) - - # Keep track if the user specified analytic to be True - if shots is None and not self._is_state_backend: - # Raise a warning if no shots were specified for a hardware device - warnings.warn(self.analytic_warning_message.format(backend), UserWarning) + self._backend = backend + self._compile_backend = compile_backend if compile_backend else backend - self.shots = 1024 + self._service = getattr(backend, "_service", None) + self._session = session - self._capabilities["returns_state"] = self._is_state_backend + kwargs["shots"] = shots # Perform validation against backend - backend_qubits = ( + available_qubits = ( backend.num_qubits if isinstance(backend, BackendV2) - else self.backend.configuration().n_qubits + else backend.configuration().n_qubits ) - if backend_qubits and len(self.wires) > int(backend_qubits): - raise ValueError(f"Backend '{backend}' supports maximum {backend_qubits} wires") + if len(self.wires) > int(available_qubits): + raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") - # Initialize inner state self.reset() + self._kwargs, self._transpile_args = self._process_kwargs( + kwargs + ) # processes kwargs and separates transpilation arguments to dev._transpile_args - self.process_kwargs(kwargs) - - def process_kwargs(self, kwargs): - """Processing the keyword arguments that were provided upon device initialization. + @property + def backend(self): + """The Qiskit backend object. - Args: - kwargs (dict): keyword arguments to be set for the device + Returns: + qiskit.providers.Backend: Qiskit backend object. """ - self.compile_backend = None - if "compile_backend" in kwargs: - self.compile_backend = kwargs.pop("compile_backend") - - if "noise_model" in kwargs: - noise_model = kwargs.pop("noise_model") - self.backend.set_options(noise_model=noise_model) - - # set transpile_args - self.set_transpile_args(**kwargs) - - # Get further arguments for run - self.run_args = {} - - # Specify to have a memory for hw/hw simulators - compile_backend = self.compile_backend or self.backend - memory = str(compile_backend) not in self._state_backends - - if memory: - kwargs["memory"] = True - - # Consider the remaining kwargs as keyword arguments to run - self.run_args.update(kwargs) - - @property - def _is_state_backend(self): - """Returns whether this device has a state backend.""" - return self.backend_name in self._state_backends or self.backend.options.get("method") in { - "unitary", - "statevector", - } + return self._backend @property - def _is_statevector_backend(self): - """Returns whether this device has a statevector backend.""" - method = "statevector" - return method in self.backend_name or self.backend.options.get("method") == method + def compile_backend(self): + """The ``compile_backend`` is a Qiskit backend object to be used for transpilation. + Returns: + qiskit.providers.backend: Qiskit backend object. + """ + return self._compile_backend @property - def _is_unitary_backend(self): - """Returns whether this device has a unitary backend.""" - method = "unitary" - return method in self.backend_name or self.backend.options.get("method") == method - - def set_transpile_args(self, **kwargs): - """The transpile argument setter. + def service(self): + """The QiskitRuntimeService service. - Keyword Args: - kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the - `Qiskit transpiler documentation `_ + Returns: + qiskit.qiskit_ibm_runtime.QiskitRuntimeService """ - transpile_sig = inspect.signature(transpile).parameters - self.transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} - self.transpile_args.pop("circuits", None) - self.transpile_args.pop("backend", None) + return self._service @property - def backend(self): - """The Qiskit backend object. + def session(self): + """The QiskitRuntimeService session. Returns: - qiskit.providers.backend: Qiskit backend object. + qiskit.qiskit_ibm_runtime.Session """ - return self._backend + return self._session - def reset(self): - """Reset the Qiskit backend device""" - # Reset only internal data, not the options that are determined on - # device creation - self._reg = QuantumRegister(self.num_wires, "q") - self._creg = ClassicalRegister(self.num_wires, "c") - self._circuit = QuantumCircuit(self._reg, self._creg, name="temp") + @property + def num_wires(self): + """Get the number of wires. - self._current_job = None - self._state = None # statevector of a simulator backend + Returns: + int: The number of wires. + """ + return len(self.wires) - def create_circuit_object(self, operations, **kwargs): - """Builds the circuit objects based on the operations and measurements - specified to apply. + def update_session(self, session): + """Update the session attribute. Args: - operations (list[~.Operation]): operations to apply to the device - - Keyword args: - rotations (list[~.Operation]): Operations that rotate the circuit - pre-measurement into the eigenbasis of the observables. + session: The new session to be set. """ - rotations = kwargs.get("rotations", []) - - applied_operations = self.apply_operations(operations) + self._session = session - # Rotating the state for measurement in the computational basis - rotation_circuits = self.apply_operations(rotations) - applied_operations.extend(rotation_circuits) + def reset(self): + """Reset the current job to None.""" + self._current_job = None - for circuit in applied_operations: - self._circuit &= circuit + def stopping_condition(self, op: qml.operation.Operator) -> bool: + """Specifies whether or not an Operator is accepted by QiskitDevice2.""" + return op.name in self.operations - if not self._is_state_backend: - # Add measurements if they are needed - for qr, cr in zip(self._reg, self._creg): - self._circuit.measure(qr, cr) - elif "aer" in self.backend_name: - self._circuit.save_state() + def observable_stopping_condition(self, obs: qml.operation.Operator) -> bool: + """Specifies whether or not an observable is accepted by QiskitDevice2.""" + return obs.name in self.observables - def apply(self, operations, **kwargs): - """Build the circuit object and apply the operations""" - self.create_circuit_object(operations, **kwargs) + def preprocess( + self, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Tuple[TransformProgram, ExecutionConfig]: + """This function defines the device transform program to be applied and an updated device configuration. - # These operations need to run for all devices - compiled_circuit = self.compile() - self.run(compiled_circuit) + Args: + execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the + parameters needed to fully describe the execution. - def apply_operations(self, operations): - """Apply the circuit operations. + Returns: + TransformProgram, ExecutionConfig: A transform program that when called returns QuantumTapes that the device + can natively execute as well as a postprocessing function to be called after execution, and a configuration with + unset specifications filled in. - This method serves as an auxiliary method to :meth:`~.QiskitDevice.apply`. + This device: - Args: - operations (List[pennylane.Operation]): operations to be applied + * Supports any operations with explicit PennyLane to Qiskit gate conversions defined in the plugin + * Does not intrinsically support parameter broadcasting - Returns: - list[QuantumCircuit]: a list of quantum circuit objects that - specify the corresponding operations """ - circuits = [] + config = execution_config + config.use_device_gradient = False - for operation in operations: - # Apply the circuit operations - device_wires = self.map_wires(operation.wires) - par = operation.parameters + transform_program = TransformProgram() - for idx, p in enumerate(par): - if isinstance(p, np.ndarray): - # Convert arrays so that Qiskit accepts the parameter - par[idx] = p.tolist() + transform_program.add_transform(validate_device_wires, self.wires, name=self.name) + transform_program.add_transform( + decompose, + stopping_condition=self.stopping_condition, + name=self.name, + skip_initial_state_prep=False, + ) + transform_program.add_transform( + validate_measurements, + sample_measurements=accepted_sample_measurement, + name=self.name, + ) + transform_program.add_transform( + validate_observables, + stopping_condition=self.observable_stopping_condition, + name=self.name, + ) - operation = operation.name + transform_program.add_transform(broadcast_expand) + transform_program.add_transform(split_non_commuting) - mapped_operation = self._operation_map[operation] + transform_program.add_transform(split_execution_types) - self.qubit_state_vector_check(operation) + return transform_program, config - qregs = [self._reg[i] for i in device_wires.labels] + def _process_kwargs(self, kwargs): + """Processes kwargs given and separates them into kwargs and transpile_args. If given + a keyword argument 'options' that is a dictionary, a common practice in + Qiskit, the options in said dictionary take precedence over any overlapping keyword + arguments defined in the kwargs. - if operation in ("QubitUnitary", "QubitStateVector", "StatePrep"): - # Need to revert the order of the quantum registers used in - # Qiskit such that it matches the PennyLane ordering - qregs = list(reversed(qregs)) + Keyword Args: + kwargs (dict): keyword arguments that set either runtime options or transpilation + options. - if operation in ("Barrier",): - # Need to add the num_qubits for instantiating Barrier in Qiskit - par = [len(self._reg)] + Returns: + kwargs, transpile_args: keyword arguments for the runtime options and keyword + arguments for the transpiler + """ - dag = circuit_to_dag(QuantumCircuit(self._reg, self._creg, name="")) + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model") + self.backend.set_options(noise_model=noise_model) - gate = mapped_operation(*par) + if "options" in kwargs: + for key, val in kwargs.pop("options").items(): + if key in kwargs: + warnings.warn( + "An overlap between what was passed in via options and what was passed in via kwargs was found." + f"The value set in options {key}={val} will be used." + ) + kwargs[key] = val - dag.apply_operation_back(gate, qargs=qregs) - circuit = dag_to_circuit(dag) - circuits.append(circuit) + shots = kwargs.pop("shots") - return circuits + if "default_shots" in kwargs: + warnings.warn( + f"default_shots was found in the keyword arguments, but it is not supported by {self.name}" + "Please use the `shots` keyword argument instead. The number of shots " + f"{shots} will be used instead." + ) + kwargs["default_shots"] = shots - def qubit_state_vector_check(self, operation): - """Input check for the StatePrepBase operations. + kwargs, transpile_args = self.get_transpile_args(kwargs) - Args: - operation (pennylane.Operation): operation to be checked + return kwargs, transpile_args - Raises: - DeviceError: If the operation is QubitStateVector or StatePrep - """ - if operation in ("QubitStateVector", "StatePrep"): - if self._is_unitary_backend: - raise DeviceError( - f"The {operation} operation " - "is not supported on the unitary simulator backend." - ) + @staticmethod + def get_transpile_args(kwargs): + """The transpile argument setter. This separates keyword arguments related to transpilation + from the rest of the keyword arguments and removes those keyword arguments from kwargs. - def compile(self): - """Compile the quantum circuit to target the provided compile_backend. + Keyword Args: + kwargs (dict): combined keyword arguments to be parsed for the Qiskit transpiler. For more details, see the + `Qiskit transpiler documentation `_ - If compile_backend is None, then the target is simply the - backend. + Returns: + kwargs (dict), transpile_args (dict): keyword arguments for the runtime options and keyword + arguments for the transpiler """ - compile_backend = self.compile_backend or self.backend - compiled_circuits = transpile(self._circuit, backend=compile_backend, **self.transpile_args) - return compiled_circuits - def run(self, qcirc): - """Run the compiled circuit and query the result. + transpile_sig = inspect.signature(transpile).parameters - Args: - qcirc (qiskit.QuantumCircuit): the quantum circuit to be run on the backend - """ - self._current_job = self.backend.run(qcirc, shots=self.shots, **self.run_args) - result = self._current_job.result() + transpile_args = {arg: kwargs.pop(arg) for arg in transpile_sig if arg in kwargs} + transpile_args.pop("circuits", None) + transpile_args.pop("backend", None) - if self._is_state_backend: - self._state = self._get_state(result) + return kwargs, transpile_args - def _get_state(self, result, experiment=None): - """Returns the statevector for state simulator backends. + def compile_circuits(self, circuits): + """Compiles multiple circuits one after the other. Args: - result (qiskit.Result): result object - experiment (str or None): the name of the experiment to get the state for. + circuits (list[QuantumCircuit]): the circuits to be compiled Returns: - array[float]: size ``(2**num_wires,)`` statevector + list[QuantumCircuit]: the list of compiled circuits """ - if self._is_statevector_backend: - state = np.asarray(result.get_statevector(experiment)) + # Compile each circuit object + compiled_circuits = [] + transpile_args = self._transpile_args - elif self._is_unitary_backend: - unitary = np.asarray(result.get_unitary(experiment)) - initial_state = np.zeros([2**self.num_wires]) - initial_state[0] = 1 + for i, circuit in enumerate(circuits): + compiled_circ = transpile(circuit, backend=self.compile_backend, **transpile_args) + compiled_circ.name = f"circ{i}" + compiled_circuits.append(compiled_circ) - state = unitary @ initial_state + return compiled_circuits - # reverse qubit order to match PennyLane convention - return state.reshape([2] * self.num_wires).T.flatten() + # pylint: disable=unused-argument, no-member + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + """Execute a circuit or a batch of circuits and turn it into results.""" + session = self._session or Session(backend=self.backend) - def generate_samples(self, circuit=None): - r"""Returns the computational basis samples generated for all wires. + results = [] - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. + if isinstance(circuits, QuantumScript): + circuits = [circuits] + + @contextmanager + def execute_circuits(session): + try: + for circ in circuits: + if circ.shots and len(circ.shots.shot_vector) > 1: + raise ValueError( + f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." + "Please use a single integer instead when specifying the number of shots." + ) + if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)) and getattr( + circ.measurements[0].obs, "pauli_rep", None + ): + execute_fn = self._execute_estimator + else: + execute_fn = self._execute_sampler + results.append(execute_fn(circ, session)) + yield results + finally: + session.close() + + with execute_circuits(session) as results: + return results + + def _execute_sampler(self, circuit, session): + """Returns the result of the execution of the circuit using the SamplerV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.sample()` will return the raw samples. Args: - circuit (str or None): the name of the circuit to get the state for + circuits (list[QuantumCircuit]): the circuits to be executed via SamplerV2 + session (Session): the session that the execution will be performed with Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + result (tuple): the processed result from SamplerV2 """ + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True)] + sampler = Sampler(session=session) + compiled_circuits = self.compile_circuits(qcirc) + sampler.options.update(**self._kwargs) - # branch out depending on the type of backend - if self._is_state_backend: - # software simulator: need to sample from probabilities - return super().generate_samples() + # len(compiled_circuits) is always 1 so the indexing does not matter. + result = sampler.run( + compiled_circuits, + shots=circuit.shots.total_shots if circuit.shots.total_shots else None, + ).result()[0] + classical_register_name = compiled_circuits[0].cregs[0].name + self._current_job = getattr(result.data, classical_register_name) - # hardware or hardware simulator - samples = self._current_job.result().get_memory(circuit) - # reverse qubit order to match PennyLane convention - return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) + # needs processing function to convert to the correct format for states, and + # also handle instances where wires were specified in probs, and for multiple probs measurements - @property - def state(self): - """Get state of the device""" - return self._state + self._samples = self.generate_samples(0) + res = [ + mp.process_samples(self._samples, wire_order=self.wires) for mp in circuit.measurements + ] - def analytic_probability(self, wires=None): - """Get the analytic probability of the device""" - if self._state is None: - return None + single_measurement = len(circuit.measurements) == 1 + res = (res[0],) if single_measurement else tuple(res) - prob = self.marginal_prob(np.abs(self._state) ** 2, wires) - return prob + return res - def compile_circuits(self, circuits): - r"""Compiles multiple circuits one after the other. + def _execute_estimator(self, circuit, session): + """Returns the result of the execution of the circuit using the EstimatorV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.var` will return the variance. Args: - circuits (list[.tapes.QuantumTape]): the circuits to be compiled + circuits (list[QuantumCircuit]): the circuits to be executed via EstimatorV2 + session (Session): the session that the execution will be performed with Returns: - list[QuantumCircuit]: the list of compiled circuits + result (tuple): the processed result from EstimatorV2 """ - # Compile each circuit object - compiled_circuits = [] - - for circuit in circuits: - # We need to reset the device here, else it will - # not start the next computation in the zero state - self.reset() - self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates) - - compiled_circ = self.compile() - compiled_circ.name = f"circ{len(compiled_circuits)}" - compiled_circuits.append(compiled_circ) + # the Estimator primitive takes care of diagonalization and measurements itself, + # so diagonalizing gates and measurements are not included in the circuit + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False)] + estimator = Estimator(session=session) + + pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] + compiled_circuits = self.compile_circuits(qcirc) + estimator.options.update(**self._kwargs) + # split into one call per measurement + # could technically be more efficient if there are some observables where we ask + # for expectation value and variance on the same observable, but spending time on + # that right now feels excessive + circ_and_obs = [(compiled_circuits[0], pauli_observables)] + result = estimator.run( + circ_and_obs, + precision=np.sqrt(1 / circuit.shots.total_shots) if circuit.shots else None, + ).result() + self._current_job = result + result = self._process_estimator_job(circuit.measurements, result) + + return result + + @staticmethod + def _process_estimator_job(measurements, job_result): + """Estimator returns the expectation value and standard error for each observable measured, + along with some metadata that contains the precision. Extracts the relevant number for each + measurement process and return the requested results from the Estimator executions. + + Note that for variance, we calculate the variance by using the standard error and the + precision value. - return compiled_circuits + Args: + measurements (list[MeasurementProcess]): the measurements in the circuit + job_result (Any): the result from EstimatorV2 - def batch_execute(self, circuits, timeout: int = None): - """Batch execute the circuits on the device""" + Returns: + result (tuple): the processed result from EstimatorV2 + """ + expvals = job_result[0].data.evs + variances = (job_result[0].data.stds / job_result[0].metadata["target_precision"]) ** 2 + result = [] + for i, mp in enumerate(measurements): + if isinstance(mp, ExpectationMP): + result.append(expvals[i]) + elif isinstance(mp, VarianceMP): + result.append(variances[i]) - compiled_circuits = self.compile_circuits(circuits) + single_measurement = len(measurements) == 1 + result = (result[0],) if single_measurement else tuple(result) - if not compiled_circuits: - # At least one circuit must always be provided to the backend. - return [] + return result - # Send the batch of circuit objects using backend.run - self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args) + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. - try: - result = self._current_job.result(timeout=timeout) - except TypeError: # pragma: no cover - # timeout not supported - result = self._current_job.result() + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. - # increment counter for number of executions of qubit device - # pylint: disable=no-member - self._num_executions += 1 + Args: + circuit (int): position of the circuit in the batch. - # Compute statistics using the state and/or samples - results = [] - for circuit, circuit_obj in zip(circuits, compiled_circuits): - # Update the tracker - if self.tracker.active: - self.tracker.update(executions=1, shots=self.shots) - self.tracker.record() - - if self._is_state_backend: - self._state = self._get_state(result, experiment=circuit_obj) - - # generate computational basis samples - if self.shots is not None or any( - isinstance(m, SAMPLE_TYPES) for m in circuit.measurements - ): - self._samples = self.generate_samples(circuit_obj) - - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - if self.tracker.active: - self.tracker.update(batches=1, batch_len=len(circuits)) - self.tracker.record() - - return results + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + counts = self._current_job.get_counts() + # Batch of circuits + if not isinstance(counts, dict): + counts = self._current_job.get_counts()[circuit] + + samples = [] + for key, value in counts.items(): + samples.extend([key] * value) + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) diff --git a/pennylane_qiskit/qiskit_device2.py b/pennylane_qiskit/qiskit_device2.py deleted file mode 100644 index ed4cf6a1c..000000000 --- a/pennylane_qiskit/qiskit_device2.py +++ /dev/null @@ -1,669 +0,0 @@ -# Copyright 2019-2024 Xanadu Quantum Technologies Inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -r""" -This module contains a prototype base class for constructing Qiskit devices -for PennyLane with the new device API. -""" -# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init, missing-function-docstring - - -import warnings -import inspect -from typing import Union, Callable, Tuple, Sequence -from contextlib import contextmanager - -import numpy as np -import pennylane as qml -from qiskit.compiler import transpile -from qiskit.providers import BackendV2 - -from qiskit_ibm_runtime import Session, SamplerV2 as Sampler, EstimatorV2 as Estimator - -from pennylane import transform -from pennylane.transforms.core import TransformProgram -from pennylane.transforms import broadcast_expand, split_non_commuting -from pennylane.tape import QuantumTape, QuantumScript -from pennylane.typing import Result, ResultBatch -from pennylane.devices import Device -from pennylane.devices.execution_config import ExecutionConfig, DefaultExecutionConfig -from pennylane.devices.preprocess import ( - decompose, - validate_observables, - validate_measurements, - validate_device_wires, -) -from pennylane.measurements import ExpectationMP, VarianceMP - -from ._version import __version__ -from .converter import QISKIT_OPERATION_MAP, circuit_to_qiskit, mp_to_pauli - -QuantumTapeBatch = Sequence[QuantumTape] -QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] -Result_or_ResultBatch = Union[Result, ResultBatch] - - -# pylint: disable=protected-access -@contextmanager -def qiskit_session(device, **kwargs): - """A context manager that creates a Qiskit Session and sets it as a session - on the device while the context manager is active. Using the context manager - will ensure the Session closes properly and is removed from the device after - completing the tasks. Any Session that was initialized and passed into the - device will be overwritten by the Qiskit Session created by this context - manager. - - Args: - device (QiskitDevice2): the device that will create remote tasks using the session - **kwargs: session keyword arguments to be used for settings for the Session. At the - time of writing, the only relevant keyword argument is "max_time", which lets you - set the maximum amount of time the sessin is open. For the most up to date information, - please refer to the Qiskit Session - `documentation `_. - - **Example:** - - .. code-block:: python - - import pennylane as qml - from pennylane_qiskit import qiskit_session - from qiskit_ibm_runtime import QiskitRuntimeService - - # get backend - service = QiskitRuntimeService(channel="ibm_quantum") - backend = service.least_busy(simulator=False, operational=True) - - # initialize device - dev = qml.device('qiskit.remote', wires=2, backend=backend) - - @qml.qnode(dev) - def circuit(x): - qml.RX(x, 0) - qml.CNOT([0, 1]) - return qml.expval(qml.PauliZ(1)) - - angle = 0.1 - - with qiskit_session(dev, max_time=60) as session: - # queue for the first execution - res = circuit(angle)[0] - - # then this loop executes immediately after without queueing again - while res > 0: - angle += 0.3 - res = circuit(angle)[0] - - Note that if you passed in a session to your device, that session will be overwritten - by `qiskit_session`. - - .. code-block:: python - - import pennylane as qml - from pennylane_qiskit import qiskit_session - from qiskit_ibm_runtime import QiskitRuntimeService, Session - - # get backend - service = QiskitRuntimeService(channel="ibm_quantum") - backend = service.least_busy(simulator=False, operational=True) - - # initialize device - dev = qml.device('qiskit.remote', wires=2, backend=backend, session=Session(backend=backend, max_time=30)) - - @qml.qnode(dev) - def circuit(x): - qml.RX(x, 0) - qml.CNOT([0, 1]) - return qml.expval(qml.PauliZ(1)) - - angle = 0.1 - - # This session will have the Qiskit default settings max_time=900 - with qiskit_session(dev) as session: - res = circuit(angle)[0] - - while res > 0: - angle += 0.3 - res = circuit(angle)[0] - """ - # Code to acquire session: - existing_session = device._session - - session_options = {"backend": device.backend, "service": device.service} - - for k, v in kwargs.items(): - # Options like service and backend should be tied to the settings set on device - if k in session_options: - warnings.warn(f"Using '{k}' set in device, {getattr(device, k)}", UserWarning) - else: - session_options[k] = v - - session = Session(**session_options) - device._session = session - try: - yield session - finally: - # Code to release session: - session.close() - device._session = existing_session - - -def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: - """Specifies whether or not a measurement is accepted when sampling.""" - - return isinstance( - m, - ( - qml.measurements.SampleMeasurement, - qml.measurements.ClassicalShadowMP, - qml.measurements.ShadowExpvalMP, - ), - ) - - -@transform -def split_execution_types( - tape: qml.tape.QuantumTape, -) -> (Sequence[qml.tape.QuantumTape], Callable): - """Split into separate tapes based on measurement type. Counts and sample-based measurements - will use the Qiskit Sampler. ExpectationValue and Variance will use the Estimator, except - when the measured observable does not have a `pauli_rep`. In that case, the Sampler will be - used, and the raw samples will be processed to give an expectation value.""" - estimator = [] - sampler = [] - - for i, mp in enumerate(tape.measurements): - if isinstance(mp, (ExpectationMP, VarianceMP)): - if mp.obs.pauli_rep: - estimator.append((mp, i)) - else: - warnings.warn( - f"The observable measured {mp.obs} does not have a `pauli_rep` " - "and will be run without using the Estimator primitive. Instead, " - "raw samples from the Sampler will be used." - ) - sampler.append((mp, i)) - else: - sampler.append((mp, i)) - - order_indices = [[i for mp, i in group] for group in [estimator, sampler]] - - tapes = [] - if estimator: - tapes.extend( - [ - qml.tape.QuantumScript( - tape.operations, - measurements=[mp for mp, i in estimator], - shots=tape.shots, - ) - ] - ) - if sampler: - tapes.extend( - [ - qml.tape.QuantumScript( - tape.operations, - measurements=[mp for mp, i in sampler], - shots=tape.shots, - ) - ] - ) - - def reorder_fn(res): - """re-order the output to the original shape and order""" - - flattened_indices = [i for group in order_indices for i in group] - flattened_results = [r for group in res for r in group] - - result = dict(zip(flattened_indices, flattened_results)) - - result = tuple(result[i] for i in sorted(result.keys())) - - return result[0] if len(result) == 1 else result - - return tapes, reorder_fn - - -class QiskitDevice2(Device): - r"""Hardware/simulator Qiskit device for PennyLane. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['aux_wire', 'q1', 'q2']``). - backend (Backend): the initialized Qiskit backend - - Keyword Args: - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. - session (Session): a Qiskit Session to use for device execution. If none is provided, a session will - be created at each device execution. - compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be - sent to the backend device, to be set if the backend desired for compliation differs from the - backend used for execution. Defaults to ``None``, which means the primary backend will be used. - **kwargs: transpilation and runtime keyword arguments to be used for measurements with Primitives. - If an `options` dictionary is defined amongst the kwargs, and there are settings that overlap - with those in kwargs, the settings in `options` will take precedence over kwargs. Keyword - arguments accepted by both the transpiler and at runtime (e.g. ``optimization_level``) - will be passed to the transpiler rather than to the Primitive. - """ - - operations = set(QISKIT_OPERATION_MAP.keys()) - observables = { - "PauliX", - "PauliY", - "PauliZ", - "Identity", - "Hadamard", - "Hermitian", - "Projector", - "Prod", - "Sum", - "LinearCombination", - "SProd", - # TODO Could support SparseHamiltonian - } - - # pylint:disable = too-many-arguments - def __init__( - self, - wires, - backend, - shots=1024, - session=None, - compile_backend=None, - **kwargs, - ): - - if shots is None: - warnings.warn( - "Expected an integer number of shots, but received shots=None. Defaulting " - "to 1024 shots. The analytic calculation of results is not supported on " - "this device. All statistics obtained from this device are estimates based " - "on samples.", - UserWarning, - ) - - shots = 1024 - - super().__init__(wires=wires, shots=shots) - - self._backend = backend - self._compile_backend = compile_backend if compile_backend else backend - - self._service = getattr(backend, "_service", None) - self._session = session - - kwargs["shots"] = shots - - # Perform validation against backend - available_qubits = ( - backend.num_qubits - if isinstance(backend, BackendV2) - else backend.configuration().n_qubits - ) - if len(self.wires) > int(available_qubits): - raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") - - self.reset() - self._kwargs, self._transpile_args = self._process_kwargs( - kwargs - ) # processes kwargs and separates transpilation arguments to dev._transpile_args - - @property - def backend(self): - """The Qiskit backend object. - - Returns: - qiskit.providers.Backend: Qiskit backend object. - """ - return self._backend - - @property - def compile_backend(self): - """The ``compile_backend`` is a Qiskit backend object to be used for transpilation. - Returns: - qiskit.providers.backend: Qiskit backend object. - """ - return self._compile_backend - - @property - def service(self): - """The QiskitRuntimeService service. - - Returns: - qiskit.qiskit_ibm_runtime.QiskitRuntimeService - """ - return self._service - - @property - def session(self): - """The QiskitRuntimeService session. - - Returns: - qiskit.qiskit_ibm_runtime.Session - """ - return self._session - - @property - def num_wires(self): - return len(self.wires) - - def update_session(self, session): - self._session = session - - def reset(self): - self._current_job = None - - def stopping_condition(self, op: qml.operation.Operator) -> bool: - """Specifies whether or not an Operator is accepted by QiskitDevice2.""" - return op.name in self.operations - - def observable_stopping_condition(self, obs: qml.operation.Operator) -> bool: - """Specifies whether or not an observable is accepted by QiskitDevice2.""" - return obs.name in self.observables - - def preprocess( - self, - execution_config: ExecutionConfig = DefaultExecutionConfig, - ) -> Tuple[TransformProgram, ExecutionConfig]: - """This function defines the device transform program to be applied and an updated device configuration. - - Args: - execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the - parameters needed to fully describe the execution. - - Returns: - TransformProgram, ExecutionConfig: A transform program that when called returns QuantumTapes that the device - can natively execute as well as a postprocessing function to be called after execution, and a configuration with - unset specifications filled in. - - This device: - - * Supports any operations with explicit PennyLane to Qiskit gate conversions defined in the plugin - * Does not intrinsically support parameter broadcasting - - """ - config = execution_config - config.use_device_gradient = False - - transform_program = TransformProgram() - - transform_program.add_transform(validate_device_wires, self.wires, name=self.name) - transform_program.add_transform( - decompose, - stopping_condition=self.stopping_condition, - name=self.name, - skip_initial_state_prep=False, - ) - transform_program.add_transform( - validate_measurements, - sample_measurements=accepted_sample_measurement, - name=self.name, - ) - transform_program.add_transform( - validate_observables, - stopping_condition=self.observable_stopping_condition, - name=self.name, - ) - - transform_program.add_transform(broadcast_expand) - transform_program.add_transform(split_non_commuting) - - transform_program.add_transform(split_execution_types) - - return transform_program, config - - def _process_kwargs(self, kwargs): - """Processes kwargs given and separates them into kwargs and transpile_args. If given - a keyword argument 'options' that is a dictionary, a common practice in - Qiskit, the options in said dictionary take precedence over any overlapping keyword - arguments defined in the kwargs. - - Keyword Args: - kwargs (dict): keyword arguments that set either runtime options or transpilation - options. - - Returns: - kwargs, transpile_args: keyword arguments for the runtime options and keyword - arguments for the transpiler - """ - - if "noise_model" in kwargs: - noise_model = kwargs.pop("noise_model") - self.backend.set_options(noise_model=noise_model) - - if "options" in kwargs: - for key, val in kwargs.pop("options").items(): - if key in kwargs: - warnings.warn( - "An overlap between what was passed in via options and what was passed in via kwargs was found." - f"The value set in options {key}={val} will be used." - ) - kwargs[key] = val - - shots = kwargs.pop("shots") - - if "default_shots" in kwargs: - warnings.warn( - f"default_shots was found in the keyword arguments, but it is not supported by {self.name}" - "Please use the `shots` keyword argument instead. The number of shots " - f"{shots} will be used instead." - ) - kwargs["default_shots"] = shots - - kwargs, transpile_args = self.get_transpile_args(kwargs) - - return kwargs, transpile_args - - @staticmethod - def get_transpile_args(kwargs): - """The transpile argument setter. This separates keyword arguments related to transpilation - from the rest of the keyword arguments and removes those keyword arguments from kwargs. - - Keyword Args: - kwargs (dict): combined keyword arguments to be parsed for the Qiskit transpiler. For more details, see the - `Qiskit transpiler documentation `_ - - Returns: - kwargs (dict), transpile_args (dict): keyword arguments for the runtime options and keyword - arguments for the transpiler - """ - - transpile_sig = inspect.signature(transpile).parameters - - transpile_args = {arg: kwargs.pop(arg) for arg in transpile_sig if arg in kwargs} - transpile_args.pop("circuits", None) - transpile_args.pop("backend", None) - - return kwargs, transpile_args - - def compile_circuits(self, circuits): - """Compiles multiple circuits one after the other. - - Args: - circuits (list[QuantumCircuit]): the circuits to be compiled - - Returns: - list[QuantumCircuit]: the list of compiled circuits - """ - # Compile each circuit object - compiled_circuits = [] - transpile_args = self._transpile_args - - for i, circuit in enumerate(circuits): - compiled_circ = transpile(circuit, backend=self.compile_backend, **transpile_args) - compiled_circ.name = f"circ{i}" - compiled_circuits.append(compiled_circ) - - return compiled_circuits - - # pylint: disable=unused-argument, no-member - def execute( - self, - circuits: QuantumTape_or_Batch, - execution_config: ExecutionConfig = DefaultExecutionConfig, - ) -> Result_or_ResultBatch: - session = self._session or Session(backend=self.backend) - - results = [] - - if isinstance(circuits, QuantumScript): - circuits = [circuits] - - @contextmanager - def execute_circuits(session): - try: - for circ in circuits: - if circ.shots and len(circ.shots.shot_vector) > 1: - raise ValueError( - f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." - "Please use a single integer instead when specifying the number of shots." - ) - if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)) and getattr( - circ.measurements[0].obs, "pauli_rep", None - ): - execute_fn = self._execute_estimator - else: - execute_fn = self._execute_sampler - results.append(execute_fn(circ, session)) - yield results - finally: - session.close() - - with execute_circuits(session) as results: - return results - - def _execute_sampler(self, circuit, session): - """Returns the result of the execution of the circuit using the SamplerV2 Primitive. - Note that this result has been processed respective to the MeasurementProcess given. - E.g. `qml.expval` returns an expectation value whereas `qml.sample()` will return the raw samples. - - Args: - circuits (list[QuantumCircuit]): the circuits to be executed via SamplerV2 - session (Session): the session that the execution will be performed with - - Returns: - result (tuple): the processed result from SamplerV2 - """ - qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True)] - sampler = Sampler(session=session) - compiled_circuits = self.compile_circuits(qcirc) - sampler.options.update(**self._kwargs) - - # len(compiled_circuits) is always 1 so the indexing does not matter. - result = sampler.run( - compiled_circuits, - shots=circuit.shots.total_shots if circuit.shots.total_shots else None, - ).result()[0] - classical_register_name = compiled_circuits[0].cregs[0].name - self._current_job = getattr(result.data, classical_register_name) - - # needs processing function to convert to the correct format for states, and - # also handle instances where wires were specified in probs, and for multiple probs measurements - - self._samples = self.generate_samples(0) - res = [ - mp.process_samples(self._samples, wire_order=self.wires) for mp in circuit.measurements - ] - - single_measurement = len(circuit.measurements) == 1 - res = (res[0],) if single_measurement else tuple(res) - - return res - - def _execute_estimator(self, circuit, session): - """Returns the result of the execution of the circuit using the EstimatorV2 Primitive. - Note that this result has been processed respective to the MeasurementProcess given. - E.g. `qml.expval` returns an expectation value whereas `qml.var` will return the variance. - - Args: - circuits (list[QuantumCircuit]): the circuits to be executed via EstimatorV2 - session (Session): the session that the execution will be performed with - - Returns: - result (tuple): the processed result from EstimatorV2 - """ - # the Estimator primitive takes care of diagonalization and measurements itself, - # so diagonalizing gates and measurements are not included in the circuit - qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False)] - estimator = Estimator(session=session) - - pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] - compiled_circuits = self.compile_circuits(qcirc) - estimator.options.update(**self._kwargs) - # split into one call per measurement - # could technically be more efficient if there are some observables where we ask - # for expectation value and variance on the same observable, but spending time on - # that right now feels excessive - circ_and_obs = [(compiled_circuits[0], pauli_observables)] - result = estimator.run( - circ_and_obs, - precision=np.sqrt(1 / circuit.shots.total_shots) if circuit.shots else None, - ).result() - self._current_job = result - result = self._process_estimator_job(circuit.measurements, result) - - return result - - @staticmethod - def _process_estimator_job(measurements, job_result): - """Estimator returns the expectation value and standard error for each observable measured, - along with some metadata that contains the precision. Extracts the relevant number for each - measurement process and return the requested results from the Estimator executions. - - Note that for variance, we calculate the variance by using the standard error and the - precision value. - - Args: - measurements (list[MeasurementProcess]): the measurements in the circuit - job_result (Any): the result from EstimatorV2 - - Returns: - result (tuple): the processed result from EstimatorV2 - """ - expvals = job_result[0].data.evs - variances = (job_result[0].data.stds / job_result[0].metadata["target_precision"]) ** 2 - result = [] - for i, mp in enumerate(measurements): - if isinstance(mp, ExpectationMP): - result.append(expvals[i]) - elif isinstance(mp, VarianceMP): - result.append(variances[i]) - - single_measurement = len(measurements) == 1 - result = (result[0],) if single_measurement else tuple(result) - - return result - - def generate_samples(self, circuit=None): - r"""Returns the computational basis samples generated for all wires. - - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. - - Args: - circuit (int): position of the circuit in the batch. - - Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` - """ - counts = self._current_job.get_counts() - # Batch of circuits - if not isinstance(counts, dict): - counts = self._current_job.get_counts()[circuit] - - samples = [] - for key, value in counts.items(): - samples.extend([key] * value) - return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) diff --git a/pennylane_qiskit/qiskit_device_legacy.py b/pennylane_qiskit/qiskit_device_legacy.py new file mode 100644 index 000000000..d44a17d75 --- /dev/null +++ b/pennylane_qiskit/qiskit_device_legacy.py @@ -0,0 +1,491 @@ +# Copyright 2019-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +This module contains a base class for constructing Qiskit devices for PennyLane. +""" +# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init + + +import abc +import inspect +import warnings + +import numpy as np +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.compiler import transpile +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.providers import Backend, BackendV2, QiskitBackendNotFoundError + +from pennylane import QubitDevice, DeviceError +from pennylane.measurements import SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP + +from .converter import QISKIT_OPERATION_MAP +from ._version import __version__ + +SAMPLE_TYPES = (SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP) + + +def _get_backend_name(backend): + try: + return backend.name() # BackendV1 + except TypeError: # pragma: no cover + return backend.name # BackendV2 + + +class QiskitDeviceLegacy(QubitDevice, abc.ABC): + r"""Abstract Qiskit device for PennyLane. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['ancilla', 'q1', 'q2']``). + provider (Provider | None): The Qiskit backend provider. + backend (str | Backend): the desired backend. If a string, a provider must be given. + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. For state vector backends, + setting to ``None`` results in computing statistics like expectation values and variances analytically. + + Keyword Args: + 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. + """ + + name = "Qiskit PennyLane plugin" + pennylane_requires = ">=0.30.0" + version = __version__ + plugin_version = __version__ + author = "Xanadu" + + _capabilities = { + "model": "qubit", + "tensor_observables": True, + "inverse_operations": True, + } + _operation_map = QISKIT_OPERATION_MAP + _state_backends = { + "statevector_simulator", + "simulator_statevector", + "unitary_simulator", + "aer_simulator_statevector", + "aer_simulator_unitary", + } + """set[str]: Set of backend names that define the backends + that support returning the underlying quantum statevector""" + + operations = set(_operation_map.keys()) + observables = { + "PauliX", + "PauliY", + "PauliZ", + "Identity", + "Hadamard", + "Hermitian", + "Projector", + } + + analytic_warning_message = ( + "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." + ) + + _eigs = {} + + def __init__(self, wires, provider, backend, shots=1024, **kwargs): + + super().__init__(wires=wires, shots=shots) + + self.provider = provider + + if isinstance(backend, Backend): + self._backend = backend + self.backend_name = _get_backend_name(backend) + elif provider is None: + raise ValueError("Must pass a provider if the backend is not a Backend instance.") + else: + try: + self._backend = provider.get_backend(backend) + except QiskitBackendNotFoundError as e: + available_backends = list(map(_get_backend_name, provider.backends())) + raise ValueError( + f"Backend '{backend}' does not exist. Available backends " + f"are:\n {available_backends}" + ) from e + + self.backend_name = _get_backend_name(self._backend) + + # Keep track if the user specified analytic to be True + if shots is None and not self._is_state_backend: + # Raise a warning if no shots were specified for a hardware device + warnings.warn(self.analytic_warning_message.format(backend), UserWarning) + + self.shots = 1024 + + self._capabilities["returns_state"] = self._is_state_backend + + # Perform validation against backend + backend_qubits = ( + backend.num_qubits + if isinstance(backend, BackendV2) + else self.backend.configuration().n_qubits + ) + if backend_qubits and len(self.wires) > int(backend_qubits): + raise ValueError(f"Backend '{backend}' supports maximum {backend_qubits} wires") + + # Initialize inner state + self.reset() + + self.process_kwargs(kwargs) + + def process_kwargs(self, kwargs): + """Processing the keyword arguments that were provided upon device initialization. + + Args: + kwargs (dict): keyword arguments to be set for the device + """ + self.compile_backend = None + if "compile_backend" in kwargs: + self.compile_backend = kwargs.pop("compile_backend") + + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model") + self.backend.set_options(noise_model=noise_model) + + # set transpile_args + self.set_transpile_args(**kwargs) + + # Get further arguments for run + self.run_args = {} + + # Specify to have a memory for hw/hw simulators + compile_backend = self.compile_backend or self.backend + memory = str(compile_backend) not in self._state_backends + + if memory: + kwargs["memory"] = True + + # Consider the remaining kwargs as keyword arguments to run + self.run_args.update(kwargs) + + @property + def _is_state_backend(self): + """Returns whether this device has a state backend.""" + return self.backend_name in self._state_backends or self.backend.options.get("method") in { + "unitary", + "statevector", + } + + @property + def _is_statevector_backend(self): + """Returns whether this device has a statevector backend.""" + method = "statevector" + return method in self.backend_name or self.backend.options.get("method") == method + + @property + def _is_unitary_backend(self): + """Returns whether this device has a unitary backend.""" + method = "unitary" + return method in self.backend_name or self.backend.options.get("method") == method + + def set_transpile_args(self, **kwargs): + """The transpile argument setter. + + Keyword Args: + kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the + `Qiskit transpiler documentation `_ + """ + transpile_sig = inspect.signature(transpile).parameters + self.transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} + self.transpile_args.pop("circuits", None) + self.transpile_args.pop("backend", None) + + @property + def backend(self): + """The Qiskit backend object. + + Returns: + qiskit.providers.backend: Qiskit backend object. + """ + return self._backend + + def reset(self): + """Reset the Qiskit backend device""" + # Reset only internal data, not the options that are determined on + # device creation + self._reg = QuantumRegister(self.num_wires, "q") + self._creg = ClassicalRegister(self.num_wires, "c") + self._circuit = QuantumCircuit(self._reg, self._creg, name="temp") + + self._current_job = None + self._state = None # statevector of a simulator backend + + def create_circuit_object(self, operations, **kwargs): + """Builds the circuit objects based on the operations and measurements + specified to apply. + + Args: + operations (list[~.Operation]): operations to apply to the device + + Keyword args: + rotations (list[~.Operation]): Operations that rotate the circuit + pre-measurement into the eigenbasis of the observables. + """ + rotations = kwargs.get("rotations", []) + + applied_operations = self.apply_operations(operations) + + # Rotating the state for measurement in the computational basis + rotation_circuits = self.apply_operations(rotations) + applied_operations.extend(rotation_circuits) + + for circuit in applied_operations: + self._circuit &= circuit + + if not self._is_state_backend: + # Add measurements if they are needed + for qr, cr in zip(self._reg, self._creg): + self._circuit.measure(qr, cr) + elif "aer" in self.backend_name: + self._circuit.save_state() + + def apply(self, operations, **kwargs): + """Build the circuit object and apply the operations""" + self.create_circuit_object(operations, **kwargs) + + # These operations need to run for all devices + compiled_circuit = self.compile() + self.run(compiled_circuit) + + def apply_operations(self, operations): + """Apply the circuit operations. + + This method serves as an auxiliary method to :meth:`~.QiskitDevice.apply`. + + Args: + operations (List[pennylane.Operation]): operations to be applied + + Returns: + list[QuantumCircuit]: a list of quantum circuit objects that + specify the corresponding operations + """ + circuits = [] + + for operation in operations: + # Apply the circuit operations + device_wires = self.map_wires(operation.wires) + par = operation.parameters + + for idx, p in enumerate(par): + if isinstance(p, np.ndarray): + # Convert arrays so that Qiskit accepts the parameter + par[idx] = p.tolist() + + operation = operation.name + + mapped_operation = self._operation_map[operation] + + self.qubit_state_vector_check(operation) + + qregs = [self._reg[i] for i in device_wires.labels] + + if operation in ("QubitUnitary", "QubitStateVector", "StatePrep"): + # Need to revert the order of the quantum registers used in + # Qiskit such that it matches the PennyLane ordering + qregs = list(reversed(qregs)) + + if operation in ("Barrier",): + # Need to add the num_qubits for instantiating Barrier in Qiskit + par = [len(self._reg)] + + dag = circuit_to_dag(QuantumCircuit(self._reg, self._creg, name="")) + + gate = mapped_operation(*par) + + dag.apply_operation_back(gate, qargs=qregs) + circuit = dag_to_circuit(dag) + circuits.append(circuit) + + return circuits + + def qubit_state_vector_check(self, operation): + """Input check for the StatePrepBase operations. + + Args: + operation (pennylane.Operation): operation to be checked + + Raises: + DeviceError: If the operation is QubitStateVector or StatePrep + """ + if operation in ("QubitStateVector", "StatePrep"): + if self._is_unitary_backend: + raise DeviceError( + f"The {operation} operation " + "is not supported on the unitary simulator backend." + ) + + def compile(self): + """Compile the quantum circuit to target the provided compile_backend. + + If compile_backend is None, then the target is simply the + backend. + """ + compile_backend = self.compile_backend or self.backend + compiled_circuits = transpile(self._circuit, backend=compile_backend, **self.transpile_args) + return compiled_circuits + + def run(self, qcirc): + """Run the compiled circuit and query the result. + + Args: + qcirc (qiskit.QuantumCircuit): the quantum circuit to be run on the backend + """ + self._current_job = self.backend.run(qcirc, shots=self.shots, **self.run_args) + result = self._current_job.result() + + if self._is_state_backend: + self._state = self._get_state(result) + + def _get_state(self, result, experiment=None): + """Returns the statevector for state simulator backends. + + Args: + result (qiskit.Result): result object + experiment (str or None): the name of the experiment to get the state for. + + Returns: + array[float]: size ``(2**num_wires,)`` statevector + """ + if self._is_statevector_backend: + state = np.asarray(result.get_statevector(experiment)) + + elif self._is_unitary_backend: + unitary = np.asarray(result.get_unitary(experiment)) + initial_state = np.zeros([2**self.num_wires]) + initial_state[0] = 1 + + state = unitary @ initial_state + + # reverse qubit order to match PennyLane convention + return state.reshape([2] * self.num_wires).T.flatten() + + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. + + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. + + Args: + circuit (str or None): the name of the circuit to get the state for + + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + + # branch out depending on the type of backend + if self._is_state_backend: + # software simulator: need to sample from probabilities + return super().generate_samples() + + # hardware or hardware simulator + samples = self._current_job.result().get_memory(circuit) + # reverse qubit order to match PennyLane convention + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) + + @property + def state(self): + """Get state of the device""" + return self._state + + def analytic_probability(self, wires=None): + """Get the analytic probability of the device""" + if self._state is None: + return None + + prob = self.marginal_prob(np.abs(self._state) ** 2, wires) + return prob + + def compile_circuits(self, circuits): + r"""Compiles multiple circuits one after the other. + + Args: + circuits (list[.tapes.QuantumTape]): the circuits to be compiled + + Returns: + list[QuantumCircuit]: the list of compiled circuits + """ + # Compile each circuit object + compiled_circuits = [] + + for circuit in circuits: + # We need to reset the device here, else it will + # not start the next computation in the zero state + self.reset() + self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates) + + compiled_circ = self.compile() + compiled_circ.name = f"circ{len(compiled_circuits)}" + compiled_circuits.append(compiled_circ) + + return compiled_circuits + + def batch_execute(self, circuits, timeout: int = None): + """Batch execute the circuits on the device""" + + compiled_circuits = self.compile_circuits(circuits) + + if not compiled_circuits: + # At least one circuit must always be provided to the backend. + return [] + + # Send the batch of circuit objects using backend.run + self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args) + + try: + result = self._current_job.result(timeout=timeout) + except TypeError: # pragma: no cover + # timeout not supported + result = self._current_job.result() + + # increment counter for number of executions of qubit device + # pylint: disable=no-member + self._num_executions += 1 + + # Compute statistics using the state and/or samples + results = [] + for circuit, circuit_obj in zip(circuits, compiled_circuits): + # Update the tracker + if self.tracker.active: + self.tracker.update(executions=1, shots=self.shots) + self.tracker.record() + + if self._is_state_backend: + self._state = self._get_state(result, experiment=circuit_obj) + + # generate computational basis samples + if self.shots is not None or any( + isinstance(m, SAMPLE_TYPES) for m in circuit.measurements + ): + self._samples = self.generate_samples(circuit_obj) + + res = self.statistics(circuit) + single_measurement = len(circuit.measurements) == 1 + res = res[0] if single_measurement else tuple(res) + results.append(res) + + if self.tracker.active: + self.tracker.update(batches=1, batch_len=len(circuits)) + self.tracker.record() + + return results diff --git a/tests/test_base_device.py b/tests/test_base_device.py index 2fd3c2fe1..d958fda8e 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -34,8 +34,8 @@ from qiskit.providers import BackendV1, BackendV2 from qiskit import QuantumCircuit, transpile -from pennylane_qiskit.qiskit_device2 import ( - QiskitDevice2, +from pennylane_qiskit.qiskit_device import ( + QiskitDevice, qiskit_session, split_execution_types, ) @@ -117,7 +117,7 @@ def close(self): # This is just to appease a test mocked_backend = MockedBackend() legacy_backend = MockedBackendLegacy() aer_backend = AerSimulator() -test_dev = QiskitDevice2(wires=5, backend=aer_backend) +test_dev = QiskitDevice(wires=5, backend=aer_backend) class TestSupportForV1andV2: @@ -129,7 +129,7 @@ class TestSupportForV1andV2: ) def test_v1_and_v2_mocked(self, backend): """Test that device initializes with no error mocked""" - dev = QiskitDevice2(wires=10, backend=backend) + dev = QiskitDevice(wires=10, backend=backend) assert dev._backend == backend @pytest.mark.parametrize( @@ -141,7 +141,7 @@ def test_v1_and_v2_mocked(self, backend): ) def test_v1_and_v2_manila(self, backend, shape): """Test that device initializes and runs without error with V1 and V2 backends by Qiskit""" - dev = QiskitDevice2(wires=5, backend=backend) + dev = QiskitDevice(wires=5, backend=backend) @qml.qnode(dev) def circuit(x): @@ -163,8 +163,8 @@ def test_compile_backend_kwarg(self): compile_backend = MockedBackend(name="compile_backend") main_backend = MockedBackend(name="main_backend") - dev1 = QiskitDevice2(wires=5, backend=main_backend) - dev2 = QiskitDevice2(wires=5, backend=main_backend, compile_backend=compile_backend) + dev1 = QiskitDevice(wires=5, backend=main_backend) + dev2 = QiskitDevice(wires=5, backend=main_backend, compile_backend=compile_backend) assert dev1._compile_backend == dev1._backend == main_backend @@ -179,7 +179,7 @@ def test_no_shots_warns_and_defaults(self): UserWarning, match="Expected an integer number of shots, but received shots=None", ): - dev = QiskitDevice2(wires=2, backend=aer_backend, shots=None) + dev = QiskitDevice(wires=2, backend=aer_backend, shots=None) assert dev.shots.total_shots == 1024 @@ -189,15 +189,15 @@ def test_backend_wire_validation(self, backend): the number of wires available on the backend, for both backend versions""" with pytest.raises(ValueError, match="supports maximum"): - QiskitDevice2(wires=500, backend=backend) + QiskitDevice(wires=500, backend=backend) def test_setting_simulator_noise_model(self): """Test that the simulator noise model saved on a passed Options object is used to set the backend noise model""" new_backend = MockedBackend() - dev1 = QiskitDevice2(wires=3, backend=aer_backend) - dev2 = QiskitDevice2(wires=3, backend=new_backend, noise_model={"placeholder": 1}) + dev1 = QiskitDevice(wires=3, backend=aer_backend) + dev2 = QiskitDevice(wires=3, backend=new_backend, noise_model={"placeholder": 1}) assert dev1.backend.options.noise_model is None assert dev2.backend.options.noise_model == {"placeholder": 1} @@ -210,7 +210,7 @@ class TestQiskitSessionManagement: def test_default_no_session_on_initialization(self, backend): """Test that the default behaviour is no session at initialization""" - dev = QiskitDevice2(wires=2, backend=backend) + dev = QiskitDevice(wires=2, backend=backend) assert dev._session is None @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) @@ -218,15 +218,15 @@ def test_initializing_with_session(self, backend): """Test that you can initialize a device with an existing Qiskit session""" session = MockSession(backend=backend, max_time="1m") - dev = QiskitDevice2(wires=2, backend=backend, session=session) + dev = QiskitDevice(wires=2, backend=backend, session=session) assert dev._session == session - @patch("pennylane_qiskit.qiskit_device2.Session") + @patch("pennylane_qiskit.qiskit_device.Session") @pytest.mark.parametrize("initial_session", [None, MockSession(aer_backend)]) def test_using_session_context(self, mock_session, initial_session): """Test that you can add a session within a context manager""" - dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -238,7 +238,7 @@ def test_using_session_context(self, mock_session, initial_session): def test_using_session_context_options(self): """Test that you can set session options using qiskit_session""" - dev = QiskitDevice2(wires=2, backend=aer_backend) + dev = QiskitDevice(wires=2, backend=aer_backend) assert dev._session is None @@ -254,7 +254,7 @@ def test_error_when_passing_unexpected_kwarg(self): Qiskit allows for more customization we can automatically accomodate those needs. Right now there are no such keyword arguments, so an error on Qiskit's side is raised.""" - dev = QiskitDevice2(wires=2, backend=aer_backend) + dev = QiskitDevice(wires=2, backend=aer_backend) assert dev._session is None @@ -269,7 +269,7 @@ def test_error_when_passing_unexpected_kwarg(self): def test_no_warning_when_using_initial_session_options(self): initial_session = Session(backend=aer_backend, max_time=30) - dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -288,7 +288,7 @@ def test_warnings_when_overriding_session_context_options(self, recorder): default options, passed in from the `qiskit_session` take precedence, barring `backend` or `service`""" initial_session = Session(backend=aer_backend) - dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session @@ -314,7 +314,7 @@ def test_warnings_when_overriding_session_context_options(self, recorder): assert dev._session == initial_session max_time_session = Session(backend=aer_backend, max_time=60) - dev = QiskitDevice2(wires=2, backend=aer_backend, session=max_time_session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=max_time_session) with qiskit_session(dev, max_time=30) as session: assert dev._session == session assert dev._session != initial_session @@ -328,7 +328,7 @@ def test_warnings_when_overriding_session_context_options(self, recorder): def test_update_session(self, initial_session): """Test that you can update the session stored on the device""" - dev = QiskitDevice2(wires=2, backend=aer_backend, session=initial_session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) assert dev._session == initial_session new_session = MockSession(backend=aer_backend, max_time="1m") @@ -519,7 +519,7 @@ def test_observable_stopping_condition(self, obs, expected): def test_preprocess_split_non_commuting(self, measurements, num_tapes): """Test that `split_non_commuting` works as expected in the preprocess function.""" - dev = QiskitDevice2(wires=5, backend=aer_backend) + dev = QiskitDevice(wires=5, backend=aer_backend) qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) program, _ = dev.preprocess() @@ -542,7 +542,7 @@ def test_preprocess_splits_incompatible_primitive_measurements(self, measurement on measurement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements are another type (Sampler).""" - dev = QiskitDevice2(wires=5, backend=aer_backend) + dev = QiskitDevice(wires=5, backend=aer_backend) qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) program, _ = dev.preprocess() @@ -591,20 +591,20 @@ def test_warning_if_shots(self): UserWarning, match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=aer_backend, default_shots=333) + dev = QiskitDevice(wires=2, backend=aer_backend, default_shots=333) # Qiskit takes in `default_shots` to define the # of shots, therefore we use # the kwarg "default_shots" rather than shots to pass it to Qiskit. assert dev._kwargs["default_shots"] == 1024 - dev = QiskitDevice2(wires=2, backend=aer_backend, shots=200) + dev = QiskitDevice(wires=2, backend=aer_backend, shots=200) assert dev._kwargs["default_shots"] == 200 with pytest.warns( UserWarning, match="default_shots was found in the keyword arguments", ): - dev = QiskitDevice2(wires=2, backend=aer_backend, options={"default_shots": 30}) + dev = QiskitDevice(wires=2, backend=aer_backend, options={"default_shots": 30}) # resets to default since we reinitialize the device assert dev._kwargs["default_shots"] == 1024 @@ -615,7 +615,7 @@ def test_warning_if_options_and_kwargs_overlap(self): UserWarning, match="An overlap between", ): - dev = QiskitDevice2( + dev = QiskitDevice( wires=2, backend=aer_backend, options={"resilience_level": 1, "optimization_level": 1}, @@ -642,7 +642,7 @@ def test_options_and_kwargs_combine_into_unified_kwargs(self, backend): """Test that options set via the keyword argument options and options set via kwargs will combine into a single unified kwargs that is passed to the device""" - dev = QiskitDevice2( + dev = QiskitDevice( wires=5, backend=backend, options={"resilience_level": 1}, @@ -666,7 +666,7 @@ def test_no_error_is_raised_if_transpilation_options_are_passed(self, backend): """Tests that when transpilation options are passed in, they are properly handled without error""" - dev = QiskitDevice2( + dev = QiskitDevice( wires=5, backend=backend, options={"resilience_level": 1, "optimization_level": 1}, @@ -692,7 +692,7 @@ def circuit(): class TestDeviceProperties: def test_name_property(self): """Test the backend property""" - assert test_dev.name == "QiskitDevice2" + assert test_dev.name == "QiskitDevice" def test_backend_property(self): """Test the backend property""" @@ -704,7 +704,7 @@ def test_compile_backend_property(self, backend): """Test the compile_backend property""" compile_backend = MockedBackend(name="compile_backend") - dev = QiskitDevice2(wires=5, backend=backend, compile_backend=compile_backend) + dev = QiskitDevice(wires=5, backend=backend, compile_backend=compile_backend) assert dev.compile_backend == dev._compile_backend assert dev.compile_backend == compile_backend @@ -717,7 +717,7 @@ def test_session_property(self): """Test the session property""" session = MockSession(backend=aer_backend) - dev = QiskitDevice2(wires=2, backend=aer_backend, session=session) + dev = QiskitDevice(wires=2, backend=aer_backend, session=session) assert dev.session == dev._session assert dev.session == session @@ -725,7 +725,7 @@ def test_num_wires_property(self): """Test the num_wires property""" wires = [1, 2, 3] - dev = QiskitDevice2(wires=wires, backend=aer_backend) + dev = QiskitDevice(wires=wires, backend=aer_backend) assert dev.num_wires == len(wires) @@ -742,7 +742,7 @@ def test_get_transpile_args(self): "circuits": [], } compile_backend = MockedBackend(name="compile_backend") - dev = QiskitDevice2( + dev = QiskitDevice( wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args ) assert dev._transpile_args == { @@ -750,14 +750,14 @@ def test_get_transpile_args(self): "seed_transpiler": 42, } - @patch("pennylane_qiskit.qiskit_device2.transpile") + @patch("pennylane_qiskit.qiskit_device.transpile") @pytest.mark.parametrize("compile_backend", [None, MockedBackend(name="compile_backend")]) def test_compile_circuits(self, transpile_mock, compile_backend): """Tests compile_circuits with a mocked transpile function to avoid calling a remote backend. Confirm compile_backend and transpile_args are used.""" transpile_args = {"seed_transpiler": 42, "optimization_level": 2} - dev = QiskitDevice2( + dev = QiskitDevice( wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args ) @@ -815,18 +815,18 @@ def get_counts(): assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == results_dict["10"] assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == results_dict["01"] - @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._execute_estimator") + @patch("pennylane_qiskit.qiskit_device.QiskitDevice._execute_estimator") def test_execute_pipeline_primitives_no_session(self, mocker): """Test that a Primitives-based device initialized with no Session creates one for the execution, and then returns the device session to None.""" - dev = QiskitDevice2(wires=5, backend=aer_backend, session=None) + dev = QiskitDevice(wires=5, backend=aer_backend, session=None) assert dev._session is None qs = QuantumScript([qml.PauliX(0), qml.PauliY(1)], measurements=[qml.expval(qml.PauliZ(0))]) - with patch("pennylane_qiskit.qiskit_device2.Session") as mock_session: + with patch("pennylane_qiskit.qiskit_device.Session") as mock_session: dev.execute(qs) mock_session.assert_called_once() # a session was created @@ -837,7 +837,7 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): """Test that a device executes measurements that require raw samples via the sampler, and the relevant primitive measurements via the estimator""" - dev = QiskitDevice2(wires=5, backend=backend, session=MockSession(backend)) + dev = QiskitDevice(wires=5, backend=backend, session=MockSession(backend)) qs = QuantumScript( [qml.PauliX(0), qml.PauliY(1)], @@ -865,8 +865,8 @@ def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): "sampler_execute_res", ] - @patch("pennylane_qiskit.qiskit_device2.Estimator") - @patch("pennylane_qiskit.qiskit_device2.QiskitDevice2._process_estimator_job") + @patch("pennylane_qiskit.qiskit_device.Estimator") + @patch("pennylane_qiskit.qiskit_device.QiskitDevice._process_estimator_job") @pytest.mark.parametrize("session", [None, MockSession(aer_backend)]) def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, session): """Test the _execute_estimator function using a mocked version of Estimator @@ -885,7 +885,7 @@ def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, ses def test_shot_vector_error_mocked(self): """Test that a device that executes a circuit with an array of shots raises the appropriate ValueError""" - dev = QiskitDevice2(wires=5, backend=aer_backend, session=MockSession(aer_backend)) + dev = QiskitDevice(wires=5, backend=aer_backend, session=MockSession(aer_backend)) qs = QuantumScript( measurements=[ qml.expval(qml.PauliX(0)), @@ -919,7 +919,7 @@ def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expec correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) """ - dev = QiskitDevice2(wires=5, backend=aer_backend) + dev = QiskitDevice(wires=5, backend=aer_backend) sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -978,7 +978,7 @@ def test_estimator_with_various_multi_qubit_pauli_obs( """ pl_dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) - dev = QiskitDevice2(wires=[0, 1, 2, 3], backend=aer_backend) + dev = QiskitDevice(wires=[0, 1, 2, 3], backend=aer_backend) sampler_execute = mocker.spy(dev, "_execute_sampler") estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1002,7 +1002,7 @@ def test_estimator_with_various_multi_qubit_pauli_obs( def test_tape_shots_used_for_estimator(self, mocker): """Tests that device uses tape shots rather than device shots for estimator""" - dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) estimator_execute = mocker.spy(dev, "_execute_estimator") @@ -1068,7 +1068,7 @@ def test_process_estimator_job(self, measurements, expectation): assert isinstance(result[0].metadata, dict) - processed_result = QiskitDevice2._process_estimator_job(qs.measurements, result) + processed_result = QiskitDevice._process_estimator_job(qs.measurements, result) assert isinstance(processed_result, tuple) assert np.allclose(processed_result, expectation, atol=0.1) @@ -1076,7 +1076,7 @@ def test_process_estimator_job(self, measurements, expectation): @pytest.mark.parametrize("num_shots", [50, 100]) def test_generate_samples(self, num_wires, num_shots): qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) - dev = QiskitDevice2(wires=num_wires, backend=aer_backend, shots=num_shots) + dev = QiskitDevice(wires=num_wires, backend=aer_backend, shots=num_shots) dev._execute_sampler(circuit=qs, session=Session(backend=aer_backend)) samples = dev.generate_samples(0) @@ -1099,7 +1099,7 @@ def test_generate_samples(self, num_wires, num_shots): def test_tape_shots_used_for_sampler(self, mocker): """Tests that device uses tape shots rather than device shots for sampler""" - dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) sampler_execute = mocker.spy(dev, "_execute_sampler") @@ -1119,7 +1119,7 @@ def circuit(): def test_error_for_shot_vector(self): """Tests that a ValueError is raised if a shot vector is passed.""" - dev = QiskitDevice2(wires=5, backend=aer_backend, shots=2) + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) @qml.qnode(dev) def circuit(): @@ -1147,7 +1147,7 @@ def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): provides an accurate answer for measurements with observables that don't have a pauli_rep. """ - dev = QiskitDevice2(wires=5, backend=aer_backend) + dev = QiskitDevice(wires=5, backend=aer_backend) pl_dev = qml.device("default.qubit", wires=5) @@ -1178,7 +1178,7 @@ def test_warning_for_split_execution_types_when_observable_no_pauli(self): """Test that a warning is raised when device is passed a measurement on an observable that does not have a pauli_rep.""" - dev = QiskitDevice2(wires=5, backend=aer_backend) + dev = QiskitDevice(wires=5, backend=aer_backend) @qml.qnode(dev) def circuit(): @@ -1198,7 +1198,7 @@ def test_qiskit_probability_output_format(self, backend): the same as pennylane's.""" dev = qml.device("default.qubit", wires=[0, 1, 2, 3, 4]) - qiskit_dev = QiskitDevice2(wires=[0, 1, 2, 3, 4], backend=backend) + qiskit_dev = QiskitDevice(wires=[0, 1, 2, 3, 4], backend=backend) @qml.qnode(dev) def circuit(): @@ -1220,7 +1220,7 @@ def test_sampler_output_shape(self, backend): """Test that the shape of the results produced from the sampler for the Qiskit device is consistent with Pennylane""" dev = qml.device("default.qubit", wires=5, shots=1024) - qiskit_dev = QiskitDevice2(wires=5, backend=backend) + qiskit_dev = QiskitDevice(wires=5, backend=backend) @qml.qnode(dev) def circuit(x): @@ -1244,7 +1244,7 @@ def test_sampler_output_shape_multi_measurements(self, backend): """Test that the shape of the results produced from the sampler for the Qiskit device is consistent with Pennylane for circuits with multiple measurements""" dev = qml.device("default.qubit", wires=5, shots=10) - qiskit_dev = QiskitDevice2(wires=5, backend=backend, shots=10) + qiskit_dev = QiskitDevice(wires=5, backend=backend, shots=10) @qml.qnode(dev) def circuit(x): @@ -1327,7 +1327,7 @@ def test_observables_that_need_split_non_commuting(self, observable): """Tests that observables that have non-commuting measurements are processed correctly when executed by the Estimator or, in the case of qml.Hadamard, executed by the Sampler via expval() or var""" - qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=30000) + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=30000) @qml.qnode(qiskit_dev) def qiskit_circuit(): @@ -1362,7 +1362,7 @@ def circuit(): def test_observables_that_need_split_non_commuting_counts(self, observable): """Tests that observables that have non-commuting measurents are processed correctly when executed by the Sampler via counts()""" - qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=4000) + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=4000) @qml.qnode(qiskit_dev) def qiskit_circuit(): @@ -1424,7 +1424,7 @@ def circuit(): def test_observables_that_need_split_non_commuting_samples(self, observable): """Tests that observables that have non-commuting measurents are processed correctly when executed by the Sampler via sample()""" - qiskit_dev = QiskitDevice2(wires=3, backend=aer_backend, shots=20000) + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=20000) @qml.qnode(qiskit_dev) def qiskit_circuit(): diff --git a/tests/test_integration.py b/tests/test_integration.py index 39d7f93f3..1e1d3db5d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -23,42 +23,29 @@ import pennylane as qml from pennylane.numpy import tensor -from semantic_version import Version import pytest import qiskit import qiskit_aer from qiskit.providers import QiskitBackendNotFoundError -from pennylane_qiskit.qiskit_device import QiskitDevice +from qiskit.providers.basic_provider import BasicProvider +from pennylane_qiskit.qiskit_device_legacy import QiskitDeviceLegacy # pylint: disable=protected-access, unused-argument, ungrouped-imports, too-many-arguments, too-few-public-methods -if Version(qiskit.__version__) < Version("1.0.0"): - pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicaer", qiskit.BasicAer)] - def check_provider_backend_compatibility(pldevice, backend_name): - """check compatibility of provided backend""" - dev_name, _ = pldevice - if (dev_name == "qiskit.aer" and "aer" not in backend_name) or ( - dev_name == "qiskit.basicaer" and "aer" in backend_name - ): - return (False, "Only the AerSimulator is supported on AerDevice") - return True, None - -else: - from qiskit.providers.basic_provider import BasicProvider +pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicsim", BasicProvider())] - pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicsim", BasicProvider())] - def check_provider_backend_compatibility(pldevice, backend_name): - """check compatibility of provided backend""" - dev_name, _ = pldevice - if dev_name == "qiskit.aer" and backend_name == "basic_simulator": - return (False, "basic_simulator is not supported on the AerDevice") +def check_provider_backend_compatibility(pldevice, backend_name): + """Check the compatibility of provided backend""" + dev_name, _ = pldevice + if dev_name == "qiskit.aer" and backend_name == "basic_simulator": + return (False, "basic_simulator is not supported on the AerDevice") - if dev_name == "qiskit.basicsim" and backend_name != "basic_simulator": - return (False, "Only the basic_simulator backend works with the BasicSimulatorDevice") - return True, None + if dev_name == "qiskit.basicsim" and backend_name != "basic_simulator": + return (False, "Only the basic_simulator backend works with the BasicSimulatorDevice") + return True, None class TestDeviceIntegration: @@ -78,7 +65,6 @@ def test_load_device(self, d, backend): assert dev.shots == 1024 assert dev.short_name == d[0] assert dev.provider == d[1] - assert dev.capabilities()["returns_state"] == (backend in state_backends) @pytest.mark.parametrize("d", pldevices) def test_load_remote_device_with_backend_instance(self, d, backend): @@ -90,32 +76,18 @@ def test_load_remote_device_with_backend_instance(self, d, backend): except QiskitBackendNotFoundError: pytest.skip("Backend is not compatible with specified device") - dev = qml.device("qiskit.remote", wires=2, backend=backend_instance, shots=1024) - assert dev.num_wires == 2 - assert dev.shots == 1024 - assert dev.short_name == "qiskit.remote" - assert dev.provider is None - assert dev.capabilities()["returns_state"] == (backend in state_backends) - - @pytest.mark.parametrize("d", pldevices) - def test_load_remote_device_by_name(self, d, backend): - """Test that the qiskit.remote device loads correctly when passed a provider and a backend - name. This test is equivalent to `test_load_device` but on the qiskit.remote device instead - of specialized devices that expose more configuration options.""" - - # check compatibility between provider and backend, and skip if incompatible - is_compatible, failure_msg = check_provider_backend_compatibility(d, backend) - if not is_compatible: - pytest.skip(failure_msg) - - _, provider = d + if backend_instance.configuration().n_qubits is None: + pytest.skip("No qubits?") - dev = qml.device("qiskit.remote", wires=2, provider=provider, backend=backend, shots=1024) - assert dev.num_wires == 2 - assert dev.shots == 1024 + dev = qml.device( + "qiskit.remote", + wires=backend_instance.configuration().n_qubits, + backend=backend_instance, + shots=1024, + ) + assert dev.num_wires == backend_instance.configuration().n_qubits + assert dev.shots.total_shots == 1024 assert dev.short_name == "qiskit.remote" - assert dev.provider == provider - assert dev.capabilities()["returns_state"] == (backend in state_backends) def test_incorrect_backend(self): """Test that exception is raised if name is incorrect""" @@ -129,12 +101,6 @@ def test_incorrect_backend_wires(self): ): qml.device("qiskit.aer", wires=100, method="statevector") - def test_remote_device_no_provider(self): - """Test that the qiskit.remote device raises a ValueError if passed a backend - by name but no provider to look up the name on.""" - with pytest.raises(ValueError, match=r"Must pass a provider"): - qml.device("qiskit.remote", wires=2, backend="aer_simulator_statevector") - def test_args(self): """Test that the device requires correct arguments""" with pytest.raises(TypeError, match="missing 1 required positional argument"): @@ -591,7 +557,7 @@ def test_one_qubit_circuit_batch_params(self, shots, d, backend, tol, mocker): b = np.linspace(0, 0.123, batch_dim) c = np.linspace(0, 0.987, batch_dim) - spy1 = mocker.spy(QiskitDevice, "batch_execute") + spy1 = mocker.spy(QiskitDeviceLegacy, "batch_execute") spy2 = mocker.spy(dev.backend, "run") @partial(qml.batch_params, all_operations=True) @@ -605,7 +571,7 @@ def circuit(x, y, z): assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - # Check that QiskitDevice.batch_execute was called + # Check that QiskitDeviceLegacy.batch_execute was called assert spy1.call_count == 1 assert spy2.call_count == 1 @@ -625,7 +591,7 @@ def test_batch_execute_parameter_shift(self, shots, d, backend, tol, mocker): dev = qml.device(d[0], wires=3, backend=backend, shots=shots) - spy1 = mocker.spy(QiskitDevice, "batch_execute") + spy1 = mocker.spy(QiskitDeviceLegacy, "batch_execute") spy2 = mocker.spy(dev.backend, "run") @qml.qnode(dev, diff_method="parameter-shift") @@ -642,7 +608,7 @@ def 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 twice + # Check that QiskitDeviceLegacy.batch_execute was called twice assert spy1.call_count == 2 # Check that run was called twice: for the partial derivatives and for diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 7eabcf9ca..8674301da 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -25,7 +25,7 @@ import pennylane as qml from pennylane_qiskit import AerDevice -from pennylane_qiskit.qiskit_device import QiskitDevice +from pennylane_qiskit.qiskit_device_legacy import QiskitDeviceLegacy # pylint: disable=protected-access, unused-argument, too-few-public-methods @@ -109,7 +109,7 @@ class TestSupportForV1andV2: ) def test_v1_and_v2_mocked(self, dev_backend): """Test that device initializes with no error mocked""" - dev = qml.device("qiskit.remote", wires=10, backend=dev_backend, use_primitives=True) + dev = qml.device("qiskit.aer", wires=10, backend=dev_backend) assert dev._backend == dev_backend @pytest.mark.parametrize( @@ -121,7 +121,7 @@ def test_v1_and_v2_mocked(self, dev_backend): ) def test_v1_and_v2_manila(self, dev_backend): """Test that device initializes with no error with V1 and V2 backends by Qiskit""" - dev = qml.device("qiskit.remote", wires=5, backend=dev_backend, use_primitives=True) + dev = qml.device("qiskit.aer", wires=5, backend=dev_backend) @qml.qnode(dev) def circuit(x): @@ -237,12 +237,12 @@ def test_calls_to_execute(self, device, n_tapes, mocker): called and not the general execute method.""" dev = device(2) - spy = mocker.spy(QiskitDevice, "execute") + spy = mocker.spy(QiskitDeviceLegacy, "execute") tapes = [self.tape1] * n_tapes dev.batch_execute(tapes) - # Check that QiskitDevice.execute was not called + # Check that QiskitDeviceLegacyLegacy.execute was not called assert spy.call_count == 0 @pytest.mark.parametrize("n_tapes", [1, 2, 3]) @@ -251,7 +251,7 @@ def test_calls_to_reset(self, n_tapes, mocker, device): times.""" dev = device(2) - spy = mocker.spy(QiskitDevice, "reset") + spy = mocker.spy(QiskitDeviceLegacy, "reset") tapes = [self.tape1] * n_tapes dev.batch_execute(tapes) From 23533e0bab13f4ba8a2120e74db9a9aa9178b0df Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:35:43 -0400 Subject: [PATCH 44/47] Plugin page update (#563) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * [skip ci] docstrings for converter functions * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * [skip ci] examples of QiskitDevice2 added to docstring * docstring changes * docstring * more docstrings * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * docstrings * docstrings * formatting * changes * some examples * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * examples and links * change inheritance for remote device * build docs * better docs * fix docs a little * remove redundant docstrings * revert * import fix * prelim changes to aer * changes to build sphinx * revert change * delete section on ibmq devices * add examples for remote.rst * plugin updates to the remote device and basicsim * doc fixes for sphinx build * docs * small fi * fix weird spacing * [skip ci] small fix * [skip ci] error in codeblock * delete extra the * [skip ci] changelog * merge confs * formatting * black * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * address comments * undo * readme * plugin page fixes * fixed documentation * fix * Update doc/index.rst Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * doc changes * typo * weird change didn't go through * change to doc * small change * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * change name of iqp token * small fix --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- CHANGELOG.md | 4 ++ README.rst | 20 ++----- doc/devices/aer.rst | 43 +++++++------- doc/devices/basicsim.rst | 3 +- doc/devices/remote.rst | 118 ++++++++++++++++++++++++++++++++++++--- doc/index.rst | 38 ++++++++++--- 6 files changed, 173 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b10d7a7b..239fb6a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ ### Documentation 📝 +* The Pennylane-Qiskit plugin page has been updated to reflect the changes in both the plugin's +capabilities and Qiskit. + [#563](https://github.com/PennyLaneAI/pennylane-qiskit/pull/563) + ### Bug fixes 🐛 ### Contributors ✍️ diff --git a/README.rst b/README.rst index fe3651891..eab139fab 100644 --- a/README.rst +++ b/README.rst @@ -68,21 +68,11 @@ To test that the PennyLane-Qiskit plugin is working correctly you can run in the source folder. -.. note:: - - Tests on the `IBMQ device `_ can - 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. At - the time of writing this means that the test are "free". - Please verify that this is also the case for your account. +.. warning:: + + When installing the Pennylane-Qiskit plugin, we recommend starting with a clean environment. + This is especially pertinent when upgrading from a pre-1.0 version of Qiskit, as described + in `Qiskit's migration guide `_. .. installation-end-inclusion-marker-do-not-remove diff --git a/doc/devices/aer.rst b/doc/devices/aer.rst index 2f39c6cf0..1dd332691 100644 --- a/doc/devices/aer.rst +++ b/doc/devices/aer.rst @@ -27,24 +27,19 @@ parameters would look like: qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(wires=1)) -You can then execute the circuit like any other function to get the quantum mechanical expectation value. +You can then execute the circuit like any other function to get the expectation value of a Pauli +operator. .. code-block:: python circuit(0.2, 0.1, 0.3) -Backends -~~~~~~~~ +Backend Methods and Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~ The default backend is the ``AerSimulator``. However, multiple other backends are also available. To get a current overview what backends are available you can query -.. code-block:: python - - dev.capabilities()['backend'] - -or, alternatively, - .. code-block:: python from qiskit_aer import Aer @@ -58,18 +53,28 @@ You can change a ``'qiskit.aer'`` device's backend with the ``backend`` argument .. code-block:: python - dev = qml.device('qiskit.aer', wires=2, backend='aer_simulator_statevector') + from qiskit_aer import UnitarySimulator + dev = qml.device('qiskit.aer', wires=2, backend=UnitarySimulator()) -Backend Methods and Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: + + Occassionally, you may see others pass in a string as a backend. For example: + + .. code-block:: python + + dev = qml.device('qiskit.aer', wires=2, backend='unitary_simulator') + + At the time of writing, this is still functional for the Aer devices. However, this will soon be + deprecated and may not function as intended. To ensure accurate results, we recommend passing in + a backend instance. -This ``AerSimulator`` backend has several available methods, which +The ``AerSimulator`` backend has several available methods, which can be passed via the ``method`` keyword argument. For example ``'automatic'``, ``'statevector'``, and ``'unitary'``. .. code-block:: python - dev = qml.device("qiskit.aer", wires=2, method="automatic") + dev = qml.device("qiskit.aer", wires=2, backend=AerSimulator(), method="automatic") Each of these methods can take different *run options*, for example to specify the numerical precision of the simulation. @@ -81,7 +86,7 @@ The options are set via additional keyword arguments: dev = qml.device( 'qiskit.aer', wires=2, - backend='unitary_simulator', + backend=AerSimulator(), validation_threshold=1e-6 ) @@ -96,9 +101,9 @@ documentation `_ qiskit tutorial): +One great feature of the ``'qiskit.aer'`` device is the ability to simulate noise. There are +different noise models, which you can instantiate and apply to the device as follows (adapted +from a `Qiskit tutorial `_.): .. code-block:: python @@ -136,4 +141,4 @@ which you can instantiate and apply to the device as follows print(circuit(0.2, 0.1, 0.3)) Please refer to the Qiskit documentation for more information on -`noise models `_. +`noise models `_. diff --git a/doc/devices/basicsim.rst b/doc/devices/basicsim.rst index cd4060b0b..21c7dad5b 100644 --- a/doc/devices/basicsim.rst +++ b/doc/devices/basicsim.rst @@ -19,6 +19,5 @@ This device uses the Qiskit ``BasicSimulator`` backend from the The `Qiskit Aer `_ device provides a fast simulator that is also capable of simulating - noise. It is available as :ref:`"qiskit.aer" `, but the backend must be - installed separately with ``pip install qiskit-aer``. + noise. It is available as :ref:`"qiskit.aer" `. \ No newline at end of file diff --git a/doc/devices/remote.rst b/doc/devices/remote.rst index f2b81b799..a2a04b476 100644 --- a/doc/devices/remote.rst +++ b/doc/devices/remote.rst @@ -4,17 +4,117 @@ The Remote device The ``'qiskit.remote'`` device is a generic adapter to use any Qiskit backend as interface for a PennyLane device. -This device is useful when retrieving backends from providers with complex search options in -their ``get_backend()`` method, or for setting options on a backend prior to wrapping it as -PennyLane device. +To access IBM backends, we recommend using `Qiskit Runtime `_. .. code-block:: python - import pennylane as qml + from qiskit_ibm_runtime import QiskitRuntimeService - def configured_backend(): - backend = SomeProvider.get_backend(...) - backend.options.update_options(...) - return backend + QiskitRuntimeService.save_account(channel="ibm_quantum", token="") - dev = qml.device('qiskit.remote', wires=2, backend=configured_backend()) + # To access saved credentials for the IBM quantum channel and select an instance + service = QiskitRuntimeService(channel="ibm_quantum", instance="my_hub/my_group/my_project") + backend = service.least_busy(operational=True, simulator=False, min_num_qubits=) + + dev = qml.device('qiskit.remote', wires=, backend=backend) + + +.. note:: + + Certain third-party backends may be using the deprecated ``Provider`` interface, in which case + you can get the backend instance from providers with complex search options using their + ``get_backend()`` method. For example: + + .. code-block:: python + + import pennylane as qml + + def configured_backend(): + backend = SomeProvider.get_backend(...) + backend.options.update_options(...) # Set backend options this way + return backend + + dev = qml.device('qiskit.remote', wires=2, backend=configured_backend()) + +After installing the plugin, this device can be used just like any other PennyLane device for defining and evaluating QNodes. +For example, a simple quantum function that returns the expectation value of a measurement and depends on +three classical input parameters can be decorated with ``qml.qnode`` as usual to construct a ``QNode``: + +.. code-block:: python + + @qml.qnode(dev) + def circuit(x, y, z): + qml.RZ(z, wires=[0]) + qml.RY(y, wires=[0]) + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(wires=1)) + +You can then execute the above quantum circuit to get the expectation value of a Pauli operator. + +.. code-block:: python + + circuit(0.2, 0.1, 0.3) + +The ``'qiskit.remote'`` device also supports the use of `local simulators `_ such as ``FakeManila``. + +.. code-block:: python + + from qiskit_ibm_runtime.fake_provider import FakeManilaV2 + backend = FakeManilaV2() + + # You could use an Aer simulator instead by using the following code: + # from qiskit_aer import AerSimulator + # backend = AerSimulator() + + dev = qml.device('qiskit.remote', wires=5, backend=backend) + +Device options +~~~~~~~~~~~~~~ + +The ``'qiskit.remote'`` device uses the `EstimatorV2 `_ +and the `SamplerV2 `_ runtime primitives to execute +the measurements. To set options for `transpilation `_ +or `runtime `_, simply pass the keyword arguments into the device. + +.. code-block:: python + + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=1, + seed_transpiler=42 + ) + # to change options, re-initialize the device + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=2, + seed_transpiler=24 + ) + +This device is not compatible with analytic mode, so an error will be raised if ``shots=0`` or ``shots=None``. +The default value of the shots argument is ``1024``. You can set the number of shots on device initialization using the +``shots`` keyword, or you can choose the number of shots on circuit execution. + +.. code-block:: python + + dev = qml.device("qiskit.remote", wires=5, backend=backend, shots=4096) + + @qml.qnode(dev) + def circuit(x, y, z): + qml.RZ(z, wires=[0]) + qml.RY(y, wires=[0]) + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(wires=1)) + + # Runs with 4096 shots + circuit(0.2, 0.1, 0.3) + + # Runs with 10000 shots + circuit(0.2, 0.1, 0.3, shots=10000) diff --git a/doc/index.rst b/doc/index.rst index 7f7237996..ddf3944d8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,7 +8,7 @@ PennyLane-Qiskit Plugin :end-before: header-end-inclusion-marker-do-not-remove -Once the PennyLane-Qiskit plugin is installed, the the Qiskit devices +Once the PennyLane-Qiskit plugin is installed, the Qiskit devices can be accessed straightaway in PennyLane, without the need to import new packages. Devices @@ -48,17 +48,39 @@ For example, the ``'qiskit.aer'`` device with two wires is called like this: Backends ~~~~~~~~ -Qiskit devices have different **backends**, which define which actual simulator or hardware is used by the -device. Different simulator backends are optimized for different types of circuits. A backend can be defined as -follows: +Qiskit devices have different **backends**, which define the actual simulator or hardware +used by the device. A backend instance should be initalized and passed to the device. + +Different simulator backends are optimized for different purposes. To change what backend is used, +a simulator backend can be defined as follows: + +.. code-block:: python + + from qiskit_aer import UnitarySimulator + + dev = qml.device('qiskit.aer', wires=, backend=UnitarySimulator()) + +.. note:: + + For ``'qiskit.aer'``, PennyLane chooses the ``aer_simulator`` as the default backend if no + backend is specified. For more details on the ``aer_simulator``, including available backend + options, see `Qiskit Aer Simulator documentation `_. + +To access a real device, we can use the ``'qiskit.remote'`` device. A real hardware backend can +be defined as follows: .. code-block:: python - dev = qml.device('qiskit.aer', wires=2, backend='unitary_simulator') + from qiskit_ibm_runtime import QiskitRuntimeService + + QiskitRuntimeService.save_account(channel="ibm_quantum", token="") + + # To access saved credentials for the IBM quantum channel and select an instance + service = QiskitRuntimeService(channel="ibm_quantum", instance="my_hub/my_group/my_project") + backend = service.least_busy(operational=True, simulator=False, min_num_qubits=) -PennyLane chooses the ``aer_simulator`` as the default backend if no backend is specified. -For more details on the ``aer_simulator``, including available backend options, see -`Qiskit Aer Simulator documentation `_. + # passing a string in backend would result in an error + dev = qml.device('qiskit.remote', wires=, backend=backend) Tutorials ~~~~~~~~~ From e9e30debe45d5e8e6291ef0c92073192e8070106 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:13:01 -0400 Subject: [PATCH 45/47] Tracker functionality (#533) * is this all? * doc strings * tests for tracker * trackers * trackers * fixed tests * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * woops * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * print out some stuff for tests * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] temp * [skip ci] There is a bug due to the post processing of results that is causing some of the assertion statements to fail. We can ignore these assertions for now and address how to rework reorder_fn to avoid this bug * [skip ci] * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * pylint * formatting * tracker comments * black * comments about the tracker * bet * fix to imports * black * temp * baller implementation * increase shot number to reduce error * edit function * black * better tests * refactor * black * delete print statement * refactor * Delete unneeded import * fix assertion * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * new tests * removed simulations keyword * some xfails pending discussion --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh --- pennylane_qiskit/qiskit_device.py | 31 +++- pennylane_qiskit/remote.py | 4 +- tests/test_base_device.py | 237 +++++++++++++++++++++++------- 3 files changed, 219 insertions(+), 53 deletions(-) diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index 8df10cadf..ae8ae4b2f 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -22,6 +22,7 @@ import inspect from typing import Union, Callable, Tuple, Sequence from contextlib import contextmanager +from functools import wraps import numpy as np import pennylane as qml @@ -43,8 +44,9 @@ validate_measurements, validate_device_wires, ) -from pennylane.measurements import ExpectationMP, VarianceMP +from pennylane.measurements import ExpectationMP, VarianceMP +from pennylane.devices.modifiers.simulator_tracking import simulator_tracking from ._version import __version__ from .converter import QISKIT_OPERATION_MAP, circuit_to_qiskit, mp_to_pauli @@ -53,6 +55,32 @@ Result_or_ResultBatch = Union[Result, ResultBatch] +def custom_simulator_tracking(cls): + """Decorator that adds custom tracking to the device class.""" + + cls = simulator_tracking(cls) + tracked_execute = cls.execute + + @wraps(tracked_execute) + def execute(self, circuits, execution_config=DefaultExecutionConfig): + results = tracked_execute(self, circuits, execution_config) + if self.tracker.active: + res = [] + del self.tracker.totals["simulations"] + del self.tracker.history["simulations"] + del self.tracker.latest["simulations"] + for r in self.tracker.history["results"]: + while isinstance(r, (list, tuple)) and len(r) == 1: + r = r[0] + res.append(r) + self.tracker.history["results"] = res + return results + + cls.execute = execute + + return cls + + # pylint: disable=protected-access @contextmanager def qiskit_session(device, **kwargs): @@ -239,6 +267,7 @@ def reorder_fn(res): return tapes, reorder_fn +@custom_simulator_tracking class QiskitDevice(Device): r"""Hardware/simulator Qiskit device for PennyLane. diff --git a/pennylane_qiskit/remote.py b/pennylane_qiskit/remote.py index 251ca5887..92181c16d 100644 --- a/pennylane_qiskit/remote.py +++ b/pennylane_qiskit/remote.py @@ -142,5 +142,5 @@ def circuit(x): short_name = "qiskit.remote" - def __init__(self, wires, backend, provider=None, shots=1024, **kwargs): - super().__init__(wires, provider=provider, backend=backend, shots=shots, **kwargs) + def __init__(self, wires, backend, shots=1024, **kwargs): + super().__init__(wires, backend=backend, shots=shots, **kwargs) diff --git a/tests/test_base_device.py b/tests/test_base_device.py index d958fda8e..30d818860 100644 --- a/tests/test_base_device.py +++ b/tests/test_base_device.py @@ -18,6 +18,7 @@ from unittest.mock import patch, Mock from flaky import flaky import numpy as np +from pennylane import numpy as pnp from pydantic_core import ValidationError import pytest @@ -476,8 +477,11 @@ def test_observable_stopping_condition(self, obs, expected): 3, ), ( - [qml.var(qml.X(0) + qml.Y(0) + qml.Z(0))], # Var should not split - 1, + pytest.param( + [qml.var(qml.X(0) + qml.Y(0) + qml.Z(0))], + 1, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) ), ( [ @@ -497,22 +501,28 @@ def test_observable_stopping_condition(self, obs, expected): 2, ), ( - [ - qml.counts(qml.X(0)), - qml.counts(qml.Y(1)), - qml.counts(qml.Z(0) @ qml.Z(1)), - qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), - ], - 3, + pytest.param( + [ + qml.counts(qml.X(0)), + qml.counts(qml.Y(1)), + qml.counts(qml.Z(0) @ qml.Z(1)), + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) ), ( - [ - qml.sample(qml.X(0)), - qml.sample(qml.Y(1)), - qml.sample(qml.Z(0) @ qml.Z(1)), - qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), - ], - 3, + pytest.param( + [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) ), ], ) @@ -729,6 +739,109 @@ def test_num_wires_property(self): assert dev.num_wires == len(wires) +class TestTrackerFunctionality: + def test_tracker_batched(self): + """Test that the tracker works for batched circuits""" + dev = qml.device("default.qubit", wires=1, shots=10000) + qiskit_dev = QiskitDevice(wires=1, backend=AerSimulator(), shots=10000) + + x = pnp.array(0.1, requires_grad=True) + + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)) + + @qml.qnode(qiskit_dev, diff_method="parameter-shift") + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)) + + with qml.Tracker(dev) as tracker: + qml.grad(circuit)(x) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qml.grad(qiskit_circuit)(x) + + assert qiskit_tracker.history["batches"] == tracker.history["batches"] + assert tracker.history["shots"] == qiskit_tracker.history["shots"] + assert np.allclose(qiskit_tracker.history["results"], tracker.history["results"], atol=0.1) + assert np.shape(qiskit_tracker.history["results"]) == np.shape(tracker.history["results"]) + assert qiskit_tracker.history["resources"][0] == tracker.history["resources"][0] + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_single_tape(self): + """Test that the tracker works for a single tape""" + dev = qml.device("default.qubit", wires=1, shots=10000) + qiskit_dev = QiskitDevice(wires=1, backend=AerSimulator(), shots=10000) + + tape = qml.tape.QuantumTape([qml.S(0)], [qml.expval(qml.X(0))]) + with qiskit_dev.tracker: + qiskit_out = qiskit_dev.execute(tape) + + with dev.tracker: + pl_out = dev.execute(tape) + + assert ( + qiskit_dev.tracker.history["resources"][0].shots + == dev.tracker.history["resources"][0].shots + ) + assert np.allclose(pl_out, qiskit_out, atol=0.1) + assert np.allclose( + qiskit_dev.tracker.history["results"], dev.tracker.history["results"], atol=0.1 + ) + + assert np.shape(qiskit_dev.tracker.history["results"]) == np.shape( + dev.tracker.history["results"] + ) + + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_split_by_measurement_type(self): + """Test that the tracker works for as intended for circuits split by measurement type""" + qiskit_dev = QiskitDevice(wires=5, backend=AerSimulator(), shots=10000) + + x = 0.1 + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)), qml.counts(qml.X(1)) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qiskit_circuit(x) + + assert qiskit_tracker.totals["executions"] == 2 + assert qiskit_tracker.totals["shots"] == 20000 + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_split_by_non_commute(self): + """Test that the tracker works for as intended for circuits split by non commute""" + qiskit_dev = QiskitDevice(wires=5, backend=AerSimulator(), shots=10000) + + x = 0.1 + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)), qml.expval(qml.X(0)) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qiskit_circuit(x) + + assert qiskit_tracker.totals["executions"] == 2 + assert qiskit_tracker.totals["shots"] == 20000 + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + class TestMockedExecution: def test_get_transpile_args(self): """Test that get_transpile_args works as expected by filtering out @@ -1351,11 +1464,17 @@ def circuit(): @pytest.mark.parametrize( "observable", [ - lambda: [qml.counts(qml.X(0) + qml.Y(0)), qml.counts(qml.X(0))], - lambda: [ - qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), - qml.counts(0.5 * qml.Y(1)), - ], + pytest.param( + lambda: [qml.counts(qml.X(0) + qml.Y(0)), qml.counts(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.counts(0.5 * qml.Y(1)), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), ], ) @flaky(max_runs=10, min_passes=7) @@ -1388,36 +1507,54 @@ def circuit(): @pytest.mark.parametrize( "observable", [ - lambda: [qml.sample(qml.X(0) + qml.Y(0)), qml.sample(qml.X(0))], - lambda: [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))], - lambda: [ - qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), - qml.sample(0.5 * qml.Y(1)), - ], - lambda: [ - qml.sample(qml.X(0)), - qml.sample(qml.Y(1)), - qml.sample(0.5 * qml.Y(1)), - qml.sample(qml.Z(0) @ qml.Z(1)), - qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), - qml.sample( - qml.ops.LinearCombination( - [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] - ) - ), - ], - lambda: [ - qml.sample( - qml.ops.LinearCombination( - [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + pytest.param( + lambda: [qml.sample(qml.X(0) + qml.Y(0)), qml.sample(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample(0.5 * qml.Y(1)), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(0.5 * qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample( + qml.ops.LinearCombination( + [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] + ) + ), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample( + qml.ops.LinearCombination( + [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + ) + ), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample( + qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) ) - ), - ], - lambda: [ - qml.sample( - qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) - ) - ], + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), ], ) @flaky(max_runs=10, min_passes=7) From a97f79c6c53f4fdab07c56eba1476d8a1f621e29 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:01:24 -0400 Subject: [PATCH 46/47] Update CHANGELOG.md Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 239fb6a3c..6d2dcbbac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,9 +48,9 @@ This release contains contributions from (in alphabetical order): ### Contributors ✍️ This release contains contributions from (in alphabetical order): -Austin Huang - Utkarsh Azad +Lillian M. A. Frederiksen +Austin Huang Mashhood Khan --- From 984c7a5822a007d6c411346fdbc0bcbc1124330e Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Wed, 24 Jul 2024 16:11:47 -0400 Subject: [PATCH 47/47] CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2dcbbac..9b5e20c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,16 @@ ### New features since last release +* Qiskit Sessions can now be used for the ``qiskit.remote`` device with the ``qiskit_session`` context + manager. + [(#551)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/551) + ### Improvements 🛠 +* Qiskit Runtime Primitives are supported by the ``qiskit.remote`` device. Circuits ran using the ``qiskit.remote`` + device will automatically call the SamplerV2 and EstimatorV2 primitives appropriately. Additionally, runtime options can be passed as keyword arguments directly to the ``qiskit.remote`` device. + [(#513)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/513) + ### Breaking changes 💔 * Support has been removed for Qiskit versions below 0.46. The minimum required version for Qiskit is now 1.0.