diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7601abde1..112103463 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11'] + python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] os: [ "macOS-latest", "ubuntu-latest", "windows-latest" ] env: LOG_LEVEL: DEBUG diff --git a/DEPRECATION.md b/DEPRECATION.md new file mode 100644 index 000000000..126daebdb --- /dev/null +++ b/DEPRECATION.md @@ -0,0 +1,203 @@ +# Deprecation Policy + +Many users and other packages depend on this package. We must +make sure that whenever we make changes to the code, we give users ample time to +adjust without breaking code that they have already written. + +Most importantly: *do not* change any interface that is public-facing unless we +absolutely have to. Adding things is ok, taking things away is annoying for +users but can be handled reasonably with plenty notice, but changing behavior +generally means users cannot write code that will work with two subsequent +versions of qiskit-ibm-runtime, which is not acceptable. + +Beware that users will often be using functions, classes and methods that we, +the Qiskit developers, may consider internal or not widely used. Do not make +assumptions that "this is buried, so nobody will be using it"; if it is public, +it is subject to the policy. The only exceptions here are functions and modules +that are explicitly internal, *i.e.* those whose names begin with a leading +underscore (`_`). + +The guiding principles are: + +- we must not remove or change code without active warnings for at least three + months or two complete version cycles; + +- there must always be a way to achieve valid goals that does not issue any + warnings; + +- never assume that a function that isn't explicitly internal isn't in use; + +- all deprecations, changes and removals are considered API changes, and can + only occur in minor releases not patch releases, per the [stable branch policy](https://github.com/Qiskit/qiskit/blob/main/MAINTAINING.md#stable-branch-policy). + + +## Removing a feature + +When removing a feature (for example a class, function or function parameter), +we will follow this procedure: + +- A deprecation warning must be issued prior to any removal. The warning + must indicate what the alternative path is, and the alternative path + must be in place when the warning is issued. When a feature is + deprecated, add a + release note with a `deprecations` section listing all deprecated paths, + their alternatives, and the reason for deprecation. [Update the tests to test the warnings](#testing-deprecated-functionality). + + *Reason*: we need to give people time to swap over without breaking their + code as soon as they upgrade. + +- Set a removal date for the old feature, and remove it (and the warnings) when + reached. This must be at least three months after the version with the + warnings was first released, and cannot be the minor version immediately + after the warnings. Add an `upgrade` release note that lists all the + removals. For example, if the alternative path was provided + and the warnings were added in `0.20.0`, the earliest version for removal + is `0.22.0`, even if `0.21.0` was released more than three months after + `0.20.0`. + + **Note: These are _minimum_** requirements. For removal of significant or core features, give + users at least an extra minor version if not longer.** + + *Reason*: there needs to be time for users to see these messages, and to give + them time to adjust. Not all users will update their version of qiskit-ibm-runtime + immediately, and some may skip minor versions. + +When a feature is marked as deprecated it is slated for removal, but users +should still be able to rely on it to work correctly. We consider a feature +marked "deprecated" as frozen; we commit to maintaining it with critical bug +fixes until it is removed, but we won't merge new functionality to it. + + +## Changing behavior + + +Changing behavior without a removal is particularly difficult to manage, because +we need to have both options available for two versions, and be able to issue +warnings. For example, changing the type of the return value from a function +will almost invariably involve making an API break, which is frustrating for +users and makes it difficult for them to use this package. + +The best solution here is often to make a new function, and then use [the procedures for removal](#removing-features) above. + +If you absolutely must change the behavior of existing code (other than fixing +bugs), you will need to use your best judgment to apply the guiding principles +at the top of this document. The most appropriate warning for behavioral +changes is usually `FutureWarning`. Some possibilities for how to effect a +change: + +- If you are changing the default behavior of a function, consider adding a + keyword argument to select between old and new behaviors. When it comes time, + you can issue a `FutureWarning` if the keyword argument is not given + (*e.g.* if it is `None`), saying that the new value will soon become the + default. You will need to go through the normal deprecation period for + removing this keyword argument after you have made the behavior change. This + will take at least six months to go through both cycles. + +- If you need to change the return type of a function, consider adding a new + function that returns the new type, and then follow the procedures for + deprecating the old function. + +- If you need to accept a new input that you cannot distinguish from an existing + possibility because of its type, consider letting it be passed by a different + keyword argument, or add a second function that only accepts the new form. + + + +## Issuing deprecation warnings + +The proper way to raise a deprecation warning is to use the `@deprecate_function` decorator, and +the `deprecate_arguments` and `issue_deprecation_msg` functions +from `qiskit_ibm_runtime.utils.deprecation`. +These will generate a standardized message and ensure an alternative path is specified. + +Usually, you should set `remedy: str` with the format `"Instead, use ..."` so that +people know how to migrate. Read those functions' docstrings for additional arguments like +`version: str`. + +If the functions in `qiskit_ibm_runtime.utils.deprecation` cannot handle your use case, consider improving +them. Otherwise, you can directly call the `warn` function +from the [warnings module in the Python standard library](https://docs.python.org/3/library/warnings.html), +using the category `DeprecationWarning`. For example: + +```python +import warnings + +def deprecated_function(): + warnings.warn( + "The function qiskit.deprecated_function() is deprecated since " + "qiskit-ibm-runtime 0.14.0, and will be removed 3 months or more later. " + "Instead, you should use qiskit.other_function().", + category=DeprecationWarning, + stacklevel=2, + ) + # ... the rest of the function ... + +``` + +Make sure you include the version of the package that introduced the deprecation +warning (so maintainers can easily see when it is valid to remove it), and what +the alternative path is. + +Take note of the `stacklevel` argument. This controls which function is +accused of being deprecated. Setting `stacklevel=1` means the +warning will blame the `warn` function itself, while `stacklevel=2` (the default) will +correctly blame the containing function. It is unusual to set this to anything +other than `2`, but can be useful if you use a helper function to issue the +same warning in multiple places. + + +## Testing deprecated functionality + +Whenever you add deprecation warnings, you will need to update tests involving +the functionality. The test suite should fail otherwise, because of the new +warnings. We must continue to test deprecated functionality throughout the +deprecation period, to ensure that it still works. + +To update the tests, you need to wrap each call of deprecated behavior in its +own assertion block. For subclasses of `unittest.TestCase` (which all Qiskit +test cases are), this is done by: + + +```python +class MyTestSuite(QiskitTestCase): + def test_deprecated_function(self): + with self.assertWarns(DeprecationWarning): + output = deprecated_function() + # ... do some things with output ... + self.assertEqual(output, expected) +``` + +## Documenting deprecations and breaking changes + +It is important to warn the user when your breaking changes are coming. + +Make sure to update the docstring of the function, so that it shows up in +API reference. + +You can add a [Sphinx deprecated directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-deprecated): + + +```python +def deprecated_function(): + """ + Short description of the deprecated function. + + .. deprecated:: 0.14.0 + The function qiskit_ibm_runtime.deprecated_function() is deprecated since + qiskit_ibm_runtime 0.14.0, and will be removed 3 months or more later. + Instead, you should use qiskit_ibm_runtime.other_function(). + + + """ + # ... the rest of the function ... +``` + + +You should also document the deprecation in the changelog by using Reno. Explain the deprecation +and how to migrate. + +In particular situations where a deprecation or change might be a major disruptor for users, a +*migration guide* might be needed. Please write these guides in Qiskit's documentation at +https://github.com/Qiskit/documentation/tree/main/docs/api/migration-guides. Once +the migration guide is written and published, deprecation +messages and documentation should link to it. diff --git a/docs/conf.py b/docs/conf.py index 769cfb73a..621a3a38d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.19.1' +release = '0.20.1' # -- General configuration --------------------------------------------------- diff --git a/docs/tutorials/Error-Suppression-and-Error-Mitigation.ipynb b/docs/tutorials/Error-Suppression-and-Error-Mitigation.ipynb index ba4a8ce40..6c93684b1 100644 --- a/docs/tutorials/Error-Suppression-and-Error-Mitigation.ipynb +++ b/docs/tutorials/Error-Suppression-and-Error-Mitigation.ipynb @@ -1953,10 +1953,9 @@ } ], "source": [ - "from qiskit.tools import jupyter\n", + "import qiskit\n", "\n", - "%qiskit_version_table\n", - "%qiskit_copyright" + "qiskit.version.get_version_info()" ] } ], diff --git a/docs/tutorials/how-to-getting-started-with-estimator.ipynb b/docs/tutorials/how-to-getting-started-with-estimator.ipynb index df2c3a9a1..9556903bd 100644 --- a/docs/tutorials/how-to-getting-started-with-estimator.ipynb +++ b/docs/tutorials/how-to-getting-started-with-estimator.ipynb @@ -46,7 +46,7 @@ } }, "source": [ - "[Primitives](https://qiskit.org/ecosystem/ibm-runtime/primitives.html) are core functions that make it easier to build modular algorithms and applications. \n", + "[Primitives](https://docs.quantum.ibm.com/run/primitives) are core functions that make it easier to build modular algorithms and applications. \n", "\n", "The initial release of Qiskit Runtime includes two primitives:\n", "\n", @@ -115,7 +115,7 @@ } }, "source": [ - "For a basic expectation value calculation you will need at least one quantum circuit to prepare our system in a precise quantum state for study. Our examples all have circuits in them, but you can use Qiskit to create your own. To learn how to create circuits by using Qiskit, see the [Circuit basics tutorial](https://qiskit.org/documentation/tutorials/circuits/01_circuit_basics.html)." + "For a basic expectation value calculation you will need at least one quantum circuit to prepare our system in a precise quantum state for study. Our examples all have circuits in them, but you can use Qiskit to create your own. To learn how to create circuits by using Qiskit, see [the documentation](https://docs.quantum.ibm.com/build/circuit-construction)." ] }, { @@ -167,7 +167,7 @@ } }, "source": [ - "You will also need at least one observable to measure. Observables represent physical properties of a quantum system (such as energy or spin), and allow said properties to be measured (such as their expectation values) for a given state of our system. For simplicity, you can use the [SparsePauliOp class](https://qiskit.org/documentation/stubs/qiskit.quantum_info.SparsePauliOp.html#qiskit.quantum_info.SparsePauliOp) in Qiskit to define them, as illustrated in the following example." + "You will also need at least one observable to measure. Observables represent physical properties of a quantum system (such as energy or spin), and allow said properties to be measured (such as their expectation values) for a given state of our system. For simplicity, you can use the [SparsePauliOp class](https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.SparsePauliOp) in Qiskit to define them, as illustrated in the following example." ] }, { @@ -218,7 +218,7 @@ }, "source": [ "\n", - "The next step is to create an instance of an `Estimator` class, which can be any of the subclasses that comply with the base specification. For simplicity, we will use Qiskit Terra's `qiskit.primitives.Estimator` class, based on the [Statevector construct](https://qiskit.org/documentation/stubs/qiskit.quantum_info.Statevector.html?highlight=statevector#qiskit.quantum_info.Statevector) (algebraic simulation).\n", + "The next step is to create an instance of an `Estimator` class, which can be any of the subclasses that comply with the base specification. For simplicity, we will use Qiskit Terra's `qiskit.primitives.Estimator` class, based on the [Statevector construct](https://docs.quantum.ibm.com/api/qiskit/qiskit.quantum_info.Statevector) (algebraic simulation).\n", "" ] }, @@ -551,7 +551,7 @@ "source": [ "Since Qiskit Runtime `Estimator` is a managed service, you will first need to initialize your account. You can then select the simulator or real backend you want to use to calculate the expectation value.\n", "\n", - "Follow the steps in the [getting started guide](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/getting_started.html) if you don't already have an account set up." + "Follow the steps in the [set up documentation](https://docs.quantum.ibm.com/start/setup-channel) if you don't already have an account set up." ] }, { @@ -788,7 +788,7 @@ } }, "source": [ - "You can use the [Options](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.options.Options.html#qiskit_ibm_runtime.options.Options) class to specify different options." + "You can use the [Options](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.options.Options) class to specify different options." ] }, { @@ -1044,7 +1044,7 @@ "\n", "If you don't specify a timeout value, it is set to the initial job's maximum execution time and is the smaller of these values:\n", "\n", - "- The system limit (see [What is the maximum execution time for a Qiskit Runtime job?](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/faqs/max_execution_time.html)).\n", + "- The system limit (see [What is the maximum execution time for a Qiskit Runtime job?](https://docs.quantum.ibm.com/run/max-execution-time)).\n", "- The `max_execution_time` defined by the program.\n", "\n", "After this time limit is reached, the session is permanently closed." @@ -1366,9 +1366,9 @@ "id": "bf0fe74a", "metadata": {}, "source": [ - "You can find more details about the ``Estimator`` methods in the [Estimator API reference](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.Estimator.html#qiskit_ibm_runtime.Estimator).\n", + "You can find more details about the ``Estimator`` methods in the [Estimator API reference](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.Estimator).\n", "\n", - "And all the available options in the [Options API reference](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.options.Options.html#qiskit_ibm_runtime.options.Options)." + "And all the available options in the [Options API reference](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.options.Options)." ] }, { @@ -1426,10 +1426,9 @@ } ], "source": [ - "from qiskit.tools.jupyter import *\n", + "import qiskit\n", "\n", - "%qiskit_version_table\n", - "%qiskit_copyright" + "qiskit.version.get_version_info()" ] } ], diff --git a/docs/tutorials/how-to-getting-started-with-sampler.ipynb b/docs/tutorials/how-to-getting-started-with-sampler.ipynb index 750d37421..f80f5948a 100644 --- a/docs/tutorials/how-to-getting-started-with-sampler.ipynb +++ b/docs/tutorials/how-to-getting-started-with-sampler.ipynb @@ -46,7 +46,7 @@ } }, "source": [ - "[Primitives](https://qiskit.org/ecosystem/ibm-runtime/primitives.html) are core functions that make it easier to build modular algorithms and applications. \n", + "[Primitives](https://docs.quantum.ibm.com/run/primitives) are core functions that make it easier to build modular algorithms and applications. \n", "\n", "The initial release of Qiskit Runtime includes two primitives:\n", "\n", @@ -115,7 +115,7 @@ } }, "source": [ - "You will need at least one quantum circuit to prepare our system in a precise quantum state for study. Our examples all have circuits in them, but you can use Qiskit to create your own. To learn how to create circuits by using Qiskit, see the [Circuit basics tutorial](https://qiskit.org/documentation/tutorials/circuits/01_circuit_basics.html)." + "You will need at least one quantum circuit to prepare our system in a precise quantum state for study. Our examples all have circuits in them, but you can use Qiskit to create your own. To learn how to create circuits by using Qiskit, see the [circuit construction documentation](https://docs.quantum.ibm.com/build/circuit-construction)." ] }, { @@ -492,7 +492,7 @@ "source": [ "Since Qiskit Runtime `Sampler` is a managed service, you will first need to initialize your account. You can then select the simulator or real backend you want to use to calculate the expectation value.\n", "\n", - "Follow the steps in the [getting started guide](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/getting_started.html) if you don't already have an account set up." + "Follow the steps in the [setup guide](https://docs.quantum.ibm.com/start/setup-channel) if you don't already have an account set up." ] }, { @@ -719,7 +719,7 @@ } }, "source": [ - "You can use the [Options](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.options.Options.html#qiskit_ibm_runtime.options.Options) class to specify different options." + "You can use the [Options](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.options.Options) class to specify different options." ] }, { @@ -973,7 +973,7 @@ "\n", "If you don't specify a timeout value, it is set to the initial job's maximum execution time and is the smaller of these values:\n", "\n", - "- The system limit (see [What is the maximum execution time for a Qiskit Runtime job?](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/faqs/max_execution_time.html)).\n", + "- The system limit (see [What is the maximum execution time for a Qiskit Runtime job?](https://docs.quantum.ibm.com/run/max-execution-time)).\n", "- The `max_execution_time` defined by the program.\n", "\n", "After this time limit is reached, the session is permanently closed." @@ -1305,9 +1305,9 @@ "id": "e5827c27", "metadata": {}, "source": [ - "You can find more details about the ``Sampler`` methods in the [Sampler API reference](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.Sampler.html#qiskit_ibm_runtime.Sampler).\n", + "You can find more details about the ``Sampler`` methods in the [Sampler API reference](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.Sampler).\n", "\n", - "And all the available options in the [Options API reference](https://qiskit.org/documentation/partners/qiskit_ibm_runtime/stubs/qiskit_ibm_runtime.options.Options.html#qiskit_ibm_runtime.options.Options)." + "And all the available options in the [Options API reference](https://docs.quantum.ibm.com/api/qiskit-ibm-runtime/qiskit_ibm_runtime.options.Options)." ] }, { @@ -1365,10 +1365,9 @@ } ], "source": [ - "from qiskit.tools.jupyter import *\n", + "import qiskit\n", "\n", - "%qiskit_version_table\n", - "%qiskit_copyright" + "qiskit.version.get_version_info()" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 65b63d47c..ebb26241a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 100 -target-versions = ['py38', 'py39', 'py310', 'py311'] +target-versions = ['py38', 'py39', 'py310', 'py311', 'py312'] diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index 41915c799..847e9aef6 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.19.1 +0.20.1 diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index ddbd41771..82b1d6814 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -230,6 +230,23 @@ def job_metadata(self, job_id: str) -> Dict[str, Any]: """ return self._api.program_job(job_id).metadata() + def create_session( + self, + backend: Optional[str] = None, + instance: Optional[str] = None, + max_time: Optional[int] = None, + channel: Optional[str] = None, + mode: Optional[str] = None, + ) -> Dict[str, Any]: + """Create a session. + + Args: + mode: Execution mode. + """ + return self._api.runtime_session(session_id=None).create( + backend, instance, max_time, channel, mode + ) + def cancel_session(self, session_id: str) -> None: """Close all jobs in the runtime session. diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index 354fc4e07..bde211df2 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -49,7 +49,7 @@ def program_job(self, job_id: str) -> "ProgramJob": """ return ProgramJob(self.session, job_id) - def runtime_session(self, session_id: str) -> "RuntimeSession": + def runtime_session(self, session_id: str = None) -> "RuntimeSession": """Return an adapter for the session. Args: diff --git a/qiskit_ibm_runtime/api/rest/runtime_session.py b/qiskit_ibm_runtime/api/rest/runtime_session.py index fb4b3b944..87f66c076 100644 --- a/qiskit_ibm_runtime/api/rest/runtime_session.py +++ b/qiskit_ibm_runtime/api/rest/runtime_session.py @@ -12,7 +12,7 @@ """Runtime Session REST adapter.""" -from typing import Dict, Any +from typing import Dict, Any, Optional from .base import RestAdapterBase from ..session import RetrySession from ..exceptions import RequestsApiError @@ -35,7 +35,34 @@ def __init__(self, session: RetrySession, session_id: str, url_prefix: str = "") 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)) + if not session_id: + super().__init__(session, "{}/sessions".format(url_prefix)) + else: + super().__init__(session, "{}/sessions/{}".format(url_prefix, session_id)) + + def create( + self, + backend: Optional[str] = None, + instance: Optional[str] = None, + max_time: Optional[int] = None, + channel: Optional[str] = None, + mode: Optional[str] = None, + ) -> Dict[str, Any]: + """Create a session""" + url = self.get_url("self") + payload = {} + if mode: + payload["mode"] = mode + if backend: + payload["backend"] = backend + if instance: + payload["instance"] = instance + if max_time: + if channel == "ibm_quantum": + payload["max_session_ttl"] = max_time # type: ignore[assignment] + else: + payload["max_ttl"] = max_time # type: ignore[assignment] + return self.session.post(url, json=payload).json() def cancel(self) -> None: """Cancel all jobs in the session.""" diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index c7bb67fb9..33c928d30 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -29,6 +29,7 @@ from .runtime_job import RuntimeJob from .ibm_backend import IBMBackend from .utils.default_session import get_cm_session +from .utils.utils import validate_isa_circuits from .constants import DEFAULT_DECODERS from .qiskit_runtime_service import QiskitRuntimeService @@ -137,6 +138,14 @@ def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJo Returns: Submitted job. """ + if ( + self._backend + and isinstance(self._backend, IBMBackend) + and isinstance(self._backend._service, QiskitRuntimeService) + and hasattr(self._backend, "target") + ): + validate_isa_circuits(primitive_inputs["circuits"], self._backend.target) + combined = Options._merge_options(self._options, user_kwargs) if self._backend: diff --git a/qiskit_ibm_runtime/batch.py b/qiskit_ibm_runtime/batch.py index 6947ddc81..18663ee09 100644 --- a/qiskit_ibm_runtime/batch.py +++ b/qiskit_ibm_runtime/batch.py @@ -12,10 +12,27 @@ """Qiskit Runtime batch mode.""" +from typing import Optional, Union +from qiskit_ibm_runtime import QiskitRuntimeService +from .ibm_backend import IBMBackend + from .session import Session class Batch(Session): """Class for creating a batch mode in Qiskit Runtime.""" - pass + def __init__( + self, + service: Optional[QiskitRuntimeService] = None, + backend: Optional[Union[str, IBMBackend]] = None, + max_time: Optional[Union[int, str]] = None, + ): + super().__init__(service=service, backend=backend, max_time=max_time) + + def _create_session(self) -> str: + """Create a session.""" + session = self._service._api_client.create_session( + self._backend, self._instance, self._max_time, self._service.channel, "batch" + ) + return session.get("id") diff --git a/qiskit_ibm_runtime/fake_provider/__init__.py b/qiskit_ibm_runtime/fake_provider/__init__.py index c35804a29..a61d0ddba 100644 --- a/qiskit_ibm_runtime/fake_provider/__init__.py +++ b/qiskit_ibm_runtime/fake_provider/__init__.py @@ -36,7 +36,7 @@ from qiskit import QuantumCircuit from qiskit import transpile - from qiskit.tools.visualization import plot_histogram + from qiskit.visualization import plot_histogram from qiskit_ibm_runtime.fake_provider import FakeManilaV2 # Get a fake backend from the fake provider diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index accf571d0..3f3d1ba05 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -756,16 +756,11 @@ def _runtime_run( warnings.warn( "A Primitive session is open but Backend.run() jobs will not be run within this session" ) + session_id = None if self._session: if not self._session.active: raise RuntimeError(f"The session {self._session.session_id} is closed.") session_id = self._session.session_id - start_session = session_id is None - max_session_time = self._session._max_time - else: - session_id = None - start_session = False - max_session_time = None log_level = getattr(self.options, "log_level", None) # temporary try: @@ -777,15 +772,11 @@ def _runtime_run( log_level=log_level, job_tags=job_tags, session_id=session_id, - start_session=start_session, - session_time=max_session_time, + start_session=False, image=image, ) except RequestsApiError as ex: raise IBMBackendApiError("Error submitting job: {}".format(str(ex))) from ex - session_id = response.get("session_id", None) - if self._session: - self._session._session_id = session_id try: job = RuntimeJob( backend=self, @@ -831,7 +822,13 @@ def _get_run_config(self, program_id: str, **kwargs: Any) -> Dict: def open_session(self, max_time: Optional[Union[int, str]] = None) -> ProviderSession: """Open session""" - self._session = ProviderSession(max_time=max_time) + if not self._configuration.simulator: + new_session = self._service._api_client.create_session( + self.name, self._instance, max_time, self._service.channel + ) + self._session = ProviderSession(max_time=max_time, session_id=new_session.get("id")) + else: + self._session = ProviderSession() return self._session @property diff --git a/qiskit_ibm_runtime/provider_session.py b/qiskit_ibm_runtime/provider_session.py index dad3dc407..5568cd6b4 100644 --- a/qiskit_ibm_runtime/provider_session.py +++ b/qiskit_ibm_runtime/provider_session.py @@ -57,6 +57,7 @@ class Session: def __init__( self, max_time: Optional[Union[int, str]] = None, + session_id: Optional[str] = None, ): """Session constructor. @@ -66,13 +67,13 @@ def __init__( 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 `system imposed maximum - `_. + `_. Raises: ValueError: If an input value is invalid. """ self._instance = None - self._session_id: Optional[str] = None + self._session_id = session_id self._active = True self._max_time = ( diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index cac6221d9..21e9307a4 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -657,7 +657,7 @@ def session_id(self) -> str: """Session ID. Returns: - Job ID of the first job in a runtime session. + Session ID. None if the backend is a simulator. """ if not self._session_id: response = self._api_client.job_get(job_id=self.job_id()) diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index cb9da574d..1c2520059 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -15,7 +15,6 @@ from typing import Dict, Optional, Type, Union, Callable, Any from types import TracebackType from functools import wraps -from threading import Lock from qiskit_ibm_runtime import QiskitRuntimeService from .runtime_job import RuntimeJob @@ -110,7 +109,6 @@ def __init__( if QiskitRuntimeService.global_service is None else QiskitRuntimeService.global_service ) - else: self._service = service @@ -118,13 +116,7 @@ def __init__( raise ValueError('"backend" is required for ``ibm_quantum`` channel.') self._instance = None - if isinstance(backend, IBMBackend): - self._instance = backend._instance - backend = backend.name - self._backend = backend - self._setup_lock = Lock() - self._session_id: Optional[str] = None self._active = True self._max_time = ( max_time @@ -132,6 +124,28 @@ def __init__( else hms_to_seconds(max_time, "Invalid max_time value: ") ) + if isinstance(backend, IBMBackend): + self._instance = backend._instance + sim_backend = backend.configuration().simulator + backend = backend.name + else: + backend_obj = self._service.backend(backend) + self._instance = backend_obj._instance + sim_backend = backend_obj.configuration().simulator + self._backend = backend + + if not sim_backend: + self._session_id = self._create_session() + else: + self._session_id = None + + def _create_session(self) -> str: + """Create a session.""" + session = self._service._api_client.create_session( + self._backend, self._instance, self._max_time, self._service.channel + ) + return session.get("id") + @_active_session def run( self, @@ -162,29 +176,15 @@ def run( options["backend"] = self._backend - if not self._session_id: - # Make sure only one thread can send the session starter job. - self._setup_lock.acquire() - # TODO: What happens if session max time != first job max time? - # Use session max time if this is first job. - options["session_time"] = self._max_time - - try: - job = self._service.run( - program_id=program_id, - options=options, - inputs=inputs, - session_id=self._session_id, - start_session=self._session_id is None, - callback=callback, - result_decoder=result_decoder, - ) - - if self._session_id is None: - self._session_id = job.job_id() - finally: - if self._setup_lock.locked(): - self._setup_lock.release() + job = self._service.run( + program_id=program_id, + options=options, + inputs=inputs, + session_id=self._session_id, + start_session=False, + callback=callback, + result_decoder=result_decoder, + ) if self._backend is None: self._backend = job.backend().name @@ -278,11 +278,11 @@ def details(self) -> Optional[Dict[str, Any]]: return None @property - def session_id(self) -> str: + def session_id(self) -> Optional[str]: """Return the session ID. Returns: - Session ID. None until a job is submitted. + Session ID. None if the backend is a simulator. """ return self._session_id diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py index 90308b73c..650ef4593 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py @@ -55,7 +55,7 @@ # Generate the main Qiskit transpile passes. pm = generate_preset_pass_manager(optimization_level=1, backend=backend) # Configure the as-late-as-possible scheduling pass - pm.scheduling = PassManager([ALAPScheduleAnalysis(durations), PadDelay()]) + pm.scheduling = PassManager([ALAPScheduleAnalysis(durations), PadDelay(durations)]) qr = QuantumRegister(3) crz = ClassicalRegister(1, name="crz") @@ -162,7 +162,7 @@ [ ConvertConditionsToIfOps(), ALAPScheduleAnalysis(durations), - PadDelay(), + PadDelay(durations), ] ) diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py index 1232750a5..712a05c81 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/block_base_padder.py @@ -28,7 +28,7 @@ from qiskit.circuit.delay import Delay from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.converters import dag_to_circuit -from qiskit.dagcircuit import DAGCircuit, DAGNode +from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOpNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -220,6 +220,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, + enable_dd: bool = False, ) -> None: """Interleave instruction sequence in between two nodes. @@ -262,7 +263,7 @@ def _get_node_duration(self, node: DAGNode) -> int: cal_key = tuple(indices), tuple(float(p) for p in node.op.params) duration = self._block_dag.calibrations[node.op.name][cal_key].duration else: - duration = node.op.duration + duration = self._durations.get(node.op, indices, unit="dt") if isinstance(duration, ParameterExpression): raise TranspilerError( @@ -346,7 +347,17 @@ def _visit_block( self._conditional_block = False for node in block_order_op_nodes(block): - self._visit_node(node) + enable_dd_node = False + # add DD if node is a named barrier + if ( + isinstance(node, DAGOpNode) + and isinstance(node.op, Barrier) + and getattr(self, "_dd_barrier", None) + and node.op.label + and self._dd_barrier in node.op.label + ): + enable_dd_node = True + self._visit_node(node, enable_dd=enable_dd_node) # Terminate the block to pad it after scheduling. prev_block_duration = self._block_duration @@ -366,7 +377,7 @@ def _visit_block( return new_block_dag - def _visit_node(self, node: DAGNode) -> None: + def _visit_node(self, node: DAGNode, enable_dd: bool = False) -> None: if isinstance(node.op, ControlFlowOp): if isinstance(node.op, IfElseOp): self._visit_if_else_op(node) @@ -376,7 +387,7 @@ def _visit_node(self, node: DAGNode) -> None: if isinstance(node.op, Delay): self._visit_delay(node) else: - self._visit_generic(node) + self._visit_generic(node, enable_dd=enable_dd) else: raise TranspilerError( f"Operation {repr(node)} is likely added after the circuit is scheduled. " @@ -501,7 +512,7 @@ def _visit_delay(self, node: DAGNode) -> None: t1 = t0 + self._get_node_duration(node) # pylint: disable=invalid-name self._block_duration = max(self._block_duration, t1) - def _visit_generic(self, node: DAGNode) -> None: + def _visit_generic(self, node: DAGNode, enable_dd: bool = False) -> None: """Visit a generic node to pad.""" # Note: t0 is the relative time with respect to the current block specified # by block_idx. @@ -529,6 +540,7 @@ def _visit_generic(self, node: DAGNode) -> None: if t0 - self._idle_after.get(bit, 0) > 0: # Find previous node on the wire, i.e. always the latest node on the wire prev_node = next(self._block_dag.predecessors(self._block_dag.output_map[bit])) + self._pad( block_idx=block_idx, qubit=bit, @@ -536,6 +548,7 @@ def _visit_generic(self, node: DAGNode) -> None: t_end=t0, next_node=node, prev_node=prev_node, + enable_dd=enable_dd, ) self._idle_after[bit] = t1 diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py index 77d893f57..a22687cf1 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py @@ -126,6 +126,7 @@ def __init__( coupling_map: CouplingMap = None, alt_spacings: Optional[Union[List[List[float]], List[float]]] = None, schedule_idle_qubits: bool = False, + dd_barrier: Optional[str] = None, ): """Dynamical decoupling initializer. @@ -181,6 +182,8 @@ def __init__( schedule_idle_qubits: Set to true if you'd like a delay inserted on idle qubits. This is useful for timeline visualizations, but may cause issues for execution on large backends. + dd_barrier: only apply DD to delays terminating with a barrier + whose label contains the specified string Raises: TranspilerError: When invalid DD sequence is specified. TranspilerError: When pulse gate with the duration which is @@ -203,6 +206,7 @@ def __init__( self._alignment = pulse_alignment self._coupling_map = coupling_map self._coupling_coloring = None + self._dd_barrier = dd_barrier if spacings is not None: try: @@ -371,6 +375,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, + enable_dd: bool = False, ) -> None: # This routine takes care of the pulse alignment constraint for the DD sequence. # Note that the alignment constraint acts on the t0 of the DAGOpNode. @@ -418,8 +423,9 @@ def _pad( ): self._dirty_qubits.remove(qubit) - if qubit not in self._dirty_qubits: - # Previous node is the start edge or reset, i.e. qubit is ground state. + if qubit not in self._dirty_qubits or (self._dd_barrier and not enable_dd): + # Previous node is the start edge or reset, i.e. qubit is ground state; + # or dd to be applied before named barrier only self._apply_scheduled_op( block_idx, t_start, Delay(time_interval, self._block_dag.unit), qubit ) diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/pad_delay.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/pad_delay.py index fd61f8c49..9c478a816 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/pad_delay.py @@ -15,6 +15,7 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGNode, DAGOutNode +from qiskit.transpiler.instruction_durations import InstructionDurations from .block_base_padder import BlockBasePadder @@ -50,16 +51,23 @@ class PadDelay(BlockBasePadder): See :class:`BlockBasePadder` pass for details. """ - def __init__(self, fill_very_end: bool = True, schedule_idle_qubits: bool = False): + def __init__( + self, + durations: InstructionDurations, + fill_very_end: bool = True, + schedule_idle_qubits: bool = False, + ): """Create new padding delay pass. Args: + durations: Durations of instructions to be used in scheduling. fill_very_end: Set ``True`` to fill the end of circuit with delay. schedule_idle_qubits: Set to true if you'd like a delay inserted on idle qubits. This is useful for timeline visualizations, but may cause issues for execution on large backends. """ super().__init__(schedule_idle_qubits=schedule_idle_qubits) + self._durations = durations self.fill_very_end = fill_very_end def _pad( @@ -70,6 +78,7 @@ def _pad( t_end: int, next_node: DAGNode, prev_node: DAGNode, + enable_dd: bool = False, ) -> None: if not self.fill_very_end and isinstance(next_node, DAGOutNode): return diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/scheduler.py index b18ee32c6..c13d72418 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/scheduler.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/scheduler.py @@ -216,7 +216,8 @@ def _get_duration(self, node: DAGNode, dag: Optional[DAGCircuit] = None) -> int: duration = dag.calibrations[node.op.name][cal_key].duration node.op.duration = duration else: - duration = node.op.duration + # map to outer dag to get the appropriate durations + duration = self._durations.get(node.op, indices, unit="dt") if isinstance(duration, ParameterExpression): raise TranspilerError( diff --git a/qiskit_ibm_runtime/utils/backend_decoder.py b/qiskit_ibm_runtime/utils/backend_decoder.py index d02fb90fb..369be0791 100644 --- a/qiskit_ibm_runtime/utils/backend_decoder.py +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -95,15 +95,16 @@ def properties_from_server_data(properties: Dict) -> BackendProperties: Returns: A ``BackendProperties`` instance. """ - properties["last_update_date"] = dateutil.parser.isoparse(properties["last_update_date"]) - for qubit in properties["qubits"]: - for nduv in qubit: - nduv["date"] = dateutil.parser.isoparse(nduv["date"]) - for gate in properties["gates"]: - for param in gate["parameters"]: - param["date"] = dateutil.parser.isoparse(param["date"]) - for gen in properties["general"]: - gen["date"] = dateutil.parser.isoparse(gen["date"]) + if isinstance(properties["last_update_date"], str): + properties["last_update_date"] = dateutil.parser.isoparse(properties["last_update_date"]) + for qubit in properties["qubits"]: + for nduv in qubit: + nduv["date"] = dateutil.parser.isoparse(nduv["date"]) + for gate in properties["gates"]: + for param in gate["parameters"]: + param["date"] = dateutil.parser.isoparse(param["date"]) + for gen in properties["general"]: + gen["date"] = dateutil.parser.isoparse(gen["date"]) properties = utc_to_local_all(properties) return BackendProperties.from_dict(properties) diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index 1c2e7c287..f86db4261 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -215,7 +215,7 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if hasattr(obj, "to_json"): return {"__type__": "to_json", "__value__": obj.to_json()} if isinstance(obj, QuantumCircuit): - kwargs = {"use_symengine": optionals.HAS_SYMENGINE} + kwargs: Dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)} if _TERRA_VERSION[0] >= 1: # NOTE: This can be updated only after the server side has # updated to a newer qiskit version. @@ -239,13 +239,13 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ data=obj, serializer=_write_parameter_expression, compress=False, - use_symengine=optionals.HAS_SYMENGINE, + use_symengine=bool(optionals.HAS_SYMENGINE), ) return {"__type__": "ParameterExpression", "__value__": value} if isinstance(obj, ParameterView): return obj.data if isinstance(obj, Instruction): - kwargs = {"use_symengine": optionals.HAS_SYMENGINE} + kwargs = {"use_symengine": bool(optionals.HAS_SYMENGINE)} if _TERRA_VERSION[0] >= 1: # NOTE: This can be updated only after the server side has # updated to a newer qiskit version. diff --git a/qiskit_ibm_runtime/utils/utils.py b/qiskit_ibm_runtime/utils/utils.py index 23831b2f6..0108a8474 100644 --- a/qiskit_ibm_runtime/utils/utils.py +++ b/qiskit_ibm_runtime/utils/utils.py @@ -17,9 +17,10 @@ import os import re import hashlib +import warnings from queue import Queue from threading import Condition -from typing import List, Optional, Any, Dict, Union, Tuple +from typing import List, Optional, Any, Dict, Union, Tuple, Sequence from urllib.parse import urlparse import requests @@ -27,9 +28,50 @@ IAMAuthenticator, ) from ibm_platform_services import ResourceControllerV2 # pylint: disable=import-error +from qiskit.circuit import QuantumCircuit +from qiskit.transpiler import Target from qiskit_ibm_runtime.exceptions import IBMInputValueError +def is_isa_circuit(circuit: QuantumCircuit, target: Target) -> bool: + """Checks if the circuit is an ISA circuit, meaning that it has a layout and that it + only uses instructions that exist in the target. + Args: + circuit: A single QuantumCircuit + target: A Qiskit Target + Returns: + Boolean True if the circuit is an ISA circuit + """ + if circuit.layout is None: + return False + for instruction in circuit.data: + name = instruction.operation.name + qargs = tuple(circuit.find_bit(x).index for x in instruction.qubits) + if not target.instruction_supported(name, qargs) and name != "barrier": + return False + return True + + +def validate_isa_circuits(circuits: Sequence[QuantumCircuit], target: Target) -> None: + """Validate if all circuits are ISA circuits + Args: + circuits: A list of QuantumCircuits + target: A Qiskit Target + Raises: + NonISACircuitsError if some of the circuits are not ISA circuits + """ + if not all(is_isa_circuit(circuit, target) for circuit in circuits): + warnings.warn( + "Circuits that do not match the target hardware definition will no longer be supported " + "after March 1, 2024. See the transpilation documentation " + "(https://docs.quantum.ibm.com/transpile) for instructions to transform circuits and " + "the primitive examples (https://docs.quantum.ibm.com/run/primitives-examples) to see " + "this coupled with operator transformations.", + DeprecationWarning, + stacklevel=6, + ) + + def validate_job_tags(job_tags: Optional[List[str]]) -> None: """Validates input job tags. diff --git a/releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml similarity index 100% rename from releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml rename to releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml diff --git a/releasenotes/notes/0.19/fix-qpy-bug-739cefc2c9018d0b.yaml b/releasenotes/notes/0.19/fix-qpy-bug-739cefc2c9018d0b.yaml new file mode 100644 index 000000000..8969fba28 --- /dev/null +++ b/releasenotes/notes/0.19/fix-qpy-bug-739cefc2c9018d0b.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue with the :func:`.qpy.dump` function, when the + ``use_symengine`` flag was set to a truthy object that evaluated to + ``True`` but was not actually the boolean ``True`` the generated QPY + payload would be corrupt. + diff --git a/releasenotes/notes/0.20/dd-named-barrier-4f32f6fcb92e20f2.yaml b/releasenotes/notes/0.20/dd-named-barrier-4f32f6fcb92e20f2.yaml new file mode 100644 index 000000000..8f4a734db --- /dev/null +++ b/releasenotes/notes/0.20/dd-named-barrier-4f32f6fcb92e20f2.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``dd_barrier`` optional input to :class:`.PadDynamicalDecoupling` + constructor to identify portions of the circuit to apply dynamical + decoupling (dd) on selectively. If this string is contained in the label of + a barrier in the circuit, dd is applied on the delays ending with it (on + the same qubits); otherwise, it is not applied. \ No newline at end of file diff --git a/releasenotes/notes/0.20/deprecate-abstract-circuits-d9bdc94b2be7ea21.yaml b/releasenotes/notes/0.20/deprecate-abstract-circuits-d9bdc94b2be7ea21.yaml new file mode 100644 index 000000000..666ce3e4b --- /dev/null +++ b/releasenotes/notes/0.20/deprecate-abstract-circuits-d9bdc94b2be7ea21.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + Circuits that do not match the target hardware definition will no longer be supported after March 1, 2024. + See the transpilation documentation (https://docs.quantum.ibm.com/transpile) for instructions to + transform circuits and the primitive examples (https://docs.quantum.ibm.com/run/primitives-examples) + to see this coupled with operator transformations. \ No newline at end of file diff --git a/releasenotes/notes/0.20/fix-durations-with-control-flow-0cf6b5c48c387ca8.yaml b/releasenotes/notes/0.20/fix-durations-with-control-flow-0cf6b5c48c387ca8.yaml new file mode 100644 index 000000000..5a3905e35 --- /dev/null +++ b/releasenotes/notes/0.20/fix-durations-with-control-flow-0cf6b5c48c387ca8.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix assignment of instruction durations when scheduling circuits with + control flow. Prior to this fix, the indices for instructions on inner + blocks were not mapped to the physical indices in the outer dag. +other: + - | + The :class:`~InstructionDurations` `durations` input is now also required + for the constructor of :class:`~PadDelay`. diff --git a/releasenotes/notes/0.20/python-3-12-support-a0390cacfe596e5b.yaml b/releasenotes/notes/0.20/python-3-12-support-a0390cacfe596e5b.yaml new file mode 100644 index 000000000..9ff247953 --- /dev/null +++ b/releasenotes/notes/0.20/python-3-12-support-a0390cacfe596e5b.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Python 3.12 is now supported. diff --git a/releasenotes/notes/0.20/session-modes-5c22b68620f8d690.yaml b/releasenotes/notes/0.20/session-modes-5c22b68620f8d690.yaml new file mode 100644 index 000000000..b9e887fe6 --- /dev/null +++ b/releasenotes/notes/0.20/session-modes-5c22b68620f8d690.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Sessions will now be started with a new ``/sessions`` endpoint that allows for different + execution modes. Batch mode is now supported through :class:`~qiskit_ibm_runtime.Batch`, and + :class:`~qiskit_ibm_runtime.Session` will work the same as way as before. + Please see https://docs.quantum.ibm.com/run/sessions for more information. + + Note that ``Session`` and ``Batch`` created from ``qiskit-ibm-runtime`` prior to this release will no longer be + supported after March 31, 2024. Please update your ``qiskit-ibm-runtime`` version as soon as possible before this date. diff --git a/releasenotes/notes/0.20/sessions-endpoint-e617b028fe4a68c4.yaml b/releasenotes/notes/0.20/sessions-endpoint-e617b028fe4a68c4.yaml new file mode 100644 index 000000000..72012e5c4 --- /dev/null +++ b/releasenotes/notes/0.20/sessions-endpoint-e617b028fe4a68c4.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Sessions started with :meth:`qiskit_ibm_runtime.IBMBackend.open_session` will use the + new ``/sessions`` endpoint. + + The sessions functionality will not change but note that ``backend.run()`` sessions + prior to this release will no longer be supported after March 31, 2024. + Please update your ``qiskit-ibm-runtime`` version as soon as possible before this date. \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 03f4af164..7236cd015 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ mypy==0.931 -pylint==2.16.2 +pylint==3.0.0 pproxy==2.7.8 nbqa==1.5.3 matplotlib>=2.1 @@ -13,6 +13,7 @@ black~=22.0 coverage>=6.3 pylatexenc scikit-learn +setuptools ddt>=1.2.0,!=1.4.0,!=1.4.3 # Documentation diff --git a/setup.py b/setup.py index c5845888d..f93ad8094 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ long_description_content_type="text/markdown", url="https://github.com/Qiskit/qiskit-ibm-runtime", author="Qiskit Development Team", - author_email="hello@qiskit.org", + author_email="qiskit@us.ibm.com", license="Apache 2.0", classifiers=[ "Environment :: Console", @@ -63,6 +63,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering", ], keywords="qiskit sdk quantum api runtime ibm", diff --git a/test/integration/test_session.py b/test/integration/test_session.py index 15e4a92f5..01ca5e422 100644 --- a/test/integration/test_session.py +++ b/test/integration/test_session.py @@ -113,9 +113,7 @@ def test_session_id(self): self.assertEqual(backend.session.session_id, None) self.assertTrue(backend.session.active) job1 = backend.run(bell()) - self.assertEqual(job1._session_id, job1.job_id()) - job2 = backend.run(bell()) - self.assertFalse(job2._session_id == job2.job_id()) + self.assertEqual(job1._session_id, None) def test_backend_run_with_session(self): """Test that 'shots' parameter is transferred correctly""" @@ -131,22 +129,20 @@ def test_backend_run_with_session(self): ) def test_backend_and_primitive_in_session(self): - """Test Sampler.run and backend.run in the same session.""" + """Test using simulator does not start a session.""" backend = self.service.get_backend("ibmq_qasm_simulator") with Session(backend=backend) as session: sampler = Sampler(session=session) job1 = sampler.run(circuits=bell()) with warnings.catch_warnings(record=True): job2 = backend.run(circuits=bell()) - self.assertEqual(job1.session_id, job1.job_id()) + self.assertIsNone(job1.session_id) self.assertIsNone(job2.session_id) with backend.open_session() as session: with warnings.catch_warnings(record=True): sampler = Sampler(backend=backend) job1 = backend.run(bell()) job2 = sampler.run(circuits=bell()) - session_id = session.session_id - self.assertEqual(session_id, job1.job_id()) self.assertIsNone(job2.session_id) def test_session_cancel(self): @@ -174,11 +170,12 @@ def test_run_after_cancel(self): backend.open_session() job2 = backend.run(bell()) - self.assertIsNotNone(job2._session_id) + self.assertTrue(job2.result()) backend.cancel_session() job3 = backend.run(circuits=bell()) self.assertIsNone(backend.session) + self.assertTrue(job3.result()) self.assertIsNone(job3._session_id) def test_session_as_context_manager(self): @@ -188,9 +185,8 @@ def test_session_as_context_manager(self): with backend.open_session() as session: job1 = backend.run(bell()) session_id = session.session_id - self.assertEqual(session_id, job1.job_id()) - job2 = backend.run(bell()) - self.assertFalse(session_id == job2.job_id()) + self.assertTrue(job1.result()) + self.assertIsNone(session_id) def test_run_after_cancel_as_context_manager(self): """Test run after cancel in context manager""" diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 8c32fa985..4cf3e101c 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -285,6 +285,22 @@ def set_job_classes(self, classes): classes = [classes] self._job_classes = classes + # pylint: disable=unused-argument + def create_session( + self, + backend: Optional[str] = None, + instance: Optional[str] = None, + max_time: Optional[int] = None, + channel: Optional[str] = None, + mode: Optional[str] = None, + ) -> Dict[str, Any]: + """Create a session.""" + return {"id": uuid.uuid4().hex} + + def close_session(self, session_id: str) -> None: + """Close a session.""" + pass + def is_qctrl_enabled(self): """Return whether or not channel_strategy q-ctrl is enabled.""" return False diff --git a/test/unit/test_batch.py b/test/unit/test_batch.py index 3b54b1094..d5fb053a2 100644 --- a/test/unit/test_batch.py +++ b/test/unit/test_batch.py @@ -12,25 +12,48 @@ """Tests for Batch class.""" -from unittest.mock import patch +from unittest.mock import MagicMock from qiskit_ibm_runtime import Batch +from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION from ..ibm_test_case import IBMTestCase class TestBatch(IBMTestCase): - """Class for testing the Session class.""" + """Class for testing the Batch class.""" def tearDown(self) -> None: super().tearDown() _DEFAULT_SESSION.set(None) - @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True) - def test_default_batch(self, mock_service): - """Test using default batch mode.""" - mock_service.global_service = None - batch = Batch(backend="ibm_gotham") - self.assertIsNotNone(batch.service) - mock_service.assert_called_once() + def test_passing_ibm_backend(self): + """Test passing in IBMBackend instance.""" + backend = MagicMock(spec=IBMBackend) + backend._instance = None + backend.name = "ibm_gotham" + session = Batch(service=MagicMock(), backend=backend) + self.assertEqual(session.backend(), "ibm_gotham") + + def test_using_ibm_backend_service(self): + """Test using service from an IBMBackend instance.""" + backend = MagicMock(spec=IBMBackend) + backend._instance = None + backend.name = "ibm_gotham" + session = Batch(backend=backend) + self.assertEqual(session.service, backend.service) + + def test_run_after_close(self): + """Test running after session is closed.""" + session = Batch(service=MagicMock(), backend="ibm_gotham") + session.cancel() + with self.assertRaises(RuntimeError): + session.run(program_id="program_id", inputs={}) + + def test_context_manager(self): + """Test session as a context manager.""" + with Batch(service=MagicMock(), backend="ibm_gotham") as session: + session.run(program_id="foo", inputs={}) + session.cancel() + self.assertFalse(session._active) diff --git a/test/unit/test_runtime_ws.py b/test/unit/test_runtime_ws.py index 3dc232a1b..0b9dab26f 100644 --- a/test/unit/test_runtime_ws.py +++ b/test/unit/test_runtime_ws.py @@ -91,6 +91,7 @@ def _patched_run(callback, *args, **kwargs): # pylint: disable=unused-argument service = MagicMock(spec=QiskitRuntimeService) service.run = _patched_run service._channel_strategy = None + service._api_client = MagicMock() circ = bell() obs = SparsePauliOp.from_list([("IZ", 1)]) diff --git a/test/unit/test_session.py b/test/unit/test_session.py index 5e228acbb..b2b84fbd2 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -12,11 +12,7 @@ """Tests for Session classession.""" -import sys -import time -from concurrent.futures import ThreadPoolExecutor, wait - -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, patch from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import Session @@ -33,7 +29,7 @@ def tearDown(self) -> None: super().tearDown() _DEFAULT_SESSION.set(None) - @patch("qiskit_ibm_runtime.session.QiskitRuntimeService", autospec=True) + @patch("qiskit_ibm_runtime.session.QiskitRuntimeService") def test_default_service(self, mock_service): """Test using default service.""" mock_service.global_service = None @@ -107,58 +103,19 @@ def test_run(self): decoder = MagicMock() max_time = 42 session = Session(service=service, backend=backend, max_time=max_time) - session_ids = [None, job.job_id()] - start_sessions = [True, False] - - for idx in range(2): - session.run( - program_id=program_id, - inputs=inputs, - options=options, - result_decoder=decoder, - ) - _, kwargs = service.run.call_args - self.assertEqual(kwargs["program_id"], program_id) - self.assertDictEqual(kwargs["options"], {"backend": backend, **options}) - self.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]) - self.assertEqual(kwargs["result_decoder"], decoder) - self.assertEqual(session.session_id, job.job_id()) - self.assertEqual(session.backend(), backend) - - def test_run_is_thread_safe(self): - """Test the session sends a session starter job once, and only once.""" - service = MagicMock() - api = MagicMock() - service._api_client = api - - def _wait_a_bit(*args, **kwargs): - # pylint: disable=unused-argument - switchinterval = sys.getswitchinterval() - time.sleep(switchinterval * 2) - return MagicMock() - - service.run = Mock(side_effect=_wait_a_bit) - session = Session(service=service, backend="ibm_gotham") - with ThreadPoolExecutor(max_workers=2) as executor: - results = list(map(lambda _: executor.submit(session.run, "", {}), range(5))) - wait(results) - - calls = service.run.call_args_list - session_starters = list(filter(lambda c: c.kwargs["start_session"] is True, calls)) - self.assertEqual(len(session_starters), 1) - - def test_close_without_run(self): - """Test closing without run.""" - service = MagicMock() - api = MagicMock() - service._api_client = api - session = Session(service=service, backend="ibm_gotham") - session.close() - api.close_session.assert_not_called() + session.run( + program_id=program_id, + inputs=inputs, + options=options, + result_decoder=decoder, + ) + _, kwargs = service.run.call_args + self.assertEqual(kwargs["program_id"], program_id) + self.assertDictEqual(kwargs["options"], {"backend": backend, **options}) + self.assertDictEqual(kwargs["inputs"], inputs) + self.assertEqual(kwargs["result_decoder"], decoder) + self.assertEqual(session.backend(), backend) def test_context_manager(self): """Test session as a context manager.""" @@ -182,14 +139,14 @@ def test_default_backend(self): def test_global_service(self): """Test that global service is used in Session""" _ = FakeRuntimeService(channel="ibm_quantum", token="abc") - session = Session(backend="ibmq_qasm_simulator") + session = Session(backend="common_backend") self.assertTrue(isinstance(session._service, FakeRuntimeService)) self.assertEqual(session._service._account.token, "abc") _ = FakeRuntimeService(channel="ibm_quantum", token="xyz") - session = Session(backend="ibmq_qasm_simulator") + session = Session(backend="common_backend") self.assertEqual(session._service._account.token, "xyz") with Session( - service=FakeRuntimeService(channel="ibm_quantum", token="uvw"), backend="ibm_gotham" + service=FakeRuntimeService(channel="ibm_quantum", token="uvw"), backend="common_backend" ) as session: self.assertEqual(session._service._account.token, "uvw") diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index d548665a1..1e025c9cb 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -1081,3 +1081,34 @@ def test_no_unused_qubits(self): dont_use = qc_dd.qubits[-2:] for op in qc_dd.data: self.assertNotIn(dont_use, op.qubits) + + def test_dd_named_barriers(self): + """Test DD applied on delays ending on named barriers.""" + + dd_sequence = [XGate(), XGate()] + pm = PassManager( + [ + ASAPScheduleAnalysis(self.durations), + PadDynamicalDecoupling( + self.durations, + dd_sequence, + pulse_alignment=1, + dd_barrier="dd", + ), + ] + ) + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.delay(1200, 0) + qc.barrier() + qc.delay(1200, 0) + qc.measure(1, 0) + qc.barrier(label="dd_0") + qc.delay(1200, 0) + qc.barrier(label="delay_only") + qc.delay(1200, 1) + qc_dd = pm.run(qc) + # only 2 X gates are applied in the single delay + # defined by the 'dd_0' barrier + self.assertEqual(len([inst for inst in qc_dd.data if isinstance(inst.operation, XGate)]), 2) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index 5903fec8e..fccd574c4 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -19,6 +19,8 @@ from qiskit.transpiler.passes import ConvertConditionsToIfOps from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +from qiskit.converters import circuit_to_dag +from qiskit.circuit import Delay from qiskit_ibm_runtime.fake_provider import FakeJakarta from qiskit_ibm_runtime.transpiler.passes.scheduling.pad_delay import PadDelay @@ -49,7 +51,12 @@ def test_if_test_gate_after_measure(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -71,7 +78,12 @@ def test_c_if_raises(self): qc.x(1).c_if(0, True) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) with self.assertRaises(TranspilerError): pm.run(qc) @@ -85,7 +97,7 @@ def test_c_if_conversion(self): [ ConvertConditionsToIfOps(), ASAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ) scheduled = pm.run(qc) @@ -106,7 +118,12 @@ def test_measure_after_measure(self): qc.measure(1, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -127,7 +144,12 @@ def test_measure_block_not_end(self): qc.measure(2, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -156,7 +178,12 @@ def test_reset_block_end(self): durations = DynamicCircuitInstructionDurations( [("x", None, 200), ("measure", None, 840), ("reset", None, 840)] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -182,7 +209,12 @@ def test_c_if_on_different_qubits(self): qc.x(2) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -210,7 +242,12 @@ def test_shorter_measure_after_measure(self): durations = DynamicCircuitInstructionDurations( [("measure", [0], 840), ("measure", [1], 540)] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -230,7 +267,12 @@ def test_measure_after_c_if(self): qc.measure(2, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -262,7 +304,12 @@ def test_parallel_gate_different_length(self): [("x", [0], 200), ("x", [1], 400), ("measure", None, 840)] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 2) @@ -287,7 +334,12 @@ def test_parallel_gate_different_length_with_barrier(self): [("x", [0], 200), ("x", [1], 400), ("measure", None, 840)] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 2) @@ -322,7 +374,7 @@ def test_active_reset_circuit(self): scheduled = PassManager( [ ASAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(qc) @@ -356,7 +408,12 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 160)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -384,7 +441,12 @@ def test_scheduling_with_calibration(self): qc.add_calibration("x", (0,), xsched) durations = DynamicCircuitInstructionDurations([("x", None, 160), ("cx", None, 600)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2) @@ -404,9 +466,10 @@ def test_padding_not_working_without_scheduling(self): qc.delay(100, 0) qc.x(0) qc.measure(0, 0) + durations = DynamicCircuitInstructionDurations() with self.assertRaises(TranspilerError): - PassManager(PadDelay()).run(qc) + PassManager(PadDelay(durations)).run(qc) def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. @@ -422,7 +485,7 @@ def test_no_pad_very_end_of_circuit(self): scheduled = PassManager( [ ASAPScheduleAnalysis(durations), - PadDelay(fill_very_end=False, schedule_idle_qubits=True), + PadDelay(durations, fill_very_end=False, schedule_idle_qubits=True), ] ).run(qc) @@ -458,7 +521,12 @@ def test_reset_terminates_block(self): ("measure", [1], 540), ] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -502,7 +570,12 @@ def test_reset_merged_with_measure(self): ("measure", [1], 540), ] ) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -538,14 +611,14 @@ def test_scheduling_is_idempotent(self): scheduled0 = PassManager( [ ASAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(qc) scheduled1 = PassManager( [ ASAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(scheduled0) @@ -559,7 +632,12 @@ def test_gate_on_measured_qubit(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -582,7 +660,12 @@ def test_grouped_measurements_prior_control_flow(self): qc.measure(2, 2) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 3) @@ -619,7 +702,12 @@ def test_back_to_back_c_if(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -657,7 +745,12 @@ def test_nested_control_scheduling(self): qc.x(3) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(4, 3) @@ -702,7 +795,12 @@ def test_while_loop(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -728,7 +826,12 @@ def test_for_loop(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -756,7 +859,7 @@ def test_registers(self): [ ConvertConditionsToIfOps(), ASAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ) scheduled = pm.run(qc) @@ -782,7 +885,12 @@ def test_c_if_plugin_conversion_with_transpile(self): backend.configuration().basis_gates.append("if_else") durations = DynamicCircuitInstructionDurations.from_backend(backend) - pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ASAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) qr0 = QuantumRegister(1, name="q") cr = ClassicalRegister(1, name="c") @@ -811,6 +919,15 @@ def test_c_if_plugin_conversion_with_transpile(self): class TestALAPSchedulingAndPaddingPass(IBMTestCase): """Tests the ALAP Scheduling passes""" + def get_delay_dict(self, circ): + """Return a dictionary with a list of delays for each qubit""" + dag = circuit_to_dag(circ) + delays = dag.op_nodes(Delay) + delay_dict = {q_ind: [] for q_ind in range(len(circ.qubits))} + for delay in delays: + delay_dict[dag.find_bit(delay.qargs[0]).index] += [delay.op.duration] + return delay_dict + def test_alap(self): """Test standard ALAP scheduling""" durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) @@ -818,7 +935,12 @@ def test_alap(self): qc.measure(0, 0) qc.x(1) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -840,7 +962,12 @@ def test_if_test_gate_after_measure(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -864,7 +991,12 @@ def test_classically_controlled_gate_after_measure(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -887,7 +1019,12 @@ def test_measure_after_measure(self): qc.measure(1, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -909,7 +1046,12 @@ def test_measure_block_not_end(self): qc.measure(2, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -939,7 +1081,12 @@ def test_reset_block_end(self): durations = DynamicCircuitInstructionDurations( [("x", None, 200), ("measure", None, 840), ("reset", None, 840)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -965,7 +1112,12 @@ def test_c_if_on_different_qubits(self): qc.x(2) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -993,7 +1145,12 @@ def test_shorter_measure_after_measure(self): durations = DynamicCircuitInstructionDurations( [("measure", [0], 840), ("measure", [1], 540)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -1013,7 +1170,12 @@ def test_measure_after_c_if(self): qc.measure(2, 0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -1044,7 +1206,12 @@ def test_parallel_gate_different_length(self): [("x", [0], 200), ("x", [1], 400), ("measure", None, 840)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 2) @@ -1069,7 +1236,12 @@ def test_parallel_gate_different_length_with_barrier(self): [("x", [0], 200), ("x", [1], 400), ("measure", None, 840)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 2) @@ -1104,7 +1276,7 @@ def test_active_reset_circuit(self): scheduled = PassManager( [ ALAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(qc) @@ -1138,7 +1310,12 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 160)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -1166,7 +1343,12 @@ def test_scheduling_with_calibration(self): qc.add_calibration("x", (0,), xsched) durations = DynamicCircuitInstructionDurations([("x", None, 160), ("cx", None, 600)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2) @@ -1186,9 +1368,10 @@ def test_padding_not_working_without_scheduling(self): qc.delay(100, 0) qc.x(0) qc.measure(0, 0) + durations = DynamicCircuitInstructionDurations() with self.assertRaises(TranspilerError): - PassManager(PadDelay()).run(qc) + PassManager(PadDelay(durations)).run(qc) def test_no_pad_very_end_of_circuit(self): """Test padding option that inserts no delay at the very end of circuit. @@ -1204,7 +1387,7 @@ def test_no_pad_very_end_of_circuit(self): scheduled = PassManager( [ ALAPScheduleAnalysis(durations), - PadDelay(fill_very_end=False, schedule_idle_qubits=True), + PadDelay(durations, fill_very_end=False, schedule_idle_qubits=True), ] ).run(qc) @@ -1244,7 +1427,12 @@ def test_reset_terminates_block(self): ("measure", [1], 540), ] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -1288,7 +1476,12 @@ def test_reset_merged_with_measure(self): ("measure", [1], 540), ] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -1329,7 +1522,7 @@ def test_already_scheduled(self): scheduled = PassManager( [ ALAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(qc) @@ -1353,14 +1546,14 @@ def test_scheduling_is_idempotent(self): scheduled0 = PassManager( [ ALAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(qc) scheduled1 = PassManager( [ ALAPScheduleAnalysis(durations), - PadDelay(schedule_idle_qubits=True), + PadDelay(durations, schedule_idle_qubits=True), ] ).run(scheduled0) @@ -1374,7 +1567,12 @@ def test_gate_on_measured_qubit(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -1397,7 +1595,12 @@ def test_grouped_measurements_prior_control_flow(self): qc.measure(2, 2) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 3) @@ -1442,7 +1645,12 @@ def test_fast_path_eligible_scheduling(self): qc.x(2) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(4, 3) @@ -1486,7 +1694,12 @@ def test_back_to_back_c_if(self): qc.x(1) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 1) @@ -1541,7 +1754,12 @@ def test_issue_458_extra_idle_bug_0(self): [("x", None, 160), ("cx", None, 700), ("measure", None, 840)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(4, 3) @@ -1590,7 +1808,12 @@ def test_issue_458_extra_idle_bug_1(self): [("rz", None, 0), ("cx", None, 700), ("measure", None, 840)] ) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(3, 3) @@ -1617,7 +1840,12 @@ def test_nested_control_scheduling(self): qc.x(3) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(4, 3) @@ -1662,7 +1890,12 @@ def test_while_loop(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -1688,7 +1921,12 @@ def test_for_loop(self): qc.x(0) durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) scheduled = pm.run(qc) expected = QuantumCircuit(2, 1) @@ -1711,7 +1949,12 @@ def test_transpile_mock_backend(self): backend.configuration().basis_gates.append("while_loop") durations = DynamicCircuitInstructionDurations.from_backend(backend) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) qr = QuantumRegister(3) cr = ClassicalRegister(2) @@ -1756,7 +1999,12 @@ def test_transpile_both_paths(self): backend.configuration().basis_gates.append("if_else") durations = DynamicCircuitInstructionDurations.from_backend(backend) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) qr = QuantumRegister(3) cr = ClassicalRegister(2) @@ -1801,7 +2049,12 @@ def test_c_if_plugin_conversion_with_transpile(self): backend.configuration().basis_gates.append("if_else") durations = DynamicCircuitInstructionDurations.from_backend(backend) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(schedule_idle_qubits=True)]) + pm = PassManager( + [ + ALAPScheduleAnalysis(durations), + PadDelay(durations, schedule_idle_qubits=True), + ] + ) qr0 = QuantumRegister(1, name="q") cr = ClassicalRegister(1, name="c") @@ -1834,7 +2087,7 @@ def test_no_unused_qubits(self): """ durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) - pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay()]) + pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(durations)]) qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -1851,3 +2104,30 @@ def test_no_unused_qubits(self): dont_use = scheduled.qubits[-1] for op in scheduled.data: self.assertNotIn(dont_use, op.qubits) + + def test_scheduling_nonuniform_durations(self): + """Test that scheduling withing control flow blocks uses the + instruction durations on the correct qubit indices""" + + backend = FakeJakarta() + backend.configuration().basis_gates.append("if_else") + durations = DynamicCircuitInstructionDurations( + [("cx", (0, 1), 250), ("cx", (1, 3), 4000), ("measure", None, 2600)] + ) + pm = PassManager([ALAPScheduleAnalysis(durations), PadDelay(durations)]) + + qc = QuantumCircuit(4, 1) + qc.barrier() + qc.measure(0, 0) + with qc.if_test((0, True)): + qc.cx(0, 1) + qc_transpiled = transpile(qc, backend, initial_layout=[1, 3, 0, 2]) + scheduled = pm.run(qc_transpiled) + delay_dict = self.get_delay_dict(scheduled.data[-1].operation.params[0]) + self.assertEqual(delay_dict[0][0], 4000) + + # different layout + qc_transpiled = transpile(qc, backend, initial_layout=[0, 1, 2, 3]) + scheduled = pm.run(qc_transpiled) + delay_dict = self.get_delay_dict(scheduled.data[-1].operation.params[0]) + self.assertEqual(delay_dict[2][0], 250) diff --git a/tox.ini b/tox.ini index f2662be4b..f219e404e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.15 -envlist = py38, py39, py310, py311, lint, docs +envlist = py38, py39, py310, py311, py312, lint, docs isolated_build = True [testenv]