Repository: Qiskit/qiskit-ibm-runtime (python)

Issue:
Backend option should be at session level, not job level
**Describe the bug**

The new `Options` class added `backend` as an option that one can specify when initializing a primitive class or running a primitive job. This implies that one can switch to using a different backend in the middle of a session, which is not currently supported by the scheduler. 

**Steps to reproduce**


**Expected behavior**


**Suggested solutions**

Make `backend` a session parameter and not part of `Options`. 

**Additional Information**

- **qiskit-ibm-runtime version**:
- **Python version**:
- **Operating system**:


Pull request description:
Make backend a session parameter
<!--
⚠️ The pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

✅ I have added the tests to cover my changes.
✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
-->

### Summary

- Make `backend` a session parameter instead of job parameter
- Create new default session if backend or service changes
- Un-deprecate `RuntimeOptions` since it no longer conflicts with `Options`
- Allow users to specify a backend as the `session` parameter to `Sampler` and `Estimator`, so `session` can continue to be optional


### Details and comments
Fixes #474



Patch:
diff --git a/docs/tutorials/how-to-getting-started-with-estimator.ipynb b/docs/tutorials/how-to-getting-started-with-estimator.ipynb
index 3efb2c5d..335f9909 100644
--- a/docs/tutorials/how-to-getting-started-with-estimator.ipynb
+++ b/docs/tutorials/how-to-getting-started-with-estimator.ipynb
@@ -61,9 +61,9 @@
     "from qiskit.quantum_info import SparsePauliOp\n",
     "\n",
     "service = QiskitRuntimeService()\n",
-    "options = Options(backend=\"ibmq_qasm_simulator\", optimization_level=1)\n",
+    "options = Options(optimization_level=1)\n",
     "\n",
-    "with Session(service) as session:\n",
+    "with Session(service=service, backend=\"ibmq_qasm_simulator\") as session:\n",
     "    estimator = Estimator(session=session, options=options)"
    ]
   },
diff --git a/docs/tutorials/how-to-getting-started-with-sampler.ipynb b/docs/tutorials/how-to-getting-started-with-sampler.ipynb
index ad0d510e..ee54c496 100644
--- a/docs/tutorials/how-to-getting-started-with-sampler.ipynb
+++ b/docs/tutorials/how-to-getting-started-with-sampler.ipynb
@@ -59,14 +59,14 @@
     "from qiskit import QuantumCircuit\n",
     "\n",
     "service = QiskitRuntimeService()\n",
-    "options = Options(backend=\"ibmq_qasm_simulator\", optimization_level=1)\n",
+    "options = Options(optimization_level=1)\n",
     "\n",
     "bell = QuantumCircuit(2)\n",
     "bell.h(0)\n",
     "bell.cx(0, 1)\n",
     "bell.measure_all()\n",
     "\n",
-    "with Session(service=service) as session:\n",
+    "with Session(service=service, backend=\"ibmq_qasm_simulator\") as session:\n",
     "    sampler = Sampler(session=session, options=options)"
    ]
   },
@@ -150,7 +150,7 @@
     "from qiskit import QuantumCircuit\n",
     "\n",
     "service = QiskitRuntimeService()\n",
-    "options = Options(backend=\"ibmq_qasm_simulator\", optimization_level=1)\n",
+    "options = Options(optimization_level=1)\n",
     "\n",
     "bell = QuantumCircuit(2)\n",
     "bell.h(0)\n",
@@ -158,7 +158,7 @@
     "bell.measure_all()\n",
     "\n",
     "# executes three Bell circuits\n",
-    "with Session(service=service) as session:\n",
+    "with Session(service=service, backend=\"ibmq_qasm_simulator\") as session:\n",
     "    sampler = Sampler(session=session, options=options)\n",
     "    job = sampler.run(circuits=[bell]*3)\n",
     "    print(job.result())"
@@ -196,7 +196,7 @@
     "from qiskit.circuit.library import RealAmplitudes\n",
     "\n",
     "service = QiskitRuntimeService()\n",
-    "options = Options(backend=\"ibmq_qasm_simulator\", optimization_level=1)\n",
+    "options = Options(optimization_level=1)\n",
     "\n",
     "# parameterized circuit\n",
     "pqc = RealAmplitudes(num_qubits=2, reps=2)\n",
@@ -208,7 +208,7 @@
     "theta2 = [1, 2, 3, 4, 5, 6]\n",
     "theta3 = [0, 1, 2, 3, 4, 5, 6, 7]\n",
     "    \n",
-    "with Session(service) as session:\n",
+    "with Session(service=service, backend=\"ibmq_qasm_simulator\") as session:\n",
     "    sampler = Sampler(session=session, options=options)\n",
     "\n",
     "    job = sampler.run(circuits=[pqc, pqc, pqc2], parameter_values=[theta1, theta2, theta3])\n",
diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py
index 0c4662fc..4656971a 100644
--- a/qiskit_ibm_runtime/__init__.py
+++ b/qiskit_ibm_runtime/__init__.py
@@ -46,7 +46,7 @@ Below is an example of using primitives within a session::
     service = QiskitRuntimeService()
 
     # Set options, which can be overwritten at job level.
-    options = Options(backend="ibmq_qasm_simulator")
+    options = Options(optimization_level=3)
 
     # Prepare inputs.
     bell = ReferenceCircuits.bell()
@@ -54,7 +54,7 @@ Below is an example of using primitives within a session::
     H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
     theta = [0, 1, 1, 2, 3, 5]
 
-    with Session(service) as session:
+    with Session(service=service, backend="ibmq_qasm_simulator") as session:
         # Submit a request to the Sampler primitive within the session.
         sampler = Sampler(session=session, options=options)
         job = sampler.run(circuits=bell)
diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py
index 9239701a..5bdee2df 100644
--- a/qiskit_ibm_runtime/estimator.py
+++ b/qiskit_ibm_runtime/estimator.py
@@ -23,7 +23,6 @@ from qiskit.opflow import PauliSumOp
 from qiskit.quantum_info.operators.base_operator import BaseOperator
 
 # pylint: disable=unused-import,cyclic-import
-import qiskit_ibm_runtime.session as session_pkg
 
 # TODO import BaseEstimator and EstimatorResult from terra once released
 from .qiskit.primitives import BaseEstimator, EstimatorResult
@@ -31,7 +30,8 @@ from .qiskit_runtime_service import QiskitRuntimeService
 from .program.result_decoder import ResultDecoder
 from .runtime_job import RuntimeJob
 from .utils.deprecation import deprecate_arguments, issue_deprecation_msg
-from .runtime_options import RuntimeOptions
+from .ibm_backend import IBMBackend
+from .session import get_default_session
 from .options import Options
 
 # pylint: disable=unused-import,cyclic-import
@@ -66,9 +66,8 @@ class Estimator(BaseEstimator):
         H2 = SparsePauliOp.from_list([("IZ", 1)])
         H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)])
 
-        with Session(service) as session:
+        with Session(service=service, backend="ibmq_qasm_simulator") as session:
             estimator = Estimator(session=session)
-            estimator.options.backend = 'ibmq_qasm_simulator'
 
             theta1 = [0, 1, 1, 2, 3, 5]
 
@@ -93,8 +92,8 @@ class Estimator(BaseEstimator):
         observables: Optional[Iterable[SparsePauliOp]] = None,
         parameters: Optional[Iterable[Iterable[Parameter]]] = None,
         service: Optional[QiskitRuntimeService] = None,
-        session: Optional[Session] = None,
-        options: Optional[Union[Dict, RuntimeOptions, Options]] = None,
+        session: Optional[Union[Session, str, IBMBackend]] = None,
+        options: Optional[Union[Dict, Options]] = None,
         skip_transpilation: Optional[bool] = False,
     ):
         """Initializes the Estimator primitive.
@@ -115,10 +114,14 @@ class Estimator(BaseEstimator):
                 defaults to `QiskitRuntimeService()` which tries to initialize your default
                 saved account.
 
-            session: Session in which to call the sampler primitive. If ``None``, a new session
-                is created using the default saved account.
+            session: Session in which to call the primitive. If an instance of
+                :class:`qiskit_ibm_runtime.IBMBackend` class or
+                string name of a backend is specified, a new session is created for
+                that backend. If ``None``, a new session is created using the default
+                saved account and a default backend (IBM Cloud channel only).
 
             options: Primitive options, see :class:`Options` for detailed description.
+                The ``backend`` keyword is still supported but is deprecated.
 
             skip_transpilation: (DEPRECATED) Transpilation is skipped if set to True. False by default.
                 Ignored ``skip_transpilation`` is also specified in ``options``.
@@ -147,14 +150,22 @@ class Estimator(BaseEstimator):
                 "service", "0.7", "Please use the session parameter instead."
             )
 
+        backend = None
+        self._session: Session = None
+
         if options is None:
             self.options = Options()
         elif isinstance(options, Options):
             self.options = copy.deepcopy(options)
             skip_transpilation = self.options.transpilation.skip_transpilation
-        elif isinstance(options, RuntimeOptions):
-            self.options = options._to_new_options()
         else:
+            backend = options.pop("backend", None)
+            if backend is not None:
+                issue_deprecation_msg(
+                    msg="The 'backend' key in 'options' has been deprecated",
+                    version="0.7",
+                    remedy="Please pass the backend when opening a session.",
+                )
             self.options = Options._from_dict(options)
             skip_transpilation = options.get("transpilation", {}).get(
                 "skip_transpilation", False
@@ -167,15 +178,11 @@ class Estimator(BaseEstimator):
             "parameters": parameters,
         }
 
-        if session:
+        if isinstance(session, Session):
             self._session = session
         else:
-            if (
-                session_pkg._DEFAULT_SESSION is None
-                or not session_pkg._DEFAULT_SESSION._active
-            ):
-                session_pkg._DEFAULT_SESSION = Session(service=service)
-            self._session = session_pkg._DEFAULT_SESSION
+            backend = session or backend
+            self._session = get_default_session(service, backend)
 
     def run(
         self,
diff --git a/qiskit_ibm_runtime/options.py b/qiskit_ibm_runtime/options.py
index 10bd5434..0e4668de 100644
--- a/qiskit_ibm_runtime/options.py
+++ b/qiskit_ibm_runtime/options.py
@@ -124,8 +124,6 @@ class Options:
             log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
             The default level is ``WARNING``.
 
-        backend: target backend to run on. This is required for ``ibm_quantum`` channel.
-
         log_level: logging level to set in the execution environment. The valid
             log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
             The default level is ``WARNING``.
@@ -199,7 +197,6 @@ class Options:
 
     optimization_level: int = 1
     resilience_level: int = 0
-    backend: str = None
     log_level: str = "WARNING"
     transpilation: Transpilation = field(default_factory=Transpilation)
     execution: Execution = field(default_factory=Execution)
@@ -240,13 +237,6 @@ class Options:
                 remedy="Please specify 'experimental':{'image': image} instead.",
             )
             experimental = {"image": data.pop("image")}
-        if "backend_name" in data.keys():
-            issue_deprecation_msg(
-                msg="The keyword 'backend_name' has been deprecated",
-                version="0.7",
-                remedy="Please use 'backend' instead.",
-            )
-            data["backend"] = data.pop("backend_name")
         transp = Transpilation(**data.pop("transpilation", {}))
         execution = Execution(**data.pop("execution", {}))
         return cls(
@@ -281,7 +271,6 @@ class Options:
         """
         experimental = options.get("experimental") or {}
         return {
-            "backend_name": options.get("backend"),
             "log_level": options.get("log_level"),
             "image": experimental.get("image", None),
         }
diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py
index 8b77025a..e2ee8e2c 100644
--- a/qiskit_ibm_runtime/qiskit_runtime_service.py
+++ b/qiskit_ibm_runtime/qiskit_runtime_service.py
@@ -84,7 +84,7 @@ class QiskitRuntimeService(Provider):
         service = QiskitRuntimeService()
 
         # Set options, which can be overwritten at job level.
-        options = Options(backend="ibmq_qasm_simulator")
+        options = Options(optimization_level=1)
 
         # Prepare inputs.
         bell = ReferenceCircuits.bell()
@@ -92,7 +92,7 @@ class QiskitRuntimeService(Provider):
         H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
         theta = [0, 1, 1, 2, 3, 5]
 
-        with Session(service) as session:
+        with Session(service=service, backend="ibmq_qasm_simulator") as session:
             # Submit a request to the Sampler primitive within the session.
             sampler = Sampler(session=session, options=options)
             job = sampler.run(circuits=bell)
@@ -860,7 +860,6 @@ class QiskitRuntimeService(Provider):
             inputs: Program input parameters. These input values are passed
                 to the runtime program.
             options: Runtime options that control the execution environment.
-                The use of :class:`RuntimeOptions` has been deprecated.
 
                 * backend: target backend to run on. This is required for ``ibm_quantum`` runtime.
                 * image: the runtime image used to execute the program, specified in
diff --git a/qiskit_ibm_runtime/runtime_options.py b/qiskit_ibm_runtime/runtime_options.py
index 9a78cb67..fe0c1dfc 100644
--- a/qiskit_ibm_runtime/runtime_options.py
+++ b/qiskit_ibm_runtime/runtime_options.py
@@ -15,39 +15,40 @@
 import re
 import logging
 from dataclasses import dataclass
-from typing import Optional, Any
+from typing import Optional
+
+from qiskit.utils.deprecation import deprecate_arguments
 
 from .exceptions import IBMInputValueError
-from .utils.deprecation import issue_deprecation_msg
 from .options import Options
+from .utils.deprecation import issue_deprecation_msg
 
 
 @dataclass
 class RuntimeOptions:
-    """Class for representing runtime execution options.
-
-    Args:
-        backend_name: target backend to run on. This is required for ``ibm_quantum`` runtime.
-        image: the runtime image used to execute the program, specified in
-            the form of ``image_name:tag``. Not all accounts are
-            authorized to select a different image.
-        log_level: logging level to set in the execution environment. The valid
-            log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
-            The default level is ``WARNING``.
-    """
-
-    # pylint: disable=unused-argument
-    def __new__(cls, *args: Any, **kwargs: Any) -> "RuntimeOptions":
-        issue_deprecation_msg(
-            msg="The RuntimeOptions class has been deprecated",
-            version="0.7",
-            remedy="Please use qiskit_ibm_runtime.Options class instead.",
-        )
-        return super().__new__(cls)
+    """Class for representing generic runtime execution options."""
 
-    backend_name: Optional[str] = None
-    image: Optional[str] = None
-    log_level: Optional[str] = None
+    @deprecate_arguments({"backend_name": "backend"})
+    def __init__(
+        self,
+        backend: Optional[str] = None,
+        image: Optional[str] = None,
+        log_level: Optional[str] = None,
+    ) -> None:
+        """RuntimeOptions constructor.
+
+        Args:
+            backend: target backend to run on. This is required for ``ibm_quantum`` channel.
+            image: the runtime image used to execute the program, specified in
+                the form of ``image_name:tag``. Not all accounts are
+                authorized to select a different image.
+            log_level: logging level to set in the execution environment. The valid
+                log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
+                The default level is ``WARNING``.
+        """
+        self.backend = backend
+        self.image = image
+        self.log_level = log_level
 
     def validate(self, channel: str) -> None:
         """Validate options.
@@ -64,9 +65,9 @@ class RuntimeOptions:
         ):
             raise IBMInputValueError('"image" needs to be in form of image_name:tag')
 
-        if channel == "ibm_quantum" and not self.backend_name:
+        if channel == "ibm_quantum" and not self.backend:
             raise IBMInputValueError(
-                '"backend_name" is required field in "options" for ``ibm_quantum`` runtime.'
+                '"backend" is required field in "options" for ``ibm_quantum`` runtime.'
             )
 
         if self.log_level and not isinstance(
@@ -79,7 +80,29 @@ class RuntimeOptions:
 
     def _to_new_options(self) -> Options:
         return Options(
-            backend=self.backend_name,
             log_level=self.log_level,
             experimental={"image": self.image},
         )
+
+    @property
+    def backend_name(self) -> str:
+        """Return backend.
+
+        Returns:
+            Backend name.
+        """
+        return self.backend
+
+    @backend_name.setter
+    def backend_name(self, name: str) -> None:
+        """Set backend name.
+
+        Args:
+            name: Backend to use.
+        """
+        issue_deprecation_msg(
+            msg="The 'backend_name' attribute is deprecated",
+            version="0.7",
+            remedy="Please use 'backend' instead.",
+        )
+        self._backend = name
diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py
index 6c2ce4af..eb3fb071 100644
--- a/qiskit_ibm_runtime/sampler.py
+++ b/qiskit_ibm_runtime/sampler.py
@@ -17,15 +17,15 @@ from typing import Dict, Iterable, Optional, Sequence, Any, Union
 import copy
 
 from qiskit.circuit import QuantumCircuit, Parameter
-import qiskit_ibm_runtime.session as session_pkg
 
 # TODO import BaseSampler and SamplerResult from terra once released
 from .qiskit.primitives import BaseSampler, SamplerResult
 from .qiskit_runtime_service import QiskitRuntimeService
 from .options import Options
-from .runtime_options import RuntimeOptions
 from .program.result_decoder import ResultDecoder
 from .runtime_job import RuntimeJob
+from .ibm_backend import IBMBackend
+from .session import get_default_session
 from .utils.deprecation import deprecate_arguments, issue_deprecation_msg
 
 # pylint: disable=unused-import,cyclic-import
@@ -68,8 +68,8 @@ class Sampler(BaseSampler):
         circuits: Optional[Union[QuantumCircuit, Iterable[QuantumCircuit]]] = None,
         parameters: Optional[Iterable[Iterable[Parameter]]] = None,
         service: Optional[QiskitRuntimeService] = None,
-        session: Optional[Session] = None,
-        options: Optional[Union[Dict, RuntimeOptions, Options]] = None,
+        session: Optional[Union[Session, str, IBMBackend]] = None,
+        options: Optional[Union[Dict, Options]] = None,
         skip_transpilation: Optional[bool] = False,
     ):
         """Initializes the Sampler primitive.
