From ba32a079d638796a477bef5de8647534bb2b3503 Mon Sep 17 00:00:00 2001 From: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:00:36 +0000 Subject: [PATCH 1/5] Add test. (#79) --- tests/backend_test.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/backend_test.py b/tests/backend_test.py index d8e969f1..5ab029c4 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -59,8 +59,6 @@ from pytket.utils.operators import QubitPauliOperator from pytket.utils.results import compare_statevectors -# TODO add tests for `get_operator_expectation_value` - skip_remote_tests: bool = os.getenv("PYTKET_RUN_REMOTE_TESTS") is None REASON = "PYTKET_RUN_REMOTE_TESTS not set (requires configuration of IBMQ account)" @@ -814,6 +812,20 @@ def test_aer_placed_expectation() -> None: assert "default register Qubits" in str(errorinfoCirc.value) +def test_operator_expectation_value() -> None: + c = Circuit(2).X(0).V(0).V(1).S(0).S(1).H(0).H(1).S(0).S(1) + op = QubitPauliOperator( + { + QubitPauliString([], []): 0.5, + QubitPauliString([Qubit(0)], [Pauli.Z]): -0.5, + } + ) + b = AerBackend() + c1 = b.get_compiled_circuit(c) + e = AerBackend().get_operator_expectation_value(c1, op) + assert np.isclose(e, 1.0) + + @pytest.mark.skipif(skip_remote_tests, reason=REASON) def test_ibmq_emulator(manila_emulator_backend: IBMQEmulatorBackend) -> None: assert manila_emulator_backend._noise_model is not None From 94eb49af851bbce58f3b2df0dd6470a8eb974c44 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Mon, 27 Feb 2023 12:20:17 +0000 Subject: [PATCH 2/5] update AerBackend docs and class highlighting (#74) * update AerBackend docs and class highlighting * Update docs/intro.txt Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> * Update docs/intro.txt Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> --------- Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> --- docs/intro.txt | 53 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/intro.txt b/docs/intro.txt index 25edd4b0..dcee4e67 100644 --- a/docs/intro.txt +++ b/docs/intro.txt @@ -16,13 +16,36 @@ Windows. To install, run: pip install pytket-qiskit -This will install `pytket` if it isn't already installed, and add new classes -and methods into the `pytket.extensions` namespace. +This will install ``pytket`` if it isn't already installed, and add new classes +and methods into the ``pytket.extensions`` namespace. + +An example using the shots-based :py:class:`AerBackend` simulator is shown below. + +:: + + from pytket.extensions.qiskit import AerBackend + + backend = AerBackend() + circ = Circuit(2).H(0).CX(0, 1).measure_all() + + # Compilation not needed here as both H and CX are supported gates + result = backend.run_circuit(circ, n_shots=1000) + +This simulator supports a large set of gates and by default has no architectural constraints or quantum noise. However the user can pass in a noise model or custom architecture to more closely model a real quantum device. + +The :py:class:`AerBackend` also supports GPU simulation which can be configured as follows. + +:: + + from pytket.extensions.qiskit import AerBackend + + backend = AerBackend() + backend._backend.set_option("device", "GPU") Access and Credentials ====================== -Accessing devices and simulators through the pytket-qiskit extension requires an IBMQ account. An account can be set up here -> https://quantum-computing.ibm.com/login. +With the exception of the Aer simulators, accessing devices and simulators through the ``pytket-qiskit`` extension requires an IBMQ account. An account can be set up here: https://quantum-computing.ibm.com/login. Once you have created an account you can obtain an API token which you can use to configure your credentials locally. @@ -32,7 +55,7 @@ Once you have created an account you can obtain an API token which you can use t set_ibmq_config(ibmq_api_token=ibm_token) -This will save your IBMQ credentials locally. After saving your credentials you can access pytket-qiskit backend repeatedly without having to re-initialise your credentials. +This will save your IBMQ credentials locally. After saving your credentials you can access ``pytket-qiskit`` backend repeatedly without having to re-initialise your credentials. If you are a member of an IBM hub then you can add this information to ``set_ibmq_config`` as well. @@ -53,7 +76,7 @@ locally without saving the token in pytket config: IBMQ.save_account(token=ibm_token) QiskitRuntimeService.save_account(channel="ibm_quantum", token=ibm_token) -To see which devices you can access you can use the ``available_devices`` method on the ``IBMQBackend`` or ``IBMQEmulatorBackend``. Note that it is possible to pass ``hub``, ``group`` and ``project`` parameters to this method. This allows you to see which devices are accessible through your IBM hub. +To see which devices you can access you can use the ``available_devices`` method on the :py:class:`IBMQBackend` or :py:class:`IBMQEmulatorBackend`. Note that it is possible to pass ``hub``, ``group`` and ``project`` parameters to this method. This allows you to see which devices are accessible through your IBM hub. :: @@ -66,7 +89,7 @@ To see which devices you can access you can use the ``available_devices`` method Backends Available Through pytket-qiskit ======================================== -The ``pytket-qiskit`` extension has several types of available ``Backend``. These are the ``IBMQBackend`` +The ``pytket-qiskit`` extension has several types of available :py:class:`Backend`. These are the :py:class:`IBMQBackend` and several types of simulator. .. list-table:: @@ -86,13 +109,13 @@ and several types of simulator. * - `AerUnitaryBackend `_ - Unitary simulator -* [1] ``AerBackend`` is noiseless by default and has no architecture. However it can accept a user defined ``NoiseModel`` and ``Architecture``. -* In addition to the backends above the pytket-qiskit extension also has the ``TketBackend``. This allows a tket ``Backend`` to be used directly through qiskit. see the `notebook example `_ on qiskit integration. +* [1] :py:class`AerBackend` is noiseless by default and has no architecture. However it can accept a user defined :py:class:`NoiseModel` and :py:class:`Architecture`. +* In addition to the backends above the ``pytket-qiskit`` extension also has the :py:class:`TketBackend`. This allows a tket :py:class:`Backend` to be used directly through qiskit. see the `notebook example `_ on qiskit integration. Default Compilation =================== -Every ``Backend`` in pytket has its own ``default_compilation_pass`` method. This method applies a sequence of optimisations to a circuit depending on the value of an ``optimisation_level`` parameter. This default compilation will ensure that the circuit meets all the constraints required to run on the Backend. The passes applied by different levels of optimisation are specified in the table below. +Every :py:class:`Backend` in pytket has its own ``default_compilation_pass`` method. This method applies a sequence of optimisations to a circuit depending on the value of an ``optimisation_level`` parameter. This default compilation will ensure that the circuit meets all the constraints required to run on the :py:class:`Backend`. The passes applied by different levels of optimisation are specified in the table below. .. list-table:: **Default compilation pass for the IBMQBackend and IBMQEmulatorBackend** :widths: 25 25 25 @@ -134,11 +157,11 @@ Every ``Backend`` in pytket has its own ``default_compilation_pass`` method. Thi * [1] If no value is specified then ``optimisation_level`` defaults to a value of 2. * [2] self.rebase_pass is a rebase to the gateset supported by the backend, For IBM quantum devices that is {X, SX, Rz, CX}. -* [3] Here ``CXMappingPass`` maps program qubits to the architecture using a `NoiseAwarePlacement `_ -* [4] ``SimplifyInitial`` has arguments ``allow_classical=False`` and ``create_all_qubits=True``. +* [3] Here :py:class:`CXMappingPass` maps program qubits to the architecture using a `NoiseAwarePlacement `_ +* [4] :py:class:`SimplifyInitial` has arguments ``allow_classical=False`` and ``create_all_qubits=True``. -**Note:** The ``default_compilation_pass`` for ``AerBackend`` is the same as above except it doesn't use ``SimplifyInitial``. +**Note:** The ``default_compilation_pass`` for :py:class:`AerBackend` is the same as above except it doesn't use :py:class:`SimplifyInitial`. Backend Predicates @@ -146,12 +169,12 @@ Backend Predicates Circuits must satisfy certain conditions before they can be processed on a device or simulator. In pytket these conditions are called predicates. -All pytket-qiskit backends have the following two predicates. +All ``pytket-qiskit`` backends have the following two predicates. -* `GateSetPredicate `_ - The circuit must contain only operations supported by the ``Backend``. To view supported Ops run ``BACKENDNAME.backend_info.gate_set``. +* `GateSetPredicate `_ - The circuit must contain only operations supported by the :py:class`Backend`. To view supported Ops run ``BACKENDNAME.backend_info.gate_set``. * `NoSymbolsPredicate `_ - Parameterised gates must have numerical values when the circuit is executed. -The ``IBMQBackend`` and ``IBMQEmulatorBackend`` may also have the following predicates depending on the capabilities of the specified device. +The :py:class:`IBMQBackend` and :py:class:`IBMQEmulatorBackend` may also have the following predicates depending on the capabilities of the specified device. * `NoClassicalControlPredicate `_ * `NoMidMeasurePredicate `_ From b028ee4a3bc64a3227e01553c92378e4f733bee4 Mon Sep 17 00:00:00 2001 From: Travis Thompson <102229498+trvto@users.noreply.github.com> Date: Mon, 13 Mar 2023 22:10:48 +0100 Subject: [PATCH 3/5] Bugfix/aer backend info (#83) * add .idea to gitignore * set supports_fast_feedforward to True for AerBackend backend info * refactor aer.py, add missing dependency on numpy * run linters * remove unneeded and wrong required gate set check * add backend_info misc to noise model characterization * use same gate set for backend info, gate set predicate and rebase pass, plus cleanup * fix import order * don't add Measure Reset Barrier and RangePredicate for AerStateBackend and AerUnitaryBackend * update extension version and changelog * black and pylint * fix unused import local pylint didn't find * fix changelog * cleanup noise_model characterisation code * pylint --- .gitignore | 1 + _metadata.py | 2 +- docs/changelog.rst | 5 + pytket/extensions/qiskit/backends/aer.py | 597 ++++++++++----------- pytket/extensions/qiskit/qiskit_convert.py | 4 +- setup.py | 1 + tests/backend_test.py | 35 +- 7 files changed, 311 insertions(+), 334 deletions(-) diff --git a/.gitignore b/.gitignore index fb1c8136..013424a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build dist *.pyc .vscode +.idea .venv .mypy_cache .hypothesis diff --git a/_metadata.py b/_metadata.py index 94ab64f7..796a4a80 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.36.0" +__extension_version__ = "0.37.0" __extension_name__ = "pytket-qiskit" diff --git a/docs/changelog.rst b/docs/changelog.rst index 910be330..e4ad850f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ~~~~~~~~~ +0.37.0 (unreleased) +------------------- + +* Fix faulty information in ``AerBackend().backend_info`` + 0.36.0 (February 2023) ---------------------- diff --git a/pytket/extensions/qiskit/backends/aer.py b/pytket/extensions/qiskit/backends/aer.py index 698a54b1..26cc40de 100644 --- a/pytket/extensions/qiskit/backends/aer.py +++ b/pytket/extensions/qiskit/backends/aer.py @@ -14,16 +14,14 @@ import itertools from collections import defaultdict +from dataclasses import dataclass from logging import warning from typing import ( - Any, - Callable, Dict, List, Optional, Sequence, Tuple, - TypeVar, Union, cast, TYPE_CHECKING, @@ -31,13 +29,12 @@ ) import numpy as np - from qiskit.providers.aer.noise import NoiseModel # type: ignore from qiskit.quantum_info.operators import Pauli as qk_Pauli # type: ignore from qiskit.quantum_info.operators.symplectic.sparse_pauli_op import SparsePauliOp # type: ignore from qiskit_aer import Aer # type: ignore from qiskit_aer.library import save_expectation_value # type: ignore # pylint: disable=unused-import - +from pytket.architecture import Architecture # type: ignore from pytket.backends import Backend, CircuitNotRunError, CircuitStatus, ResultHandle from pytket.backends.backendinfo import BackendInfo from pytket.backends.backendresult import BackendResult @@ -55,6 +52,7 @@ NaivePlacementPass, ) from pytket.pauli import Pauli, QubitPauliString # type: ignore +from pytket.placement import NoiseAwarePlacement # type: ignore from pytket.predicates import ( # type: ignore ConnectivityPredicate, GateSetPredicate, @@ -63,19 +61,16 @@ NoSymbolsPredicate, Predicate, ) -from pytket.extensions.qiskit.qiskit_convert import ( - tk_to_qiskit, - _gate_str_2_optype, - get_avg_characterisation, -) -from pytket.extensions.qiskit.result_convert import qiskit_result_to_backendresult -from pytket.extensions.qiskit._metadata import __extension_version__ -from pytket.architecture import Architecture # type: ignore -from pytket.placement import NoiseAwarePlacement # type: ignore from pytket.utils.operators import QubitPauliOperator from pytket.utils.results import KwargTypes from .ibm_utils import _STATUS_MAP, _batch_circuits +from .._metadata import __extension_version__ +from ..qiskit_convert import ( + tk_to_qiskit, + _gate_str_2_optype, +) +from ..result_convert import qiskit_result_to_backendresult if TYPE_CHECKING: from qiskit.providers.aer import AerJob # type: ignore @@ -88,41 +83,35 @@ def _default_q_index(q: Qubit) -> int: return int(q.index[0]) -_required_gates: Set[OpType] = {OpType.CX, OpType.U1, OpType.U2, OpType.U3} +def _tket_gate_set_from_qiskit_backend( + qiskit_backend: "QiskitAerBackend", +) -> Set[OpType]: + config = qiskit_backend.configuration() + gate_set = { + _gate_str_2_optype[gate_str] + for gate_str in config.basis_gates + if gate_str in _gate_str_2_optype + } + if "unitary" in config.basis_gates: + gate_set.add(OpType.Unitary1qBox) + gate_set.add(OpType.Unitary3qBox) + # special case mapping TK1 to U + gate_set.add(OpType.TK1) + return gate_set class _AerBaseBackend(Backend): """Common base class for all Aer simulator backends""" - _persistent_handles = False + _qiskit_backend: "QiskitAerBackend" + _backend_info: BackendInfo + _memory: bool + _required_predicates: List[Predicate] + _noise_model: Optional[NoiseModel] = None - def __init__(self, backend_name: str): - super().__init__() - self._backend: "QiskitAerBackend" = Aer.get_backend(backend_name) - - self._gate_set: Set[OpType] = { - _gate_str_2_optype[gate_str] - for gate_str in self._backend.configuration().basis_gates - if gate_str in _gate_str_2_optype - } - # special case mapping TK1 to U - self._gate_set.add(OpType.TK1) - if not self._gate_set >= _required_gates: - raise NotImplementedError( - f"Gate set {self._gate_set} missing at least one of {_required_gates}" - ) - self._backend_info = BackendInfo( - type(self).__name__, - backend_name, - __extension_version__, - Architecture([]), - self._gate_set, - supports_midcircuit_measurement=True, # is this correct? - misc={"characterisation": None}, - ) - - self._memory = False - self._noise_model: Optional[NoiseModel] = None + @property + def required_predicates(self) -> List[Predicate]: + return self._required_predicates @property def _result_id_type(self) -> _ResultIdTuple: @@ -134,9 +123,74 @@ def backend_info(self) -> BackendInfo: def rebase_pass(self) -> BasePass: return auto_rebase_pass( - self._gate_set, + self._backend_info.gate_set, + ) + + def _arch_dependent_default_compilation_pass( + self, arch: Architecture, optimisation_level: int = 2 + ) -> BasePass: + assert optimisation_level in range(3) + arch_specific_passes = [ + CXMappingPass( + arch, + NoiseAwarePlacement( + arch, + self._backend_info.averaged_node_gate_errors, + self._backend_info.averaged_edge_gate_errors, + self._backend_info.averaged_readout_errors, + ), + directed_cx=True, + delay_measures=False, + ), + NaivePlacementPass(arch), + ] + if optimisation_level == 0: + return SequencePass( + [ + DecomposeBoxes(), + self.rebase_pass(), + *arch_specific_passes, + self.rebase_pass(), + ] + ) + if optimisation_level == 1: + return SequencePass( + [ + DecomposeBoxes(), + SynthesiseTket(), + *arch_specific_passes, + SynthesiseTket(), + ] + ) + return SequencePass( + [ + DecomposeBoxes(), + FullPeepholeOptimise(), + *arch_specific_passes, + CliffordSimp(False), + SynthesiseTket(), + ] ) + def _arch_independent_default_compilation_pass( + self, optimisation_level: int = 2 + ) -> BasePass: + assert optimisation_level in range(3) + if optimisation_level == 0: + return SequencePass([DecomposeBoxes(), self.rebase_pass()]) + if optimisation_level == 1: + return SequencePass([DecomposeBoxes(), SynthesiseTket()]) + return SequencePass([DecomposeBoxes(), FullPeepholeOptimise()]) + + def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: + arch = self._backend_info.architecture + if arch.coupling and self._backend_info.get_misc("characterisation"): + return self._arch_dependent_default_compilation_pass( + arch, optimisation_level + ) + + return self._arch_independent_default_compilation_pass(optimisation_level) + def process_circuits( self, circuits: Sequence[Circuit], @@ -170,7 +224,7 @@ def process_circuits( qcs.append(qc) seed = cast(Optional[int], kwargs.get("seed")) - job = self._backend.run( + job = self._qiskit_backend.run( qcs, shots=n_shots, memory=self._memory, @@ -231,7 +285,7 @@ def _snapshot_expectation_value( ) qc = tk_to_qiskit(circuit) qc.save_expectation_value(hamiltonian, qc.qubits, "snap") - job = self._backend.run(qc) + job = self._qiskit_backend.run(qc) return cast( complex, job.result().data(qc)["snap"], @@ -258,6 +312,13 @@ def get_pauli_expectation_value( :return: :math:`\\left<\\psi | P | \\psi \\right>` :rtype: complex """ + if self._noise_model: + raise RuntimeError( + ( + "Snapshot based expectation value not supported with noise model. " + "Use shots." + ) + ) if not self._supports_expectation: raise NotImplementedError("Cannot get expectation value from this backend") @@ -285,6 +346,13 @@ def get_operator_expectation_value( :return: :math:`\\left<\\psi | H | \\psi \\right>` :rtype: complex """ + if self._noise_model: + raise RuntimeError( + ( + "Snapshot based expectation value not supported with noise model. " + "Use shots." + ) + ) if not self._supports_expectation: raise NotImplementedError("Cannot get expectation value from this backend") @@ -292,73 +360,53 @@ def get_operator_expectation_value( return self._snapshot_expectation_value(state_circuit, sparse_op, valid_check) -class _AerStateBaseBackend(_AerBaseBackend): - def __init__(self, *args: str): - self._qlists: Dict[ResultHandle, Tuple[int, ...]] = {} - super().__init__(*args) +@dataclass(frozen=True) +class NoiseModelCharacterisation: + """Class to hold information from the processing of the noise model""" - @property - def required_predicates(self) -> List[Predicate]: - return [ - NoClassicalControlPredicate(), - NoFastFeedforwardPredicate(), - GateSetPredicate( - self._backend_info.gate_set.union( - { - OpType.noop, - OpType.Unitary1qBox, - } - ) - ), - ] + architecture: Architecture + node_errors: Optional[Dict] = None + edge_errors: Optional[Dict] = None + readout_errors: Optional[Dict] = None + averaged_node_errors: Optional[Dict[Node, float]] = None + averaged_edge_errors: Optional[Dict[Node, float]] = None + averaged_readout_errors: Optional[Dict[Node, float]] = None + generic_q_errors: Optional[Dict] = None - def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: - assert optimisation_level in range(3) - if optimisation_level == 0: - return SequencePass([DecomposeBoxes(), self.rebase_pass()]) - elif optimisation_level == 1: - return SequencePass([DecomposeBoxes(), SynthesiseTket()]) - else: - return SequencePass([DecomposeBoxes(), FullPeepholeOptimise()]) - def process_circuits( - self, - circuits: Sequence[Circuit], - n_shots: Union[None, int, Sequence[Optional[int]]] = None, - valid_check: bool = True, - **kwargs: KwargTypes, - ) -> List[ResultHandle]: - handles = super().process_circuits( - circuits, n_shots=None, valid_check=valid_check, **kwargs - ) - return handles - - def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult: - if handle in self._cache: - if "result" in self._cache[handle]: - return cast(BackendResult, self._cache[handle]["result"]) +def _map_trivial_noise_model_to_none( + noise_model: Optional[NoiseModel], +) -> Optional[NoiseModel]: + if noise_model and all(value == [] for value in noise_model.to_dict().values()): + return None + return noise_model - self._check_handle_type(handle) - try: - job: "AerJob" = self._cache[handle]["job"] - except KeyError: - raise CircuitNotRunError(handle) - res = job.result() - backresults = qiskit_result_to_backendresult(res) - for circ_index, backres in enumerate(backresults): - newhandle = ResultHandle(handle[0], circ_index) - self._cache[newhandle]["result"] = backres - - return cast(BackendResult, self._cache[handle]["result"]) +def _get_characterisation_of_noise_model( + noise_model: Optional[NoiseModel], gate_set: Set[OpType] +) -> NoiseModelCharacterisation: + if noise_model is None: + return NoiseModelCharacterisation(architecture=Architecture([])) + return _process_noise_model(noise_model, gate_set) class AerBackend(_AerBaseBackend): + _persistent_handles = False _supports_shots = True _supports_counts = True _supports_expectation = True _expectation_allows_nonhermitian = False + _memory = True + + _qiskit_backend_name = "aer_simulator" + _allowed_special_gates = { + OpType.Measure, + OpType.Barrier, + OpType.Reset, + OpType.RangePredicate, + } + def __init__( self, noise_model: Optional[NoiseModel] = None, @@ -373,228 +421,140 @@ def __init__( for available values. Defaults to "automatic". :type simulation_method: str """ - super().__init__("aer_simulator") - - if not noise_model or all( - value == [] for value in noise_model.to_dict().values() - ): - self._noise_model = None - else: - self._noise_model = noise_model - characterisation = _process_model(noise_model, self._backend_info.gate_set) - averaged_errors = get_avg_characterisation(characterisation) - - arch = characterisation["Architecture"] - self._backend_info.architecture = arch - self._backend_info.all_node_gate_errors = characterisation["NodeErrors"] - self._backend_info.all_edge_gate_errors = characterisation["EdgeErrors"] - self._backend_info.all_readout_errors = characterisation["ReadoutErrors"] - - self._backend_info.averaged_node_gate_errors = averaged_errors[ - "node_errors" - ] - self._backend_info.averaged_edge_gate_errors = averaged_errors[ - "edge_errors" - ] - self._backend_info.averaged_readout_errors = averaged_errors[ - "readout_errors" - ] - - characterisation_keys = [ - "GenericOneQubitQErrors", - "GenericTwoQubitQErrors", - ] - # filter entries to keep - characterisation = { - k: v for k, v in characterisation.items() if k in characterisation_keys - } - self._backend_info.misc["characterisation"] = characterisation - - self._memory = True + super().__init__() + self._qiskit_backend: "QiskitAerBackend" = Aer.get_backend( + self._qiskit_backend_name + ) + self._qiskit_backend.set_options(method=simulation_method) + gate_set = _tket_gate_set_from_qiskit_backend(self._qiskit_backend).union( + self._allowed_special_gates + ) + self._noise_model = _map_trivial_noise_model_to_none(noise_model) + characterisation = _get_characterisation_of_noise_model( + self._noise_model, gate_set + ) - self._backend.set_options(method=simulation_method) + self._backend_info = BackendInfo( + name=type(self).__name__, + device_name=self._qiskit_backend_name, + version=__extension_version__, + architecture=characterisation.architecture, + gate_set=gate_set, + supports_midcircuit_measurement=True, # is this correct? + supports_fast_feedforward=True, + all_node_gate_errors=characterisation.node_errors, + all_edge_gate_errors=characterisation.edge_errors, + all_readout_errors=characterisation.readout_errors, + averaged_node_gate_errors=characterisation.averaged_node_errors, + averaged_edge_gate_errors=characterisation.averaged_edge_errors, + averaged_readout_errors=characterisation.averaged_readout_errors, + misc={"characterisation": characterisation.generic_q_errors}, + ) - @property - def required_predicates(self) -> List[Predicate]: - pred_list = [ + self._required_predicates = [ NoSymbolsPredicate(), - GateSetPredicate( - self._backend_info.gate_set.union( - { - OpType.Measure, - OpType.Reset, - OpType.Barrier, - OpType.noop, - OpType.Unitary1qBox, - OpType.RangePredicate, - } - ) - ), + GateSetPredicate(self._backend_info.gate_set), ] - arch = self._backend_info.architecture - if arch.coupling: - # architecture is non-trivial - pred_list.append(ConnectivityPredicate(arch)) - return pred_list - - def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: - assert optimisation_level in range(3) - passlist = [DecomposeBoxes()] - if optimisation_level == 0: - passlist.append(self.rebase_pass()) - elif optimisation_level == 1: - passlist.append(SynthesiseTket()) - else: - passlist.append(FullPeepholeOptimise()) - arch = self._backend_info.architecture - if arch.coupling and self._backend_info.get_misc("characterisation"): + if characterisation.architecture.coupling: # architecture is non-trivial - passlist.append( - CXMappingPass( - arch, - NoiseAwarePlacement( - arch, - self._backend_info.averaged_node_gate_errors, - self._backend_info.averaged_edge_gate_errors, - self._backend_info.averaged_readout_errors, - ), - directed_cx=True, - delay_measures=False, - ) + self._required_predicates.append( + ConnectivityPredicate(characterisation.architecture) ) - passlist.append(NaivePlacementPass(arch)) - if optimisation_level == 0: - passlist.append(self.rebase_pass()) - elif optimisation_level == 1: - passlist.append(SynthesiseTket()) - else: - passlist.extend([CliffordSimp(False), SynthesiseTket()]) - return SequencePass(passlist) - def process_circuits( - self, - circuits: Sequence[Circuit], - n_shots: Union[None, int, Sequence[Optional[int]]] = None, - valid_check: bool = True, - **kwargs: KwargTypes, - ) -> List[ResultHandle]: - """ - See :py:meth:`pytket.backends.Backend.process_circuits`. - Supported kwargs: `seed`. - """ - # discard result but useful to validate n_shots - Backend._get_n_shots_as_list( - n_shots, - len(circuits), - optional=False, - ) - return super().process_circuits(circuits, n_shots, valid_check, **kwargs) - def get_pauli_expectation_value( - self, - state_circuit: Circuit, - pauli: QubitPauliString, - valid_check: bool = True, - ) -> complex: - """Calculates the expectation value of the given circuit using the built-in Aer - snapshot functionality. - Requires a simple circuit with default register qubits, and no noise model. - - :param state_circuit: Circuit that generates the desired state - :math:`\\left|\\psi\\right>`. - :type state_circuit: Circuit - :param pauli: Pauli operator - :type pauli: QubitPauliString - :param valid_check: Explicitly check that the circuit satisfies all required - predicates to run on the backend. Defaults to True - :type valid_check: bool, optional - :return: :math:`\\left<\\psi | P | \\psi \\right>` - :rtype: complex - """ - if self._noise_model: - raise RuntimeError( - ( - "Snapshot based expectation value not supported with noise model. " - "Use shots." - ) - ) +class AerStateBackend(_AerBaseBackend): + _persistent_handles = False + _supports_shots = True + _supports_counts = True + _supports_expectation = True + _expectation_allows_nonhermitian = False - return super().get_pauli_expectation_value(state_circuit, pauli, valid_check) + _noise_model = None + _memory = False - def get_operator_expectation_value( - self, - state_circuit: Circuit, - operator: QubitPauliOperator, - valid_check: bool = True, - ) -> complex: - """Calculates the expectation value of the given circuit with respect to the - operator using the built-in Aer snapshot functionality - Requires a simple circuit with default register qubits, and no noise model. - - :param state_circuit: Circuit that generates the desired state - :math:`\\left|\\psi\\right>`. - :type state_circuit: Circuit - :param operator: Operator :math:`H`. - :type operator: QubitPauliOperator - :param valid_check: Explicitly check that the circuit satisfies all required - predicates to run on the backend. Defaults to True - :type valid_check: bool, optional - :return: :math:`\\left<\\psi | H | \\psi \\right>` - :rtype: complex - """ - if self._noise_model: - raise RuntimeError( - ( - "Snapshot based expectation value not supported with noise model. " - "Use shots." - ) - ) + _qiskit_backend_name = "aer_simulator_statevector" + _supports_state = True - return super().get_operator_expectation_value( - state_circuit, operator, valid_check + def __init__(self) -> None: + """Backend for running simulations on the Qiskit Aer Statevector simulator.""" + super().__init__() + self._qiskit_backend: "QiskitAerBackend" = Aer.get_backend( + self._qiskit_backend_name ) + self._backend_info = BackendInfo( + name=type(self).__name__, + device_name=self._qiskit_backend_name, + version=__extension_version__, + architecture=Architecture([]), + gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend), + supports_midcircuit_measurement=True, # is this correct? + misc={"characterisation": None}, + ) + self._required_predicates = [ + NoClassicalControlPredicate(), + NoFastFeedforwardPredicate(), + GateSetPredicate(self._backend_info.gate_set), + ] -class AerStateBackend(_AerStateBaseBackend): - _supports_state = True +class AerUnitaryBackend(_AerBaseBackend): + _persistent_handles = False + _supports_shots = True + _supports_counts = True _supports_expectation = True _expectation_allows_nonhermitian = False - def __init__(self) -> None: - """Backend for running simulations on the Qiskit Aer Statevector simulator.""" - super().__init__("aer_simulator_statevector") - + _memory = False + _noise_model = None -class AerUnitaryBackend(_AerStateBaseBackend): + _qiskit_backend_name = "aer_simulator_unitary" _supports_unitary = True def __init__(self) -> None: """Backend for running simulations on the Qiskit Aer Unitary simulator.""" - super().__init__("aer_simulator_unitary") + super().__init__() + self._qiskit_backend: "QiskitAerBackend" = Aer.get_backend( + self._qiskit_backend_name + ) + self._backend_info = BackendInfo( + name=type(self).__name__, + device_name=self._qiskit_backend_name, + version=__extension_version__, + architecture=Architecture([]), + gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend), + supports_midcircuit_measurement=True, # is this correct? + misc={"characterisation": None}, + ) + self._required_predicates = [ + NoClassicalControlPredicate(), + NoFastFeedforwardPredicate(), + GateSetPredicate(self._backend_info.gate_set), + ] -def _process_model(noise_model: NoiseModel, gate_set: Set[OpType]) -> dict: +def _process_noise_model( + noise_model: NoiseModel, gate_set: Set[OpType] +) -> NoiseModelCharacterisation: # obtain approximations for gate errors from noise model by using probability of # "identity" error assert OpType.CX in gate_set # TODO explicitly check for and separate 1 and 2 qubit gates - supported_single_optypes = gate_set.difference({OpType.CX}) - supported_single_optypes.add(OpType.Reset) errors = [ e for e in noise_model.to_dict()["errors"] if e["type"] == "qerror" or e["type"] == "roerror" ] - link_errors: dict = defaultdict(dict) - node_errors: dict = defaultdict(dict) - readout_errors: dict = {} + node_errors: dict[Node, dict[OpType, float]] = defaultdict(dict) + link_errors: dict[Tuple[Node, Node], dict[OpType, float]] = defaultdict(dict) + readout_errors: dict[Node, list[list[float]]] = {} generic_single_qerrors_dict: dict = defaultdict(list) generic_2q_qerrors_dict: dict = defaultdict(list) + qubits_set: set = set() # remember which qubits have explicit link errors - link_errors_qubits: set = set() + qubits_with_link_errors: set = set() coupling_map = [] for error in errors: @@ -616,13 +576,16 @@ def _process_model(noise_model: NoiseModel, gate_set: Set[OpType]) -> dict: if len(qubits) == 1: [q] = qubits optype = _gate_str_2_optype[name] + qubits_set.add(q) if error["type"] == "qerror": - node_errors[q].update({optype: 1 - gate_fid}) + node_errors[Node(q)].update({optype: float(1 - gate_fid)}) generic_single_qerrors_dict[q].append( [error["instructions"], error["probabilities"]] ) elif error["type"] == "roerror": - readout_errors[q] = error["probabilities"] + readout_errors[Node(q)] = cast( + List[List[float]], error["probabilities"] + ) else: raise RuntimeWarning("Error type not 'qerror' or 'roerror'.") elif len(qubits) == 2: @@ -630,51 +593,55 @@ def _process_model(noise_model: NoiseModel, gate_set: Set[OpType]) -> dict: # the resulting noise channel is composed and reflected in probabilities [q0, q1] = qubits optype = _gate_str_2_optype[name] - link_errors[(q0, q1)].update({optype: 1 - gate_fid}) - link_errors_qubits.add(q0) - link_errors_qubits.add(q1) + link_errors.update() + link_errors[(Node(q0), Node(q1))].update({optype: float(1 - gate_fid)}) + qubits_with_link_errors.add(q0) + qubits_with_link_errors.add(q1) # to simulate a worse reverse direction square the fidelity - link_errors[(q1, q0)].update({optype: 1 - gate_fid**2}) + link_errors[(Node(q1), Node(q0))].update({optype: float(1 - gate_fid**2)}) generic_2q_qerrors_dict[(q0, q1)].append( [error["instructions"], error["probabilities"]] ) coupling_map.append(qubits) # free qubits (ie qubits with no link errors) have full connectivity - free_qubits = set(node_errors).union(set(readout_errors)) - link_errors_qubits + free_qubits = qubits_set - qubits_with_link_errors for q in free_qubits: - for lq in link_errors_qubits: + for lq in qubits_with_link_errors: coupling_map.append([q, lq]) coupling_map.append([lq, q]) for pair in itertools.permutations(free_qubits, 2): coupling_map.append(pair) - # map type (k1 -> k2) -> v[k1] -> v[k2] - K1 = TypeVar("K1") - K2 = TypeVar("K2") - V = TypeVar("V") - convert_keys_t = Callable[[Callable[[K1], K2], Dict[K1, V]], Dict[K2, V]] - # convert qubits to architecture Nodes - convert_keys: convert_keys_t = lambda f, d: {f(k): v for k, v in d.items()} - node_errors = convert_keys(lambda q: Node(q), node_errors) - link_errors = convert_keys(lambda p: (Node(p[0]), Node(p[1])), link_errors) - readout_errors = convert_keys(lambda q: Node(q), readout_errors) - - characterisation: Dict[str, Any] = {} - characterisation["NodeErrors"] = node_errors - characterisation["EdgeErrors"] = link_errors - characterisation["ReadoutErrors"] = readout_errors - characterisation["GenericOneQubitQErrors"] = [ - [k, v] for k, v in generic_single_qerrors_dict.items() - ] - characterisation["GenericTwoQubitQErrors"] = [ - [list(k), v] for k, v in generic_2q_qerrors_dict.items() - ] - characterisation["Architecture"] = Architecture(coupling_map) - - return characterisation + generic_q_errors = { + "GenericOneQubitQErrors": [ + [k, v] for k, v in generic_single_qerrors_dict.items() + ], + "GenericTwoQubitQErrors": [ + [list(k), v] for k, v in generic_2q_qerrors_dict.items() + ], + } + + averaged_node_errors: dict[Node, float] = { + k: sum(v.values()) / len(v) for k, v in node_errors.items() + } + averaged_link_errors = {k: sum(v.values()) / len(v) for k, v in link_errors.items()} + averaged_readout_errors = { + k: (v[0][1] + v[1][0]) / 2.0 for k, v in readout_errors.items() + } + + return NoiseModelCharacterisation( + node_errors=dict(node_errors), + edge_errors=dict(link_errors), + readout_errors=readout_errors, + averaged_node_errors=averaged_node_errors, + averaged_edge_errors=averaged_link_errors, + averaged_readout_errors=averaged_readout_errors, + generic_q_errors=generic_q_errors, + architecture=Architecture(coupling_map), + ) def _sparse_to_zx_tup( diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index b296d7b9..638a6d54 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -203,11 +203,13 @@ def _tk_gate_set(backend: "QiskitBackend") -> Set[OpType]: """Set of tket gate types supported by the qiskit backend""" config = backend.configuration() if config.simulator: - return { + gate_set = { _gate_str_2_optype[gate_str] for gate_str in config.basis_gates if gate_str in _gate_str_2_optype }.union({OpType.Measure, OpType.Reset, OpType.Barrier}) + return gate_set + else: return { _gate_str_2_optype[gate_str] diff --git a/setup.py b/setup.py index bc9f75e3..0214c914 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ "qiskit ~= 0.41.0", "qiskit-ibm-runtime ~= 0.8.0", "qiskit-aer ~= 0.11.2", + "numpy", ], classifiers=[ "Environment :: Console", diff --git a/tests/backend_test.py b/tests/backend_test.py index 5ab029c4..43d89f77 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -247,21 +247,19 @@ def test_process_characterisation_incomplete_noise_model() -> None: arch = back.backend_info.architecture nodes = arch.nodes - assert set(arch.coupling) == set( - [ - (nodes[0], nodes[1]), - (nodes[0], nodes[2]), - (nodes[0], nodes[3]), - (nodes[1], nodes[2]), - (nodes[1], nodes[3]), - (nodes[2], nodes[0]), - (nodes[2], nodes[1]), - (nodes[2], nodes[3]), - (nodes[3], nodes[0]), - (nodes[3], nodes[1]), - (nodes[3], nodes[2]), - ] - ) + assert set(arch.coupling) == { + (nodes[0], nodes[1]), + (nodes[0], nodes[2]), + (nodes[0], nodes[3]), + (nodes[1], nodes[2]), + (nodes[1], nodes[3]), + (nodes[2], nodes[0]), + (nodes[2], nodes[1]), + (nodes[2], nodes[3]), + (nodes[3], nodes[0]), + (nodes[3], nodes[1]), + (nodes[3], nodes[2]), + } def test_circuit_compilation_complete_noise_model() -> None: @@ -908,11 +906,14 @@ def test_simulation_method() -> None: counts = b.run_circuit(clifford_T_circ, n_shots=4).get_counts() assert sum(val for _, val in counts.items()) == 4 - with pytest.raises(AttributeError) as warninfo: + with pytest.raises(CircuitNotValidError) as warninfo: # check for the error thrown when non-clifford circuit used with # stabilizer backend stabilizer_backend.run_circuit(clifford_T_circ, n_shots=4).get_counts() - assert "Attribute header is not defined" in str(warninfo.value) + assert ( + "Circuit with index 0 in submitted does not satisfy GateSetPredicate" + in str(warninfo.value) + ) def test_aer_expanded_gates() -> None: From ea188d4e21a62706455b55cf8234e851310d645b Mon Sep 17 00:00:00 2001 From: melf Date: Fri, 17 Mar 2023 09:03:15 +0000 Subject: [PATCH 4/5] update version and changelog --- docs/changelog.rst | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e4ad850f..50213ff5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,11 @@ Changelog ~~~~~~~~~ -0.37.0 (unreleased) +0.37.0 (March 2023) ------------------- * Fix faulty information in ``AerBackend().backend_info`` +* Updated pytket version requirement to 1.13. 0.36.0 (February 2023) ---------------------- diff --git a/setup.py b/setup.py index 0214c914..8f0e6787 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, install_requires=[ - "pytket ~= 1.11", + "pytket ~= 1.13", "qiskit ~= 0.41.0", "qiskit-ibm-runtime ~= 0.8.0", "qiskit-aer ~= 0.11.2", From f253440ee971bfa8241fd149a5aa27276313fbd8 Mon Sep 17 00:00:00 2001 From: melf Date: Fri, 17 Mar 2023 09:05:36 +0000 Subject: [PATCH 5/5] update sphinx_book_theme version to ~= 0.3.3 --- .github/workflows/docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs/requirements.txt b/.github/workflows/docs/requirements.txt index 31e91260..71ae0626 100644 --- a/.github/workflows/docs/requirements.txt +++ b/.github/workflows/docs/requirements.txt @@ -1,3 +1,3 @@ sphinx ~= 4.3.2 -sphinx_book_theme +sphinx_book_theme ~= 0.3.3 sphinx-copybutton