diff --git a/qiskit_ibm_runtime/fake_provider/fake_backend.py b/qiskit_ibm_runtime/fake_provider/fake_backend.py index 69ea4c5e8..39d53e42f 100644 --- a/qiskit_ibm_runtime/fake_provider/fake_backend.py +++ b/qiskit_ibm_runtime/fake_provider/fake_backend.py @@ -158,6 +158,10 @@ def _set_defs_dict_from_json(self) -> None: decode_pulse_defaults(defs_dict) self._defs_dict = defs_dict + def _supports_dynamic_circuits(self) -> bool: + supported_features = self._conf_dict.get("supported_features") or [] + return "qasm3" in supported_features + def _load_json(self, filename: str) -> dict: with open( # pylint: disable=unspecified-encoding os.path.join(self.dirname, filename) diff --git a/qiskit_ibm_runtime/fake_provider/local_service.py b/qiskit_ibm_runtime/fake_provider/local_service.py index 9b9717827..d8ba49724 100644 --- a/qiskit_ibm_runtime/fake_provider/local_service.py +++ b/qiskit_ibm_runtime/fake_provider/local_service.py @@ -18,7 +18,7 @@ import logging import warnings from dataclasses import asdict -from typing import Dict, Literal, Union +from typing import Callable, Dict, List, Literal, Optional, Union from qiskit.primitives import ( BackendEstimator, @@ -28,8 +28,12 @@ ) from qiskit.primitives.primitive_job import PrimitiveJob from qiskit.providers.backend import BackendV1, BackendV2 +from qiskit.providers.exceptions import QiskitBackendNotFoundError +from qiskit.providers.providerutils import filter_backends from qiskit.utils import optionals +from .fake_backend import FakeBackendV2 # pylint: disable=cyclic-import +from .fake_provider import FakeProviderForBackendV2 # pylint: disable=unused-import, cyclic-import from ..ibm_backend import IBMBackend from ..runtime_options import RuntimeOptions @@ -39,9 +43,7 @@ class QiskitRuntimeLocalService: """Class for local testing mode.""" - def __init__( - self, - ) -> None: + def __init__(self) -> None: """QiskitRuntimeLocalService constructor. @@ -51,6 +53,91 @@ def __init__( """ self._channel_strategy = None + def backend(self, name: str = None) -> FakeBackendV2: + """Return a single fake backend matching the specified filters. + + Args: + name: The name of the backend. + + Returns: + Backend: A backend matching the filtering. + """ + return self.backends(name=name)[0] + + def backends( + self, + name: Optional[str] = None, + min_num_qubits: Optional[int] = None, + dynamic_circuits: Optional[bool] = None, + filters: Optional[Callable[[FakeBackendV2], bool]] = None, + ) -> List[FakeBackendV2]: + """Return all the available fake backends, subject to optional filtering. + + Args: + name: Backend name to filter by. + min_num_qubits: Minimum number of qubits the fake backend has to have. + dynamic_circuits: Filter by whether the fake backend supports dynamic circuits. + filters: More complex filters, such as lambda functions. + For example:: + + from qiskit_ibm_runtime.fake_provider.local_service import QiskitRuntimeLocalService + + QiskitRuntimeService.backends( + filters=lambda backend: (backend.online_date.year == 2021) + ) + QiskitRuntimeLocalService.backends( + filters=lambda backend: (backend.num_qubits > 30 and backend.num_qubits < 100) + ) + + Returns: + The list of available fake backends that match the filters. + + Raises: + QiskitBackendNotFoundError: If none of the available fake backends matches the given + filters. + """ + backends = FakeProviderForBackendV2().backends(name) + err = QiskitBackendNotFoundError("No backend matches the criteria.") + + if name: + for b in backends: + if b.name == name: + backends = [b] + break + else: + raise err + + if min_num_qubits: + backends = [b for b in backends if b.num_qubits >= min_num_qubits] + + if dynamic_circuits is not None: + backends = [b for b in backends if b._supports_dynamic_circuits() == dynamic_circuits] + + backends = filter_backends(backends, filters=filters) + + if not backends: + raise err + + return backends + + def least_busy( + self, + min_num_qubits: Optional[int] = None, + filters: Optional[Callable[[FakeBackendV2], bool]] = None, + ) -> FakeBackendV2: + """Mimics the :meth:`QiskitRuntimeService.least_busy` method by returning a randomly-chosen + fake backend. + + Args: + min_num_qubits: Minimum number of qubits the fake backend has to have. + filters: More complex filters, such as lambda functions, that can be defined as for the + :meth:`backends` method. + + Returns: + A fake backend. + """ + return self.backends(min_num_qubits=min_num_qubits, filters=filters)[0] + def run( self, program_id: Literal["sampler", "estimator"], diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index d3e483f74..abd6908a4 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -471,9 +471,11 @@ def backends( For example:: QiskitRuntimeService.backends( - filters=lambda b: b.max_shots > 50000) + filters=lambda b: b.max_shots > 50000 + ) QiskitRuntimeService.backends( filters=lambda x: ("rz" in x.basis_gates ) + ) use_fractional_gates: Set True to allow for the backends to include fractional gates in target. Currently this feature cannot be used simulataneously with the dynamic circuits, PEC, or PEA. diff --git a/release-notes/unreleased/1764.feat.rst b/release-notes/unreleased/1764.feat.rst new file mode 100644 index 000000000..71acba2a4 --- /dev/null +++ b/release-notes/unreleased/1764.feat.rst @@ -0,0 +1 @@ +Added ``backend``, ``backends``, and ``least_busy`` methods to ``QiskitRuntimeLocalService``. diff --git a/test/unit/fake_provider/test_local_service.py b/test/unit/fake_provider/test_local_service.py new file mode 100644 index 000000000..73f68e3f9 --- /dev/null +++ b/test/unit/fake_provider/test_local_service.py @@ -0,0 +1,97 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test of generated fake backends.""" +from ddt import data, ddt + +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from qiskit_ibm_runtime.fake_provider.fake_backend import FakeBackendV2 +from qiskit_ibm_runtime.fake_provider import FakeAlgiers, FakeTorino, FakeProviderForBackendV2 +from qiskit_ibm_runtime.fake_provider.local_service import QiskitRuntimeLocalService +from ...ibm_test_case import IBMTestCase + + +@ddt +class QiskitRuntimeLocalServiceTest(IBMTestCase): + """Qiskit runtime local service test.""" + + def test_backend(self): + """Tests the ``backend`` method.""" + service = QiskitRuntimeLocalService() + assert isinstance(service.backend(), FakeBackendV2) + assert isinstance(service.backend("fake_algiers"), FakeAlgiers) + assert isinstance(service.backend("fake_torino"), FakeTorino) + + def test_backends(self): + """Tests the ``backends`` method.""" + all_backends = QiskitRuntimeLocalService().backends() + expected = FakeProviderForBackendV2().backends() + assert len(all_backends) == len(expected) + + for b1, b2 in zip(all_backends, expected): + assert isinstance(b1, b2.__class__) + + def test_backends_name_filter(self): + """Tests the ``name`` filter of the ``backends`` method.""" + backends = QiskitRuntimeLocalService().backends("fake_torino") + assert len(backends) == 1 + assert isinstance(backends[0], FakeTorino) + + def test_backends_min_num_qubits_filter(self): + """Tests the ``min_num_qubits`` filter of the ``backends`` method.""" + for b in QiskitRuntimeLocalService().backends(min_num_qubits=27): + assert b.num_qubits >= 27 + + @data(False, True) + def test_backends_dynamic_circuits_filter(self, supports): + """Tests the ``dynamic_circuits`` filter of the ``backends`` method.""" + for b in QiskitRuntimeLocalService().backends(dynamic_circuits=supports): + assert b._supports_dynamic_circuits() == supports + + def test_backends_filters(self): + """Tests the ``filters`` argument of the ``backends`` method.""" + for b in QiskitRuntimeLocalService().backends( + filters=lambda b: (b.online_date.year == 2021) + ): + assert b.online_date.year == 2021 + + for b in QiskitRuntimeLocalService().backends( + filters=lambda b: (b.num_qubits > 30 and b.num_qubits < 100) + ): + assert b.num_qubits > 30 and b.num_qubits < 100 + + def test_backends_filters_combined(self): + """Tests the ``backends`` method with more than one filter.""" + service = QiskitRuntimeLocalService() + + backends1 = service.backends(name="fake_torino", min_num_qubits=27) + assert len(backends1) == 1 + assert isinstance(backends1[0], FakeTorino) + + backends2 = service.backends( + min_num_qubits=27, filters=lambda b: (b.online_date.year == 2021) + ) + assert len(backends2) == 7 + + def test_backends_errors(self): + """Tests the errors raised by the ``backends`` method.""" + service = QiskitRuntimeLocalService() + + with self.assertRaises(QiskitBackendNotFoundError): + service.backends("torino") + with self.assertRaises(QiskitBackendNotFoundError): + service.backends("fake_torino", filters=lambda b: (b.online_date.year == 1992)) + + def test_least_busy(self): + """Tests the ``least_busy`` method.""" + assert isinstance(QiskitRuntimeLocalService().least_busy(), FakeBackendV2)