diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 13c7595846..35ab947d96 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -18,12 +18,9 @@ import copy import logging from dataclasses import asdict -import warnings from qiskit.providers.options import Options as TerraOptions -from qiskit_ibm_provider.session import get_cm_session as get_cm_provider_session - from .options import Options from .options.utils import set_default_error_levels from .runtime_job import RuntimeJob @@ -98,6 +95,7 @@ def __init__( if isinstance(backend, IBMBackend): self._service = backend.service self._backend = backend + self._session = self._backend.session elif isinstance(backend, str): self._service = ( QiskitRuntimeService() @@ -121,11 +119,6 @@ def __init__( raise ValueError( "A backend or session must be specified when not using ibm_cloud channel." ) - # Check if initialized within a IBMBackend session. If so, issue a warning. - if get_cm_provider_session(): - warnings.warn( - "A Backend.run() session is open but Primitives will not be run within this session" - ) def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJob: """Run the primitive. diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 5e0604e007..659d25bb8c 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -39,12 +39,6 @@ MeasureChannel, ) from qiskit.transpiler.target import Target - -# temporary until we unite the 2 Session classes -from qiskit_ibm_provider.session import ( - Session as ProviderSession, -) # temporary until we unite the 2 Session classes - from .utils.utils import validate_job_tags from . import qiskit_runtime_service # pylint: disable=unused-import,cyclic-import from .runtime_job import RuntimeJob @@ -54,7 +48,8 @@ from .utils.backend_converter import ( convert_to_target, ) -from .utils.default_session import get_cm_session as get_cm_primitive_session +from .session import Session # pylint: disable=cyclic-import +from .utils.default_session import get_cm_session from .utils.backend_decoder import ( defaults_from_server_data, properties_from_server_data, @@ -194,7 +189,8 @@ def __init__( self._defaults = None self._target = None self._max_circuits = configuration.max_experiments - self._session: ProviderSession = None + self._session: Session = None + if ( not self._configuration.simulator and hasattr(self.options, "noise_model") @@ -582,6 +578,7 @@ def __deepcopy__(self, _memo: dict = None) -> "IBMBackend": def run( self, circuits: Union[QuantumCircuit, str, List[Union[QuantumCircuit, str]]], + session: Session = None, dynamic: bool = None, job_tags: Optional[List[str]] = None, init_circuit: Optional[QuantumCircuit] = None, @@ -731,6 +728,7 @@ def run( run_config_dict["circuits"] = circuits return self._runtime_run( + session=session, program_id=program_id, inputs=run_config_dict, backend_name=self.name, @@ -743,6 +741,7 @@ def _runtime_run( program_id: str, inputs: Dict, backend_name: str, + session: Optional[Session] = None, job_tags: Optional[List[str]] = None, image: Optional[str] = None, ) -> RuntimeJob: @@ -751,14 +750,12 @@ def _runtime_run( if self._service._channel == "ibm_quantum": hgp_name = self._instance or self._service._get_hgp().name - # Check if initialized within a Primitive session. If so, issue a warning. - if get_cm_primitive_session(): - warnings.warn( - "A Primitive session is open but Backend.run() jobs will not be run within this session" - ) - if self._session: - if not self._session.active: - raise RuntimeError(f"The session {self._session.session_id} is closed.") + if session: + self._session = session + elif get_cm_session(): + self._session = get_cm_session() + + if self._session and self._session._active: session_id = self._session.session_id start_session = session_id is None max_session_time = self._session._max_time @@ -829,13 +826,13 @@ def _get_run_config(self, program_id: str, **kwargs: Any) -> Dict: run_config_dict[key] = backend_options[key] return run_config_dict - def open_session(self, max_time: Optional[Union[int, str]] = None) -> ProviderSession: + def open_session(self, max_time: Optional[Union[int, str]] = None) -> Session: """Open session""" - self._session = ProviderSession(max_time=max_time) + self._session = Session(backend=self, max_time=max_time) return self._session @property - def session(self) -> ProviderSession: + def session(self) -> Session: """Return session""" return self._session diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index cb9da574da..ec68a08306 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -17,10 +17,10 @@ from functools import wraps from threading import Lock -from qiskit_ibm_runtime import QiskitRuntimeService +from . import qiskit_runtime_service # pylint: disable=cyclic-import from .runtime_job import RuntimeJob from .utils.result_decoder import ResultDecoder -from .ibm_backend import IBMBackend +from . import ibm_backend from .utils.default_session import set_cm_session from .utils.deprecation import deprecate_arguments from .utils.converters import hms_to_seconds @@ -75,8 +75,8 @@ class Session: def __init__( self, - service: Optional[QiskitRuntimeService] = None, - backend: Optional[Union[str, IBMBackend]] = None, + service: Optional["qiskit_runtime_service.QiskitRuntimeService"] = None, + backend: Optional[Union[str, "ibm_backend.IBMBackend"]] = None, max_time: Optional[Union[int, str]] = None, ): # pylint: disable=line-too-long """Session constructor. @@ -102,13 +102,13 @@ def __init__( """ if service is None: - if isinstance(backend, IBMBackend): + if isinstance(backend, ibm_backend.IBMBackend): self._service = backend.service else: self._service = ( - QiskitRuntimeService() - if QiskitRuntimeService.global_service is None - else QiskitRuntimeService.global_service + qiskit_runtime_service.QiskitRuntimeService() + if qiskit_runtime_service.QiskitRuntimeService.global_service is None + else qiskit_runtime_service.QiskitRuntimeService.global_service ) else: @@ -118,7 +118,7 @@ def __init__( raise ValueError('"backend" is required for ``ibm_quantum`` channel.') self._instance = None - if isinstance(backend, IBMBackend): + if isinstance(backend, ibm_backend.IBMBackend): self._instance = backend._instance backend = backend.name self._backend = backend @@ -287,7 +287,7 @@ def session_id(self) -> str: return self._session_id @property - def service(self) -> QiskitRuntimeService: + def service(self) -> "qiskit_runtime_service.QiskitRuntimeService": """Return service associated with this session. Returns: @@ -299,8 +299,8 @@ def service(self) -> QiskitRuntimeService: def from_id( cls, session_id: str, - service: Optional[QiskitRuntimeService] = None, - backend: Optional[Union[str, IBMBackend]] = None, + service: Optional["qiskit_runtime_service.QiskitRuntimeService"] = None, + backend: Optional[Union[str, "ibm_backend.IBMBackend"]] = None, ) -> "Session": """Construct a Session object with a given session_id diff --git a/releasenotes/notes/unite_sessions-bc69cc9d4c37d551.yaml b/releasenotes/notes/unite_sessions-bc69cc9d4c37d551.yaml new file mode 100644 index 0000000000..d85519abe0 --- /dev/null +++ b/releasenotes/notes/unite_sessions-bc69cc9d4c37d551.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Session in `qiskit_ibm_runtime` and in `qiskit_ibm_provider` are now the same. + It is possible now to run a primitive and backend.run() in the same session. + For example: + + .. code-block:: + + backend = self.service.get_backend("ibmq_qasm_simulator") + with Session(backend=backend) as session: + sampler = Sampler(session=session) + job1 = sampler.run(circuits=ReferenceCircuits.bell()) + job2 = backend.run(circuits=ReferenceCircuits.bell(), session=session) + # both jobs will be run in the same session + print(job1.session_id) + print(job2.session_id) + diff --git a/test/integration/test_session.py b/test/integration/test_session.py index 15e4a92f5e..47fdaf64ab 100644 --- a/test/integration/test_session.py +++ b/test/integration/test_session.py @@ -12,7 +12,6 @@ """Integration tests for Session.""" -import warnings from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp @@ -106,21 +105,23 @@ def test_session_from_id(self, service): class TestBackendRunInSession(IBMIntegrationTestCase): """Integration tests for Backend.run in Session.""" - def test_session_id(self): + @run_integration_test + def test_session_id(self, service): """Test that session_id is updated correctly and maintained throughout the session""" - backend = self.service.get_backend("ibmq_qasm_simulator") + backend = service.get_backend("ibmq_qasm_simulator") backend.open_session() self.assertEqual(backend.session.session_id, None) - self.assertTrue(backend.session.active) + self.assertTrue(backend.session._active) job1 = backend.run(bell()) self.assertEqual(job1._session_id, job1.job_id()) job2 = backend.run(bell()) self.assertFalse(job2._session_id == job2.job_id()) - def test_backend_run_with_session(self): + @run_integration_test + def test_backend_run_with_session(self, service): """Test that 'shots' parameter is transferred correctly""" shots = 1000 - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") backend.open_session() result = backend.run(circuits=bell(), shots=shots).result() backend.cancel_session() @@ -130,44 +131,79 @@ def test_backend_run_with_session(self): result.get_counts()["00"], result.get_counts()["11"], delta=shots / 10 ) - def test_backend_and_primitive_in_session(self): - """Test Sampler.run and backend.run in the same session.""" - backend = self.service.get_backend("ibmq_qasm_simulator") + @run_integration_test + def test_backend_run_session_param(self, service): + """Test Sampler.run and backend.run in the same session. + Session is defined as parameter""" + backend = service.get_backend("ibmq_qasm_simulator") with Session(backend=backend) as session: sampler = Sampler(session=session) job1 = sampler.run(circuits=bell()) - with warnings.catch_warnings(record=True): - job2 = backend.run(circuits=bell()) + job2 = backend.run(circuits=bell(), session=session) self.assertEqual(job1.session_id, job1.job_id()) - self.assertIsNone(job2.session_id) + self.assertEqual(job2.session_id, job1.job_id()) + + @run_integration_test + def test_backend_run_session_context_manager(self, service): + """Test Sampler.run and backend.run in the same session. + Session is defined in context manager""" + backend = service.get_backend("ibmq_qasm_simulator") + with Session(backend=backend) as session: + sampler = Sampler(session=session) + job1 = sampler.run(circuits=bell()) + job2 = backend.run(circuits=bell()) + self.assertEqual(session.session_id, job1.job_id()) + self.assertEqual(job2.session_id, session.session_id) + + @run_integration_test + def test_backend_open_session(self, service): + """Test Sampler.run and backend.run in the same session. + Session is created by open_session.""" + backend = service.get_backend("ibmq_qasm_simulator") + with backend.open_session() as session: + sampler = Sampler(backend=backend, session=session) + job1 = backend.run(bell()) + job2 = sampler.run(circuits=bell()) + session_id = session.session_id + self.assertEqual(session_id, job1.job_id()) + self.assertEqual(job2.session_id, session_id) + + @run_integration_test + def test_backend_open_session_with_context_manager(self, service): + """Test Sampler.run and backend.run in the same session. + Session is created by open_session. Sampler gets + Session from backend without parameter""" + backend = service.get_backend("ibmq_qasm_simulator") with backend.open_session() as session: - with warnings.catch_warnings(record=True): - sampler = Sampler(backend=backend) + sampler = Sampler(backend=backend) job1 = backend.run(bell()) job2 = sampler.run(circuits=bell()) session_id = session.session_id self.assertEqual(session_id, job1.job_id()) - self.assertIsNone(job2.session_id) + self.assertEqual(job2.session_id, session_id) - def test_session_cancel(self): + @run_integration_test + def test_session_cancel(self, service): """Test closing a session""" - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") backend.open_session() - self.assertTrue(backend.session.active) + self.assertTrue(backend.session._active) backend.cancel_session() self.assertIsNone(backend.session) - def test_session_close(self): + @run_integration_test + def test_session_close(self, service): """Test closing a session""" - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") backend.open_session() - self.assertTrue(backend.session.active) + self.assertTrue(backend.session._active) backend.close_session() self.assertIsNone(backend.session) - def test_run_after_cancel(self): + @run_integration_test + def test_run_after_cancel(self, service): """Test running after session is cancelled.""" - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") job1 = backend.run(circuits=bell()) self.assertIsNone(backend.session) self.assertIsNone(job1._session_id) @@ -181,9 +217,10 @@ def test_run_after_cancel(self): self.assertIsNone(backend.session) self.assertIsNone(job3._session_id) - def test_session_as_context_manager(self): + @run_integration_test + def test_session_as_context_manager(self, service): """Test session as a context manager""" - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") with backend.open_session() as session: job1 = backend.run(bell()) @@ -192,9 +229,10 @@ def test_session_as_context_manager(self): job2 = backend.run(bell()) self.assertFalse(session_id == job2.job_id()) - def test_run_after_cancel_as_context_manager(self): + @run_integration_test + def test_run_after_cancel_as_context_manager(self, service): """Test run after cancel in context manager""" - backend = self.service.backend("ibmq_qasm_simulator") + backend = service.backend("ibmq_qasm_simulator") with backend.open_session() as session: _ = backend.run(bell()) self.assertEqual(backend.session, session) @@ -202,3 +240,15 @@ def test_run_after_cancel_as_context_manager(self): job = backend.run(circuits=bell()) self.assertIsNone(backend.session) self.assertIsNone(job._session_id) + + @run_integration_test + def test_run_after_session(self, service): + """Test run after session was closed""" + backend = service.backend("ibmq_qasm_simulator") + with Session(backend=backend) as session: + job1 = backend.run(bell()) + self.assertTrue(job1.result()) + self.assertEqual(backend.session, session) + + job2 = backend.run(circuits=bell()) + self.assertTrue(job2.result()) diff --git a/test/unit/test_batch.py b/test/unit/test_batch.py index 3b54b10944..ca0f5897bb 100644 --- a/test/unit/test_batch.py +++ b/test/unit/test_batch.py @@ -27,7 +27,7 @@ def tearDown(self) -> None: super().tearDown() _DEFAULT_SESSION.set(None) - @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True) + @patch("qiskit_ibm_runtime.session.qiskit_runtime_service.QiskitRuntimeService", autospec=True) def test_default_batch(self, mock_service): """Test using default batch mode.""" mock_service.global_service = None diff --git a/test/unit/test_session.py b/test/unit/test_session.py index fc62751e51..5af02fb5bc 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -33,7 +33,7 @@ def tearDown(self) -> None: super().tearDown() _DEFAULT_SESSION.set(None) - @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True) + @patch("qiskit_ibm_runtime.session.qiskit_runtime_service.QiskitRuntimeService", autospec=True) def test_default_service(self, mock_service): """Test using default service.""" mock_service.global_service = None