diff --git a/_metadata.py b/_metadata.py index 6bde9e0b..3cea4161 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.31.0" +__extension_version__ = "0.32.0" __extension_name__ = "pytket-qiskit" diff --git a/docs/changelog.rst b/docs/changelog.rst index 2a9fb936..892ca758 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog ~~~~~~~~~ +0.32.0 (December 2022) +---------------------- + +* Use ``qiskit_ibm_runtime`` services for sampling on ``IBMQBackend`` and + ``IBMQEmulatorBackend``. Note that shots tables (ordered lists of results) are + no longer available from these backends. (``BackendResult.get_shots()`` will + fail; use ``get_counts()`` instead.) + +* Fix incorrect circuit permutation handling for ``AerUnitaryBackend`` and ``AerStateBackend``. + 0.31.0 (November 2022) ---------------------- diff --git a/pytket/extensions/qiskit/backends/ibm.py b/pytket/extensions/qiskit/backends/ibm.py index fdf77787..32939e8b 100644 --- a/pytket/extensions/qiskit/backends/ibm.py +++ b/pytket/extensions/qiskit/backends/ibm.py @@ -15,6 +15,7 @@ import itertools import logging from ast import literal_eval +from collections import Counter import json from typing import ( cast, @@ -31,12 +32,16 @@ import qiskit # type: ignore from qiskit import IBMQ -from qiskit.qobj import QobjExperimentHeader # type: ignore -from qiskit.providers.ibmq.exceptions import IBMQBackendApiError # type: ignore -from qiskit.providers.ibmq.job import IBMQJob # type: ignore -from qiskit.result import Result, models # type: ignore +from qiskit.primitives import SamplerResult # type: ignore from qiskit.tools.monitor import job_monitor # type: ignore -from qiskit.providers.ibmq.utils.utils import api_status_to_job_status # type: ignore +from qiskit.result.distributions import QuasiDistribution # type: ignore +from qiskit_ibm_runtime import ( # type: ignore + QiskitRuntimeService, + Session, + Options, + Sampler, + RuntimeJob, +) from pytket.circuit import Circuit, OpType # type: ignore from pytket.backends import Backend, CircuitNotRunError, CircuitStatus, ResultHandle @@ -71,12 +76,10 @@ Predicate, ) from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, _tk_gate_set -from pytket.extensions.qiskit.result_convert import ( - qiskit_experimentresult_to_backendresult, -) from pytket.architecture import FullyConnected # type: ignore from pytket.placement import NoiseAwarePlacement # type: ignore from pytket.utils import prepare_circuit +from pytket.utils.outcomearray import OutcomeArray from pytket.utils.results import KwargTypes from .ibm_utils import _STATUS_MAP, _batch_circuits from .config import QiskitConfig @@ -86,35 +89,18 @@ IBMQBackend as _QiskIBMQBackend, AccountProvider, ) - from qiskit.providers.models import QasmBackendConfiguration # type: ignore _DEBUG_HANDLE_PREFIX = "_MACHINE_DEBUG_" -def _gen_debug_results(n_qubits: int, shots: int, index: int) -> Result: - raw_counts = {"0x0": shots} - raw_memory = ["0x0"] * shots - base_result_args = dict( - backend_name="test_backend", - backend_version="1.0.0", - qobj_id="id-123", - job_id="job-123", - success=True, - ) - data = models.ExperimentResultData(counts=raw_counts, memory=raw_memory) - exp_result_header = QobjExperimentHeader( - creg_sizes=[["c", n_qubits]], memory_slots=n_qubits - ) - exp_result = models.ExperimentResult( - shots=shots, - success=True, - meas_level=2, - data=data, - header=exp_result_header, - memory=True, +def _gen_debug_results(n_qubits: int, shots: int, index: int) -> SamplerResult: + debug_dist = {n: 0.0 for n in range(pow(2, n_qubits))} + debug_dist[0] = 1.0 + qd = QuasiDistribution(debug_dist) + return SamplerResult( + quasi_dists=[qd] * (index + 1), + metadata=[{"header_metadata": {}, "shots": shots}] * (index + 1), ) - results = [exp_result] * (index + 1) - return Result(results=results, **base_result_args) class NoIBMQAccountError(Exception): @@ -128,7 +114,7 @@ def __init__(self) -> None: class IBMQBackend(Backend): - _supports_shots = True + _supports_shots = False _supports_counts = True _supports_contextual_optimisation = True _persistent_handles = True @@ -141,6 +127,7 @@ def __init__( project: Optional[str] = None, monitor: bool = True, account_provider: Optional["AccountProvider"] = None, + token: Optional[str] = None, ): """A backend for running circuits on remote IBMQ devices. The provider arguments of `hub`, `group` and `project` can @@ -166,6 +153,8 @@ def __init__( Used to pass credentials in if not configured on local machine (as well as hub, group and project). Defaults to None. :type account_provider: Optional[AccountProvider] + :param token: Authentication token to use the `QiskitRuntimeService`. + :type token: Optional[str] """ super().__init__() self._pytket_config = QiskitConfig.from_default_config_file() @@ -175,18 +164,21 @@ def __init__( else account_provider ) self._backend: "_QiskIBMQBackend" = self._provider.get_backend(backend_name) - self._config = self._backend.configuration() - self._max_per_job = getattr(self._config, "max_experiments", 1) + config = self._backend.configuration() + self._max_per_job = getattr(config, "max_experiments", 1) gate_set = _tk_gate_set(self._backend) self._backend_info = self._get_backend_info(self._backend) + self._service = QiskitRuntimeService(channel="ibm_quantum", token=token) + self._session = Session(service=self._service, backend=backend_name) + self._standard_gateset = gate_set >= {OpType.X, OpType.SX, OpType.Rz, OpType.CX} self._monitor = monitor # cache of results keyed by job id and circuit index - self._ibm_res_cache: Dict[Tuple[str, int], models.ExperimentResult] = dict() + self._ibm_res_cache: Dict[Tuple[str, int], Counter] = dict() self._MACHINE_DEBUG = False @@ -370,7 +362,8 @@ def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: @property def _result_id_type(self) -> _ResultIdTuple: - return (str, int, str) + # IBMQ job ID, index, number of measurements per shot, post-processing circuit + return (str, int, int, str) def rebase_pass(self) -> BasePass: return auto_rebase_pass( @@ -423,40 +416,47 @@ def process_circuits( if self._MACHINE_DEBUG: for i, ind in enumerate(indices_chunk): handle_list[ind] = ResultHandle( - _DEBUG_HANDLE_PREFIX - + str((batch_chunk[i].n_qubits, n_shots, batch_id)), + _DEBUG_HANDLE_PREFIX + str((n_shots, batch_id)), i, + batch_chunk[i].n_qubits, ppcirc_strs[i], ) else: - job = self._backend.run( - qcs, shots=n_shots, memory=self._config.memory - ) - jobid = job.job_id() + options = Options() + options.optimization_level = 0 + options.resilience_level = 0 + options.transpilation.skip_transpilation = True + options.execution.shots = n_shots + sampler = Sampler(session=self._session, options=options) + job = sampler.run(circuits=qcs) + job_id = job.job_id for i, ind in enumerate(indices_chunk): - handle_list[ind] = ResultHandle(jobid, i, ppcirc_strs[i]) + handle_list[ind] = ResultHandle( + job_id, i, qcs[i].count_ops()["measure"], ppcirc_strs[i] + ) batch_id += 1 for handle in handle_list: assert handle is not None self._cache[handle] = dict() return cast(List[ResultHandle], handle_list) - def _retrieve_job(self, jobid: str) -> IBMQJob: - return self._backend.retrieve_job(jobid) + def _retrieve_job(self, jobid: str) -> RuntimeJob: + return self._service.job(jobid) def cancel(self, handle: ResultHandle) -> None: if not self._MACHINE_DEBUG: jobid = cast(str, handle[0]) job = self._retrieve_job(jobid) - cancelled = job.cancel() - if not cancelled: - warn(f"Unable to cancel job {jobid}") + try: + job.cancel() + except Exception as e: + warn(f"Unable to cancel job {jobid}: {e}") def circuit_status(self, handle: ResultHandle) -> CircuitStatus: self._check_handle_type(handle) jobid = cast(str, handle[0]) - apistatus = self._provider._api_client.job_status(jobid)["status"] - ibmstatus = api_status_to_job_status(apistatus) + job = self._service.job(jobid) + ibmstatus = job.status() return CircuitStatus(_STATUS_MAP[ibmstatus], ibmstatus.value) def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult: @@ -469,33 +469,43 @@ def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResul cached_result = self._cache[handle] if "result" in cached_result: return cast(BackendResult, cached_result["result"]) - jobid, index, ppcirc_str = handle + jobid, index, n_meas, ppcirc_str = handle ppcirc_rep = json.loads(ppcirc_str) ppcirc = Circuit.from_dict(ppcirc_rep) if ppcirc_rep is not None else None cache_key = (jobid, index) if cache_key not in self._ibm_res_cache: if self._MACHINE_DEBUG or jobid.startswith(_DEBUG_HANDLE_PREFIX): shots: int - n_qubits: int - n_qubits, shots, _ = literal_eval(jobid[len(_DEBUG_HANDLE_PREFIX) :]) - res = _gen_debug_results(n_qubits, shots, index) + shots, _ = literal_eval(jobid[len(_DEBUG_HANDLE_PREFIX) :]) + res = _gen_debug_results(n_meas, shots, index) else: try: job = self._retrieve_job(jobid) - except IBMQBackendApiError: + except Exception as e: + warn(f"Unable to retrieve job {jobid}: {e}") raise CircuitNotRunError(handle) if self._monitor and job: job_monitor(job) - newkwargs = { - key: kwargs[key] for key in ("wait", "timeout") if key in kwargs - } - res = job.result(**newkwargs) - - for circ_index, r in enumerate(res.results): - self._ibm_res_cache[(jobid, circ_index)] = r - result = qiskit_experimentresult_to_backendresult( - self._ibm_res_cache[cache_key], ppcirc - ) + + res = job.result(timeout=kwargs.get("timeout", None)) + for circ_index, (r, d) in enumerate(zip(res.quasi_dists, res.metadata)): + self._ibm_res_cache[(jobid, circ_index)] = Counter( + {n: int(0.5 + d["shots"] * p) for n, p in r.items()} + ) + + counts = self._ibm_res_cache[cache_key] # Counter[int] + # Convert to `OutcomeArray`: + tket_counts: Counter = Counter() + for outcome_key, sample_count in counts.items(): + array = OutcomeArray.from_ints( + ints=[outcome_key], + width=n_meas, + big_endian=False, + ) + tket_counts[array] = sample_count + # Convert to `BackendResult`: + result = BackendResult(counts=tket_counts, ppcirc=ppcirc) + self._cache[handle] = {"result": result} return result diff --git a/pytket/extensions/qiskit/backends/ibmq_emulator.py b/pytket/extensions/qiskit/backends/ibmq_emulator.py index 702a1f70..471f341f 100644 --- a/pytket/extensions/qiskit/backends/ibmq_emulator.py +++ b/pytket/extensions/qiskit/backends/ibmq_emulator.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ast import literal_eval +from collections import Counter +import itertools import json from typing import ( cast, @@ -21,44 +24,44 @@ Sequence, Tuple, Union, - TYPE_CHECKING, ) +from warnings import warn from qiskit.providers.aer import AerSimulator # type: ignore from qiskit.providers.aer.noise.noise_model import NoiseModel # type: ignore +from qiskit.providers.ibmq import AccountProvider # type: ignore +from qiskit_ibm_runtime import ( # type: ignore + QiskitRuntimeService, + Session, + Options, + Sampler, + RuntimeJob, +) -from pytket.backends import Backend, CircuitNotRunError, ResultHandle +from pytket.backends import Backend, CircuitNotRunError, ResultHandle, CircuitStatus from pytket.backends.backendinfo import BackendInfo from pytket.backends.backendresult import BackendResult from pytket.backends.resulthandle import _ResultIdTuple -from pytket.circuit import Circuit # type: ignore +from pytket.circuit import Bit, Circuit, OpType # type: ignore from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit -from pytket.extensions.qiskit.result_convert import ( - qiskit_experimentresult_to_backendresult, -) +from pytket.passes import BasePass # type: ignore +from pytket.predicates import Predicate # type: ignore from pytket.utils import prepare_circuit +from pytket.utils.outcomearray import OutcomeArray from pytket.utils.results import KwargTypes -from .aer import AerBackend from .ibm import IBMQBackend -from .ibm_utils import _batch_circuits - -if TYPE_CHECKING: - from pytket.predicates import Predicate # type: ignore - from pytket.passes import BasePass # type: ignore - from qiskit.providers.aer import AerJob # type: ignore - from qiskit.providers.ibmq import AccountProvider # type: ignore - from qiskit.result.models import ExperimentResult # type: ignore +from .ibm_utils import _STATUS_MAP, _batch_circuits -class IBMQEmulatorBackend(AerBackend): - """A backend which uses the AerBackend to emulate the behaviour of IBMQBackend. - Attempts to perform the same compilation and predicate checks as IBMQBackend. +class IBMQEmulatorBackend(Backend): + """A backend which uses the ibmq_qasm_simulator to emulate the behaviour of + IBMQBackend. Performs the same compilation and predicate checks as IBMQBackend. Requires a valid IBMQ account. """ - _supports_shots = True + _supports_shots = False _supports_counts = True _supports_contextual_optimisation = True _persistent_handles = False @@ -71,43 +74,52 @@ def __init__( group: Optional[str] = None, project: Optional[str] = None, account_provider: Optional["AccountProvider"] = None, + token: Optional[str] = None, ): """Construct an IBMQEmulatorBackend. Identical to :py:class:`IBMQBackend` constructor, except there is no `monitor` parameter. See :py:class:`IBMQBackend` docs for more details. """ - + super().__init__() self._ibmq = IBMQBackend( backend_name=backend_name, hub=hub, group=group, project=project, account_provider=account_provider, + token=token, ) + + self._service = QiskitRuntimeService(channel="ibm_quantum", token=token) + self._session = Session(service=self._service, backend="ibmq_qasm_simulator") + + # Get noise model: aer_sim = AerSimulator.from_backend(self._ibmq._backend) - super().__init__(noise_model=NoiseModel.from_backend(aer_sim)) - self._backend = aer_sim + self._noise_model = NoiseModel.from_backend(aer_sim) # cache of results keyed by job id and circuit index - self._ibm_res_cache: Dict[Tuple[str, int], ExperimentResult] = dict() + self._ibm_res_cache: Dict[Tuple[str, int], Counter] = dict() @property def backend_info(self) -> BackendInfo: - return self._ibmq.backend_info - - def rebase_pass(self) -> "BasePass": - return self._ibmq.rebase_pass() + return self._ibmq._backend_info @property - def required_predicates(self) -> List["Predicate"]: - return list(self._ibmq.required_predicates) + def required_predicates(self) -> List[Predicate]: + return self._ibmq.required_predicates - def default_compilation_pass(self, optimisation_level: int = 2) -> "BasePass": - return self._ibmq.default_compilation_pass(optimisation_level) + def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: + return self._ibmq.default_compilation_pass( + optimisation_level=optimisation_level + ) @property def _result_id_type(self) -> _ResultIdTuple: - return (str, int, str) + # job ID, index, stringified sequence of measured bits, post-processing circuit + return (str, int, str, str) + + def rebase_pass(self) -> BasePass: + return self._ibmq.rebase_pass() def process_circuits( self, @@ -124,64 +136,116 @@ def process_circuits( n_shots_list = Backend._get_n_shots_as_list( n_shots, len(circuits), - optional=True, + optional=False, ) - if valid_check: - self._check_all_circuits(circuits) - - postprocess = kwargs.get("postprocess", False) - seed = cast(Optional[int], kwargs.get("seed")) - handle_list: List[Optional[ResultHandle]] = [None] * len(circuits) circuit_batches, batch_order = _batch_circuits(circuits, n_shots_list) + batch_id = 0 # identify batches for debug purposes only for (n_shots, batch), indices in zip(circuit_batches, batch_order): - qcs, ppcirc_strs = [], [] - for tkc in batch: - if postprocess: - c0, ppcirc = prepare_circuit(tkc, allow_classical=False) - ppcirc_rep = ppcirc.to_dict() - else: - c0, ppcirc_rep = tkc, None - qcs.append(tk_to_qiskit(c0)) - ppcirc_strs.append(json.dumps(ppcirc_rep)) - job = self._backend.run( - qcs, - shots=n_shots, - memory=self._memory, - seed_simulator=seed, - noise_model=self._noise_model, - ) - jobid = job.job_id() - for i, ind in enumerate(indices): - handle = ResultHandle(jobid, i, ppcirc_strs[i]) - handle_list[ind] = handle - self._cache[handle] = {"job": job} + for chunk in itertools.zip_longest( + *([iter(zip(batch, indices))] * self._ibmq._max_per_job) + ): + filtchunk = list(filter(lambda x: x is not None, chunk)) + batch_chunk, indices_chunk = zip(*filtchunk) + + if valid_check: + self._check_all_circuits(batch_chunk) + + postprocess = kwargs.get("postprocess", False) + + qcs, c_bit_strs, ppcirc_strs = [], [], [] + for tkc in batch_chunk: + if postprocess: + c0, ppcirc = prepare_circuit(tkc, allow_classical=False) + ppcirc_rep = ppcirc.to_dict() + else: + c0, ppcirc_rep = tkc, None + qcs.append(tk_to_qiskit(c0)) + measured_bits = sorted( + [cmd.args[1] for cmd in tkc if cmd.op.type == OpType.Measure] + ) + c_bit_strs.append( + repr([(b.reg_name, b.index) for b in measured_bits]) + ) + ppcirc_strs.append(json.dumps(ppcirc_rep)) + options = Options() + options.resilience_level = 0 + options.execution.shots = n_shots + options.simulator.noise_model = self._noise_model + options.seed_simulator = kwargs.get("seed") + sampler = Sampler(session=self._session, options=options) + job = sampler.run(circuits=qcs) + job_id = job.job_id + for i, ind in enumerate(indices_chunk): + handle_list[ind] = ResultHandle( + job_id, i, c_bit_strs[i], ppcirc_strs[i] + ) + batch_id += 1 + for handle in handle_list: + assert handle is not None + self._cache[handle] = dict() return cast(List[ResultHandle], handle_list) + def _retrieve_job(self, jobid: str) -> RuntimeJob: + return self._service.job(jobid) + + def cancel(self, handle: ResultHandle) -> None: + jobid = cast(str, handle[0]) + job = self._retrieve_job(jobid) + try: + job.cancel() + except Exception as e: + warn(f"Unable to cancel job {jobid}: {e}") + + def circuit_status(self, handle: ResultHandle) -> CircuitStatus: + self._check_handle_type(handle) + jobid = cast(str, handle[0]) + job = self._service.job(jobid) + ibmstatus = job.status() + return CircuitStatus(_STATUS_MAP[ibmstatus], ibmstatus.value) + def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult: """ See :py:meth:`pytket.backends.Backend.get_result`. - Supported kwargs: none. + Supported kwargs: `timeout`, `wait`. """ self._check_handle_type(handle) - if handle in self._cache and "result" in self._cache[handle]: - return cast(BackendResult, self._cache[handle]["result"]) - jobid, index, ppcirc_str = handle + if handle in self._cache: + cached_result = self._cache[handle] + if "result" in cached_result: + return cast(BackendResult, cached_result["result"]) + jobid, index, c_bit_str, ppcirc_str = handle + c_bits = [Bit(reg_name, index) for reg_name, index in literal_eval(c_bit_str)] ppcirc_rep = json.loads(ppcirc_str) ppcirc = Circuit.from_dict(ppcirc_rep) if ppcirc_rep is not None else None cache_key = (jobid, index) if cache_key not in self._ibm_res_cache: try: - job: "AerJob" = self._cache[handle]["job"] - except KeyError: + job = self._retrieve_job(jobid) + except Exception as e: + warn(f"Unable to retrieve job {jobid}: {e}") raise CircuitNotRunError(handle) - res = job.result() - for circ_index, r in enumerate(res.results): - self._ibm_res_cache[(jobid, circ_index)] = r - result = qiskit_experimentresult_to_backendresult( - self._ibm_res_cache[cache_key], ppcirc - ) + + res = job.result(timeout=kwargs.get("timeout", None)) + for circ_index, (r, d) in enumerate(zip(res.quasi_dists, res.metadata)): + self._ibm_res_cache[(jobid, circ_index)] = Counter( + {n: int(0.5 + d["shots"] * p) for n, p in r.items()} + ) + + counts = self._ibm_res_cache[cache_key] # Counter[int] + # Convert to `OutcomeArray`: + tket_counts: Counter = Counter() + for outcome_key, sample_count in counts.items(): + array = OutcomeArray.from_ints( + ints=[outcome_key], + width=len(c_bits), + big_endian=False, + ) + tket_counts[array] = sample_count + # Convert to `BackendResult`: + result = BackendResult(c_bits=c_bits, counts=tket_counts, ppcirc=ppcirc) + self._cache[handle] = {"result": result} return result diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index 61c111f7..fe393c9b 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -548,31 +548,6 @@ def append_tk_command_to_qiskit( return qcirc.append(g, qargs=qargs) -def _get_implicit_swaps(circuit: Circuit) -> List[Tuple[Qubit, Qubit]]: - # We implement the implicit qubit permutation using SWAPs - qubits = circuit.qubits - perm = circuit.implicit_qubit_permutation() - # output wire -> qubit - qubit_2_wire = wire_2_qubit = {q: q for q in qubits} - # qubit -> output wire - swaps = [] - for q in qubits: - q_wire = qubit_2_wire[q] - target_wire = perm[q] - if q_wire == target_wire: - continue - # find which qubit is on target_wire - p = wire_2_qubit[target_wire] - # swap p and q so q is on the target wire - swaps.append((q_wire, target_wire)) - # update dicts - qubit_2_wire[q] = target_wire - qubit_2_wire[p] = q_wire - wire_2_qubit[q_wire] = p - wire_2_qubit[target_wire] = q - return swaps - - # Define varibles for RebaseCustom _cx_replacement = Circuit(2).CX(0, 1) @@ -612,12 +587,15 @@ def tk_to_qiskit( :type tkcirc: Circuit :param reverse_index: Reverse the order of wires :type reverse_index: bool - :param replace_implicit_swaps: Implement implicit permutation using SWAPs + :param replace_implicit_swaps: Implement implicit permutation by adding SWAPs + to the end of the circuit. :type replace_implicit_swaps: bool :return: The converted circuit :rtype: QuantumCircuit """ tkc = tkcirc.copy() # Make a local copy of tkcirc + if replace_implicit_swaps: + tkc.replace_implicit_wire_swaps() qcirc = QuantumCircuit(name=tkc.name) qreg_sizes: Dict[str, int] = {} for qb in tkc.qubits: @@ -671,14 +649,6 @@ def tk_to_qiskit( updates[p] = new_p qcirc.assign_parameters(updates, inplace=True) - if replace_implicit_swaps: - swaps = _get_implicit_swaps(tkc) - for p, q in swaps: - qcirc.swap( - qregmap[p.reg_name][p.index[0]], - qregmap[q.reg_name][q.index[0]], - ) - if reverse_index: return qcirc.reverse_bits() return qcirc diff --git a/pytket/extensions/qiskit/tket_pass.py b/pytket/extensions/qiskit/tket_pass.py index dfa5829a..7baa1e65 100644 --- a/pytket/extensions/qiskit/tket_pass.py +++ b/pytket/extensions/qiskit/tket_pass.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from qiskit.dagcircuit import DAGCircuit # type: ignore from qiskit.providers import BackendV1 # type: ignore from qiskit.transpiler.basepasses import TransformationPass, BasePass as qBasePass # type: ignore @@ -73,7 +74,12 @@ class TketAutoPass(TketPass): "aer_simulator_unitary": AerUnitaryBackend, } - def __init__(self, backend: BackendV1, optimisation_level: int = 2): + def __init__( + self, + backend: BackendV1, + optimisation_level: int = 2, + token: Optional[str] = None, + ): """Identifies a Qiskit backend and provides the corresponding default compilation pass from pytket as a :py:class:`qiskit.transpiler.TransformationPass`. @@ -84,11 +90,13 @@ def __init__(self, backend: BackendV1, optimisation_level: int = 2): optimising. Level 1 additionally performs some light optimisations. Level 2 adds more computationally intensive optimisations. Defaults to 2. :type optimisation_level: int, optional + :param token: Authentication token to use the `QiskitRuntimeService`. + :type token: Optional[str] """ if isinstance(backend._provider, AerProvider): tk_backend = self._aer_backend_map[backend.name()]() elif isinstance(backend._provider, AccountProvider): - tk_backend = IBMQBackend(backend.name()) + tk_backend = IBMQBackend(backend.name(), token=token) else: raise NotImplementedError("This backend provider is not supported.") super().__init__(tk_backend.default_compilation_pass(optimisation_level)) diff --git a/setup.py b/setup.py index e34c97e3..8db503be 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,11 @@ license="Apache 2", packages=find_namespace_packages(include=["pytket.*"]), include_package_data=True, - install_requires=["pytket ~= 1.9", "qiskit ~= 0.39.0"], + install_requires=[ + "pytket ~= 1.9", + "qiskit ~= 0.39.0", + "qiskit_ibm_runtime ~= 0.8.0", + ], classifiers=[ "Environment :: Console", "Programming Language :: Python :: 3.8", diff --git a/tests/backend_test.py b/tests/backend_test.py index 4aa6b9f9..fd260d56 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -107,6 +107,23 @@ def test_statevector() -> None: assert np.allclose(state1, state * 1j, atol=1e-10) +def test_statevector_sim_with_permutation() -> None: + # https://github.com/CQCL/pytket-qiskit/issues/35 + b = AerStateBackend() + c = Circuit(3).X(0).SWAP(0, 1).SWAP(0, 2) + qubits = c.qubits + sv = b.run_circuit(c).get_state() + # convert swaps to implicit permutation + c.replace_SWAPs() + assert c.implicit_qubit_permutation() == { + qubits[0]: qubits[1], + qubits[1]: qubits[2], + qubits[2]: qubits[0], + } + sv1 = b.run_circuit(c).get_state() + assert np.allclose(sv, sv1, atol=1e-10) + + def test_sim() -> None: c = circuit_gen(True) b = AerBackend() @@ -397,16 +414,13 @@ def test_machine_debug(manila_backend: IBMQBackend) -> None: cast(str, hand[0]).startswith(_DEBUG_HANDLE_PREFIX) for hand in handles ) - correct_shots = np.zeros((4, 2)) correct_counts = {(0, 0): 4} res = backend.run_circuit(c, n_shots=4) - assert np.all(res.get_shots() == correct_shots) assert res.get_counts() == correct_counts # check that generating new shots still works res = backend.run_circuit(c, n_shots=4) - assert np.all(res.get_shots() == correct_shots) assert res.get_counts() == correct_counts finally: # ensure shared backend is reset for other tests @@ -433,7 +447,7 @@ def test_nshots_batching(manila_backend: IBMQBackend) -> None: cast(str, hand[0]) == _DEBUG_HANDLE_PREFIX + suffix for hand, suffix in zip( handles, - [f"{(2, 10, 0)}", f"{(2, 12, 1)}", f"{(2, 10, 0)}", f"{(2, 13, 2)}"], + [f"{(10, 0)}", f"{(12, 1)}", f"{(10, 0)}", f"{(13, 2)}"], ) ) finally: @@ -441,20 +455,14 @@ def test_nshots_batching(manila_backend: IBMQBackend) -> None: backend._MACHINE_DEBUG = False -def test_nshots() -> None: - backends = [AerBackend()] - if not skip_remote_tests: - backends.append( - IBMQEmulatorBackend( - "ibmq_manila", hub="ibm-q", group="open", project="main" - ) - ) - for b in backends: +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +def test_nshots(manila_emulator_backend: IBMQEmulatorBackend) -> None: + for b in [AerBackend(), manila_emulator_backend]: circuit = Circuit(1).X(0) circuit.measure_all() n_shots = [1, 2, 3] results = b.get_results(b.process_circuits([circuit] * 3, n_shots=n_shots)) - assert [len(r.get_shots()) for r in results] == n_shots + assert [sum(r.get_counts().values()) for r in results] == n_shots def test_pauli_statevector() -> None: @@ -807,15 +815,12 @@ def test_aer_placed_expectation() -> None: @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_ibmq_emulator() -> None: - b_emu = IBMQEmulatorBackend( - "ibmq_manila", hub="ibm-q", group="open", project="main" - ) - assert b_emu._noise_model is not None - b_ibm = b_emu._ibmq +def test_ibmq_emulator(manila_emulator_backend: IBMQEmulatorBackend) -> None: + assert manila_emulator_backend._noise_model is not None + b_ibm = manila_emulator_backend._ibmq b_aer = AerBackend() for ol in range(3): - comp_pass = b_emu.default_compilation_pass(ol) + comp_pass = manila_emulator_backend.default_compilation_pass(ol) c = Circuit(3, 3) c.H(0) c.CX(0, 1) @@ -824,23 +829,29 @@ def test_ibmq_emulator() -> None: c_cop = c.copy() comp_pass.apply(c_cop) c.measure_all() - for bac in (b_emu, b_ibm): + for bac in (manila_emulator_backend, b_ibm): assert all(pred.verify(c_cop) for pred in bac.required_predicates) c_cop_2 = c.copy() c_cop_2 = b_aer.get_compiled_circuit(c_cop_2, ol) if ol == 0: - assert not all(pred.verify(c_cop_2) for pred in b_emu.required_predicates) + assert not all( + pred.verify(c_cop_2) + for pred in manila_emulator_backend.required_predicates + ) circ = Circuit(2, 2).H(0).CX(0, 1).measure_all() copy_circ = circ.copy() - b_emu.rebase_pass().apply(copy_circ) - assert b_emu.required_predicates[1].verify(copy_circ) - circ = b_emu.get_compiled_circuit(circ) - b_noi = AerBackend(noise_model=b_emu._noise_model) - emu_shots = b_emu.run_circuit(circ, n_shots=10, seed=10).get_shots() - aer_shots = b_noi.run_circuit(circ, n_shots=10, seed=10).get_shots() - assert np.array_equal(emu_shots, aer_shots) + manila_emulator_backend.rebase_pass().apply(copy_circ) + assert manila_emulator_backend.required_predicates[1].verify(copy_circ) + circ = manila_emulator_backend.get_compiled_circuit(circ) + b_noi = AerBackend(noise_model=manila_emulator_backend._noise_model) + emu_counts = manila_emulator_backend.run_circuit( + circ, n_shots=10, seed=10 + ).get_counts() + aer_counts = b_noi.run_circuit(circ, n_shots=10, seed=10).get_counts() + # Even with the same seed, the results may differ. + assert sum(emu_counts.values()) == sum(aer_counts.values()) @given( @@ -903,19 +914,19 @@ def test_aer_expanded_gates() -> None: @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_remote_simulator() -> None: - remote_qasm = IBMQBackend( - "ibmq_qasm_simulator", hub="ibm-q", group="open", project="main" - ) +def test_remote_simulator(qasm_simulator_backend: IBMQBackend) -> None: c = Circuit(3).CX(0, 1) c.add_gate(OpType.ZZPhase, 0.1, [0, 1]) c.add_gate(OpType.CY, [0, 1]) c.add_gate(OpType.CCX, [0, 1, 2]) c.measure_all() - assert remote_qasm.valid_circuit(c) + assert qasm_simulator_backend.valid_circuit(c) - assert sum(remote_qasm.run_circuit(c, n_shots=10).get_counts().values()) == 10 + assert ( + sum(qasm_simulator_backend.run_circuit(c, n_shots=10).get_counts().values()) + == 10 + ) @pytest.mark.skipif(skip_remote_tests, reason=REASON) @@ -1056,7 +1067,7 @@ def test_postprocess(lima_backend: IBMQBackend) -> None: c.SX(0).SX(1).CX(0, 1).measure_all() c = b.get_compiled_circuit(c) h = b.process_circuit(c, n_shots=10, postprocess=True) - ppcirc = Circuit.from_dict(json.loads(cast(str, h[2]))) + ppcirc = Circuit.from_dict(json.loads(cast(str, h[3]))) ppcmds = ppcirc.get_commands() assert len(ppcmds) > 0 assert all(ppcmd.op.type == OpType.ClassicalTransform for ppcmd in ppcmds) @@ -1064,35 +1075,33 @@ def test_postprocess(lima_backend: IBMQBackend) -> None: @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_postprocess_emu() -> None: - b = IBMQEmulatorBackend("ibmq_manila", hub="ibm-q", group="open", project="main") - assert b.supports_contextual_optimisation +def test_postprocess_emu(manila_emulator_backend: IBMQEmulatorBackend) -> None: + assert manila_emulator_backend.supports_contextual_optimisation c = Circuit(2, 2) c.SX(0).SX(1).CX(0, 1).measure_all() - c = b.get_compiled_circuit(c) - h = b.process_circuit(c, n_shots=10, postprocess=True) - ppcirc = Circuit.from_dict(json.loads(cast(str, h[2]))) + c = manila_emulator_backend.get_compiled_circuit(c) + h = manila_emulator_backend.process_circuit(c, n_shots=10, postprocess=True) + ppcirc = Circuit.from_dict(json.loads(cast(str, h[3]))) ppcmds = ppcirc.get_commands() assert len(ppcmds) > 0 assert all(ppcmd.op.type == OpType.ClassicalTransform for ppcmd in ppcmds) - r = b.get_result(h) - shots = r.get_shots() - assert len(shots) == 10 + r = manila_emulator_backend.get_result(h) + counts = r.get_counts() + assert sum(counts.values()) == 10 @pytest.mark.timeout(None) @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_cloud_stabiliser() -> None: - b = IBMQBackend("simulator_stabilizer", hub="ibm-q", group="open", project="main") +def test_cloud_stabiliser(simulator_stabilizer_backend: IBMQBackend) -> None: c = Circuit(2, 2) c.H(0).SX(1).CX(0, 1).measure_all() - c = b.get_compiled_circuit(c, 0) - h = b.process_circuit(c, n_shots=10) - assert sum(b.get_result(h).get_counts().values()) == 10 + c = simulator_stabilizer_backend.get_compiled_circuit(c, 0) + h = simulator_stabilizer_backend.process_circuit(c, n_shots=10) + assert sum(simulator_stabilizer_backend.get_result(h).get_counts().values()) == 10 c = Circuit(2, 2) c.H(0).SX(1).Rz(0.1, 0).CX(0, 1).measure_all() - assert not b.valid_circuit(c) + assert not simulator_stabilizer_backend.valid_circuit(c) @pytest.mark.skipif(skip_remote_tests, reason=REASON) @@ -1112,12 +1121,11 @@ def test_available_devices() -> None: @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_backendinfo_serialization1() -> None: +def test_backendinfo_serialization1( + manila_emulator_backend: IBMQEmulatorBackend, +) -> None: # https://github.com/CQCL/tket/issues/192 - backend = IBMQEmulatorBackend( - "ibmq_manila", hub="ibm-q", group="open", project="main" - ) - backend_info_json = backend.backend_info.to_dict() + backend_info_json = manila_emulator_backend.backend_info.to_dict() s = json.dumps(backend_info_json) backend_info_json1 = json.loads(s) assert backend_info_json == backend_info_json1 diff --git a/tests/conftest.py b/tests/conftest.py index 3e336e21..bb3a5819 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ import os import pytest from qiskit import IBMQ # type: ignore -from pytket.extensions.qiskit import IBMQBackend +from pytket.extensions.qiskit import IBMQBackend, IBMQEmulatorBackend @pytest.fixture(autouse=True, scope="session") @@ -34,9 +34,66 @@ def setup_qiskit_account() -> None: @pytest.fixture(scope="module") def manila_backend() -> IBMQBackend: - return IBMQBackend("ibmq_manila", hub="ibm-q", group="open", project="main") + return IBMQBackend( + "ibmq_manila", + hub="ibm-q", + group="open", + project="main", + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) @pytest.fixture(scope="module") def lima_backend() -> IBMQBackend: - return IBMQBackend("ibmq_lima", hub="ibm-q", group="open", project="main") + return IBMQBackend( + "ibmq_lima", + hub="ibm-q", + group="open", + project="main", + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) + + +@pytest.fixture(scope="module") +def qasm_simulator_backend() -> IBMQBackend: + return IBMQBackend( + "ibmq_qasm_simulator", + hub="ibm-q", + group="open", + project="main", + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) + + +@pytest.fixture(scope="module") +def simulator_stabilizer_backend() -> IBMQBackend: + return IBMQBackend( + "simulator_stabilizer", + hub="ibm-q", + group="open", + project="main", + monitor=False, + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) + + +@pytest.fixture(scope="module") +def manila_emulator_backend() -> IBMQEmulatorBackend: + return IBMQEmulatorBackend( + "ibmq_manila", + hub="ibm-q", + group="open", + project="main", + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) + + +@pytest.fixture(scope="module") +def belem_emulator_backend() -> IBMQEmulatorBackend: + return IBMQEmulatorBackend( + "ibmq_belem", + hub="ibm-q", + group="open", + project="main", + token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN"), + ) diff --git a/tests/qiskit_backend_test.py b/tests/qiskit_backend_test.py index 48629f35..38429d00 100644 --- a/tests/qiskit_backend_test.py +++ b/tests/qiskit_backend_test.py @@ -118,16 +118,14 @@ def test_cancel() -> None: @pytest.mark.skipif(skip_remote_tests, reason=REASON) -def test_qiskit_counts(provider: Optional[AccountProvider]) -> None: +def test_qiskit_counts(belem_emulator_backend: IBMQEmulatorBackend) -> None: num_qubits = 2 qc = QuantumCircuit(num_qubits) qc.h(0) qc.cx(0, 1) circfn = CircuitStateFn(qc) - b = IBMQEmulatorBackend("ibmq_belem", account_provider=provider) - - s = CircuitSampler(TketBackend(b)) + s = CircuitSampler(TketBackend(belem_emulator_backend)) res = s.sample_circuits([circfn]) diff --git a/tests/qiskit_convert_test.py b/tests/qiskit_convert_test.py index bd673ce6..d121980d 100644 --- a/tests/qiskit_convert_test.py +++ b/tests/qiskit_convert_test.py @@ -282,7 +282,9 @@ def test_tketautopass() -> None: backends.append(provider.get_backend("ibmq_manila")) for back in backends: for o_level in range(3): - tkpass = TketAutoPass(back, o_level) + tkpass = TketAutoPass( + back, o_level, token=os.getenv("PYTKET_REMOTE_QISKIT_TOKEN") + ) qc = get_test_circuit(True) pm = PassManager(passes=tkpass) pm.run(qc)