Skip to content

Commit

Permalink
Remove references to Provider class
Browse files Browse the repository at this point in the history
  • Loading branch information
wshanks committed Dec 10, 2024
1 parent 9bf2848 commit e411b27
Showing 1 changed file with 128 additions and 97 deletions.
225 changes: 128 additions & 97 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from __future__ import annotations
import logging
import re
from typing import Dict, Optional, List, Union, Any, Callable, Tuple, TYPE_CHECKING
from typing import Dict, Optional, List, Union, Any, Callable, Protocol, Tuple, TYPE_CHECKING
from datetime import datetime, timezone
from concurrent import futures
from functools import wraps
Expand Down Expand Up @@ -81,6 +81,48 @@
LOG = logging.getLogger(__name__)


class BaseProvider(Protocol):
"""Interface definition of a provider class as needed for experiment data"""

def job(self, job_id: str) -> Job:
"""Retrieve a job object using its job ID
Args:
job_id: Job ID.
Returns:
The retrieved job
"""
raise NotImplementedError


class IBMProvider(BaseProvider, Protocol):
"""Provider interface needed for supporting features like IBM Quantum
This interface is the subset of
:class:`~qiskit_ibm_runtime.QiskitRuntimeService` needed for all features
of Qiskit Experiments. Another provider could implement this interface to
support these features as well.
"""
def active_account(self) -> dict[str, str] | None:
"""Return the IBM Quantum account information currently in use
This method returns the current account information in a dictionary
format. It is used to copy the credentials for use with
``qiskit-ibm-experiment`` without requiring specifying the credentials
for the provider and ``qiskit-ibm-experiment`` separately
It should include ``"url"`` and ``"token"`` as keys for the
authentication to work.
Returns:
A dictionary with information about the account currently in the session.
"""
raise NotImplementedError


Provider = Union[BaseProvider, IBMProvider]


def do_auto_save(func: Callable):
"""Decorate the input function to auto save data."""

