diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 1acdafa996..f79b2ba33d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -20,6 +20,9 @@ on:
jobs:
Deploy:
runs-on: ubuntu-latest
+ environment: release
+ permissions:
+ id-token: write
strategy:
matrix:
python-version: [3.9]
@@ -28,12 +31,12 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
+ - name: Install deps
+ run: pip install -U pip setuptools build
+ - name: Build sdist
+ run: python3 -m build
+ - uses: actions/upload-artifact@v3
+ with:
+ path: ./dist/*
- name: Deploy to Pypi
- env:
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
- TWINE_USERNAME: qiskit
- run : |
- pip install -U twine pip setuptools virtualenv wheel
- python3 setup.py sdist bdist_wheel
- twine upload dist/qiskit*
- shell: bash
\ No newline at end of file
+ uses: pypa/gh-action-pypi-publish@release/v1
\ No newline at end of file
diff --git a/README.md b/README.md
index 892e7d4932..5bbb1e37de 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@
**Qiskit** is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
-**Qiskit IBM Runtime** is a new environment offered by IBM Quantum that streamlines quantum computations and provides optimal
-implementations of the Qiskit primitives `sampler` and `estimator` for IBM Quantum hardware. It is designed to use additional classical compute resources to execute quantum circuits with more efficiency on quantum processors, by including near-time computations such as error suppression and error mitigation. Examples of error suppression include dynamical decoupling, noise-aware compilation, error mitigation including readout mitigation, zero-noise extrapolation (ZNE), and probabilistic error cancellation (PEC).
+**Qiskit IBM Runtime** is a new environment offered by IBM Quantum that streamlines quantum computations and provides optimal
+implementations of the Qiskit primitives `sampler` and `estimator` for IBM Quantum hardware. It is designed to use additional classical compute resources to execute quantum circuits with more efficiency on quantum processors, by including near-time computations such as error suppression and error mitigation. Examples of error suppression include dynamical decoupling, noise-aware compilation, error mitigation including readout mitigation, zero-noise extrapolation (ZNE), and probabilistic error cancellation (PEC).
Using the runtime service, a research team at IBM Quantum was able to achieve a 120x speedup
in their lithium hydride simulation. For more information, see the
@@ -30,8 +30,6 @@ pip install qiskit-ibm-runtime
### Qiskit Runtime service on IBM Quantum Platform
-The default method for using the runtime service is IBM Quantum Platform.
-
You will need your IBM Quantum API token to authenticate with the runtime service:
1. Create an IBM Quantum account or log in to your existing account by visiting the [IBM Quantum login page].
@@ -114,7 +112,7 @@ All quantum applications and algorithms level are fundamentally built using thre
The IBM Runtime service offers these primitives with additional features, such as built-in error suppression and mitigation.
-There are several different options you can specify when calling the primitives. See [`qiskit_ibm_runtime.Options`](https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/options.py#L103) class for more information.
+There are several different options you can specify when calling the primitives. See [`qiskit_ibm_runtime.Options`](https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/options/options.py#L33) class for more information.
### Sampler
@@ -138,7 +136,7 @@ bell.cx(0, 1)
# 2. Map the qubits to a classical register in ascending order
bell.measure_all()
-# 3. Execute using the Sampler primitive
+# 3. Execute using the Sampler primitive
backend = service.get_backend('ibmq_qasm_simulator')
sampler = Sampler(backend=backend, options=options)
job = sampler.run(circuits=bell)
@@ -174,14 +172,14 @@ qc_example.cx(0, 2) # condition 2nd qubit on 0th qubit
# 2. the observable to be measured
M1 = SparsePauliOp.from_list([("XXY", 1), ("XYX", 1), ("YXX", 1), ("YYY", -1)])
-# batch of theta parameters to be executed
+# batch of theta parameters to be executed
points = 50
theta1 = []
for x in range(points):
theta = [x*2.0*np.pi/50]
theta1.append(theta)
-# 3. Execute using the Estimator primitive
+# 3. Execute using the Estimator primitive
backend = service.get_backend('ibmq_qasm_simulator')
estimator = Estimator(backend, options=options)
job = estimator.run(circuits=[qc_example]*points, observables=[M1]*points, parameter_values=theta1)
@@ -197,7 +195,7 @@ This code batches together 50 parameters to be executed in a single job. If a us
In many algorithms and applications, an Estimator needs to be called iteratively without incurring queuing delays on each iteration. To solve this, the IBM Runtime service provides a **Session**. A session starts when the first job within the session is started, and subsequent jobs within the session are prioritized by the scheduler.
You can use the [`qiskit_ibm_runtime.Session`](https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/session.py) class to start a
-session. Consider the same example above and try to find the optimal `theta`. The following example uses the [golden search method](https://en.wikipedia.org/wiki/Golden-section_search) to iteratively find the optimal theta that maximizes the observable.
+session. Consider the same example above and try to find the optimal `theta`. The following example uses the [golden search method](https://en.wikipedia.org/wiki/Golden-section_search) to iteratively find the optimal theta that maximizes the observable.
To invoke the `Estimator` primitive within a session:
@@ -224,15 +222,15 @@ qc_example.cx(0, 2) # condition 2nd qubit on 0th qubit
M1 = SparsePauliOp.from_list([("XXY", 1), ("XYX", 1), ("YXX", 1), ("YYY", -1)])
-gr = (np.sqrt(5) + 1) / 2 # golden ratio
+gr = (np.sqrt(5) + 1) / 2 # golden ratio
thetaa = 0 # lower range of theta
thetab = 2*np.pi # upper range of theta
-tol = 1e-1 # tol
+tol = 1e-1 # tol
-# 3. Execute iteratively using the Estimator primitive
+# 3. Execute iteratively using the Estimator primitive
with Session(service=service, backend="ibmq_qasm_simulator") as session:
estimator = Estimator(session=session, options=options)
- #next test range
+ #next test range
thetac = thetab - (thetab - thetaa) / gr
thetad = thetaa + (thetab - thetaa) / gr
while abs(thetab - thetaa) > tol:
@@ -245,8 +243,8 @@ with Session(service=service, backend="ibmq_qasm_simulator") as session:
thetaa = thetac
thetac = thetab - (thetab - thetaa) / gr
thetad = thetaa + (thetab - thetaa) / gr
-
- # Final job to evaluate Estimator at midpoint found using golden search method
+
+ # Final job to evaluate Estimator at midpoint found using golden search method
theta_mid = (thetab + thetaa) / 2
job = estimator.run(circuits=qc_example, observables=M1, parameter_values=theta_mid)
print(f"Session ID is {session.session_id}")
@@ -256,6 +254,38 @@ with Session(service=service, backend="ibmq_qasm_simulator") as session:
This code returns `Job result is [4.] at theta = 1.575674623307102` using only nine iterations. This is a very powerful extension to the primitives. However, using too much code between iterative calls can lock the QPU and use excessive QPU time, which is expensive. We recommend only using sessions when needed. The Sampler can also be used within a session, but there are not any well-defined examples for this.
+## Instances
+
+Access to IBM Quantum Platform channel is controlled by the instances (previously called providers) to which you are assigned. An instance is defined by a hierarchical organization of hub, group, and project. A hub is the top level of a given hierarchy (organization) and contains within it one or more groups. These groups are in turn populated with projects. The combination of hub/group/project is called an instance. Users can belong to more than one instance at any time.
+
+> **_NOTE:_** IBM Cloud instances are different from IBM Quantum Platform instances. IBM Cloud does not use the hub/group/project structure for user management. To view and create IBM Cloud instances, visit the [IBM Cloud Quantum Instances page](https://cloud.ibm.com/quantum/instances).
+
+To view a list of your instances, visit your [account settings page](https://www.quantum-computing.ibm.com/account) or use the `instances()` method.
+
+You can specify an instance when initializing the service or provider, or when picking a backend:
+
+```python
+# Optional: List all the instances you can access.
+service = QiskitRuntimeService(channel='ibm_quantum')
+print(service.instances())
+
+# Optional: Specify the instance at service level. This becomes the default unless overwritten.
+service = QiskitRuntimeService(channel='ibm_quantum', instance="hub1/group1/project1")
+backend1 = service.backend("ibmq_manila")
+
+# Optional: Specify the instance at the backend level, which overwrites the service-level specification when this backend is used.
+backend2 = service.backend("ibmq_manila", instance="hub2/group2/project2")
+
+sampler1 = Sampler(backend=backend1) # this will use hub1/group1/project1
+sampler2 = Sampler(backend=backend2) # this will use hub2/group2/project2
+```
+
+If you do not specify an instance, then the code will select one in the following order:
+
+1. If your account only has access to one instance, it is selected by default.
+2. If your account has access to multiple instances, but only one can access the requested backend, the instance with access is selected.
+3. In all other cases, the code selects the first instance other than ibm-q/open/main that has access to the backend.
+
## Access your IBM Quantum backends
A **backend** is a quantum device or simulator capable of running quantum circuits or pulse schedules.
diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst
index e4d661a008..6f917320aa 100644
--- a/docs/_templates/autosummary/class.rst
+++ b/docs/_templates/autosummary/class.rst
@@ -17,11 +17,9 @@
.. rubric:: Attributes
- .. autosummary::
- :toctree: ../stubs/
{% for item in all_attributes %}
{%- if not item.startswith('_') %}
- {{ name }}.{{ item }}
+ .. autoattribute:: {{ name }}.{{ item }}
{%- endif -%}
{%- endfor %}
{% endif %}
@@ -32,16 +30,14 @@
.. rubric:: Methods
- .. autosummary::
- :toctree: ../stubs/
{% for item in all_methods %}
{%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %}
- {{ name }}.{{ item }}
+ .. automethod:: {{ name }}.{{ item }}
{%- endif -%}
{%- endfor %}
{% for item in inherited_members %}
{%- if item in ['__call__', '__mul__', '__getitem__', '__len__'] %}
- {{ name }}.{{ item }}
+ .. automethod:: {{ name }}.{{ item }}
{%- endif -%}
{%- endfor %}
diff --git a/docs/cloud/cost.rst b/docs/cloud/cost.rst
index 6c43de223e..b313daa537 100644
--- a/docs/cloud/cost.rst
+++ b/docs/cloud/cost.rst
@@ -8,7 +8,7 @@ Time limits on programs
The maximum execution time for the Sampler primitive is 10000 seconds (2.78 hours). The maximum execution time for the Estimator primitive is 18000 seconds (5 hours).
-Additionally, the system limit on the job execution time is 3 hours for a job that is running on a simulator and 8 hours for a job running on a physical system.
+Additionally, the system limit on the system execution time is 3 hours for a job that is running on a simulator and 8 hours for a job running on a physical system.
How to limit your cost
***********************
diff --git a/docs/conf.py b/docs/conf.py
index 653be22c24..9c89d3c49e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,7 +42,7 @@
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
-release = '0.12.1'
+release = '0.13.1'
docs_url_prefix = "ecosystem/ibm-runtime"
@@ -146,18 +146,20 @@
# -- Options for HTML output -------------------------------------------------
-html_theme = 'qiskit_sphinx_theme'
+html_theme = "qiskit-ecosystem"
+html_title = f"{project} {release}"
-html_logo = 'images/logo.png'
-html_last_updated_fmt = '%Y/%m/%d'
+html_logo = "images/ibm-quantum-logo.png"
html_theme_options = {
- 'logo_only': True,
- 'display_version': True,
- 'prev_next_buttons_location': 'bottom',
- 'style_external_links': True,
+ # Because this is an IBM-focused project, we use a blue color scheme.
+ "light_css_variables": {
+ "color-brand-primary": "var(--qiskit-color-blue)",
+ },
}
+html_last_updated_fmt = '%Y/%m/%d'
+
html_sourcelink_suffix = ''
autoclass_content = 'both'
diff --git a/docs/faqs/max_execution_time.rst b/docs/faqs/max_execution_time.rst
index 6b2cab1123..32e7633727 100644
--- a/docs/faqs/max_execution_time.rst
+++ b/docs/faqs/max_execution_time.rst
@@ -13,12 +13,11 @@ a job exceeds this time limit, it is forcibly cancelled and a ``RuntimeJobMaxTim
exception is raised.
.. note::
- As of August 7, 2023, the ``max_execution_time`` value is based on quantum
- time instead of wall clock time. Quantum time represents the time that the QPU
+ As of August 7, 2023, the ``max_execution_time`` value is based on system execution time, which is the time that the QPU
complex (including control software, control electronics, QPU, and so on) is engaged in
- processing the job.
+ processing the job, instead of wall clock time.
- Simulator jobs continue to use wall clock time because they do not have quantum time.
+ Simulator jobs continue to use wall clock time.
You can set the maximum execution time (in seconds) on the job options by using one of the following methods:
@@ -32,12 +31,12 @@ You can set the maximum execution time (in seconds) on the job options by using
# Create the options object with attributes and values
options = {"max_execution_time": 360}
-You can also find quantum time used by previously completed jobs by using:
+You can also find the system execution time for previously completed jobs by using:
.. code-block:: python
- # Find quantum time used by the job
- print(f"Quantum time used by job {job.job_id()} was {job.metrics()['usage']['quantum_seconds']} seconds")
+ # Find the system execution time
+ print(f"Job {job.job_id()} system execution time was {job.metrics()['usage']['seconds']} seconds")
In addition, the system calculates an appropriate job timeout value based on the
input circuits and options. This system-calculated timeout is currently capped
@@ -48,42 +47,15 @@ For example, if you specify ``max_execution_time=5000``, but the system determin
it should not take more than 5 minutes (300 seconds) to execute the job, then the job will be
cancelled after 5 minutes.
-Session time limits
-***************************
-
-When a session is started, it is assigned a maximum session timeout value.
-After this timeout is reached, the session is terminated, any jobs that are already running continue running, and any queued jobs that remain in the session are put into a ``failed`` state.
-You can set the maximum session timeout value using the ``max_time`` parameter:
-
-.. code-block:: python
-
- # Set the session max time
- with Session(max_time="1h"):
- ...
-
-If you don't specify a session ``max_time``, the system defaults are used:
-
-+--------------+------------------+--------------+-----------+
-| Primitive programs | Private programs |
-+==============+==================+==============+===========+
-| Premium User | Open User | Premium User | Open User |
-+--------------+------------------+--------------+-----------+
-| 8h | 4h | 8h | N/A |
-+--------------+------------------+--------------+-----------+
-
-Note that a *premium user* here means a user who has access to backends in providers other than ``ibm-q/open/main``.
-
-.. note::
- Session ``max_time`` is based on wall clock time, not quantum time.
-
-
-Additionally, there is a 5 minute *interactive* timeout value. If there are no session jobs queued within that window, the session is temporarily deactivated and normal job selection resumes. During job selection, if the job scheduler gets a new job from the session and its maximum timeout value has not been reached, the session is reactivated until its maximum timeout value is reached.
+Session maximum execution time
+*******************************
-.. note:: The timer for the session's ``max_time`` is not paused during any temporary deactivation periods.
+When a session is started, it is assigned a maximum session timeout value. After this timeout is reached, the session is terminated, any jobs that are already running continue running, and any queued jobs that remain in the session are put into a failed state. For instructions to set the session maximum time, see `Specify the session length <../how_to/run_session#session_length.html>`__.
Other limitations
***************************
- Programs cannot exceed 750KB in size.
-- Inputs to jobs cannot exceed 64MB in size.
\ No newline at end of file
+- Inputs to jobs cannot exceed 64MB in size.
+- Open plan users can use up to 10 minutes of system execution time per month (resets at 00:00 UTC on the first of each month). System execution time is the amount of time that the system is dedicated to processing your job. You can track your monthly usage on the `Platform dashboard, `__ `Jobs, `__ and `Account `__ page.
\ No newline at end of file
diff --git a/docs/how_to/backends.rst b/docs/how_to/backends.rst
index c215f55355..3ec7d47b04 100644
--- a/docs/how_to/backends.rst
+++ b/docs/how_to/backends.rst
@@ -122,11 +122,9 @@ If you are using a runtime session, add the ``backend`` option when starting you
service = QiskitRuntimeService()
with Session(service=service, backend="ibmq_qasm_simulator") as session:
- estimator = Estimator(session=session, options=options)
- job = estimator.run(circuit, observable)
- result = job.result()
- # Close the session only if all jobs are finished, and you don't need to run more in the session
- session.close() # Closes the session
+ estimator = Estimator(session=session, options=options)
+ job = estimator.run(circuit, observable)
+ result = job.result()
display(circuit.draw("mpl"))
print(f" > Observable: {observable.paulis}")
diff --git a/docs/how_to/error-mitigation.rst b/docs/how_to/error-mitigation.rst
index edbcfdaa47..166916fc22 100644
--- a/docs/how_to/error-mitigation.rst
+++ b/docs/how_to/error-mitigation.rst
@@ -140,8 +140,6 @@ The Estimator interface lets users seamlessly work with the variety of error mit
estimator = Estimator(session=session, options=options)
job = estimator.run(circuits=[psi1], observables=[H1], parameter_values=[theta1])
psi1_H1 = job.result()
- # Close the session only if all jobs are finished, and you don't need to run more in the session
- session.close()
.. note::
As you increase the resilience level, you will be able to use additional methods to improve the accuracy of your result. However, because the methods become more advanced with each level, they require additional sampling overhead (time) to generate more accurate expectation values.
@@ -191,17 +189,8 @@ As a part of the beta release of the resilience options, users will be able conf
+---------------------------------------------------------------+----------------------------------+--------------------------------------------------------+
| Options | Inputs | Description |
+===============================================================+==================================+========================================================+
-| options.resilience.noise_amplifier(Optional[str]) | ``TwoQubitAmplifier`` [Default] | Amplifies noise of all two qubit gates by performing |
-| | | local gate folding. |
-| select your amplification strategy +----------------------------------+--------------------------------------------------------+
-| | ``CxAmplifier`` | Amplifies noise of all CNOT gates by performing local |
-| | | gate folding. |
-| +----------------------------------+--------------------------------------------------------+
-| | ``LocalFoldingAmplifier`` | Amplifies noise of all gates by performing local |
-| | | gate folding. |
-| +----------------------------------+--------------------------------------------------------+
-| | ``GlobalFoldingAmplifier`` | Amplifies noise of the input circuit by performing |
-| | | global folding of the entire input circuit. |
+| options.resilience.noise_amplifier(Optional[str]) | ``LocalFoldingAmplifier`` | Amplifies noise of all gates by performing local |
+| (currently only one available option) | | gate folding. |
+---------------------------------------------------------------+----------------------------------+--------------------------------------------------------+
| options.resilience.noise_factors((Optional[Sequence[float]]) | (1, 3, 5) [Default] | Noise amplification factors, where `1` represents the |
| | | baseline noise. They all need to be greater than or |
@@ -228,7 +217,7 @@ Example of adding ``resilience_options`` into your estimator session
options.optimization_level = 3
options.resilience_level = 2
options.resilience.noise_factors = (1, 2, 3, 4)
- options.resilience.noise_amplifier = 'CxAmplifier'
+ options.resilience.noise_amplifier = 'LocalFoldingAmplifier'
options.resilience.extrapolator = 'QuadraticExtrapolator'
@@ -236,6 +225,4 @@ Example of adding ``resilience_options`` into your estimator session
estimator = Estimator(session=session, options=options)
job = estimator.run(circuits=[psi1], observables=[H1], parameter_values=[theta1])
psi1_H1 = job.result()
- # Close the session only if all jobs are finished, and you don't need to run more in the session
- session.close()
diff --git a/docs/how_to/error-suppression.rst b/docs/how_to/error-suppression.rst
index db91e3aa00..04132cad11 100644
--- a/docs/how_to/error-suppression.rst
+++ b/docs/how_to/error-suppression.rst
@@ -66,8 +66,6 @@ Example: configure Estimator with optimization levels
estimator = Estimator(session=session, options=options)
job = estimator.run(circuits=[psi], observables=[H], parameter_values=[theta])
psi1_H1 = job.result()
- # Close the session only if all jobs are finished, and you don't need to run more in the session
- session.close()
.. note::
If optimization level is not specified, the service uses ``optimization_level = 3``.
diff --git a/docs/how_to/run_session.rst b/docs/how_to/run_session.rst
index 796a74ab8f..1339e983a8 100644
--- a/docs/how_to/run_session.rst
+++ b/docs/how_to/run_session.rst
@@ -1,4 +1,4 @@
-Run a primitive in a session
+Run jobs in a session
=================================
There are several ways to set up and use sessions. The following information should not be considered mandatory steps to follow. Instead, choose the configuration that best suits your needs. To learn more about sessions, see `Introduction to sessions <../sessions.html>`__. This information assumes that you are using Qiskit Runtime `primitives <../primitives.html>`__.
@@ -34,7 +34,7 @@ A session can be created by initializing the `Session` class, which can then be
**Context manager**
-The context manager automatically opens a session for you. A session is started when the first primitive job in this context manager starts (not when it is queued). Primitives created in the context automatically use that session. Example:
+The context manager automatically opens and closes a session for you. A session is started when the first primitive job in this context manager starts (not when it is queued). Primitives created in the context automatically use that session. Example:
.. code-block:: python
@@ -66,13 +66,14 @@ There are two ways to specify a backend in a session:
with Session(backend=backend):
...
+.. _session_length:
Specify the session length
--------------------------
When a session is started, it is assigned a maximum session timeout value. After the session has been open the specified amount of time, the session expires and is forcefully closed. You can no longer submit jobs to that session. See `What happens when a session ends <../sessions.html#ends>`__ for further details.
-You can configure the maximum session timeout value through the ``max_time`` parameter, which can be specified as seconds (int) or a string, like "2h 30m 40s". This value has to be greater than the ``max_execution_time`` of the job and less than the system’s ``max_time``. The default value is the system’s ``max_time``. See `What is the maximum execution time for a Qiskit Runtime job? <../faqs/max_execution_time.html>`__ to determine the system limit.
+You can configure the maximum session timeout value through the ``max_time`` parameter, which can be specified as seconds (int) or a string, like "2h 30m 40s". This value has to be greater than the ``max_execution_time`` of the job and less than the system’s ``max_time``. The default value is the system’s ``max_time``. See `Determine session details <#determine-session-details>`__ to determine the system limit.
When setting the session length, consider how long each job within the session might take. For example, if you run five jobs within a session and each job is estimated to be five minutes long, the maximum time for the session should at least 25 min.
@@ -81,27 +82,186 @@ When setting the session length, consider how long each job within the session m
with Session(service=service, backend=backend, max_time="25m"):
...
-There is also an interactive timeout value (5 minutes), which is not configurable. If no session jobs are queued within that window, the session is temporarily deactivated. For more details about session length and timeout, see `How long a session stays active <../sessions.html#active>`__.
+There is also an interactive timeout value (ITTL) that cannot be configured. If no session jobs are queued within that window, the session is temporarily deactivated. For more details about session length and timeout, see `How long a session stays active <../sessions.html#active>`__. To determine a session's ITTL, follow the instructions in `Determine session details <#determine-session-details>`__ and look for the ``interactive_timeout`` value.
-.. _close session:
+
+.. _close_session:
Close a session
---------------
-When jobs are all done, it is recommended that you use `session.close()` to close the session. This allows the scheduler to run the next job without waiting for the session timeout, therefore making it easier for everyone. You cannot submit jobs to a closed session.
+With `qiskit-ibm-runtime` 0.13 or later releases, when the session context manager is exited, the session is put into `In progress, not accepting new jobs` status. This means that the session will finish processing all running or queued jobs until the maximum timeout value is reached. After all jobs are completed, the session is immediately closed. This allows the
+scheduler to run the next job without waiting for the session interactive timeout,
+therefore reducing the average job queueing time. You cannot submit jobs to a
+closed session.
-.. warning::
- Close a session only after all session jobs **complete**, rather than immediately after they have all been submitted. Session jobs that are not completed will fail.
+This behavior exists in `qiskit-ibm-runtime` 0.13 or later releases only. Previously, `session.close()` **canceled** the session.
.. code-block:: python
- with Session(service=service, backend=backend) as session:
+ with Session(service=service, backend=backend):
estimator = Estimator()
job = estimator.run(...)
- # Do not close here, the job might not be completed!
- result = job.result()
- # job.result() is blocking, so this job is now finished and the session can be safely closed.
- session.close()
+
+ # The session is no longer accepting jobs but the submitted job will run to completion
+ result = job.result()
+
+.. _cancel_session:
+
+Cancel a session
+----------------
+
+If a session is canceled, the session is put into `Closed` status. Any jobs that are already running continue to run but queued jobs are put into a failed state and no further jobs can be submitted to the session. This is a convenient way to quickly fail all queued jobs within a session.
+
+### For Qiskit runtime releases 0.13 or later
+
+Use the `session.cancel()` method to cancel a session.
+
+.. code-block:: python
+
+ with Session(service=service, backend=backend) as session:
+ estimator = Estimator()
+ job1 = estimator.run(...)
+ job2 = estimator.run(...)
+ # You can use session.cancel() to fail all pending jobs, for example,
+ # if you realize you made a mistake.
+ session.cancel()
+
+For Qiskit Runtime releases 0.13 or later
++++++++++++++++++++++++++++++++++++++++++
+
+Use the `session.cancel()` method to cancel a session.
+
+.. code-block:: python
+
+ with Session(service=service, backend=backend) as session:
+ estimator = Estimator()
+ job1 = estimator.run(...)
+ job2 = estimator.run(...)
+ # You can use session.cancel() to fail all pending jobs, for example,
+ # if you realize you made a mistake.
+ session.cancel()
+
+For Qiskit Runtime releases before 0.13
++++++++++++++++++++++++++++++++++++++++++
+
+Use the `session.close()` method to cancel a session. This allows the
+scheduler to run the next job without waiting for the session timeout,
+therefore making it easier for everyone. You cannot submit jobs to a
+closed session.
+
+.. code-block:: python
+
+ with Session(service=service, backend=backend) as session:
+ estimator = Estimator()
+ job = estimator.run(...)
+ # Do not close here, the job might not be completed!
+ result = job.result()
+ # Reaching this line means that the job is finished.
+ # This close() method would fail all pending jobs.
+ session.close()
+
+Invoke multiple primitives in a session
+----------------------------------------
+You are not restricted to a single primitive function within a session. In this section we will show you an example of using multiple primitives.
+
+First we prepare a circuit for the Sampler primitive.
+
+.. code-block:: python
+
+ from qiskit.circuit.random import random_circuit
+
+ sampler_circuit = random_circuit(2, 2, seed=0).decompose(reps=1)
+ sampler_circuit.measure_all()
+ display(circuit.draw("mpl"))
+
+The following example shows how you can create both an instance of the `Sampler` class and one of the `Estimator` class and invoke their `run()` methods within a session.
+
+.. code-block:: python
+
+ from qiskit_ibm_runtime import Session, Sampler, Estimator
+
+ with Session(backend=backend):
+ sampler = Sampler()
+ estimator = Estimator()
+
+ result = sampler.run(sampler_circuit).result()
+ print(f">>> Quasi-probability distribution from the sampler job: {result.quasi_dists[0]}")
+
+ result = estimator.run(circuit, observable).result()
+ print(f">>> Expectation value from the estimator job: {result.values[0]}")
+
+The calls can also be synchronous. You don’t need to wait for the result of a previous job before submitting another one, as shown below:
+
+.. code-block:: python
+
+ from qiskit_ibm_runtime import Session, Sampler, Estimator
+
+ with Session(backend=backend):
+ sampler = Sampler()
+ estimator = Estimator()
+
+ sampler_job = sampler.run(sampler_circuit)
+ estimator_job = estimator.run(circuit, observable)
+
+ print(
+ f">>> Quasi-probability distribution from the sampler job: {sampler_job.result().quasi_dists[0]}"
+ )
+ print(f">>> Expectation value from the estimator job: {estimator_job.result().values[0]}")
+
+.. _session_status:
+
+Query session status
+---------------------
+
+
+You can query the status of a session using `session.status()`. You can also view a session's status on the Jobs page for your channel.
+
+Session status can be one of the following:
+
+- `Pending`: Session has not started or has been deactivated. The next session job needs to wait in the queue like other jobs.
+- `In progress, accepting new jobs`: Session is active and accepting new jobs.
+- `In progress, not accepting new jobs`: Session is active but not accepting new jobs. Job submission to the session will be rejected, but outstanding session jobs will run to completion. The session will be automatically closed once all jobs finish.
+- `Closed`: Session maximum timeout value has been reached, or session was explicitly closed.
+
+.. _session_details:
+
+Determine session details
+--------------------------
+
+You can find details about a session by using the `session.details()` method, from the `Quantum Platform Jobs page `__, or from the IBM Cloud Jobs page, which you access from your `Instances page `__. From the session details you can determine the `maximum <..sessions#max-ttl.html>`__ and `interactive <..sessions#ttl.html>`__ time to live (TTL) values, its status, whether it's currently accepting jobs, and more.
+
+Example:
+
+.. code-block:: python
+
+ from qiskit_ibm_runtime import QiskitRuntimeService
+
+ service = QiskitRuntimeService()
+
+ with Session(service=service, backend="ibmq_qasm_simulator") as session:
+ estimator = Estimator()
+ job = estimator.run(circuit, observable)
+ print(session.details())
+
+Output:
+
+.. code-block:: text
+
+ {
+ 'id': 'cki5d18m3kt305s4pndg',
+ 'backend_name': 'ibm_algiers',
+ 'interactive_timeout': 300, # This is the interactive timeout, in seconds
+ 'max_time': 28800, # This is the maximum session timeout, in seconds
+ 'active_timeout': 28800,
+ 'state': 'closed',
+ 'accepting_jobs': True,
+ 'last_job_started': '2023-10-09T19:37:42.004Z',
+ 'last_job_completed': '2023-10-09T19:38:10.064Z',
+ 'started_at': '2023-10-09T19:37:42.004Z',
+ 'closed_at': '2023-10-09T19:38:39.406Z'
+ }
+
Full example
------------
@@ -122,14 +282,12 @@ In this example, we start a session, run an Estimator job, and output the result
options.resilience_level = 2
service = QiskitRuntimeService()
- with Session(service=service, backend="ibmq_qasm_simulator") as session:
+ with Session(service=service, backend="ibmq_qasm_simulator"):
estimator = Estimator(options=options)
job = estimator.run(circuit, observable)
result = job.result()
- # Close the session only if all jobs are finished, and you don't need to run more in the session
- session.close()
display(circuit.draw("mpl"))
print(f" > Observable: {observable.paulis}")
print(f" > Expectation value: {result.values[0]}")
- print(f" > Metadata: {result.metadata[0]}")
+ print(f" > Metadata: {result.metadata[0]}")
\ No newline at end of file
diff --git a/docs/images/ibm-quantum-logo.png b/docs/images/ibm-quantum-logo.png
new file mode 100644
index 0000000000..252c31b2f7
Binary files /dev/null and b/docs/images/ibm-quantum-logo.png differ
diff --git a/docs/index.rst b/docs/index.rst
index 44748369fc..f1a57983e3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,5 +1,5 @@
#########################################
-Qiskit Runtime 0.12.0 documentation
+Qiskit Runtime 0.13.0 documentation
#########################################
Overview
diff --git a/docs/sessions.rst b/docs/sessions.rst
index bf4ed150d6..ba28143c8d 100644
--- a/docs/sessions.rst
+++ b/docs/sessions.rst
@@ -1,13 +1,11 @@
Introduction to sessions
=============================
-A session is a contract between the user and the Qiskit Runtime service that ensures that a collection of jobs can be grouped and jointly prioritized by the quantum computer’s job scheduler. This eliminates artificial delays caused by other users’ jobs running on the same quantum device during the session time.
+A session allows a collection of jobs to be grouped and jointly scheduled by the Qiskit Runtime service, facilitating iterative use of quantum computers without incurring queuing delays on each iteration. This eliminates artificial delays caused by other users’ jobs running on the same quantum device during the session.
.. image:: images/session-overview.png
:width: 400
-In simple terms, once your session is active, jobs submitted within the session are not interrupted by other users’ jobs.
-
Compared with jobs that use the `fair-share scheduler `__, sessions become particularly beneficial when running programs that require iterative calls between classical and quantum resources, where a large number of jobs are submitted sequentially. This is the case, for example, when training a variational algorithm such as VQE or QAOA, or in device characterization experiments.
Runtime sessions can be used in conjunction with Qiskit Runtime primitives. Primitive program interfaces vary based on the type of task that you want to run on the quantum computer and the corresponding data that you want returned as a result. After identifying the appropriate primitive for your program, you can use Qiskit to prepare inputs, such as circuits, observables (for Estimator), and customizable options to optimize your job. For more information, see the `Primitives `__ topic.
@@ -38,74 +36,107 @@ A quantum processor still executes one job at a time. Therefore, jobs that belon
.. note::
* Internal systems jobs such as calibration have priority over session jobs.
-Iterations and batching
---------------------------
-
-Sessions can be used for iterative or batch execution.
-
-Iterative
-+++++++++++++++++++++
+Maximum session timeout
+++++++++++++++++++++++++++++
-Any session job submitted within the five-minute interactive timeout, also known as Time to live (TTL), is processed immediately. This allows some time for variational algorithms, such as VQE, to perform classical post-processing.
+When a session is started, it is assigned a *maximum session timeout*
+value. You can set this value by using the ``max_time`` parameter, which
+can be greater than the program's ``max_execution_time``. For
+instructions, see `Run a primitive in a session `__.
-- The quantum device is locked to the session user unless the TTL is reached.
-- Post-processing could be done anywhere, such as a personal computer, cloud service, or an HPC environment.
+If you do not specify a timeout value, it is set to the system limit.
-.. image:: images/iterative.png
+To find the maximum session timeout value for a session, follow the instructions in `Determine session details `__.
-Batch
-+++++++++++++++++++++
-Ideal for running experiments closely together to avoid device drifts, that is, to maintain device characterization.
-
-- Suitable for batching many jobs together.
-- Jobs that fit within the maximum session time run back-to-back on hardware.
+.. _ttl:
-.. note::
- When batching, jobs are not guaranteed to run in the order they are submitted.
+Interactive timeout value
++++++++++++++++++++++++++++++
-.. image:: images/batch.png
+Every session has an *interactive timeout value* (ITTL, or interactive time to live). If there are no session jobs queued within the
+ITTL window, the session is temporarily deactivated and normal job
+selection resumes. A deactivated session can be resumed if it has not
+reached its maximum timeout value. The session is resumed when a
+subsequent session job starts. Once a session is deactivated, its next
+job waits in the queue like other jobs.
-.. _active:
+After a session is deactivated, the next job in the queue is selected to
+run. This newly selected job (which can belong to a different user) can
+run as a singleton, but it can also start a different session. In other
+words, a deactivated session does not block the creation of other
+sessions. Jobs from this new session would then take priority until it
+is deactivated or closed, at which point normal job selection resumes.
-How long a session stays active
---------------------------------
+To find the interactive timeout value for a session, follow the instructions in `Determine session details `__.
-The length of time a session is active is controlled by the *maximum session timeout* (``max_time``) value and the *interactive* timeout value (TTL). The ``max_time`` timer starts when the session becomes active. That is, when the first job runs, not when it is queued. It does not stop if a session becomes inactive. The TTL timer starts each time a session job finishes.
+.. _ends:
-Maximum session timeout
-++++++++++++++++++++++++++++
+What happens when a session ends
+-------------------------------------
-When a session is started, it is assigned a *maximum session timeout* value. You can set this value by using the ``max_time`` parameter, which can be greater than the program's ``max_execution_time``. For instructions, see `Run a primitive in a session `__.
+A session ends by reaching its maximum timeout value, when it is `closed `__, or when it is canceled by using the `session.cancel()` method. What happens to unfinished session jobs when the session ends depends on how it ended:
-If you do not specify a timeout value, it is the smaller of these values:
+.. note::
+ Previously, `session.close()` **canceled** the session. Starting with `qiskit-ibm-runtime` 0.13, `session.close()` **closes** the session. The `session.cancel()` method was added in `qiskit-ibm-runtime` 0.13.
+
+If the maximum timeout value was reached:
+ - Any jobs that are already running continue to run.
+ - Any queued jobs remaining in the session are put into a failed state.
+ - No further jobs can be submitted to the session.
+ - The session cannot be reopened.
+
+If the maximum timeout value has not been reached:
+
+- When using `qiskit-ibm-runtime` 0.13 or later releases:
+ - If a session is closed:
+ - Session status becomes "In progress, not accepting new jobs".
+ - New job submissions to the session are rejected.
+ - Queued or running jobs continue to run.
+ - The session cannot be reopened.
+ - If a session is canceled:
+ - Session status becomes "Closed."
+ - Running jobs continue to run.
+ - Queued jobs are put into a failed state.
+ - The session cannot be reopened.
+
+- When using Qiskit Runtime releases before 0.13:
+ - Any jobs that are already running continue to run.
+ - Any queued jobs remaining in the session are put into a failed state.
+ - No further jobs can be submitted to the session.
+ - The session cannot be reopened.
+
+Different ways of using sessions
+----------------------------------
- * The system limit
- * The ``max_execution_time`` defined by the program
+Sessions can be used for iterative or batch execution.
-See `What is the maximum execution time for a Qiskit Runtime job? `__ to determine the system limit and the ``max_execution_time`` for primitive programs.
+Iterative
++++++++++++++++++++++
-.. _ttl:
+Any session job submitted within the five-minute interactive timeout, also known as interactive time to live (ITTL), is processed immediately. This allows some time for variational algorithms, such as VQE, to perform classical post-processing.
-Interactive timeout value
-+++++++++++++++++++++++++++++
+- The quantum device is locked to the session user unless the TTL is reached.
+- Post-processing could be done anywhere, such as a personal computer, cloud service, or an HPC environment.
-Every session has an *interactive timeout value*, or time to live (TTL), of five minutes, which cannot be changed. If there are no session jobs queued within the TTL window, the session is temporarily deactivated and normal job selection resumes. A deactivated session can be resumed if it has not reached its maximum timeout value. The session is resumed when a subsequent session job starts. Once a session is deactivated, its next job waits in the queue like other jobs.
+.. image:: images/iterative.png
-After a session is deactivated, the next job in the queue is selected to run. This newly selected job (which can belong to a different user) can run as a singleton, but it can also start a different session. In other words, a deactivated session does not block the creation of other sessions. Jobs from this new session would then take priority until it is deactivated or closed, at which point normal job selection resumes.
+.. note::
+ There might be a limit imposed on the ITTL value depending on whether your hub is Premium, Open, and so on.
-.. _ends:
+Batch
++++++++++++++++++++++
-What happens when a session ends
--------------------------------------
+Ideal for running experiments closely together to avoid device drifts, that is, to maintain device characterization.
-A session ends by reaching its maximum timeout value or when it is manually closed by the user. Do not close a session until all jobs **complete**. See `Close a session `__ for details. After a session is closed, the following occurs:
+- Suitable for batching many jobs together.
+- Jobs that fit within the maximum session time run back-to-back on hardware.
-* Any queued jobs remaining in the session (whether they are queued or not) are put into a failed state.
-* No further jobs can be submitted to the session.
-* The session cannot be reopened.
+.. note::
+ When batching, jobs are not guaranteed to run in the order they are submitted.
+.. image:: images/batch.png
Sessions and reservations
-------------------------
@@ -114,6 +145,16 @@ IBM Quantum Premium users can access both reservations and sessions on specific
.. image:: images/jobs-failing.png
+Summary
+---------
+
+- Jobs within an active session take priority over other queued jobs.
+- A session becomes active when its first job starts running.
+- A session stays active until one of the following happens:
+ - Its maximum timeout value is reached. In this case all queued jobs are canceled, but running jobs will finish.
+ - Its interactive timeout value is reached. In this case the session is deactivated but can be resumed if another session job starts running.
+ - The session is closed or cancelled. This can be done using the corresponding methods or upon exiting a session context.
+- Sessions can be used for iterative or batch execution.
Next steps
------------
diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt
index 34a83616bb..c317a91891 100644
--- a/qiskit_ibm_runtime/VERSION.txt
+++ b/qiskit_ibm_runtime/VERSION.txt
@@ -1 +1 @@
-0.12.1
+0.13.1
diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py
index 98a187fe66..64a3c8eb6d 100644
--- a/qiskit_ibm_runtime/__init__.py
+++ b/qiskit_ibm_runtime/__init__.py
@@ -66,8 +66,6 @@
circuits=[psi], observables=[H1], parameter_values=[theta]
)
print(f"Estimator results: {job.result()}")
- # Close the session only if all jobs are finished and you don't need to run more in the session.
- session.close()
Backend data
------------
diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py
index b28fd52e78..2e5ed6bbb4 100644
--- a/qiskit_ibm_runtime/accounts/account.py
+++ b/qiskit_ibm_runtime/accounts/account.py
@@ -12,6 +12,7 @@
"""Account related classes and functions."""
+from abc import abstractmethod
import logging
from typing import Optional, Literal
from urllib.parse import urlparse
@@ -22,7 +23,6 @@
from .exceptions import InvalidAccountError, CloudResourceNameResolutionError
from ..api.auth import QuantumAuth, CloudAuth
-
from ..utils import resolve_crn
AccountType = Optional[Literal["cloud", "legacy"]]
@@ -34,13 +34,11 @@
class Account:
- """Class that represents an account."""
+ """Class that represents an account. This is an abstract class."""
def __init__(
self,
- channel: ChannelType,
token: str,
- url: Optional[str] = None,
instance: Optional[str] = None,
proxies: Optional[ProxyConfiguration] = None,
verify: Optional[bool] = True,
@@ -57,12 +55,9 @@ def __init__(
verify: Whether to verify server's TLS certificate.
channel_strategy: Error mitigation strategy.
"""
- resolved_url = url or (
- IBM_QUANTUM_API_URL if channel == "ibm_quantum" else IBM_CLOUD_API_URL
- )
- self.channel = channel
+ self.channel: str = None
+ self.url: str = None
self.token = token
- self.url = resolved_url
self.instance = instance
self.proxies = proxies
self.verify = verify
@@ -78,56 +73,65 @@ def to_saved_format(self) -> dict:
@classmethod
def from_saved_format(cls, data: dict) -> "Account":
"""Creates an account instance from data saved on disk."""
+ channel = data.get("channel")
proxies = data.get("proxies")
- return cls(
- channel=data.get("channel"),
- url=data.get("url"),
- token=data.get("token"),
- instance=data.get("instance"),
- proxies=ProxyConfiguration(**proxies) if proxies else None,
- verify=data.get("verify", True),
- channel_strategy=data.get("channel_strategy"),
+ proxies = ProxyConfiguration(**proxies) if proxies else None
+ url = data.get("url")
+ token = data.get("token")
+ instance = data.get("instance")
+ verify = data.get("verify", True)
+ channel_strategy = data.get("channel_strategy")
+ return cls.create_account(
+ channel=channel,
+ url=url,
+ token=token,
+ instance=instance,
+ proxies=proxies,
+ verify=verify,
+ channel_strategy=channel_strategy,
)
+ @classmethod
+ def create_account(
+ cls,
+ channel: str,
+ token: str,
+ url: Optional[str] = None,
+ instance: Optional[str] = None,
+ proxies: Optional[ProxyConfiguration] = None,
+ verify: Optional[bool] = True,
+ channel_strategy: Optional[str] = None,
+ ) -> "Account":
+ """Creates an account for a specific channel."""
+ if channel == "ibm_quantum":
+ return QuantumAccount(
+ url=url,
+ token=token,
+ instance=instance,
+ proxies=proxies,
+ verify=verify,
+ channel_strategy=channel_strategy,
+ )
+ elif channel == "ibm_cloud":
+ return CloudAccount(
+ url=url,
+ token=token,
+ instance=instance,
+ proxies=proxies,
+ verify=verify,
+ channel_strategy=channel_strategy,
+ )
+ else:
+ raise InvalidAccountError(
+ f"Invalid `channel` value. Expected one of "
+ f"{['ibm_cloud', 'ibm_quantum']}, got '{channel}'."
+ )
+
def resolve_crn(self) -> None:
"""Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service
instance name and updates the ``instance`` attribute accordingly.
-
- No-op if ``channel`` attribute is set to ``ibm_quantum``.
- No-op if ``instance`` attribute is set to a Cloud Resource Name (CRN).
-
- Raises:
- CloudResourceNameResolutionError: if CRN value cannot be resolved.
- """
- if self.channel == "ibm_cloud":
- crn = resolve_crn(
- channel=self.channel,
- url=self.url,
- token=self.token,
- instance=self.instance,
- )
- if len(crn) == 0:
- raise CloudResourceNameResolutionError(
- f"Failed to resolve CRN value for the provided service name {self.instance}."
- )
- if len(crn) > 1:
- # handle edge-case where multiple service instances with the same name exist
- logger.warning(
- "Multiple CRN values found for service name %s: %s. Using %s.",
- self.instance,
- crn,
- crn[0],
- )
-
- # overwrite with CRN value
- self.instance = crn[0]
-
- def get_auth_handler(self) -> AuthBase:
- """Returns the respective authentication handler."""
- if self.channel == "ibm_cloud":
- return CloudAuth(api_key=self.token, crn=self.instance)
-
- return QuantumAuth(access_token=self.token)
+ Relevant for "ibm_cloud" channel only."""
+ pass
def __eq__(self, other: object) -> bool:
if not isinstance(other, Account):
@@ -156,7 +160,7 @@ def validate(self) -> "Account":
self._assert_valid_channel(self.channel)
self._assert_valid_token(self.token)
self._assert_valid_url(self.url)
- self._assert_valid_instance(self.channel, self.instance)
+ self._assert_valid_instance(self.instance)
self._assert_valid_proxies(self.proxies)
self._assert_valid_channel_strategy(self.channel_strategy)
return self
@@ -178,7 +182,7 @@ def _assert_valid_channel(channel: ChannelType) -> None:
if not (channel in ["ibm_cloud", "ibm_quantum"]):
raise InvalidAccountError(
f"Invalid `channel` value. Expected one of "
- f"{['ibm_cloud', 'ibm_quantum']}, got '{channel}'."
+ f"['ibm_cloud', 'ibm_quantum'], got '{channel}'."
)
@staticmethod
@@ -204,18 +208,121 @@ def _assert_valid_proxies(config: ProxyConfiguration) -> None:
config.validate()
@staticmethod
- def _assert_valid_instance(channel: ChannelType, instance: str) -> None:
+ @abstractmethod
+ def _assert_valid_instance(instance: str) -> None:
+ """Assert that the instance name is valid for the given account type."""
+ pass
+
+
+class QuantumAccount(Account):
+ """Class that represents an account with channel 'ibm_quantum.'"""
+
+ def __init__(
+ self,
+ token: str,
+ url: Optional[str] = None,
+ instance: Optional[str] = None,
+ proxies: Optional[ProxyConfiguration] = None,
+ verify: Optional[bool] = True,
+ channel_strategy: Optional[str] = None,
+ ):
+ """Account constructor.
+
+ Args:
+ token: Account token to use.
+ url: Authentication URL.
+ instance: Service instance to use.
+ proxies: Proxy configuration.
+ verify: Whether to verify server's TLS certificate.
+ channel_strategy: Error mitigation strategy.
+ """
+ super().__init__(token, instance, proxies, verify, channel_strategy)
+ resolved_url = url or IBM_QUANTUM_API_URL
+ self.channel = "ibm_quantum"
+ self.url = resolved_url
+
+ def get_auth_handler(self) -> AuthBase:
+ """Returns the Quantum authentication handler."""
+ return QuantumAuth(access_token=self.token)
+
+ @staticmethod
+ def _assert_valid_instance(instance: str) -> None:
"""Assert that the instance name is valid for the given account type."""
- if channel == "ibm_cloud":
- if not (isinstance(instance, str) and len(instance) > 0):
+ if instance is not None:
+ try:
+ from_instance_format(instance)
+ except:
raise InvalidAccountError(
- f"Invalid `instance` value. Expected a non-empty string, got '{instance}'."
+ f"Invalid `instance` value. Expected hub/group/project format, got {instance}"
)
- if channel == "ibm_quantum":
- if instance is not None:
- try:
- from_instance_format(instance)
- except:
- raise InvalidAccountError(
- f"Invalid `instance` value. Expected hub/group/project format, got {instance}"
- )
+
+
+class CloudAccount(Account):
+ """Class that represents an account with channel 'ibm_cloud'."""
+
+ def __init__(
+ self,
+ token: str,
+ url: Optional[str] = None,
+ instance: Optional[str] = None,
+ proxies: Optional[ProxyConfiguration] = None,
+ verify: Optional[bool] = True,
+ channel_strategy: Optional[str] = None,
+ ):
+ """Account constructor.
+
+ Args:
+ token: Account token to use.
+ url: Authentication URL.
+ instance: Service instance to use.
+ proxies: Proxy configuration.
+ verify: Whether to verify server's TLS certificate.
+ channel_strategy: Error mitigation strategy.
+ """
+ super().__init__(token, instance, proxies, verify, channel_strategy)
+ resolved_url = url or IBM_CLOUD_API_URL
+ self.channel = "ibm_cloud"
+ self.url = resolved_url
+
+ def get_auth_handler(self) -> AuthBase:
+ """Returns the Cloud authentication handler."""
+ return CloudAuth(api_key=self.token, crn=self.instance)
+
+ def resolve_crn(self) -> None:
+ """Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service
+ instance name and updates the ``instance`` attribute accordingly.
+
+ No-op if ``instance`` attribute is set to a Cloud Resource Name (CRN).
+
+ Raises:
+ CloudResourceNameResolutionError: if CRN value cannot be resolved.
+ """
+ crn = resolve_crn(
+ channel="ibm_cloud",
+ url=self.url,
+ token=self.token,
+ instance=self.instance,
+ )
+ if len(crn) == 0:
+ raise CloudResourceNameResolutionError(
+ f"Failed to resolve CRN value for the provided service name {self.instance}."
+ )
+ if len(crn) > 1:
+ # handle edge-case where multiple service instances with the same name exist
+ logger.warning(
+ "Multiple CRN values found for service name %s: %s. Using %s.",
+ self.instance,
+ crn,
+ crn[0],
+ )
+
+ # overwrite with CRN value
+ self.instance = crn[0]
+
+ @staticmethod
+ def _assert_valid_instance(instance: str) -> None:
+ """Assert that the instance name is valid for the given account type."""
+ if not (isinstance(instance, str) and len(instance) > 0):
+ raise InvalidAccountError(
+ f"Invalid `instance` value. Expected a non-empty string, got '{instance}'."
+ )
diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py
index 6e80c9e03e..34eab1dd81 100644
--- a/qiskit_ibm_runtime/accounts/management.py
+++ b/qiskit_ibm_runtime/accounts/management.py
@@ -26,8 +26,6 @@
)
_QISKITRC_CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".qiskit", "qiskitrc")
_DEFAULT_ACCOUNT_NAME = "default"
-_DEFAULT_ACCOUNT_NAME_LEGACY = "default-legacy"
-_DEFAULT_ACCOUNT_NAME_CLOUD = "default-cloud"
_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM = "default-ibm-quantum"
_DEFAULT_ACCOUNT_NAME_IBM_CLOUD = "default-ibm-cloud"
_DEFAULT_CHANNEL_TYPE: ChannelType = "ibm_cloud"
@@ -50,28 +48,30 @@ def save(
verify: Optional[bool] = None,
overwrite: Optional[bool] = False,
channel_strategy: Optional[str] = None,
+ set_as_default: Optional[bool] = None,
) -> None:
"""Save account on disk."""
- cls.migrate(filename=filename)
channel = channel or os.getenv("QISKIT_IBM_CHANNEL") or _DEFAULT_CHANNEL_TYPE
name = name or cls._get_default_account_name(channel)
filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE
filename = os.path.expanduser(filename)
+ config = Account.create_account(
+ channel=channel,
+ token=token,
+ url=url,
+ instance=instance,
+ proxies=proxies,
+ verify=verify,
+ channel_strategy=channel_strategy,
+ )
return save_config(
filename=filename,
name=name,
overwrite=overwrite,
- config=Account(
- token=token,
- url=url,
- instance=instance,
- channel=channel,
- proxies=proxies,
- verify=verify,
- channel_strategy=channel_strategy,
- )
+ config=config
# avoid storing invalid accounts
.validate().to_saved_format(),
+ set_as_default=set_as_default,
)
@staticmethod
@@ -84,7 +84,6 @@ def list(
"""List all accounts in a given filename, or in the default account file."""
filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE
filename = os.path.expanduser(filename)
- AccountManager.migrate(filename)
def _matching_name(account_name: str) -> bool:
return name is None or name == account_name
@@ -139,8 +138,23 @@ def get(
Args:
filename: Full path of the file from which to get the account.
- name: Account name. Takes precedence if `auth` is also specified.
+ name: Account name.
channel: Channel type.
+ Order of precedence for selecting the account:
+ 1. If name is specified, get account with that name
+ 2. If the environment variables define an account, get that one
+ 3. If the channel parameter is defined,
+ a. get the account of this channel type defined as "is_default_account"
+ b. get the account of this channel type with default name
+ c. get any account of this channel type
+ 4. If the channel is defined in "QISKIT_IBM_CHANNEL"
+ a. get the account of this channel type defined as "is_default_account"
+ b. get the account of this channel type with default name
+ c. get any account of this channel type
+ 5. If a default account is defined in the json file, get that account
+ 6. Get any account that is defined in the json file with
+ preference for _DEFAULT_CHANNEL_TYPE.
+
Returns:
Account information.
@@ -150,7 +164,6 @@ def get(
"""
filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE
filename = os.path.expanduser(filename)
- cls.migrate(filename)
if name:
saved_account = read_config(filename=filename, name=name)
if not saved_account:
@@ -162,18 +175,20 @@ def get(
if env_account is not None:
return env_account
- if channel:
- saved_account = read_config(
- filename=filename,
- name=cls._get_default_account_name(channel=channel),
- )
- if saved_account is None:
- if os.path.isfile(_QISKITRC_CONFIG_FILE):
- return cls._from_qiskitrc_file()
- raise AccountNotFoundError(f"No default {channel} account saved.")
+ all_config = read_config(filename=filename)
+ # Get the default account for the given channel.
+ # If channel == None, get the default account, for any channel, if it exists
+ saved_account = cls._get_default_account(all_config, channel)
+
+ if saved_account is not None:
return Account.from_saved_format(saved_account)
- all_config = read_config(filename=filename)
+ # Get the default account from the channel defined in the environment variable
+ account = cls._get_default_account(all_config, channel=channel_)
+ if account is not None:
+ return Account.from_saved_format(account)
+
+ # check for any account
for channel_type in _CHANNEL_TYPES:
account_name = cls._get_default_account_name(channel=channel_type)
if account_name in all_config:
@@ -194,54 +209,12 @@ def delete(
"""Delete account from disk."""
filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE
filename = os.path.expanduser(filename)
- cls.migrate(filename=filename)
name = name or cls._get_default_account_name(channel)
return delete_config(
filename=filename,
name=name,
)
- @classmethod
- def migrate(cls, filename: Optional[str] = None) -> None:
- """Migrate accounts on disk by removing `auth` and adding `channel`."""
- filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE
- filename = os.path.expanduser(filename)
- data = read_config(filename=filename)
- for key, value in data.items():
- if key == _DEFAULT_ACCOUNT_NAME_CLOUD:
- value.pop("auth", None)
- value.update(channel="ibm_cloud")
- delete_config(filename=filename, name=key)
- save_config(
- filename=filename,
- name=_DEFAULT_ACCOUNT_NAME_IBM_CLOUD,
- config=value,
- overwrite=False,
- )
- elif key == _DEFAULT_ACCOUNT_NAME_LEGACY:
- value.pop("auth", None)
- value.update(channel="ibm_quantum")
- delete_config(filename=filename, name=key)
- save_config(
- filename=filename,
- name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM,
- config=value,
- overwrite=False,
- )
- else:
- if isinstance(value, dict) and "auth" in value:
- if value["auth"] == "cloud":
- value.update(channel="ibm_cloud")
- elif value["auth"] == "legacy":
- value.update(channel="ibm_quantum")
- value.pop("auth", None)
- save_config(
- filename=filename,
- name=key,
- config=value,
- overwrite=True,
- )
-
@classmethod
def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account]:
"""Read account from environment variable."""
@@ -249,13 +222,41 @@ def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account
url = os.getenv("QISKIT_IBM_URL")
if not (token and url):
return None
- return Account(
+ return Account.create_account(
token=token,
url=url,
instance=os.getenv("QISKIT_IBM_INSTANCE"),
channel=channel,
)
+ @classmethod
+ def _get_default_account(
+ cls, all_config: dict, channel: Optional[str] = None
+ ) -> Optional[dict]:
+ default_channel_account = None
+ any_channel_account = None
+
+ for account_name in all_config:
+ account = all_config[account_name]
+ if channel:
+ if account.get("channel") == channel and account.get("is_default_account"):
+ return account
+ if account.get(
+ "channel"
+ ) == channel and account_name == cls._get_default_account_name(channel):
+ default_channel_account = account
+ if account.get("channel") == channel:
+ any_channel_account = account
+ else:
+ if account.get("is_default_account"):
+ return account
+
+ if default_channel_account:
+ return default_channel_account
+ elif any_channel_account:
+ return any_channel_account
+ return None
+
@classmethod
def _get_default_account_name(cls, channel: ChannelType) -> str:
return (
@@ -277,7 +278,7 @@ def _from_qiskitrc_file(cls) -> Optional[Account]:
filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE,
name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM,
overwrite=False,
- config=Account(
+ config=Account.create_account(
token=qiskitrc_data.get("token", None),
url=qiskitrc_data.get("url", None),
instance=qiskitrc_data.get("default_provider", None),
diff --git a/qiskit_ibm_runtime/accounts/storage.py b/qiskit_ibm_runtime/accounts/storage.py
index db463de27d..2564329978 100644
--- a/qiskit_ibm_runtime/accounts/storage.py
+++ b/qiskit_ibm_runtime/accounts/storage.py
@@ -22,7 +22,9 @@
logger = logging.getLogger(__name__)
-def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None:
+def save_config(
+ filename: str, name: str, config: dict, overwrite: bool, set_as_default: Optional[bool] = None
+) -> None:
"""Save configuration data in a JSON file under the given name."""
logger.debug("Save configuration data for '%s' in '%s'", name, filename)
_ensure_file_exists(filename)
@@ -35,8 +37,24 @@ def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None
f"Named account ({name}) already exists. " f"Set overwrite=True to overwrite."
)
+ data[name] = config
+
+ # if set_as_default, but another account is defined as default, user must specify overwrite to change
+ # the default account.
+ if set_as_default:
+ data[name]["is_default_account"] = True
+ for account_name in data:
+ account = data[account_name]
+ if account_name != name and account.get("is_default_account"):
+ if overwrite:
+ del account["is_default_account"]
+ else:
+ raise AccountAlreadyExistsError(
+ f"default_account ({name}) already exists. "
+ f"Set overwrite=True to overwrite."
+ )
+
with open(filename, mode="w", encoding="utf-8") as json_out:
- data[name] = config
json.dump(data, json_out, sort_keys=True, indent=4)
diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py
index 0176cd8494..9222248e72 100644
--- a/qiskit_ibm_runtime/api/clients/runtime.py
+++ b/qiskit_ibm_runtime/api/clients/runtime.py
@@ -337,21 +337,42 @@ def job_metadata(self, job_id: str) -> Dict[str, Any]:
"""
return self._api.program_job(job_id).metadata()
- def close_session(self, session_id: str) -> None:
- """Close the runtime session.
+ def cancel_session(self, session_id: str) -> None:
+ """Close all jobs in the runtime session.
Args:
session_id: Session ID.
"""
+ self._api.runtime_session(session_id=session_id).cancel()
+
+ def close_session(self, session_id: str) -> None:
+ """Update session so jobs can no longer be submitted."""
self._api.runtime_session(session_id=session_id).close()
- def list_backends(self, hgp: Optional[str] = None) -> List[str]:
+ def session_details(self, session_id: str) -> Dict[str, Any]:
+ """Get session details.
+
+ Args:
+ session_id: Session ID.
+
+ Returns:
+ Session details.
+ """
+ return self._api.runtime_session(session_id=session_id).details()
+
+ def list_backends(
+ self, hgp: Optional[str] = None, channel_strategy: Optional[str] = None
+ ) -> List[str]:
"""Return IBM backends available for this service instance.
+ Args:
+ hgp: Filter by hub/group/project.
+ channel_strategy: Filter by channel strategy.
+
Returns:
IBM backends available for this service instance.
"""
- return self._api.backends(hgp=hgp)["devices"]
+ return self._api.backends(hgp=hgp, channel_strategy=channel_strategy)["devices"]
def backend_configuration(self, backend_name: str) -> Dict[str, Any]:
"""Return the configuration of the IBM backend.
@@ -390,9 +411,7 @@ def backend_properties(
Raises:
NotImplementedError: If `datetime` is specified.
"""
- if datetime:
- raise NotImplementedError("'datetime' is not supported with cloud runtime.")
- return self._api.backend(backend_name).properties()
+ return self._api.backend(backend_name).properties(datetime=datetime)
def backend_pulse_defaults(self, backend_name: str) -> Dict:
"""Return the pulse defaults of the IBM backend.
diff --git a/qiskit_ibm_runtime/api/rest/base.py b/qiskit_ibm_runtime/api/rest/base.py
new file mode 100644
index 0000000000..04e8ab0e60
--- /dev/null
+++ b/qiskit_ibm_runtime/api/rest/base.py
@@ -0,0 +1,57 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2021.
+#
+# 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.
+
+"""Base REST adapter."""
+
+from ..session import RetrySession
+
+
+class RestAdapterBase:
+ """Base class for REST adapters."""
+
+ URL_MAP = {} # type: ignore[var-annotated]
+ """Mapping between the internal name of an endpoint and the actual URL."""
+
+ _HEADER_JSON_CONTENT = {"Content-Type": "application/json"}
+
+ def __init__(self, session: RetrySession, prefix_url: str = "") -> None:
+ """RestAdapterBase constructor.
+
+ Args:
+ session: Session to be used in the adapter.
+ prefix_url: String to be prepend to all URLs.
+ """
+ self.session = session
+ self.prefix_url = prefix_url
+
+ def get_url(self, identifier: str) -> str:
+ """Return the resolved URL for the specified identifier.
+
+ Args:
+ identifier: Internal identifier of the endpoint.
+
+ Returns:
+ The resolved URL of the endpoint (relative to the session base URL).
+ """
+ return "{}{}".format(self.prefix_url, self.URL_MAP[identifier])
+
+ def get_prefixed_url(self, prefix: str, identifier: str) -> str:
+ """Return an adjusted URL for the specified identifier.
+
+ Args:
+ prefix: string to be prepended to the URL.
+ identifier: Internal identifier of the endpoint.
+
+ Returns:
+ The resolved facade URL of the endpoint.
+ """
+ return "{}{}{}".format(prefix, self.prefix_url, self.URL_MAP[identifier])
diff --git a/qiskit_ibm_runtime/api/rest/cloud_backend.py b/qiskit_ibm_runtime/api/rest/cloud_backend.py
index 237ceef067..d15f2fc471 100644
--- a/qiskit_ibm_runtime/api/rest/cloud_backend.py
+++ b/qiskit_ibm_runtime/api/rest/cloud_backend.py
@@ -12,7 +12,8 @@
"""IBM Cloud Backend REST adapter."""
-from typing import Dict, Any
+from typing import Dict, Any, Optional
+from datetime import datetime as python_datetime
from qiskit_ibm_provider.api.rest.base import RestAdapterBase
from ..session import RetrySession
@@ -48,14 +49,19 @@ def configuration(self) -> Dict[str, Any]:
url = self.get_url("configuration")
return self.session.get(url).json()
- def properties(self) -> Dict[str, Any]:
+ def properties(self, datetime: Optional[python_datetime] = None) -> Dict[str, Any]:
"""Return backend properties.
Returns:
JSON response of backend properties.
"""
url = self.get_url("properties")
- response = self.session.get(url).json()
+
+ params = {}
+ if datetime:
+ params["updated_before"] = datetime.isoformat()
+
+ response = self.session.get(url, params=params).json()
# Adjust name of the backend.
if response:
response["backend_name"] = self.backend_name
diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py
index 86c97bb695..df264c7a20 100644
--- a/qiskit_ibm_runtime/api/rest/runtime.py
+++ b/qiskit_ibm_runtime/api/rest/runtime.py
@@ -19,8 +19,8 @@
from qiskit_ibm_provider.api.rest.base import RestAdapterBase
from qiskit_ibm_provider.api.rest.program_job import ProgramJob
-from qiskit_ibm_provider.api.rest.runtime_session import RuntimeSession
from qiskit_ibm_provider.utils import local_to_utc
+from .runtime_session import RuntimeSession
from .program import Program
from ...utils import RuntimeEncoder
@@ -271,17 +271,25 @@ def backend(self, backend_name: str) -> CloudBackend:
return CloudBackend(self.session, backend_name)
def backends(
- self, hgp: Optional[str] = None, timeout: Optional[float] = None
+ self,
+ hgp: Optional[str] = None,
+ timeout: Optional[float] = None,
+ channel_strategy: Optional[str] = None,
) -> Dict[str, List[str]]:
"""Return a list of IBM backends.
Args:
+ hgp: The service instance to use, only for ``ibm_quantum`` channel, in h/g/p format.
timeout: Number of seconds to wait for the request.
+ channel_strategy: Error mitigation strategy.
Returns:
JSON response.
"""
url = self.get_url("backends")
+ params = {}
if hgp:
- return self.session.get(url, params={"provider": hgp}).json()
- return self.session.get(url, timeout=timeout).json()
+ params["provider"] = hgp
+ if channel_strategy:
+ params["channel_strategy"] = channel_strategy
+ return self.session.get(url, params=params, timeout=timeout).json()
diff --git a/qiskit_ibm_runtime/api/rest/runtime_session.py b/qiskit_ibm_runtime/api/rest/runtime_session.py
new file mode 100644
index 0000000000..fb4b3b9445
--- /dev/null
+++ b/qiskit_ibm_runtime/api/rest/runtime_session.py
@@ -0,0 +1,63 @@
+# 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.
+
+"""Runtime Session REST adapter."""
+
+from typing import Dict, Any
+from .base import RestAdapterBase
+from ..session import RetrySession
+from ..exceptions import RequestsApiError
+from ...exceptions import IBMRuntimeError
+
+
+class RuntimeSession(RestAdapterBase):
+ """Rest adapter for session related endpoints."""
+
+ URL_MAP = {
+ "self": "",
+ "close": "/close",
+ }
+
+ def __init__(self, session: RetrySession, session_id: str, url_prefix: str = "") -> None:
+ """Job constructor.
+
+ Args:
+ session: RetrySession to be used in the adapter.
+ session_id: Job ID of the first job in a runtime session.
+ url_prefix: Prefix to use in the URL.
+ """
+ super().__init__(session, "{}/sessions/{}".format(url_prefix, session_id))
+
+ def cancel(self) -> None:
+ """Cancel all jobs in the session."""
+ url = self.get_url("close")
+ self.session.delete(url)
+
+ def close(self) -> None:
+ """Set accepting_jobs flag to false, so no more jobs can be submitted."""
+ payload = {"accepting_jobs": False}
+ url = self.get_url("self")
+ try:
+ self.session.patch(url, json=payload)
+ except RequestsApiError as ex:
+ if ex.status_code == 404:
+ pass
+ else:
+ raise IBMRuntimeError(f"Error closing session: {ex}")
+
+ def details(self) -> Dict[str, Any]:
+ """Return the details of this session."""
+ try:
+ return self.session.get(self.get_url("self")).json()
+ # return None if API is not supported
+ except: # pylint: disable=bare-except
+ return None
diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py
index 50f08c0231..3adc658b48 100644
--- a/qiskit_ibm_runtime/base_primitive.py
+++ b/qiskit_ibm_runtime/base_primitive.py
@@ -27,7 +27,6 @@
from .session import get_cm_session
from .constants import DEFAULT_DECODERS
from .qiskit_runtime_service import QiskitRuntimeService
-from .utils.deprecation import issue_deprecation_msg
# pylint: disable=unused-import,cyclic-import
from .session import Session
@@ -69,7 +68,6 @@ def __init__(
# The base class, however, uses a `_run_options` which is an instance of
# qiskit.providers.Options. We largely ignore this _run_options because we use
# a nested dictionary to categorize options.
-
self._session: Optional[Session] = None
self._service: QiskitRuntimeService = None
self._backend: Optional[IBMBackend] = None
@@ -80,38 +78,8 @@ def __init__(
self._backend = self._service.backend(
name=self._session.backend(), instance=self._session._instance
)
- elif isinstance(session, IBMBackend):
- issue_deprecation_msg(
- msg="Passing a backend instance as the ``session`` parameter is deprecated",
- version="0.10.0",
- remedy="Please pass it as the ``backend`` parameter instead.",
- )
- self._service = session.service
- self._backend = session
- elif isinstance(session, str):
- issue_deprecation_msg(
- msg="Passing a backend name as the ``session`` parameter is deprecated",
- version="0.10.0",
- remedy="Please pass it as the ``backend`` parameter instead.",
- )
- self._service = (
- QiskitRuntimeService()
- if QiskitRuntimeService.global_service is None
- else QiskitRuntimeService.global_service
- )
- self._backend = self._service.backend(session)
- elif isinstance(backend, Session):
- issue_deprecation_msg(
- msg="``session`` is no longer the first parameter when initializing "
- "a Qiskit Runtime primitive",
- version="0.10.0",
- remedy="Please use ``session=session`` instead.",
- )
- self._session = backend
- self._service = self._session.service
- self._backend = self._service.backend(
- name=self._session.backend(), instance=self._session._instance
- )
+ elif session is not None:
+ raise ValueError("session must be of type Session or None")
elif isinstance(backend, IBMBackend):
self._service = backend.service
self._backend = backend
diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py
index a579c6d0e1..a22d1c9112 100644
--- a/qiskit_ibm_runtime/estimator.py
+++ b/qiskit_ibm_runtime/estimator.py
@@ -16,6 +16,7 @@
import os
from typing import Optional, Dict, Sequence, Any, Union, Mapping
import logging
+import typing
import numpy as np
from numpy.typing import ArrayLike
@@ -28,7 +29,6 @@
from qiskit.circuit import Parameter
from qiskit.primitives.base.base_primitive import _isreal
-# TODO import _circuit_key from terra once 0.23 is released
from .runtime_job import RuntimeJob
from .ibm_backend import IBMBackend
from .options import Options
@@ -39,6 +39,9 @@
# pylint: disable=unused-import,cyclic-import
from .session import Session
+if typing.TYPE_CHECKING:
+ from qiskit.opflow import PauliSumOp
+
logger = logging.getLogger(__name__)
@@ -103,9 +106,6 @@ class Estimator(BasePrimitive, BaseEstimator):
parameter_values=[theta1]*2
)
print(psi1_H23.result())
- # Close the session only if all jobs are finished
- # and you don't need to run more in the session
- session.close()
"""
_PROGRAM_ID = "estimator"
diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py
index 9137b43070..9b4b1ee1c4 100644
--- a/qiskit_ibm_runtime/exceptions.py
+++ b/qiskit_ibm_runtime/exceptions.py
@@ -13,6 +13,7 @@
"""Exceptions related to the IBM Runtime service."""
from qiskit.exceptions import QiskitError
+from qiskit.providers.exceptions import JobTimeoutError, JobError
class IBMError(QiskitError):
@@ -69,7 +70,7 @@ class RuntimeProgramNotFound(IBMRuntimeError):
pass
-class RuntimeJobFailureError(IBMRuntimeError):
+class RuntimeJobFailureError(JobError):
"""Error raised when a runtime job failed."""
pass
@@ -87,7 +88,7 @@ class RuntimeInvalidStateError(IBMRuntimeError):
pass
-class RuntimeJobTimeoutError(IBMRuntimeError):
+class RuntimeJobTimeoutError(JobTimeoutError):
"""Error raised when waiting for job times out."""
pass
diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py
index fcf3f25367..a3da57099d 100644
--- a/qiskit_ibm_runtime/ibm_backend.py
+++ b/qiskit_ibm_runtime/ibm_backend.py
@@ -228,7 +228,7 @@ def _get_properties(self, datetime: Optional[python_datetime] = None) -> None:
"""Gets backend properties and decodes it"""
if datetime:
datetime = local_to_utc(datetime)
- if not self._properties:
+ if datetime or not self._properties:
api_properties = self._api_client.backend_properties(self.name, datetime=datetime)
if api_properties:
backend_properties = properties_from_server_data(api_properties)
@@ -241,9 +241,9 @@ def _get_defaults(self) -> None:
if api_defaults:
self._defaults = defaults_from_server_data(api_defaults)
- def _convert_to_target(self) -> None:
+ def _convert_to_target(self, refresh: bool = False) -> None:
"""Converts backend configuration, properties and defaults to Target object"""
- if not self._target:
+ if refresh or not self._target:
self._target = convert_to_target(
configuration=self._configuration,
properties=self._properties,
@@ -256,9 +256,6 @@ def _default_options(cls) -> Options:
return Options(
shots=4000,
memory=False,
- qubit_lo_freq=None,
- meas_lo_freq=None,
- schedule_los=None,
meas_level=MeasLevel.CLASSIFIED,
meas_return=MeasReturnType.AVERAGE,
memory_slots=None,
@@ -330,7 +327,7 @@ def target_history(self, datetime: Optional[python_datetime] = None) -> Target:
"""
self._get_properties(datetime=datetime)
self._get_defaults()
- self._convert_to_target()
+ self._convert_to_target(refresh=True)
return self._target
def properties(
@@ -343,7 +340,7 @@ def properties(
properties of the backend.
The schema for backend properties can be found in
- `Qiskit/ibm-quantum-schemas
+ `Qiskit/ibm-quantum-schemas/backend_properties
`_.
Args:
@@ -374,8 +371,6 @@ def properties(
if datetime:
if not isinstance(datetime, python_datetime):
raise TypeError("'{}' is not of type 'datetime'.")
- if isinstance(self._api_client, RuntimeClient):
- raise NotImplementedError("'datetime' is not supported by cloud runtime.")
datetime = local_to_utc(datetime)
if datetime or refresh or self._properties is None:
api_properties = self._api_client.backend_properties(self.name, datetime=datetime)
@@ -415,7 +410,7 @@ def defaults(self, refresh: bool = False) -> Optional[PulseDefaults]:
"""Return the pulse defaults for the backend.
The schema for default pulse configuration can be found in
- `Qiskit/ibm-quantum-schemas
+ `Qiskit/ibm-quantum-schemas/default_pulse_configuration
`_.
Args:
@@ -443,7 +438,7 @@ def configuration(
as its name, number of qubits, basis gates, coupling map, quantum volume, etc.
The schema for backend configuration can be found in
- `Qiskit/ibm-quantum-schemas
+ `Qiskit/ibm-quantum-schemas/backend_configuration
`_.
Returns:
diff --git a/qiskit_ibm_runtime/options/environment_options.py b/qiskit_ibm_runtime/options/environment_options.py
index becd2cdb42..ebb183adf3 100644
--- a/qiskit_ibm_runtime/options/environment_options.py
+++ b/qiskit_ibm_runtime/options/environment_options.py
@@ -15,8 +15,6 @@
from typing import Optional, Callable, List, Literal, get_args
from dataclasses import dataclass, field
-from .utils import _flexible
-
LogLevelType = Literal[
"DEBUG",
"INFO",
@@ -26,7 +24,6 @@
]
-@_flexible
@dataclass
class EnvironmentOptions:
"""Options related to the execution environment.
diff --git a/qiskit_ibm_runtime/options/execution_options.py b/qiskit_ibm_runtime/options/execution_options.py
index 6dc063daac..aaf4fede65 100644
--- a/qiskit_ibm_runtime/options/execution_options.py
+++ b/qiskit_ibm_runtime/options/execution_options.py
@@ -16,7 +16,6 @@
from typing import Literal, get_args, Optional
from numbers import Integral
-from .utils import _flexible
ExecutionSupportedOptions = Literal[
"shots",
@@ -27,7 +26,6 @@
]
-@_flexible
@dataclass
class ExecutionOptions:
"""Execution options.
diff --git a/qiskit_ibm_runtime/options/options.py b/qiskit_ibm_runtime/options/options.py
index 75d4b428b5..0e29d05320 100644
--- a/qiskit_ibm_runtime/options/options.py
+++ b/qiskit_ibm_runtime/options/options.py
@@ -19,7 +19,7 @@
from qiskit.transpiler import CouplingMap
-from .utils import _flexible, Dict, _remove_dict_none_values
+from .utils import Dict, _to_obj, _remove_dict_none_values
from .environment_options import EnvironmentOptions
from .execution_options import ExecutionOptions
from .simulator_options import SimulatorOptions
@@ -31,7 +31,6 @@
DDSequenceType = Literal[None, "XX", "XpXm", "XY4"]
-@_flexible
@dataclass
class Options:
"""Options for the primitives.
@@ -64,12 +63,16 @@ class Options:
`_.
for more information about the error mitigation methods used at each level.
- max_execution_time: Maximum execution time in seconds. If
- a job exceeds this time limit, it is forcibly cancelled. If ``None``, the
- maximum execution time of the primitive is used.
- This value must be in between 300 seconds and the
- `system imposed maximum
- `_.
+ max_execution_time: Maximum execution time in seconds, which is based
+ on system execution time (not wall clock time). System execution time is
+ the amount of time that the system is dedicated to processing your job.
+ If a job exceeds this time limit, it is forcibly cancelled.
+ Simulator jobs continue to use wall clock time.
+
+ Refer to the
+ `Max execution time documentation
+ `_.
+ for more information.
dynamical_decoupling: Optional, specify a dynamical decoupling sequence to use.
Allowed values are ``"XX"``, ``"XpXm"``, ``"XY4"``.
@@ -99,7 +102,6 @@ class Options:
_MAX_OPTIMIZATION_LEVEL = 3
_MAX_RESILIENCE_LEVEL_ESTIMATOR = 3
_MAX_RESILIENCE_LEVEL_SAMPLER = 1
- _MIN_EXECUTION_TIME = 300
_MAX_EXECUTION_TIME = 8 * 60 * 60 # 8 hours for real device
optimization_level: Optional[int] = None
@@ -122,6 +124,14 @@ class Options:
"twirling": TwirlingOptions,
}
+ def __post_init__(self): # type: ignore
+ """Convert dictionary fields to object."""
+ obj_fields = getattr(self, "_obj_fields", {})
+ for key in list(obj_fields):
+ if hasattr(self, key):
+ orig_val = getattr(self, key)
+ setattr(self, key, _to_obj(obj_fields[key], orig_val))
+
@staticmethod
def _get_program_inputs(options: dict) -> dict:
"""Convert the input options to program compatible inputs.
@@ -230,20 +240,44 @@ def validate_options(options: dict) -> None:
ResilienceOptions.validate_resilience_options(options.get("resilience"))
TranspilationOptions.validate_transpilation_options(options.get("transpilation"))
execution_time = options.get("max_execution_time")
- if not execution_time is None:
- if (
- execution_time < Options._MIN_EXECUTION_TIME
- or execution_time > Options._MAX_EXECUTION_TIME
- ):
+ if execution_time is not None:
+ if execution_time > Options._MAX_EXECUTION_TIME:
raise ValueError(
- f"max_execution_time must be between "
- f"{Options._MIN_EXECUTION_TIME} and {Options._MAX_EXECUTION_TIME} seconds."
+ f"max_execution_time must be below " f"{Options._MAX_EXECUTION_TIME} seconds."
)
EnvironmentOptions.validate_environment_options(options.get("environment"))
ExecutionOptions.validate_execution_options(options.get("execution"))
SimulatorOptions.validate_simulator_options(options.get("simulator"))
+ @staticmethod
+ def _remove_none_values(options: dict) -> dict:
+ """Remove `None` values from the options dictionary."""
+ new_options = {}
+ for key, value in options.items():
+ if value is not None:
+ if isinstance(value, dict):
+ new_suboptions = {}
+ for subkey, subvalue in value.items():
+ if subvalue is not None:
+ new_suboptions[subkey] = subvalue
+ new_options[key] = new_suboptions
+ else:
+ new_options[key] = value
+
+ return new_options
+
+ @staticmethod
+ def _set_default_resilience_options(options: dict) -> dict:
+ """Set default resilience options for resilience level 2."""
+ if options["resilience_level"] == 2:
+ if not options["resilience"]["noise_factors"]:
+ options["resilience"]["noise_factors"] = (1, 3, 5)
+ if not options["resilience"]["extrapolator"]:
+ options["resilience"]["extrapolator"] = "LinearExtrapolator"
+
+ return options
+
@staticmethod
def _get_runtime_options(options: dict) -> dict:
"""Extract runtime options.
diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py
index 6ace12515d..1c66e66452 100644
--- a/qiskit_ibm_runtime/options/resilience_options.py
+++ b/qiskit_ibm_runtime/options/resilience_options.py
@@ -15,19 +15,13 @@
from typing import Sequence, Literal, get_args, Union
from dataclasses import dataclass
-from .utils import _flexible
-from ..utils.deprecation import issue_deprecation_msg, deprecate_arguments
-
ResilienceSupportedOptions = Literal[
"noise_amplifier",
"noise_factors",
"extrapolator",
]
NoiseAmplifierType = Literal[
- "TwoQubitAmplifier",
- "GlobalFoldingAmplifier",
"LocalFoldingAmplifier",
- "CxAmplifier",
]
ExtrapolatorType = Literal[
"LinearExtrapolator",
@@ -48,7 +42,6 @@
]
-@_flexible
@dataclass
class ResilienceOptions:
"""Resilience options.
@@ -59,10 +52,9 @@ class ResilienceOptions:
Only applicable for ``resilience_level=2``.
Default: (1, 3, 5) if resilience level is 2. Otherwise ``None``.
- noise_amplifier (DEPRECATED): A noise amplification strategy. One of ``"TwoQubitAmplifier"``,
- ``"GlobalFoldingAmplifier"``, ``"LocalFoldingAmplifier"``, ``"CxAmplifier"``.
- Only applicable for ``resilience_level=2``.
- Default: "TwoQubitAmplifier" if resilience level is 2. Otherwise ``None``.
+ noise_amplifier (DEPRECATED): A noise amplification strategy. Currently only
+ ``"LocalFoldingAmplifier"`` is supported Only applicable for ``resilience_level=2``.
+ Default: "LocalFoldingAmplifier".
extrapolator (DEPRECATED): An extrapolation strategy. One of ``"LinearExtrapolator"``,
``"QuadraticExtrapolator"``, ``"CubicExtrapolator"``, ``"QuarticExtrapolator"``.
@@ -139,42 +131,19 @@ def validate_resilience_options(resilience_options: dict) -> None:
ValueError: if extrapolator == "CubicExtrapolator" and number of noise_factors < 4.
TypeError: if an input value has an invalid type.
"""
- noise_amplifier = resilience_options.get("noise_amplifier")
- if noise_amplifier is not None:
- issue_deprecation_msg(
- msg="The 'noise_amplifier' resilience option is deprecated",
- version="0.12.0",
- period="1 month",
- remedy="After the deprecation period, only local folding amplification "
- "will be supported. "
- "Refer to https://github.com/qiskit-community/prototype-zne "
- "for global folding amplification in ZNE.",
- )
- if noise_amplifier not in get_args(NoiseAmplifierType):
- raise ValueError(
- f"Unsupported value {noise_amplifier} for noise_amplifier. "
- f"Supported values are {get_args(NoiseAmplifierType)}"
- )
-
- if resilience_options.get("noise_factors", None) is not None:
- deprecate_arguments(
- deprecated="noise_factors",
- version="0.13.0",
- remedy="Please use 'zne_noise_factors' instead.",
+ noise_amplifier = resilience_options.get("noise_amplifier") or "LocalFoldingAmplifier"
+ if noise_amplifier not in get_args(NoiseAmplifierType):
+ raise ValueError(
+ f"Unsupported value {noise_amplifier} for noise_amplifier. "
+ f"Supported values are {get_args(NoiseAmplifierType)}"
)
extrapolator = resilience_options.get("extrapolator")
- if extrapolator is not None:
- deprecate_arguments(
- deprecated="extrapolator",
- version="0.13.0",
- remedy="Please use 'zne_extrapolator' instead.",
+ if extrapolator and extrapolator not in get_args(ExtrapolatorType):
+ raise ValueError(
+ f"Unsupported value {extrapolator} for extrapolator. "
+ f"Supported values are {get_args(ExtrapolatorType)}"
)
- if extrapolator not in get_args(ExtrapolatorType):
- raise ValueError(
- f"Unsupported value {extrapolator} for extrapolator. "
- f"Supported values are {get_args(ExtrapolatorType)}"
- )
if (
extrapolator == "QuarticExtrapolator"
diff --git a/qiskit_ibm_runtime/options/simulator_options.py b/qiskit_ibm_runtime/options/simulator_options.py
index 85847282e6..5506e48458 100644
--- a/qiskit_ibm_runtime/options/simulator_options.py
+++ b/qiskit_ibm_runtime/options/simulator_options.py
@@ -19,10 +19,7 @@
from qiskit.exceptions import MissingOptionalLibraryError
from qiskit.providers import BackendV1, BackendV2
from qiskit.utils import optionals
-
-from qiskit.transpiler import CouplingMap
-
-from .utils import _flexible
+from qiskit.transpiler import CouplingMap # pylint: disable=unused-import
if TYPE_CHECKING:
import qiskit_aer
@@ -35,7 +32,6 @@
]
-@_flexible
@dataclass()
class SimulatorOptions:
"""Simulator options.
@@ -78,6 +74,9 @@ def set_backend(self, backend: Union[BackendV1, BackendV2]) -> None:
Args:
backend: backend to be set.
+
+ Raises:
+ MissingOptionalLibraryError if qiskit-aer is not found.
"""
if not optionals.HAS_AER:
raise MissingOptionalLibraryError(
diff --git a/qiskit_ibm_runtime/options/transpilation_options.py b/qiskit_ibm_runtime/options/transpilation_options.py
index 90200f8ea0..fb3e96ae6e 100644
--- a/qiskit_ibm_runtime/options/transpilation_options.py
+++ b/qiskit_ibm_runtime/options/transpilation_options.py
@@ -15,7 +15,6 @@
from typing import Optional, List, Union, Literal, get_args
from dataclasses import dataclass
-from .utils import _flexible
TranspilationSupportedOptions = Literal[
"skip_transpilation",
@@ -39,7 +38,6 @@
]
-@_flexible
@dataclass
class TranspilationOptions:
"""Transpilation options.
diff --git a/qiskit_ibm_runtime/options/twirling_options.py b/qiskit_ibm_runtime/options/twirling_options.py
index 13fbc9af4c..5d14507f20 100644
--- a/qiskit_ibm_runtime/options/twirling_options.py
+++ b/qiskit_ibm_runtime/options/twirling_options.py
@@ -15,8 +15,6 @@
from typing import Literal, get_args
from dataclasses import dataclass
-from .utils import _flexible
-
TwirlingStrategyType = Literal[
None,
@@ -27,7 +25,6 @@
]
-@_flexible
@dataclass
class TwirlingOptions:
"""Twirling options.
diff --git a/qiskit_ibm_runtime/options/utils.py b/qiskit_ibm_runtime/options/utils.py
index 58e8a98da4..0968c0ace4 100644
--- a/qiskit_ibm_runtime/options/utils.py
+++ b/qiskit_ibm_runtime/options/utils.py
@@ -13,7 +13,6 @@
"""Utility functions for options."""
from typing import Optional
-from dataclasses import fields, field, make_dataclass
from ..ibm_backend import IBMBackend
@@ -76,49 +75,6 @@ def _to_obj(cls_, data): # type: ignore
)
-def _post_init(self): # type: ignore
- """Convert dictionary fields to object."""
-
- obj_fields = getattr(self, "_obj_fields", {})
- for key in obj_fields.keys():
- if hasattr(self, key):
- orig_val = getattr(self, key)
- setattr(self, key, _to_obj(obj_fields[key], orig_val))
-
-
-def _flexible(cls): # type: ignore
- """Decorator used to allow a flexible dataclass.
-
- This is used to dynamically create a new dataclass with the
- arbitrary kwargs input converted to fields. It also converts
- input dictionary to objects based on the _obj_fields attribute.
- """
-
- def __new__(cls, *_, **kwargs): # type: ignore
- all_fields = []
- orig_field_names = set()
-
- for fld in fields(cls):
- all_fields.append((fld.name, fld.type, fld))
- orig_field_names.add(fld.name)
-
- for key, val in kwargs.items():
- if key not in orig_field_names:
- all_fields.append((key, type(val), field(default=None)))
-
- new_cls = make_dataclass(
- cls.__name__,
- all_fields,
- bases=(cls,),
- namespace={"__post_init__": _post_init},
- )
- obj = object.__new__(new_cls)
- return obj
-
- cls.__new__ = __new__
- return cls
-
-
class Dict:
"""Fake Dict type.
diff --git a/qiskit_ibm_runtime/program/program_backend.py b/qiskit_ibm_runtime/program/program_backend.py
index 33e1de425a..6225dfdce9 100644
--- a/qiskit_ibm_runtime/program/program_backend.py
+++ b/qiskit_ibm_runtime/program/program_backend.py
@@ -16,7 +16,6 @@
from typing import Union, List, Dict
from abc import abstractmethod, ABC
-from qiskit.pulse import Schedule
from qiskit.providers.backend import BackendV1 as Backend
from qiskit.providers.job import JobV1 as Job
from qiskit.circuit import QuantumCircuit
@@ -35,7 +34,7 @@ class ProgramBackend(Backend, ABC):
@abstractmethod
def run(
self,
- circuits: Union[QuantumCircuit, Schedule, List[Union[QuantumCircuit, Schedule]]],
+ circuits: Union[QuantumCircuit, List[QuantumCircuit]],
**run_config: Dict,
) -> Job:
"""Run on the backend.
@@ -47,8 +46,8 @@ def run(
Args:
circuits: An individual or a
- list of :class:`~qiskit.circuits.QuantumCircuit` or
- :class:`~qiskit.pulse.Schedule` objects to run on the backend.
+ list of :class:`~qiskit.circuits.QuantumCircuit`
+ to run on the backend.
**run_config: Extra arguments used to configure the run.
Returns:
diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py
index a1ea28103f..b6799de2a2 100644
--- a/qiskit_ibm_runtime/qiskit_runtime_service.py
+++ b/qiskit_ibm_runtime/qiskit_runtime_service.py
@@ -34,7 +34,7 @@
from qiskit_ibm_provider.utils.backend_decoder import configuration_from_server_data
from qiskit_ibm_runtime import ibm_backend
-from .accounts import AccountManager, Account, AccountType, ChannelType
+from .accounts import AccountManager, Account, ChannelType
from .api.clients import AuthClient, VersionClient
from .api.clients.runtime import RuntimeClient
from .api.exceptions import RequestsApiError
@@ -99,9 +99,6 @@ class QiskitRuntimeService(Provider):
circuits=[psi], observables=[H1], parameter_values=[theta]
)
print(f"Estimator results: {job.result()}")
- # Close the session only if all jobs are finished
- # and you don't need to run more in the session.
- session.close()
The example above uses the dedicated :class:`~qiskit_ibm_runtime.Sampler`
and :class:`~qiskit_ibm_runtime.Estimator` classes. You can also
@@ -139,6 +136,7 @@ def __init__(
- Account with the input `name`, if specified.
- Default account for the `channel` type, if `channel` is specified but `token` is not.
- Account defined by the input `channel` and `token`, if specified.
+ - Account defined by the `default_channel` if defined in filename
- Account defined by the environment variables, if defined.
- Default account for the ``ibm_cloud`` account, if one is available.
- Default account for the ``ibm_quantum`` account, if one is available.
@@ -218,6 +216,10 @@ def __init__(
for backend_name in hgp.backends:
if backend_name not in self._backends:
self._backends[backend_name] = None
+ self._current_instance = self._account.instance
+ if not self._current_instance:
+ self._current_instance = self._get_hgp().name
+ logger.info("Default instance: %s", self._current_instance)
QiskitRuntimeService.global_service = self
# TODO - it'd be nice to allow some kind of autocomplete, but `service.ibmq_foo`
@@ -230,7 +232,6 @@ def _discover_account(
url: Optional[str] = None,
instance: Optional[str] = None,
channel: Optional[ChannelType] = None,
- auth: Optional[AccountType] = None,
filename: Optional[str] = None,
name: Optional[str] = None,
proxies: Optional[ProxyConfiguration] = None,
@@ -250,29 +251,26 @@ def _discover_account(
)
if name:
if filename:
- if any([auth, channel, token, url]):
+ if any([channel, token, url]):
logger.warning(
- "Loading account from file %s with name %s. Any input 'auth', "
+ "Loading account from file %s with name %s. Any input "
"'channel', 'token' or 'url' are ignored.",
filename,
name,
)
else:
- if any([auth, channel, token, url]):
+ if any([channel, token, url]):
logger.warning(
- "Loading account with name %s. Any input 'auth', "
+ "Loading account with name %s. Any input "
"'channel', 'token' or 'url' are ignored.",
name,
)
account = AccountManager.get(filename=filename, name=name)
- elif auth or channel:
- if auth and auth not in ["legacy", "cloud"]:
- raise ValueError("'auth' can only be 'cloud' or 'legacy'")
+ elif channel:
if channel and channel not in ["ibm_cloud", "ibm_quantum"]:
raise ValueError("'channel' can only be 'ibm_cloud' or 'ibm_quantum'")
- channel = channel or self._get_channel_for_auth(auth=auth)
if token:
- account = Account(
+ account = Account.create_account(
channel=channel,
token=token,
url=url,
@@ -288,9 +286,10 @@ def _discover_account(
elif any([token, url]):
# Let's not infer based on these attributes as they may change in the future.
raise ValueError(
- "'channel' or 'auth' is required if 'token', or 'url' is specified but 'name' is not."
+ "'channel' is required if 'token', or 'url' is specified but 'name' is not."
)
+ # channel is not defined yet, get it from the AccountManager
if account is None:
account = AccountManager.get(filename=filename)
@@ -302,8 +301,7 @@ def _discover_account(
account.verify = verify
# resolve CRN if needed
- if account.channel == "ibm_cloud":
- self._resolve_crn(account)
+ self._resolve_crn(account)
# ensure account is valid, fail early if not
account.validate()
@@ -317,7 +315,7 @@ def _discover_cloud_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]:
A dict of the remote backend instances, keyed by backend name.
"""
ret = OrderedDict() # type: ignore[var-annotated]
- backends_list = self._api_client.list_backends()
+ backends_list = self._api_client.list_backends(channel_strategy=self._channel_strategy)
for backend_name in backends_list:
raw_config = self._api_client.backend_configuration(backend_name=backend_name)
config = configuration_from_server_data(
@@ -681,13 +679,6 @@ def delete_account(
"""
return AccountManager.delete(filename=filename, name=name, channel=channel)
- @staticmethod
- def _get_channel_for_auth(auth: str) -> str:
- """Returns channel type based on auth"""
- if auth == "legacy":
- return "ibm_quantum"
- return "ibm_cloud"
-
@staticmethod
def save_account(
token: Optional[str] = None,
@@ -700,6 +691,7 @@ def save_account(
verify: Optional[bool] = None,
overwrite: Optional[bool] = False,
channel_strategy: Optional[str] = None,
+ set_as_default: Optional[bool] = None,
) -> None:
"""Save the account to disk for future use.
@@ -720,6 +712,8 @@ def save_account(
verify: Verify the server's TLS certificate.
overwrite: ``True`` if the existing account is to be overwritten.
channel_strategy: Error mitigation strategy.
+ set_as_default: If ``True``, the account is saved in filename,
+ as the default account.
"""
AccountManager.save(
@@ -733,6 +727,7 @@ def save_account(
verify=verify,
overwrite=overwrite,
channel_strategy=channel_strategy,
+ set_as_default=set_as_default,
)
@staticmethod
@@ -976,15 +971,6 @@ def run(
RuntimeProgramNotFound: If the program cannot be found.
IBMRuntimeError: An error occurred running the program.
"""
- # TODO: Remove this after 3 months
- if program_id in ["hello-world", "vqe", "qaoa"]:
- raise IBMInputValueError(
- "The hello-world, vqe, and qaoa programs have been retired in the "
- "Qiskit Runtime service. Please visit https://qiskit.org/ecosystem/ibm-runtime "
- "for an introduction on Sessions and Primitives, and to access "
- "tutorials on how to execute VQE and QAOA using Qiskit Runtime Primitives."
- )
-
qrt_options: RuntimeOptions = options
if options is None:
qrt_options = RuntimeOptions()
@@ -1003,6 +989,9 @@ def run(
# Find the right hgp
hgp = self._get_hgp(instance=qrt_options.instance, backend_name=qrt_options.backend)
hgp_name = hgp.name
+ if hgp_name != self._current_instance:
+ self._current_instance = hgp_name
+ logger.info("Instance selected: %s", self._current_instance)
backend = self.backend(name=qrt_options.backend, instance=hgp_name)
status = backend.status()
if status.operational is True and status.status_msg != "active":
@@ -1027,6 +1016,12 @@ def run(
if self._channel_strategy == "default"
else self._channel_strategy,
)
+ if self._channel == "ibm_quantum":
+ messages = response.get("messages")
+ if messages:
+ warning_message = messages[0].get("data")
+ warnings.warn(warning_message)
+
except RequestsApiError as ex:
if ex.status_code == 404:
raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None
@@ -1510,15 +1505,6 @@ def instances(self) -> List[str]:
return list(self._hgps.keys())
return []
- @property
- def auth(self) -> str:
- """Return the authentication type used.
-
- Returns:
- The authentication type used.
- """
- return "cloud" if self._channel == "ibm_cloud" else "legacy"
-
@property
def channel(self) -> str:
"""Return the channel type used.
diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py
index b6d9eeb8d0..2e359d5e2a 100644
--- a/qiskit_ibm_runtime/runtime_job.py
+++ b/qiskit_ibm_runtime/runtime_job.py
@@ -46,7 +46,6 @@
from .api.exceptions import RequestsApiError
from .api.client_parameters import ClientParameters
from .utils.utils import CallableStr
-from .utils.deprecation import issue_deprecation_msg
logger = logging.getLogger(__name__)
@@ -388,11 +387,6 @@ def metrics(self) -> Dict[str, Any]:
IBMRuntimeError: If a network error occurred.
"""
try:
- issue_deprecation_msg(
- msg="The 'bss.seconds' attribute is deprecated",
- version="0.11.1",
- remedy="Use the 'usage.seconds' attribute instead.",
- )
return self._api_client.job_metadata(self.job_id())
except RequestsApiError as err:
raise IBMRuntimeError(f"Failed to get job metadata: {err}") from None
@@ -458,12 +452,15 @@ def _set_status(self, job_response: Dict) -> None:
"""
try:
reason = job_response["state"].get("reason")
+ reason_code = job_response["state"].get("reason_code")
if reason:
# TODO remove this in https://github.com/Qiskit/qiskit-ibm-runtime/issues/989
if reason.upper() == "RAN TOO LONG":
self._reason = reason.upper()
else:
self._reason = reason
+ if reason_code:
+ self._reason = f"Error code {reason_code}; {self._reason}"
self._status = self._status_from_job_response(job_response)
except KeyError:
raise IBMError(f"Unknown status: {job_response['state']['status']}")
@@ -489,6 +486,7 @@ def _error_msg_from_job_response(self, response: Dict) -> str:
Error message.
"""
status = response["state"]["status"].upper()
+
job_result_raw = self._download_external_result(
self._api_client.job_results(job_id=self.job_id())
)
@@ -666,9 +664,9 @@ def usage_estimation(self) -> Dict[str, Any]:
"""Return the usage estimation infromation for this job.
Returns:
- ``quantum_seconds`` which is the estimated quantum time
+ ``quantum_seconds`` which is the estimated system execution time
of the job in seconds. Quantum time represents the time that
- the QPU complex is occupied exclusively by the job.
+ the system is dedicated to processing your job.
"""
if not self._usage_estimation:
response = self._api_client.job_get(job_id=self.job_id())
diff --git a/qiskit_ibm_runtime/runtime_options.py b/qiskit_ibm_runtime/runtime_options.py
index d795ec1439..eea530628a 100644
--- a/qiskit_ibm_runtime/runtime_options.py
+++ b/qiskit_ibm_runtime/runtime_options.py
@@ -58,8 +58,11 @@ def __init__(
access to the target backend is randomly selected.
job_tags: Tags to be assigned to the job. The tags can subsequently be used
as a filter in the :meth:`jobs()` function call.
- max_execution_time: Maximum execution time in seconds. If
- a job exceeds this time limit, it is forcibly cancelled.
+ max_execution_time: Maximum execution time in seconds, which is based
+ on system execution time (not wall clock time). System execution time is the
+ amount of time that the system is dedicated to processing your job. If a job exceeds
+ this time limit, it is forcibly cancelled. Simulator jobs continue to use wall
+ clock time.
session_time: Length of session in seconds.
"""
self.backend = backend
diff --git a/qiskit_ibm_runtime/runtime_session.py b/qiskit_ibm_runtime/runtime_session.py
deleted file mode 100644
index 737928bfd7..0000000000
--- a/qiskit_ibm_runtime/runtime_session.py
+++ /dev/null
@@ -1,146 +0,0 @@
-# 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.
-
-"""Qiskit runtime session."""
-
-from typing import Dict, Any, Optional, Type, Union
-from types import TracebackType
-import copy
-from functools import wraps
-from dataclasses import asdict
-
-from qiskit_ibm_runtime import qiskit_runtime_service # pylint: disable=unused-import
-from .runtime_job import RuntimeJob
-from .runtime_program import ParameterNamespace
-from .runtime_options import RuntimeOptions
-from .program.result_decoder import ResultDecoder
-
-
-def _active_session(func): # type: ignore
- """Decorator used to ensure the session is active."""
-
- @wraps(func)
- def _wrapper(self, *args, **kwargs): # type: ignore
- if not self._active:
- raise RuntimeError("The session is closed.")
- return func(self, *args, **kwargs)
-
- return _wrapper
-
-
-class RuntimeSession:
- """Runtime Session"""
-
- def __init__(
- self,
- service: "qiskit_runtime_service.QiskitRuntimeService",
- program_id: str,
- inputs: Union[Dict, ParameterNamespace],
- options: Optional[Union[RuntimeOptions, Dict]] = None,
- max_time: Optional[int] = None,
- ):
- """RuntimeSession constructor.
- Args:
- service: Runtime service.
- program_id: Program ID.
- inputs: Initial program inputs.
- options: Runtime options.
- max_time: (DEPRECATED) Maximum amount of time, a runtime session can
- be open before being forcibly closed.
- """
- self._service = service
- self._program_id = program_id
- self._options: dict = options
- if isinstance(options, RuntimeOptions):
- self._options = asdict(options)
- if max_time:
- self._options["session_time"] = max_time
- self._initial_inputs = inputs
- self._initial_job: Optional[RuntimeJob] = None
- self._job: Optional[RuntimeJob] = None
- self._session_id: Optional[str] = None
- self._active = True
- self._start_session = True
-
- @_active_session
- def write(self, **kwargs: Dict) -> None:
- """Write to the session."""
- if self._session_id is None:
- inputs = copy.copy(self._initial_inputs)
- else:
- inputs = {}
- inputs.update(kwargs)
- if self._session_id is None:
- self._start_session = True
- self._initial_job = self._run(inputs=inputs)
- self._job = self._initial_job
- self._session_id = self._job.job_id()
- else:
- self._start_session = False
- self._options["session_time"] = None
- self._job = self._run(inputs=inputs)
-
- def _run(self, inputs: Union[Dict, ParameterNamespace]) -> RuntimeJob:
- """Run a program"""
- return self._service.run(
- program_id=self._program_id,
- options=self._options,
- inputs=inputs,
- session_id=self._session_id,
- start_session=self._start_session,
- )
-
- @_active_session
- def read(
- self,
- timeout: Optional[float] = None,
- decoder: Optional[Type[ResultDecoder]] = None,
- ) -> Any:
- """Read from the session.
- Args:
- timeout: Number of seconds to wait for job.
- decoder: A :class:`ResultDecoder` subclass used to decode job results.
- Returns:
- Data returned from the session.
- """
- return self._job.result(timeout=timeout, decoder=decoder)
-
- def info(self) -> Dict:
- """Return session information.
- Returns:
- Session information.
- """
- out = {}
- if self._options:
- out["backend"] = self._options["backend"] or "unknown" # type: ignore
- if self._job:
- out["job_id"] = self._job.job_id()
- out["job_status"] = self._job.status()
- out["backend"] = self._job.backend()
- return out
-
- def close(self) -> None:
- """Close the session."""
- self._active = False
- if self._session_id:
- self._service._api_client.close_session(self._session_id)
-
- def __enter__(self) -> "RuntimeSession":
- return self
-
- def __exit__(
- self,
- exc_type: Optional[Type[BaseException]],
- exc_val: Optional[BaseException],
- exc_tb: Optional[TracebackType],
- ) -> None:
- self.close()
diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py
index 81a69ca5ad..0b19bcc5c9 100644
--- a/qiskit_ibm_runtime/sampler.py
+++ b/qiskit_ibm_runtime/sampler.py
@@ -60,10 +60,6 @@ class Sampler(BasePrimitive, BaseSampler):
print(f"Job result: {job.result()}")
# You can run more jobs inside the session
-
- # Close the session only if all jobs are finished
- # and you don't need to run more in the session.
- session.close()
"""
def __init__(
diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py
index 60054d61d8..36f7a0f628 100644
--- a/qiskit_ibm_runtime/session.py
+++ b/qiskit_ibm_runtime/session.py
@@ -12,7 +12,7 @@
"""Qiskit Runtime flexible session."""
-from typing import Dict, Optional, Type, Union, Callable
+from typing import Dict, Optional, Type, Union, Callable, Any
from types import TracebackType
from functools import wraps
from contextvars import ContextVar
@@ -24,8 +24,6 @@
from .runtime_program import ParameterNamespace
from .program.result_decoder import ResultDecoder
from .ibm_backend import IBMBackend
-from .utils.deprecation import issue_deprecation_msg
-from .exceptions import IBMInputValueError
def _active_session(func): # type: ignore
@@ -64,9 +62,6 @@ class Session:
job = sampler.run(ReferenceCircuits.bell())
print(f"Sampler job ID: {job.job_id()}")
print(f"Sampler job result: {job.result()}")
- # Close the session only if all jobs are finished and
- # you don't need to run more in the session.
- session.close()
"""
@@ -90,7 +85,7 @@ def __init__(
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".
- This value must be in between 300 seconds and the
+ This value must be less than the
`system imposed maximum
`_.
@@ -144,36 +139,19 @@ def run(
inputs: Program input parameters. These input values are passed
to the runtime program.
options: Runtime options that control the execution environment.
- See :class:`qiskit_ibm_runtime.RuntimeOptions` for all available options,
- EXCEPT ``backend``, which should be specified during session initialization.
+ See :class:`qiskit_ibm_runtime.RuntimeOptions` for all available options.
callback: Callback function to be invoked for any interim results and final result.
Returns:
Submitted job.
-
- Raises:
- IBMInputValueError: If a backend is passed in through options that does not match
- the current session backend.
"""
options = options or {}
if "instance" not in options:
options["instance"] = self._instance
- if "backend" in options:
- issue_deprecation_msg(
- "'backend' is no longer a supported option within a session",
- "0.9",
- "Instead, specify a backend when creating a Session instance.",
- 3,
- )
- if self._backend and options["backend"] != self._backend:
- raise IBMInputValueError(
- f"The backend '{options['backend']}' is different from",
- f"the session backend '{self._backend}'",
- )
- else:
- options["backend"] = self._backend
+
+ options["backend"] = self._backend
if not self._session_id:
# TODO: What happens if session max time != first job max time?
@@ -198,8 +176,16 @@ def run(
return job
+ def cancel(self) -> None:
+ """Cancel all pending jobs in a session."""
+ self._active = False
+ if self._session_id:
+ self._service._api_client.cancel_session(self._session_id)
+
def close(self) -> None:
- """Close the session."""
+ """Close the session so new jobs will no longer be accepted, but existing
+ queued or running jobs will run to completion. The session will be terminated once there
+ are no more pending jobs."""
self._active = False
if self._session_id:
self._service._api_client.close_session(self._session_id)
@@ -212,6 +198,68 @@ def backend(self) -> Optional[str]:
"""
return self._backend
+ def status(self) -> Optional[str]:
+ """Return current session status.
+
+ Returns:
+ The current status of the session, including:
+ Pending: Session is created but not active.
+ It will become active when the next job of this session is dequeued.
+ In progress, accepting new jobs: session is active and accepting new jobs.
+ In progress, not accepting new jobs: session is active and not accepting new jobs.
+ Closed: max_time expired or session was explicitly closed.
+ None: status details are not available.
+ """
+ details = self.details()
+ if details:
+ state = details["state"]
+ accepting_jobs = details["accepting_jobs"]
+ if state in ["open", "inactive"]:
+ return "Pending"
+ if state == "active" and accepting_jobs:
+ return "In progress, accepting new jobs"
+ if state == "active" and not accepting_jobs:
+ return "In progress, not accepting new jobs"
+ return state.capitalize()
+
+ return None
+
+ def details(self) -> Optional[Dict[str, Any]]:
+ """Return session details.
+
+ Returns:
+ A dictionary with the sessions details, including:
+ id: id of the session.
+ backend_name: backend used for the session.
+ interactive_timeout: The maximum idle time (in seconds) between jobs that
+ is allowed to occur before the session is deactivated.
+ max_time: Maximum allowed time (in seconds) for the session, subject to plan limits.
+ active_timeout: The maximum time (in seconds) a session can stay active.
+ state: State of the session - open, active, inactive, or closed.
+ accepting_jobs: Whether or not the session is accepting jobs.
+ last_job_started: Timestamp of when the last job in the session started.
+ last_job_completed: Timestamp of when the last job in the session completed.
+ started_at: Timestamp of when the session was started.
+ closed_at: Timestamp of when the session was closed.
+ """
+ if self._session_id:
+ response = self._service._api_client.session_details(self._session_id)
+ if response:
+ return {
+ "id": response.get("id"),
+ "backend_name": response.get("backend_name"),
+ "interactive_timeout": response.get("interactive_ttl"),
+ "max_time": response.get("max_ttl"),
+ "active_timeout": response.get("active_ttl"),
+ "state": response.get("state"),
+ "accepting_jobs": response.get("accepting_jobs"),
+ "last_job_started": response.get("last_job_started"),
+ "last_job_completed": response.get("last_job_completed"),
+ "started_at": response.get("started_at"),
+ "closed_at": response.get("closed_at"),
+ }
+ return None
+
@property
def session_id(self) -> str:
"""Return the session ID.
@@ -230,6 +278,30 @@ def service(self) -> QiskitRuntimeService:
"""
return self._service
+ @classmethod
+ def from_id(
+ cls,
+ session_id: str,
+ service: Optional[QiskitRuntimeService] = None,
+ backend: Optional[Union[str, IBMBackend]] = None,
+ ) -> "Session":
+ """Construct a Session object with a given session_id
+
+ Args:
+ session_id: the id of the session to be created. This can be an already
+ existing session id.
+ service: instance of the ``QiskitRuntimeService`` class.
+ backend: instance of :class:`qiskit_ibm_runtime.IBMBackend` class or
+ string name of backend.
+
+ Returns:
+ A new Session with the given ``session_id``
+
+ """
+ session = cls(service, backend)
+ session._session_id = session_id
+ return session
+
def __enter__(self) -> "Session":
set_cm_session(self)
return self
@@ -241,6 +313,7 @@ def __exit__(
exc_tb: Optional[TracebackType],
) -> None:
set_cm_session(None)
+ self.close()
# Default session
diff --git a/qiskit_ibm_runtime/utils/backend_converter.py b/qiskit_ibm_runtime/utils/backend_converter.py
index 241db1b3b1..d889b11b67 100644
--- a/qiskit_ibm_runtime/utils/backend_converter.py
+++ b/qiskit_ibm_runtime/utils/backend_converter.py
@@ -127,7 +127,7 @@ def convert_to_target(
target.granularity = configuration.timing_constraints.get("granularity")
target.min_length = configuration.timing_constraints.get("min_length")
target.pulse_alignment = configuration.timing_constraints.get("pulse_alignment")
- target.aquire_alignment = configuration.timing_constraints.get("acquire_alignment")
+ target.acquire_alignment = configuration.timing_constraints.get("acquire_alignment")
# If pulse defaults exists use that as the source of truth
if defaults is not None:
faulty_qubits = set()
diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py
index d5f51935bb..de477f982d 100644
--- a/qiskit_ibm_runtime/utils/json.py
+++ b/qiskit_ibm_runtime/utils/json.py
@@ -207,6 +207,8 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
return {"__type__": "ndarray", "__value__": obj.tolist()}
value = _serialize_and_encode(obj, np.save, allow_pickle=False)
return {"__type__": "ndarray", "__value__": value}
+ if isinstance(obj, np.int64):
+ return obj.item()
if isinstance(obj, set):
return {"__type__": "set", "__value__": list(obj)}
if isinstance(obj, Result):
diff --git a/qiskit_ibm_runtime/utils/qctrl.py b/qiskit_ibm_runtime/utils/qctrl.py
index bc8338e47c..f3acd1ec27 100644
--- a/qiskit_ibm_runtime/utils/qctrl.py
+++ b/qiskit_ibm_runtime/utils/qctrl.py
@@ -32,14 +32,10 @@ def validate(options: Dict[str, Any]) -> None:
# Default validation otherwise.
TranspilationOptions.validate_transpilation_options(options.get("transpilation"))
execution_time = options.get("max_execution_time")
- if not execution_time is None:
- if (
- execution_time < Options._MIN_EXECUTION_TIME
- or execution_time > Options._MAX_EXECUTION_TIME
- ):
+ if execution_time is not None:
+ if execution_time > Options._MAX_EXECUTION_TIME:
raise ValueError(
- f"max_execution_time must be between "
- f"{Options._MIN_EXECUTION_TIME} and {Options._MAX_EXECUTION_TIME} seconds."
+ f"max_execution_time must be below " f"{Options._MAX_EXECUTION_TIME} seconds."
)
EnvironmentOptions.validate_environment_options(options.get("environment"))
@@ -61,7 +57,7 @@ def _raise_if_error_in_options(options: Dict[str, Any]) -> None:
arguments={},
)
- optimization_level = options.get("optimization_level", 1)
+ optimization_level = options.get("optimization_level", 3)
_check_argument(
optimization_level > 0,
description="Q-CTRL Primitives do not support optimization level 0. Please\
diff --git a/releasenotes/notes/0.11/job-cost-estimation-d0ba83dbc95c3f67.yaml b/releasenotes/notes/0.11/job-cost-estimation-d0ba83dbc95c3f67.yaml
index 39396bdbd5..be82253c92 100644
--- a/releasenotes/notes/0.11/job-cost-estimation-d0ba83dbc95c3f67.yaml
+++ b/releasenotes/notes/0.11/job-cost-estimation-d0ba83dbc95c3f67.yaml
@@ -2,5 +2,5 @@
features:
- |
Added a new property, :meth:`~qiskit_ibm_runtime.RuntimeJob.usage_estimation`
- that returns the estimated running time, ``quantum_seconds``. Quantum time
- represents the time that the QPU complex is occupied exclusively by the job.
+ that returns the estimated system execution time, ``quantum_seconds``. System execution time
+ represents the amount of time that the system is dedicated to processing your job.
diff --git a/releasenotes/notes/0.12/backend-properties-datetime-0fe6a364c0a291d2.yaml b/releasenotes/notes/0.12/backend-properties-datetime-0fe6a364c0a291d2.yaml
new file mode 100644
index 0000000000..72c9228648
--- /dev/null
+++ b/releasenotes/notes/0.12/backend-properties-datetime-0fe6a364c0a291d2.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Retrieving backend properties with :meth:`~qiskit_ibm_runtime.IBMBackend.properties`
+ now supports passing a ``datetime`` parameter to retrieve properties from a past date.
+
diff --git a/releasenotes/notes/0.12/channel-strategy-backend-filter-a4fe5248d9aea9c1.yaml b/releasenotes/notes/0.12/channel-strategy-backend-filter-a4fe5248d9aea9c1.yaml
new file mode 100644
index 0000000000..58c7cf7122
--- /dev/null
+++ b/releasenotes/notes/0.12/channel-strategy-backend-filter-a4fe5248d9aea9c1.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ If using a ``channel_strategy``, only backends that support that ``channel_strategy``
+ will be accessible to the user.
+
diff --git a/releasenotes/notes/data-tracking-updates-97327c62c51b5891.yaml b/releasenotes/notes/0.12/data-tracking-updates-97327c62c51b5891.yaml
similarity index 100%
rename from releasenotes/notes/data-tracking-updates-97327c62c51b5891.yaml
rename to releasenotes/notes/0.12/data-tracking-updates-97327c62c51b5891.yaml
diff --git a/releasenotes/notes/default-channel-strategy-6899049ad4a7321b.yaml b/releasenotes/notes/0.12/default-channel-strategy-6899049ad4a7321b.yaml
similarity index 100%
rename from releasenotes/notes/default-channel-strategy-6899049ad4a7321b.yaml
rename to releasenotes/notes/0.12/default-channel-strategy-6899049ad4a7321b.yaml
diff --git a/releasenotes/notes/0.12/default-resilience-options-7929458af000314f.yaml b/releasenotes/notes/0.12/default-resilience-options-7929458af000314f.yaml
new file mode 100644
index 0000000000..74c9e9dc91
--- /dev/null
+++ b/releasenotes/notes/0.12/default-resilience-options-7929458af000314f.yaml
@@ -0,0 +1,10 @@
+---
+fixes:
+ - |
+ The ``noise_factors`` and ``extrapolator`` options in :class:`qiskit_ibm_runtime.options.ResilienceOptions`
+ will now default to ``None`` unless ``resilience_level`` is set to 2.
+ Only options relevant to the resilience level will be set, so when using ``resilience_level``
+ 2, ``noise_factors`` will still default to ``(1, 3, 5)`` and ``extrapolator`` will default to
+ ``LinearExtrapolator``. Additionally, options with a value of ``None`` will no longer be sent to
+ the server.
+
diff --git a/releasenotes/notes/0.12/default_account-13d86d50f5b1d972.yaml b/releasenotes/notes/0.12/default_account-13d86d50f5b1d972.yaml
new file mode 100644
index 0000000000..b84f81d215
--- /dev/null
+++ b/releasenotes/notes/0.12/default_account-13d86d50f5b1d972.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Added the option to define a default account in the account json file.
+ To select an account as default, define ``set_as_default=True`` in
+ ``QiskitRuntimeService.save_account()``.
diff --git a/releasenotes/notes/0.12/error-codes-82a392efad5963da.yaml b/releasenotes/notes/0.12/error-codes-82a392efad5963da.yaml
new file mode 100644
index 0000000000..5f95903a0e
--- /dev/null
+++ b/releasenotes/notes/0.12/error-codes-82a392efad5963da.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Job error messages now include the error code. Error codes can be found in
+ https://docs.quantum-computing.ibm.com/errors.
\ No newline at end of file
diff --git a/releasenotes/notes/error-message-case-31b4b2b7a5a2f624.yaml b/releasenotes/notes/0.12/error-message-case-31b4b2b7a5a2f624.yaml
similarity index 100%
rename from releasenotes/notes/error-message-case-31b4b2b7a5a2f624.yaml
rename to releasenotes/notes/0.12/error-message-case-31b4b2b7a5a2f624.yaml
diff --git a/releasenotes/notes/0.12/from_id-23fc85f3fbf01e0b.yaml b/releasenotes/notes/0.12/from_id-23fc85f3fbf01e0b.yaml
new file mode 100644
index 0000000000..7e61ea5a51
--- /dev/null
+++ b/releasenotes/notes/0.12/from_id-23fc85f3fbf01e0b.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Added new method ``Session.from_id`` which creates a new session with a given id.
+
+
diff --git a/releasenotes/notes/0.12/job-quota-warning-0512f30571897f53.yaml b/releasenotes/notes/0.12/job-quota-warning-0512f30571897f53.yaml
new file mode 100644
index 0000000000..a484c733be
--- /dev/null
+++ b/releasenotes/notes/0.12/job-quota-warning-0512f30571897f53.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ There will now be a warning if a user submits a job that is predicted to
+ exceed their system execution time monthly quota of 10 minutes.
+ This only applies to jobs run on real hardware in the instance ``ibm-q/open/main``.
+ If the job does end up exceeding the quota, it will be canceled.
diff --git a/releasenotes/notes/0.12/max-execution-time-definition-196cb6297693c0f2.yaml b/releasenotes/notes/0.12/max-execution-time-definition-196cb6297693c0f2.yaml
new file mode 100644
index 0000000000..70f55273ed
--- /dev/null
+++ b/releasenotes/notes/0.12/max-execution-time-definition-196cb6297693c0f2.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ The `max_execution_time` option is now based on system execution time instead of wall clock time.
+ System execution time is the amount of time that the system is dedicated to processing your job.
+ If a job exceeds this time limit, it is forcibly cancelled.
+ Simulator jobs continue to use wall clock time.
diff --git a/releasenotes/notes/q-ctrl-validation-08d249f1e84a43a5.yaml b/releasenotes/notes/0.12/q-ctrl-validation-08d249f1e84a43a5.yaml
similarity index 100%
rename from releasenotes/notes/q-ctrl-validation-08d249f1e84a43a5.yaml
rename to releasenotes/notes/0.12/q-ctrl-validation-08d249f1e84a43a5.yaml
diff --git a/releasenotes/notes/0.13/expose-session-details-c4a44316d30dad33.yaml b/releasenotes/notes/0.13/expose-session-details-c4a44316d30dad33.yaml
new file mode 100644
index 0000000000..6e525c509a
--- /dev/null
+++ b/releasenotes/notes/0.13/expose-session-details-c4a44316d30dad33.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Added a new method, :meth:`~qiskit_ibm_runtime.Session.details` that returns information
+ about a session, including: maximum session time, active time remaining, the current state,
+ and whether or not the session is accepting jobs.
+
+ Also added :meth:`~qiskit_ibm_runtime.Session.status`, which returns the current status of
+ the session.
+
diff --git a/releasenotes/notes/0.13/fix_np_int64-864b605a88f57419.yaml b/releasenotes/notes/0.13/fix_np_int64-864b605a88f57419.yaml
new file mode 100644
index 0000000000..106c68469a
--- /dev/null
+++ b/releasenotes/notes/0.13/fix_np_int64-864b605a88f57419.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+ - |
+ Fixed a bug where ``shots`` passed in as a numpy type were not being
+ serialized correctly.
diff --git a/releasenotes/notes/0.13/log-instance-selected-a18c4791418b5e0d.yaml b/releasenotes/notes/0.13/log-instance-selected-a18c4791418b5e0d.yaml
new file mode 100644
index 0000000000..3d78f85d1e
--- /dev/null
+++ b/releasenotes/notes/0.13/log-instance-selected-a18c4791418b5e0d.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ At initialization, if not passed in directly, the default ``instance`` selected by the provider
+ will be logged at the "INFO" level. When running a job, if the backend selected is not in
+ the default instance but in a different instance the user also has access to, that instance
+ will also be logged.
diff --git a/releasenotes/notes/0.13/session-accepting-jobs-d7ef6b60c0f5527b.yaml b/releasenotes/notes/0.13/session-accepting-jobs-d7ef6b60c0f5527b.yaml
new file mode 100644
index 0000000000..1a3421a49c
--- /dev/null
+++ b/releasenotes/notes/0.13/session-accepting-jobs-d7ef6b60c0f5527b.yaml
@@ -0,0 +1,13 @@
+---
+upgrade:
+ - |
+ :meth:`qiskit_ibm_runtime.Session.close` has been updated to mark a ``Session`` as no longer
+ accepting new jobs. The session won't accept more jobs but it will continue to run any
+ queued jobs until they are done or the max time expires. This will also happen
+ automatically when the session context manager is exited. When a session that is not accepting
+ jobs has run out of jobs to run, it's immediately closed, freeing up the backend to run more jobs rather
+ than wait for the interactive timeout.
+
+ The old close method behavior has been moved to a new method,
+ :meth:`qiskit_ibm_runtime.Session.cancel`, where all queued jobs within a session are
+ cancelled and terminated.
diff --git a/releasenotes/notes/0.13/target-history-date-bug-7d6dad84fc5b3d2e.yaml b/releasenotes/notes/0.13/target-history-date-bug-7d6dad84fc5b3d2e.yaml
new file mode 100644
index 0000000000..5c26b30650
--- /dev/null
+++ b/releasenotes/notes/0.13/target-history-date-bug-7d6dad84fc5b3d2e.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fixed a bug in :meth:`~qiskit_ibm_runtime.IBMBackend.target_history` where
+ the datetime parameter was not being used to retrieve backend properties from the
+ specified date.
diff --git a/releasenotes/notes/remove_kwargs_options-9024d3ec6572a53e.yaml b/releasenotes/notes/remove_kwargs_options-9024d3ec6572a53e.yaml
new file mode 100644
index 0000000000..e40a4f40c1
--- /dev/null
+++ b/releasenotes/notes/remove_kwargs_options-9024d3ec6572a53e.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Arbitrary keys and values are no longer allowed in ``Options``.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ecbaf24d0b..8387babf6f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -18,7 +18,7 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3
# Documentation
nbsphinx
-Sphinx>=5
+Sphinx>=6
sphinx-tabs>=1.1.11
sphinx-automodapi
sphinx-autodoc-typehints<=1.19.2
@@ -26,4 +26,4 @@ sphinx-design>=0.4.0
sphinx-intl
jupyter-sphinx
reno>=2.11.0
-qiskit-sphinx-theme~=1.12.0
+qiskit-sphinx-theme~=1.16.0
diff --git a/test/account.py b/test/account.py
index 1987eb5913..65da49c73a 100644
--- a/test/account.py
+++ b/test/account.py
@@ -152,6 +152,7 @@ def get_account_config_contents(
instance=None,
verify=None,
proxies=None,
+ set_default=None,
):
"""Generate qiskitrc content"""
if instance is None:
@@ -177,4 +178,6 @@ def get_account_config_contents(
out[name]["verify"] = verify
if proxies is not None:
out[name]["proxies"] = proxies
+ if set_default:
+ out[name]["is_default_account"] = True
return out
diff --git a/test/integration/test_account.py b/test/integration/test_account.py
index 902bd1c0dd..ffe8929e2e 100644
--- a/test/integration/test_account.py
+++ b/test/integration/test_account.py
@@ -80,3 +80,16 @@ def test_resolve_crn_for_invalid_service_instance_name(self):
token=self.dependencies.token,
instance=service_instance_name,
)
+
+ def test_logging_instance_at_init(self):
+ """Test instance is logged at initialization if instance not passed in."""
+ if self.dependencies.channel == "ibm_cloud":
+ self.skipTest("Not supported on ibm_cloud")
+
+ with self.assertLogs("qiskit_ibm_runtime", "INFO") as logs:
+ QiskitRuntimeService(
+ channel="ibm_quantum",
+ url=self.dependencies.url,
+ token=self.dependencies.token,
+ )
+ self.assertIn("instance", logs.output[0])
diff --git a/test/integration/test_backend.py b/test/integration/test_backend.py
index 0c51f1ca71..63a77d496f 100644
--- a/test/integration/test_backend.py
+++ b/test/integration/test_backend.py
@@ -13,6 +13,7 @@
"""Tests for backend functions using real runtime service."""
from unittest import SkipTest
+from datetime import datetime, timedelta
import copy
from qiskit.transpiler.target import Target
@@ -94,6 +95,14 @@ def test_backend_target(self):
self.assertIsNotNone(backend.target)
self.assertIsInstance(backend.target, Target)
+ @production_only
+ def test_backend_target_history(self):
+ """Check retrieving backend target_history."""
+ backend = self.backend
+ with self.subTest(backend=backend.name):
+ self.assertIsNotNone(backend.target_history())
+ self.assertIsNotNone(backend.target_history(datetime=datetime.now() - timedelta(30)))
+
def test_backend_max_circuits(self):
"""Check if the max_circuits property is set."""
backend = self.backend
@@ -130,7 +139,11 @@ def test_backend_properties(self):
with self.subTest(backend=backend.name):
if backend.simulator:
raise SkipTest("Skip since simulator does not have properties.")
- self.assertIsNotNone(backend.properties())
+ properties = backend.properties()
+ properties_today = backend.properties(datetime=datetime.today())
+ self.assertIsNotNone(properties)
+ self.assertIsNotNone(properties_today)
+ self.assertEqual(properties.backend_version, properties_today.backend_version)
@production_only
def test_backend_pulse_defaults(self):
diff --git a/test/integration/test_estimator.py b/test/integration/test_estimator.py
index c3b533d22c..48cca7087c 100644
--- a/test/integration/test_estimator.py
+++ b/test/integration/test_estimator.py
@@ -22,7 +22,6 @@
from qiskit.primitives import BaseEstimator, EstimatorResult
from qiskit_ibm_runtime import Estimator, Session
-from qiskit_ibm_runtime.exceptions import RuntimeJobFailureError
from ..decorators import run_integration_test
from ..ibm_test_case import IBMIntegrationTestCase
@@ -105,7 +104,6 @@ def test_estimator_session(self, service):
self.assertIsInstance(result5, EstimatorResult)
self.assertEqual(len(result5.values), len(circuits5))
self.assertEqual(len(result5.metadata), len(circuits5))
- session.close()
@run_integration_test
def test_estimator_callback(self, service):
@@ -132,7 +130,6 @@ def _callback(job_id_, result_):
self.assertTrue((result.values == ws_result_values).all())
self.assertEqual(len(job_ids), 1)
self.assertEqual(job.job_id(), job_ids.pop())
- session.close()
@run_integration_test
def test_estimator_coeffs(self, service):
@@ -182,25 +179,10 @@ def test_estimator_coeffs(self, service):
chsh1_runtime = job1.result()
chsh2_runtime = job2.result()
- session.close()
self.assertTrue(np.allclose(chsh1_terra.values, chsh1_runtime.values, rtol=0.3))
self.assertTrue(np.allclose(chsh2_terra.values, chsh2_runtime.values, rtol=0.3))
- @run_integration_test
- def test_estimator_error_messages(self, service):
- """Test that the correct error message is displayed"""
- circuit = QuantumCircuit(2, 2)
- circuit.h(0)
- with Session(service, self.backend) as session:
- estimator = Estimator(session=session)
- job = estimator.run(circuits=circuit, observables="II")
- with self.assertRaises(RuntimeJobFailureError) as err:
- job.result()
- self.assertIn("register name", str(err.exception))
- self.assertFalse("python -m uvicorn server.main" in str(err.exception))
- self.assertIn("register name", str(job.error_message()))
-
@run_integration_test
def test_estimator_no_session(self, service):
"""Test estimator primitive without a session."""
diff --git a/test/integration/test_job.py b/test/integration/test_job.py
index 8c63a8d8de..444d420f23 100644
--- a/test/integration/test_job.py
+++ b/test/integration/test_job.py
@@ -130,12 +130,6 @@ def test_run_program_override_max_execution_time(self, service):
job.wait_for_final_state()
self.assertEqual(job._api_client.job_get(job.job_id())["cost"], job_max_execution_time)
- @run_integration_test
- def test_invalid_max_execution_time_fails(self, service):
- """Test that program fails when max_execution_time is less than 300."""
- with self.assertRaises(ValueError):
- self._run_program(service, max_execution_time=299)
-
@run_integration_test
@production_only
def test_cancel_job_queued(self, service):
@@ -284,6 +278,7 @@ def test_job_creation_date(self, service):
for rjob in rjobs:
self.assertTrue(rjob.creation_date)
+ @unittest.skip("Skipping until primitives add more logging")
@run_integration_test
def test_job_logs(self, service):
"""Test job logs."""
diff --git a/test/integration/test_options.py b/test/integration/test_options.py
index eb51247e26..00db917746 100644
--- a/test/integration/test_options.py
+++ b/test/integration/test_options.py
@@ -70,7 +70,7 @@ def test_noise_model(self, service):
@run_integration_test
def test_simulator_transpile(self, service):
"""Test simulator transpile options."""
- backend = service.backends(simulator=True)[0]
+ backend = service.backend("ibmq_qasm_simulator")
self.log.info("Using backend %s", backend.name)
circ = QuantumCircuit(2, 2)
@@ -109,7 +109,7 @@ def test_unsupported_input_combinations(self, service):
obs = SparsePauliOp.from_list([("I", 1)])
options = Options()
options.resilience_level = 3
- backend = service.backends(simulator=True)[0]
+ backend = service.backend("ibmq_qasm_simulator")
with Session(service=service, backend=backend) as session:
with self.assertRaises(ValueError) as exc:
inst = Estimator(session=session, options=options)
@@ -122,7 +122,7 @@ def test_default_resilience_settings(self, service):
circ = QuantumCircuit(1)
obs = SparsePauliOp.from_list([("I", 1)])
options = Options(resilience_level=2)
- backend = service.backends(simulator=True)[0]
+ backend = service.backend("ibmq_qasm_simulator")
with Session(service=service, backend=backend) as session:
inst = Estimator(session=session, options=options)
job = inst.run(circ, observables=obs)
@@ -135,8 +135,8 @@ def test_default_resilience_settings(self, service):
with Session(service=service, backend=backend) as session:
inst = Estimator(session=session, options=options)
job = inst.run(circ, observables=obs)
- self.assertIsNone(job.inputs["resilience_settings"]["noise_factors"])
- self.assertIsNone(job.inputs["resilience_settings"]["extrapolator"])
+ self.assertNotIn("noise_factors", job.inputs["resilience_settings"])
+ self.assertNotIn("extrapolator", job.inputs["resilience_settings"])
@production_only
@run_integration_test
@@ -152,7 +152,7 @@ def test_all_resilience_levels(self, service):
psi1 = RealAmplitudes(num_qubits=2, reps=2)
h_1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
- backend = service.backends(simulator=True)[0]
+ backend = service.backend("ibmq_qasm_simulator")
options = Options()
options.simulator.coupling_map = [[0, 1], [1, 0]]
diff --git a/test/integration/test_sampler.py b/test/integration/test_sampler.py
index 0e26ab3e01..835b174938 100644
--- a/test/integration/test_sampler.py
+++ b/test/integration/test_sampler.py
@@ -70,7 +70,6 @@ def test_sampler_non_parameterized_circuits(self, service):
for i in range(len(circuits3)):
self.assertAlmostEqual(result3.quasi_dists[i][3], 0.5, delta=0.1)
self.assertAlmostEqual(result3.quasi_dists[i][0], 0.5, delta=0.1)
- session.close()
@run_integration_test
def test_sampler_primitive_parameterized_circuits(self, service):
@@ -98,7 +97,6 @@ def test_sampler_primitive_parameterized_circuits(self, service):
self.assertIsInstance(result, SamplerResult)
self.assertEqual(len(result.quasi_dists), len(circuits0))
self.assertEqual(len(result.metadata), len(circuits0))
- session.close()
@run_integration_test
def test_sampler_skip_transpile(self, service):
@@ -114,7 +112,6 @@ def test_sampler_skip_transpile(self, service):
sampler.run(circuits=circ, skip_transpilation=True).result()
# If transpilation not skipped the error would be something about cannot expand.
self.assertIn("invalid instructions", err.exception.message)
- session.close()
@run_integration_test
def test_sampler_optimization_level(self, service):
@@ -129,7 +126,6 @@ def test_sampler_optimization_level(self, service):
)
self.assertAlmostEqual(result.quasi_dists[0][3], 0.5, delta=0.1)
self.assertAlmostEqual(result.quasi_dists[0][0], 0.5, delta=0.1)
- session.close()
@run_integration_test
def test_sampler_callback(self, service):
@@ -154,21 +150,6 @@ def _callback(job_id_, result_):
self.assertEqual(result.quasi_dists, ws_result_quasi)
self.assertEqual(len(job_ids), 1)
self.assertEqual(job.job_id(), job_ids.pop())
- session.close()
-
- @run_integration_test
- def test_sampler_error_messages(self, service):
- """Test that the correct error message is displayed"""
- circuit = QuantumCircuit(2, 2)
- circuit.h(0)
- with Session(service, self.backend) as session:
- sampler = Sampler(session=session)
- job = sampler.run(circuits=circuit)
- with self.assertRaises(RuntimeJobFailureError) as err:
- job.result()
- self.assertIn("No counts for experiment", str(err.exception))
- self.assertFalse("python -m uvicorn server.main" in err.exception.message)
- self.assertIn("No counts for experiment", str(job.error_message()))
@run_integration_test
def test_sampler_no_session(self, service):
diff --git a/test/integration/test_session.py b/test/integration/test_session.py
index cc3277bdae..6b6bdb285e 100644
--- a/test/integration/test_session.py
+++ b/test/integration/test_session.py
@@ -84,3 +84,16 @@ def test_using_correct_instance(self, service):
job = sampler.run(ReferenceCircuits.bell(), shots=400)
self.assertEqual(instance, backend._instance)
self.assertEqual(instance, job.backend()._instance)
+
+ @run_integration_test
+ def test_session_from_id(self, service):
+ """Test creating a session from a given id"""
+ backend = service.backend("ibmq_qasm_simulator")
+ with Session(service, backend=backend) as session:
+ sampler = Sampler(session=session)
+ job = sampler.run(ReferenceCircuits.bell(), shots=400)
+ session_id = job.session_id
+ new_session = Session.from_id(backend=backend, session_id=session_id)
+ sampler = Sampler(session=new_session)
+ job = sampler.run(ReferenceCircuits.bell(), shots=400)
+ self.assertEqual(session_id, job.session_id)
diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py
index ab5daa13c9..1f2c16fb8d 100644
--- a/test/unit/mock/fake_runtime_client.py
+++ b/test/unit/mock/fake_runtime_client.py
@@ -20,6 +20,8 @@
from concurrent.futures import ThreadPoolExecutor
from typing import Optional, Dict, Any, List
+from qiskit.providers.exceptions import QiskitBackendNotFoundError
+
from qiskit_ibm_provider.utils.hgp import from_instance_format
from qiskit_ibm_runtime.api.exceptions import RequestsApiError
from qiskit_ibm_runtime.utils import RuntimeEncoder
@@ -514,7 +516,10 @@ def _get_job(self, job_id: str, exclude_params: bool = None) -> Any:
raise RequestsApiError("Job not found", status_code=404)
return self._jobs[job_id]
- def list_backends(self, hgp: Optional[str] = None) -> List[str]:
+ # pylint: disable=unused-argument
+ def list_backends(
+ self, hgp: Optional[str] = None, channel_strategy: Optional[str] = None
+ ) -> List[str]:
"""Return IBM backends available for this service instance."""
return [back.name for back in self._backends if back.has_access(hgp)]
@@ -540,4 +545,4 @@ def _find_backend(self, backend_name):
for back in self._backends:
if back.name == backend_name:
return back
- raise ValueError(f"Backend {backend_name} not found")
+ raise QiskitBackendNotFoundError(f"Backend {backend_name} not found")
diff --git a/test/unit/test_account.py b/test/unit/test_account.py
index dce40f9311..4091667146 100644
--- a/test/unit/test_account.py
+++ b/test/unit/test_account.py
@@ -29,8 +29,6 @@
)
from qiskit_ibm_runtime.accounts.account import IBM_CLOUD_API_URL, IBM_QUANTUM_API_URL
from qiskit_ibm_runtime.accounts.management import (
- _DEFAULT_ACCOUNT_NAME_LEGACY,
- _DEFAULT_ACCOUNT_NAME_CLOUD,
_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM,
_DEFAULT_ACCOUNT_NAME_IBM_CLOUD,
)
@@ -44,14 +42,14 @@
custom_envs,
)
-_TEST_IBM_QUANTUM_ACCOUNT = Account(
+_TEST_IBM_QUANTUM_ACCOUNT = Account.create_account(
channel="ibm_quantum",
token="token-x",
url="https://auth.quantum-computing.ibm.com/api",
instance="ibm-q/open/main",
)
-_TEST_IBM_CLOUD_ACCOUNT = Account(
+_TEST_IBM_CLOUD_ACCOUNT = Account.create_account(
channel="ibm_cloud",
token="token-y",
url="https://cloud.ibm.com",
@@ -61,25 +59,6 @@
),
)
-_TEST_LEGACY_ACCOUNT = {
- "auth": "legacy",
- "token": "token-x",
- "url": "https://auth.quantum-computing.ibm.com/api",
- "instance": "ibm-q/open/main",
-}
-
-_TEST_CLOUD_ACCOUNT = {
- "auth": "cloud",
- "token": "token-y",
- "url": "https://cloud.ibm.com",
- "instance": "crn:v1:bluemix:public:quantum-computing:us-east:a/...::",
- "proxies": {
- "username_ntlm": "bla",
- "password_ntlm": "blub",
- "urls": {"https": "127.0.0.1"},
- },
-}
-
_TEST_FILENAME = "/tmp/temp_qiskit_account.json"
@@ -101,7 +80,7 @@ def test_invalid_channel(self):
with self.assertRaises(InvalidAccountError) as err:
invalid_channel: Any = "phantom"
- Account(
+ Account.create_account(
channel=invalid_channel,
token=self.dummy_token,
url=self.dummy_ibm_cloud_url,
@@ -115,7 +94,7 @@ def test_invalid_token(self):
for token in invalid_tokens:
with self.subTest(token=token):
with self.assertRaises(InvalidAccountError) as err:
- Account(
+ Account.create_account(
channel="ibm_cloud",
token=token,
url=self.dummy_ibm_cloud_url,
@@ -131,7 +110,7 @@ def test_invalid_url(self):
for params in subtests:
with self.subTest(params=params):
with self.assertRaises(InvalidAccountError) as err:
- Account(**params, token=self.dummy_token).validate()
+ Account.create_account(**params, token=self.dummy_token).validate()
self.assertIn("Invalid `url` value.", str(err.exception))
def test_invalid_instance(self):
@@ -145,7 +124,7 @@ def test_invalid_instance(self):
for params in subtests:
with self.subTest(params=params):
with self.assertRaises(InvalidAccountError) as err:
- Account(
+ Account.create_account(
**params, token=self.dummy_token, url=self.dummy_ibm_cloud_url
).validate()
self.assertIn("Invalid `instance` value.", str(err.exception))
@@ -158,7 +137,7 @@ def test_invalid_channel_strategy(self):
for params in subtests:
with self.subTest(params=params):
with self.assertRaises(InvalidAccountError) as err:
- Account(
+ Account.create_account(
**params,
token=self.dummy_token,
url=self.dummy_ibm_cloud_url,
@@ -183,7 +162,7 @@ def test_invalid_proxy_config(self):
for params in subtests:
with self.subTest(params=params):
with self.assertRaises(ValueError) as err:
- Account(
+ Account.create_account(
**params,
channel="ibm_quantum",
token=self.dummy_token,
@@ -229,138 +208,6 @@ def test_save_without_overwrite(self):
overwrite=False,
)
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT})
- @no_envs(["QISKIT_IBM_TOKEN"])
- def test_save_channel_ibm_cloud_over_auth_cloud_without_overwrite(self):
- """Test to overwrite an existing auth "cloud" account with channel "ibm_cloud"
- and without setting overwrite=True."""
- with self.assertRaises(AccountAlreadyExistsError):
- AccountManager.save(
- token=_TEST_IBM_CLOUD_ACCOUNT.token,
- url=_TEST_IBM_CLOUD_ACCOUNT.url,
- instance=_TEST_IBM_CLOUD_ACCOUNT.instance,
- channel="ibm_cloud",
- name=None,
- overwrite=False,
- )
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT})
- @no_envs(["QISKIT_IBM_TOKEN"])
- def test_save_channel_ibm_quantum_over_auth_legacy_without_overwrite(self):
- """Test to overwrite an existing auth "legacy" account with channel "ibm_quantum"
- and without setting overwrite=True."""
- with self.assertRaises(AccountAlreadyExistsError):
- AccountManager.save(
- token=_TEST_IBM_QUANTUM_ACCOUNT.token,
- url=_TEST_IBM_QUANTUM_ACCOUNT.url,
- instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
- channel="ibm_quantum",
- name=None,
- overwrite=False,
- )
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT})
- @no_envs(["QISKIT_IBM_TOKEN"])
- def test_save_channel_ibm_quantum_over_auth_legacy_with_overwrite(self):
- """Test to overwrite an existing auth "legacy" account with channel "ibm_quantum"
- and with setting overwrite=True."""
- AccountManager.save(
- token=_TEST_IBM_QUANTUM_ACCOUNT.token,
- url=_TEST_IBM_QUANTUM_ACCOUNT.url,
- instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
- channel="ibm_quantum",
- name=None,
- overwrite=True,
- )
- self.assertEqual(_TEST_IBM_QUANTUM_ACCOUNT, AccountManager.get(channel="ibm_quantum"))
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT})
- @no_envs(["QISKIT_IBM_TOKEN"])
- def test_save_channel_ibm_cloud_over_auth_cloud_with_overwrite(self):
- """Test to overwrite an existing auth "cloud" account with channel "ibm_cloud"
- and with setting overwrite=True."""
- AccountManager.save(
- token=_TEST_IBM_CLOUD_ACCOUNT.token,
- url=_TEST_IBM_CLOUD_ACCOUNT.url,
- instance=_TEST_IBM_CLOUD_ACCOUNT.instance,
- channel="ibm_cloud",
- proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies,
- name=None,
- overwrite=True,
- channel_strategy="q-ctrl",
- )
- self.assertEqual(_TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(channel="ibm_cloud"))
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={"personal-account": _TEST_CLOUD_ACCOUNT})
- def test_save_channel_ibm_cloud_with_name_over_auth_cloud_with_overwrite(self):
- """Test to overwrite an existing named auth "cloud" account with channel "ibm_cloud"
- and with setting overwrite=True."""
- AccountManager.save(
- token=_TEST_IBM_CLOUD_ACCOUNT.token,
- url=_TEST_IBM_CLOUD_ACCOUNT.url,
- instance=_TEST_IBM_CLOUD_ACCOUNT.instance,
- channel="ibm_cloud",
- proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies,
- name="personal-account",
- overwrite=True,
- )
- self.assertEqual(_TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(name="personal-account"))
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={"personal-account": _TEST_CLOUD_ACCOUNT})
- def test_save_channel_ibm_cloud_with_name_over_auth_cloud_without_overwrite(self):
- """Test to overwrite an existing named auth "cloud" account with channel "ibm_cloud"
- and without setting overwrite=True."""
- with self.assertRaises(AccountAlreadyExistsError):
- AccountManager.save(
- token=_TEST_IBM_CLOUD_ACCOUNT.token,
- url=_TEST_IBM_CLOUD_ACCOUNT.url,
- instance=_TEST_IBM_CLOUD_ACCOUNT.instance,
- channel="ibm_cloud",
- proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies,
- name="personal-account",
- overwrite=False,
- )
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={"personal-account": _TEST_LEGACY_ACCOUNT})
- def test_save_channel_ibm_quantum_with_name_over_auth_legacy_with_overwrite(self):
- """Test to overwrite an existing named auth "legacy" account with channel "ibm_quantum"
- and with setting overwrite=True."""
- AccountManager.save(
- token=_TEST_IBM_QUANTUM_ACCOUNT.token,
- url=_TEST_IBM_QUANTUM_ACCOUNT.url,
- instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
- channel="ibm_quantum",
- proxies=_TEST_IBM_QUANTUM_ACCOUNT.proxies,
- name="personal-account",
- overwrite=True,
- )
- self.assertEqual(_TEST_IBM_QUANTUM_ACCOUNT, AccountManager.get(name="personal-account"))
-
- # TODO remove test when removing auth parameter
- @temporary_account_config_file(contents={"personal-account": _TEST_LEGACY_ACCOUNT})
- def test_save_channel_ibm_quantum_with_name_over_auth_legacy_without_overwrite(
- self,
- ):
- """Test to overwrite an existing named auth "legacy" account with channel "ibm_quantum"
- and without setting overwrite=True."""
- with self.assertRaises(AccountAlreadyExistsError):
- AccountManager.save(
- token=_TEST_IBM_QUANTUM_ACCOUNT.token,
- url=_TEST_IBM_QUANTUM_ACCOUNT.url,
- instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
- channel="ibm_quantum",
- proxies=_TEST_IBM_QUANTUM_ACCOUNT.proxies,
- name="personal-account",
- overwrite=False,
- )
-
@temporary_account_config_file(contents={"conflict": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format()})
def test_get_none(self):
"""Test to get an account with an invalid name."""
@@ -457,30 +304,15 @@ def test_list(self):
self.assertEqual(accounts["key1"], _TEST_IBM_CLOUD_ACCOUNT)
self.assertTrue(accounts["key2"], _TEST_IBM_QUANTUM_ACCOUNT)
- with temporary_account_config_file(
- contents={
- _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT,
- _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_CLOUD_ACCOUNT,
- }
- ), self.subTest("non-empty list of auth accounts"):
- accounts = AccountManager.list()
-
- self.assertEqual(len(accounts), 2)
- self.assertEqual(accounts[_DEFAULT_ACCOUNT_NAME_IBM_CLOUD], _TEST_IBM_CLOUD_ACCOUNT)
- self.assertTrue(accounts[_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM], _TEST_IBM_QUANTUM_ACCOUNT)
-
- with temporary_account_config_file(contents={}), self.subTest("empty list of accounts"):
- self.assertEqual(len(AccountManager.list()), 0)
-
with temporary_account_config_file(
contents={
"key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(),
"key2": _TEST_IBM_QUANTUM_ACCOUNT.to_saved_format(),
- _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: Account(
- "ibm_cloud", "token-ibm-cloud", instance="crn:123"
+ _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: Account.create_account(
+ channel="ibm_cloud", token="token-ibm-cloud", instance="crn:123"
).to_saved_format(),
- _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: Account(
- "ibm_quantum", "token-ibm-quantum"
+ _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: Account.create_account(
+ channel="ibm_quantum", token="token-ibm-quantum"
).to_saved_format(),
}
), self.subTest("filtered list of accounts"):
@@ -504,35 +336,6 @@ def test_list(self):
self.assertEqual(len(accounts), 1)
self.assertListEqual(accounts, ["key1"])
- # TODO remove test when removing auth parameter
- with temporary_account_config_file(
- contents={
- "key1": _TEST_CLOUD_ACCOUNT,
- "key2": _TEST_LEGACY_ACCOUNT,
- _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT,
- _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT,
- }
- ), self.subTest("filtered list of auth accounts"):
- accounts = list(AccountManager.list(channel="ibm_cloud").keys())
- self.assertEqual(len(accounts), 2)
- self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_CLOUD, "key1"])
-
- accounts = list(AccountManager.list(channel="ibm_quantum").keys())
- self.assertEqual(len(accounts), 2)
- self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, "key2"])
-
- accounts = list(AccountManager.list(channel="ibm_cloud", default=True).keys())
- self.assertEqual(len(accounts), 1)
- self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_CLOUD])
-
- accounts = list(AccountManager.list(channel="ibm_cloud", default=False).keys())
- self.assertEqual(len(accounts), 1)
- self.assertListEqual(accounts, ["key1"])
-
- accounts = list(AccountManager.list(name="key1").keys())
- self.assertEqual(len(accounts), 1)
- self.assertListEqual(accounts, ["key1"])
-
@temporary_account_config_file(
contents={
"key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(),
@@ -555,32 +358,10 @@ def test_delete(self):
self.assertTrue(len(AccountManager.list()) == 0)
- @temporary_account_config_file(
- contents={
- "key1": _TEST_CLOUD_ACCOUNT,
- _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT,
- _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT,
- }
- )
- def test_delete_auth(self):
- """Test delete accounts already saved using auth."""
-
- with self.subTest("delete named account"):
- self.assertTrue(AccountManager.delete(name="key1"))
- self.assertFalse(AccountManager.delete(name="key1"))
-
- with self.subTest("delete default auth='legacy' account using channel"):
- self.assertTrue(AccountManager.delete(channel="ibm_quantum"))
-
- with self.subTest("delete default auth='cloud' account using channel"):
- self.assertTrue(AccountManager.delete())
-
- self.assertTrue(len(AccountManager.list()) == 0)
-
def test_delete_filename(self):
"""Test delete accounts with filename parameter."""
- filename = "~/account_to_delete.json"
+ filename = _TEST_FILENAME
name = "key1"
channel = "ibm_quantum"
AccountManager.save(channel=channel, filename=filename, name=name, token="temp_token")
@@ -606,6 +387,180 @@ def test_account_with_filename(self):
)
self.assertEqual(account.token, dummy_token)
+ @temporary_account_config_file()
+ def test_default_env_channel(self):
+ """Test that if QISKIT_IBM_CHANNEL is set in the environment, this channel will be used"""
+ token = uuid.uuid4().hex
+ # unset default_channel in the environment
+ with temporary_account_config_file(token=token), no_envs("QISKIT_IBM_CHANNEL"):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_cloud")
+
+ # set channel to default channel in the environment
+ subtests = ["ibm_quantum", "ibm_cloud"]
+ for channel in subtests:
+ channel_env = {"QISKIT_IBM_CHANNEL": channel}
+ with temporary_account_config_file(channel=channel, token=token), custom_envs(
+ channel_env
+ ):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, channel)
+
+ def test_save_default_account(self):
+ """Test that if a default_account is defined in the qiskit-ibm.json file,
+ this account will be used"""
+ AccountManager.save(
+ filename=_TEST_FILENAME,
+ name=_DEFAULT_ACCOUNT_NAME_IBM_CLOUD,
+ token=_TEST_IBM_CLOUD_ACCOUNT.token,
+ url=_TEST_IBM_CLOUD_ACCOUNT.url,
+ instance=_TEST_IBM_CLOUD_ACCOUNT.instance,
+ channel="ibm_cloud",
+ overwrite=True,
+ set_as_default=True,
+ )
+ AccountManager.save(
+ filename=_TEST_FILENAME,
+ name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM,
+ token=_TEST_IBM_QUANTUM_ACCOUNT.token,
+ url=_TEST_IBM_QUANTUM_ACCOUNT.url,
+ instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
+ channel="ibm_quantum",
+ overwrite=True,
+ )
+
+ with no_envs("QISKIT_IBM_CHANNEL"), no_envs("QISKIT_IBM_TOKEN"):
+ account = AccountManager.get(filename=_TEST_FILENAME)
+ self.assertEqual(account.channel, "ibm_cloud")
+ self.assertEqual(account.token, _TEST_IBM_CLOUD_ACCOUNT.token)
+
+ AccountManager.save(
+ filename=_TEST_FILENAME,
+ name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM,
+ token=_TEST_IBM_QUANTUM_ACCOUNT.token,
+ url=_TEST_IBM_QUANTUM_ACCOUNT.url,
+ instance=_TEST_IBM_QUANTUM_ACCOUNT.instance,
+ channel="ibm_quantum",
+ overwrite=True,
+ set_as_default=True,
+ )
+ with no_envs("QISKIT_IBM_CHANNEL"), no_envs("QISKIT_IBM_TOKEN"):
+ account = AccountManager.get(filename=_TEST_FILENAME)
+ self.assertEqual(account.channel, "ibm_quantum")
+ self.assertEqual(account.token, _TEST_IBM_QUANTUM_ACCOUNT.token)
+
+ @temporary_account_config_file()
+ def test_set_channel_precedence(self):
+ """Test the precedence of the various methods to set the account:
+ account name > env_variables > channel parameter default account
+ > default account > default account from default channel"""
+ cloud_token = uuid.uuid4().hex
+ default_token = uuid.uuid4().hex
+ preferred_token = uuid.uuid4().hex
+ any_token = uuid.uuid4().hex
+ channel_env = {"QISKIT_IBM_CHANNEL": "ibm_cloud"}
+ contents = {
+ _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: {
+ "channel": "ibm_cloud",
+ "token": cloud_token,
+ "instance": "some_instance",
+ },
+ _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: {
+ "channel": "ibm_quantum",
+ "token": default_token,
+ },
+ "preferred-ibm-quantum": {
+ "channel": "ibm_quantum",
+ "token": preferred_token,
+ "is_default_account": True,
+ },
+ "any-quantum": {
+ "channel": "ibm_quantum",
+ "token": any_token,
+ },
+ }
+
+ # 'name' parameter
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService(name="any-quantum")
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, any_token)
+
+ # No name or channel params, no env vars, get the account specified as "is_default_account"
+ with temporary_account_config_file(contents=contents), no_envs(
+ "QISKIT_IBM_CHANNEL"
+ ), no_envs("QISKIT_IBM_TOKEN"):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, preferred_token)
+
+ # parameter 'channel' is specified, it overrides channel in env
+ # account specified as "is_default_account"
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService(channel="ibm_quantum")
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, preferred_token)
+
+ # account with default name for the channel
+ contents["preferred-ibm-quantum"]["is_default_account"] = False
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService(channel="ibm_quantum")
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, default_token)
+
+ # any account for this channel
+ del contents["default-ibm-quantum"]
+ # channel_env = {"QISKIT_IBM_CHANNEL": "ibm_quantum"}
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService(channel="ibm_quantum")
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, any_token)
+
+ # no channel param, get account that is specified as "is_default_account"
+ # for channel from env
+ contents["preferred-ibm-quantum"]["is_default_account"] = True
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, preferred_token)
+
+ # no channel param, account with default name for the channel from env
+ del contents["preferred-ibm-quantum"]["is_default_account"]
+ contents["default-ibm-quantum"] = {
+ "channel": "ibm_quantum",
+ "token": default_token,
+ }
+ channel_env = {"QISKIT_IBM_CHANNEL": "ibm_quantum"}
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, default_token)
+
+ # no channel param, any account for the channel from env
+ del contents["default-ibm-quantum"]
+ with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs(
+ "QISKIT_IBM_TOKEN"
+ ):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_quantum")
+ self.assertEqual(service._account.token, any_token)
+ # default channel
+ with temporary_account_config_file(contents=contents), no_envs("QISKIT_IBM_CHANNEL"):
+ service = FakeRuntimeService()
+ self.assertEqual(service.channel, "ibm_cloud")
+
def tearDown(self) -> None:
"""Test level tear down."""
super().tearDown()
@@ -735,7 +690,10 @@ def test_enable_account_both_channel(self):
token = uuid.uuid4().hex
contents = get_account_config_contents(channel="ibm_cloud", token=token)
contents.update(get_account_config_contents(channel="ibm_quantum", token=uuid.uuid4().hex))
- with temporary_account_config_file(contents=contents), no_envs(["QISKIT_IBM_TOKEN"]):
+
+ with temporary_account_config_file(contents=contents), no_envs(
+ ["QISKIT_IBM_TOKEN", "QISKIT_IBM_CHANNEL"]
+ ):
service = FakeRuntimeService()
self.assertTrue(service._account)
self.assertEqual(service._account.token, token)
@@ -754,7 +712,7 @@ def test_enable_account_by_env_channel(self):
"QISKIT_IBM_URL": url,
"QISKIT_IBM_INSTANCE": "h/g/p" if channel == "ibm_quantum" else "crn:12",
}
- with custom_envs(envs):
+ with custom_envs(envs), no_envs("QISKIT_IBM_CHANNEL"):
service = FakeRuntimeService(channel=channel)
self.assertTrue(service._account)
@@ -871,7 +829,7 @@ def test_enable_account_by_env_pref(self):
"QISKIT_IBM_URL": url,
"QISKIT_IBM_INSTANCE": "my_crn",
}
- with custom_envs(envs):
+ with custom_envs(envs), no_envs("QISKIT_IBM_CHANNEL"):
service = FakeRuntimeService(**extra)
self.assertTrue(service._account)
diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py
index 3d47860296..604b78cb19 100644
--- a/test/unit/test_data_serialization.py
+++ b/test/unit/test_data_serialization.py
@@ -20,46 +20,14 @@
from datetime import datetime
import numpy as np
-import scipy.sparse
-from qiskit.algorithms.optimizers import (
- ADAM,
- GSLS,
- SPSA,
- QNSPSA,
- L_BFGS_B,
- NELDER_MEAD,
-)
+
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.test.reference_circuits import ReferenceCircuits
from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate
-from qiskit.opflow import (
- PauliSumOp,
- MatrixOp,
- PauliOp,
- CircuitOp,
- EvolvedOp,
- TaperedPauliSumOp,
- Z2Symmetries,
- I,
- X,
- Y,
- Z,
- StateFn,
- CircuitStateFn,
- DictStateFn,
- VectorStateFn,
- OperatorStateFn,
- SparseVectorStateFn,
- CVaRMeasurement,
- ComposedOp,
- SummedOp,
- TensoredOp,
-)
from qiskit.providers.fake_provider import FakeNairobi
from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector
from qiskit.result import Result
from qiskit_aer.noise import NoiseModel
-
from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder
from .mock.fake_runtime_client import CustomResultRuntimeJob
from .mock.fake_runtime_service import FakeRuntimeService
@@ -125,73 +93,47 @@ def test_coder_qc(self):
def test_coder_operators(self):
"""Test runtime encoder and decoder for operators."""
+
+ # filter warnings triggered by opflow imports
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", category=DeprecationWarning, module=r"qiskit\.opflow\."
+ )
+ from qiskit.opflow import PauliSumOp # pylint: disable=import-outside-toplevel
+
+ # catch warnings triggered by opflow use
+ with warnings.catch_warnings(record=True) as w_log:
+ deprecated_op = PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]))
+ self.assertTrue(len(w_log) > 0)
+
coeff_x = Parameter("x")
coeff_y = coeff_x + 1
- quantum_circuit = QuantumCircuit(1)
- quantum_circuit.h(0)
- operator = 2.0 * I ^ I
- z2_symmetries = Z2Symmetries(
- [Pauli("IIZI"), Pauli("ZIII")],
- [Pauli("IIXI"), Pauli("XIII")],
- [1, 3],
- [-1, 1],
- )
- isqrt2 = 1 / np.sqrt(2)
- sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]])
subtests = (
- PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3),
- PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=coeff_y),
- PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j),
- PauliSumOp.from_list([("II", -1.052373245772859), ("IZ", 0.39793742484318045)]),
- MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=coeff_x),
- PauliOp(primitive=Pauli("Y"), coeff=coeff_x),
- CircuitOp(quantum_circuit, coeff=coeff_x),
- EvolvedOp(operator, coeff=coeff_x),
- TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries),
- StateFn(quantum_circuit, coeff=coeff_x),
- CircuitStateFn(quantum_circuit, is_measurement=True),
- DictStateFn("1" * 3, is_measurement=True),
- VectorStateFn(np.ones(2**3, dtype=complex)),
- OperatorStateFn(CircuitOp(QuantumCircuit(1))),
- SparseVectorStateFn(sparse),
- Statevector([1, 0]),
- CVaRMeasurement(Z, 0.2),
- ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]),
- SummedOp([X ^ X * 2, Y ^ Y], 2),
- TensoredOp([(X ^ Y), (Z ^ I)]),
- (Z ^ Z) ^ (I ^ 2),
+ SparsePauliOp(Pauli("XYZX"), coeffs=[2]),
+ SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]),
+ SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]),
+ deprecated_op,
)
+
for operator in subtests:
with self.subTest(operator=operator):
encoded = json.dumps(operator, cls=RuntimeEncoder)
self.assertIsInstance(encoded, str)
- decoded = json.loads(encoded, cls=RuntimeDecoder)
- self.assertEqual(operator, decoded)
- def test_coder_optimizers(self):
- """Test runtime encoder and decoder for optimizers."""
- subtests = (
- (ADAM, {"maxiter": 100, "amsgrad": True}),
- (GSLS, {"maxiter": 50, "min_step_size": 0.01}),
- (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}),
- (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}),
- # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682
- (L_BFGS_B, {}),
- (NELDER_MEAD, {}),
- # Enable when https://github.com/scikit-quant/scikit-quant/issues/24 is fixed
- # (IMFIL, {"maxiter": 20}),
- # (SNOBFIT, {"maxiter": 200, "maxfail": 20}),
- )
- for opt_cls, settings in subtests:
- with self.subTest(opt_cls=opt_cls):
- optimizer = opt_cls(**settings)
- encoded = json.dumps(optimizer, cls=RuntimeEncoder)
- self.assertIsInstance(encoded, str)
- decoded = json.loads(encoded, cls=RuntimeDecoder)
- self.assertTrue(isinstance(decoded, opt_cls))
- for key, value in settings.items():
- self.assertEqual(decoded.settings[key], value)
+ with warnings.catch_warnings():
+ # filter warnings triggered by opflow imports
+ # in L146 of utils/json.py
+ warnings.filterwarnings(
+ "ignore", category=DeprecationWarning, module=r"qiskit\.opflow\."
+ )
+ warnings.filterwarnings(
+ "ignore",
+ category=DeprecationWarning,
+ module=r"qiskit_ibm_runtime\.utils\.json",
+ )
+ decoded = json.loads(encoded, cls=RuntimeDecoder)
+ self.assertEqual(operator, decoded)
def test_coder_noise_model(self):
"""Test encoding and decoding a noise model."""
@@ -244,6 +186,13 @@ def test_encoder_instruction(self):
decoded = json.loads(encoded, cls=RuntimeDecoder)
self.assertEqual(decoded, obj)
+ def test_encoder_np_number(self):
+ """Test encoding and decoding instructions"""
+ encoded = json.dumps(np.int64(100), cls=RuntimeEncoder)
+ self.assertIsInstance(encoded, str)
+ decoded = json.loads(encoded, cls=RuntimeDecoder)
+ self.assertEqual(decoded, 100)
+
def test_encoder_callable(self):
"""Test encoding a callable."""
with warnings.catch_warnings(record=True) as warn_cm:
@@ -268,8 +217,7 @@ def test_decoder_import(self):
temp_fp.close()
subtests = (
- PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3),
- DictStateFn("1" * 3, is_measurement=True),
+ SparsePauliOp(Pauli("XYZX"), coeffs=[2]),
Statevector([1, 0]),
)
for operator in subtests:
diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py
index f7751ce6dc..7d29a9df48 100644
--- a/test/unit/test_estimator.py
+++ b/test/unit/test_estimator.py
@@ -12,7 +12,6 @@
"""Tests for estimator class."""
-import warnings
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp, Pauli, random_hermitian, random_pauli_list
@@ -20,11 +19,11 @@
import numpy as np
-from qiskit_ibm_runtime import Estimator, Session, Options
+from qiskit_ibm_runtime import Estimator, Session
+from .mock.fake_runtime_service import FakeRuntimeService
from ..ibm_test_case import IBMTestCase
from ..utils import get_mocked_backend
-from .mock.fake_runtime_service import FakeRuntimeService
class TestEstimator(IBMTestCase):
@@ -41,6 +40,7 @@ def test_unsupported_values_for_estimator_options(self):
{"resilience_level": 4, "optimization_level": 3},
{"optimization_level": 4, "resilience_level": 2},
]
+
with Session(
service=FakeRuntimeService(channel="ibm_quantum", token="abc"),
backend="common_backend",
@@ -51,28 +51,6 @@ def test_unsupported_values_for_estimator_options(self):
_ = inst.run(self.circuit, observables=self.observables, **bad_opt)
self.assertIn(list(bad_opt.keys())[0], str(exc.exception))
- def test_deprecated_noise_amplifier(self):
- """Test noise_amplifier deprecation."""
- opt = Options()
- opt.resilience.noise_amplifier = "GlobalFoldingAmplifier"
-
- with warnings.catch_warnings(record=True) as warn:
- warnings.simplefilter("always")
- estimator = Estimator(backend=get_mocked_backend(), options=opt)
- estimator.run(self.circuit, self.observables)
- self.assertEqual(len(warn), 1, "Deprecation warning not found.")
- self.assertIn("noise_amplifier", str(warn[-1].message))
-
- def test_deprecated_noise_amplifier_run(self):
- """Test noise_amplifier deprecation in run."""
-
- with warnings.catch_warnings(record=True) as warn:
- warnings.simplefilter("always")
- estimator = Estimator(backend=get_mocked_backend())
- estimator.run(self.circuit, self.observables, noise_amplifier="GlobalFoldingAmplifier")
- self.assertEqual(len(warn), 1, "Deprecation warning not found.")
- self.assertIn("noise_amplifier", str(warn[-1].message))
-
def test_observable_types_single_circuit(self):
"""Test different observable types for a single circuit."""
all_obs = [
diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py
index 6d5f2814fa..185505ee44 100644
--- a/test/unit/test_ibm_primitives.py
+++ b/test/unit/test_ibm_primitives.py
@@ -15,7 +15,6 @@
import sys
import os
from unittest.mock import MagicMock, patch
-import warnings
from dataclasses import asdict
from typing import Dict
@@ -82,28 +81,6 @@ def test_dict_options(self):
inst = cls(session=MagicMock(spec=MockSession), options=options)
self.assertTrue(dict_paritally_equal(inst.options.__dict__, options))
- def test_backend_in_options(self):
- """Test specifying backend in options."""
- primitives = [Sampler, Estimator]
- backend_name = "ibm_gotham"
- backend = MagicMock(spec=IBMBackend)
- backend._instance = None
- backend.name = backend_name
- backends = [backend_name, backend]
- for cls in primitives:
- for backend in backends:
- with self.subTest(primitive=cls, backend=backend):
- options = {"backend": backend}
- with warnings.catch_warnings(record=True) as warn:
- warnings.simplefilter("always")
- cls(session=MagicMock(spec=MockSession), options=options)
- self.assertTrue(
- all(
- issubclass(one_warn.category, DeprecationWarning)
- for one_warn in warn
- )
- )
-
def test_runtime_options(self):
"""Test RuntimeOptions specified as primitive options."""
session = MagicMock(spec=MockSession)
@@ -170,13 +147,11 @@ def test_init_with_session_backend_str(self):
for cls in primitives:
with self.subTest(primitive=cls), patch(
"qiskit_ibm_runtime.base_primitive.QiskitRuntimeService"
- ) as mock_service:
- with self.assertWarns(DeprecationWarning):
- mock_service.reset_mock()
- mock_service.global_service = None
+ ):
+ with self.assertRaises(ValueError) as exc:
inst = cls(session=backend_name)
- mock_service.assert_called_once()
self.assertIsNone(inst.session)
+ self.assertIn("session must be of type Session or None", str(exc.exception))
def test_init_with_backend_instance(self):
"""Test initializing a primitive with a backend instance."""
@@ -198,9 +173,10 @@ def test_init_with_backend_instance(self):
runtime_options = service.run.call_args.kwargs["options"]
self.assertEqual(runtime_options["backend"], backend.name)
- with self.assertWarns(DeprecationWarning):
+ with self.assertRaises(ValueError) as exc:
inst = cls(session=backend)
self.assertIsNone(inst.session)
+ self.assertIn("session must be of type Session or None", str(exc.exception))
def test_init_with_backend_session(self):
"""Test initializing a primitive with both backend and session."""
@@ -413,22 +389,6 @@ def test_run_overwrite_runtime_options(self):
rt_options = kwargs["options"]
self._assert_dict_partially_equal(rt_options, options)
- def test_kwarg_options(self):
- """Test specifying arbitrary options."""
- session = MagicMock(spec=MockSession)
- primitives = [Sampler, Estimator]
- for cls in primitives:
- with self.subTest(primitive=cls):
- options = Options(foo="foo") # pylint: disable=unexpected-keyword-arg
- inst = cls(session=session, options=options)
- inst.run(self.qx, observables=self.obs)
- if sys.version_info >= (3, 8):
- inputs = session.run.call_args.kwargs["inputs"]
- else:
- _, kwargs = session.run.call_args
- inputs = kwargs["inputs"]
- self.assertEqual(inputs.get("foo"), "foo")
-
def test_run_kwarg_options(self):
"""Test specifying arbitrary options in run."""
session = MagicMock(spec=MockSession)
@@ -475,10 +435,6 @@ def test_set_options(self):
new_options = [
({"optimization_level": 2}, Options()),
({"optimization_level": 3, "shots": 200}, Options()),
- (
- {"shots": 300, "foo": "foo"},
- Options(foo="foo"), # pylint: disable=unexpected-keyword-arg
- ),
]
session = MagicMock(spec=MockSession)
@@ -547,7 +503,12 @@ def test_default_error_levels(self):
simulator={"noise_model": "foo"},
)
inst = cls(session=session, options=options)
- inst.run(self.qx, observables=self.obs)
+
+ if isinstance(inst, Estimator):
+ inst.run(self.qx, observables=self.obs)
+ else:
+ inst.run(self.qx)
+
if sys.version_info >= (3, 8):
inputs = session.run.call_args.kwargs["inputs"]
else:
@@ -681,7 +642,6 @@ def test_transpilation_options(self):
def test_max_execution_time_options(self):
"""Test transpilation options."""
options_dicts = [
- {"max_execution_time": Options._MIN_EXECUTION_TIME - 1},
{"max_execution_time": Options._MAX_EXECUTION_TIME + 1},
]
session = MagicMock(spec=MockSession)
@@ -701,7 +661,7 @@ def test_max_execution_time_options(self):
inst = cls(session=session, options=opts_dict)
inst.run(self.qx, observables=self.obs)
self.assertIn(
- "max_execution_time must be between 300 and 28800 seconds",
+ "max_execution_time must be below 28800 seconds",
str(exc.exception),
)
@@ -724,11 +684,12 @@ def test_raise_faulty_qubits(self):
estimator = Estimator(session=session)
with self.assertRaises(ValueError) as err:
- sampler.run(transpiled, skip_transpilation=True)
+ estimator.run(transpiled, observable, skip_transpilation=True)
self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception))
+ transpiled.measure_all()
with self.assertRaises(ValueError) as err:
- estimator.run(transpiled, observable, skip_transpilation=True)
+ sampler.run(transpiled, skip_transpilation=True)
self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception))
def test_raise_faulty_qubits_many(self):
@@ -753,11 +714,14 @@ def test_raise_faulty_qubits_many(self):
estimator = Estimator(session=session)
with self.assertRaises(ValueError) as err:
- sampler.run(transpiled, skip_transpilation=True)
+ estimator.run(transpiled, [observable, observable], skip_transpilation=True)
self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception))
+ for circ in transpiled:
+ circ.measure_all()
+
with self.assertRaises(ValueError) as err:
- estimator.run(transpiled, [observable, observable], skip_transpilation=True)
+ sampler.run(transpiled, skip_transpilation=True)
self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception))
def test_raise_faulty_edge(self):
@@ -779,12 +743,13 @@ def test_raise_faulty_edge(self):
estimator = Estimator(session=session)
with self.assertRaises(ValueError) as err:
- sampler.run(transpiled, skip_transpilation=True)
+ estimator.run(transpiled, observable, skip_transpilation=True)
self.assertIn("cx", str(err.exception))
self.assertIn(f"faulty edge {tuple(edge_qubits)}", str(err.exception))
+ transpiled.measure_all()
with self.assertRaises(ValueError) as err:
- estimator.run(transpiled, observable, skip_transpilation=True)
+ sampler.run(transpiled, skip_transpilation=True)
self.assertIn("cx", str(err.exception))
self.assertIn(f"faulty edge {tuple(edge_qubits)}", str(err.exception))
@@ -807,11 +772,12 @@ def test_faulty_qubit_not_used(self):
estimator = Estimator(session=session)
with patch.object(Session, "run") as mock_run:
- sampler.run(transpiled, skip_transpilation=True)
+ estimator.run(transpiled, observable, skip_transpilation=True)
mock_run.assert_called_once()
+ transpiled.measure_active()
with patch.object(Session, "run") as mock_run:
- estimator.run(transpiled, observable, skip_transpilation=True)
+ sampler.run(transpiled, skip_transpilation=True)
mock_run.assert_called_once()
def test_faulty_edge_not_used(self):
@@ -835,11 +801,12 @@ def test_faulty_edge_not_used(self):
estimator = Estimator(session=session)
with patch.object(Session, "run") as mock_run:
- sampler.run(transpiled, skip_transpilation=True)
+ estimator.run(transpiled, observable, skip_transpilation=True)
mock_run.assert_called_once()
+ transpiled.measure_all()
with patch.object(Session, "run") as mock_run:
- estimator.run(transpiled, observable, skip_transpilation=True)
+ sampler.run(transpiled, skip_transpilation=True)
mock_run.assert_called_once()
def test_no_raise_skip_transpilation(self):
@@ -864,11 +831,12 @@ def test_no_raise_skip_transpilation(self):
estimator = Estimator(session=session)
with patch.object(Session, "run") as mock_run:
- sampler.run(transpiled)
+ estimator.run(transpiled, observable)
mock_run.assert_called_once()
+ transpiled.measure_all()
with patch.object(Session, "run") as mock_run:
- estimator.run(transpiled, observable)
+ sampler.run(transpiled)
mock_run.assert_called_once()
def _update_dict(self, dict1, dict2):
diff --git a/test/unit/test_logger.py b/test/unit/test_logger.py
index 986ce3e5e8..3fa558ad9b 100644
--- a/test/unit/test_logger.py
+++ b/test/unit/test_logger.py
@@ -108,7 +108,6 @@ def test_valid_log_levels_mixed_casing(self):
"be {}.".format(logger.level, level_value),
)
- # TODO: NamedTemporaryFiles do not support name in Windows
@skipIf(os.name == "nt", "Test not supported in Windows")
def test_log_file(self):
"""Test setting up a logger by specifying a file and log level."""
diff --git a/test/unit/test_options.py b/test/unit/test_options.py
index e6432ec7cb..0b7ee595dc 100644
--- a/test/unit/test_options.py
+++ b/test/unit/test_options.py
@@ -12,7 +12,6 @@
"""Tests for Options class."""
-import warnings
from dataclasses import asdict
from ddt import data, ddt
@@ -62,51 +61,6 @@ def test_merge_options(self):
f"options={options}, combined={combined}",
)
- def test_merge_options_extra_fields(self):
- """Test merging options with extra fields."""
- options_vars = [
- (
- {
- "initial_layout": [2, 3],
- "transpilation": {"layout_method": "trivial"},
- "foo": "foo",
- },
- Options(foo="foo"), # pylint: disable=unexpected-keyword-arg
- ),
- (
- {
- "initial_layout": [3, 4],
- "transpilation": {"layout_method": "dense", "bar": "bar"},
- },
- Options(transpilation={"bar": "bar"}),
- ),
- (
- {
- "initial_layout": [1, 2],
- "foo": "foo",
- "transpilation": {"layout_method": "dense", "foo": "foo"},
- },
- Options( # pylint: disable=unexpected-keyword-arg
- foo="foo", transpilation={"foo": "foo"}
- ),
- ),
- ]
- for new_ops, expected in options_vars:
- with self.subTest(new_ops=new_ops):
- options = Options()
- combined = Options._merge_options(asdict(options), new_ops)
-
- # Make sure the values are equal.
- self.assertTrue(
- flat_dict_partially_equal(combined, new_ops),
- f"new_ops={new_ops}, combined={combined}",
- )
- # Make sure the structure didn't change.
- self.assertTrue(
- dict_keys_equal(combined, asdict(expected)),
- f"expected={expected}, combined={combined}",
- )
-
def test_runtime_options(self):
"""Test converting runtime options."""
full_options = RuntimeOptions(
@@ -137,14 +91,8 @@ def test_program_inputs(self):
environment={"log_level": "DEBUG"},
simulator={"noise_model": noise_model},
resilience={"noise_factors": (0, 2, 4)},
- foo="foo",
- bar="bar",
)
-
- with warnings.catch_warnings(record=True) as warn:
- warnings.simplefilter("always")
- inputs = Options._get_program_inputs(asdict(options))
- self.assertEqual(len(warn), 2)
+ inputs = Options._get_program_inputs(asdict(options))
expected = {
"execution": {"shots": 100, "noise_model": noise_model},
@@ -157,7 +105,6 @@ def test_program_inputs(self):
"resilience": {
"noise_factors": (0, 2, 4),
},
- "foo": "foo",
}
self.assertTrue(
dict_paritally_equal(inputs, expected),
@@ -191,6 +138,30 @@ def test_init_options_with_dictionary(self):
# Make sure the structure didn't change.
self.assertTrue(dict_keys_equal(asdict(Options()), options), f"options={options}")
+ def test_kwargs_options(self):
+ """Test specifying arbitrary options."""
+ with self.assertRaises(TypeError) as exc:
+ _ = Options(foo="foo") # pylint: disable=unexpected-keyword-arg
+ self.assertIn(
+ "__init__() got an unexpected keyword argument 'foo'",
+ str(exc.exception),
+ )
+
+ def test_backend_in_options(self):
+ """Test specifying backend in options."""
+ backend_name = "ibm_gotham"
+ backend = FakeManila()
+ backend._instance = None
+ backend.name = backend_name
+ backends = [backend_name, backend]
+ for backend in backends:
+ with self.assertRaises(TypeError) as exc:
+ _ = Options(backend=backend) # pylint: disable=unexpected-keyword-arg
+ self.assertIn(
+ "__init__() got an unexpected keyword argument 'backend'",
+ str(exc.exception),
+ )
+
def test_unsupported_options(self):
"""Test error on unsupported second level options"""
# defining minimal dict of options
diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py
index 02d5a6fbe7..2d32766f2e 100644
--- a/test/unit/test_sampler.py
+++ b/test/unit/test_sampler.py
@@ -12,7 +12,7 @@
"""Tests for sampler class."""
-from qiskit.circuit import QuantumCircuit
+from qiskit.test.reference_circuits import ReferenceCircuits
from qiskit_ibm_runtime import Sampler, Session
from ..ibm_test_case import IBMTestCase
@@ -28,11 +28,12 @@ def test_unsupported_values_for_sampler_options(self):
{"resilience_level": 2, "optimization_level": 3},
{"optimization_level": 4, "resilience_level": 1},
]
+
with Session(
service=FakeRuntimeService(channel="ibm_quantum", token="abc"),
backend="common_backend",
) as session:
- circuit = QuantumCircuit(1, 1)
+ circuit = ReferenceCircuits.bell()
for bad_opt in options_bad:
inst = Sampler(session=session)
with self.assertRaises(ValueError) as exc:
diff --git a/test/unit/test_session.py b/test/unit/test_session.py
index 446caacc47..9ac349c3f3 100644
--- a/test/unit/test_session.py
+++ b/test/unit/test_session.py
@@ -16,7 +16,6 @@
from qiskit_ibm_runtime import Session
from qiskit_ibm_runtime.ibm_backend import IBMBackend
-from qiskit_ibm_runtime.exceptions import IBMInputValueError
import qiskit_ibm_runtime.session as session_pkg
from .mock.fake_runtime_service import FakeRuntimeService
from ..ibm_test_case import IBMTestCase
@@ -76,18 +75,10 @@ def test_max_time(self):
def test_run_after_close(self):
"""Test running after session is closed."""
session = Session(service=MagicMock(), backend="ibm_gotham")
- session.close()
+ session.cancel()
with self.assertRaises(RuntimeError):
session.run(program_id="program_id", inputs={})
- def test_conflicting_backend(self):
- """Test passing in different backend through options."""
- service = MagicMock()
- backend = "ibm_gotham"
- session = Session(service=service, backend=backend)
- with self.assertRaises(IBMInputValueError):
- session.run(program_id="test", inputs={}, options={"backend": "different_backend"})
-
def test_run(self):
"""Test the run method."""
job = MagicMock()
@@ -114,7 +105,7 @@ def test_run(self):
_, kwargs = service.run.call_args
self.assertEqual(kwargs["program_id"], program_id)
self.assertDictEqual(kwargs["options"], {"backend": backend, **options})
- self.assertDictContainsSubset({"session_time": 42}, kwargs["options"])
+ self.assertTrue({"session_time": 42}.items() <= kwargs["options"].items())
self.assertDictEqual(kwargs["inputs"], inputs)
self.assertEqual(kwargs["session_id"], session_ids[idx])
self.assertEqual(kwargs["start_session"], start_sessions[idx])
@@ -135,7 +126,7 @@ 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={})
- session.close()
+ session.cancel()
self.assertFalse(session._active)
def test_default_backend(self):
@@ -163,3 +154,11 @@ def test_global_service(self):
service=FakeRuntimeService(channel="ibm_quantum", token="uvw"), backend="ibm_gotham"
) as session:
self.assertEqual(session._service._account.token, "uvw")
+
+ def test_session_from_id(self):
+ """Create session with given session_id"""
+ service = MagicMock()
+ session_id = "123"
+ session = Session.from_id(session_id=session_id, service=service)
+ session.run(program_id="foo", inputs={})
+ self.assertEqual(session.session_id, session_id)
diff --git a/test/utils.py b/test/utils.py
index 4ec9bc0af8..09e96ffd75 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -134,8 +134,7 @@ def wait_for_status(job, status, poll_time=1, time_out=20):
def get_real_device(service):
"""Get a real device for the service."""
try:
- # TODO: Remove filters when ibmq_berlin is removed
- return service.least_busy(simulator=False, filters=lambda b: b.name != "ibmq_berlin").name
+ return service.least_busy(simulator=False).name
except QiskitBackendNotFoundError:
raise unittest.SkipTest("No real device") # cloud has no real device