Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding backend related methods to QiskitRuntimeLocalService #1764

Merged
merged 12 commits into from
Jun 26, 2024
4 changes: 4 additions & 0 deletions qiskit_ibm_runtime/fake_provider/fake_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
95 changes: 91 additions & 4 deletions qiskit_ibm_runtime/fake_provider/local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -39,9 +43,7 @@
class QiskitRuntimeLocalService:
"""Class for local testing mode."""

def __init__(
self,
) -> None:
def __init__(self) -> None:
"""QiskitRuntimeLocalService constructor.


Expand All @@ -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"],
Expand Down
4 changes: 3 additions & 1 deletion qiskit_ibm_runtime/qiskit_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions release-notes/unreleased/1764.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``backend``, ``backends``, and ``least_busy`` methods to ``QiskitRuntimeLocalService``.
97 changes: 97 additions & 0 deletions test/unit/fake_provider/test_local_service.py
Original file line number Diff line number Diff line change
@@ -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)