Expand Down Expand Up @@ -261,12 +303,15 @@ def __init__(
self._set_backend(backend, recursive=False)
self.provider = provider
if provider is None and backend is not None:
self.provider = backend.provider
self._service = service
if self._service is None and self.provider is not None:
self._service = self.get_service_from_provider(self.provider)
if self._service is None and self.provider is None and self.backend is not None:
self._service = self.get_service_from_backend(self.backend)
# BackendV2 has a provider attribute but BackendV3 probably will not
self.provider = getattr(backend, "provider", None)
if self.provider is None and hasattr(backend, "service"):
# qiskit_ibm_runtime.IBMBackend stores its Provider-like object in
# the "service" attribute
self.provider = backend.service
# Experiment service like qiskit_ibm_experiment.IBMExperimentService,
# not to be confused with qiskit_ibm_runtime.QiskitRuntimeService
self.service = service
self._auto_save = False
self._created_in_db = False
self._extra_data = kwargs
Expand Down Expand Up @@ -604,27 +649,12 @@ def _set_backend(self, new_backend: Backend, recursive: bool = True) -> None:
self._db_data.backend = self._backend_data.name
if self._db_data.backend is None:
self._db_data.backend = str(new_backend)
provider = self._backend_data.provider
if provider is not None:
self._set_hgp_from_provider(provider)
# qiskit-ibm-runtime style
elif hasattr(self._backend, "_instance") and self._backend._instance:
if hasattr(self._backend, "_instance") and self._backend._instance:
self.hgp = self._backend._instance
if recursive:
for data in self.child_data():
data._set_backend(new_backend)

def _set_hgp_from_provider(self, provider):
try:
# qiskit-ibm-provider style
if hasattr(provider, "_hgps"):
for hgp_string, hgp in provider._hgps.items():
if self.backend.name in hgp.backends:
self.hgp = hgp_string
break
except (AttributeError, IndexError, QiskitError):
pass

@property
def hgp(self) -> str:
"""Returns Hub/Group/Project data as a formatted string"""
Expand Down Expand Up @@ -674,6 +704,66 @@ def service(self, service: IBMExperimentService) -> None:
"""
self._set_service(service)

def _infer_service(self, warn: bool):
"""Try to configure service if it has not been configured
This method should be called before any method that needs to work with
the experiment service.
Args:
warn: Warn if the service could not be set up from the backend or
provider attributes.
Returns:
True if a service instance has been set up
"""
if self.service is None:
self.service = self.get_service_from_backend(self.backend)
if self.service is None:
self.service = self.get_service_from_provider(self.provider)

if self.service is None:
LOG.warning("Experiment service has not been configured. Can not save!")

return self.service is not None

def _set_service(self, service: IBMExperimentService) -> None:
"""Set the service to be used for storing experiment data,
to this experiment itself and its descendants.
Args:
service: Service to be used.
Raises:
ExperimentDataError: If an experiment service is already being used and `replace==False`.
"""
self._service = service
with contextlib.suppress(Exception):
self.auto_save = self.service.options.get("auto_save", False)
for data in self.child_data():
data._set_service(service)

@staticmethod
def get_service_from_backend(backend) -> IBMExperimentService | None:
"""Initializes the service from the backend data"""
provider = getattr(backend, "service", None)
return ExperimentData.get_service_from_provider(provider)

@staticmethod
def get_service_from_provider(provider) -> IBMExperimentService | None:
"""Initializes the service from the provider data"""
if not hasattr(provider, "active_account"):
return

account = provider.active_account()
url = account.get("url")
token = account.get("token")
try:
if url is not None and token is not None:
return IBMExperimentService(token=token, url=url)
except Exception: # pylint: disable=broad-except
LOG.warning("Failed to connect to experiment service", exc_info=True)

@property
def provider(self) -> Optional[Provider]:
"""Return the backend provider.
Expand Down Expand Up @@ -1133,16 +1223,6 @@ def _retrieve_data(self):
try: # qiskit-ibm-runtime syntax
job = self.provider.job(jid)
retrieved_jobs[jid] = job
except AttributeError: # TODO: remove this path for qiskit-ibm-provider
try:
job = self.provider.retrieve_job(jid)
retrieved_jobs[jid] = job
except Exception: # pylint: disable=broad-except
LOG.warning(
"Unable to retrieve data from job [Job ID: %s]: %s",
jid,
traceback.format_exc(),
)
except Exception: # pylint: disable=broad-except
LOG.warning(
"Unable to retrieve data from job [Job ID: %s]: %s", jid, traceback.format_exc()
Expand Down Expand Up @@ -1299,10 +1379,10 @@ def add_figures(
self._db_data.figure_names.append(fig_name)

save = save_figure if save_figure is not None else self.auto_save
if save and self._service:
if save and self._infer_service(warn=True):
if isinstance(figure, pyplot.Figure):
figure = plot_to_svg_bytes(figure)
self._service.create_or_update_figure(
self.service.create_or_update_figure(
experiment_id=self.experiment_id,
figure=figure,
figure_name=fig_name,
Expand Down Expand Up @@ -1333,7 +1413,7 @@ def delete_figure(
del self._figures[figure_key]
self._deleted_figures.append(figure_key)

if self._service and self.auto_save:
if self.auto_save and self._infer_service(warn=True):
with service_exception_to_warning():
self.service.delete_figure(experiment_id=self.experiment_id, figure_name=figure_key)
self._deleted_figures.remove(figure_key)
Expand Down Expand Up @@ -1382,7 +1462,7 @@ def figure(
figure_key = self._find_figure_key(figure_key)

figure_data = self._figures.get(figure_key, None)
if figure_data is None and self.service:
if figure_data is None and self._infer_service(warn=False):
figure = self.service.figure(experiment_id=self.experiment_id, figure_name=figure_key)
figure_data = FigureData(figure=figure, name=figure_key)
self._figures[figure_key] = figure_data
Expand Down Expand Up @@ -1489,10 +1569,10 @@ def add_analysis_results(
created_time=created_time,
**extra_values,
)
if self.auto_save:
if self.auto_save and self._infer_service(warn=True):
service_result = _series_to_service_result(
series=self._analysis_results.get_data(uid, columns="all").iloc[0],
service=self._service,
service=self.service,
auto_save=False,
)
service_result.save()
Expand All @@ -1515,7 +1595,7 @@ def delete_analysis_result(
"""
uids = self._analysis_results.del_data(result_key)

if self._service and self.auto_save:
if self.auto_save and self._infer_service(warn=True):
with service_exception_to_warning():
for uid in uids:
self.service.delete_analysis_result(result_id=uid)
Expand Down Expand Up @@ -1662,7 +1742,7 @@ def analysis_results(
service_results.append(
_series_to_service_result(
series=series,
service=self._service,
service=self.service,
auto_save=self._auto_save,
)
)
Expand Down Expand Up @@ -1696,6 +1776,7 @@ def save_metadata(self) -> None:
See :meth:`qiskit.providers.experiment.IBMExperimentService.create_experiment`
for fields that are saved.
"""
self._infer_service(warn=False)
self._save_experiment_metadata()
for data in self.child_data():
data.save_metadata()
Expand All @@ -1714,7 +1795,7 @@ def _save_experiment_metadata(self, suppress_errors: bool = True) -> None:
See :meth:`qiskit.providers.experiment.IBMExperimentService.create_experiment`
for fields that are saved.
"""
if not self._service:
if not self.service:
LOG.warning(
"Experiment cannot be saved because no experiment service is available. "
"An experiment service is available, for example, "
Expand Down Expand Up @@ -1792,7 +1873,8 @@ def save(
additional tags or notes) use :meth:`save_metadata`.
"""
# TODO - track changes
if not self._service:
self._infer_service(warn=False)
if not self.service:
LOG.warning(
"Experiment cannot be saved because no experiment service is available. "
"An experiment service is available, for example, "
Expand Down Expand Up @@ -1828,7 +1910,7 @@ def save(
# Calling API per entry takes huge amount of time.
legacy_result = _series_to_service_result(
series=series,
service=self._service,
service=self.service,
auto_save=False,
)
analysis_results_to_create.append(legacy_result._db_data)
Expand All @@ -1849,7 +1931,7 @@ def save(

for result in self._deleted_analysis_results.copy():
with service_exception_to_warning():
self._service.delete_analysis_result(result_id=result)
self.service.delete_analysis_result(result_id=result)
self._deleted_analysis_results.remove(result)

if save_figures:
Expand All @@ -1874,7 +1956,7 @@ def save(

for name in self._deleted_figures.copy():
with service_exception_to_warning():
self._service.delete_figure(experiment_id=self.experiment_id, figure_name=name)
self.service.delete_figure(experiment_id=self.experiment_id, figure_name=name)
self._deleted_figures.remove(name)

# save artifacts
Expand Down Expand Up @@ -2518,26 +2600,6 @@ def _set_child_data(self, child_data: List[ExperimentData]):
self.add_child_data(data)
self._db_data.metadata["child_data_ids"] = self._child_data.keys()

def _set_service(self, service: IBMExperimentService, replace: bool = None) -> None:
"""Set the service to be used for storing experiment data,
to this experiment itself and its descendants.
Args:
service: Service to be used.
replace: Should an existing service be replaced?
If not, and a current service exists, exception is raised
Raises:
ExperimentDataError: If an experiment service is already being used and `replace==False`.
"""
if self._service and not replace:
raise ExperimentDataError("An experiment service is already being used.")
self._service = service
with contextlib.suppress(Exception):
self.auto_save = self._service.options.get("auto_save", False)
for data in self.child_data():
data._set_service(service)

def add_tags_recursive(self, tags2add: List[str]) -> None:
"""Add tags to this experiment itself and its descendants
Expand Down Expand Up @@ -2684,37 +2746,6 @@ def __getstate__(self):

return state

@staticmethod
def get_service_from_backend(backend):
"""Initializes the service from the backend data"""
# qiskit-ibm-runtime style
try:
if hasattr(backend, "service"):
token = backend.service._account.token
return IBMExperimentService(token=token, url=backend.service._account.url)
return ExperimentData.get_service_from_provider(backend.provider)
except Exception: # pylint: disable=broad-except
return None

@staticmethod
def get_service_from_provider(provider):
"""Initializes the service from the provider data"""
try:
# qiskit-ibm-provider style
if hasattr(provider, "_account"):
warnings.warn(
"qiskit-ibm-provider has been deprecated in favor of qiskit-ibm-runtime. Support"
"for qiskit-ibm-provider backends will be removed in Qiskit Experiments 0.7.",
DeprecationWarning,
stacklevel=2,
)
return IBMExperimentService(
token=provider._account.token, url=provider._account.url
)
return None
except Exception: # pylint: disable=broad-except
return None

def __setstate__(self, state):
self.__dict__.update(state)
# Initialize non-pickled attributes
Expand Down

0 comments on commit e411b27

Please sign in to comment.