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

Add refresh method to fake backends #1740

Merged
merged 24 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion qiskit_ibm_runtime/fake_provider/fake_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"""
Base class for dummy backends.
"""

import logging
import warnings
import collections
import json
Expand All @@ -40,9 +40,13 @@
from qiskit.providers.basic_provider import BasicSimulator

from qiskit_ibm_runtime.utils.backend_converter import convert_to_target
from .. import QiskitRuntimeService
from ..utils.backend_encoder import BackendEncoder

from ..utils.deprecation import issue_deprecation_msg

logger = logging.getLogger(__name__)


class _Credentials:
def __init__(self, token: str = "123456", url: str = "https://") -> None:
Expand Down Expand Up @@ -458,6 +462,65 @@ def _get_noise_model_from_backend_v2( # type: ignore

return noise_model

def refresh(self, service: QiskitRuntimeService) -> None:
"""Update the data files from its real counterpart

This method pulls the latest backend data files from their real counterpart and
overwrites the corresponding files in the local installation:
* ../fake_provider/backends/{backend_name}/conf_{backend_name}.json
* ../fake_provider/backends/{backend_name}/defs_{backend_name}.json
* ../fake_provider/backends/{backend_name}/props_{backend_name}.json

The new data files will persist through sessions so the files will stay updated unless they
are manually reverted locally or when qiskit-ibm-runtime is upgraded/reinstalled.

Args:
service: A :class:`QiskitRuntimeService` instance

Raises:
Exception: If the real target doesn't exist or can't be accessed
"""

version = self.backend_version
prod_name = self.backend_name.replace("fake", "ibm")
try:
backends = service.backends(prod_name)
real_backend = backends[0]

real_props = real_backend.properties()
real_config = real_backend.configuration()
real_defs = real_backend.defaults()

if real_props:
new_version = real_props.backend_version

if new_version > version:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we really rely on the version? I don't think it gets updated every time new calibration is pushed. And why even check the version if the server side data is already the freshest.

props_path = os.path.join(self.dirname, self.props_filename)
with open(props_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(real_props.to_dict(), cls=BackendEncoder))
kt474 marked this conversation as resolved.
Show resolved Hide resolved

if real_config:
config_path = os.path.join(self.dirname, self.conf_filename)
config_dict = real_config.to_dict()
with open(config_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(config_dict, cls=BackendEncoder))

if real_defs:
defs_path = os.path.join(self.dirname, self.defs_filename)
with open(defs_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(real_defs.to_dict(), cls=BackendEncoder))

logger.info(
"The backend %s has been updated from {version} to %s version.",
self.backend_name,
real_props.backend_version,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just updates the files but doesn't actually refresh the target if it's already built since it's cached.

else:
logger.info("There are no available new updates for %s.", self.backend_name)

except Exception as ex:
logger.info("The refreshing of %s has failed: %s", self.backend_name, str(ex))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should raise an exception or at least a warning. Info level log doesn't get surfaced by default.



class FakeBackend(BackendV1):
"""This is a dummy backend just for testing purposes."""
Expand Down
36 changes: 36 additions & 0 deletions qiskit_ibm_runtime/utils/backend_encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# 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.

"""Utilities for working with IBM Quantum backends."""
import json
from datetime import datetime
from typing import Any

from qiskit.circuit import ParameterExpression


class BackendEncoder(json.JSONEncoder):
"""A json encoder for qobj"""

def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
"""Default encoding"""
# Convert numpy arrays:
if hasattr(obj, "tolist"):
return obj.tolist()
# Use Qobj complex json format:
if isinstance(obj, complex):
return [obj.real, obj.imag]
if isinstance(obj, ParameterExpression):
return float(obj)
if isinstance(obj, datetime):
return obj.isoformat()
return json.JSONEncoder.default(self, obj)
4 changes: 4 additions & 0 deletions release-notes/unreleased/1740.upgrade.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The instances of :class:`FakeBackend` can now be updated from
their real counterpart using the :meth:`FakeBackendV2.refresh`.
To pull the latest real backend data files, access permission is
required.
19 changes: 18 additions & 1 deletion test/integration/test_fake_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
FakeSherbrooke,
FakePrague,
)
from ..ibm_test_case import IBMTestCase
from ..ibm_test_case import IBMTestCase, IBMIntegrationTestCase

FAKE_PROVIDER_FOR_BACKEND_V2 = FakeProviderForBackendV2()
FAKE_PROVIDER = FakeProvider()
Expand Down Expand Up @@ -171,3 +171,20 @@ def test_non_cx_tests(self):
self.assertIsInstance(backend.target.operation_from_name("cz"), CZGate)
backend = FakeSherbrooke()
self.assertIsInstance(backend.target.operation_from_name("ecr"), ECRGate)


class TestRefreshFakeBackends(IBMIntegrationTestCase):

@classmethod
def setUpClass(cls):
# pylint: disable=arguments-differ
# pylint: disable=no-value-for-parameter
super().setUpClass()

def test_refresh_method(self):
"""Test refresh method"""
old_backend = FakeSherbrooke()
with self.assertLogs("qiskit_ibm_runtime", level="INFO"):
old_backend.refresh(self.service)
new_backend = FakeSherbrooke()
self.assertGreaterEqual(old_backend.backend_version, new_backend.backend_version)
Comment on lines +186 to +190
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand this test. If FakeSherbrooke currently has version 1. The server data has version 2. If the refresh() code is not working correctly, both old_backend and new_backend would remain at version 1, and the test would still pass.