@@ -87,10 +87,14 @@ class Sampler(BaseSampler):
                 defaults to `QiskitRuntimeService()` which tries to initialize your default
                 saved account.
 
-            session: Session in which to call the sampler primitive. If ``None``, a new session
-                is created using the default saved account.
+            session: Session in which to call the primitive. If an instance of
+                :class:`qiskit_ibm_runtime.IBMBackend` class or
+                string name of a backend is specified, a new session is created for
+                that backend. If ``None``, a new session is created using the default
+                saved account and a default backend (IBM Cloud channel only).
 
             options: Primitive options, see :class:`Options` for detailed description.
+                The ``backend`` keyword is still supported but is deprecated.
 
             skip_transpilation (DEPRECATED): Transpilation is skipped if set to True. False by default.
                 Ignored ``skip_transpilation`` is also specified in ``options``.
@@ -119,14 +123,22 @@ class Sampler(BaseSampler):
                 "service", "0.7", "Please use the session parameter instead."
             )
 
+        backend = None
+        self._session: Session = None
+
         if options is None:
             self.options = Options()
         elif isinstance(options, Options):
             self.options = copy.deepcopy(options)
             skip_transpilation = self.options.transpilation.skip_transpilation
-        elif isinstance(options, RuntimeOptions):
-            self.options = options._to_new_options()
         else:
