diff --git a/docs/conf.py b/docs/conf.py index 33130e4a65..ff73d37f04 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -171,7 +171,6 @@ "matplotlib": ("https://matplotlib.org/stable/", None), "qiskit": ("https://qiskit.org/documentation/", None), "uncertainties": ("https://pythonhosted.org/uncertainties", None), - "qiskit_ibm_provider": ("https://qiskit.org/ecosystem/ibm-provider/", None), "qiskit_aer": ("https://qiskit.org/ecosystem/aer", None), "qiskit_dynamics": ("https://qiskit.org/documentation/dynamics", None), "qiskit_ibm_runtime": ("https://qiskit.org/ecosystem/ibm-runtime/", None), diff --git a/docs/howtos/cloud_service.rst b/docs/howtos/cloud_service.rst index 11bce3f7f8..b69fcd5c98 100644 --- a/docs/howtos/cloud_service.rst +++ b/docs/howtos/cloud_service.rst @@ -18,8 +18,9 @@ Saving ~~~~~~ .. note:: - This guide requires :mod:`qiskit-ibm-provider`. For how to migrate from the deprecated :mod:`qiskit-ibmq-provider` to :mod:`qiskit-ibm-provider`, - consult the `migration guide `_.\ + This guide requires :mod:`qiskit-ibm-runtime` version 0.15 and up, which can be installed with ``python -m pip install qiskit-ibm-runtime``. + For how to migrate from the older :mod:`qiskit-ibm-provider` to :mod:`qiskit-ibm-runtime`, + consult the `migration guide `_.\ You must run the experiment on a real IBM backend and not a simulator to be able to save the experiment data. This is done by calling @@ -27,12 +28,12 @@ backend and not a simulator to be able to save the experiment data. This is done .. jupyter-input:: - from qiskit_ibm_provider import IBMProvider + from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_experiments.library.characterization import T1 import numpy as np - provider = IBMProvider() - backend = provider.get_backend("ibmq_lima") + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.backend("ibm_osaka") t1_delays = np.arange(1e-6, 600e-6, 50e-6) @@ -142,7 +143,7 @@ The :meth:`~.ExperimentData.auto_save` feature automatically saves changes to th .. jupyter-output:: You can view the experiment online at https://quantum.ibm.com/experiments/cdaff3fa-f621-4915-a4d8-812d05d9a9ca - + Setting ``auto_save = True`` works by triggering :meth:`.ExperimentData.save`. diff --git a/docs/howtos/rerun_analysis.rst b/docs/howtos/rerun_analysis.rst index b1a7107b6f..532d968cf2 100644 --- a/docs/howtos/rerun_analysis.rst +++ b/docs/howtos/rerun_analysis.rst @@ -12,9 +12,9 @@ Solution -------- .. note:: - Some of this guide uses the :mod:`qiskit-ibm-provider` package. For how to migrate from - the deprecated ``qiskit-ibmq-provider`` to ``qiskit-ibm-provider``, consult the - `migration guide `_.\ + This guide requires :mod:`qiskit-ibm-runtime` version 0.15 and up, which can be installed with ``python -m pip install qiskit-ibm-runtime``. + For how to migrate from the older :mod:`qiskit-ibm-provider` to :mod:`qiskit-ibm-runtime`, + consult the `migration guide `_.\ Once you recreate the exact experiment you ran and all of its parameters and options, you can call the :meth:`.add_jobs` method with a list of :class:`Job diff --git a/docs/howtos/runtime_sessions.rst b/docs/howtos/runtime_sessions.rst index a9519ba3a2..b7ecc63383 100644 --- a/docs/howtos/runtime_sessions.rst +++ b/docs/howtos/runtime_sessions.rst @@ -10,7 +10,12 @@ You want to run experiments in a `Runtime session Solution -------- -Use the :class:`~qiskit_ibm_provider.IBMBackend` object in ``qiskit-ibm-provider``, which supports sessions. +.. note:: + This guide requires :mod:`qiskit-ibm-runtime` version 0.15 and up, which can be installed with ``python -m pip install qiskit-ibm-runtime``. + For how to migrate from the older :mod:`qiskit-ibm-provider` to :mod:`qiskit-ibm-runtime`, + consult the `migration guide `_.\ + +Use the :class:`~qiskit_ibm_runtime.IBMBackend` object in :mod:`qiskit-ibm-runtime`, which supports sessions. In this example, we will set the ``max_circuits`` property to an artificially low value so that the experiment will be split into multiple jobs that run sequentially in a single session. When running real experiments with a @@ -18,24 +23,22 @@ large number of circuits that can't fit in a single job, it may be helpful to fo .. jupyter-input:: - from qiskit_ibm_provider import IBMProvider + from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_experiments.library.tomography import ProcessTomography from qiskit import QuantumCircuit - provider = IBMProvider() - backend = provider.get_backend("ibm_nairobi") + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.backend("ibm_osaka") qc = QuantumCircuit(1) qc.x(0) - with backend.open_session() as session: - exp = ProcessTomography(qc) - exp.set_experiment_options(max_circuits=3) - exp_data = exp.run(backend) - exp_data.block_for_results() - # Calling cancel because session.close() is not available for qiskit-ibm-provider<=0.7.2. - # It is safe to call cancel since block_for_results() ensures there are no outstanding jobs - # still running that would be canceled. - session.cancel() + backend.open_session() + exp = ProcessTomography(qc) + # Artificially lower circuits per job, adjust value for your own application + exp.set_experiment_options(max_circuits=3) + exp_data = exp.run(backend) + # This will prevent further jobs from being submitted without terminating current jobs + backend.close_session() Note that runtime primitives are not currently supported natively in Qiskit Experiments, so the ``backend.run()`` path is required to run experiments. diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index b6dfff5793..9913ec24ec 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -32,6 +32,7 @@ import sys import json import traceback +import warnings import numpy as np import pandas as pd from dateutil import tz @@ -652,16 +653,15 @@ def _set_backend(self, new_backend: Backend, recursive: bool = True) -> None: 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"): + 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-ibmq-provider style - if hasattr(provider, "credentials"): - creds = provider.credentials - self.hgp = f"{creds.hub}/{creds.group}/{creds.project}" # qiskit-ibm-provider style if hasattr(provider, "_hgps"): for hgp_string, hgp in provider._hgps.items(): @@ -2528,21 +2528,31 @@ def __getstate__(self): @staticmethod def get_service_from_backend(backend): """Initializes the service from the backend data""" - return ExperimentData.get_service_from_provider(backend.provider) + # 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""" - db_url = "https://auth.quantum.ibm.com/api" try: - # qiskit-ibmq-provider style - if hasattr(provider, "credentials"): - token = provider.credentials.token # qiskit-ibm-provider style if hasattr(provider, "_account"): - token = provider._account.token - service = IBMExperimentService(token=token, url=db_url) - return service + 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.6.", + DeprecationWarning, + stacklevel=2, + ) + return IBMExperimentService( + token=provider._account.token, url=provider._account.url + ) + return None except Exception: # pylint: disable=broad-except return None diff --git a/releasenotes/notes/runtime-provider-support-5358b72ec0035419.yaml b/releasenotes/notes/runtime-provider-support-5358b72ec0035419.yaml new file mode 100644 index 0000000000..8a879f49d7 --- /dev/null +++ b/releasenotes/notes/runtime-provider-support-5358b72ec0035419.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Experiments run via the ``qiskit-ibm-runtime`` provider can now be saved + to the cloud service. +upgrade: + - | + With the impending deprecation of the ``qiskit-ibm-provider`` package, support for + ``qiskit-ibm-provider`` is now deprecated and will be removed + in the next release. Users should migrate to ``qiskit-ibm-runtime`` following the + `migration guide `. diff --git a/requirements-extras.txt b/requirements-extras.txt index 20acc7d83f..707149b52d 100644 --- a/requirements-extras.txt +++ b/requirements-extras.txt @@ -1,4 +1,3 @@ -qiskit-ibm-provider>=0.6.1 # for submitting experiments to backends through the IBM provider cvxpy>=1.3.2 # for tomography scikit-learn # for discriminators qiskit-aer>=0.13.2 diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index 003d21af0f..164e5160f7 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -33,6 +33,7 @@ from qiskit.result import Result from qiskit.providers import JobV1 as Job from qiskit.providers import JobStatus +from qiskit.providers.backend import Backend from qiskit_ibm_experiment import IBMExperimentService from qiskit_experiments.framework import ExperimentData from qiskit_experiments.framework import AnalysisResult @@ -57,6 +58,15 @@ def setUp(self): super().setUp() self.backend = FakeMelbourneV2() + def generate_mock_job(self): + """Helper method to generate a mock job.""" + job = mock.create_autospec(Job, instance=True) + # mock a backend without provider + backend = mock.create_autospec(Backend, instance=True) + backend.provider = None + job.backend.return_value = backend + return job + def test_db_experiment_data_attributes(self): """Test DB experiment data attributes.""" attrs = { @@ -119,12 +129,12 @@ def test_add_data_result_metadata(self): def test_add_data_job(self): """Test add job data.""" - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() a_job.result.return_value = self._get_job_result(3) num_circs = 3 jobs = [] for _ in range(2): - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result.return_value = self._get_job_result(2, label_from=num_circs) job.status.return_value = JobStatus.DONE jobs.append(job) @@ -163,7 +173,7 @@ def _callback(_exp_data): nonlocal called_back called_back = True - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() a_job.result.return_value = self._get_job_result(2) a_job.status.return_value = JobStatus.DONE @@ -217,7 +227,8 @@ def _callback(_exp_data, **kwargs): nonlocal called_back called_back = True - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() + a_job.backend.return_value = mock.create_autospec(Backend, instance=True) a_job.result.return_value = self._get_job_result(2) a_job.status.return_value = JobStatus.DONE @@ -235,7 +246,7 @@ def test_add_data_pending_post_processing(self): def _callback(_exp_data, **kwargs): kwargs["event"].wait(timeout=3) - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() a_job.result.return_value = self._get_job_result(2) a_job.status.return_value = JobStatus.DONE @@ -452,14 +463,14 @@ def test_delayed_backend(self): exp_data = ExperimentData(experiment_type="qiskit_test") self.assertIsNone(exp_data.backend) exp_data.save_metadata() - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() exp_data.add_jobs(a_job) self.assertIsNotNone(exp_data.backend) def test_different_backend(self): """Test setting a different backend.""" exp_data = ExperimentData(backend=self.backend, experiment_type="qiskit_test") - a_job = mock.create_autospec(Job, instance=True) + a_job = self.generate_mock_job() self.assertNotEqual(exp_data.backend, a_job.backend()) with self.assertLogs("qiskit_experiments", "WARNING"): exp_data.add_jobs(a_job) @@ -609,12 +620,12 @@ def test_auto_save(self): def test_status_job_pending(self): """Test experiment status when job is pending.""" - job1 = mock.create_autospec(Job, instance=True) + job1 = self.generate_mock_job() job1.result.return_value = self._get_job_result(3) job1.status.return_value = JobStatus.DONE event = threading.Event() - job2 = mock.create_autospec(Job, instance=True) + job2 = self.generate_mock_job() job2.result = lambda *args, **kwargs: event.wait(timeout=15) job2.status = lambda: JobStatus.CANCELLED if event.is_set() else JobStatus.RUNNING self.addCleanup(event.set) @@ -634,11 +645,11 @@ def test_status_job_pending(self): def test_status_job_error(self): """Test experiment status when job failed.""" - job1 = mock.create_autospec(Job, instance=True) + job1 = self.generate_mock_job() job1.result.return_value = self._get_job_result(3) job1.status.return_value = JobStatus.DONE - job2 = mock.create_autospec(Job, instance=True) + job2 = self.generate_mock_job() job2.status.return_value = JobStatus.ERROR exp_data = ExperimentData(experiment_type="qiskit_test") @@ -649,7 +660,7 @@ def test_status_job_error(self): def test_status_post_processing(self): """Test experiment status during post processing.""" - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result.return_value = self._get_job_result(3) job.status.return_value = JobStatus.DONE @@ -664,7 +675,7 @@ def test_status_post_processing(self): def test_status_cancelled_analysis(self): """Test experiment status during post processing.""" - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result.return_value = self._get_job_result(3) job.status.return_value = JobStatus.DONE @@ -686,7 +697,7 @@ def test_status_post_processing_error(self): def _post_processing(*args, **kwargs): raise ValueError("Kaboom!") - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result.return_value = self._get_job_result(3) job.status.return_value = JobStatus.DONE @@ -701,7 +712,7 @@ def _post_processing(*args, **kwargs): def test_status_done(self): """Test experiment status when all jobs are done.""" - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result.return_value = self._get_job_result(3) job.status.return_value = JobStatus.DONE exp_data = ExperimentData(experiment_type="qiskit_test") @@ -734,7 +745,7 @@ def _job_cancel(): exp_data = ExperimentData(experiment_type="qiskit_test") event = threading.Event() self.addCleanup(event.set) - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.job_id.return_value = "1234" job.cancel = _job_cancel job.result = _job_result @@ -760,7 +771,7 @@ def _job_result(): def _analysis(*args): # pylint: disable = unused-argument event.wait(timeout=15) - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.job_id.return_value = "1234" job.result = _job_result job.status = lambda: JobStatus.DONE if event.is_set() else JobStatus.RUNNING @@ -796,7 +807,7 @@ def _analysis(expdata, name=None, timeout=0): # pylint: disable = unused-argume event.wait(timeout=timeout) run_analysis.append(name) - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.job_id.return_value = "1234" job.result = _job_result job.status = lambda: JobStatus.DONE if event.is_set() else JobStatus.RUNNING @@ -848,7 +859,7 @@ def _status(): return JobStatus.CANCELLED return JobStatus.RUNNING - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.job_id.return_value = "1234" job.result = _job_result job.cancel = event.set @@ -874,7 +885,7 @@ def _job_result(): event.wait(timeout=15) raise ValueError("Job was cancelled.") - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.job_id.return_value = "1234" job.result = _job_result job.cancel = event.set @@ -906,11 +917,11 @@ def test_errors(self): def _post_processing(*args, **kwargs): # pylint: disable=unused-argument raise ValueError("Kaboom!") - job1 = mock.create_autospec(Job, instance=True) + job1 = self.generate_mock_job() job1.job_id.return_value = "1234" job1.status.return_value = JobStatus.DONE - job2 = mock.create_autospec(Job, instance=True) + job2 = self.generate_mock_job() job2.status.return_value = JobStatus.ERROR job2.job_id.return_value = "5678" @@ -1031,7 +1042,7 @@ def _sleeper(*args, **kwargs): # pylint: disable=unused-argument return self._get_job_result(1) sleep_count = 0 - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result = _sleeper exp_data = ExperimentData(experiment_type="qiskit_test") exp_data.add_jobs(job) @@ -1069,12 +1080,12 @@ def _job2_result(): return job_results2 exp_data = ExperimentData(experiment_type="qiskit_test") - job = mock.create_autospec(Job, instance=True) + job = self.generate_mock_job() job.result = _job1_result exp_data.add_jobs(job) copied = exp_data.copy(copy_results=False) - job2 = mock.create_autospec(Job, instance=True) + job2 = self.generate_mock_job() job2.result = _job2_result copied.add_jobs(job2) event.set()