From 852479b8cb7ee7ac726354430ae5a2c56c00df3d Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Wed, 7 Feb 2024 14:36:57 -0500 Subject: [PATCH] Fast forward experimental-0.2 (#1389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate fake backends from `qiskit.providers.fake_provider` (#1140) * Migrate fake provider * fix lint * Only migrate backends and fake_provider file to minimize duplication * Add reno * Update snippet to use runtime service --------- Co-authored-by: Kevin Tian * Fix BYOB job results bytes typing issue (#1220) * try/except raw result truncation * catch typeError * revert try/except, use response.text * update unit test * Fix fake backends integration test (#1225) * Clean up/fix integration test * Update test case * Fix black * Added deprecation warning when using qiskitrc file (#1219) * Added deprecation warning when using qiskitrc file * Fixed bug whereby deprecation warnings cause failure for all 'qiskit*' modules. Changed warning to deprecation warning. * Release note * Small grammer fix * black --------- Co-authored-by: Kevin Tian * Update session.from_id() (#1163) * update session.from_id() * add deprecation warning * add reno * Prepare release 0.15.0 (#1226) * Update main branch 0.16.0 (#1227) * Revert `Session.from_id 0.15.0` changes (#1229) * Revert "Update session.from_id() (#1163)" This reverts commit 8fa0472e15ea36341acd6de67efa54a9815354e8. * add reno * Prepare release 0.15.1 (#1230) * Update main branch to 0.16.0 again (#1232) * Remove all code related to custom programs (#1192) * initial removal * cleanup more files * move result_decoder * fix lint * update unit tests * update integration tests * more docs changes * release note * fix path --------- Co-authored-by: Jessie Yu * Make sessions thread safe (#1196) * Make sessions thread safe. The lock prevents other threads from sending new jobs to the service only if the session hasnot been setup yet. I.e.: when sending the first "session starter job", or root job. After this happens, the code is left unlocked to allow threads to access the service concurrently. Error handling is needed to ensure the lock is always released. * Allow multiple sessions to execute simultaneously * Make session test more deterministic --------- Co-authored-by: Kevin Tian Co-authored-by: Jessie Yu * Instance & channel_strategy validation (#1233) * compare cloud instance with channel strategy * fix unit tests * error wording --------- Co-authored-by: Jessie Yu * Prepare release 0.16.1 (#1239) * release notes * fix release * update to 0.17 (#1240) * Stop referring to qiskit.org in API docs (#1242) * Add job.properties() method (#1252) * Add job.properties() method * add reno * Remove non-API docs and translations (#1256) This is the same as https://github.com/Qiskit/qiskit/pull/11352. Now that qiskit.org/ecosystem/ibm-runtime redirects to docs.quantum.ibm.com, we can simplify the docs. See the PR description in https://github.com/Qiskit/qiskit/pull/11352 for details on the motivation for each change. The only difference from Qiskit is that we still keep the tutorials around because learning.quantum.ibm.com uses them. I used a script with `ripgrep` to confirm that none of the images were in use. * Raise error if backend retrieved not in current instance (#1249) * Error if backend not in instance * add reno * update reno * docs build * fix docs again * docs formatting :/ * add integration test * Update test/integration/test_backend.py Co-authored-by: merav-aharoni * Update test/integration/test_backend.py Co-authored-by: merav-aharoni --------- Co-authored-by: merav-aharoni * Fix backend_converter `faulty_qubit` name error (#1257) * fix backend_converter * add reno * Update Tutorial section names (#1241) * The transpilation tutorial has been updated to use the new runtime transpilation passes, and Batch * Fix style checker * Updated names * Updated section names for transpiled tutorial * Updated grover * Fixed consistency * Final updates --------- Co-authored-by: Kevin Tian * make test_backend_wrong_instance IQP only (#1258) * Add pointer to qiskit/documentation issues (#1260) * add pointer to qiskit/documentation issues * Update .github/ISSUE_TEMPLATE/config.yml Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --------- Co-authored-by: Kevin Tian Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> * Update `test_backend_wrong_instance` (#1259) * wip debug wrong instance test * enable integration test * use backend names instead of obj * Don't block for the first job in a session (#1170) * don't block if backend not selected * add reno * add test * Add `activated_at` field to session.details() (#1266) * add activated_at to session.details() * add reno * Revert "Don't block for the first job in a session (#1170)" (#1267) * Revert "Don't block for the first job in a session (#1170)" This reverts commit f2e24e49829360af4381cb0ffc46f62e343054fa. * keep test * Prepare release 0.17.0 (#1268) * update main branch 0.17.1 (#1269) * Remove test_session_no_backend (#1272) * Added RuntimeJob.queue_info() method (#1210) * Added support for RuntimeJob.queueinfo * Moved methods for consistency with main * Changed backend_status to job.status * lint * update test --------- Co-authored-by: Kevin Tian * Replace programs with primitives in api docs (#1281) * programs -> primitives * address comment * Deprecate service.runtime (#1278) * Deprecate service.runtime * add reno * Removed support for qiskitrc file (#1121) * Removed support for qiskitrc file * lint --------- Co-authored-by: Kevin Tian * Add max_time parameter to IBMBackend.open_session() (#1274) * Added max_time to IBMBackend.open_session. * Release note * Changed format of test * Migrate fake backend base classes (#1270) Co-authored-by: Kevin Tian * Add Q-CTRL integration tests (#1173) * add initial qctrl test file * add test & decorator * add tests from Mirko * Added assertness for preciseness of results * remove print statements * move into e2e folder * setup ci * Update q-ctrl-tests.yml * add account test * add more account tests * Update test/qctrl/test_qctrl.py Co-authored-by: Rathish Cholarajan * address comments * update date to 2023 * Update test/qctrl/test_qctrl.py Co-authored-by: Blake Johnson * Update test/qctrl/test_qctrl.py Co-authored-by: Blake Johnson --------- Co-authored-by: merav-aharoni Co-authored-by: Rathish Cholarajan Co-authored-by: Blake Johnson * update grover tutorial equation (#1293) * Add dynamic circuits backend filter (#1291) * Add dynamic circuits backend filter * add reno * switch docs from mathjax to katex (#1301) Updates the sphinx documentation build to use KaTeX instead of MathJax because the final docs build uses KaTeX. The same change was made in Qiskit in https://github.com/Qiskit/qiskit/pull/11435. * Remove qiskit.org links (#1302) * Update session_id docstring (#1304) * Update provider and qiskit versions (#1305) * Exclude params on job retrieval by default (#1308) * exclude params on job retrieval by default * add reno * add test * Reactive the viewcode extension for docs (#1312) We got source links working in API docs on docs.quantum.ibm.com! See https://github.com/Qiskit/documentation/pull/620. To do this, we need to active `sphinx.ext.viewcode`. Note that we still plan to improve this implementation in the future to have more precise links by using `sphinx.ext.linkcode`, tracked by https://github.com/Qiskit/documentation/issues/517. This PR is to unblock the first iteration of this source links mechanism. * Add config files to manifest (#1319) * Fix pip environment resolution (#1329) Calling `pip install` multiple times can (deeply unfortunately) allow an environment to become out-of-sync; `pip` doesn't "remember" the constraints of previous installation commands. One of the largest effects here is that doing `pip install -e .` _followed_ by `pip install git+@main` installs `qiskit-terra` from the initial installation (via `qiskit==0.45.2`), then installs `qiskit==1.0.0.dev0` from Git, which is an incompatible environment. Doing the whole environment resolution in a single `pip install` command fixes this, as the `qiskit` dependency is correctly resolved to be _only_ the `1.0.0.dev0` version. Co-authored-by: Kevin Tian * Use IBM-Provider-vendored pub/sub mechanism (#1328) This is being removed from Qiskit 1.0, and the IBM Provider is (until deprecated) taking over brokering the IBM-Runtime-internal events. Co-authored-by: Kevin Tian * Add transpiler tests (#1323) * Add transpiler tests * remove qiskittestcase * Prepare release 0.18.0 (#1335) * Update main branch 0.18.1 (#1336) * Turn off qiskit.org docs deploy (#1340) * Remove `qiskit.test` (#1292) * replace qiskit.test.ReferenceCircuits * replace BaseQiskitTest * Update qiskit_ibm_runtime/qiskit_runtime_service.py Co-authored-by: Jessie Yu * Update qiskit_ibm_runtime/sampler.py Co-authored-by: Jessie Yu * Update qiskit_ibm_runtime/session.py Co-authored-by: Jessie Yu * Update test/utils.py Co-authored-by: Jessie Yu --------- Co-authored-by: Jessie Yu * Add missing API Refs to docs (#1343) ### Summary The documentation from the `transpile` and `fake_provider` modules does not appear in the rendered docs in qiskit.org/documentation or docs.quantum-computing.ibm.com. ### Details and comments Fixes [Qiskit Docs Issue #238](https://github.com/Qiskit/documentation/issues/238) Replaces PR #1330. Transpiler Doc files added: - qiskit_ibm_runtime.transpiler.passes.html - qiskit_ibm_provider.transpiler.passes.basis.html - qiskit_ibm_provider.transpiler.passes.scheduling.ALAPScheduleAnalysis.html - qiskit_ibm_provider.transpiler.passes.scheduling.ASAPScheduleAnalysis.html - qiskit_ibm_provider.transpiler.passes.scheduling.BlockBasePadder.html - qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitInstructionDurations.html - qiskit_ibm_provider.transpiler.passes.scheduling.html - qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay.html - qiskit_ibm_provider.transpiler.passes.scheduling.PadDynamicalDecoupling.html
Fake Provider Docs added: - qiskit_ibm_runtime.fake_provider.FakeAlmaden.html - qiskit_ibm_runtime.fake_provider.FakeAlmadenV2.html - qiskit_ibm_runtime.fake_provider.FakeArmonk.html - qiskit_ibm_runtime.fake_provider.FakeArmonkV2.html - qiskit_ibm_runtime.fake_provider.FakeAthens.html - qiskit_ibm_runtime.fake_provider.FakeAthensV2.html - qiskit_ibm_runtime.fake_provider.FakeAuckland.html - qiskit_ibm_runtime.fake_provider.FakeBelem.html - qiskit_ibm_runtime.fake_provider.FakeBelemV2.html - qiskit_ibm_runtime.fake_provider.FakeBoeblingen.html - qiskit_ibm_runtime.fake_provider.FakeBoeblingenV2.html - qiskit_ibm_runtime.fake_provider.FakeBogota.html - qiskit_ibm_runtime.fake_provider.FakeBogotaV2.html - qiskit_ibm_runtime.fake_provider.FakeBrooklyn.html - qiskit_ibm_runtime.fake_provider.FakeBrooklynV2.html - qiskit_ibm_runtime.fake_provider.FakeBurlington.html - qiskit_ibm_runtime.fake_provider.FakeBurlingtonV2.html - qiskit_ibm_runtime.fake_provider.FakeCairo.html - qiskit_ibm_runtime.fake_provider.FakeCairoV2.html - qiskit_ibm_runtime.fake_provider.FakeCambridge.html - qiskit_ibm_runtime.fake_provider.FakeCambridgeV2.html - qiskit_ibm_runtime.fake_provider.FakeCasablanca.html - qiskit_ibm_runtime.fake_provider.FakeCasablancaV2.html - qiskit_ibm_runtime.fake_provider.FakeEssex.html - qiskit_ibm_runtime.fake_provider.FakeEssexV2.html - qiskit_ibm_runtime.fake_provider.FakeGeneva.html - qiskit_ibm_runtime.fake_provider.FakeGuadalupe.html - qiskit_ibm_runtime.fake_provider.FakeGuadalupeV2.html - qiskit_ibm_runtime.fake_provider.FakeHanoi.html - qiskit_ibm_runtime.fake_provider.FakeHanoiV2.html - qiskit_ibm_runtime.fake_provider.FakeJakarta.html - qiskit_ibm_runtime.fake_provider.FakeJakartaV2.html - qiskit_ibm_runtime.fake_provider.FakeJohannesburg.html - qiskit_ibm_runtime.fake_provider.FakeJohannesburgV2.html - qiskit_ibm_runtime.fake_provider.FakeKolkata.html - qiskit_ibm_runtime.fake_provider.FakeKolkataV2.html - qiskit_ibm_runtime.fake_provider.FakeLagos.html - qiskit_ibm_runtime.fake_provider.FakeLagosV2.html - qiskit_ibm_runtime.fake_provider.FakeLima.html - qiskit_ibm_runtime.fake_provider.FakeLimaV2.html - qiskit_ibm_runtime.fake_provider.FakeLondon.html - qiskit_ibm_runtime.fake_provider.FakeLondonV2.html - qiskit_ibm_runtime.fake_provider.FakeManhattan.html - qiskit_ibm_runtime.fake_provider.FakeManhattanV2.html - qiskit_ibm_runtime.fake_provider.FakeManila.html - qiskit_ibm_runtime.fake_provider.FakeManilaV2.html - qiskit_ibm_runtime.fake_provider.FakeMelbourne.html - qiskit_ibm_runtime.fake_provider.FakeMelbourneV2.html - qiskit_ibm_runtime.fake_provider.FakeMontreal.html - qiskit_ibm_runtime.fake_provider.FakeMontrealV2.html - qiskit_ibm_runtime.fake_provider.FakeMumbai.html - qiskit_ibm_runtime.fake_provider.FakeMumbaiV2.html - qiskit_ibm_runtime.fake_provider.FakeNairobi.html - qiskit_ibm_runtime.fake_provider.FakeNairobiV2.html - qiskit_ibm_runtime.fake_provider.FakeOslo.html - qiskit_ibm_runtime.fake_provider.FakeOurense.html - qiskit_ibm_runtime.fake_provider.FakeOurenseV2.html - qiskit_ibm_runtime.fake_provider.FakeParis.html - qiskit_ibm_runtime.fake_provider.FakeParisV2.html - qiskit_ibm_runtime.fake_provider.FakePerth.html - qiskit_ibm_runtime.fake_provider.FakePoughkeepsie.html - qiskit_ibm_runtime.fake_provider.FakePoughkeepsieV2.html - qiskit_ibm_runtime.fake_provider.FakePrague.html - qiskit_ibm_runtime.fake_provider.FakeProviderForBackendV2.html - qiskit_ibm_runtime.fake_provider.FakeProvider.html - qiskit_ibm_runtime.fake_provider.FakeQuito.html - qiskit_ibm_runtime.fake_provider.FakeQuitoV2.html - qiskit_ibm_runtime.fake_provider.FakeRochester.html - qiskit_ibm_runtime.fake_provider.FakeRochesterV2.html - qiskit_ibm_runtime.fake_provider.FakeRome.html - qiskit_ibm_runtime.fake_provider.FakeRomeV2.html - qiskit_ibm_runtime.fake_provider.FakeRueschlikon.html - qiskit_ibm_runtime.fake_provider.FakeSantiago.html - qiskit_ibm_runtime.fake_provider.FakeSantiagoV2.html - qiskit_ibm_runtime.fake_provider.FakeSherbrooke.html - qiskit_ibm_runtime.fake_provider.FakeSingapore.html - qiskit_ibm_runtime.fake_provider.FakeSingaporeV2.html - qiskit_ibm_runtime.fake_provider.FakeSydney.html - qiskit_ibm_runtime.fake_provider.FakeSydneyV2.html - qiskit_ibm_runtime.fake_provider.FakeTenerife.html - qiskit_ibm_runtime.fake_provider.FakeTokyo.html - qiskit_ibm_runtime.fake_provider.FakeToronto.html - qiskit_ibm_runtime.fake_provider.FakeTorontoV2.html - qiskit_ibm_runtime.fake_provider.FakeValencia.html - qiskit_ibm_runtime.fake_provider.FakeValenciaV2.html - qiskit_ibm_runtime.fake_provider.FakeVigo.html - qiskit_ibm_runtime.fake_provider.FakeVigoV2.html - qiskit_ibm_runtime.fake_provider.FakeWashington.html - qiskit_ibm_runtime.fake_provider.FakeWashingtonV2.html - qiskit_ibm_runtime.fake_provider.FakeYorktown.html - qiskit_ibm_runtime.fake_provider.FakeYorktownV2.html
Co-authored-by: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> * Set use_symengine on qpy generation if symengine is installed (#1355) This commit sets the use_symengine flag during qpy export used during job payload generation if symengine is installed. For qiskit <1.0 symengine is a hard dependency on platforms that have support for it (x86_64, aarch64, and ppc64le linux and macOS) and an optional requirement on other qiskit supported platforms, and when available it greatly improves the runtime performance of symbolic expressions in Qiskit. By default QPY opts for the more compatible sympy representation of an unbound ParameterExpression as it's maximally compatible, and means that no matter the environment the payload will be loaded from it will be able to parse the payload. In QPY version 10 a new flag was added to the payload definition that enables users to use a native binary symengine representation of a symbolic expression that is significantly faster to generate and parse, but it still defaults to sympy as using this flag limits the compatibility of the payload. However, for runtime job submission we know that the service always has symengine available so we should be using this flag for job submission if the local environment has symengine installed as it will greatly speed up the serialization of unbound ParameterExpression objects. In Qiskit 1.0, symengine is promoted to a hard requriment for all environments and use_symengine=True is the default in Qiskit's qpy module. But, setting it like this is still a good idea as it will always be safe to do this, it's just in qiskit 1.0 symengine will always be installed. * Fix for unit tests (#1356) * Run all unit tests * Import fake providers * Format * Fake providers not used in this branch * Unused import --------- Co-authored-by: Kevin Tian * Update flaky integration tests (#1359) * Fix bad Sphinx API doc Provider naming (#1360) This resulted in the imports being called `qiskit_ibm_provider` rather than `qiskit_ibm_runtime`. Co-authored-by: Frank Harkins * Merge qiskit-ibm-provider into qiskit-ibm-runtime (#1285) * initial commit * more replacements * more replacements * add auth/api * add reno * add qpy module back * address comments * fix docs build * Update invalid token test (#1367) * Copy pub/sub code into qiskit-ibm-runtime (#1349) * Copy pub/sub code into qiskit-ibm-runtime * fix docs build * Remove mthree from requirements-dev (#1370) * Handle Qiskit 1.0 removals (#1363) * Handle Qiskit 1.0 removals * Remove removed qiskit modules * add both qiskit synthesis imports * fix docs build * fix lint errors from 1.0.0RC1 * update basicaer import * Update qiskit_ibm_runtime/fake_provider/fake_backend.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * address comments * Update imports & docstrings --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Copy over provider session & remove qiskit-ibm-provider (#1368) * Move provider's session & remove qiskit-ibm-provider * fix black * Handle qpy serialization across versions and delete vendored fork (#1377) * Handle qpy serialization across versions and delete vendored fork For the next qiskit-ibm-runtime release it is desired to have it support both Qiskit 0.45.x/0.46.x and Qiskit 1.0.0. To do this with QPY it means we need to emit a fixed qpy version of 10 as that's currently the maximum version the server side can parse. The easiest way to manage this across versions is actually to rely on Qiskit's qpy module again. Starting in Qiskit 1.0.0 the qiskit.qpy.dump() function a new kwarg, version exists to specify the version of qpy emitted. We can use this to control the rollout of QPY format versions as the server side is updated. For right now this is fixed to version 10 which is the same as the QPY format version emitted by 0.45.x and 0.46.0. Because the versions are the same we can just use qiskit's qpy module moving forward as either we're on qiskit 0.45.x/0.46.x and will emit version 10 (as there is no option) or we're on >=1.0.0 and we explicitly set the qpy version to one that's compatible with the server side. The qpy internals usage to serialize parameters and parameter expressions are left in place (although expanded on to include #1355 for parameter expressions) as an alternative needs to be considered for this. It is not a good idea to rely on qpy internals and explicitly private functions to do partial serialization. This commit also deletes the vendored fork as it's never been included in a released version of qiskit-ibm-runtime and nothing is using it anymore. Related to: #1375 * Fix lint * Fix lint again * Run integration tests against Qiskit main (#1376) * Fix qiskit main integratino tests cron job (#1379) * Prepare release 0.19.0 (#1380) * Prepare release 0.19 * add reno stating compatibility * Update main branch version 0.19.1 (#1382) * Porting qiskit-ibm-provider/787: Fix `DynamicCircuitInstructionDurations.from_backend` for both `Backend versions` (#1383) * porting qiskit-ibm-provider/pull/787 * porting qiskit-ibm-provider/pull/787 * black * oops * monkey patch https://github.com/Qiskit/qiskit/pull/11727 * black lynt * mypy --------- Co-authored-by: Kevin Tian * Cast use_symengine input to a bool (#1385) * Cast use_symengine input to a bool This commit works around a bug in Qiskit 0.45.x, 0.46.0, and 1.0.0rc1 with the `use_symengine` flag on `qpy.dump()`. The dump function has a bug when it receives a truthy value instead of a bool literal that it will generate a corrupt qpy because of a mismatch between how the encoding was processed (the encoding is incorrectly set to sympy in the file header but uses symengine encoding in the actual body of the circuit. This is being fixed in Qiskit/qiskit#11730 for 1.0.0, and will be backported to 0.46.1. But to ensure compatibility with 0.45.x, 0.46.0, and 1.0.0rc1 while waiting for those releases we can workaround this by just casting the value to a boolean. * Fix mypy failures * Mypy fixes again * Prepare release 0.19.1 (#1386) * Update main branch version 0.19.2 (#1388) * fix import issues * more import issues * disable docs warning --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Kevin Tian Co-authored-by: merav-aharoni Co-authored-by: Salvador de la Puente González Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Co-authored-by: Sanket Panda Co-authored-by: abbycross Co-authored-by: Kevin Tian Co-authored-by: Rathish Cholarajan Co-authored-by: Blake Johnson Co-authored-by: Kevin J. Sung Co-authored-by: Jake Lishman Co-authored-by: Kaelyn Ferris <43348706+kaelynj@users.noreply.github.com> Co-authored-by: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> Co-authored-by: Matthew Treinish Co-authored-by: Diego Ristè <10402430+dieris@users.noreply.github.com> Co-authored-by: Frank Harkins Co-authored-by: Luciano Bello --- .../integration-tests-qiskit-main.yml | 52 ++ .pylintrc | 1 + docs/apidocs/fake_provider.rst | 4 + docs/apidocs/ibm-runtime.rst | 2 + docs/apidocs/transpiler.rst | 4 + docs/conf.py | 4 +- program_source/circuit_runner/__init__.py | 0 qiskit_ibm_runtime/VERSION.txt | 2 +- qiskit_ibm_runtime/accounts/account.py | 4 +- qiskit_ibm_runtime/accounts/management.py | 3 +- qiskit_ibm_runtime/api/client_parameters.py | 2 +- qiskit_ibm_runtime/api/clients/__init__.py | 8 +- qiskit_ibm_runtime/api/clients/auth.py | 172 ++++++ qiskit_ibm_runtime/api/clients/backend.py | 4 +- .../api/clients/base_websocket_client.py | 300 ++++++++++ qiskit_ibm_runtime/api/clients/runtime.py | 3 +- qiskit_ibm_runtime/api/clients/runtime_ws.py | 74 +++ qiskit_ibm_runtime/api/clients/version.py | 46 ++ qiskit_ibm_runtime/api/rest/__init__.py | 2 +- qiskit_ibm_runtime/api/rest/base.py | 12 - qiskit_ibm_runtime/api/rest/cloud_backend.py | 2 +- qiskit_ibm_runtime/api/rest/program_job.py | 109 ++++ qiskit_ibm_runtime/api/rest/root.py | 103 ++++ qiskit_ibm_runtime/api/rest/runtime.py | 6 +- qiskit_ibm_runtime/base_primitive.py | 2 +- qiskit_ibm_runtime/estimator.py | 8 +- qiskit_ibm_runtime/exceptions.py | 20 +- qiskit_ibm_runtime/fake_provider/__init__.py | 8 +- .../fake_provider/fake_backend.py | 18 +- qiskit_ibm_runtime/hub_group_project.py | 2 +- qiskit_ibm_runtime/ibm_backend.py | 34 +- qiskit_ibm_runtime/ibm_qubit_properties.py | 54 ++ qiskit_ibm_runtime/provider_session.py | 132 +++++ qiskit_ibm_runtime/proxies/__init__.py | 17 + qiskit_ibm_runtime/proxies/configuration.py | 127 +++++ qiskit_ibm_runtime/qiskit_runtime_service.py | 7 +- qiskit_ibm_runtime/runtime_job.py | 3 +- qiskit_ibm_runtime/session.py | 3 +- qiskit_ibm_runtime/transpiler/__init__.py | 2 +- .../transpiler/passes/__init__.py | 4 +- .../transpiler/passes/basis/__init__.py | 4 +- .../transpiler/passes/scheduling/__init__.py | 49 +- .../passes/scheduling/dynamical_decoupling.py | 18 +- .../transpiler/passes/scheduling/utils.py | 112 +++- qiskit_ibm_runtime/transpiler/plugin.py | 2 +- qiskit_ibm_runtime/utils/__init__.py | 4 +- qiskit_ibm_runtime/utils/backend_converter.py | 2 +- qiskit_ibm_runtime/utils/backend_decoder.py | 167 ++++++ qiskit_ibm_runtime/utils/converters.py | 239 ++++++++ qiskit_ibm_runtime/utils/hgp.py | 42 ++ qiskit_ibm_runtime/utils/json.py | 26 +- qiskit_ibm_runtime/utils/options.py | 58 ++ qiskit_ibm_runtime/utils/pubsub.py | 182 ++++++ ...lidate-provider-code-b07fea8644aa8f43.yaml | 5 + ...ix-duration-patching-b80d45d77481dfa6.yaml | 11 + ...iskit-1.0-compatible-6fbf17d2dd28cb48.yaml | 7 + .../notes/fix-qpy-bug-739cefc2c9018d0b.yaml | 8 + requirements-dev.txt | 1 - requirements.txt | 1 - setup.py | 1 - test/fake_account_client.py | 531 ------------------ test/integration/test_auth_client.py | 5 +- test/integration/test_backend.py | 4 +- test/integration/test_ibm_job.py | 37 +- test/integration/test_ibm_job_attributes.py | 2 +- test/integration/test_options.py | 2 +- test/integration/test_proxies.py | 17 +- test/integration/test_results.py | 32 -- test/integration/test_retrieve_job.py | 4 +- test/qctrl/__init__.py | 11 + test/qctrl/test_qctrl.py | 2 - test/unit/fake_provider/__init__.py | 11 + test/unit/fake_provider/test_fake_backends.py | 2 +- test/unit/mock/fake_api_backend.py | 2 +- test/unit/mock/fake_runtime_client.py | 2 +- test/unit/mock/proxy_server.py | 2 +- test/unit/test_account.py | 2 +- test/unit/test_backend.py | 5 +- test/unit/test_backend_retrieval.py | 2 +- test/unit/test_client_parameters.py | 2 +- test/unit/test_data_serialization.py | 2 +- test/unit/test_ibm_primitives.py | 2 +- test/unit/test_ibm_primitives_v2.py | 2 +- test/unit/test_options.py | 3 +- test/unit/test_runtime_ws.py | 2 - test/unit/test_session.py | 3 +- .../scheduling/test_dynamical_decoupling.py | 35 +- .../passes/scheduling/test_scheduler.py | 27 +- .../passes/scheduling/test_utils.py | 28 + test/utils.py | 26 +- tox.ini | 2 +- 91 files changed, 2292 insertions(+), 816 deletions(-) create mode 100644 .github/workflows/integration-tests-qiskit-main.yml create mode 100644 docs/apidocs/fake_provider.rst create mode 100644 docs/apidocs/transpiler.rst delete mode 100644 program_source/circuit_runner/__init__.py create mode 100644 qiskit_ibm_runtime/api/clients/auth.py create mode 100644 qiskit_ibm_runtime/api/clients/base_websocket_client.py create mode 100644 qiskit_ibm_runtime/api/clients/runtime_ws.py create mode 100644 qiskit_ibm_runtime/api/clients/version.py create mode 100644 qiskit_ibm_runtime/api/rest/program_job.py create mode 100644 qiskit_ibm_runtime/api/rest/root.py create mode 100644 qiskit_ibm_runtime/ibm_qubit_properties.py create mode 100644 qiskit_ibm_runtime/provider_session.py create mode 100644 qiskit_ibm_runtime/proxies/__init__.py create mode 100644 qiskit_ibm_runtime/proxies/configuration.py create mode 100644 qiskit_ibm_runtime/utils/backend_decoder.py create mode 100644 qiskit_ibm_runtime/utils/converters.py create mode 100644 qiskit_ibm_runtime/utils/hgp.py create mode 100644 qiskit_ibm_runtime/utils/options.py create mode 100644 qiskit_ibm_runtime/utils/pubsub.py create mode 100644 releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml create mode 100644 releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml create mode 100644 releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml create mode 100644 releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml delete mode 100644 test/fake_account_client.py create mode 100644 test/qctrl/__init__.py create mode 100644 test/unit/fake_provider/__init__.py diff --git a/.github/workflows/integration-tests-qiskit-main.yml b/.github/workflows/integration-tests-qiskit-main.yml new file mode 100644 index 000000000..2aee5c0ec --- /dev/null +++ b/.github/workflows/integration-tests-qiskit-main.yml @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +name: Integration Tests (Qiskit Main) +on: + schedule: + # run integration tests against Qiskit main once a week + - cron: '0 0 * * 0' + workflow_dispatch: +jobs: + integration-tests: + if: github.repository_owner == 'Qiskit' + name: Run integration tests - ${{ matrix.environment }} + runs-on: ${{ matrix.os }} + strategy: + # avoid cancellation of in-progress jobs if any matrix job fails + fail-fast: false + matrix: + python-version: [ 3.9 ] + os: [ "ubuntu-latest" ] + environment: [ "ibm-quantum-production", "ibm-quantum-staging", "ibm-cloud-production", "ibm-cloud-staging" ] + environment: ${{ matrix.environment }} + env: + QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN }} + QISKIT_IBM_URL: ${{ secrets.QISKIT_IBM_URL }} + QISKIT_IBM_INSTANCE: ${{ secrets.QISKIT_IBM_INSTANCE }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -r requirements-dev.txt -e . git+https://github.com/Qiskit/qiskit.git + - name: Run integration tests + run: make integration-test diff --git a/.pylintrc b/.pylintrc index 5d5cb1eb0..a455fd605 100644 --- a/.pylintrc +++ b/.pylintrc @@ -379,6 +379,7 @@ function-naming-style=snake_case good-names=i, j, k, + dt, ex, Run, _ diff --git a/docs/apidocs/fake_provider.rst b/docs/apidocs/fake_provider.rst new file mode 100644 index 000000000..357d30545 --- /dev/null +++ b/docs/apidocs/fake_provider.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.fake_provider + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/ibm-runtime.rst b/docs/apidocs/ibm-runtime.rst index 29b06f462..cafc459a1 100644 --- a/docs/apidocs/ibm-runtime.rst +++ b/docs/apidocs/ibm-runtime.rst @@ -9,3 +9,5 @@ qiskit-ibm-runtime API reference runtime_service options + transpiler + fake_provider diff --git a/docs/apidocs/transpiler.rst b/docs/apidocs/transpiler.rst new file mode 100644 index 000000000..157dd4062 --- /dev/null +++ b/docs/apidocs/transpiler.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.transpiler + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/conf.py b/docs/conf.py index e8f3e7eb0..b312c6c34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,8 +25,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -docs_url_prefix = "ecosystem/ibm-runtime" -release = '0.18.1' +release = '0.19.2' # -- General configuration --------------------------------------------------- @@ -41,6 +40,7 @@ 'reno.sphinxext', 'nbsphinx', 'sphinxcontrib.katex', + 'matplotlib.sphinxext.plot_directive', ] templates_path = ['_templates'] diff --git a/program_source/circuit_runner/__init__.py b/program_source/circuit_runner/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index 249afd517..61e6e92d9 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.18.1 +0.19.2 diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 2e5ed6bbb..eaa93ccc8 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -18,8 +18,8 @@ from urllib.parse import urlparse from requests.auth import AuthBase -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.utils.hgp import from_instance_format +from ..proxies import ProxyConfiguration +from ..utils.hgp import from_instance_format from .exceptions import InvalidAccountError, CloudResourceNameResolutionError from ..api.auth import QuantumAuth, CloudAuth diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index 4992998a6..14aadd89b 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -15,8 +15,7 @@ import os from typing import Optional, Dict -from qiskit_ibm_provider.proxies import ProxyConfiguration - +from ..proxies import ProxyConfiguration from .exceptions import AccountNotFoundError from .account import Account, ChannelType from .storage import save_config, read_config, delete_config diff --git a/qiskit_ibm_runtime/api/client_parameters.py b/qiskit_ibm_runtime/api/client_parameters.py index 3c722b789..1cffd4948 100644 --- a/qiskit_ibm_runtime/api/client_parameters.py +++ b/qiskit_ibm_runtime/api/client_parameters.py @@ -13,7 +13,7 @@ """Represent IBM Quantum account client parameters.""" from typing import Dict, Optional, Any, Union -from qiskit_ibm_provider.proxies import ProxyConfiguration +from ..proxies import ProxyConfiguration from ..utils import get_runtime_api_base_url from ..api.auth import QuantumAuth, CloudAuth diff --git a/qiskit_ibm_runtime/api/clients/__init__.py b/qiskit_ibm_runtime/api/clients/__init__.py index eba744e56..58af1d496 100644 --- a/qiskit_ibm_runtime/api/clients/__init__.py +++ b/qiskit_ibm_runtime/api/clients/__init__.py @@ -12,8 +12,8 @@ """IBM Quantum API clients.""" -from qiskit_ibm_provider.api.clients.base import BaseClient, WebsocketClientCloseCode -from qiskit_ibm_provider.api.clients.auth import AuthClient -from qiskit_ibm_provider.api.clients.version import VersionClient -from qiskit_ibm_provider.api.clients.runtime_ws import RuntimeWebsocketClient +from .base_websocket_client import WebsocketClientCloseCode +from .auth import AuthClient +from .version import VersionClient +from .runtime_ws import RuntimeWebsocketClient from .runtime import RuntimeClient diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py new file mode 100644 index 000000000..da27813de --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/auth.py @@ -0,0 +1,172 @@ +# 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. + +"""Client for accessing IBM Quantum authentication services.""" + +from typing import Dict, List, Optional, Any, Union +from requests.exceptions import RequestException + +from ..auth import QuantumAuth +from ..exceptions import AuthenticationLicenseError, RequestsApiError +from ..rest import Api +from ..session import RetrySession +from ..client_parameters import ClientParameters + + +class AuthClient: + """Client for accessing IBM Quantum authentication services.""" + + def __init__(self, client_params: ClientParameters) -> None: + """AuthClient constructor. + + Args: + client_params: Parameters used for server connection. + """ + self.api_token = client_params.token + self.auth_url = client_params.url + self._service_urls = {} # type: ignore[var-annotated] + + self.auth_api = Api(RetrySession(self.auth_url, **client_params.connection_parameters())) + self.base_api = self._init_service_clients(**client_params.connection_parameters()) + + def _init_service_clients(self, **request_kwargs: Any) -> Api: + """Initialize the clients used for communicating with the API. + + Args: + **request_kwargs: Arguments for the request ``Session``. + + Returns: + Client for the API server. + """ + # Request an access token. + self.access_token = self._request_access_token() + self.auth_api.session.auth = QuantumAuth(access_token=self.access_token) + self._service_urls = self.user_urls() + + # Create the api server client, using the access token. + base_api = Api( + RetrySession( + self._service_urls["http"], + auth=QuantumAuth(access_token=self.access_token), + **request_kwargs, + ) + ) + + return base_api + + def _request_access_token(self) -> str: + """Request a new access token from the API authentication service. + + Returns: + A new access token. + + Raises: + AuthenticationLicenseError: If the user hasn't accepted the license agreement. + RequestsApiError: If the request failed. + """ + try: + response = self.auth_api.login(self.api_token) + return response["id"] + except RequestsApiError as ex: + # Get the original exception that raised. + original_exception = ex.__cause__ + + if isinstance(original_exception, RequestException): + # Get the response from the original request exception. + error_response = ( + # pylint: disable=no-member + original_exception.response + ) + if error_response is not None and error_response.status_code == 401: + try: + error_code = error_response.json()["error"]["name"] + if error_code == "ACCEPT_LICENSE_REQUIRED": + message = error_response.json()["error"]["message"] + raise AuthenticationLicenseError(message) + except (ValueError, KeyError): + # the response did not contain the expected json. + pass + raise + + # User account-related public functions. + + def user_urls(self) -> Dict[str, str]: + """Retrieve the API URLs from the authentication service. + + Returns: + A dict with the base URLs for the services. Currently + supported keys are: + + * ``http``: The API URL for HTTP communication. + * ``ws``: The API URL for websocket communication. + * ``services`: The API URL for additional services. + """ + response = self.auth_api.user_info() + return response["urls"] + + def user_hubs(self) -> List[Dict[str, str]]: + """Retrieve the hub/group/project sets available to the user. + + The first entry in the list will be the default set, as indicated by + the ``isDefault`` field from the API. + + Returns: + A list of dictionaries with the hub, group, and project values keyed by + ``hub``, ``group``, and ``project``, respectively. + """ + response = self.base_api.hubs() + + hubs = [] # type: ignore[var-annotated] + for hub in response: + hub_name = hub["name"] + for group_name, group in hub["groups"].items(): + for project_name, project in group["projects"].items(): + entry = { + "hub": hub_name, + "group": group_name, + "project": project_name, + } + + # Move to the top if it is the default h/g/p. + if project.get("isDefault"): + hubs.insert(0, entry) + else: + hubs.append(entry) + + return hubs + + # Miscellaneous public functions. + + def api_version(self) -> Dict[str, Union[str, bool]]: + """Return the version of the API. + + Returns: + API version. + """ + return self.base_api.version() + + def current_access_token(self) -> Optional[str]: + """Return the current access token. + + Returns: + The access token in use. + """ + return self.access_token + + def current_service_urls(self) -> Dict: + """Return the current service URLs. + + Returns: + A dict with the base URLs for the services, in the same + format as :meth:`user_urls()`. + """ + return self._service_urls diff --git a/qiskit_ibm_runtime/api/clients/backend.py b/qiskit_ibm_runtime/api/clients/backend.py index d779ea7f5..b53996ff5 100644 --- a/qiskit_ibm_runtime/api/clients/backend.py +++ b/qiskit_ibm_runtime/api/clients/backend.py @@ -17,12 +17,10 @@ from datetime import datetime as python_datetime from abc import ABC, abstractmethod -from qiskit_ibm_provider.api.clients.base import BaseClient - logger = logging.getLogger(__name__) -class BaseBackendClient(BaseClient, ABC): +class BaseBackendClient(ABC): """Client for accessing backend information.""" @abstractmethod diff --git a/qiskit_ibm_runtime/api/clients/base_websocket_client.py b/qiskit_ibm_runtime/api/clients/base_websocket_client.py new file mode 100644 index 000000000..d918eafc3 --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/base_websocket_client.py @@ -0,0 +1,300 @@ +# 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. +# pylint: disable=unused-argument + +"""Base clients for accessing IBM Quantum.""" + +import logging +from typing import Optional, Any, Dict +from queue import Queue +from abc import ABC +from abc import abstractmethod +import traceback +import time +import enum + +from websocket import WebSocketApp, STATUS_NORMAL, STATUS_ABNORMAL_CLOSED + +from ..client_parameters import ClientParameters +from ..exceptions import WebsocketError, WebsocketTimeoutError + +logger = logging.getLogger(__name__) + + +class WebsocketClientCloseCode(enum.IntEnum): + """Possible values used for closing websocket connection.""" + + NORMAL = 1 + TIMEOUT = 2 + PROTOCOL_ERROR = 3 + CANCEL = 4 + + +class BaseWebsocketClient(ABC): + """Base class for websocket clients.""" + + BACKOFF_MAX = 8 + """Maximum time to wait between retries.""" + + def __init__( + self, + websocket_url: str, + client_params: ClientParameters, + job_id: str, + message_queue: Optional[Queue] = None, + ) -> None: + """BaseWebsocketClient constructor. + + Args: + websocket_url: URL for websocket communication with IBM Quantum. + client_params: Parameters used for server connection. + job_id: Job ID. + message_queue: Queue used to hold received messages. + """ + self._websocket_url = websocket_url.rstrip("/") + self._proxy_params = ( + client_params.proxies.to_ws_params(self._websocket_url) if client_params.proxies else {} + ) + self._access_token = client_params.token + self._job_id = job_id + self._message_queue = message_queue + self._header: Optional[Dict] = None + self._ws: Optional[WebSocketApp] = None + + self._authenticated = False + self._cancelled = False + self.connected = False + self._last_message: Any = None + self._current_retry = 0 + self._server_close_code = STATUS_ABNORMAL_CLOSED + self._client_close_code = None + self._error: Optional[str] = None + + def on_open(self, wsa: WebSocketApp) -> None: + """Called when websocket connection established. + + Args: + wsa: WebSocketApp object. + """ + logger.debug("Websocket connection established for job %s", self._job_id) + self.connected = True + if self._cancelled: + # Immediately disconnect if pre-cancelled. + self.disconnect(WebsocketClientCloseCode.CANCEL) + + def on_message(self, wsa: WebSocketApp, message: str) -> None: + """Called when websocket message received. + + Args: + wsa: WebSocketApp object. + message: Message received. + """ + try: + self._handle_message(message) + except Exception as err: # pylint: disable=broad-except + self._error = self._format_exception(err) + self.disconnect(WebsocketClientCloseCode.PROTOCOL_ERROR) + + @abstractmethod + def _handle_message(self, message: str) -> None: + """Handle received message. + + Args: + message: Message received. + """ + pass + + def on_close(self, wsa: WebSocketApp, status_code: int, msg: str) -> None: + """Called when websocket connection clsed. + + Args: + wsa: WebSocketApp object. + status_code: Status code. + msg: Close message. + """ + # Assume abnormal close if no code is given. + self._server_close_code = status_code or STATUS_ABNORMAL_CLOSED + self.connected = False + logger.debug( + "Websocket connection for job %s closed. status code=%s, message=%s", + self._job_id, + status_code, + msg, + ) + + def on_error(self, wsa: WebSocketApp, error: Exception) -> None: + """Called when a websocket error occurred. + + Args: + wsa: WebSocketApp object. + error: Encountered error. + """ + self._error = self._format_exception(error) + + def stream( + self, + url: str, + retries: int = 5, + backoff_factor: float = 0.5, + ) -> Any: + """Stream from the websocket. + + Args: + url: Websocket url to use. + retries: Max number of retries. + backoff_factor: Backoff factor used to calculate the + time to wait between retries. + + Returns: + The final message received. + + Raises: + WebsocketError: If the websocket connection ended unexpectedly. + WebsocketTimeoutError: If the operation timed out. + """ + self._reset_state() + self._cancelled = False + + while self._current_retry <= retries: + self._ws = WebSocketApp( + url, + header=self._header, + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + ) + try: + logger.debug( + "Starting new websocket connection: %s using proxy %s", + url, + self._proxy_params, + ) + self._reset_state() + self._ws.run_forever(ping_interval=60, ping_timeout=10, **self._proxy_params) + self.connected = False + + logger.debug("Websocket run_forever finished.") + + # Handle path-specific errors + self._handle_stream_iteration() + + if self._client_close_code in ( + WebsocketClientCloseCode.NORMAL, + WebsocketClientCloseCode.CANCEL, + ): + # If we closed the connection with a normal code. + return self._last_message + + if self._client_close_code == WebsocketClientCloseCode.TIMEOUT: + raise WebsocketTimeoutError( + "Timeout reached while getting job status." + ) from None + + if self._server_close_code == STATUS_NORMAL and self._error is None: + return self._last_message + + msg_to_log = ( + f"A websocket error occurred while streaming for job " + f"{self._job_id}. Connection closed with {self._server_close_code}." + ) + if self._error is not None: + msg_to_log += f"\n{self._error}" + logger.info(msg_to_log) + + self._current_retry += 1 + if self._current_retry > retries: + error_message = ( + "Max retries exceeded: Failed to establish a websocket connection." + ) + if self._error: + error_message += f" Error: {self._error}" + + raise WebsocketError(error_message) + finally: + self.disconnect(None) + + # Sleep then retry. + backoff_time = self._backoff_time(backoff_factor, self._current_retry) + logger.info( + "Retrying get_job_status via websocket after %s seconds: Attempt #%s", + backoff_time, + self._current_retry, + ) + time.sleep(backoff_time) + + # Execution should not reach here, sanity check. + exception_message = ( + "Max retries exceeded: Failed to establish a websocket " + "connection due to a network error." + ) + + logger.info(exception_message) + raise WebsocketError(exception_message) + + @abstractmethod + def _handle_stream_iteration(self) -> None: + """Called at the end of an iteration.""" + pass + + def _backoff_time(self, backoff_factor: float, current_retry_attempt: int) -> float: + """Calculate the backoff time to wait for. + + Exponential backoff time formula:: + {backoff_factor} * (2 ** (current_retry_attempt - 1)) + + Args: + backoff_factor: Backoff factor, in seconds. + current_retry_attempt: Current number of retry attempts. + + Returns: + The number of seconds to wait for, before making the next retry attempt. + """ + backoff_time = backoff_factor * (2 ** (current_retry_attempt - 1)) + return min(self.BACKOFF_MAX, backoff_time) + + def disconnect( + self, + close_code: Optional[WebsocketClientCloseCode] = WebsocketClientCloseCode.NORMAL, + ) -> None: + """Close the websocket connection. + + Args: + close_code: Disconnect status code. + """ + if self._ws is not None: + logger.debug("Client closing websocket connection with code %s.", close_code) + self._client_close_code = close_code + self._ws.close() + if close_code == WebsocketClientCloseCode.CANCEL: + self._cancelled = True + + def _format_exception(self, error: Exception) -> str: + """Format the exception. + + Args: + error: Exception to be formatted. + + Returns: + Formatted exception. + """ + return "".join( + traceback.format_exception(type(error), error, getattr(error, "__traceback__", "")) + ) + + def _reset_state(self) -> None: + """Reset state for a new connection.""" + self._authenticated = False + self.connected = False + self._error = None + self._server_close_code = None + self._client_close_code = None diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index e1e08c499..ddbd41771 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -17,13 +17,12 @@ from datetime import datetime as python_datetime from requests import Response -from qiskit_ibm_provider.utils.hgp import from_instance_format from qiskit_ibm_runtime.api.session import RetrySession from .backend import BaseBackendClient from ..rest.runtime import Runtime from ..client_parameters import ClientParameters - +from ...utils.hgp import from_instance_format logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py new file mode 100644 index 000000000..bc88ed276 --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/runtime_ws.py @@ -0,0 +1,74 @@ +# 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. + +"""Client for accessing IBM Quantum runtime service.""" + +import logging +from typing import Optional +from queue import Queue + +from .base_websocket_client import BaseWebsocketClient +from ..client_parameters import ClientParameters + +logger = logging.getLogger(__name__) + + +class RuntimeWebsocketClient(BaseWebsocketClient): + """Client for websocket communication with the IBM Quantum runtime service.""" + + def __init__( + self, + websocket_url: str, + client_params: ClientParameters, + job_id: str, + message_queue: Optional[Queue] = None, + ) -> None: + """WebsocketClient constructor. + + Args: + websocket_url: URL for websocket communication with IBM Quantum. + client_params: Parameters used for server connection. + job_id: Job ID. + message_queue: Queue used to hold received messages. + """ + super().__init__(websocket_url, client_params, job_id, message_queue) + self._header = client_params.get_auth_handler().get_headers() + + def _handle_message(self, message: str) -> None: + """Handle received message. + + Args: + message: Message received. + """ + if not self._authenticated: + self._authenticated = True # First message is an ACK + else: + self._message_queue.put_nowait(message) + self._current_retry = 0 + + def job_results(self, max_retries: int = 5, backoff_factor: float = 0.5) -> None: + """Return the interim result of a runtime job. + + Args: + max_retries: Max number of retries. + backoff_factor: Backoff factor used to calculate the + time to wait between retries. + + Raises: + WebsocketError: If a websocket error occurred. + """ + url = "{}/stream/jobs/{}".format(self._websocket_url, self._job_id) + self.stream(url=url, retries=max_retries, backoff_factor=backoff_factor) + + def _handle_stream_iteration(self) -> None: + """Handle a streaming iteration.""" + pass diff --git a/qiskit_ibm_runtime/api/clients/version.py b/qiskit_ibm_runtime/api/clients/version.py new file mode 100644 index 000000000..c8628f04d --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/version.py @@ -0,0 +1,46 @@ +# 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. + +"""Client for determining the version of an IBM Quantum service.""" + +from typing import Dict, Union, Any + +from ..rest.root import Api +from ..session import RetrySession + + +class VersionClient: + """Client for determining the version of an IBM Quantum service.""" + + def __init__(self, url: str, **request_kwargs: Any) -> None: + """VersionClient constructor. + + Args: + url: URL of the service. + **request_kwargs: Arguments for the request ``Session``. + """ + self.client_version_finder = Api(RetrySession(url, **request_kwargs)) + + def version(self) -> Dict[str, Union[bool, str]]: + """Return the version information. + + Returns: + A dictionary with information about the API version, + with the following keys: + + * ``new_api`` (bool): Whether the new API is being used + + And the following optional keys: + + * ``api-*`` (str): The versions of each individual API component + """ + return self.client_version_finder.version() diff --git a/qiskit_ibm_runtime/api/rest/__init__.py b/qiskit_ibm_runtime/api/rest/__init__.py index 32092ddd9..f98193de6 100644 --- a/qiskit_ibm_runtime/api/rest/__init__.py +++ b/qiskit_ibm_runtime/api/rest/__init__.py @@ -16,4 +16,4 @@ Job adaptor, for example, handles all /Jobs/{job id} endpoints. """ -from qiskit_ibm_provider.api.rest.root import Api +from .root import Api diff --git a/qiskit_ibm_runtime/api/rest/base.py b/qiskit_ibm_runtime/api/rest/base.py index 04e8ab0e6..809dc606f 100644 --- a/qiskit_ibm_runtime/api/rest/base.py +++ b/qiskit_ibm_runtime/api/rest/base.py @@ -43,15 +43,3 @@ def get_url(self, identifier: str) -> str: 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 d15f2fc47..7964e5d58 100644 --- a/qiskit_ibm_runtime/api/rest/cloud_backend.py +++ b/qiskit_ibm_runtime/api/rest/cloud_backend.py @@ -15,7 +15,7 @@ from typing import Dict, Any, Optional from datetime import datetime as python_datetime -from qiskit_ibm_provider.api.rest.base import RestAdapterBase +from qiskit_ibm_runtime.api.rest.base import RestAdapterBase from ..session import RetrySession diff --git a/qiskit_ibm_runtime/api/rest/program_job.py b/qiskit_ibm_runtime/api/rest/program_job.py new file mode 100644 index 000000000..e50d10af7 --- /dev/null +++ b/qiskit_ibm_runtime/api/rest/program_job.py @@ -0,0 +1,109 @@ +# 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. + +"""Program Job REST adapter.""" + +import json +from typing import Dict +from requests import Response + +from .base import RestAdapterBase +from ..session import RetrySession +from ...utils.json import RuntimeDecoder + + +class ProgramJob(RestAdapterBase): + """Rest adapter for program job related endpoints.""" + + URL_MAP = { + "self": "", + "results": "/results", + "cancel": "/cancel", + "logs": "/logs", + "interim_results": "/interim_results", + "metrics": "/metrics", + "tags": "/tags", + } + + def __init__(self, session: RetrySession, job_id: str, url_prefix: str = "") -> None: + """ProgramJob constructor. + + Args: + session: Session to be used in the adapter. + job_id: ID of the program job. + url_prefix: Prefix to use in the URL. + """ + super().__init__(session, "{}/jobs/{}".format(url_prefix, job_id)) + + def get(self, exclude_params: bool = None) -> Dict: + """Return program job information. + + Args: + exclude_params: If ``True``, the params will not be included in the response. + + Returns: + JSON response. + """ + payload = {} + if exclude_params: + payload["exclude_params"] = "true" + return self.session.get(self.get_url("self"), params=payload).json(cls=RuntimeDecoder) + + def delete(self) -> None: + """Delete program job.""" + self.session.delete(self.get_url("self")) + + def interim_results(self) -> str: + """Return program job interim results. + + Returns: + Interim results. + """ + response = self.session.get(self.get_url("interim_results")) + return response.text + + def results(self) -> str: + """Return program job results. + + Returns: + Job results. + """ + response = self.session.get(self.get_url("results")) + return response.text + + def cancel(self) -> None: + """Cancel the job.""" + self.session.post(self.get_url("cancel")) + + def logs(self) -> str: + """Retrieve job logs. + + Returns: + Job logs. + """ + return self.session.get(self.get_url("logs")).text + + def metadata(self) -> Dict: + """Retrieve job metadata. + + Returns: + Job Metadata. + """ + return self.session.get(self.get_url("metrics")).json() + + def update_tags(self, tags: list) -> Response: + """Update job tags. + + Returns: + API Response. + """ + return self.session.put(self.get_url("tags"), data=json.dumps({"tags": tags})) diff --git a/qiskit_ibm_runtime/api/rest/root.py b/qiskit_ibm_runtime/api/rest/root.py new file mode 100644 index 000000000..f5f47af70 --- /dev/null +++ b/qiskit_ibm_runtime/api/rest/root.py @@ -0,0 +1,103 @@ +# 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. + +"""Root REST adapter.""" + +import logging +from typing import Dict, List, Any, Union +import json + +from .base import RestAdapterBase +from .program_job import ProgramJob + +logger = logging.getLogger(__name__) + + +class Api(RestAdapterBase): + """Rest adapter for general endpoints.""" + + URL_MAP = { + "login": "/users/loginWithToken", + "user_info": "/users/me", + "hubs": "/Network", + "version": "/version", + "bookings": "/Network/bookings/v2", + } + + def job(self, job_id: str) -> ProgramJob: + """Return an adapter for the job. + + Args: + job_id: ID of the job. + + Returns: + The backend adapter. + """ + return ProgramJob(self.session, job_id) + + # Client functions. + + def hubs(self) -> List[Dict[str, Any]]: + """Return the list of hub/group/project sets available to the user. + + Returns: + JSON response. + """ + url = self.get_url("hubs") + return self.session.get(url).json() + + def version(self) -> Dict[str, Union[str, bool]]: + """Return the version information. + + Returns: + A dictionary with information about the API version, + with the following keys: + + * ``new_api`` (bool): Whether the new API is being used + + And the following optional keys: + + * ``api-*`` (str): The versions of each individual API component + """ + url = self.get_url("version") + response = self.session.get(url) + + try: + version_info = response.json() + version_info["new_api"] = True + except json.JSONDecodeError: + return {"new_api": False, "api": response.text} + + return version_info + + def login(self, api_token: str) -> Dict[str, Any]: + """Login with token. + + Args: + api_token: API token. + + Returns: + JSON response. + """ + url = self.get_url("login") + return self.session.post(url, json={"apiToken": api_token}).json() + + def user_info(self) -> Dict[str, Any]: + """Return user information. + + Returns: + JSON response of user information. + """ + url = self.get_url("user_info") + response = self.session.get(url).json() + + return response diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index b5a58648b..354fc4e07 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -17,9 +17,9 @@ from typing import Dict, Any, List, Union, Optional import json -from qiskit_ibm_provider.api.rest.base import RestAdapterBase -from qiskit_ibm_provider.api.rest.program_job import ProgramJob -from qiskit_ibm_provider.utils import local_to_utc +from qiskit_ibm_runtime.api.rest.base import RestAdapterBase +from qiskit_ibm_runtime.api.rest.program_job import ProgramJob +from qiskit_ibm_runtime.utils import local_to_utc from .runtime_session import RuntimeSession from ...utils import RuntimeEncoder diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index ca93ebfde..effaa6897 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -22,7 +22,7 @@ from qiskit.providers.options import Options as TerraOptions -from qiskit_ibm_provider.session import get_cm_session as get_cm_provider_session +from .provider_session import get_cm_session as get_cm_provider_session from .options import Options from .options.options import BaseOptions, OptionsV2 diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 8d2ba9b80..ba7e56f4d 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -16,7 +16,6 @@ import os from typing import Optional, Dict, Sequence, Any, Union import logging -import typing from qiskit.circuit import QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -35,9 +34,6 @@ # pylint: disable=unused-import,cyclic-import from .session import Session -if typing.TYPE_CHECKING: - from qiskit.opflow import PauliSumOp - logger = logging.getLogger(__name__) @@ -236,7 +232,7 @@ def __init__( def run( # pylint: disable=arguments-differ self, circuits: QuantumCircuit | Sequence[QuantumCircuit], - observables: BaseOperator | PauliSumOp | Sequence[BaseOperator | PauliSumOp], + observables: BaseOperator | Sequence[BaseOperator], parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, **kwargs: Any, ) -> RuntimeJob: @@ -272,7 +268,7 @@ def run( # pylint: disable=arguments-differ def _run( # pylint: disable=arguments-differ self, circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], + observables: Sequence[BaseOperator], parameter_values: Sequence[Sequence[float]], **kwargs: Any, ) -> RuntimeJob: diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 9b4b1ee1c..f2fe7be11 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -28,12 +28,30 @@ class IBMAccountError(IBMError): pass -class IBMBackendApiProtocolError(IBMError): +class IBMBackendError(IBMError): + """Base class for errors raised by the backend modules.""" + + pass + + +class IBMBackendApiProtocolError(IBMBackendError): """Errors raised when an unexpected value is received from the server.""" pass +class IBMBackendValueError(IBMBackendError, ValueError): + """Value errors raised by the backend modules.""" + + pass + + +class IBMBackendApiError(IBMBackendError): + """Errors that occur unexpectedly when querying the server.""" + + pass + + class IBMInputValueError(IBMError): """Error raised due to invalid input value.""" diff --git a/qiskit_ibm_runtime/fake_provider/__init__.py b/qiskit_ibm_runtime/fake_provider/__init__.py index 722ed02dc..c35804a29 100644 --- a/qiskit_ibm_runtime/fake_provider/__init__.py +++ b/qiskit_ibm_runtime/fake_provider/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -====================================================== +======================================================= Fake Provider (:mod:`qiskit_ibm_runtime.fake_provider`) -====================================================== +======================================================= .. currentmodule:: qiskit_ibm_runtime.fake_provider @@ -48,11 +48,11 @@ circuit.cx(0,1) circuit.cx(0,2) circuit.measure_all() - circuit.draw('mpl') + circuit.draw('mpl', style="iqp") # Transpile the ideal circuit to a circuit that can be directly executed by the backend transpiled_circuit = transpile(circuit, backend) - transpiled_circuit.draw('mpl') + transpiled_circuit.draw('mpl', style="iqp") # Run the transpiled circuit using the simulated fake backend job = backend.run(transpiled_circuit) diff --git a/qiskit_ibm_runtime/fake_provider/fake_backend.py b/qiskit_ibm_runtime/fake_provider/fake_backend.py index 023d4beee..58b7ac19b 100644 --- a/qiskit_ibm_runtime/fake_provider/fake_backend.py +++ b/qiskit_ibm_runtime/fake_provider/fake_backend.py @@ -29,7 +29,6 @@ from qiskit import pulse from qiskit.exceptions import QiskitError from qiskit.utils import optionals as _optionals -from qiskit.providers import basicaer from qiskit.transpiler import Target from qiskit.providers import Options from qiskit.providers.backend_compat import convert_to_target @@ -39,6 +38,11 @@ decode_pulse_defaults, ) +try: + from qiskit.providers.basicaer import QasmSimulatorPy as BasicSimulator +except ImportError: + from qiskit.providers.basic_provider import BasicSimulator + class _Credentials: def __init__(self, token: str = "123456", url: str = "https://") -> None: @@ -128,7 +132,7 @@ def _setup_sim(self) -> None: self.set_options(noise_model=noise_model) else: - self.sim = basicaer.QasmSimulatorPy() + self.sim = BasicSimulator() def _get_conf_dict_from_json(self) -> dict: if not self.conf_filename: @@ -205,7 +209,7 @@ def _default_options(cls) -> Options: return AerSimulator._default_options() else: - return basicaer.QasmSimulatorPy._default_options() + return BasicSimulator._default_options() @property def dtm(self) -> float: @@ -304,12 +308,12 @@ def run(self, run_input, **options): # type: ignore This method runs circuit jobs (an individual or a list of QuantumCircuit ) and pulse jobs (an individual or a list of Schedule or ScheduleBlock) - using BasicAer or Aer simulator and returns a + using BasicAer simulator/ BasicSimulator or Aer simulator and returns a :class:`~qiskit.providers.Job` object. If qiskit-aer is installed, jobs will be run using AerSimulator with noise model of the fake backend. Otherwise, jobs will be run using - BasicAer simulator without noise. + BasicAer simulator/ BasicSimulator simulator without noise. Currently noisy simulation of a pulse job is not supported yet in FakeBackendV2. @@ -475,7 +479,7 @@ def _setup_sim(self) -> None: # it when run() is called self.set_options(noise_model=noise_model) else: - self.sim = basicaer.QasmSimulatorPy() + self.sim = BasicSimulator() def properties(self) -> BackendProperties: """Return backend properties""" @@ -536,7 +540,7 @@ def _default_options(cls) -> Options: return QasmSimulator._default_options() else: - return basicaer.QasmSimulatorPy._default_options() + return BasicSimulator._default_options() def run(self, run_input, **kwargs): # type: ignore """Main job in simulator""" diff --git a/qiskit_ibm_runtime/hub_group_project.py b/qiskit_ibm_runtime/hub_group_project.py index 1b213eadb..6fe5a60c5 100644 --- a/qiskit_ibm_runtime/hub_group_project.py +++ b/qiskit_ibm_runtime/hub_group_project.py @@ -15,7 +15,6 @@ import logging from typing import Any, List -from qiskit_ibm_provider.utils.hgp import from_instance_format from qiskit_ibm_runtime import ( # pylint: disable=unused-import ibm_backend, qiskit_runtime_service, @@ -23,6 +22,7 @@ from .api.client_parameters import ClientParameters from .api.clients import RuntimeClient +from .utils.hgp import from_instance_format logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 77374df2d..accf571d0 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -40,39 +40,31 @@ ) from qiskit.transpiler.target import Target -from qiskit_ibm_provider.utils.backend_decoder import ( - defaults_from_server_data, - properties_from_server_data, -) -from qiskit_ibm_provider.utils import local_to_utc, are_circuits_dynamic -from qiskit_ibm_provider.utils.options import QASM2Options, QASM3Options -from qiskit_ibm_provider.exceptions import IBMBackendValueError, IBMBackendApiError -from qiskit_ibm_provider.api.exceptions import RequestsApiError - # temporary until we unite the 2 Session classes -from qiskit_ibm_provider.session import ( +from .provider_session import ( Session as ProviderSession, -) # temporary until we unite the 2 Session classes +) from .utils.utils import validate_job_tags from . import qiskit_runtime_service # pylint: disable=unused-import,cyclic-import from .runtime_job import RuntimeJob from .api.clients import RuntimeClient -from .api.clients.backend import BaseBackendClient -from .exceptions import IBMBackendApiProtocolError +from .exceptions import IBMBackendApiProtocolError, IBMBackendValueError, IBMBackendApiError from .utils.backend_converter import ( convert_to_target, ) from .utils.default_session import get_cm_session as get_cm_primitive_session +from .utils.backend_decoder import ( + defaults_from_server_data, + properties_from_server_data, +) +from .utils.options import QASM2Options, QASM3Options +from .api.exceptions import RequestsApiError +from .utils import local_to_utc, are_circuits_dynamic + +from .utils.pubsub import Publisher -# If using a new-enough version of the IBM Provider, access the pub/sub -# mechanism from it as a broker, but fall back to Qiskit if we're using -# an old version (in which case it will also be falling back to Qiskit). -try: - from qiskit_ibm_provider.utils.pubsub import Publisher -except ImportError: - from qiskit.tools.events.pubsub import Publisher # pylint: disable=ungrouped-imports logger = logging.getLogger(__name__) @@ -179,7 +171,7 @@ def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], service: "qiskit_runtime_service.QiskitRuntimeService", - api_client: BaseBackendClient, + api_client: RuntimeClient, instance: Optional[str] = None, ) -> None: """IBMBackend constructor. diff --git a/qiskit_ibm_runtime/ibm_qubit_properties.py b/qiskit_ibm_runtime/ibm_qubit_properties.py new file mode 100644 index 000000000..d6228bc4a --- /dev/null +++ b/qiskit_ibm_runtime/ibm_qubit_properties.py @@ -0,0 +1,54 @@ +# 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. + +"""Module for Qubit Properties of an IBM Quantum Backend.""" + +from qiskit.providers.backend import QubitProperties + + +class IBMQubitProperties(QubitProperties): + """A representation of the properties of a qubit on an IBM backend.""" + + __slots__ = ( # pylint: disable=redefined-slots-in-subclass + "t1", + "t2", + "frequency", + "anharmonicity", + "operational", + ) + + def __init__( # type: ignore[no-untyped-def] + self, + t1=None, + t2=None, + frequency=None, + anharmonicity=None, + operational=True, + ): + """Create a new ``IBMQubitProperties`` object + + Args: + t1: The T1 time for a qubit in secs + t2: The T2 time for a qubit in secs + frequency: The frequency of a qubit in Hz + anharmonicity: The anharmonicity of a qubit in Hz + operational: A boolean value representing if this qubit is operational. + """ + super().__init__(t1=t1, t2=t2, frequency=frequency) + self.anharmonicity = anharmonicity + self.operational = operational + + def __repr__(self): # type: ignore[no-untyped-def] + return ( + f"IBMQubitProperties(t1={self.t1}, t2={self.t2}, frequency={self.frequency}, " + f"anharmonicity={self.anharmonicity})" + ) diff --git a/qiskit_ibm_runtime/provider_session.py b/qiskit_ibm_runtime/provider_session.py new file mode 100644 index 000000000..dad3dc407 --- /dev/null +++ b/qiskit_ibm_runtime/provider_session.py @@ -0,0 +1,132 @@ +# 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 flexible session.""" + +from typing import Optional, Type, Union +from types import TracebackType +from contextvars import ContextVar + +from .utils.converters import hms_to_seconds + + +class Session: + """Class for creating a flexible Qiskit Runtime session. + + A Qiskit Runtime ``session`` allows you to group a collection of iterative calls to + the quantum computer. A session is started when the first job within the session + is started. Subsequent jobs within the session are prioritized by the scheduler. + Data used within a session, such as transpiled circuits, is also cached to avoid + unnecessary overhead. + + You can open a Qiskit Runtime session using this ``Session`` class + and submit one or more jobs. + + For example:: + + from qiskit.test.reference_circuits import ReferenceCircuits + from qiskit_ibm_runtime import QiskitRuntimeService + + circ = ReferenceCircuits.bell() + backend = QiskitRuntimeService().get_backend("ibmq_qasm_simulator") + + backend.open_session() + job = backend.run(circ) + print(f"Job ID: {job.job_id()}") + print(f"Result: {job.result()}") + # Close the session only if all jobs are finished and + # you don't need to run more in the session. + backend.cancel_session() + + Session can also be used as a context manager:: + + with backend.open_session() as session: + job = backend.run(ReferenceCircuits.bell()) + + """ + + def __init__( + self, + max_time: Optional[Union[int, str]] = None, + ): + """Session constructor. + + Args: + 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 + `system imposed maximum + `_. + + Raises: + ValueError: If an input value is invalid. + """ + self._instance = None + self._session_id: Optional[str] = None + self._active = True + + self._max_time = ( + max_time + if max_time is None or isinstance(max_time, int) + else hms_to_seconds(max_time, "Invalid max_time value: ") + ) + + @property + def session_id(self) -> str: + """Return the session ID. + + Returns: + Session ID. None until a job runs in the session. + """ + return self._session_id + + @property + def active(self) -> bool: + """Return the status of the session. + + Returns: + True if the session is active, False otherwise. + """ + return self._active + + def cancel(self) -> None: + """Set the session._active status to False""" + self._active = False + + def __enter__(self) -> "Session": + set_cm_session(self) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + set_cm_session(None) + + +# Default session +_DEFAULT_SESSION: ContextVar[Optional[Session]] = ContextVar("_DEFAULT_SESSION", default=None) +_IN_SESSION_CM: ContextVar[bool] = ContextVar("_IN_SESSION_CM", default=False) + + +def set_cm_session(session: Optional[Session]) -> None: + """Set the context manager session.""" + _DEFAULT_SESSION.set(session) + _IN_SESSION_CM.set(session is not None) + + +def get_cm_session() -> Session: + """Return the context managed session.""" + return _DEFAULT_SESSION.get() diff --git a/qiskit_ibm_runtime/proxies/__init__.py b/qiskit_ibm_runtime/proxies/__init__.py new file mode 100644 index 000000000..c0212bfb0 --- /dev/null +++ b/qiskit_ibm_runtime/proxies/__init__.py @@ -0,0 +1,17 @@ +# 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. + +""" +Proxy configuration. +""" + +from .configuration import ProxyConfiguration diff --git a/qiskit_ibm_runtime/proxies/configuration.py b/qiskit_ibm_runtime/proxies/configuration.py new file mode 100644 index 000000000..0ed390dad --- /dev/null +++ b/qiskit_ibm_runtime/proxies/configuration.py @@ -0,0 +1,127 @@ +# 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. + +"""Proxy related classes and functions.""" + + +from dataclasses import dataclass +from typing import Optional, Dict, Any +from urllib.parse import urlparse + +from requests_ntlm import HttpNtlmAuth + + +@dataclass +class ProxyConfiguration: + """Class for representing a proxy configuration. + + Args + urls: a dictionary mapping protocol or protocol and host to the URL of the proxy. Refer to + https://docs.python-requests.org/en/latest/api/#requests.Session.proxies for details. + username_ntlm: username used to enable NTLM user authentication. + password_ntlm: password used to enable NTLM user authentication. + """ + + urls: Optional[Dict[str, str]] = None + username_ntlm: Optional[str] = None + password_ntlm: Optional[str] = None + + def validate(self) -> None: + """Validate configuration. + + Raises: + ValueError: If configuration is invalid. + """ + if not any( + [ + isinstance(self.username_ntlm, str) and isinstance(self.password_ntlm, str), + self.username_ntlm is None and self.password_ntlm is None, + ] + ): + raise ValueError( + f"Invalid proxy configuration for NTLM authentication. None or both of username and " + f"password must be provided. Got username_ntlm={self.username_ntlm}, " + f"password_ntlm={self.password_ntlm}." + ) + + if self.urls is not None and not isinstance(self.urls, dict): + raise ValueError( + f"Invalid proxy configuration. Expected `urls` to contain a dictionary mapping protocol " + f"or protocol and host to the URL of the proxy. Got {self.urls}" + ) + + def to_dict(self) -> dict: + """Transform configuration to dictionary.""" + + return {k: v for k, v in self.__dict__.items() if v is not None} + + def to_request_params(self) -> dict: + """Transform configuration to request parameters. + + Returns: + A dictionary with proxy configuration parameters in the format + expected by ``requests``. The following keys can be present: + ``proxies``and ``auth``. + """ + + request_kwargs = {} + if self.urls: + request_kwargs["proxies"] = self.urls + + if self.username_ntlm and self.password_ntlm: + request_kwargs["auth"] = HttpNtlmAuth(self.username_ntlm, self.password_ntlm) + + return request_kwargs + + def to_ws_params(self, ws_url: str) -> dict: + """Extract proxy information for websocket. + + Args: + ws_url: Websocket URL. + + Returns: + A dictionary with proxy configuration parameters in the format expected by websocket. + The following keys can be present: ``http_proxy_host``and ``http_proxy_port``, + ``proxy_type``, ``http_proxy_auth``. + """ + out: Any = {} + + if self.urls: + proxies = self.urls + url_parts = urlparse(ws_url) + proxy_keys = [ + ws_url, + "wss", + "https://" + url_parts.hostname, + "https", + "all://" + url_parts.hostname, + "all", + ] + for key in proxy_keys: + if key in proxies: + proxy_parts = urlparse(proxies[key], scheme="http") + out["http_proxy_host"] = proxy_parts.hostname + out["http_proxy_port"] = proxy_parts.port + out["proxy_type"] = ( + "http" if proxy_parts.scheme.startswith("http") else proxy_parts.scheme + ) + if proxy_parts.username and proxy_parts.password: + out["http_proxy_auth"] = ( + proxy_parts.username, + proxy_parts.password, + ) + break + + if self.username_ntlm and self.password_ntlm: + out["http_proxy_auth"] = (self.username_ntlm, self.password_ntlm) + + return out diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index f3cbdda4c..681c49ef9 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -29,10 +29,11 @@ QasmBackendConfiguration, ) -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.utils.hgp import to_instance_format, from_instance_format -from qiskit_ibm_provider.utils.backend_decoder import configuration_from_server_data from qiskit_ibm_runtime import ibm_backend +from .proxies import ProxyConfiguration +from .utils.hgp import to_instance_format, from_instance_format +from .utils.backend_decoder import configuration_from_server_data + from .utils.utils import validate_job_tags from .accounts import AccountManager, Account, ChannelType diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index e14843fb4..5bd2e1d7b 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -28,9 +28,10 @@ from qiskit.providers.job import JobV1 as Job # pylint: disable=unused-import,cyclic-import -from qiskit_ibm_provider.utils import utc_to_local + from qiskit_ibm_runtime import qiskit_runtime_service +from .utils import utc_to_local from .utils.utils import validate_job_tags from .utils.estimator_result_decoder import EstimatorResultDecoder from .utils.queueinfo import QueueInfo diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index cd9d66502..cb9da574d 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -17,14 +17,13 @@ from functools import wraps from threading import Lock -from qiskit_ibm_provider.utils.converters import hms_to_seconds - from qiskit_ibm_runtime import QiskitRuntimeService from .runtime_job import RuntimeJob from .utils.result_decoder import ResultDecoder from .ibm_backend import IBMBackend from .utils.default_session import set_cm_session from .utils.deprecation import deprecate_arguments +from .utils.converters import hms_to_seconds def _active_session(func): # type: ignore diff --git a/qiskit_ibm_runtime/transpiler/__init__.py b/qiskit_ibm_runtime/transpiler/__init__.py index d6e62daa4..838b70926 100644 --- a/qiskit_ibm_runtime/transpiler/__init__.py +++ b/qiskit_ibm_runtime/transpiler/__init__.py @@ -12,7 +12,7 @@ """ ==================================================================== -IBM Backend Transpiler Tools (:mod:`qiskit_ibm_provider.transpiler`) +IBM Backend Transpiler Tools (:mod:`qiskit_ibm_runtime.transpiler`) ==================================================================== A collection of transpiler tools for working with IBM Quantum's diff --git a/qiskit_ibm_runtime/transpiler/passes/__init__.py b/qiskit_ibm_runtime/transpiler/passes/__init__.py index 2fe16514c..2bd2bf181 100644 --- a/qiskit_ibm_runtime/transpiler/passes/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/__init__.py @@ -12,10 +12,10 @@ """ ================================================================ -Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) +Transpiler Passes (:mod:`qiskit_ibm_runtime.transpiler.passes`) ================================================================ -.. currentmodule:: qiskit_ibm_provider.transpiler.passes +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes A collection of transpiler passes for IBM backends. diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py index 0a71af010..e9e19bc13 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py @@ -12,10 +12,10 @@ """ ========================================================== -Basis (:mod:`qiskit_ibm_provider.transpiler.passes.basis`) +Basis (:mod:`qiskit_ibm_runtime.transpiler.passes.basis`) ========================================================== -.. currentmodule:: qiskit_ibm_provider.transpiler.passes.basis +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes.basis Passes to layout circuits to IBM backend's instruction sets. """ diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py index c3017e9bc..90308b73c 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py @@ -12,10 +12,10 @@ """ ==================================================================== -Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) +Scheduling (:mod:`qiskit_ibm_runtime.transpiler.passes.scheduling`) ==================================================================== -.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes.scheduling A collection of scheduling passes for working with IBM Quantum's next-generation backends that support advanced "dynamic circuit" capabilities. Ie., @@ -38,11 +38,10 @@ from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.passmanager import PassManager - from qiskit_ibm_provider.transpiler.passes.scheduling import DynamicCircuitInstructionDurations - from qiskit_ibm_provider.transpiler.passes.scheduling import ALAPScheduleAnalysis - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDelay - from qiskit.providers.fake_provider import FakeJakarta - + from qiskit_ibm_runtime.transpiler.passes.scheduling import DynamicCircuitInstructionDurations + from qiskit_ibm_runtime.transpiler.passes.scheduling import ALAPScheduleAnalysis + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDelay + from qiskit_ibm_runtime.fake_provider import FakeJakarta backend = FakeJakarta() @@ -80,7 +79,7 @@ # Transpile. scheduled_teleport = pm.run(teleport) - scheduled_teleport.draw(output="mpl") + scheduled_teleport.draw(output="mpl", style="iqp") Instead of padding with delays we may also insert a dynamical decoupling sequence @@ -90,7 +89,7 @@ from qiskit.circuit.library import XGate - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDynamicalDecoupling dd_sequence = [XGate(), XGate()] @@ -105,7 +104,7 @@ dd_teleport = pm.run(teleport) - dd_teleport.draw(output="mpl") + dd_teleport.draw(output="mpl", style="iqp") When compiling a circuit with Qiskit, it is more efficient and more robust to perform all the transformations in a single transpilation. This has been done above by extending Qiskit's preset @@ -123,7 +122,7 @@ qc_c_if = QuantumCircuit(1, 1) qc_c_if.x(0).c_if(0, 1) - qc_c_if.draw(output="mpl") + qc_c_if.draw(output="mpl", style="iqp") The :class:`.IBMBackend` configures a translation plugin :class:`.IBMTranslationPlugin` to automatically @@ -146,7 +145,7 @@ ) qc_if_dd = pm.run(qc_c_if, backend) - qc_if_dd.draw(output="mpl") + qc_if_dd.draw(output="mpl", style="iqp") If you are not using the transpiler plugin stages to @@ -168,7 +167,7 @@ ) qc_if_dd = pm.run(qc_c_if) - qc_if_dd.draw(output="mpl") + qc_if_dd.draw(output="mpl", style="iqp") Exploiting IBM backend's local parallel "fast-path" @@ -194,7 +193,7 @@ with qc.if_test((1, 1)): qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") The circuit below will not use the fast-path as the conditional gate is @@ -207,7 +206,7 @@ with qc.if_test((0, 1)): qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Similarly, the circuit below contains gates on multiple qubits and will not be performed using the fast-path. @@ -220,7 +219,7 @@ qc.x(0) qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") A fast-path block may contain multiple gates as long as they are on the fast-path qubit. If there are multiple fast-path blocks being performed in parallel each block will be @@ -238,7 +237,7 @@ with qc.if_test((1, 1)): qc.delay(1600, 1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") This behavior is also applied to the else condition of a fast-path eligible branch. @@ -253,7 +252,7 @@ with else_: qc.delay(1600, 0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") If a single measurement result is used with several conditional blocks, if there is a fast-path @@ -272,7 +271,7 @@ # Does not use fast-path qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") If you wish to prevent the usage of the fast-path you may insert a barrier between the measurement and the conditional branch. @@ -286,7 +285,7 @@ with qc.if_test((0, 1)): qc.x(0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Conditional measurements are not eligible for the fast-path. @@ -298,7 +297,7 @@ # Does not use the fast-path qc.measure(0, 1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Similarly nested control-flow is not eligible. @@ -312,7 +311,7 @@ with qc.if_test((0, 1)): qc.x(0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") The scheduler is aware of the fast-path behavior and will not insert delays on idle qubits @@ -345,11 +344,11 @@ qc.delay(1000, 0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") qc_dd = pm.run(qc) - qc_dd.draw(output="mpl") + qc_dd.draw(output="mpl", style="iqp") .. note:: If there are qubits that are *not* involved in a fast-path decision it is not @@ -374,7 +373,7 @@ # since the condition is compile time evaluated. qc.x(2) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Scheduling & Dynamical Decoupling diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py index 006c53feb..77d893f57 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py @@ -23,12 +23,16 @@ from qiskit.circuit.reset import Reset from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode, DAGOpNode from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates from qiskit.transpiler import CouplingMap +try: + from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer +except ImportError: + from qiskit.synthesis import OneQubitEulerDecomposer + from .block_base_padder import BlockBasePadder @@ -56,8 +60,8 @@ class PadDynamicalDecoupling(BlockBasePadder): from qiskit.transpiler import PassManager, InstructionDurations from qiskit.visualization import timeline_drawer - from qiskit_ibm_provider.transpiler.passes.scheduling import ALAPScheduleAnalysis - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + from qiskit_ibm_runtime.transpiler.passes.scheduling import ALAPScheduleAnalysis + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDynamicalDecoupling circ = QuantumCircuit(4) circ.h(0) @@ -103,7 +107,7 @@ def uhrig_pulse_location(k): .. note:: You need to call - :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.ALAPScheduleAnalysis` + :class:`~qiskit_ibm_runtime.transpiler.passes.scheduling.ALAPScheduleAnalysis` before running dynamical decoupling to guarantee your circuit satisfies acquisition alignment constraints for dynamic circuit backends. """ @@ -321,7 +325,11 @@ def _pre_runhook(self, dag: DAGCircuit) -> None: self._dd_sequence_lengths[qubit] = [] physical_index = dag.qubits.index(qubit) - if self._qubits and physical_index not in self._qubits: + if ( + self._qubits + and physical_index not in self._qubits + or qubit in self._idle_qubits + ): continue for index, gate in enumerate(seq): diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py index bf7665cd1..ec4710492 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py @@ -21,7 +21,9 @@ InstructionDurations, InstructionDurationsType, ) +from qiskit.transpiler.target import Target from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers import Backend, BackendV1 def block_order_op_nodes(dag: DAGCircuit) -> Generator[DAGOpNode, None, None]: @@ -150,6 +152,75 @@ def __init__( self._enable_patching = enable_patching super().__init__(instruction_durations=instruction_durations, dt=dt) + @classmethod + def from_backend(cls, backend: Backend) -> "DynamicCircuitInstructionDurations": + """Construct a :class:`DynamicInstructionDurations` object from the backend. + Args: + backend: backend from which durations (gate lengths) and dt are extracted. + Returns: + DynamicInstructionDurations: The InstructionDurations constructed from backend. + """ + if isinstance(backend, BackendV1): + # TODO Remove once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1 + # From here --------------------------------------- + def patch_from_backend(cls, backend: Backend): # type: ignore + """ + REMOVE me once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1 + """ + instruction_durations = [] + backend_properties = backend.properties() + if hasattr(backend_properties, "_gates"): + for gate, insts in backend_properties._gates.items(): + for qubits, props in insts.items(): + if "gate_length" in props: + gate_length = props["gate_length"][ + 0 + ] # Throw away datetime at index 1 + instruction_durations.append((gate, qubits, gate_length, "s")) + for ( + q, # pylint: disable=invalid-name + props, + ) in backend.properties()._qubits.items(): + if "readout_length" in props: + readout_length = props["readout_length"][ + 0 + ] # Throw away datetime at index 1 + instruction_durations.append(("measure", [q], readout_length, "s")) + try: + dt = backend.configuration().dt + except AttributeError: + dt = None + + return cls(instruction_durations, dt=dt) + + return patch_from_backend(DynamicCircuitInstructionDurations, backend) + # To here --------------------------------------- (remove comment ignore annotations too) + return super( # type: ignore # pylint: disable=unreachable + DynamicCircuitInstructionDurations, cls + ).from_backend(backend) + + # Get durations from target if BackendV2 + return cls.from_target(backend.target) + + @classmethod + def from_target(cls, target: Target) -> "DynamicCircuitInstructionDurations": + """Construct a :class:`DynamicInstructionDurations` object from the target. + Args: + target: target from which durations (gate lengths) and dt are extracted. + Returns: + DynamicInstructionDurations: The InstructionDurations constructed from backend. + """ + + instruction_durations_dict = target.durations().duration_by_name_qubits + instruction_durations = [] + for instr_key, instr_value in instruction_durations_dict.items(): + instruction_durations += [(*instr_key, *instr_value)] + try: + dt = target.dt + except AttributeError: + dt = None + return cls(instruction_durations, dt=dt) + def update( self, inst_durations: Optional[InstructionDurationsType], dt: float = None ) -> "DynamicCircuitInstructionDurations": @@ -206,15 +277,23 @@ def _patch_instruction(self, key: InstrKey) -> None: elif name == "reset": self._patch_reset(key) + def _convert_and_patch_key(self, key: InstrKey) -> None: + """Convert duration to dt and patch key""" + prev_duration, unit = self._get_duration(key) + if unit != "dt": + prev_duration = self._convert_unit(prev_duration, unit, "dt") + # raise TranspilerError('Can currently only patch durations of "dt".') + odd_cycle_correction = self._get_odd_cycle_correction() + new_duration = prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction + if unit != "dt": # convert back to original unit + new_duration = self._convert_unit(new_duration, "dt", unit) + self._patch_key(key, new_duration, unit) + def _patch_measurement(self, key: InstrKey) -> None: """Patch measurement duration by extending duration by 160dt as temporarily required by the dynamic circuit backend. """ - prev_duration, unit = self._get_duration_dt(key) - if unit != "dt": - raise TranspilerError('Can currently only patch durations of "dt".') - odd_cycle_correction = self._get_odd_cycle_correction() - self._patch_key(key, prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, unit) + self._convert_and_patch_key(key) # Enforce patching of reset on measurement update self._patch_reset(("reset", key[1], key[2])) @@ -227,31 +306,24 @@ def _patch_reset(self, key: InstrKey) -> None: # triggers the end of scheduling after the measurement pulse measure_key = ("measure", key[1], key[2]) try: - measure_duration, unit = self._get_duration_dt(measure_key) + measure_duration, unit = self._get_duration(measure_key) self._patch_key(key, measure_duration, unit) except KeyError: # Fall back to reset key if measure not available - prev_duration, unit = self._get_duration_dt(key) - if unit != "dt": - raise TranspilerError('Can currently only patch durations of "dt".') - odd_cycle_correction = self._get_odd_cycle_correction() - self._patch_key( - key, - prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, - unit, - ) + self._convert_and_patch_key(key) - def _get_duration_dt(self, key: InstrKey) -> Tuple[int, str]: + def _get_duration(self, key: InstrKey) -> Tuple[int, str]: """Handling for the complicated structure of this class. TODO: This class implementation should be simplified in Qiskit. Too many edge cases. """ if key[1] is None and key[2] is None: - return self.duration_by_name[key[0]] + duration = self.duration_by_name[key[0]] elif key[2] is None: - return self.duration_by_name_qubits[(key[0], key[1])] - - return self.duration_by_name_qubits_params[key] + duration = self.duration_by_name_qubits[(key[0], key[1])] + else: + duration = self.duration_by_name_qubits_params[key] + return duration def _patch_key(self, key: InstrKey, duration: int, unit: str) -> None: """Handling for the complicated structure of this class. diff --git a/qiskit_ibm_runtime/transpiler/plugin.py b/qiskit_ibm_runtime/transpiler/plugin.py index 75f70cfe4..077534395 100644 --- a/qiskit_ibm_runtime/transpiler/plugin.py +++ b/qiskit_ibm_runtime/transpiler/plugin.py @@ -20,7 +20,7 @@ from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes import ConvertConditionsToIfOps -from qiskit_ibm_provider.transpiler.passes.basis.convert_id_to_delay import ( +from qiskit_ibm_runtime.transpiler.passes.basis.convert_id_to_delay import ( ConvertIdToDelay, ) diff --git a/qiskit_ibm_runtime/utils/__init__.py b/qiskit_ibm_runtime/utils/__init__.py index 04e10428b..985f05b3a 100644 --- a/qiskit_ibm_runtime/utils/__init__.py +++ b/qiskit_ibm_runtime/utils/__init__.py @@ -35,11 +35,13 @@ to_python_identifier """ -from qiskit_ibm_provider.utils.converters import ( +from .converters import ( utc_to_local, local_to_utc, seconds_to_duration, duration_difference, + are_circuits_dynamic, ) from .utils import to_python_identifier, is_crn, get_runtime_api_base_url, resolve_crn from .json import RuntimeEncoder, RuntimeDecoder, to_base64_string +from . import pubsub diff --git a/qiskit_ibm_runtime/utils/backend_converter.py b/qiskit_ibm_runtime/utils/backend_converter.py index 1bf390bfc..3db605d10 100644 --- a/qiskit_ibm_runtime/utils/backend_converter.py +++ b/qiskit_ibm_runtime/utils/backend_converter.py @@ -36,7 +36,7 @@ PulseDefaults, ) -from qiskit_ibm_provider.ibm_qubit_properties import IBMQubitProperties +from ..ibm_qubit_properties import IBMQubitProperties def convert_to_target( diff --git a/qiskit_ibm_runtime/utils/backend_decoder.py b/qiskit_ibm_runtime/utils/backend_decoder.py new file mode 100644 index 000000000..d02fb90fb --- /dev/null +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -0,0 +1,167 @@ +# 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. + +"""Utilities for working with IBM Quantum backends.""" + +from typing import List, Dict, Union, Optional +import logging +import traceback + +import dateutil.parser +from qiskit.providers.models import ( + BackendProperties, + PulseDefaults, + PulseBackendConfiguration, + QasmBackendConfiguration, +) + +from .converters import utc_to_local_all + +logger = logging.getLogger(__name__) + + +def configuration_from_server_data( + raw_config: Dict, + instance: str = "", +) -> Optional[Union[QasmBackendConfiguration, PulseBackendConfiguration]]: + """Create an IBMBackend instance from raw server data. + + Args: + raw_config: Raw configuration. + instance: Service instance. + + Returns: + Backend configuration. + """ + # Make sure the raw_config is of proper type + if not isinstance(raw_config, dict): + logger.warning( # type: ignore[unreachable] + "An error occurred when retrieving backend " + "information. Some backends might not be available." + ) + return None + try: + _decode_backend_configuration(raw_config) + try: + return PulseBackendConfiguration.from_dict(raw_config) + except (KeyError, TypeError): + return QasmBackendConfiguration.from_dict(raw_config) + except Exception: # pylint: disable=broad-except + logger.warning( + 'Remote backend "%s" for service instance %s could not be instantiated due ' + "to an invalid server-side configuration", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(instance), + ) + logger.debug("Invalid device configuration: %s", traceback.format_exc()) + return None + + +def defaults_from_server_data(defaults: Dict) -> PulseDefaults: + """Decode pulse defaults data. + + Args: + defaults: Raw pulse defaults data. + + Returns: + A ``PulseDefaults`` instance. + """ + for item in defaults["pulse_library"]: + _decode_pulse_library_item(item) + + for cmd in defaults["cmd_def"]: + if "sequence" in cmd: + for instr in cmd["sequence"]: + _decode_pulse_qobj_instr(instr) + + return PulseDefaults.from_dict(defaults) + + +def properties_from_server_data(properties: Dict) -> BackendProperties: + """Decode backend properties. + + Args: + properties: Raw properties data. + + 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"]) + + properties = utc_to_local_all(properties) + return BackendProperties.from_dict(properties) + + +def _decode_backend_configuration(config: Dict) -> None: + """Decode backend configuration. + + Args: + config: A ``QasmBackendConfiguration`` or ``PulseBackendConfiguration`` + in dictionary format. + """ + config["online_date"] = dateutil.parser.isoparse(config["online_date"]) + + if "u_channel_lo" in config: + for u_channel_list in config["u_channel_lo"]: + for u_channel_lo in u_channel_list: + u_channel_lo["scale"] = _to_complex(u_channel_lo["scale"]) + + +def _to_complex(value: Union[List[float], complex]) -> complex: + """Convert the input value to type ``complex``. + + Args: + value: Value to be converted. + + Returns: + Input value in ``complex``. + + Raises: + TypeError: If the input value is not in the expected format. + """ + if isinstance(value, list) and len(value) == 2: + return complex(value[0], value[1]) + elif isinstance(value, complex): + return value + + raise TypeError("{} is not in a valid complex number format.".format(value)) + + +def _decode_pulse_library_item(pulse_library_item: Dict) -> None: + """Decode a pulse library item. + + Args: + pulse_library_item: A ``PulseLibraryItem`` in dictionary format. + """ + pulse_library_item["samples"] = [ + _to_complex(sample) for sample in pulse_library_item["samples"] + ] + + +def _decode_pulse_qobj_instr(pulse_qobj_instr: Dict) -> None: + """Decode a pulse Qobj instruction. + + Args: + pulse_qobj_instr: A ``PulseQobjInstruction`` in dictionary format. + """ + if "val" in pulse_qobj_instr: + pulse_qobj_instr["val"] = _to_complex(pulse_qobj_instr["val"]) + if "parameters" in pulse_qobj_instr and "amp" in pulse_qobj_instr["parameters"]: + pulse_qobj_instr["parameters"]["amp"] = _to_complex(pulse_qobj_instr["parameters"]["amp"]) diff --git a/qiskit_ibm_runtime/utils/converters.py b/qiskit_ibm_runtime/utils/converters.py new file mode 100644 index 000000000..2a48a3f44 --- /dev/null +++ b/qiskit_ibm_runtime/utils/converters.py @@ -0,0 +1,239 @@ +# 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. + +"""Utilities related to conversion.""" + +import re +from datetime import datetime, timedelta, timezone +from math import ceil +from typing import Union, Tuple, Any, Optional, List + +from dateutil import tz, parser +from qiskit.circuit import QuantumCircuit, ControlFlowOp +from qiskit_ibm_runtime.exceptions import IBMInputValueError + + +def utc_to_local(utc_dt: Union[datetime, str]) -> datetime: + """Convert a UTC ``datetime`` object or string to a local timezone ``datetime``. + + Args: + utc_dt: Input UTC `datetime` or string. + + Returns: + A ``datetime`` with the local timezone. + + Raises: + TypeError: If the input parameter value is not valid. + """ + if isinstance(utc_dt, str): + utc_dt = parser.parse(utc_dt) + if not isinstance(utc_dt, datetime): + raise TypeError("Input `utc_dt` is not string or datetime.") + utc_dt = utc_dt.replace(tzinfo=timezone.utc) # type: ignore[arg-type] + local_dt = utc_dt.astimezone(tz.tzlocal()) # type: ignore[attr-defined] + return local_dt + + +def local_to_utc(local_dt: Union[datetime, str]) -> datetime: + """Convert a local ``datetime`` object or string to a UTC ``datetime``. + + Args: + local_dt: Input local ``datetime`` or string. + + Returns: + A ``datetime`` in UTC. + + Raises: + TypeError: If the input parameter value is not valid. + """ + if isinstance(local_dt, str): + local_dt = parser.parse(local_dt) + if not isinstance(local_dt, datetime): + raise TypeError("Input `local_dt` is not string or datetime.") + + # Input is considered local if it's ``utcoffset()`` is ``None`` or none-zero. + if local_dt.utcoffset() is None or local_dt.utcoffset() != timedelta(0): + local_dt = local_dt.replace(tzinfo=tz.tzlocal()) + return local_dt.astimezone(tz.UTC) + return local_dt # Already in UTC. + + +def local_to_utc_str(local_dt: Union[datetime, str], suffix: str = "Z") -> str: + """Convert a local ``datetime`` object or string to a UTC string. + + Args: + local_dt: Input local ``datetime`` or string. + suffix: ``Z`` or ``+``, indicating whether the suffix should be ``Z`` or + ``+00:00``. + + Returns: + UTC datetime in ISO format. + """ + utc_dt_str = local_to_utc(local_dt).isoformat() + if suffix == "Z": + utc_dt_str = utc_dt_str.replace("+00:00", "Z") + return utc_dt_str + + +def convert_tz(input_dt: Optional[datetime], to_utc: bool) -> Optional[datetime]: + """Convert input timestamp timezone. + + Args: + input_dt: Timestamp to be converted. + to_utc: True if to convert to UTC, otherwise to local timezone. + + Returns: + Converted timestamp, or ``None`` if input is ``None``. + """ + if input_dt is None: + return None + if to_utc: + return local_to_utc(input_dt) + return utc_to_local(input_dt) + + +def utc_to_local_all(data: Any) -> Any: + """Recursively convert all ``datetime`` in the input data from local time to UTC. + + Note: + Only lists and dictionaries are traversed. + + Args: + data: Data to be converted. + + Returns: + Converted data. + """ + if isinstance(data, datetime): + return utc_to_local(data) + elif isinstance(data, list): + return [utc_to_local_all(elem) for elem in data] + elif isinstance(data, dict): + return {key: utc_to_local_all(elem) for key, elem in data.items()} + return data + + +def str_to_utc(utc_dt: Optional[str]) -> Optional[datetime]: + """Convert a UTC string to a ``datetime`` object with UTC timezone. + + Args: + utc_dt: Input UTC string in ISO format. + + Returns: + A ``datetime`` with the UTC timezone, or ``None`` if the input is ``None``. + """ + if not utc_dt: + return None + parsed_dt = parser.isoparse(utc_dt) + return parsed_dt.replace(tzinfo=timezone.utc) + + +def seconds_to_duration(seconds: float) -> Tuple[int, int, int, int, int]: + """Converts seconds in a datetime delta to a duration. + + Args: + seconds: Number of seconds in time delta. + + Returns: + A tuple containing the duration in terms of days, + hours, minutes, seconds, and milliseconds. + """ + days = int(seconds // (3600 * 24)) + hours = int((seconds // 3600) % 24) + minutes = int((seconds // 60) % 60) + seconds = seconds % 60 + millisec = 0 + if seconds < 1: + millisec = int(ceil(seconds * 1000)) + seconds = 0 + else: + seconds = int(seconds) + return days, hours, minutes, seconds, millisec + + +def duration_difference(date_time: datetime) -> str: + """Compute the estimated duration until the given datetime. + + Args: + date_time: The input local datetime. + + Returns: + String giving the estimated duration. + """ + time_delta = date_time.replace(tzinfo=None) - datetime.now() + time_tuple = seconds_to_duration(time_delta.total_seconds()) + # The returned tuple contains the duration in terms of + # days, hours, minutes, seconds, and milliseconds. + time_str = "" + if time_tuple[0]: + time_str += "{} days".format(time_tuple[0]) + time_str += " {} hrs".format(time_tuple[1]) + elif time_tuple[1]: + time_str += "{} hrs".format(time_tuple[1]) + time_str += " {} min".format(time_tuple[2]) + elif time_tuple[2]: + time_str += "{} min".format(time_tuple[2]) + time_str += " {} sec".format(time_tuple[3]) + elif time_tuple[3]: + time_str += "{} sec".format(time_tuple[3]) + return time_str + + +def hms_to_seconds(hms: str, msg_prefix: str = "") -> int: + """Convert duration specified as hours minutes seconds to seconds. + + Args: + hms: The string input duration (in hours minutes seconds). Ex: 2h 10m 20s + msg_prefix: Additional message to prefix the error. + + Returns: + Total seconds (int) in the duration. + + Raises: + IBMInputValueError: when the given hms string is in an invalid format + """ + + parsed_time = re.findall(r"(\d+[dhms])", hms) + total_seconds = 0 + + if parsed_time: + for time_unit in parsed_time: + unit = time_unit[-1] + value = int(time_unit[:-1]) + if unit == "d": + total_seconds += value * 86400 + elif unit == "h": + total_seconds += value * 3600 + elif unit == "m": + total_seconds += value * 60 + elif unit == "s": + total_seconds += value + else: + raise IBMInputValueError(f"{msg_prefix} Invalid input: {unit}") + else: + raise IBMInputValueError(f"{msg_prefix} Invalid input: {parsed_time}") + + return total_seconds + + +def are_circuits_dynamic(circuits: List[QuantumCircuit]) -> bool: + """Checks if the input circuits are dynamic.""" + for circuit in circuits: + if isinstance(circuit, str): + return True + for inst in circuit: + if ( + isinstance(inst.operation, ControlFlowOp) + or getattr(inst.operation, "condition", None) is not None + ): + return True + return False diff --git a/qiskit_ibm_runtime/utils/hgp.py b/qiskit_ibm_runtime/utils/hgp.py new file mode 100644 index 000000000..47438fc9b --- /dev/null +++ b/qiskit_ibm_runtime/utils/hgp.py @@ -0,0 +1,42 @@ +# 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. + +"""Hub/group/project utility functions.""" + +from typing import Tuple +from ..exceptions import IBMInputValueError + + +def from_instance_format(instance: str) -> Tuple[str, str, str]: + """Convert the input instance to [hub, group, project]. + + Args: + instance: Service instance in hub/group/project format. + + Returns: + Hub, group, and project. + + Raises: + IBMInputValueError: If input is not in the correct format. + """ + try: + hub, group, project = instance.split("/") + return hub, group, project + except (ValueError, AttributeError): + raise IBMInputValueError( + f"Input instance value {instance} is not in the" f"correct hub/group/project format." + ) + + +def to_instance_format(hub: str, group: str, project: str) -> str: + """Convert input to hub/group/project format.""" + return f"{hub}/{group}/{project}" diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index fcc634c9e..5bccfb3bf 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -56,16 +56,15 @@ from qiskit.circuit.parametertable import ParameterView from qiskit.result import Result from qiskit.version import __version__ as _terra_version_string - -from qiskit_ibm_provider.qpy import ( - _write_parameter, +from qiskit.utils import optionals +from qiskit.qpy import ( _write_parameter_expression, _read_parameter_expression, _read_parameter_expression_v3, - _read_parameter, - dump, load, + dump, ) +from qiskit.qpy.binary_io.value import _write_parameter, _read_parameter # TODO: Remove when they are in terra from ..qiskit.primitives import ObservablesArray, BindingsArray @@ -221,10 +220,15 @@ 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: 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. + kwargs["version"] = 10 value = _serialize_and_encode( data=obj, serializer=lambda buff, data: dump( - data, buff, RuntimeEncoder + data, buff, RuntimeEncoder, **kwargs ), # type: ignore[no-untyped-call] ) return {"__type__": "QuantumCircuit", "__value__": value} @@ -240,18 +244,26 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ data=obj, serializer=_write_parameter_expression, compress=False, + 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": 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. + kwargs["version"] = 10 # Append instruction to empty circuit quantum_register = QuantumRegister(obj.num_qubits) quantum_circuit = QuantumCircuit(quantum_register) quantum_circuit.append(obj, quantum_register) value = _serialize_and_encode( data=quantum_circuit, - serializer=lambda buff, data: dump(data, buff), # type: ignore[no-untyped-call] + serializer=lambda buff, data: dump( + data, buff, **kwargs + ), # type: ignore[no-untyped-call] ) return {"__type__": "Instruction", "__value__": value} if isinstance(obj, BasePub): diff --git a/qiskit_ibm_runtime/utils/options.py b/qiskit_ibm_runtime/utils/options.py new file mode 100644 index 000000000..34874dfdb --- /dev/null +++ b/qiskit_ibm_runtime/utils/options.py @@ -0,0 +1,58 @@ +# 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. + +"""Backend run options.""" + +from dataclasses import asdict, dataclass +from typing import Dict, Union, Any, Optional +from qiskit.circuit import QuantumCircuit +from qiskit.qobj.utils import MeasLevel, MeasReturnType + + +@dataclass +class CommonOptions: + """Options common for both paths.""" + + shots: int = 4000 + meas_level: Union[int, MeasLevel] = MeasLevel.CLASSIFIED + init_qubits: bool = True + rep_delay: Optional[float] = None + memory: bool = False + meas_return: Union[str, MeasReturnType] = MeasReturnType.AVERAGE + + def to_transport_dict(self) -> Dict[str, Any]: + """Remove None values so runtime defaults are used.""" + dict_ = asdict(self) + for key in list(dict_.keys()): + if dict_[key] is None: + del dict_[key] + return dict_ + + +@dataclass +class QASM3Options(CommonOptions): + """Options for the QASM3 path.""" + + init_circuit: Optional[QuantumCircuit] = None + init_num_resets: Optional[int] = None + + +@dataclass +class QASM2Options(CommonOptions): + """Options for the QASM2 path.""" + + header: Optional[Dict] = None + init_qubits: bool = True + use_measure_esp: Optional[bool] = None + # Simulator only + noise_model: Any = None + seed_simulator: Optional[int] = None diff --git a/qiskit_ibm_runtime/utils/pubsub.py b/qiskit_ibm_runtime/utils/pubsub.py new file mode 100644 index 000000000..e05fd211c --- /dev/null +++ b/qiskit_ibm_runtime/utils/pubsub.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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. + +""" +Message broker for the Publisher / Subscriber mechanism +""" + +from __future__ import annotations + +import typing + +from qiskit.exceptions import QiskitError + +try: + from qiskit.tools.events.pubsub import _Broker as _QiskitBroker +except ImportError: + _QiskitBroker = None + +_Callback = typing.Callable[..., None] + + +class _Broker: + """The event/message broker. It's a singleton. + + In order to keep consistency across all the components, it would be great to + have a specific format for new events, documenting their usage. + It's the responsibility of the component emitting an event to document it's usage in + the component docstring. + + Event format:: + + ".." + + Examples: + + * "ibm.job.start" + """ + + _instance: _Broker | None = None + _subscribers: dict[str, list[_Subscription]] = {} + + @staticmethod + def __new__(cls: type[_Broker]) -> _Broker: + if _Broker._instance is None: + # Backwards compatibility for Qiskit pre-1.0; if the Qiskit-internal broker + # singleton exists then we use that instead of defining a new one, so that + # the event streams will be unified even if someone is still using the + # Qiskit entry points to subscribe. + # + # This dynamic switch assumes that the interface of this vendored `Broker` + # code remains identical to the Qiskit 0.45 version. + _Broker._instance = object.__new__(_QiskitBroker or cls) + return _Broker._instance + + class _Subscription: + def __init__(self, event: str, callback: _Callback): + self.event: str = event + self.callback: _Callback = callback + + def __eq__(self, other: object) -> bool: + """Overrides the default implementation""" + if isinstance(other, self.__class__): + return self.event == other.event and id(self.callback) == id( + other.callback + ) # Allow 1:N subscribers + return False + + def subscribe(self, event: str, callback: _Callback) -> bool: + """Subscribes to an event, so when it's emitted all the callbacks subscribed, + will be executed. We are not allowing double registration. + + Args: + event (string): The event to subscribed in the form of: + "terra..." + callback (callable): The callback that will be executed when an event is + emitted. + """ + if not callable(callback): + raise QiskitError("Callback is not a callable!") + + if event not in self._subscribers: + self._subscribers[event] = [] + + new_subscription = self._Subscription(event, callback) + if new_subscription in self._subscribers[event]: + # We are not allowing double subscription + return False + + self._subscribers[event].append(new_subscription) + return True + + def dispatch(self, event: str, *args: typing.Any, **kwargs: typing.Any) -> None: + """Emits an event if there are any subscribers. + + Args: + event (String): The event to be emitted + args: Arguments linked with the event + kwargs: Named arguments linked with the event + """ + # No event, no subscribers. + if event not in self._subscribers: + return + + for subscriber in self._subscribers[event]: + subscriber.callback(*args, **kwargs) + + def unsubscribe(self, event: str, callback: _Callback) -> bool: + """Unsubscribe the specific callback to the event. + + Args + event (String): The event to unsubscribe + callback (callable): The callback that won't be executed anymore + + Returns + True: if we have successfully unsubscribed to the event + False: if there's no callback previously registered + """ + + try: + self._subscribers[event].remove(self._Subscription(event, callback)) + except KeyError: + return False + + return True + + def clear(self) -> None: + """Unsubscribe everything, leaving the Broker without subscribers/events.""" + self._subscribers.clear() + + +class Publisher: + """Represents a "publisher". + + Every component (class) can become a :class:`Publisher` and send events by + inheriting this class. Functions can call this class like:: + + Publisher().publish("event", args, ... ) + """ + + def __init__(self) -> None: + self._broker: _Broker = _Broker() + + def publish(self, event: str, *args: typing.Any, **kwargs: typing.Any) -> None: + """Triggers an event, and associates some data to it, so if there are any + subscribers, their callback will be called synchronously.""" + return self._broker.dispatch(event, *args, **kwargs) + + +class Subscriber: + """Represents a "subscriber". + + Every component (class) can become a :class:`Subscriber` and subscribe to events, + that will call callback functions when they are emitted. + """ + + def __init__(self) -> None: + self._broker: _Broker = _Broker() + + def subscribe(self, event: str, callback: _Callback) -> bool: + """Subscribes to an event, associating a callback function to that event, so + when the event occurs, the callback will be called. + + This is a blocking call, so try to keep callbacks as lightweight as possible.""" + return self._broker.subscribe(event, callback) + + def unsubscribe(self, event: str, callback: _Callback) -> bool: + """Unsubscribe a pair event-callback, so the callback will not be called anymore + when the event occurs.""" + return self._broker.unsubscribe(event, callback) + + def clear(self) -> None: + """Unsubscribe everything""" + self._broker.clear() diff --git a/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml b/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml new file mode 100644 index 000000000..5edb74f18 --- /dev/null +++ b/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + `qiskit-ibm-provider` is pending deprecation, and therefore will no longer be a + dependency for `qiskit-ibm-runtime`. diff --git a/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml new file mode 100644 index 000000000..bfe9cf25e --- /dev/null +++ b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Fix the patching of :class:`.DynamicCircuitInstructions` for instructions + with durations that are not in units of ``dt``. +upgrade: + - | + Extend :meth:`.DynamicCircuitInstructions.from_backend` to extract and + patch durations from both :class:`.BackendV1` and :class:`.BackendV2` + objects. Also add :meth:`.DynamicCircuitInstructions.from_target` to use a + :class:`.Target` object instead. diff --git a/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml b/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml new file mode 100644 index 000000000..53deecd60 --- /dev/null +++ b/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + `qiskit-ibm-runtime` is now compatible with Qiskit versions `>= 0.45`, + including `1.0.0`. + + diff --git a/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml b/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml new file mode 100644 index 000000000..8969fba28 --- /dev/null +++ b/releasenotes/notes/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/requirements-dev.txt b/requirements-dev.txt index 61ee53c7e..03f4af164 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,6 @@ websockets>=8 black~=22.0 coverage>=6.3 pylatexenc -mthree scikit-learn ddt>=1.2.0,!=1.4.0,!=1.4.3 diff --git a/requirements.txt b/requirements.txt index 9203f9632..15b6946d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,3 @@ python-dateutil>=2.8.0 websocket-client>=1.5.1 typing-extensions>=4.0.0 ibm-platform-services>=0.22.6 -qiskit-ibm-provider>=0.8.0 diff --git a/setup.py b/setup.py index eb68ea705..4dc75e143 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ "websocket-client>=1.5.1", "ibm-platform-services>=0.22.6", "pydantic", - "qiskit-ibm-provider>=0.8.0", ] # Handle version. diff --git a/test/fake_account_client.py b/test/fake_account_client.py deleted file mode 100644 index 5a2ec9654..000000000 --- a/test/fake_account_client.py +++ /dev/null @@ -1,531 +0,0 @@ -# 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. - -"""Fake AccountClient.""" - -import copy - -# TODO This can probably be merged with the one in test_ibm_job_states -import time -import uuid -import warnings -from concurrent.futures import ThreadPoolExecutor, wait -from datetime import timedelta, datetime -from random import randrange -from typing import Dict, Any - -from qiskit.providers.fake_provider.backends.poughkeepsie.fake_poughkeepsie import ( - FakePoughkeepsie, -) - -from qiskit_ibm_provider.api.exceptions import ( - RequestsApiError, - UserTimeoutExceededError, -) -from qiskit_ibm_provider.apiconstants import ApiJobStatus, API_JOB_FINAL_STATES - -VALID_RESULT_RESPONSE = { - "backend_name": "ibmqx2", - "backend_version": "1.1.1", - "job_id": "XC1323XG2", - "qobj_id": "Experiment1", - "success": True, - "results": [], -} -"""A valid job result response.""" - -VALID_RESULT = { - "header": { - "name": "Bell state", - "creg_sizes": [["c", 2]], - "clbit_labels": [["c", 0], ["c", 1]], - "qubit_labels": [["q", 0], ["q", 1]], - }, - "shots": 1024, - "status": "DONE", - "success": True, - "data": {"counts": {"0x0": 484, "0x3": 540}}, -} - -API_STATUS_TO_INT = { - ApiJobStatus.CREATING: 0, - ApiJobStatus.VALIDATING: 1, - ApiJobStatus.QUEUED: 2, - ApiJobStatus.RUNNING: 3, - ApiJobStatus.COMPLETED: 4, - ApiJobStatus.ERROR_RUNNING_JOB: 4, - ApiJobStatus.ERROR_VALIDATING_JOB: 4, - ApiJobStatus.CANCELLED: 4, -} - - -class BaseFakeJob: - """Base class for faking a remote job.""" - - _job_progress = [ - ApiJobStatus.CREATING, - ApiJobStatus.VALIDATING, - ApiJobStatus.QUEUED, - ApiJobStatus.RUNNING, - ApiJobStatus.COMPLETED, - ] - - def __init__( - self, - executor, - job_id, - qobj, - backend_name, - job_tags=None, - job_name=None, - experiment_id=None, - run_mode=None, - progress_time=0.5, - **kwargs, - ): - """Initialize a fake job.""" - self._job_id = job_id - self._status = ApiJobStatus.CREATING - self.qobj = qobj - self._result = None - self._backend_name = backend_name - self._job_tags = job_tags - self._job_name = job_name - self._experiment_id = experiment_id - self._creation_date = datetime.now() - self._run_mode = run_mode - self._queue_pos = kwargs.pop("queue_pos", "auto") - self._comp_time = kwargs.pop("est_completion", "auto") - self._queue_info = None - self._progress_time = progress_time - self._future = executor.submit(self._auto_progress) - - def _auto_progress(self): - """Automatically update job status.""" - for status in self._job_progress: - time.sleep(self._progress_time) - self._status = status - - if self._status == ApiJobStatus.COMPLETED: - self._save_result() - elif self._status == ApiJobStatus.ERROR_RUNNING_JOB: - self._save_bad_result() - - def _save_result(self): - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - for _ in range(len(self.qobj["experiments"])): - valid_result = copy.deepcopy(VALID_RESULT) - counts = randrange(1024) - valid_result["data"]["counts"] = {"0x0": counts, "0x3": 1024 - counts} - new_result["results"].append(valid_result) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - self._result = new_result - - def _save_bad_result(self): - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - new_result["success"] = False - new_result["error"] = {"message": "Kaboom", "code": 1234} - self._result = new_result - - def data(self): - """Return job data.""" - status = self._status - data = { - "job_id": self._job_id, - "kind": "q-object", - "status": status.value, - "creation_date": self._creation_date.isoformat(), - "_backend_info": {"name": self._backend_name}, - "client_info": {"qiskit": "0.23.5"}, - } - if self._job_tags: - data["tags"] = self._job_tags.copy() - if self._job_name: - data["name"] = self._job_name - if self._experiment_id: - data["experiment_id"] = self._experiment_id - if status == ApiJobStatus.ERROR_VALIDATING_JOB: - data["error"] = {"message": "Validation failed.", "code": 1234} - if status in [ApiJobStatus.RUNNING] + list(API_JOB_FINAL_STATES) and self._run_mode: - data["run_mode"] = self._run_mode - - time_per_step = {} - timestamp = self._creation_date - for api_stat in API_STATUS_TO_INT: # pylint: disable=consider-using-dict-items - if API_STATUS_TO_INT[status] > API_STATUS_TO_INT[api_stat]: - time_per_step[api_stat.value] = timestamp.isoformat() - timestamp += timedelta(seconds=30) - elif status == api_stat: - time_per_step[api_stat.value] = timestamp.isoformat() - timestamp += timedelta(seconds=30) - data["time_per_step"] = time_per_step - - return data - - def _get_info_queue(self): - self._queue_info = { - "status": "PENDING_IN_QUEUE", - "position": randrange(1, 10) if self._queue_pos == "auto" else self._queue_pos, - } - if self._queue_info["position"] is None: - return self._queue_info - - est_comp_ts = ( - self._creation_date + timedelta(minutes=10 * self._queue_info["position"]) - if self._comp_time == "auto" - else self._comp_time - ) - if est_comp_ts is None: - return self._queue_info - - self._queue_info["estimated_complete_time"] = est_comp_ts.isoformat() - self._queue_info["estimated_start_time"] = (est_comp_ts - timedelta(minutes=20)).isoformat() - - return self._queue_info - - def cancel(self): - """Cancel the job.""" - self._future.cancel() - wait([self._future]) - self._status = ApiJobStatus.CANCELLED - self._result = None - - def result(self): - """Return job result.""" - if not self._result: - raise RequestsApiError("Result is not available") - return self._result - - def status_data(self): - """Return job status data, including queue info.""" - status = self._status - data = {"status": status.value} - if status == ApiJobStatus.QUEUED: - data["info_queue"] = self._get_info_queue() - return data - - def status(self): - """Return job status.""" - return self._status - - def name(self): - """Return job name.""" - return self._job_name - - -class CancelableFakeJob(BaseFakeJob): - """Fake job that can be canceled.""" - - _job_progress = [ - ApiJobStatus.CREATING, - ApiJobStatus.VALIDATING, - ApiJobStatus.RUNNING, - ] - - -class NewFieldFakeJob(BaseFakeJob): - """Fake job that contains additional fields.""" - - def data(self): - """Return job data.""" - data = super().data() - data["new_field"] = "foo" - return data - - -class MissingFieldFakeJob(BaseFakeJob): - """Fake job that does not contain required fields.""" - - def data(self): - """Return job data.""" - data = super().data() - del data["job_id"] - return data - - -class FailedFakeJob(BaseFakeJob): - """Fake job that fails.""" - - _job_progress = [ApiJobStatus.CREATING, ApiJobStatus.VALIDATING] - - def __init__(self, *args, **kwargs): - # failure_type can be "validation", "result", or "partial" - self._failure_type = kwargs.pop("failure_type", "validation") - self._job_progress = FailedFakeJob._job_progress.copy() - if self._failure_type == "validation": - self._job_progress.append(ApiJobStatus.ERROR_VALIDATING_JOB) - else: - self._job_progress.extend([ApiJobStatus.RUNNING, ApiJobStatus.ERROR_RUNNING_JOB]) - super().__init__(*args, **kwargs) - - def _save_bad_result(self): - if self._failure_type != "partial": - super()._save_bad_result() - return - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - new_result["success"] = False - # Good first result. - valid_result = copy.deepcopy(VALID_RESULT) - counts = randrange(1024) - valid_result["data"]["counts"] = {"0x0": counts, "0x3": 1024 - counts} - new_result["results"].append(valid_result) - - for _ in range(1, len(self.qobj["experiments"])): - valid_result = copy.deepcopy(VALID_RESULT) - valid_result["success"] = False - valid_result["status"] = "This circuit failed." - new_result["results"].append(valid_result) - self._result = new_result - - -class FixedStatusFakeJob(BaseFakeJob): - """Fake job that stays in a specific status.""" - - def __init__(self, *args, **kwargs): - self._fixed_status = kwargs.pop("fixed_status") - super().__init__(*args, **kwargs) - - def _auto_progress(self): - """Automatically update job status.""" - for status in self._job_progress: - time.sleep(0.5) - self._status = status - if status == self._fixed_status: - break - - if self._status == ApiJobStatus.COMPLETED: - self._save_result() - - -class BaseFakeAccountClient: - """Base class for faking the AccountClient.""" - - def __init__( - self, - job_limit=-1, - job_class=BaseFakeJob, - job_kwargs=None, - props_count=None, - queue_positions=None, - est_completion=None, - run_mode=None, - ): - """Initialize a fake account client.""" - self._jobs = {} - self._results_retrieved = set() - self._job_limit = job_limit - self._executor = ThreadPoolExecutor() - self._job_class = job_class - if isinstance(self._job_class, list): - self._job_class.reverse() - self._job_kwargs = job_kwargs or {} - self._props_count = props_count or 0 - self._props_date = datetime.now().isoformat() - self._queue_positions = queue_positions.copy() if queue_positions else [] - self._queue_positions.reverse() - self._est_completion = est_completion.copy() if est_completion else [] - self._est_completion.reverse() - self._run_mode = run_mode - self._default_job_class = BaseFakeJob - - def list_jobs(self, limit, skip, descending=True, extra_filter=None): - """Return a list of jobs.""" - # pylint: disable=unused-argument - extra_filter = extra_filter or {} - if all(fil in extra_filter for fil in ["creationDate", "id"]): - return {} - tag = extra_filter.get("tags", None) - all_job_data = [] - for job in list(self._jobs.values())[skip : skip + limit]: - job_data = job.data() - if tag is None or tag in job_data["tags"]: - all_job_data.append(job_data) - if not descending: - all_job_data.reverse() - return all_job_data - - def job_submit( - self, - backend_name, - qobj_dict, - job_name, - job_tags, - experiment_id, - *_args, - **_kwargs, - ): - """Submit a Qobj to a device.""" - if self._job_limit != -1 and self._unfinished_jobs() >= self._job_limit: - raise RequestsApiError( - "400 Client Error: Bad Request for url: . Reached " - "maximum number of concurrent jobs, Error code: 3458." - ) - - new_job_id = uuid.uuid4().hex - if isinstance(self._job_class, list): - job_class = self._job_class.pop() if self._job_class else self._default_job_class - else: - job_class = self._job_class - job_kwargs = copy.copy(self._job_kwargs) - if self._queue_positions: - job_kwargs["queue_pos"] = self._queue_positions.pop() - if self._est_completion: - job_kwargs["est_completion"] = self._est_completion.pop() - - run_mode = self._run_mode - if run_mode == "dedicated_once": - run_mode = "dedicated" - self._run_mode = "fairshare" - - new_job = job_class( - executor=self._executor, - job_id=new_job_id, - qobj=qobj_dict, - backend_name=backend_name, - job_tags=job_tags, - job_name=job_name, - experiment_id=experiment_id, - run_mode=run_mode, - **job_kwargs, - ) - self._jobs[new_job_id] = new_job - return new_job.data() - - def job_download_qobj(self, job_id, *_args, **_kwargs): - """Retrieve and return a Qobj.""" - return copy.deepcopy(self._get_job(job_id).qobj) - - def job_result(self, job_id, *_args, **_kwargs): - """Return a random job result.""" - if job_id in self._results_retrieved: - warnings.warn(f"Result already retrieved for job {job_id}") - self._results_retrieved.add(job_id) - return self._get_job(job_id).result() - - def job_get(self, job_id, *_args, **_kwargs): - """Return information about a job.""" - return self._get_job(job_id).data() - - def job_status(self, job_id, *_args, **_kwargs): - """Return the status of a job.""" - return self._get_job(job_id).status_data() - - def job_final_status(self, job_id, *_args, **_kwargs): - """Wait until the job progress to a final state.""" - job = self._get_job(job_id) - status = job.status() - while status not in API_JOB_FINAL_STATES: - time.sleep(0.5) - status_data = job.status_data() - status = ApiJobStatus(status_data["status"]) - if _kwargs.get("status_queue", None): - data = {"status": status.value} - if status is ApiJobStatus.QUEUED: - data["infoQueue"] = {"status": "PENDING_IN_QUEUE", "position": 1} - _kwargs["status_queue"].put(status_data) - return self.job_status(job_id) - - def job_properties(self, *_args, **_kwargs): - """Return the backend properties of a job.""" - props = FakePoughkeepsie().properties().to_dict() - if self._props_count > 0: - self._props_count -= 1 - new_dt = datetime.now() + timedelta(hours=randrange(300)) - self._props_date = new_dt.isoformat() - props["last_update_date"] = self._props_date - return props - - def job_cancel(self, job_id, *_args, **_kwargs): - """Submit a request for cancelling a job.""" - self._get_job(job_id).cancel() - return {"cancelled": True} - - def backend_job_limit(self, *_args, **_kwargs): - """Return the job limit for the backend.""" - return {"maximumJobs": self._job_limit, "runningJobs": self._unfinished_jobs()} - - def job_update_attribute(self, job_id, attr_name, attr_value, *_args, **_kwargs): - """Update the specified job attribute with the given value.""" - job = self._get_job(job_id) - if attr_name == "name": - job._job_name = attr_value - if attr_name == "tags": - job._job_tags = attr_value.copy() - return {attr_name: attr_value} - - def backend_status(self, backend_name: str) -> Dict[str, Any]: - """Return the status of the backend.""" - return { - "backend_name": backend_name, - "backend_version": "0.0.0", - "operational": True, - "pending_jobs": 0, - "status_msg": "active", - } - - def tear_down(self): - """Clean up job threads.""" - for job_id in list(self._jobs.keys()): - try: - self._jobs[job_id].cancel() - except KeyError: - pass - - def _unfinished_jobs(self): - """Return the number of unfinished jobs.""" - return sum(1 for job in self._jobs.values() if job.status() not in API_JOB_FINAL_STATES) - - def _get_job(self, job_id): - """Return job if found.""" - if job_id not in self._jobs: - raise RequestsApiError("Job not found. Error code: 3250.") - return self._jobs[job_id] - - -class JobSubmitFailClient(BaseFakeAccountClient): - """Fake AccountClient used to fail a job submit.""" - - def __init__(self, failed_indexes): - """JobSubmitFailClient constructor.""" - if not isinstance(failed_indexes, list): - failed_indexes = [failed_indexes] - self._failed_indexes = failed_indexes - self._job_count = -1 - super().__init__() - - def job_submit(self, *_args, **_kwargs): # pylint: disable=arguments-differ - """Failing job submit.""" - self._job_count += 1 - if self._job_count in self._failed_indexes: - raise RequestsApiError("Job submit failed!") - return super().job_submit(*_args, **_kwargs) - - -class JobTimeoutClient(BaseFakeAccountClient): - """Fake AccountClient used to fail a job submit.""" - - def __init__(self, *args, max_fail_count=-1, **kwargs): - """JobTimeoutClient constructor.""" - self._fail_count = max_fail_count - super().__init__(*args, **kwargs) - - def job_final_status(self, job_id, *_args, **_kwargs): - """Wait until the job progress to a final state.""" - if self._fail_count != 0: - self._fail_count -= 1 - raise UserTimeoutExceededError("Job timed out!") - return super().job_final_status(job_id, *_args, **_kwargs) diff --git a/test/integration/test_auth_client.py b/test/integration/test_auth_client.py index 37c4d19bb..9bab9ac5e 100644 --- a/test/integration/test_auth_client.py +++ b/test/integration/test_auth_client.py @@ -14,7 +14,8 @@ import re -from qiskit_ibm_provider.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient from ..ibm_test_case import IBMTestCase @@ -41,7 +42,7 @@ def test_url_404(self, dependencies: IntegrationTestDependencies) -> None: def test_invalid_token(self, dependencies: IntegrationTestDependencies) -> None: """Test login using invalid token.""" qe_token = "INVALID_TOKEN" - with self.assertRaises(RequestsApiError): + with self.assertRaises(IBMNotAuthorizedError): _ = self._init_auth_client(qe_token, dependencies.url) @integration_test_setup(supported_channel=["ibm_quantum"], init_service=False) diff --git a/test/integration/test_backend.py b/test/integration/test_backend.py index cc9d4c29d..0e23415cd 100644 --- a/test/integration/test_backend.py +++ b/test/integration/test_backend.py @@ -20,8 +20,8 @@ from qiskit import QuantumCircuit from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit_ibm_provider.ibm_qubit_properties import IBMQubitProperties -from qiskit_ibm_provider.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.ibm_qubit_properties import IBMQubitProperties +from qiskit_ibm_runtime.exceptions import IBMBackendValueError from qiskit_ibm_runtime import QiskitRuntimeService diff --git a/test/integration/test_ibm_job.py b/test/integration/test_ibm_job.py index 45e56fcc1..09184b65a 100644 --- a/test/integration/test_ibm_job.py +++ b/test/integration/test_ibm_job.py @@ -14,18 +14,13 @@ import copy import time from datetime import datetime, timedelta -from unittest import SkipTest, mock -from unittest import skip +from unittest import SkipTest from dateutil import tz from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.compiler import transpile from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit_ibm_provider.api.rest.job import Job as RestJob -from qiskit_ibm_provider.exceptions import IBMBackendApiError - -from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.exceptions import RuntimeJobTimeoutError, RuntimeJobNotFound from ..ibm_test_case import IBMIntegrationTestCase @@ -226,7 +221,7 @@ def test_retrieve_jobs_order(self): job = self.sim_backend.run(self.bell) job.wait_for_final_state() newest_jobs = self.service.jobs( - limit=10, + limit=20, pending=False, descending=True, created_after=self.last_month, @@ -273,34 +268,6 @@ def test_wait_for_final_state_timeout(self): thread.join(0.1) cancel_job_safe(job, self.log) - @skip("not supported by api") - def test_job_submit_partial_fail(self): - """Test job submit partial fail.""" - job_id = [] - - def _side_effect(self, *args, **kwargs): - # pylint: disable=unused-argument - job_id.append(self.job_id) - raise RequestsApiError("Kaboom") - - fail_points = ["put_object_storage", "callback_upload"] - - for fail_method in fail_points: - with self.subTest(fail_method=fail_method): - with mock.patch.object( - RestJob, fail_method, side_effect=_side_effect, autospec=True - ): - with self.assertRaises(IBMBackendApiError): - self.sim_backend.run(self.bell) - - self.assertTrue(job_id, "Job ID not saved.") - job = self.service.job(job_id[0]) - self.assertEqual( - job.status(), - JobStatus.CANCELLED, - f"Job {job.job_id()} status is {job.status()} and not cancelled!", - ) - def test_job_circuits(self): """Test job circuits.""" self.assertEqual(str(self.bell), str(self.sim_job.inputs["circuits"][0])) diff --git a/test/integration/test_ibm_job_attributes.py b/test/integration/test_ibm_job_attributes.py index fd9ff1ad5..4469f4da6 100644 --- a/test/integration/test_ibm_job_attributes.py +++ b/test/integration/test_ibm_job_attributes.py @@ -22,7 +22,7 @@ from qiskit import QuantumCircuit from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit_ibm_provider.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.exceptions import IBMBackendValueError from qiskit_ibm_runtime import IBMBackend, RuntimeJob from qiskit_ibm_runtime.exceptions import IBMInputValueError diff --git a/test/integration/test_options.py b/test/integration/test_options.py index 00db91774..89c90006e 100644 --- a/test/integration/test_options.py +++ b/test/integration/test_options.py @@ -13,12 +13,12 @@ """Tests for job functions using real runtime service.""" from qiskit import QuantumCircuit -from qiskit.providers.fake_provider import FakeManila from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime import Session, Sampler, Options, Estimator +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime.exceptions import RuntimeJobFailureError from ..ibm_test_case import IBMIntegrationTestCase diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index a6c6c0ed5..41028ba8f 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -17,13 +17,12 @@ from requests.exceptions import ProxyError -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.api.exceptions import RequestsApiError as ProviderRequestsApiError +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError as RuntimeRequestsApiError +from qiskit_ibm_runtime.api.exceptions import RequestsApiError from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup @@ -160,7 +159,7 @@ def test_invalid_proxy_port_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(RuntimeRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) client.jobs_get(limit=1) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -174,7 +173,7 @@ def test_invalid_proxy_port_authclient(self, dependencies: IntegrationTestDepend url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -184,7 +183,7 @@ def test_invalid_proxy_port_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_PORT_PROXIES) version_finder.version() @@ -201,7 +200,7 @@ def test_invalid_proxy_address_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(RuntimeRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) client.jobs_get(limit=1) @@ -218,7 +217,7 @@ def test_invalid_proxy_address_authclient( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -228,7 +227,7 @@ def test_invalid_proxy_address_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_ADDRESS_PROXIES) version_finder.version() diff --git a/test/integration/test_results.py b/test/integration/test_results.py index ad02a24da..6431e4a1d 100644 --- a/test/integration/test_results.py +++ b/test/integration/test_results.py @@ -21,7 +21,6 @@ from ..unit.mock.proxy_server import MockProxyServer, use_proxies from ..ibm_test_case import IBMIntegrationJobTestCase from ..decorators import run_integration_test -from ..utils import cancel_job_safe, wait_for_status class TestIntegrationResults(IBMIntegrationJobTestCase): @@ -199,37 +198,6 @@ def result_callback(job_id, result): self.assertEqual(iterations - 1, final_it) self.assertIsNotNone(job._ws_client._server_close_code) - @run_integration_test - def test_callback_cancel_job(self, service): - """Test canceling a running job while streaming results.""" - - def result_callback(job_id, result): - # pylint: disable=unused-argument - nonlocal final_it - if "iteration" in result: - final_it = result["iteration"] - - final_it = 0 - iterations = 5 - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] - - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(service) - - job = self._run_program( - service=service, - interim_results="foo", - callback=result_callback, - ) - wait_for_status(job, status) - if not cancel_job_safe(job, self.log): - return - time.sleep(3) # Wait for cleanup - self.assertIsNotNone(job._ws_client._server_close_code) - self.assertLess(final_it, iterations) - @skip("skip until qiskit-ibm-runtime #933 is fixed") @run_integration_test def test_websocket_proxy(self, service): diff --git a/test/integration/test_retrieve_job.py b/test/integration/test_retrieve_job.py index 222fff7f7..3af9528e8 100644 --- a/test/integration/test_retrieve_job.py +++ b/test/integration/test_retrieve_job.py @@ -184,7 +184,9 @@ def test_jobs_filter_by_date(self, service): job = self._run_program(service) job.wait_for_final_state() time_after_job = datetime.now(timezone.utc) - rjobs = service.jobs(created_before=time_after_job, created_after=current_time) + rjobs = service.jobs( + created_before=time_after_job, created_after=current_time, pending=False, limit=20 + ) self.assertTrue(job.job_id() in [j.job_id() for j in rjobs]) for job in rjobs: self.assertTrue(job.creation_date <= time_after_job) diff --git a/test/qctrl/__init__.py b/test/qctrl/__init__.py new file mode 100644 index 000000000..139b265e1 --- /dev/null +++ b/test/qctrl/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. diff --git a/test/qctrl/test_qctrl.py b/test/qctrl/test_qctrl.py index 0eeebbdfb..7d5a5bf6e 100644 --- a/test/qctrl/test_qctrl.py +++ b/test/qctrl/test_qctrl.py @@ -12,8 +12,6 @@ """Tests for job functions using real runtime service.""" -import time - from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector, hellinger_fidelity from qiskit.providers.jobstatus import JobStatus diff --git a/test/unit/fake_provider/__init__.py b/test/unit/fake_provider/__init__.py new file mode 100644 index 000000000..139b265e1 --- /dev/null +++ b/test/unit/fake_provider/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. diff --git a/test/unit/fake_provider/test_fake_backends.py b/test/unit/fake_provider/test_fake_backends.py index 066583c7d..e4ec08834 100644 --- a/test/unit/fake_provider/test_fake_backends.py +++ b/test/unit/fake_provider/test_fake_backends.py @@ -18,7 +18,7 @@ from qiskit.utils import optionals from qiskit_ibm_runtime.fake_provider import FakeAthens, FakePerth -from ..ibm_test_case import IBMTestCase +from ...ibm_test_case import IBMTestCase def get_test_circuit(): diff --git a/test/unit/mock/fake_api_backend.py b/test/unit/mock/fake_api_backend.py index 6ad2e4f5c..75b06c9d0 100644 --- a/test/unit/mock/fake_api_backend.py +++ b/test/unit/mock/fake_api_backend.py @@ -15,7 +15,7 @@ from datetime import datetime as python_datetime from dataclasses import dataclass -from qiskit.providers.fake_provider import FakeLima +from qiskit_ibm_runtime.fake_provider import FakeLima @dataclass diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 7be655d9a..66e302282 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -22,7 +22,7 @@ from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit_ibm_provider.utils.hgp import from_instance_format +from qiskit_ibm_runtime.utils.hgp import from_instance_format from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.utils import RuntimeEncoder diff --git a/test/unit/mock/proxy_server.py b/test/unit/mock/proxy_server.py index 16f95c680..a50b8141b 100644 --- a/test/unit/mock/proxy_server.py +++ b/test/unit/mock/proxy_server.py @@ -15,7 +15,7 @@ import subprocess from contextlib import contextmanager -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration class MockProxyServer: diff --git a/test/unit/test_account.py b/test/unit/test_account.py index 5c0494784..cd730c0b4 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -19,7 +19,7 @@ from typing import Any from unittest import skipIf -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.accounts import ( AccountManager, Account, diff --git a/test/unit/test_backend.py b/test/unit/test_backend.py index d9f081823..3eda26ed2 100644 --- a/test/unit/test_backend.py +++ b/test/unit/test_backend.py @@ -16,11 +16,10 @@ import warnings from qiskit import transpile, qasm3, QuantumCircuit -from qiskit.providers.fake_provider import FakeManila from qiskit.providers.models import BackendStatus -from qiskit_ibm_provider.exceptions import IBMBackendValueError - +from qiskit_ibm_runtime.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime.ibm_backend import IBMBackend from ..ibm_test_case import IBMTestCase diff --git a/test/unit/test_backend_retrieval.py b/test/unit/test_backend_retrieval.py index de809d096..0de18f164 100644 --- a/test/unit/test_backend_retrieval.py +++ b/test/unit/test_backend_retrieval.py @@ -15,7 +15,7 @@ import uuid from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.fake_provider import FakeLima +from qiskit_ibm_runtime.fake_provider import FakeLima from .mock.fake_runtime_service import FakeRuntimeService from .mock.fake_api_backend import FakeApiBackendSpecs diff --git a/test/unit/test_client_parameters.py b/test/unit/test_client_parameters.py index 6b8716bd1..fe5019f9c 100644 --- a/test/unit/test_client_parameters.py +++ b/test/unit/test_client_parameters.py @@ -16,7 +16,7 @@ from requests_ntlm import HttpNtlmAuth -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.auth import CloudAuth, QuantumAuth diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index ad5eeb42d..ca6fcc19f 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -26,12 +26,12 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate -from qiskit.providers.fake_provider import FakeNairobi import qiskit.quantum_info as qi 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 qiskit_ibm_runtime.fake_provider import FakeNairobi # TODO: Remove when they are in terra from qiskit_ibm_runtime.qiskit.primitives import BindingsArray, ObservablesArray diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py index cc7054107..e4b6889c7 100644 --- a/test/unit/test_ibm_primitives.py +++ b/test/unit/test_ibm_primitives.py @@ -23,7 +23,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import SparsePauliOp -from qiskit.providers.fake_provider import FakeManila from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime import ( @@ -34,6 +33,7 @@ ) from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION +from qiskit_ibm_runtime.fake_provider import FakeManila from ..ibm_test_case import IBMTestCase from ..utils import ( diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index f8d9ceba1..438ad5f06 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -24,7 +24,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.providers.fake_provider import FakeManila +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import ( Sampler, diff --git a/test/unit/test_options.py b/test/unit/test_options.py index c670b5e66..b6ac0f768 100644 --- a/test/unit/test_options.py +++ b/test/unit/test_options.py @@ -17,7 +17,7 @@ from ddt import data, ddt from pydantic import ValidationError from qiskit.providers import BackendV1 -from qiskit.providers.fake_provider import FakeManila, FakeNairobiV2 + from qiskit.transpiler import CouplingMap from qiskit_aer.noise import NoiseModel @@ -25,6 +25,7 @@ from qiskit_ibm_runtime.options.utils import merge_options from qiskit_ibm_runtime.options import EstimatorOptions, SamplerOptions from qiskit_ibm_runtime.utils.qctrl import _warn_and_clean_options +from qiskit_ibm_runtime.fake_provider import FakeManila, FakeNairobiV2 from ..ibm_test_case import IBMTestCase from ..utils import dict_keys_equal, dict_paritally_equal, flat_dict_partially_equal, combine diff --git a/test/unit/test_runtime_ws.py b/test/unit/test_runtime_ws.py index 27c3230c4..3dc232a1b 100644 --- a/test/unit/test_runtime_ws.py +++ b/test/unit/test_runtime_ws.py @@ -15,7 +15,6 @@ import time from unittest.mock import MagicMock -from qiskit.providers.fake_provider import FakeQasmSimulator from qiskit.quantum_info import SparsePauliOp from qiskit_ibm_runtime import ( RuntimeJob, @@ -235,7 +234,6 @@ def _get_job(self, callback=None, job_id=JOB_ID_PROGRESS_DONE, backend=None): params = ClientParameters( channel="ibm_quantum", token="my_token", url=MockWsServer.VALID_WS_URL ) - backend = backend or FakeQasmSimulator() job = RuntimeJob( backend=backend, api_client=BaseFakeRuntimeClient(), diff --git a/test/unit/test_session.py b/test/unit/test_session.py index a8bd57c3d..db7816ad9 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -15,10 +15,9 @@ import sys import time from concurrent.futures import ThreadPoolExecutor, wait - from unittest.mock import MagicMock, Mock, patch -from qiskit_ibm_runtime.fake_provider import FakeManila +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import Session from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index 145420478..d548665a1 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -35,13 +35,13 @@ DynamicCircuitInstructionDurations, ) -from .control_flow_test_case import ControlFlowTestCase +from .....ibm_test_case import IBMTestCase # pylint: disable=invalid-name,not-context-manager @ddt -class TestPadDynamicalDecoupling(ControlFlowTestCase): +class TestPadDynamicalDecoupling(IBMTestCase): """Tests PadDynamicalDecoupling pass.""" def setUp(self): @@ -1038,18 +1038,32 @@ def test_disjoint_coupling_map(self): self.assertEqual(delay_dict[0], delay_dict[2]) def test_no_unused_qubits(self): - """Test DD with if_test circuit that unused qubits are untouched and not scheduled. - - This ensures that programs don't have unnecessary information for unused qubits. - Which might hurt performance in later executon stages. + """Test DD with if_test circuit that unused qubits are untouched and + not scheduled. Unused qubits may also have missing durations when + not operational. + This ensures that programs don't have unnecessary information for + unused qubits. + Which might hurt performance in later execution stages. """ + # Here "x" on qubit 3 is not defined + durations = DynamicCircuitInstructionDurations( + [ + ("h", 0, 50), + ("x", 0, 50), + ("x", 1, 50), + ("x", 2, 50), + ("measure", 0, 840), + ("reset", 0, 1340), + ] + ) + dd_sequence = [XGate(), XGate()] pm = PassManager( [ ASAPScheduleAnalysis(self.durations), PadDynamicalDecoupling( - self.durations, + durations, dd_sequence, pulse_alignment=1, sequence_min_length_ratios=[0.0], @@ -1057,16 +1071,13 @@ def test_no_unused_qubits(self): ] ) - qc = QuantumCircuit(3, 1) + qc = QuantumCircuit(4, 1) qc.measure(0, 0) qc.x(1) - with qc.if_test((0, True)): - qc.x(1) - qc.measure(0, 0) with qc.if_test((0, True)): qc.x(0) qc.x(1) qc_dd = pm.run(qc) - dont_use = qc_dd.qubits[-1] + dont_use = qc_dd.qubits[-2:] for op in qc_dd.data: self.assertNotIn(dont_use, op.qubits) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index e9ed82e1f..5903fec8e 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -15,12 +15,12 @@ from unittest.mock import patch from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile -from qiskit.providers.fake_provider import FakeJakarta from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.transpiler.passes import ConvertConditionsToIfOps from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +from qiskit_ibm_runtime.fake_provider import FakeJakarta from qiskit_ibm_runtime.transpiler.passes.scheduling.pad_delay import PadDelay from qiskit_ibm_runtime.transpiler.passes.scheduling.scheduler import ( ALAPScheduleAnalysis, @@ -30,12 +30,12 @@ DynamicCircuitInstructionDurations, ) -from .control_flow_test_case import ControlFlowTestCase +from .....ibm_test_case import IBMTestCase # pylint: disable=invalid-name,not-context-manager -class TestASAPSchedulingAndPaddingPass(ControlFlowTestCase): +class TestASAPSchedulingAndPaddingPass(IBMTestCase): """Tests the ASAP Scheduling passes""" def test_if_test_gate_after_measure(self): @@ -808,7 +808,7 @@ def test_c_if_plugin_conversion_with_transpile(self): self.assertEqual(expected, scheduled) -class TestALAPSchedulingAndPaddingPass(ControlFlowTestCase): +class TestALAPSchedulingAndPaddingPass(IBMTestCase): """Tests the ALAP Scheduling passes""" def test_alap(self): @@ -1774,23 +1774,16 @@ def test_transpile_both_paths(self): qr = QuantumRegister(7, name="q") expected = QuantumCircuit(qr, cr) - expected.delay(24080, qr[1]) - expected.delay(24080, qr[2]) - expected.delay(24080, qr[3]) - expected.delay(24080, qr[4]) - expected.delay(24080, qr[5]) - expected.delay(24080, qr[6]) + for q_ind in range(1, 7): + expected.delay(24240, qr[q_ind]) expected.measure(qr[0], cr[0]) with expected.if_test((cr[0], 1)): expected.x(qr[0]) with expected.if_test((cr[0], 1)): - expected.delay(160, qr[0]) expected.x(qr[1]) - expected.delay(160, qr[2]) - expected.delay(160, qr[3]) - expected.delay(160, qr[4]) - expected.delay(160, qr[5]) - expected.delay(160, qr[6]) + for q_ind in range(7): + if q_ind != 1: + expected.delay(160, qr[q_ind]) self.assertEqual(expected, scheduled) def test_c_if_plugin_conversion_with_transpile(self): @@ -1837,7 +1830,7 @@ def test_no_unused_qubits(self): """Test DD with if_test circuit that unused qubits are untouched and not scheduled. This ensures that programs don't have unnecessary information for unused qubits. - Which might hurt performance in later executon stages. + Which might hurt performance in later execution stages. """ durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) diff --git a/test/unit/transpiler/passes/scheduling/test_utils.py b/test/unit/transpiler/passes/scheduling/test_utils.py index 50cd79ff7..e53cf59e6 100644 --- a/test/unit/transpiler/passes/scheduling/test_utils.py +++ b/test/unit/transpiler/passes/scheduling/test_utils.py @@ -15,6 +15,7 @@ from qiskit_ibm_runtime.transpiler.passes.scheduling.utils import ( DynamicCircuitInstructionDurations, ) +from qiskit_ibm_runtime.fake_provider import FakeKolkata, FakeKolkataV2 from .....ibm_test_case import IBMTestCase @@ -51,6 +52,33 @@ def test_patch_measure(self): self.assertEqual(short_odd_durations.get("measure", (0,)), 1224) self.assertEqual(short_odd_durations.get("reset", (0,)), 1224) + def test_durations_from_backend_v1(self): + """Test loading and patching durations from a V1 Backend""" + + durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkata()) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + + def test_durations_from_backend_v2(self): + """Test loading and patching durations from a V2 Backend""" + + durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkataV2()) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + + def test_durations_from_target(self): + """Test loading and patching durations from a target""" + + durations = DynamicCircuitInstructionDurations.from_target(FakeKolkataV2().target) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + def test_patch_disable(self): """Test if schedules circuits with c_if after measure with a common clbit. See: https://github.com/Qiskit/qiskit-terra/issues/7654""" diff --git a/test/utils.py b/test/utils.py index 5473ed5af..ea4ca2023 100644 --- a/test/utils.py +++ b/test/utils.py @@ -15,16 +15,15 @@ import os import logging import time +import itertools import unittest from unittest import mock from typing import Dict, Optional, Any from datetime import datetime - from ddt import data, unpack -from qiskit.compiler import transpile -from qiskit.test.utils import generate_cases from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.compiler import transpile from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.models import BackendStatus, BackendProperties @@ -305,6 +304,27 @@ def submit_and_cancel(backend: IBMBackend, logger: logging.Logger) -> RuntimeJob return job +class Case(dict): + """""" + + +def generate_cases(docstring, dsc=None, name=None, **kwargs): + """Combines kwargs in Cartesian product and creates Case with them""" + ret = [] + keys = kwargs.keys() + vals = kwargs.values() + for values in itertools.product(*vals): + case = Case(zip(keys, values)) + if docstring is not None: + setattr(case, "__doc__", docstring.format(**case)) + if dsc is not None: + setattr(case, "__doc__", dsc.format(**case)) + if name is not None: + setattr(case, "__name__", name.format(**case)) + ret.append(case) + return ret + + def combine(**kwargs): """Decorator to create combinations and tests @combine(level=[0, 1, 2, 3], diff --git a/tox.ini b/tox.ini index f2662be4b..773bc5918 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,7 @@ envdir = .tox/docs deps = -r requirements-dev.txt commands = - sphinx-build -j auto -W -b html {posargs} {toxinidir}/docs {toxinidir}/docs/_build/html + sphinx-build -j auto -b html {posargs} {toxinidir}/docs {toxinidir}/docs/_build/html [testenv:docs-clean] skip_install = true