+            backend = options.pop("backend", None)
+            if backend is not None:
+                issue_deprecation_msg(
+                    msg="The 'backend' key in 'options' has been deprecated",
+                    version="0.7",
+                    remedy="Please pass the backend when opening a session.",
+                )
             self.options = Options._from_dict(options)
             skip_transpilation = options.get("transpilation", {}).get(
                 "skip_transpilation", False
@@ -134,15 +146,11 @@ class Sampler(BaseSampler):
         self.options.transpilation.skip_transpilation = skip_transpilation
 
         self._initial_inputs = {"circuits": circuits, "parameters": parameters}
-        if session:
+        if isinstance(session, Session):
             self._session = session
         else:
-            if (
-                session_pkg._DEFAULT_SESSION is None
-                or not session_pkg._DEFAULT_SESSION._active
-            ):
-                session_pkg._DEFAULT_SESSION = Session(service=service)
-            self._session = session_pkg._DEFAULT_SESSION
+            backend = session or backend
+            self._session = get_default_session(service, backend)
 
     def run(
         self,
diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py
index 33331cd6..b622334f 100644
--- a/qiskit_ibm_runtime/session.py
+++ b/qiskit_ibm_runtime/session.py
@@ -20,6 +20,7 @@ from qiskit_ibm_runtime import QiskitRuntimeService
 from .runtime_job import RuntimeJob
 from .runtime_program import ParameterNamespace
 from .program.result_decoder import ResultDecoder
+from .ibm_backend import IBMBackend
 from .utils.converters import hms_to_seconds
 
 
@@ -47,9 +48,9 @@ class Session:
         from qiskit.test.reference_circuits import ReferenceCircuits
         from qiskit_ibm_runtime import Sampler, Session, Options
 
-        options = Options(backend="ibmq_qasm_simulator", optimization_level=3)
+        options = Options(optimization_level=3)
 
-        with Session() as session:
+        with Session(backend="ibmq_qasm_simulator") as session:
             sampler = Sampler(session=session, options=options)
             job = sampler.run(circ)
             print(f"Sampler job ID: {job.job_id}")
@@ -59,6 +60,7 @@ class Session:
     def __init__(
         self,
         service: Optional[QiskitRuntimeService] = None,
+        backend: Optional[Union[str, IBMBackend]] = None,
         max_time: Optional[Union[int, str]] = None,
     ):
         """Session constructor.
@@ -67,12 +69,25 @@ class Session:
             service: Optional instance of the ``QiskitRuntimeService`` class,
                 defaults to ``QiskitRuntimeService()`` which tries to initialize
                 your default saved account.
+            backend: Optional instance of :class:`qiskit_ibm_runtime.IBMBackend` class or
+                string name of backend. If not specified, a backend will be selected
+                automatically (IBM Cloud channel only).
             max_time: (EXPERIMENTAL setting, can break between releases without warning)
                 Maximum amount of time, a runtime session can be open before being
                 forcibly closed. Can be specified as seconds (int) or a string like "2h 30m 40s".
+
+        Raises:
+            ValueError: If an input value is invalid.
         """
 
         self._service = service or QiskitRuntimeService()
+
+        if self._service.channel == "ibm_quantum" and not backend:
+            raise ValueError('"backend" is required for ``ibm_quantum`` channel.')
+        if isinstance(backend, IBMBackend):
+            backend = backend.name
+        self._backend = backend
+
         self._session_id: Optional[str] = None
         self._active = True
 
@@ -98,7 +113,6 @@ class Session:
                 to the runtime program.
             options: Runtime options that control the execution environment.
 
-                * backend: target backend to run on. This is required for ``ibm_quantum`` runtime.
                 * image: the runtime image used to execute the program, specified in
                   the form of ``image_name:tag``. Not all accounts are
                   authorized to select a different image.
@@ -114,6 +128,8 @@ class Session:
 
         # TODO: Do we really need to specify a None max time if session has started?
         max_time = self._max_time if not self._session_id else None
+        options = options or {}
+        options["backend"] = self._backend
 
         job = self._service.run(
             program_id=program_id,
@@ -128,6 +144,9 @@ class Session:
         if self._session_id is None:
             self._session_id = job.job_id
 
+        if self._backend is None:
+            self._backend = job.backend.name
+
         return job
 
     def close(self) -> None:
@@ -154,6 +173,15 @@ class Session:
         """
         return self._service
 
+    @property
+    def backend(self) -> Optional[str]:
+        """Return backend for this session.
+
+        Returns:
+            Backend for this session. None if unknown.
+        """
+        return self._backend
+
     def __enter__(self) -> "Session":
         return self
 
@@ -168,3 +196,35 @@ class Session:
 
 # Default session
 _DEFAULT_SESSION: Optional[Session] = None
+
+
+def get_default_session(
+    service: Optional[QiskitRuntimeService] = None,
+    backend: Optional[Union[str, IBMBackend]] = None,
+) -> Session:
+    """Return the default session.
+
+    Args:
+        service: Service to use to create a default session.
+        backend: Backend for the default session.
+    """
+    if service is None:
+        service = (
+            backend.service
+            if isinstance(backend, IBMBackend)
+            else QiskitRuntimeService()
+        )
+    if isinstance(backend, IBMBackend):
+        backend = backend.name
+
+    global _DEFAULT_SESSION  # pylint: disable=global-statement
+    if (
+        _DEFAULT_SESSION is None
+        or not _DEFAULT_SESSION._active
+        or _DEFAULT_SESSION._backend != backend
+        or (service is not None and _DEFAULT_SESSION.service.channel != service.channel)
+    ):
+        if _DEFAULT_SESSION and _DEFAULT_SESSION._active:
+            _DEFAULT_SESSION.close()
+        _DEFAULT_SESSION = Session(service=service, backend=backend)
+    return _DEFAULT_SESSION
diff --git a/releasenotes/notes/0.7/flexible-session-16ee0d1cba4863cd.yaml b/releasenotes/notes/0.7/flexible-session-16ee0d1cba4863cd.yaml
index 828cbc4f..6775c7d5 100644
--- a/releasenotes/notes/0.7/flexible-session-16ee0d1cba4863cd.yaml
+++ b/releasenotes/notes/0.7/flexible-session-16ee0d1cba4863cd.yaml
@@ -17,7 +17,7 @@ features:
       service = QiskitRuntimeService()
 
       # Set options, which can be overwritten at job level.
-      options = Options(backend="ibmq_qasm_simulator")
+      options = Options(optimization_level=1)
 
       # Prepare inputs.
       bell = ReferenceCircuits.bell()
@@ -25,7 +25,7 @@ features:
       H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
       theta = [0, 1, 1, 2, 3, 5]
 
-      with Session(service) as session:
+      with Session(service=service, backend="ibmq_qasm_simulator") as session:
           # Submit a request to the Sampler primitive within the session.
           sampler = Sampler(session=session, options=options)
           job = sampler.run(circuits=bell)
@@ -44,10 +44,10 @@ features:
       from qiskit_ibm_runtime import Session, Sampler, Options
       from qiskit.test.reference_circuits import ReferenceCircuits
 
-      options = Options(backend="ibmq_qasm_simulator")
+      options = Options()
       options.optimization_level = 3  # This can be done using auto-complete.
 
-      with Session() as session:
+      with Session(backend="ibmq_qasm_simulator") as session:
         # Pass the options to Sampler.
         sampler = Sampler(session=session, options=options)
 
@@ -97,6 +97,3 @@ deprecations:
       options = Options()
       # This can be done using auto-complete.
       options.transpilation.skip_transpilation = True
-  - |
-    The `qiskit_ibm_runtime.RuntimeOptions` class has been deprecated and
-    replaced by `qiskit_ibm_runtime.Options`.
\ No newline at end of file
diff --git a/releasenotes/notes/runtime-options-38915f0a5cee60cb.yaml b/releasenotes/notes/runtime-options-38915f0a5cee60cb.yaml
new file mode 100644
index 00000000..695e3f3c
--- /dev/null
+++ b/releasenotes/notes/runtime-options-38915f0a5cee60cb.yaml
@@ -0,0 +1,5 @@
+---
+deprecations:
+  - |
+    The ``backend_name`` attribute in :class:`qiskit_ibm_runtime.RuntimeOptions`
+    is deprecated and replaced by ``backend``.


Test patch:
diff --git a/test/integration/test_estimator.py b/test/integration/test_estimator.py
index 1d8db322..67b90efa 100644
--- a/test/integration/test_estimator.py
+++ b/test/integration/test_estimator.py
@@ -28,7 +28,7 @@ class TestIntegrationEstimator(IBMIntegrationTestCase):
     def test_estimator_session(self, service):
         """Verify if estimator primitive returns expected results"""
 
-        options = {"backend": "ibmq_qasm_simulator"}
+        backend = "ibmq_qasm_simulator"
 
         psi1 = RealAmplitudes(num_qubits=2, reps=2)
         psi2 = RealAmplitudes(num_qubits=2, reps=3)
@@ -38,8 +38,8 @@ class TestIntegrationEstimator(IBMIntegrationTestCase):
         H2 = SparsePauliOp.from_list([("IZ", 1)])
         H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)])
 
-        with Session(service) as session:
-            estimator = Estimator(session=session, options=options)
+        with Session(service, backend) as session:
+            estimator = Estimator(session=session)
             self.assertIsInstance(estimator, BaseEstimator)
 
             theta1 = [0, 1, 1, 2, 3, 5]
diff --git a/test/integration/test_sampler.py b/test/integration/test_sampler.py
index 4ca898df..bd49c784 100644
--- a/test/integration/test_sampler.py
+++ b/test/integration/test_sampler.py
@@ -29,15 +29,15 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
     def setUp(self) -> None:
         super().setUp()
         self.bell = ReferenceCircuits.bell()
-        self.options = {"backend": "ibmq_qasm_simulator"}
+        self.backend = "ibmq_qasm_simulator"
 
     @run_integration_test
     def test_sampler_non_parameterized_single_circuit(self, service):
         """Verify if sampler primitive returns expected results for non-parameterized circuits."""
 
         # Execute a Bell circuit
-        with Session(service) as session:
-            sampler = Sampler(session=session, options=self.options)
+        with Session(service, self.backend) as session:
+            sampler = Sampler(session=session)
             self.assertIsInstance(sampler, BaseSampler)
             job = sampler.run(circuits=self.bell)
             result = job.result()
@@ -52,8 +52,8 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
     def test_sampler_non_parameterized_circuits(self, service):
         """Test sampler with multiple non-parameterized circuits."""
         # Execute three Bell circuits
-        with Session(service) as session:
-            sampler = Sampler(session=session, options=self.options)
+        with Session(service, self.backend) as session:
+            sampler = Sampler(session=session)
             self.assertIsInstance(sampler, BaseSampler)
             circuits = [self.bell] * 3
 
@@ -98,8 +98,8 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
         theta2 = [1, 2, 3, 4, 5, 6]
         theta3 = [0, 1, 2, 3, 4, 5, 6, 7]
 
-        with Session(service) as session:
-            sampler = Sampler(session=session, options=self.options)
+        with Session(service, self.backend) as session:
+            sampler = Sampler(session=session)
             self.assertIsInstance(sampler, BaseSampler)
 
             circuits0 = [pqc, pqc, pqc2]
@@ -119,8 +119,8 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
         circ.append(custom_gate, [0])
         circ.measure(0, 0)
 
-        with Session(service) as session:
-            sampler = Sampler(session=session, options=self.options)
+        with Session(service, self.backend) as session:
+            sampler = Sampler(session=session)
             sampler.options.transpilation.skip_transpilation = True
             with self.assertRaises(RuntimeJobFailureError) as err:
                 sampler.run(circuits=circ).result()
@@ -130,8 +130,8 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
     @run_integration_test
     def test_sampler_optimization_level(self, service):
         """Test transpiler optimization level is properly mapped."""
-        with Session(service) as session:
-            sampler = Sampler(session=session, options=self.options)
+        with Session(service, self.backend) as session:
+            sampler = Sampler(session=session)
             sampler.options.optimization_level = 3
             result = sampler.run(self.bell).result()
             self.assertAlmostEqual(result.quasi_dists[0]["11"], 0.5, delta=0.05)
@@ -152,7 +152,9 @@ class TestIntegrationIBMSampler(IBMIntegrationTestCase):
         theta3 = [0, 1, 2, 3, 4, 5, 6, 7]
 
         with Sampler(
-            circuits=[pqc, pqc2], service=service, options=self.options
+            circuits=[pqc, pqc2],
+            service=service,
+            options={"backend": "ibmq_qasm_simulator"},
         ) as sampler:
             self.assertIsInstance(sampler, BaseSampler)
 
diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py
index 4bcb3f2e..96706a3f 100644
--- a/test/unit/test_ibm_primitives.py
+++ b/test/unit/test_ibm_primitives.py
@@ -13,22 +13,28 @@
 """Tests for primitive classes."""
 
 import sys
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, patch, ANY
 from dataclasses import asdict
 
-from qiskit_ibm_runtime import Sampler, Estimator, Options, RuntimeOptions
+from qiskit_ibm_runtime import Sampler, Estimator, Options, Session
+from qiskit_ibm_runtime.ibm_backend import IBMBackend
+import qiskit_ibm_runtime.session as session_pkg
 from ..ibm_test_case import IBMTestCase
 
 
 class TestPrimitives(IBMTestCase):
     """Class for testing the Sampler class."""
 
+    def tearDown(self) -> None:
+        super().tearDown()
+        session_pkg._DEFAULT_SESSION = None
+
     def test_skip_transpilation(self):
         """Test skip_transpilation is hornored."""
         primitives = [Sampler, Estimator]
         for cls in primitives:
             with self.subTest(primitive=cls):
-                inst = cls(session=MagicMock(), skip_transpilation=True)
+                inst = cls(session=MagicMock(spec=Session), skip_transpilation=True)
                 self.assertTrue(inst.options.transpilation.skip_transpilation)
 
     def test_skip_transpilation_overwrite(self):
@@ -39,7 +45,9 @@ class TestPrimitives(IBMTestCase):
         for cls in primitives:
             with self.subTest(primitive=cls):
                 inst = cls(
-                    session=MagicMock(), options=options, skip_transpilation=True
+                    session=MagicMock(spec=Session),
+                    options=options,
+                    skip_transpilation=True,
                 )
                 self.assertFalse(inst.options.transpilation.skip_transpilation)
 
@@ -51,6 +59,7 @@ class TestPrimitives(IBMTestCase):
                 "resilience_level": 1,
                 "transpilation": {"seed_transpiler": 24},
                 "execution": {"shots": 100, "init_qubits": True},
+                "log_level": "INFO",
             },
             {"transpilation": {}},
         ]
@@ -58,31 +67,51 @@ class TestPrimitives(IBMTestCase):
         for cls in primitives:
             for options in options_vars:
                 with self.subTest(primitive=cls, options=options):
-                    inst = cls(session=MagicMock(), options=options)
+                    inst = cls(session=MagicMock(spec=Session), options=options)
                     expected = asdict(Options())
                     self._update_dict(expected, options)
                     self.assertDictEqual(expected, asdict(inst.options))
 
-    def test_runtime_options(self):
-        """Test passing in runtime options."""
-        options_vars = [
-            {"backend_name": "foo", "image": "foo:bar"},
-            RuntimeOptions(backend_name="foo", image="foo:bar"),
-        ]
+    def test_backend_in_options(self):
+        """Test specifying backend in options."""
         primitives = [Sampler, Estimator]
+        backend_name = "ibm_gotham"
+        backend = MagicMock(spec=IBMBackend)
+        backend.name = backend_name
+        backends = [backend_name, backend]
         for cls in primitives:
-            for options in options_vars:
-                with self.subTest(primitive=cls, options=options):
-                    inst = cls(session=MagicMock(), options=options)
-                    if not isinstance(options, dict):
-                        options = asdict(options)
-                    self.assertEqual(options["backend_name"], inst.options.backend)
-                    self.assertEqual(
-                        options["image"], inst.options.experimental["image"]
-                    )
-
-    @patch("qiskit_ibm_runtime.estimator.Session")
-    @patch("qiskit_ibm_runtime.sampler.Session")
+            for backend in backends:
+                with self.subTest(primitive=cls, backend=backend):
+                    options = {"backend": backend}
+                    inst = cls(service=MagicMock(), options=options)
+                    self.assertEqual(inst.session.backend, backend_name)
+
+    @patch("qiskit_ibm_runtime.session.Session")
+    @patch("qiskit_ibm_runtime.session.QiskitRuntimeService")
+    def test_backend_str_as_session(self, _, mock_session):
+        """Test specifying a backend name as session."""
+        primitives = [Sampler, Estimator]
+        backend_name = "ibm_gotham"
+
+        for cls in primitives:
+            with self.subTest(primitive=cls):
+                _ = cls(session=backend_name)
+                mock_session.assert_called_with(service=ANY, backend=backend_name)
+
+    def test_backend_as_session(self):
+        """Test specifying a backend as session."""
+        primitives = [Sampler, Estimator]
+        backend = MagicMock(spec=IBMBackend)
+        backend.name = "ibm_gotham"
+        backend.service = MagicMock()
+
+        for cls in primitives:
+            with self.subTest(primitive=cls):
+                inst = cls(session=backend)
+                self.assertEqual(inst.session.backend, backend.name)
+
+    @patch("qiskit_ibm_runtime.session.Session")
+    @patch("qiskit_ibm_runtime.session.QiskitRuntimeService")
     def test_default_session(self, *_):
         """Test a session is created if not passed in."""
         sampler = Sampler()
@@ -92,7 +121,7 @@ class TestPrimitives(IBMTestCase):
 
     def test_run_inputs_default(self):
         """Test run using default options."""
-        session = MagicMock()
+        session = MagicMock(spec=Session)
         options_vars = [
             (Options(resilience_level=9), {"resilience_settings": {"level": 9}}),
             (
@@ -123,7 +152,7 @@ class TestPrimitives(IBMTestCase):
 
     def test_run_inputs_updated_default(self):
         """Test run using updated default options."""
-        session = MagicMock()
+        session = MagicMock(spec=Session)
         primitives = [Sampler, Estimator]
         for cls in primitives:
             with self.subTest(primitive=cls):
@@ -150,7 +179,7 @@ class TestPrimitives(IBMTestCase):
 
     def test_run_inputs_overwrite(self):
         """Test run using overwritten options."""
-        session = MagicMock()
+        session = MagicMock(spec=Session)
         options_vars = [
             ({"resilience_level": 9}, {"resilience_settings": {"level": 9}}),
             ({"shots": 200}, {"run_options": {"shots": 200}}),
diff --git a/test/unit/test_session.py b/test/unit/test_session.py
new file mode 100644
index 00000000..614e01bf
--- /dev/null
+++ b/test/unit/test_session.py
@@ -0,0 +1,210 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2022.
+#
+# 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.
+
+"""Tests for Session classession."""
+
+from unittest.mock import MagicMock, patch
+
+from qiskit_ibm_runtime import Session
+from qiskit_ibm_runtime.ibm_backend import IBMBackend
+from qiskit_ibm_runtime.session import get_default_session
+import qiskit_ibm_runtime.session as session_pkg
+from ..ibm_test_case import IBMTestCase
+
+
+class TestSession(IBMTestCase):
+    """Class for testing the Session classession."""
+
+    def tearDown(self) -> None:
+        super().tearDown()
+        session_pkg._DEFAULT_SESSION = None
+
+    @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True)
+    def test_default_service(self, mock_service):
+        """Test using default service."""
+        session = Session(backend="ibm_gotham")
+        self.assertIsNotNone(session.service)
+        mock_service.assert_called_once()
+
+    def test_missing_backend(self):
+        """Test missing backend."""
+        service = MagicMock()
+        service.channel = "ibm_quantum"
+        with self.assertRaises(ValueError):
+            Session(service=service)
+
+    def test_passing_ibm_backend(self):
+        """Test passing in IBMBackend instance."""
+        backend = MagicMock(spec=IBMBackend)
+        backend.name = "ibm_gotham"
+        session = Session(service=MagicMock(), backend=backend)
+        self.assertEqual(session.backend, "ibm_gotham")
+
+    def test_max_time(self):
+        """Test max time."""
+        max_times = [
+            (42, 42),
+            ("1h", 1 * 60 * 60),
+            ("2h 30m 40s", 2 * 60 * 60 + 30 * 60 + 40),
+            ("40s 1h", 40 + 1 * 60 * 60),
+        ]
+        for max_t, expected in max_times:
+            with self.subTest(max_time=max_t):
+                session = Session(
+                    service=MagicMock(), backend="ibm_gotham", max_time=max_t
+                )
+                self.assertEqual(session._max_time, expected)
+
+    def test_run_after_close(self):
+        """Test running after session is closed."""
+        session = Session(service=MagicMock(), backend="ibm_gotham")
+        session.close()
+        with self.assertRaises(RuntimeError):
+            session.run(program_id="program_id", inputs={})
+
+    def test_run(self):
+        """Test the run method."""
+        job = MagicMock()
+        job.job_id = "12345"
+        service = MagicMock()
+        service.run.return_value = job
+        backend = "ibm_gotham"
+        inputs = {"name": "bruce wayne"}
+        options = {"log_level": "INFO"}
+        program_id = "batman_begins"
+        decoder = MagicMock()
+        max_time = 42
+        session = Session(service=service, backend=backend, max_time=max_time)
+        session_ids = [None, job.job_id]
+        start_sessions = [True, False]
+
+        for idx in range(2):
+            session.run(
+                program_id=program_id,
+                inputs=inputs,
+                options=options,
+                result_decoder=decoder,
+            )
+            _, kwargs = service.run.call_args
+            self.assertEqual(kwargs["program_id"], program_id)
+            self.assertDictEqual(kwargs["options"], {"backend": backend, **options})
+            self.assertDictEqual(kwargs["inputs"], inputs)
+            self.assertEqual(kwargs["session_id"], session_ids[idx])
+            self.assertEqual(kwargs["start_session"], start_sessions[idx])
+            self.assertEqual(kwargs["result_decoder"], decoder)
+            self.assertEqual(session.session_id, job.job_id)
+            self.assertEqual(session.backend, backend)
+
+    def test_close_without_run(self):
+        """Test closing without run."""
+        service = MagicMock()
+        api = MagicMock()
+        service._api_client = api
+        session = Session(service=service, backend="ibm_gotham")
+        session.close()
+        api.close_session.assert_not_called()
+
+    def test_context_manager(self):
+        """Test session as a context manager."""
+        with Session(service=MagicMock(), backend="ibm_gotham") as session:
+            session.run(program_id="foo", inputs={})
+        self.assertFalse(session._active)
+
+    def test_default_backend(self):
+        """Test default backend set."""
+        job = MagicMock()
+        job.backend.name = "ibm_gotham"
+        service = MagicMock()
+        service.run.return_value = job
+        service.channel = "ibm_cloud"
+
+        session = Session(service=service)
+        session.run(program_id="foo", inputs={})
+        self.assertEqual(session.backend, "ibm_gotham")
+
+    def test_opening_default_session(self):
+        """Test opening default session."""
+        backend = "ibm_gotham"
+        service = MagicMock()
+        session = get_default_session(service=service, backend=backend)
+        self.assertIsInstance(session, Session)
+        self.assertEqual(session.service, service)
+        self.assertEqual(session.backend, backend)
+
+        session2 = get_default_session(service=service, backend=backend)
+        self.assertEqual(session, session2)
+
+    def test_closed_default_session(self):
+        """Test default session closed."""
+        backend = "ibm_gotham"
+        service = MagicMock()
+        session = get_default_session(service=service, backend=backend)
+        session.close()
+
+        session2 = get_default_session(service=service, backend=backend)
+        self.assertNotEqual(session, session2)
+        self.assertEqual(session2.service, service)
+        self.assertEqual(session2.backend, backend)
+
+    def test_default_session_different_backend(self):
+        """Test default session backend change."""
+        service = MagicMock()
+        session = get_default_session(service=service, backend="ibm_gotham")
+        session2 = get_default_session(service=service, backend="ibm_metropolis")
+        self.assertNotEqual(session, session2)
+        self.assertFalse(session._active)
+        self.assertEqual(session2.backend, "ibm_metropolis")
+        self.assertTrue(session2._active)
+
+    def test_default_session_different_service(self):
+        """Test default session service change."""
+        service2 = MagicMock()
+        backend = "ibm_gotham"
+        session = get_default_session(service=MagicMock(), backend=backend)
+        session2 = get_default_session(service=service2, backend=backend)
+        self.assertNotEqual(session, session2)
+        self.assertFalse(session._active)
+        self.assertTrue(session2._active)
+
+    @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True)
+    def test_default_session_no_service(self, mock_service):
+        """Test getting default session with no service."""
+        backend = "ibm_gotham"
+        session = get_default_session(backend=backend)
+        self.assertIsInstance(session, Session)
+        self.assertEqual(session.backend, backend)
+        mock_service.assert_called_once()
+
+    def test_default_session_backend_service(self):
+        """Test getting default session using service from backend."""
+        backend = MagicMock(spec=IBMBackend)
+        service = MagicMock()
+        backend.service = service
+        backend.name = "ibm_gotham"
+        session = get_default_session(backend=backend)
+        self.assertIsInstance(session, Session)
+        self.assertEqual(session.service, backend.service)
+
+    def test_default_session_no_backend_quantum(self):
+        """Test getting default session with no backend."""
+        service = MagicMock()
+        service.channel = "ibm_quantum"
+        with self.assertRaises(ValueError):
+            _ = get_default_session(service=service)
+
+    @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True)
+    def test_default_session_no_service_backend(self, mock_service):
+        """Test getting default session without service and backend."""
+        mock_inst = mock_service.return_value
+        mock_inst.channel = "ibm_cloud"
+        session = get_default_session()
+        self.assertIsInstance(session, Session)