diff --git a/.github/workflows/cron-other.yml b/.github/workflows/cron-other.yml deleted file mode 100644 index 035b72c881..0000000000 --- a/.github/workflows/cron-other.yml +++ /dev/null @@ -1,102 +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. - -name: Cron-test -on: - schedule: - - cron: '0 5 * * *' -jobs: - test1: - name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -U git+https://github.com/Qiskit/qiskit-aer.git - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test1 - test2: - name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test2 - test3: - name: tests3-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test3 diff --git a/.github/workflows/cron-prod.yml b/.github/workflows/cron-prod.yml index 5878dc54c6..f1652c5861 100644 --- a/.github/workflows/cron-prod.yml +++ b/.github/workflows/cron-prod.yml @@ -1,28 +1,107 @@ +# 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. + name: Cron-prod on: schedule: - cron: '0 4 * * *' jobs: - runtime-integration: - name: runtime-integration - runs-on: macOS-latest + test1: + name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_DEVICE: ${{ secrets.QISKIT_IBM_RUNTIME_DEVICE }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: Install Deps run: | python -m pip install --upgrade pip pip install -c constraints.txt -e . pip install -U -c constraints.txt -r requirements-dev.txt - name: Run Tests - run: make runtime_integration \ No newline at end of file + run: make test1 + test2: + name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test2 + test3: + name: tests3-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test3 diff --git a/.github/workflows/cron-slow-terra-main.yml b/.github/workflows/cron-slow-terra-main.yml new file mode 100644 index 0000000000..35da9cc17e --- /dev/null +++ b/.github/workflows/cron-slow-terra-main.yml @@ -0,0 +1,45 @@ +# 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. + +name: Slow-test-terra-main +on: + schedule: + - cron: '0 7 * * *' +jobs: + terra-main: + name: terra-main + runs-on: macOS-latest + env: + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_TESTS: run_slow + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + pip install -U git+https://github.com/Qiskit/qiskit-terra.git + - name: Run Tests + run: make test diff --git a/.github/workflows/cron-slow.yml b/.github/workflows/cron-slow.yml index a5c3203dee..5c04335853 100644 --- a/.github/workflows/cron-slow.yml +++ b/.github/workflows/cron-slow.yml @@ -19,10 +19,12 @@ jobs: name: slow-test runs-on: macOS-latest env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_TESTS: run_slow @@ -40,30 +42,3 @@ jobs: pip install -U -c constraints.txt -r requirements-dev.txt - name: Run Tests run: make test - terra-main: - name: terra-main - runs-on: macOS-latest - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_TESTS: run_slow - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - pip install -U git+https://github.com/Qiskit/qiskit-terra.git - pip install -U git+https://github.com/Qiskit/qiskit-aer.git - - name: Run Tests - run: make test diff --git a/.github/workflows/cron-staging.yml b/.github/workflows/cron-staging.yml index 29a6538f0e..460f6136f6 100644 --- a/.github/workflows/cron-staging.yml +++ b/.github/workflows/cron-staging.yml @@ -13,29 +13,98 @@ name: Cron-staging on: schedule: - - cron: '0 4 * * *' + - cron: '0 5 * * *' jobs: - runtime-integration: - name: runtime-integration - runs-on: macOS-latest + test1: + name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] env: - QISKIT_IBM_RUNTIME_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_API_TOKEN }} - QISKIT_IBM_RUNTIME_STAGING_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_API_URL }} - QISKIT_IBM_RUNTIME_STAGING_DEVICE: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_DEVICE }} - QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS: True + QISKIT_IBM_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_API_TOKEN }} + QISKIT_IBM_STAGING_API_URL: ${{ secrets.QISKIT_IBM_STAGING_API_URL }} + QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }} + QISKIT_IBM_STAGING_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_TOKEN }} + QISKIT_IBM_STAGING_CLOUD_URL: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_URL }} + QISKIT_IBM_STAGING_CLOUD_CRN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_CRN }} + USE_STAGING_CREDENTIALS: True LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: Install Deps run: | python -m pip install --upgrade pip pip install -c constraints.txt -e . pip install -U -c constraints.txt -r requirements-dev.txt - name: Run Tests - run: make runtime_integration + run: make test1 + test2: + name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_API_TOKEN }} + QISKIT_IBM_STAGING_API_URL: ${{ secrets.QISKIT_IBM_STAGING_API_URL }} + QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }} + QISKIT_IBM_STAGING_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_TOKEN }} + QISKIT_IBM_STAGING_CLOUD_URL: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_URL }} + QISKIT_IBM_STAGING_CLOUD_CRN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_CRN }} + QISKIT_IBM_USE_STAGING_CREDENTIALS: True + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test2 + test3: + name: tests3-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_API_TOKEN }} + QISKIT_IBM_STAGING_API_URL: ${{ secrets.QISKIT_IBM_STAGING_API_URL }} + QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }} + QISKIT_IBM_STAGING_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_TOKEN }} + QISKIT_IBM_STAGING_CLOUD_URL: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_URL }} + QISKIT_IBM_STAGING_CLOUD_CRN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_CRN }} + QISKIT_IBM_USE_STAGING_CREDENTIALS: True + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b8c179ff5..10cbed8907 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,10 +63,12 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c5bd027614..ad7c722ee7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,10 +21,12 @@ jobs: python-version: [3.7, 3.8, 3.9] os: ["macOS-latest", "ubuntu-latest", "windows-latest"] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_TESTS: skip_online diff --git a/.pylintrc b/.pylintrc index d3b190571f..6e21b3c068 100644 --- a/.pylintrc +++ b/.pylintrc @@ -68,6 +68,7 @@ confidence= # --disable=W". disable=arguments-renamed, # TODO: investigate / re-enable bad-mcs-classmethod-argument, # TODO: investigate / re-enable + bad-continuation, bad-whitespace # differences of opinion with black consider-iterating-dictionary, # TODO: investigate / re-enable consider-using-dict-items, # TODO: investigate / re-enable consider-using-f-string, # TODO: investigate / re-enable diff --git a/Makefile b/Makefile index dd6fe7052e..46ab98b362 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ # that they have been altered from the originals. -.PHONY: lint style test mypy test1 test2 test3 runtime_integration +.PHONY: lint style test mypy test1 test2 test3 lint: pylint -rn qiskit_ibm_runtime test @@ -27,16 +27,13 @@ test: python -m unittest -v test1: - python -m unittest -v test/ibm/test_ibm_backend.py test/ibm/test_account_client.py test/ibm/test_tutorials.py test/ibm/test_basic_server_paths.py + python -m unittest -v test/test_integration_backend.py test/test_integration_program.py test2: - python -m unittest -v test/ibm/test_proxies.py test/ibm/test_ibm_logger.py test/ibm/test_filter_backends.py test/ibm/test_registration.py + python -m unittest -v test/test_integration_job.py test3: - python -m unittest -v test/ibm/test_serialization.py test/ibm/test_jupyter.py test/ibm/test_ibm_provider.py - -runtime_integration: - python -m unittest -v test/ibm/runtime/test_runtime_integration.py + python -m unittest -v test/test_integration_retrieve_job.py test/test_integration_interim_results.py black: black qiskit_ibm_runtime setup.py test \ No newline at end of file diff --git a/README.md b/README.md index 4bc71ed17c..f53ab08a80 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ pip install qiskit-ibm-runtime print(service.backends()) # get IBM's simulator backend - simulator_backend = service.get_backend('ibmq_qasm_simulator') + simulator_backend = service.backend('ibmq_qasm_simulator') ``` ### Load Account from Environment Variables @@ -79,12 +79,6 @@ for discussion and simple questions. To join our Slack community use the invite link at [Qiskit.org]. For questions that are more suited for a forum we use the `Qiskit` tag in [Stack Exchange]. -## Authors and Citation - -The Qiskit IBM Runtime is the work of [many people] who contribute to the -project at different levels. If you use Qiskit, please cite as per the included -[BibTeX file]. - ## License [Apache License 2.0]. diff --git a/docs/apidocs/ibm-runtime.rst b/docs/apidocs/ibm-runtime.rst index 94182a4b58..0b4f2c2571 100644 --- a/docs/apidocs/ibm-runtime.rst +++ b/docs/apidocs/ibm-runtime.rst @@ -1,12 +1,10 @@ ***************************************** -Qiskit IBM Quantum Provider API Reference +Qiskit IBM Runtime API Reference ***************************************** .. toctree:: :maxdepth: 1 ibm_runtime - ibm_credentials ibm_visualization - ibm_jupyter - ibm_utils + ibm_jupyter \ No newline at end of file diff --git a/docs/apidocs/ibm_credentials.rst b/docs/apidocs/ibm_credentials.rst deleted file mode 100644 index 8c7d366fd9..0000000000 --- a/docs/apidocs/ibm_credentials.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit_ibm_runtime-credentials: - -.. automodule:: qiskit_ibm_runtime.credentials - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/apidocs/ibm_utils.rst b/docs/apidocs/ibm_utils.rst deleted file mode 100644 index 35d8d11e04..0000000000 --- a/docs/apidocs/ibm_utils.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit_ibm_runtime-utils: - -.. automodule:: qiskit_ibm_runtime.utils - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/test/Runtime 0.1.ipynb b/docs/test/Runtime 0.1.ipynb new file mode 100644 index 0000000000..abf0162a7c --- /dev/null +++ b/docs/test/Runtime 0.1.ipynb @@ -0,0 +1,532 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7f409f1f", + "metadata": {}, + "source": [ + "# Acceptance Criteria for Runtime 0.1 release" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d039414", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import IBMRuntimeService" + ] + }, + { + "cell_type": "markdown", + "id": "a7300dad", + "metadata": {}, + "source": [ + "# Authentication\n", + "\n", + "## Legacy\n", + "Using IBM Quantum API token.\n", + "\n", + "### Save Legacy Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0326e78b-b3df-40ad-9adb-db7a03a29f9c", + "metadata": {}, + "outputs": [], + "source": [ + "# Save default legacy account\n", + "IBMRuntimeService.save_account(\n", + " token=\"xyz\", # optional\n", + " url=\"https://auth.quantum-computing.ibm.com/api\", # optional\n", + " instance=\"qiskit-hub/qiskit-group/qiskit-project\", # optional\n", + " auth=\"legacy\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca5c496d", + "metadata": {}, + "outputs": [], + "source": [ + "# Save legacy account with a name\n", + "# Note: if there is already an account saved with same name then it will be updated\n", + "IBMRuntimeService.save_account(\n", + " token=\"abc\", # optional\n", + " url=\"https://auth.quantum-computing.ibm.com/api\", # optional\n", + " instance=\"qiskit-hub-1/qiskit-group-1/qiskit-project-1\", # optional\n", + " auth=\"legacy\",\n", + " name=\"my-legacy-account\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5232022a", + "metadata": {}, + "source": [ + "### Load Legacy Account from Saved Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f45dccc", + "metadata": {}, + "outputs": [], + "source": [ + "# Load default legacy account\n", + "service = IBMRuntimeService(auth=\"legacy\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfcc2cec", + "metadata": {}, + "outputs": [], + "source": [ + "# Load legacy account with name\n", + "service = IBMRuntimeService(name=\"my-legacy-account\")" + ] + }, + { + "cell_type": "markdown", + "id": "76d4c05a", + "metadata": {}, + "source": [ + "### Enable Legacy Account for current session without saving" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec0980d9", + "metadata": {}, + "outputs": [], + "source": [ + "# Enable legacy account with auth, token and url (optional) parameters\n", + "service = IBMRuntimeService(auth=\"legacy\", token=\"xyz\", url=\"https://auth.quantum-computing.ibm.com/api\")" + ] + }, + { + "cell_type": "markdown", + "id": "4bf082bf", + "metadata": {}, + "source": [ + "### Enable Legacy Account using Env variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c81d66c", + "metadata": {}, + "outputs": [], + "source": [ + "# Env variables take preference over saved credentials\n", + "QISKIT_IBM_API_TOKEN=xyz\n", + "QISKIT_IBM_API_URL=https://auth.quantum-computing.ibm.com/api\n", + "QISKIT_IBM_INSTANCE=qiskit-hub-1/qiskit-group-1/qiskit-project-1\n", + "\n", + "# Load legacy account from environment variables\n", + "service = IBMRuntimeService()" + ] + }, + { + "cell_type": "markdown", + "id": "11908543", + "metadata": {}, + "source": [ + "### Delete Legacy Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "056dafef", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete default legacy account\n", + "IBMRuntimeService.delete_account(auth=\"legacy\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb530615", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete legacy account with name\n", + "IBMRuntimeService.delete_account(name=\"my-legacy-account\")" + ] + }, + { + "cell_type": "markdown", + "id": "471e1741", + "metadata": {}, + "source": [ + "## Cloud\n", + "Using IBM Cloud API key and CRN.\n", + "\n", + "### Save Cloud Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e63a883", + "metadata": {}, + "outputs": [], + "source": [ + "# Save default cloud account\n", + "IBMRuntimeService.save_account(\n", + " auth=\"cloud\", \n", + " token=\"xyz\", \n", + " url=\"https://cloud.ibm.com\",\n", + " instance=\" or \" # full CRN or service instance name\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "361fbf7b", + "metadata": {}, + "outputs": [], + "source": [ + "# Save cloud account with a name\n", + "# Note: if there is already an account saved with same name then it will be updated\n", + "IBMRuntimeService.save_account(\n", + " name=\"my-cloud-account\"\n", + " auth=\"cloud\", \n", + " token=\"xyz\", \n", + " url=\"https://cloud.ibm.com\",\n", + " instance=\" or \" # full CRN or service instance name\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "e1daafd6", + "metadata": {}, + "source": [ + "### Load Cloud Account from Saved Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ee3ac49", + "metadata": {}, + "outputs": [], + "source": [ + "# Load default cloud account\n", + "# Note: if there is only legacy account saved, this should return the default legacy account\n", + "service = IBMRuntimeService()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "286dcf5a", + "metadata": {}, + "outputs": [], + "source": [ + "# Load default cloud account\n", + "service = IBMRuntimeService(auth=\"cloud\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25a7923c", + "metadata": {}, + "outputs": [], + "source": [ + "# Load cloud account with name\n", + "service = IBMRuntimeService(name=\"my-cloud-account\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e9a4280", + "metadata": {}, + "outputs": [], + "source": [ + "# Load cloud account with full CRN or service instance name\n", + "service = IBMRuntimeService(instance=\"crn:v1:bluemix:public:quantum-computing:us-east:a/...\") # full CRN\n", + "service = IBMRuntimeService(instance=\"Quantum Services-bg\") # service instance name" + ] + }, + { + "cell_type": "markdown", + "id": "a59f2c90", + "metadata": {}, + "source": [ + "### Enable Cloud Account for current session without saving" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b57fe754", + "metadata": {}, + "outputs": [], + "source": [ + "# Enable cloud account with auth, token, url (optional) and instance parameters\n", + "service = IBMRuntimeService(auth=\"cloud\", token=\"xyz\", url=\"https://cloud.ibm.com\", instance=\" or \") # full CRN or service instance name" + ] + }, + { + "cell_type": "markdown", + "id": "65704f5b", + "metadata": {}, + "source": [ + "### Enable Cloud Account using Env variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a69d93d6", + "metadata": {}, + "outputs": [], + "source": [ + "# Env variables take preference over saved credentials\n", + "QISKIT_IBM_API_TOKEN=xyz\n", + "QISKIT_IBM_API_URL=https://cloud.ibm.com\n", + "QISKIT_IBM_INSTANCE= or # full CRN or service instance name\n", + "\n", + "# Load cloud account from environment variables\n", + "service = IBMRuntimeService()" + ] + }, + { + "cell_type": "markdown", + "id": "2b616b77", + "metadata": {}, + "source": [ + "### Delete Cloud Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63980cb1", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete default cloud account\n", + "IBMRuntimeService.delete_account(auth=\"cloud\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52d20714", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete default cloud account\n", + "IBMRuntimeService.delete_account()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39f2e2a0", + "metadata": {}, + "outputs": [], + "source": [ + "# Delete cloud account with name\n", + "IBMRuntimeService.delete_account(name=\"my-cloud-account\")" + ] + }, + { + "cell_type": "markdown", + "id": "5c9b579d", + "metadata": {}, + "source": [ + "## Common\n", + "### Show active account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb3f8045", + "metadata": {}, + "outputs": [], + "source": [ + "# Get account currently in use for the session\n", + "service.active_account()" + ] + }, + { + "cell_type": "markdown", + "id": "74ddc4d4", + "metadata": {}, + "source": [ + "### Show saved accounts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "212eb615", + "metadata": {}, + "outputs": [], + "source": [ + "# Show all saved accounts\n", + "IBMRuntimeService.saved_accounts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3e80285", + "metadata": {}, + "outputs": [], + "source": [ + "# Show default saved accounts\n", + "IBMRuntimeService.saved_accounts(default=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3a73d3b", + "metadata": {}, + "outputs": [], + "source": [ + "# Show all legacy saved accounts\n", + "IBMRuntimeService.saved_accounts(auth=\"legacy\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dccdee27", + "metadata": {}, + "outputs": [], + "source": [ + "# Show all cloud saved accounts\n", + "IBMRuntimeService.saved_accounts(auth=\"cloud\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3ff7b0f", + "metadata": {}, + "outputs": [], + "source": [ + "# Show saved accounts by name\n", + "IBMRuntimeService.saved_accounts(name=\"my-cloud-account\")" + ] + }, + { + "cell_type": "markdown", + "id": "12a965ac", + "metadata": {}, + "source": [ + "### Logout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a036899", + "metadata": {}, + "outputs": [], + "source": [ + "# Clears authorization cache on the server\n", + "service.logout()" + ] + }, + { + "cell_type": "markdown", + "id": "a4e89ca7", + "metadata": {}, + "source": [ + "# Programs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24a5d9a8", + "metadata": {}, + "outputs": [], + "source": [ + "TODO" + ] + }, + { + "cell_type": "markdown", + "id": "43476307", + "metadata": {}, + "source": [ + "# Jobs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e98f475b", + "metadata": {}, + "outputs": [], + "source": [ + "TODO" + ] + }, + { + "cell_type": "markdown", + "id": "bde58337", + "metadata": {}, + "source": [ + "# Backends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b8ae744", + "metadata": {}, + "outputs": [], + "source": [ + "TODO" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qiskit-ibm-test", + "language": "python", + "name": "qiskit-ibm-test" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index b501ab5980..90c5c43a8f 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -202,6 +202,7 @@ def interim_result_callback(job_id, interim_result): RuntimeEncoder RuntimeDecoder ParameterNamespace + RuntimeOptions """ # """ # =================================================== @@ -259,10 +260,6 @@ def interim_result_callback(job_id, interim_result): # IBMError # IBMProviderError # IBMProviderValueError -# IBMProviderCredentialsNotFound -# IBMProviderCredentialsInvalidFormat -# IBMProviderCredentialsInvalidToken -# IBMProviderCredentialsInvalidUrl # IBMBackendError # IBMBackendApiError # IBMBackendApiProtocolError @@ -279,7 +276,6 @@ def interim_result_callback(job_id, interim_result): from .ibm_backend import IBMBackend from .exceptions import * from .utils.utils import setup_logger -from .runner_result import RunnerResult from .version import __version__ from .ibm_runtime_service import IBMRuntimeService @@ -289,6 +285,7 @@ def interim_result_callback(job_id, interim_result): from .program.program_backend import ProgramBackend from .program.result_decoder import ResultDecoder from .utils.json import RuntimeEncoder, RuntimeDecoder +from .runtime_options import RuntimeOptions # Setup the logger for the IBM Quantum Provider package. logger = logging.getLogger(__name__) @@ -301,61 +298,3 @@ def interim_result_callback(job_id, interim_result): """The environment variable name that is used to set the level for the IBM Quantum logger.""" QISKIT_IBM_RUNTIME_LOG_FILE = "QISKIT_IBM_RUNTIME_LOG_FILE" """The environment variable name that is used to set the file for the IBM Quantum logger.""" - - -def least_busy( - backends: List[Union[Backend, BaseBackend]], - reservation_lookahead: Optional[int] = 60, -) -> Union[Backend, BaseBackend]: - """Return the least busy backend from a list. - - Return the least busy available backend for those that - have a ``pending_jobs`` in their ``status``. Note that local - backends may not have this attribute. - - Args: - backends: The backends to choose from. - reservation_lookahead: A backend is considered unavailable if it - has reservations in the next ``n`` minutes, where ``n`` is - the value of ``reservation_lookahead``. - If ``None``, reservations are not taken into consideration. - - Returns: - The backend with the fewest number of pending jobs. - - Raises: - IBMError: If the backends list is empty, or if none of the backends - is available, or if a backend in the list - does not have the ``pending_jobs`` attribute in its status. - """ - if not backends: - raise IBMError( - "Unable to find the least_busy backend from an empty list." - ) from None - try: - candidates = [] - now = datetime.now() - for back in backends: - backend_status = back.status() - if not backend_status.operational or backend_status.status_msg != "active": - continue - if reservation_lookahead and isinstance(back, IBMBackend): - end_time = now + timedelta(minutes=reservation_lookahead) - try: - if back.reservations(now, end_time): - continue - except Exception as err: # pylint: disable=broad-except - logger.warning( - "Unable to find backend reservation information. " - "It will not be taken into consideration. %s", - str(err), - ) - candidates.append(back) - if not candidates: - raise IBMError("No backend matches the criteria.") - return min(candidates, key=lambda b: b.status().pending_jobs) - except AttributeError as ex: - raise IBMError( - "A backend in the list does not have the `pending_jobs` " - "attribute in its status." - ) from ex diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 021b0702bb..256af5ed39 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -16,46 +16,16 @@ from typing import Optional from urllib.parse import urlparse +from requests.auth import AuthBase from typing_extensions import Literal +from ..proxies import ProxyConfiguration +from ..utils.hgp import from_instance_format +from ..api.auth import LegacyAuth, CloudAuth AccountType = Optional[Literal["cloud", "legacy"]] LEGACY_API_URL = "https://auth.quantum-computing.ibm.com/api" -CLOUD_API_URL = "https://cloud.ibm.com" - - -def _assert_valid_auth(auth: AccountType) -> None: - """Assert that the auth parameter is valid.""" - if not (auth in ["cloud", "legacy"]): - raise ValueError( - f"Inappropriate `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'." - ) - - -def _assert_valid_token(token: str) -> None: - """Assert that the token is valid.""" - if not (isinstance(token, str) and len(token) > 0): - raise ValueError( - f"Inappropriate `token` value. Expected a non-empty string, got '{token}'." - ) - - -def _assert_valid_url(url: str) -> None: - """Assert that the URL is valid.""" - try: - urlparse(url) - except: - raise ValueError(f"Inappropriate `url` value. Failed to parse '{url}' as URL.") - - -def _assert_valid_instance(auth: AccountType, instance: str) -> None: - """Assert that the instance name is valid for the given account type.""" - if auth == "cloud": - if not (isinstance(instance, str) and len(instance) > 0): - raise ValueError( - f"Inappropriate `instance` value. Expected a non-empty string." - ) - # TODO: add validation for legacy instance when adding test coverage +CLOUD_API_URL = "https://us-east.quantum-computing.cloud.ibm.com" class Account: @@ -65,40 +35,131 @@ def __init__( self, auth: AccountType, token: str, - url: Optional[str], + url: Optional[str] = None, instance: Optional[str] = None, - # TODO: add validation for proxies input format - proxies: Optional[dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, ): - """Account constructor.""" - _assert_valid_auth(auth) - self.auth = auth + """Account constructor. + + Args: + auth: Authentication type, ``cloud`` or ``legacy``. + token: Account token to use. + url: Authentication URL. + instance: Service instance to use. + proxies: Proxy configuration. + verify: Whether to verify server's TLS certificate. + """ + resolved_url = url or (LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL) - _assert_valid_token(token) + self.auth = auth self.token = token - - resolved_url = url or LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL - _assert_valid_url(resolved_url) self.url = resolved_url - - _assert_valid_instance(auth, instance) self.instance = instance self.proxies = proxies self.verify = verify def to_saved_format(self) -> dict: """Returns a dictionary that represents how the account is saved on disk.""" - return {k: v for k, v in self.__dict__.items() if v is not None} + result = {k: v for k, v in self.__dict__.items() if v is not None} + if self.proxies: + result["proxies"] = self.proxies.to_dict() + return result @classmethod def from_saved_format(cls, data: dict) -> "Account": """Creates an account instance from data saved on disk.""" + proxies = data.get("proxies") return cls( auth=data.get("auth"), url=data.get("url"), token=data.get("token"), instance=data.get("instance"), - proxies=data.get("proxies"), - verify=data.get("verify"), + proxies=ProxyConfiguration(**proxies) if proxies else None, + verify=data.get("verify", True), + ) + + def get_auth_handler(self) -> AuthBase: + """Returns the respective authentication handler.""" + if self.auth == "cloud": + return CloudAuth(api_key=self.token, crn=self.instance) + + return LegacyAuth(access_token=self.token) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Account): + return False + return all( + [ + self.auth == other.auth, + self.token == other.token, + self.url == other.url, + self.instance == other.instance, + self.proxies == other.proxies, + self.verify == other.verify, + ] ) + + def validate(self) -> "Account": + """Validates the account instance. + + Raises: + ValueError: if the account is invalid + + Returns: + This Account instance. + """ + + self._assert_valid_auth(self.auth) + self._assert_valid_token(self.token) + self._assert_valid_url(self.url) + self._assert_valid_instance(self.auth, self.instance) + self._assert_valid_proxies(self.proxies) + return self + + @staticmethod + def _assert_valid_auth(auth: AccountType) -> None: + """Assert that the auth parameter is valid.""" + if not (auth in ["cloud", "legacy"]): + raise ValueError( + f"Invalid `auth` value. Expected one of ['cloud', 'legacy'], got '{auth}'." + ) + + @staticmethod + def _assert_valid_token(token: str) -> None: + """Assert that the token is valid.""" + if not (isinstance(token, str) and len(token) > 0): + raise ValueError( + f"Invalid `token` value. Expected a non-empty string, got '{token}'." + ) + + @staticmethod + def _assert_valid_url(url: str) -> None: + """Assert that the URL is valid.""" + try: + urlparse(url) + except: + raise ValueError(f"Invalid `url` value. Failed to parse '{url}' as URL.") + + @staticmethod + def _assert_valid_proxies(config: ProxyConfiguration) -> None: + """Assert that the proxy configuration is valid.""" + if config is not None: + config.validate() + + @staticmethod + def _assert_valid_instance(auth: AccountType, instance: str) -> None: + """Assert that the instance name is valid for the given account type.""" + if auth == "cloud": + if not (isinstance(instance, str) and len(instance) > 0): + raise ValueError( + f"Invalid `instance` value. Expected a non-empty string, got '{instance}'." + ) + if auth == "legacy": + if instance is not None: + try: + from_instance_format(instance) + except: + raise ValueError( + f"Invalid `instance` value. Expected hub/group/project format, got {instance}" + ) diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index ca7cdd63ce..ef149b9707 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -13,35 +13,42 @@ """Account management related classes and functions.""" import os -from typing import Optional, Union +from typing import Optional, Dict from .account import Account, AccountType +from ..proxies import ProxyConfiguration from .storage import save_config, read_config, delete_config -_DEFAULT_ACCOUNG_CONFIG_JSON_FILE = os.path.join( +_DEFAULT_ACCOUNT_CONFIG_JSON_FILE = os.path.join( os.path.expanduser("~"), ".qiskit", "qiskit-ibm.json" ) _DEFAULT_ACCOUNT_NAME = "default" +_DEFAULT_ACCOUNT_NAME_LEGACY = "default-legacy" +_DEFAULT_ACCOUNT_NAME_CLOUD = "default-cloud" +_DEFAULT_ACCOUNT_TYPE: AccountType = "cloud" +_ACCOUNT_TYPES = [_DEFAULT_ACCOUNT_TYPE, "legacy"] class AccountManager: """Class that bundles account management related functionality.""" - @staticmethod + @classmethod def save( + cls, token: Optional[str] = None, url: Optional[str] = None, instance: Optional[str] = None, auth: Optional[AccountType] = None, name: Optional[str] = _DEFAULT_ACCOUNT_NAME, - proxies: Optional[dict] = None, + proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, ) -> None: """Save account on disk.""" + config_key = name or cls._get_default_account_name(auth) return save_config( - filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, - name=name, + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, + name=config_key, config=Account( token=token, url=url, @@ -49,23 +56,134 @@ def save( auth=auth, proxies=proxies, verify=verify, - ).to_saved_format(), + ) + # avoid storing invalid accounts + .validate().to_saved_format(), ) @staticmethod - def list() -> Union[dict, None]: + def list( + default: Optional[bool] = None, + auth: Optional[str] = None, + name: Optional[str] = None, + ) -> Dict[str, Account]: """List all accounts saved on disk.""" - return read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE) + def _matching_name(account_name: str) -> bool: + return name is None or name == account_name - @staticmethod - def get(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> Account: - """Read account from disk.""" - return Account.from_saved_format( - read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, name=name) + def _matching_auth(account: Account) -> bool: + return auth is None or account.auth == auth + + def _matching_default(account_name: str) -> bool: + default_accounts = [ + _DEFAULT_ACCOUNT_NAME, + _DEFAULT_ACCOUNT_NAME_LEGACY, + _DEFAULT_ACCOUNT_NAME_CLOUD, + ] + if default is None: + return True + elif default is False: + return account_name not in default_accounts + else: + return account_name in default_accounts + + # load all accounts + all_accounts = map( + lambda kv: (kv[0], Account.from_saved_format(kv[1])), + read_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE).items(), ) - @staticmethod - def delete(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> bool: + # filter based on input parameters + filtered_accounts = dict( + list( + filter( + lambda kv: _matching_auth(kv[1]) + and _matching_default(kv[0]) + and _matching_name(kv[0]), + all_accounts, + ) + ) + ) + + return filtered_accounts + + @classmethod + def get( + cls, name: Optional[str] = None, auth: Optional[AccountType] = None + ) -> Optional[Account]: + """Read account from disk. + + Args: + name: Account name. Takes precedence if `auth` is also specified. + auth: Account auth type. + + Returns: + Account information. + + Raises: + ValueError: If the input value cannot be found on disk. + """ + if name: + saved_account = read_config( + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, name=name + ) + if not saved_account: + raise ValueError( + f"Account with the name {name} does not exist on disk." + ) + return Account.from_saved_format(saved_account) + + auth_ = auth or _DEFAULT_ACCOUNT_TYPE + env_account = cls._from_env_variables(auth_) + if env_account is not None: + return env_account + + if auth: + saved_account = read_config( + filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE, + name=cls._get_default_account_name(auth), + ) + if saved_account is None: + raise ValueError(f"No default {auth} account saved.") + return Account.from_saved_format(saved_account) + + all_config = read_config(filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE) + for account_type in _ACCOUNT_TYPES: + account_name = cls._get_default_account_name(account_type) + if account_name in all_config: + return Account.from_saved_format(all_config[account_name]) + + return None + + @classmethod + def delete( + cls, + name: Optional[str] = None, + auth: Optional[str] = None, + ) -> bool: """Delete account from disk.""" - return delete_config(name=name, filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE) + + config_key = name or cls._get_default_account_name(auth) + return delete_config( + name=config_key, filename=_DEFAULT_ACCOUNT_CONFIG_JSON_FILE + ) + + @classmethod + def _from_env_variables(cls, auth: Optional[AccountType]) -> Optional[Account]: + """Read account from environment variable.""" + token = os.getenv("QISKIT_IBM_API_TOKEN") + url = os.getenv("QISKIT_IBM_API_URL") + if not (token and url): + return None + return Account( + token=token, url=url, instance=os.getenv("QISKIT_IBM_INSTANCE"), auth=auth + ) + + @classmethod + def _get_default_account_name(cls, auth: AccountType) -> str: + return ( + _DEFAULT_ACCOUNT_NAME_LEGACY + if auth == "legacy" + else _DEFAULT_ACCOUNT_NAME_CLOUD + ) diff --git a/qiskit_ibm_runtime/accounts/storage.py b/qiskit_ibm_runtime/accounts/storage.py index aa827b7230..fa8cc56038 100644 --- a/qiskit_ibm_runtime/accounts/storage.py +++ b/qiskit_ibm_runtime/accounts/storage.py @@ -13,8 +13,11 @@ """Utility functions related to storing account configuration on disk.""" import json +import logging import os -from typing import Optional, Union +from typing import Optional, Dict + +logger = logging.getLogger(__name__) def save_config( @@ -23,7 +26,7 @@ def save_config( config: dict, ) -> None: """Save configuration data in a JSON file under the given name.""" - + logger.debug("Save configuration data for '%s' in '%s'", name, filename) _ensure_file_exists(filename) with open(filename, mode="r") as json_in: @@ -37,9 +40,9 @@ def save_config( def read_config( filename: str, name: Optional[str] = None, -) -> Union[dict, None]: - """Save configuration data from a JSON file.""" - +) -> Optional[Dict]: + """Read configuration data from a JSON file.""" + logger.debug("Read configuration data for '%s' from '%s'", name, filename) _ensure_file_exists(filename) with open(filename) as json_file: @@ -58,6 +61,8 @@ def delete_config( ) -> bool: """Delete configuration data from a JSON file.""" + logger.debug("Delete configuration data for '%s' from '%s'", name, filename) + _ensure_file_exists(filename) with open(filename, mode="r") as json_in: data = json.load(json_in) @@ -73,6 +78,7 @@ def delete_config( def _ensure_file_exists(filename: str, initial_content: str = "{}") -> None: if not os.path.isfile(filename): + logger.debug("Create empty configuration file at %s", filename) # create parent directories os.makedirs(os.path.dirname(filename), exist_ok=True) diff --git a/qiskit_ibm_runtime/api/auth.py b/qiskit_ibm_runtime/api/auth.py index 33b5794c14..43a5ac955d 100644 --- a/qiskit_ibm_runtime/api/auth.py +++ b/qiskit_ibm_runtime/api/auth.py @@ -12,6 +12,8 @@ """Authentication helpers.""" +from typing import Dict + from requests import PreparedRequest from requests.auth import AuthBase @@ -35,10 +37,13 @@ def __eq__(self, other: object) -> bool: return False def __call__(self, r: PreparedRequest) -> PreparedRequest: - r.headers["Service-CRN"] = self.crn - r.headers["Authorization"] = f"apikey {self.api_key}" + r.headers.update(self.get_headers()) return r + def get_headers(self) -> Dict: + """Return authorization information to be stored in header.""" + return {"Service-CRN": self.crn, "Authorization": f"apikey {self.api_key}"} + class LegacyAuth(AuthBase): """Attaches Legacy Authentication to the given Request object.""" @@ -53,5 +58,9 @@ def __eq__(self, other: object) -> bool: return False def __call__(self, r: PreparedRequest) -> PreparedRequest: - r.headers["X-Access-Token"] = self.access_token + r.headers.update(self.get_headers()) return r + + def get_headers(self) -> Dict: + """Return authorization information to be stored in header.""" + return {"X-Access-Token": self.access_token} diff --git a/qiskit_ibm_runtime/api/client_parameters.py b/qiskit_ibm_runtime/api/client_parameters.py new file mode 100644 index 0000000000..69544eb091 --- /dev/null +++ b/qiskit_ibm_runtime/api/client_parameters.py @@ -0,0 +1,71 @@ +# 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. + +"""Represent IBM Quantum account client parameters.""" + +from typing import Dict, Optional, Any, Union + +from ..api.auth import LegacyAuth, CloudAuth +from ..proxies import ProxyConfiguration + +TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" +"""str: Template for creating an IBM Quantum URL with hub/group/project information.""" + + +class ClientParameters: + """IBM Quantum account client parameters.""" + + def __init__( + self, + auth_type: str, + token: str, + url: str = None, + instance: Optional[str] = None, + proxies: Optional[ProxyConfiguration] = None, + verify: bool = True, + ) -> None: + """ClientParameters constructor. + + Args: + token: IBM Quantum API token. + url: IBM Quantum URL (gets replaced with a new-style URL with hub, group, project). + proxies: Proxy configuration. + verify: If ``False``, ignores SSL certificates errors. + """ + self.token = token + self.instance = instance + self.auth_type = auth_type + self.url = url + self.proxies = proxies + self.verify = verify + + def get_auth_handler(self) -> Union[CloudAuth, LegacyAuth]: + """Returns the respective authentication handler.""" + if self.auth_type == "cloud": + return CloudAuth(api_key=self.token, crn=self.instance) + + return LegacyAuth(access_token=self.token) + + def connection_parameters(self) -> Dict[str, Any]: + """Construct connection related parameters. + + Returns: + A dictionary with connection-related parameters in the format + expected by ``requests``. The following keys can be present: + ``proxies``, ``verify``, and ``auth``. + """ + request_kwargs: Any = {"verify": self.verify} + + if self.proxies: + request_kwargs.update(self.proxies.to_request_params()) + + return request_kwargs diff --git a/qiskit_ibm_runtime/api/clients/account.py b/qiskit_ibm_runtime/api/clients/account.py index 2b842928fe..e374e968ba 100644 --- a/qiskit_ibm_runtime/api/clients/account.py +++ b/qiskit_ibm_runtime/api/clients/account.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 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 @@ -15,54 +15,45 @@ import logging from typing import List, Dict, Any, Optional -from datetime import datetime +from datetime import datetime as python_datetime -from qiskit_ibm_runtime.credentials import Credentials +from qiskit_ibm_runtime.utils.hgp import from_instance_format -from ..rest import Api, Account -from ..rest.backend import Backend +from .backend import BaseBackendClient +from ..rest import Account from ..session import RetrySession -from .base import BaseClient +from ..client_parameters import ClientParameters logger = logging.getLogger(__name__) -class AccountClient(BaseClient): +class AccountClient(BaseBackendClient): """Client for accessing an individual IBM Quantum account.""" - def __init__(self, credentials: Credentials, **request_kwargs: Any) -> None: + def __init__(self, params: ClientParameters) -> None: """AccountClient constructor. Args: - credentials: Account credentials. - **request_kwargs: Arguments for the request ``Session``. + params: Parameters used for server connection. """ self._session = RetrySession( - credentials.base_url, auth=credentials.get_auth_handler(), **request_kwargs + params.url, auth=params.get_auth_handler(), **params.connection_parameters() ) - # base_api is used to handle endpoints that don't include h/g/p. - # account_api is for h/g/p. - self.base_api = Api(self._session) + hub, group, project = from_instance_format(params.instance) self.account_api = Account( session=self._session, - hub=credentials.hub, - group=credentials.group, - project=credentials.project, + hub=hub, + group=group, + project=project, ) - self._credentials = credentials - # Backend-related public functions. - - def list_backends(self, timeout: Optional[float] = None) -> List[Dict[str, Any]]: + def list_backends(self) -> List[Dict[str, Any]]: """Return backends available for this provider. - Args: - timeout: Number of seconds to wait for the request. - Returns: Backends available for this provider. """ - return self.account_api.backends(timeout=timeout) + return self.account_api.backends() def backend_status(self, backend_name: str) -> Dict[str, Any]: """Return the status of the backend. @@ -76,7 +67,7 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: return self.account_api.backend(backend_name).status() def backend_properties( - self, backend_name: str, datetime: Optional[datetime] = None + self, backend_name: str, datetime: Optional[python_datetime] = None ) -> Dict[str, Any]: """Return the properties of the backend. @@ -87,7 +78,6 @@ def backend_properties( Returns: Backend properties. """ - # pylint: disable=redefined-outer-name return self.account_api.backend(backend_name).properties(datetime=datetime) def backend_pulse_defaults(self, backend_name: str) -> Dict: @@ -100,41 +90,3 @@ def backend_pulse_defaults(self, backend_name: str) -> Dict: Backend pulse defaults. """ return self.account_api.backend(backend_name).pulse_defaults() - - def backend_job_limit(self, backend_name: str) -> Dict[str, Any]: - """Return the job limit for the backend. - - Args: - backend_name: The name of the backend. - - Returns: - Backend job limit. - """ - return self.account_api.backend(backend_name).job_limit() - - def backend_reservations( - self, - backend_name: str, - start_datetime: Optional[datetime] = None, - end_datetime: Optional[datetime] = None, - ) -> List: - """Return backend reservation information. - - Args: - backend_name: Name of the backend. - start_datetime: Starting datetime in UTC. - end_datetime: Ending datetime in UTC. - - Returns: - Backend reservation information. - """ - backend_api = Backend(self._session, backend_name, "/Network") - return backend_api.reservations(start_datetime, end_datetime) - - def my_reservations(self) -> List: - """Return backend reservations made by the caller. - - Returns: - Backend reservation information. - """ - return self.base_api.reservations() diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py index 1d9ba17f97..fd679b827e 100644 --- a/qiskit_ibm_runtime/api/clients/auth.py +++ b/qiskit_ibm_runtime/api/clients/auth.py @@ -19,6 +19,7 @@ from ..exceptions import AuthenticationLicenseError, RequestsApiError from ..rest import Api from ..session import RetrySession +from ..client_parameters import ClientParameters from .base import BaseClient @@ -26,20 +27,22 @@ class AuthClient(BaseClient): """Client for accessing IBM Quantum authentication services.""" - def __init__(self, api_token: str, auth_url: str, **request_kwargs: Any) -> None: + def __init__(self, client_params: ClientParameters) -> None: """AuthClient constructor. Args: - api_token: IBM Quantum API token. - auth_url: URL for the authentication service. - **request_kwargs: Arguments for the request ``Session``. + client_params: Parameters used for server connection. """ - self.api_token = api_token - self.auth_url = auth_url + self.api_token = client_params.token + self.auth_url = client_params.url self._service_urls = {} # type: ignore[var-annotated] - self.auth_api = Api(RetrySession(auth_url, **request_kwargs)) - self.base_api = self._init_service_clients(**request_kwargs) + 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. @@ -165,7 +168,7 @@ def current_access_token(self) -> Optional[str]: """ return self.access_token - def current_service_urls(self) -> Dict[str, str]: + def current_service_urls(self) -> Dict: """Return the current service URLs. Returns: diff --git a/qiskit_ibm_runtime/api/clients/backend.py b/qiskit_ibm_runtime/api/clients/backend.py new file mode 100644 index 0000000000..a043f982fa --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/backend.py @@ -0,0 +1,65 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# 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 backend information.""" + +import logging +from typing import Dict, Any, Optional +from datetime import datetime as python_datetime +from abc import ABC, abstractmethod + +from .base import BaseClient + +logger = logging.getLogger(__name__) + + +class BaseBackendClient(BaseClient, ABC): + """Client for accessing backend information.""" + + @abstractmethod + def backend_status(self, backend_name: str) -> Dict[str, Any]: + """Return the status of the backend. + + Args: + backend_name: The name of the backend. + + Returns: + Backend status. + """ + pass + + @abstractmethod + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: + """Return the properties of the backend. + + Args: + backend_name: The name of the backend. + datetime: Date and time for additional filtering of backend properties. + + Returns: + Backend properties. + """ + pass + + @abstractmethod + def backend_pulse_defaults(self, backend_name: str) -> Dict: + """Return the pulse defaults of the backend. + + Args: + backend_name: The name of the backend. + + Returns: + Backend pulse defaults. + """ + pass diff --git a/qiskit_ibm_runtime/api/clients/base.py b/qiskit_ibm_runtime/api/clients/base.py index 05b96d085d..010063cb25 100644 --- a/qiskit_ibm_runtime/api/clients/base.py +++ b/qiskit_ibm_runtime/api/clients/base.py @@ -24,9 +24,8 @@ from websocket import WebSocketApp, STATUS_NORMAL, STATUS_ABNORMAL_CLOSED -from ...credentials import Credentials +from ..client_parameters import ClientParameters from ..exceptions import WebsocketError, WebsocketTimeoutError -from .utils import ws_proxy_params logger = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class BaseWebsocketClient(BaseClient, ABC): def __init__( self, websocket_url: str, - credentials: Credentials, + client_params: ClientParameters, job_id: str, message_queue: Optional[Queue] = None, ) -> None: @@ -63,15 +62,17 @@ def __init__( Args: websocket_url: URL for websocket communication with IBM Quantum. - credentials: Account credentials. + 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 = ws_proxy_params( - credentials=credentials, ws_url=self._websocket_url + self._proxy_params = ( + client_params.proxies.to_ws_params(self._websocket_url) + if client_params.proxies + else {} ) - self._access_token = credentials.access_token + self._access_token = client_params.token self._job_id = job_id self._message_queue = message_queue self._header: Optional[Dict] = None diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 3dd42a2b72..e3e402e961 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -14,33 +14,36 @@ import logging from typing import Any, Dict, List, Optional +from datetime import datetime as python_datetime -from qiskit_ibm_runtime.credentials import Credentials 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__) -class RuntimeClient: +class RuntimeClient(BaseBackendClient): """Client for accessing runtime service.""" def __init__( self, - credentials: Credentials, + params: ClientParameters, ) -> None: - """RandomClient constructor. + """RuntimeClient constructor. Args: - credentials: Account credentials. + params: Connection parameters. """ self._session = RetrySession( - base_url=credentials.runtime_url or credentials.base_url, - auth=credentials.get_auth_handler(), - **credentials.connection_parameters() + base_url=params.url, + auth=params.get_auth_handler(), + **params.connection_parameters() ) - self.api = Runtime(self._session) + self._api = Runtime(self._session) def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: """Return a list of runtime programs. @@ -52,7 +55,7 @@ def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: Returns: A list of runtime programs. """ - return self.api.list_programs(limit, skip) + return self._api.list_programs(limit, skip) def program_create( self, @@ -76,7 +79,7 @@ def program_create( Returns: Server response. """ - return self.api.create_program( + return self._api.create_program( program_data=program_data, name=name, description=description, @@ -94,7 +97,7 @@ def program_get(self, program_id: str) -> Dict: Returns: Program information. """ - return self.api.program(program_id).get() + return self._api.program(program_id).get() def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. @@ -106,38 +109,43 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: """ if public: - self.api.program(program_id).make_public() + self._api.program(program_id).make_public() else: - self.api.program(program_id).make_private() + self._api.program(program_id).make_private() def program_run( self, program_id: str, - credentials: Credentials, - backend_name: str, + backend_name: Optional[str], params: Dict, - image: str, + image: Optional[str], + hgp: Optional[str], + log_level: Optional[str], ) -> Dict: """Run the specified program. Args: program_id: Program ID. - credentials: Credentials used to run the program. backend_name: Name of the backend to run the program. params: Parameters to use. image: The runtime image to use. + hgp: Hub/group/project to use. + log_level: Log level to use. Returns: JSON response. """ - return self.api.program_run( + hgp_dict = {} + if hgp: + hub, group, project = from_instance_format(hgp) + hgp_dict = {"hub": hub, "group": group, "project": project} + return self._api.program_run( program_id=program_id, - hub=credentials.hub, - group=credentials.group, - project=credentials.project, backend_name=backend_name, params=params, image=image, + log_level=log_level, + **hgp_dict ) def program_delete(self, program_id: str) -> None: @@ -146,7 +154,7 @@ def program_delete(self, program_id: str) -> None: Args: program_id: Program ID. """ - self.api.program(program_id).delete() + self._api.program(program_id).delete() def program_update( self, @@ -168,10 +176,10 @@ def program_update( spec: Backend requirements, parameters, interim results, return values, etc. """ if program_data: - self.api.program(program_id).update_data(program_data) + self._api.program(program_id).update_data(program_data) if any([name, description, max_execution_time, spec]): - self.api.program(program_id).update_metadata( + self._api.program(program_id).update_metadata( name=name, description=description, max_execution_time=max_execution_time, @@ -187,7 +195,7 @@ def job_get(self, job_id: str) -> Dict: Returns: JSON response. """ - response = self.api.program_job(job_id).get() + response = self._api.program_job(job_id).get() logger.debug("Runtime job get response: %s", response) return response @@ -216,7 +224,7 @@ def jobs_get( Returns: JSON response. """ - return self.api.jobs_get( + return self._api.jobs_get( limit=limit, skip=skip, pending=pending, @@ -235,7 +243,7 @@ def job_results(self, job_id: str) -> str: Returns: Job result. """ - return self.api.program_job(job_id).results() + return self._api.program_job(job_id).results() def job_interim_results(self, job_id: str) -> str: """Get the interim results of a program job. @@ -246,7 +254,7 @@ def job_interim_results(self, job_id: str) -> str: Returns: Job interim results. """ - return self.api.program_job(job_id).interim_results() + return self._api.program_job(job_id).interim_results() def job_cancel(self, job_id: str) -> None: """Cancel a job. @@ -254,7 +262,7 @@ def job_cancel(self, job_id: str) -> None: Args: job_id: Runtime job ID. """ - self.api.program_job(job_id).cancel() + self._api.program_job(job_id).cancel() def job_delete(self, job_id: str) -> None: """Delete a job. @@ -262,7 +270,7 @@ def job_delete(self, job_id: str) -> None: Args: job_id: Runtime job ID. """ - self.api.program_job(job_id).delete() + self._api.program_job(job_id).delete() def job_logs(self, job_id: str) -> str: """Get the job logs. @@ -273,11 +281,7 @@ def job_logs(self, job_id: str) -> str: Returns: Job logs. """ - return self.api.program_job(job_id).logs() - - def logout(self) -> None: - """Clear authorization cache.""" - self.api.logout() + return self._api.program_job(job_id).logs() # IBM Cloud only functions @@ -287,7 +291,7 @@ def list_backends(self) -> List[str]: Returns: IBM Cloud backends available for this service instance. """ - return self.api.backends()["devices"] + return self._api.backends()["devices"] def backend_configuration(self, backend_name: str) -> Dict[str, Any]: """Return the configuration of the IBM Cloud backend. @@ -298,7 +302,7 @@ def backend_configuration(self, backend_name: str) -> Dict[str, Any]: Returns: Backend configuration. """ - return self.api.backend(backend_name).configuration() + return self._api.backend(backend_name).configuration() def backend_status(self, backend_name: str) -> Dict[str, Any]: """Return the status of the IBM Cloud backend. @@ -309,18 +313,26 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: Returns: Backend status. """ - return self.api.backend(backend_name).status() + return self._api.backend(backend_name).status() - def backend_properties(self, backend_name: str) -> Dict[str, Any]: + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: """Return the properties of the IBM Cloud backend. Args: backend_name: The name of the IBM Cloud backend. + datetime: Date and time for additional filtering of backend properties. Returns: Backend properties. + + Raises: + NotImplementedError: If `datetime` is specified. """ - return self.api.backend(backend_name).properties() + if datetime: + raise NotImplementedError("'datetime' is not supported with cloud runtime.") + return self._api.backend(backend_name).properties() def backend_pulse_defaults(self, backend_name: str) -> Dict: """Return the pulse defaults of the IBM Cloud backend. @@ -331,4 +343,4 @@ def backend_pulse_defaults(self, backend_name: str) -> Dict: Returns: Backend pulse defaults. """ - return self.api.backend(backend_name).pulse_defaults() + return self._api.backend(backend_name).pulse_defaults() diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py index 5883d27221..51742f10ca 100644 --- a/qiskit_ibm_runtime/api/clients/runtime_ws.py +++ b/qiskit_ibm_runtime/api/clients/runtime_ws.py @@ -16,8 +16,8 @@ from typing import Optional from queue import Queue -from ...credentials import Credentials from .base import BaseWebsocketClient +from ..client_parameters import ClientParameters logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ class RuntimeWebsocketClient(BaseWebsocketClient): def __init__( self, websocket_url: str, - credentials: Credentials, + client_params: ClientParameters, job_id: str, message_queue: Optional[Queue] = None, ) -> None: @@ -36,12 +36,12 @@ def __init__( Args: websocket_url: URL for websocket communication with IBM Quantum. - credentials: Account credentials. + client_params: Parameters used for server connection. job_id: Job ID. message_queue: Queue used to hold received messages. """ - super().__init__(websocket_url, credentials, job_id, message_queue) - self._header = {"X-Access-Token": credentials.access_token} + 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. diff --git a/qiskit_ibm_runtime/api/clients/utils.py b/qiskit_ibm_runtime/api/clients/utils.py deleted file mode 100644 index 2152f5f103..0000000000 --- a/qiskit_ibm_runtime/api/clients/utils.py +++ /dev/null @@ -1,68 +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. - -"""Utilities for working with IBM Quantum API.""" - -from typing import Dict -from urllib.parse import urlparse - -from ...credentials import Credentials - - -def ws_proxy_params(credentials: Credentials, ws_url: str) -> Dict: - """Extract proxy information for websocket. - - Args: - credentials: Account credentials. - ws_url: Websocket URL. - - Returns: - Proxy information to be used by the websocket client. - """ - conn_data = credentials.connection_parameters() - out = {} - - if "proxies" in conn_data: - proxies = conn_data["proxies"] - 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 "auth" in conn_data: - out["http_proxy_auth"] = ( - credentials.proxies["username_ntlm"], - credentials.proxies["password_ntlm"], - ) - - return out diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index 4fec6d6a69..42442c9b3f 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -31,7 +31,6 @@ class Runtime(RestAdapterBase): URL_MAP = { "programs": "/programs", "jobs": "/jobs", - "logout": "/logout", "backends": "/devices", } @@ -113,23 +112,25 @@ def create_program( def program_run( self, program_id: str, - hub: str, - group: str, - project: str, - backend_name: str, + backend_name: Optional[str], params: Dict, - image: str, + image: Optional[str] = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + log_level: Optional[str] = None, ) -> Dict: """Execute the program. Args: program_id: Program ID. - hub: Hub to be used. - group: Group to be used. - project: Project to be used. backend_name: Name of the backend. params: Program parameters. image: Runtime image. + hub: Hub to be used. + group: Group to be used. + project: Project to be used. + log_level: Log level to use. Returns: JSON response. @@ -137,13 +138,18 @@ def program_run( url = self.get_url("jobs") payload = { "program_id": program_id, - "hub": hub, - "group": group, - "project": project, - "backend": backend_name, "params": params, - "runtime": image, } + if image: + payload["runtime"] = image + if log_level: + payload["log_level"] = log_level + if backend_name: + payload["backend"] = backend_name + if all([hub, group, project]): + payload["hub"] = hub + payload["group"] = group + payload["project"] = project data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() @@ -186,11 +192,6 @@ def jobs_get( payload["provider"] = f"{hub}/{group}/{project}" return self.session.get(url, params=payload).json() - def logout(self) -> None: - """Clear authorization cache.""" - url = self.get_url("logout") - self.session.post(url) - # IBM Cloud only functions def backend(self, backend_name: str) -> CloudBackend: diff --git a/qiskit_ibm_runtime/api/session.py b/qiskit_ibm_runtime/api/session.py index 07f7dc6ca9..20f390ba7e 100644 --- a/qiskit_ibm_runtime/api/session.py +++ b/qiskit_ibm_runtime/api/session.py @@ -255,7 +255,7 @@ def request( # type: ignore[override] headers.update(kwargs.pop("headers", {})) try: - self._log_request_info(url, method, kwargs) + self._log_request_info(final_url, method, kwargs) response = super().request(method, final_url, headers=headers, **kwargs) response.raise_for_status() except RequestException as ex: diff --git a/qiskit_ibm_runtime/credentials/__init__.py b/qiskit_ibm_runtime/credentials/__init__.py deleted file mode 100644 index 10b22854e4..0000000000 --- a/qiskit_ibm_runtime/credentials/__init__.py +++ /dev/null @@ -1,101 +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. - -""" -====================================================== -Credentials (:mod:`qiskit_ibm_runtime.credentials`) -====================================================== - -.. currentmodule:: qiskit_ibm_runtime.credentials - -Utilities for working with IBM Quantum account credentials. - -Classes -========= - -.. autosummary:: - :toctree: ../stubs/ - - Credentials - -Exceptions -========== -.. autosummary:: - :toctree: ../stubs/ - - -""" - -import logging -from collections import OrderedDict -from typing import Dict, Tuple, Any - -from .credentials import Credentials -from .environ import read_credentials_from_environ -from .exceptions import ( - CredentialsError, - HubGroupProjectIDInvalidStateError, -) -from .hub_group_project_id import HubGroupProjectID - -logger = logging.getLogger(__name__) - - -def discover_credentials() -> Tuple[Dict[HubGroupProjectID, Credentials], Dict]: - """Automatically discover credentials for IBM Quantum. - - This method looks for credentials in the following places in order and - returns the first ones found: - - 1. The environment variables. - - Raises: - HubGroupProjectIDInvalidStateError: If the default provider stored on - disk could not be parsed. - - Returns: - A tuple containing the found credentials and the stored - preferences, if any, in the configuration file. The format - for the found credentials is ``{credentials_unique_id: Credentials}``, - whereas the preferences is ``{credentials_unique_id: {category: {key: val}}}``. - """ - credentials_: Dict[HubGroupProjectID, Credentials] = {} - preferences: Dict[HubGroupProjectID, Dict] = {} - - # dict[str:function] that defines the different locations for looking for - # credentials, and their precedence order. - readers = OrderedDict( - [ - ("environment variables", (read_credentials_from_environ, {})), - ] - ) # type: OrderedDict[str, Any] - - # Attempt to read the credentials from the different sources. - for display_name, (reader_function, kwargs) in readers.items(): - try: - stored_account_info = reader_function(**kwargs) # type: ignore[arg-type] - if display_name == "qiskitrc": - # Read from `qiskitrc`, which may have stored preferences. - credentials_, preferences = stored_account_info - else: - credentials_ = stored_account_info - if credentials_: - logger.info("Using credentials from %s", display_name) - break - except CredentialsError as ex: - logger.warning( - "Automatic discovery of %s credentials failed: %s", - display_name, - str(ex), - ) - - return credentials_, preferences diff --git a/qiskit_ibm_runtime/credentials/credentials.py b/qiskit_ibm_runtime/credentials/credentials.py deleted file mode 100644 index 5c009b05ef..0000000000 --- a/qiskit_ibm_runtime/credentials/credentials.py +++ /dev/null @@ -1,198 +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. - -"""Represent IBM Quantum account credentials.""" - -import re -from typing import Dict, Tuple, Optional, Any - -from requests.auth import AuthBase -from requests_ntlm import HttpNtlmAuth - -from .hub_group_project_id import HubGroupProjectID -from ..accounts import AccountType -from ..api.auth import LegacyAuth, CloudAuth -from ..utils import crn_to_api_host - -REGEX_IBM_HUBS = ( - "(?Phttp[s]://.+/api)" - "/Hubs/(?P[^/]+)/Groups/(?P[^/]+)/Projects/(?P[^/]+)" -) -"""str: Regex that matches an IBM Quantum URL with hub information.""" - -TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" -"""str: Template for creating an IBM Quantum URL with hub/group/project information.""" - - -class Credentials: - """IBM Quantum account credentials and preferences. - - Note: - By convention, two credentials that have the same hub, group, - and project are considered equivalent, regardless of other attributes. - """ - - def __init__( - self, - token: str, - url: str = None, - auth: Optional[AccountType] = None, - instance: Optional[str] = None, - auth_url: Optional[str] = None, - websockets_url: Optional[str] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - proxies: Optional[Dict] = None, - verify: bool = True, - services: Optional[Dict] = None, - access_token: Optional[str] = None, - preferences: Optional[Dict] = None, - default_provider: Optional[HubGroupProjectID] = None, - ) -> None: - """Credentials constructor. - - Args: - token: IBM Quantum API token. - url: IBM Quantum URL (gets replaced with a new-style URL with hub, group, project). - auth_url: IBM Quantum Auth API URL (always https://auth.quantum-computing.ibm.com/api). - websockets_url: URL for websocket server. - hub: The hub to use. - group: The group to use. - project: The project to use. - proxies: Proxy configuration. - verify: If ``False``, ignores SSL certificates errors. - services: Additional services for this account. - access_token: IBM Quantum access token. - preferences: Application preferences. Used for dictating preferred - action in services like the `ExperimentService`. - default_provider: Default provider to use. - """ - self.auth = auth - self.token = token - self.instance = instance - self.access_token = access_token - ( - self.url, - self.base_url, - self.hub, - self.group, - self.project, - ) = _unify_ibm_quantum_url(auth, url, instance, hub, group, project) - self.auth_url = auth_url or url - self.websockets_url = websockets_url - self.proxies = proxies or {} - self.verify = verify - self.preferences = preferences or {} - self.default_provider = default_provider - - # Initialize additional service URLs. - services = services or {} - self.extractor_url = services.get("extractorsService", None) - self.experiment_url = services.get("resultsDB", None) - self.runtime_url = services.get("runtime", None) - - def get_auth_handler(self) -> AuthBase: - """Returns the respective authentication handler.""" - if self.auth == "cloud": - return CloudAuth(api_key=self.token, crn=self.instance) - - return LegacyAuth(access_token=self.access_token) - - def is_ibm_quantum(self) -> bool: - """Return whether the credentials represent an IBM Quantum account.""" - return all([self.hub, self.group, self.project]) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Credentials): - return False - return (self.token == other.token) & (self.unique_id() == other.unique_id()) - - def unique_id(self) -> HubGroupProjectID: - """Return a value that uniquely identifies these credentials. - - By convention, two credentials that have the same hub, group, - and project are considered equivalent. - - Returns: - A ``HubGroupProjectID`` instance. - """ - return HubGroupProjectID(self.hub, self.group, self.project) - - def connection_parameters(self) -> Dict[str, Any]: - """Construct connection related parameters. - - Returns: - A dictionary with connection-related parameters in the format - expected by ``requests``. The following keys can be present: - ``proxies``, ``verify``, and ``auth``. - """ - request_kwargs = {"verify": self.verify} - - if self.proxies: - if "urls" in self.proxies: - request_kwargs["proxies"] = self.proxies["urls"] - - if "username_ntlm" in self.proxies and "password_ntlm" in self.proxies: - request_kwargs["auth"] = HttpNtlmAuth( - self.proxies["username_ntlm"], self.proxies["password_ntlm"] - ) - - return request_kwargs - - -def _unify_ibm_quantum_url( - auth: AccountType, - url: Optional[str] = None, - instance: Optional[str] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, -) -> Tuple[str, str, Optional[str], Optional[str], Optional[str]]: - """Return a new-style set of credential values (url and hub parameters). - - Args: - url: URL for IBM Quantum. - hub: The hub to use. - group: The group to use. - project: The project to use. - - Returns: - A tuple that consists of ``url``, ``base_url``, ``hub``, ``group``, - and ``project``, where - - * url: The new-style IBM Quantum URL that contains - the hub, group, and project names. - * base_url: Base URL that does not contain the hub, group, and - project names. - * hub: The hub to use. - * group: The group to use. - * project: The project to use. - """ - # Check if the URL is "new style", and retrieve embedded parameters from it. - regex_match = re.match(REGEX_IBM_HUBS, url, re.IGNORECASE) - base_url = url - - if auth == "cloud": - base_url = crn_to_api_host(instance) - elif regex_match: - base_url, hub, group, project = regex_match.groups() - else: - if hub and group and project: - # Assume it is an IBM Quantum URL, and update the url. - url = TEMPLATE_IBM_HUBS.format( - prefix=url, hub=hub, group=group, project=project - ) - else: - # Cleanup the hub, group and project, without modifying the url. - hub = group = project = None - return url, base_url, hub, group, project diff --git a/qiskit_ibm_runtime/credentials/environ.py b/qiskit_ibm_runtime/credentials/environ.py deleted file mode 100644 index e59292665c..0000000000 --- a/qiskit_ibm_runtime/credentials/environ.py +++ /dev/null @@ -1,67 +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. - -"""Utilities for reading credentials from environment variables.""" - -import os -from typing import Dict - -from .credentials import Credentials -from .hub_group_project_id import HubGroupProjectID - -VARIABLES_MAP = { - "QISKIT_IBM_RUNTIME_API_TOKEN": "token", - "QISKIT_IBM_RUNTIME_API_URL": "url", - "QISKIT_IBM_RUNTIME_HUB": "hub", - "QISKIT_IBM_RUNTIME_GROUP": "group", - "QISKIT_IBM_RUNTIME_PROJECT": "project", -} -"""Dictionary that maps `ENV_VARIABLE_NAME` to credential parameter.""" - - -def read_credentials_from_environ() -> Dict[HubGroupProjectID, Credentials]: - """Extract credentials from the environment variables. - - Returns: - A dictionary containing the credentials, in the - ``{credentials_unique_id: Credentials}`` format. - """ - # The token is the only required parameter. - if not ( - os.getenv("QISKIT_IBM_RUNTIME_API_TOKEN") - and os.getenv("QISKIT_IBM_RUNTIME_API_URL") - ): - return {} - - # Build the credentials based on environment variables. - credentials_dict = {} - hub = None - group = None - project = None - - for envar_name, credential_key in VARIABLES_MAP.items(): - if os.getenv(envar_name): - credentials_dict[credential_key] = os.getenv(envar_name) - if envar_name == "QISKIT_IBM_RUNTIME_API_URL": - credentials_dict["auth_url"] = os.getenv(envar_name) - elif envar_name == "QISKIT_IBM_RUNTIME_HUB": - hub = os.getenv(envar_name) - elif envar_name == "QISKIT_IBM_RUNTIME_GROUP": - group = os.getenv(envar_name) - elif envar_name == "QISKIT_IBM_RUNTIME_PROJECT": - project = os.getenv(envar_name) - - if all([hub, group, project]): - credentials_dict["default_provider"] = HubGroupProjectID(hub, group, project) - - credentials = Credentials(**credentials_dict) # type: ignore[arg-type] - return {credentials.unique_id(): credentials} diff --git a/qiskit_ibm_runtime/credentials/exceptions.py b/qiskit_ibm_runtime/credentials/exceptions.py deleted file mode 100644 index a5102620b2..0000000000 --- a/qiskit_ibm_runtime/credentials/exceptions.py +++ /dev/null @@ -1,33 +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. - -"""Exception for the ``Credentials`` module.""" - -from ..exceptions import IBMError - - -class CredentialsError(IBMError): - """Base class for errors raised during credential management.""" - - pass - - -class HubGroupProjectIDError(IBMError): - """Base class for errors raised by the hub_group_project_id module.""" - - pass - - -class HubGroupProjectIDInvalidStateError(HubGroupProjectIDError): - """Errors raised when a HubGroupProjectID is in an invalid state for an operation.""" - - pass diff --git a/qiskit_ibm_runtime/credentials/hub_group_project_id.py b/qiskit_ibm_runtime/credentials/hub_group_project_id.py deleted file mode 100644 index 545b2bea5b..0000000000 --- a/qiskit_ibm_runtime/credentials/hub_group_project_id.py +++ /dev/null @@ -1,116 +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. - -"""Model for representing a hub/group/project id tuple.""" - -from typing import Tuple, Optional - -from .exceptions import HubGroupProjectIDInvalidStateError - - -class HubGroupProjectID: - """Class for representing a hub/group/project id.""" - - def __init__(self, hub: str = None, group: str = None, project: str = None) -> None: - """HubGroupProjectID constructor.""" - self.hub = hub - self.group = group - self.project = project - - @classmethod - def from_stored_format(cls, hgp_id: str) -> "HubGroupProjectID": - """Instantiates a ``HubGroupProjectID`` instance from a string. - - Note: - The format for the string is ``//``. - It is saved inside the configuration file to specify a default provider. - - Raises: - HubGroupProjectIDInvalidStateError: If the specified string, used for conversion, is - in an invalid format. - - Returns: - A ``HubGroupProjectID`` instance. - """ - try: - hub, group, project = hgp_id.split("/") - if (not hub) or (not group) or (not project): - raise HubGroupProjectIDInvalidStateError( - 'The hub/group/project id "{}" is in an invalid format. ' - 'Every field must be specified: hub = "{}", group = "{}", project = "{}".'.format( - hgp_id, hub, group, project - ) - ) - except ValueError: - # Not enough, or too many, values were provided. - raise HubGroupProjectIDInvalidStateError( - 'The hub/group/project id "{}" is in an invalid format. ' - 'Use the "//" format.'.format( - hgp_id - ) - ) - - return cls(hub, group, project) - - @classmethod - def from_credentials( - cls, credentials_obj: "Credentials" # type: ignore - ) -> "HubGroupProjectID": - """Instantiates a ``HubGroupProjectID`` instance from ``Credentials``. - - Returns: - A ``HubGroupProjectID`` instance. - """ - hub, group, project = [ - getattr(credentials_obj, key) for key in ["hub", "group", "project"] - ] - return cls(hub, group, project) - - def to_stored_format(self) -> str: - """Returns ``HubGroupProjectID`` as a string. - - Note: - The format of the string returned is ``//``. - It is used to save a default hub/group/project in the configuration file. - - Raises: - HubGroupProjectIDInvalidStateError: If the ``HubGroupProjectID`` cannot be - represented as a string to store on disk (i.e. Some of the hub, group, - project fields are empty strings or ``None``). - - Returns: - A string representation of the hub/group/project id, used to store to disk. - """ - if (not self.hub) or (not self.group) or (not self.project): - raise HubGroupProjectIDInvalidStateError( - "The hub/group/project id cannot be represented in the stored format. " - 'Every field must be specified: hub = "{}", group = "{}", project = "{}".'.format( - self.hub, self.group, self.project - ) - ) - return "/".join([self.hub, self.group, self.project]) - - def to_tuple(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: - """Returns ``HubGroupProjectID`` as a tuple.""" - return self.hub, self.group, self.project - - def __eq__(self, other: "HubGroupProjectID") -> bool: # type: ignore - """Two instances are equal if they define the same hub, group, project.""" - return (self.hub, self.group, self.project) == ( - other.hub, - other.group, - other.project, - ) - - def __hash__(self) -> int: - """Returns a hash for an instance.""" - return hash((self.hub, self.group, self.project)) diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 621f89cd0b..e6135d80bb 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -33,36 +33,6 @@ class IBMProviderValueError(IBMProviderError): pass -class IBMProviderCredentialsNotFound(IBMProviderError): - """Errors raised when credentials are not found.""" - - pass - - -class IBMProviderMultipleCredentialsFound(IBMProviderError): - """Errors raised when multiple credentials are found.""" - - pass - - -class IBMProviderCredentialsInvalidFormat(IBMProviderError): - """Errors raised when the credentials format is invalid.""" - - pass - - -class IBMProviderCredentialsInvalidToken(IBMProviderError): - """Errors raised when an IBM Quantum token is invalid.""" - - pass - - -class IBMProviderCredentialsInvalidUrl(IBMProviderError): - """Errors raised when an IBM Quantum URL is invalid.""" - - pass - - class IBMBackendError(IBMError): """Base class for errors raised by the backend modules.""" @@ -105,37 +75,37 @@ class IBMApiError(IBMError): pass -class QiskitRuntimeError(IBMError): +class IBMRuntimeError(IBMError): """Base class for errors raised by the runtime service modules.""" pass -class RuntimeDuplicateProgramError(QiskitRuntimeError): +class RuntimeDuplicateProgramError(IBMRuntimeError): """Error raised when a program being uploaded already exists.""" pass -class RuntimeProgramNotFound(QiskitRuntimeError): +class RuntimeProgramNotFound(IBMRuntimeError): """Error raised when a program is not found.""" pass -class RuntimeJobFailureError(QiskitRuntimeError): +class RuntimeJobFailureError(IBMRuntimeError): """Error raised when a runtime job failed.""" pass -class RuntimeJobNotFound(QiskitRuntimeError): +class RuntimeJobNotFound(IBMRuntimeError): """Error raised when a job is not found.""" pass -class RuntimeInvalidStateError(QiskitRuntimeError): +class RuntimeInvalidStateError(IBMRuntimeError): """Errors raised when the state is not valid for the operation.""" pass diff --git a/qiskit_ibm_runtime/hub_group_project.py b/qiskit_ibm_runtime/hub_group_project.py index 34b27c66fe..0cf12e2096 100644 --- a/qiskit_ibm_runtime/hub_group_project.py +++ b/qiskit_ibm_runtime/hub_group_project.py @@ -14,20 +14,16 @@ import logging from collections import OrderedDict -import traceback from typing import Any, Dict, Optional -from qiskit.providers.backend import BackendV1 as Backend -from qiskit.providers.models import PulseBackendConfiguration, QasmBackendConfiguration from qiskit_ibm_runtime import ( # pylint: disable=unused-import - ibm_runtime_service, ibm_backend, ) -from .utils.backend import decode_backend_configuration from .api.clients import AccountClient -from .credentials import Credentials -from .exceptions import IBMInputValueError +from .utils.backend_decoder import configuration_from_server_data +from .api.client_parameters import ClientParameters +from .utils.hgp import from_instance_format logger = logging.getLogger(__name__) @@ -37,31 +33,19 @@ class HubGroupProject: def __init__( self, - credentials: Credentials, - service: "ibm_runtime_service.IBMRuntimeService", - is_open: bool, + client_params: ClientParameters, + instance: str, ) -> None: """HubGroupProject constructor Args: - credentials: IBM Quantum credentials. - service: IBM Quantum account provider. - is_open: True means open access, False means premium + client_params: Parameters used for server connection. + instance: Hub/group/project. """ - self.credentials = credentials - self._service = service - self.is_open = is_open - self._api_client = AccountClient( - self.credentials, **self.credentials.connection_parameters() - ) + self._api_client = AccountClient(client_params) # Initialize the internal list of backends. self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} - self._service_urls = { - "backend": self.credentials.url, - "experiment": self.credentials.experiment_url, - "random": self.credentials.extractor_url, - "runtime": self.credentials.runtime_url, - } + self._hub, self._group, self._project = from_instance_format(instance) @property def backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: @@ -83,72 +67,49 @@ def backends(self, value: Dict[str, "ibm_backend.IBMBackend"]) -> None: """ self._backends = value - def _discover_remote_backends( - self, timeout: Optional[float] = None - ) -> Dict[str, "ibm_backend.IBMBackend"]: + def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: """Return the remote backends available for this hub/group/project. - Args: - timeout: Maximum number of seconds to wait for the discovery of - remote backends. - Returns: A dict of the remote backend instances, keyed by backend name. """ - ret = OrderedDict() # type: ignore[var-annotated] - configs_list = self._api_client.list_backends(timeout=timeout) + ret = OrderedDict() + configs_list = self._api_client.list_backends() for raw_config in configs_list: - # Make sure the raw_config is of proper type - if not isinstance(raw_config, dict): - logger.warning( - "An error occurred when retrieving backend " - "information. Some backends might not be available." - ) + config = configuration_from_server_data( + raw_config=raw_config, instance=self.name + ) + if not config: continue - try: - decode_backend_configuration(raw_config) - try: - config = PulseBackendConfiguration.from_dict(raw_config) - except (KeyError, TypeError): - config = QasmBackendConfiguration.from_dict(raw_config) - backend_cls = ( - ibm_backend.IBMSimulator - if config.simulator - else ibm_backend.IBMBackend - ) - ret[config.backend_name] = backend_cls( - configuration=config, - service=self._service, - credentials=self.credentials, - account_client=self._api_client, - ) - except Exception: # pylint: disable=broad-except - logger.warning( - 'Remote backend "%s" for provider %s could not be instantiated due to an ' - "invalid config: %s", - raw_config.get("backend_name", raw_config.get("name", "unknown")), - repr(self), - traceback.format_exc(), - ) + backend_cls = ( + ibm_backend.IBMSimulator if config.simulator else ibm_backend.IBMBackend + ) + ret[config.backend_name] = backend_cls( + configuration=config, + api_client=self._api_client, + ) return ret - def get_backend(self, name: str) -> Optional[Backend]: + def backend(self, name: str) -> Optional["ibm_backend.IBMBackend"]: """Get backend by name.""" return self._backends.get(name, None) - def has_service(self, name: str) -> bool: - """Check if hgp has service by name.""" - if name not in self._service_urls: - raise IBMInputValueError(f"Unknown service {name} specified.") - return self._service_urls[name] is not None + @property + def name(self) -> str: + """Returns the unique id. + + Returns: + An ID uniquely represents this h/g/p. + """ + return f"{self._hub}/{self._group}/{self._project}" def __repr__(self) -> str: - credentials_info = "hub='{}', group='{}', project='{}'".format( - self.credentials.hub, self.credentials.group, self.credentials.project + hgp_info = "hub='{}', group='{}', project='{}'".format( + self._hub, self._group, self._project ) - return "<{}({})>".format(self.__class__.__name__, credentials_info) + return "<{}({})>".format(self.__class__.__name__, hgp_info) def __eq__(self, other: Any) -> bool: if not isinstance(other, HubGroupProject): return False - return self.credentials == other.credentials + return self.name == other.name diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 294155be0f..659680e9f8 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -14,7 +14,7 @@ import logging -from typing import List, Union, Optional, Any +from typing import Union, Optional, Any from datetime import datetime as python_datetime from qiskit.qobj.utils import MeasLevel, MeasReturnType @@ -28,16 +28,14 @@ ) from qiskit.providers.models import QasmBackendConfiguration, PulseBackendConfiguration -# pylint: disable=unused-import, cyclic-import -from qiskit_ibm_runtime import ibm_runtime_service - from .api.clients import AccountClient, RuntimeClient -from .backendreservation import BackendReservation -from .credentials import Credentials +from .api.clients.backend import BaseBackendClient from .exceptions import IBMBackendApiProtocolError -from .utils.converters import utc_to_local_all, local_to_utc -from .utils.backend import decode_pulse_defaults, decode_backend_properties -from .utils.backend import convert_reservation_data +from .utils.converters import local_to_utc +from .utils.backend_decoder import ( + defaults_from_server_data, + properties_from_server_data, +) logger = logging.getLogger(__name__) @@ -67,27 +65,17 @@ class IBMBackend(Backend): def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - account_client: Optional[AccountClient] = None, - runtime_client: Optional[RuntimeClient] = None, + api_client: BaseBackendClient, ) -> None: """IBMBackend constructor. Args: configuration: Backend configuration. - service: IBM Quantum account provider. - credentials: IBM Quantum credentials. api_client: IBM Quantum client used to communicate with the server. """ - super().__init__(provider=service, configuration=configuration) + super().__init__(configuration=configuration) - self._account_client = account_client - self._runtime_client = runtime_client - self._credentials = credentials - self.hub = credentials.hub - self.group = credentials.group - self.project = credentials.project + self._api_client = api_client # Attributes used by caching functions. self._properties = None @@ -131,6 +119,7 @@ def properties( datetime: By specifying `datetime`, this function returns an instance of the :class:`BackendProperties` whose timestamp is closest to, but older than, the specified `datetime`. + Note that this is only supported using legacy runtime. Returns: The backend properties or ``None`` if the backend properties are not @@ -138,6 +127,7 @@ def properties( Raises: TypeError: If an input argument is not of the correct type. + NotImplementedError: If `datetime` is specified when cloud rutime is used. """ # pylint: disable=arguments-differ if not isinstance(refresh, bool): @@ -145,24 +135,23 @@ def properties( "The 'refresh' argument needs to be a boolean. " "{} is of type {}".format(refresh, type(refresh)) ) - if datetime and not isinstance(datetime, python_datetime): - raise TypeError("'{}' is not of type 'datetime'.") if datetime: + if not isinstance(datetime, python_datetime): + raise TypeError("'{}' is not of type 'datetime'.") + if isinstance(self._api_client, RuntimeClient): + raise NotImplementedError( + "'datetime' is not supported by cloud runtime." + ) datetime = local_to_utc(datetime) if datetime or refresh or self._properties is None: - if self._account_client: - api_properties = self._account_client.backend_properties( - self.name(), datetime=datetime - ) - elif self._runtime_client: - api_properties = self._runtime_client.backend_properties(self.name()) + api_properties = self._api_client.backend_properties( + self.name(), datetime=datetime + ) if not api_properties: return None - decode_backend_properties(api_properties) - api_properties = utc_to_local_all(api_properties) - backend_properties = BackendProperties.from_dict(api_properties) + backend_properties = properties_from_server_data(api_properties) if datetime: # Don't cache result. return backend_properties self._properties = backend_properties @@ -182,10 +171,7 @@ def status(self) -> BackendStatus: Raises: IBMBackendApiProtocolError: If the status for the backend cannot be formatted properly. """ - if self._account_client: - api_status = self._account_client.backend_status(self.name()) - elif self._runtime_client: - api_status = self._runtime_client.backend_status(self.name()) + api_status = self._api_client.backend_status(self.name()) try: return BackendStatus.from_dict(api_status) @@ -210,45 +196,14 @@ def defaults(self, refresh: bool = False) -> Optional[PulseDefaults]: The backend pulse defaults or ``None`` if the backend does not support pulse. """ if refresh or self._defaults is None: - if self._account_client: - api_defaults = self._api_client.backend_pulse_defaults(self.name()) - elif self._runtime_client: - api_defaults = self._runtime_client.backend_pulse_defaults(self.name()) + api_defaults = self._api_client.backend_pulse_defaults(self.name()) if api_defaults: - decode_pulse_defaults(api_defaults) - self._defaults = PulseDefaults.from_dict(api_defaults) + self._defaults = defaults_from_server_data(api_defaults) else: self._defaults = None return self._defaults - def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None, - ) -> List[BackendReservation]: - """Return backend reservations. - - If start_datetime and/or end_datetime is specified, reservations with - time slots that overlap with the specified time window will be returned. - - Some of the reservation information is only available if you are the - owner of the reservation. - - Args: - start_datetime: Filter by the given start date/time, in local timezone. - end_datetime: Filter by the given end date/time, in local timezone. - - Returns: - A list of reservations that match the criteria. - """ - start_datetime = local_to_utc(start_datetime) if start_datetime else None - end_datetime = local_to_utc(end_datetime) if end_datetime else None - raw_response = self._account_client.backend_reservations( - self.name(), start_datetime, end_datetime - ) - return convert_reservation_data(raw_response, self.name()) - def configuration( self, ) -> Union[QasmBackendConfiguration, PulseBackendConfiguration]: @@ -300,19 +255,15 @@ class IBMRetiredBackend(IBMBackend): def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - api_client: AccountClient, + api_client: Optional[AccountClient] = None, ) -> None: """IBMRetiredBackend constructor. Args: configuration: Backend configuration. - service: IBM Quantum account provider. - credentials: IBM Quantum credentials. api_client: IBM Quantum client used to communicate with the server. """ - super().__init__(configuration, service, credentials, api_client) + super().__init__(configuration, api_client) self._status = BackendStatus( backend_name=self.name(), backend_version=self.configuration().backend_version, @@ -340,20 +291,11 @@ def status(self) -> BackendStatus: """Return the backend status.""" return self._status - def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None, - ) -> List[BackendReservation]: - return [] - @classmethod def from_name( cls, backend_name: str, - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - api: AccountClient, + api: Optional[AccountClient] = None, ) -> "IBMRetiredBackend": """Return a retired backend from its name.""" configuration = QasmBackendConfiguration( @@ -370,4 +312,4 @@ def from_name( gates=[GateConfig(name="TODO", parameters=[], qasm_def="TODO")], coupling_map=[[0, 1]], ) - return cls(configuration, service, credentials, api) + return cls(configuration, api) diff --git a/qiskit_ibm_runtime/ibm_runtime_service.py b/qiskit_ibm_runtime/ibm_runtime_service.py index 16c24336e4..4f19accb8d 100644 --- a/qiskit_ibm_runtime/ibm_runtime_service.py +++ b/qiskit_ibm_runtime/ibm_runtime_service.py @@ -12,45 +12,41 @@ """Qiskit runtime service.""" -import copy import json import logging -import re import traceback import warnings from collections import OrderedDict from typing import Dict, Callable, Optional, Union, List, Any, Type -from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import BackendV1 as Backend from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.models import PulseBackendConfiguration, QasmBackendConfiguration from qiskit.providers.providerutils import filter_backends -from qiskit.transpiler import Layout -from qiskit_ibm_runtime import runtime_job, ibm_backend # pylint: disable=unused-import +from qiskit_ibm_runtime import ibm_backend from .accounts import AccountManager, Account, AccountType +from .accounts.exceptions import AccountsError +from .proxies import ProxyConfiguration from .api.clients import AuthClient, VersionClient from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError -from .backendreservation import BackendReservation from .constants import QISKIT_IBM_RUNTIME_API_URL -from .credentials import Credentials, HubGroupProjectID from .exceptions import IBMNotAuthorizedError, IBMInputValueError, IBMProviderError from .exceptions import ( - QiskitRuntimeError, + IBMRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound, - IBMProviderCredentialsInvalidUrl, ) from .hub_group_project import HubGroupProject # pylint: disable=cyclic-import from .program.result_decoder import ResultDecoder -from .runner_result import RunnerResult from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ParameterNamespace from .utils import RuntimeDecoder, to_base64_string, to_python_identifier -from .utils.backend import convert_reservation_data, decode_backend_configuration +from .utils.backend_decoder import configuration_from_server_data +from .utils.hgp import to_instance_format, from_instance_format +from .api.client_parameters import ClientParameters +from .runtime_options import RuntimeOptions logger = logging.getLogger(__name__) @@ -117,81 +113,159 @@ class IBMRuntimeService: def __init__( self, + auth: Optional[AccountType] = None, token: Optional[str] = None, url: Optional[str] = None, - instance: Optional[str] = None, - auth: Optional[AccountType] = None, name: Optional[str] = None, + instance: Optional[str] = None, proxies: Optional[dict] = None, verify: Optional[bool] = None, ) -> None: """IBMRuntimeService constructor + An account is selected in the following order: + + - Account with the input `name`, if specified. + - Default account for the `auth` type, if `auth` is specified but `token` is not. + - Account defined by the input `auth` and `token`, if specified. + - Account defined by the environment variables, if defined. + - Default account for the cloud account, if one is available. + - Default account for the legacy account, if one is available. + + `instance`, `proxies`, and `verify` can be used to overwrite corresponding + values in the loaded account. + Args: + auth: Authentication type. ``cloud`` or ``legacy``. token: IBM Cloud API key or IBM Quantum API token. url: The API URL. Defaults to https://cloud.ibm.com (cloud) or https://auth.quantum-computing.ibm.com/api (legacy). - instance: The CRN (cloud) or hub/group/project (legacy). - auth: Authentication type. `cloud` or `legacy`. name: Name of the account to load. - proxies: Proxy configuration for the server. - verify: Verify the server's TLS certificate. + instance: The service instance to use. For cloud runtime, this is the Cloud Resource + Name (CRN). For legacy runtime, this is the hub/group/project in that format. + proxies: Proxy configuration. Supported optional keys are + ``urls`` (a dictionary mapping protocol or protocol and host to the URL of the proxy, + documented at https://docs.python-requests.org/en/latest/api/#requests.Session.proxies), + ``username_ntlm``, ``password_ntlm`` (username and password to enable NTLM user + authentication) + verify: Whether to verify the server's TLS certificate. Returns: An instance of IBMRuntimeService. + + Raises: + IBMInputValueError: If an input is invalid. """ super().__init__() - # TODO: add support for loading default account when optional parameters are not set - # i.e. fallback to environment variables - # i.e. fallback to default account saved on disk - self.account = ( - AccountManager.get(name=name) - if name - else Account( - auth=auth, - token=token, - url=url, - instance=instance, - proxies=proxies, - verify=verify, - ) + self._account = self._discover_account( + token=token, + url=url, + instance=instance, + auth=auth, + name=name, + proxies=ProxyConfiguration(**proxies) if proxies else None, + verify=verify, ) - self.account_credentials = Credentials( - auth=self.account.auth, - token=self.account.token, - url=self.account.url, - instance=self.account.instance, - proxies=self.account.proxies, - verify=self.account.verify, + + self._client_params = ClientParameters( + auth_type=self._account.auth, + token=self._account.token, + url=self._account.url, + instance=self._account.instance, + proxies=self._account.proxies, + verify=self._account.verify, ) + + self._auth = self._account.auth self._programs: Dict[str, RuntimeProgram] = {} self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} - if auth == "cloud": - self._api_client = RuntimeClient(credentials=self.account_credentials) - self._backends = self._discover_remote_backends() + if self._auth == "cloud": + self._api_client = RuntimeClient(self._client_params) + # TODO: We can make the backend discovery lazy + self._backends = self._discover_cloud_backends() + return else: - self._initialize_hgps(credentials=self.account_credentials) - self._api_client = None - hgps = self._get_hgps() - for hgp in hgps: + auth_client = self._authenticate_legacy_account(self._client_params) + # Update client parameters to use authenticated values. + self._client_params.url = auth_client.current_service_urls()["services"][ + "runtime" + ] + self._client_params.token = auth_client.current_access_token() + self._api_client = RuntimeClient(self._client_params) + self._hgps = self._initialize_hgps(auth_client) + for hgp in self._hgps.values(): for backend_name, backend in hgp.backends.items(): if backend_name not in self._backends: self._backends[backend_name] = backend - if not self._api_client and hgp.has_service("runtime"): - self._default_hgp = hgp - self._api_client = RuntimeClient(self._default_hgp.credentials) - self._access_token = self._default_hgp.credentials.access_token - self._ws_url = self._default_hgp.credentials.runtime_url.replace( - "https", "wss" - ) - self._programs = {} - - self._discover_backends() - - def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: + + # TODO - it'd be nice to allow some kind of autocomplete, but `service.ibmq_foo` + # just seems wrong since backends are not runtime service instances. + # self._discover_backends() + + def _discover_account( + self, + token: Optional[str] = None, + url: Optional[str] = None, + instance: Optional[str] = None, + auth: Optional[AccountType] = None, + name: Optional[str] = None, + proxies: Optional[ProxyConfiguration] = None, + verify: Optional[bool] = None, + ) -> Account: + """Discover account.""" + account = None + verify_ = verify or True + if name: + if any([auth, token, url]): + logger.warning( + "Loading account with name %s. Any input 'auth', 'token', 'url' are ignored.", + name, + ) + account = AccountManager.get(name=name) + elif auth: + if auth not in ["legacy", "cloud"]: + raise ValueError("'auth' can only be 'cloud' or 'legacy'") + if token: + return Account( + auth=auth, + token=token, + url=url, + instance=instance, + proxies=proxies, + verify=verify_, + ).validate() + if url: + logger.warning( + "Loading default %s account. Input 'url' is ignored.", auth + ) + account = AccountManager.get(auth=auth) + elif any([token, url]): + # Let's not infer based on these attributes as they may change in the future. + raise ValueError( + "'auth' is required if 'token', or 'url' is specified but 'name' is not." + ) + + if account is None: + account = AccountManager.get() + if account is None: + raise AccountsError("Unable to find account.") + + if instance: + account.instance = instance + if proxies: + account.proxies = proxies + if verify is not None: + account.verify = verify + + # ensure account is valid, fail early if not + account.validate() + + return account + + def _discover_cloud_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: """Return the remote backends available for this service instance. Returns: @@ -203,99 +277,92 @@ def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: raw_config = self._api_client.backend_configuration( backend_name=backend_name ) - # Make sure the raw_config is of proper type - if not isinstance(raw_config, dict): - logger.warning( - "An error occurred when retrieving backend " - "information. Some backends might not be available." - ) + config = configuration_from_server_data( + raw_config=raw_config, instance=self._account.instance + ) + if not config: continue - try: - decode_backend_configuration(raw_config) - try: - config = PulseBackendConfiguration.from_dict(raw_config) - except (KeyError, TypeError): - config = QasmBackendConfiguration.from_dict(raw_config) - backend_cls = ( - ibm_backend.IBMSimulator - if config.simulator - else ibm_backend.IBMBackend - ) - ret[config.backend_name] = backend_cls( - configuration=config, - service=self, - credentials=self.account_credentials, - runtime_client=self._api_client, - ) - except Exception: # pylint: disable=broad-except - logger.warning( - 'Remote backend "%s" for service instance %s could not be instantiated due to an ' - "invalid config: %s", - raw_config.get("backend_name", raw_config.get("name", "unknown")), - repr(self), - traceback.format_exc(), - ) + backend_cls = ( + ibm_backend.IBMSimulator if config.simulator else ibm_backend.IBMBackend + ) + ret[config.backend_name] = backend_cls( + configuration=config, + api_client=self._api_client, + ) + return ret - def _initialize_hgps( - self, credentials: Credentials, preferences: Optional[Dict] = None - ) -> None: + def _authenticate_legacy_account( + self, client_params: ClientParameters + ) -> AuthClient: """Authenticate against IBM Quantum and populate the hub/group/projects. Args: - credentials: Credentials for IBM Quantum. - preferences: Account preferences. + client_params: Parameters used for server connection. Raises: - IBMProviderCredentialsInvalidUrl: If the URL specified is not - a valid IBM Quantum authentication URL. - IBMProviderError: If no hub/group/project could be found for this account. + IBMInputValueError: If the URL specified is not a valid IBM Quantum authentication URL. + IBMNotAuthorizedError: If the account is not authorized to use runtime. + + Returns: + Authentication client. """ - self._hgps: Dict[HubGroupProjectID, HubGroupProject] = OrderedDict() - version_info = self._check_api_version(credentials) + version_info = self._check_api_version(client_params) # Check the URL is a valid authentication URL. if not version_info["new_api"] or "api-auth" not in version_info: - raise IBMProviderCredentialsInvalidUrl( + raise IBMInputValueError( "The URL specified ({}) is not an IBM Quantum authentication URL. " "Valid authentication URL: {}.".format( - credentials.url, QISKIT_IBM_RUNTIME_API_URL + client_params.url, QISKIT_IBM_RUNTIME_API_URL ) ) - auth_client = AuthClient( - credentials.token, - credentials.base_url, - **credentials.connection_parameters(), - ) + auth_client = AuthClient(client_params) + service_urls = auth_client.current_service_urls() + if not service_urls.get("services", {}).get(SERVICE_NAME): + raise IBMNotAuthorizedError( + "This account is not authorized to use legacy runtime service." + ) + return auth_client + + def _initialize_hgps( + self, + auth_client: AuthClient, + ) -> Dict: + """Authenticate against IBM Quantum and populate the hub/group/projects. + + Args: + auth_client: Authentication data. + + Raises: + IBMInputValueError: If the URL specified is not a valid IBM Quantum authentication URL. + IBMProviderError: If no hub/group/project could be found for this account. + + Returns: + The hub/group/projects for this account. + """ + # pylint: disable=unsubscriptable-object + hgps: OrderedDict[str, HubGroupProject] = OrderedDict() service_urls = auth_client.current_service_urls() user_hubs = auth_client.user_hubs() - preferences = preferences or {} - is_open = True # First hgp is open access for hub_info in user_hubs: # Build credentials. - hgp_credentials = Credentials( - auth=credentials.auth, - token=credentials.token, - access_token=auth_client.current_access_token(), - instance=credentials.instance, + hgp_params = ClientParameters( + auth_type=self._account.auth, + token=auth_client.current_access_token(), url=service_urls["http"], - auth_url=credentials.auth_url, - websockets_url=service_urls["ws"], - proxies=credentials.proxies, - verify=credentials.verify, - services=service_urls.get("services", {}), - default_provider=credentials.default_provider, - **hub_info, - ) - hgp_credentials.preferences = preferences.get( - hgp_credentials.unique_id(), {} + instance=to_instance_format( + hub_info["hub"], hub_info["group"], hub_info["project"] + ), + proxies=self._account.proxies, + verify=self._account.verify, ) + # Build the hgp. try: hgp = HubGroupProject( - credentials=hgp_credentials, service=self, is_open=is_open + client_params=hgp_params, instance=hgp_params.instance ) - self._hgps[hgp.credentials.unique_id()] = hgp - is_open = False # hgps after first are premium and not open access + hgps[hgp.name] = hgp except Exception: # pylint: disable=broad-except # Catch-all for errors instantiating the hgp. logger.warning( @@ -303,122 +370,88 @@ def _initialize_hgps( hub_info, traceback.format_exc(), ) - if not self._hgps: + if not hgps: raise IBMProviderError( - "No hub/group/project could be found for this account." + "No hub/group/project that supports Qiskit Runtime could " + "be found for this account." ) # Move open hgp to end of the list - if len(self._hgps) > 1: - open_hgp = self._get_hgp() - self._hgps.move_to_end(open_hgp.credentials.unique_id()) - if credentials.default_provider: - # Move user selected hgp to front of the list - hub, group, project = credentials.default_provider.to_tuple() - default_hgp = self._get_hgp(hub=hub, group=group, project=project) - self._hgps.move_to_end(default_hgp.credentials.unique_id(), last=False) + if len(hgps) > 1: + open_key, open_val = hgps.popitem(last=False) + hgps[open_key] = open_val + + default_hgp = self._account.instance + if default_hgp: + if default_hgp in hgps: + # Move user selected hgp to front of the list + hgps.move_to_end(default_hgp, last=False) + else: + warnings.warn( + f"Default hub/group/project {default_hgp} not " + "found for the account and is ignored." + ) + return hgps @staticmethod - def _check_api_version(credentials: Credentials) -> Dict[str, Union[bool, str]]: - """Check the version of the remote server in a set of credentials. + def _check_api_version(params: ClientParameters) -> Dict[str, Union[bool, str]]: + """Check the version of the remote server in a set of client parameters. Args: - credentials: IBM Quantum Credentials + params: Parameters used for server connection. Returns: A dictionary with version information. """ - version_finder = VersionClient( - credentials.base_url, **credentials.connection_parameters() - ) + version_finder = VersionClient(url=params.url, **params.connection_parameters()) return version_finder.version() def _get_hgp( self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + instance: Optional[str] = None, backend_name: Optional[str] = None, - service_name: Optional[str] = None, ) -> HubGroupProject: - """Return an instance of `HubGroupProject` for a single hub/group/project combination. + """Return an instance of `HubGroupProject`. This function also allows to find the `HubGroupProject` that contains a backend - `backend_name` providing service `service_name`. + `backend_name`. Args: - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + instance: The hub/group/project to use. backend_name: Name of the IBM Quantum backend. - service_name: Name of the IBM Quantum service. Returns: An instance of `HubGroupProject` that matches the specified criteria or the default. Raises: - IBMProviderError: If no hub/group/project matches the specified criteria, - if more than one hub/group/project matches the specified criteria, if - no hub/group/project could be found for this account or if no backend matches the - criteria. + IBMInputValueError: If no hub/group/project matches the specified criteria, + or if the input value is in an incorrect format. + QiskitBackendNotFoundError: If backend cannot be found. """ - # If any `hub`, `group`, or `project` is specified, make sure all parameters are set. - if any([hub, group, project]) and not all([hub, group, project]): - raise IBMProviderError( - "The hub, group, and project parameters must all be " - "specified. " - 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) - ) - hgps = self._get_hgps(hub=hub, group=group, project=project) - if any([hub, group, project]): - if not hgps: - raise IBMProviderError( - "No hub/group/project matches the specified criteria: " - "hub = {}, group = {}, project = {}".format(hub, group, project) + if instance: + _ = from_instance_format(instance) # Verify format + if instance not in self._hgps: + raise IBMInputValueError( + f"Hub/group/project {instance} " + "could not be found for this account." ) - if len(hgps) > 1: - raise IBMProviderError( - "More than one hub/group/project matches the " - "specified criteria. hub = {}, group = {}, project = {}".format( - hub, group, project - ) + if backend_name and not self._hgps[instance].backend(backend_name): + raise QiskitBackendNotFoundError( + f"Backend {backend_name} cannot be found in " + f"hub/group/project {instance}" ) - elif not hgps: - # Prevent edge case where no hub/group/project is available. - raise IBMProviderError( - "No hub/group/project could be found for this account." - ) - elif backend_name and service_name: - for hgp in hgps: - if hgp.has_service(service_name) and hgp.get_backend(backend_name): - return hgp - raise IBMProviderError("No backend matches the criteria.") - return hgps[0] - - def _get_hgps( - self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - ) -> List[HubGroupProject]: - """Return a list of `HubGroupProject` instances, subject to optional filtering. + return self._hgps[instance] - Args: - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + if not backend_name: + return list(self._hgps.values())[0] - Returns: - A list of `HubGroupProject` instances that match the specified criteria. - """ - filters: List[Callable[[HubGroupProjectID], bool]] = [] - if hub: - filters.append(lambda hgp: hgp.hub == hub) - if group: - filters.append(lambda hgp: hgp.group == group) - if project: - filters.append(lambda hgp: hgp.project == project) - hgps = [hgp for key, hgp in self._hgps.items() if all(f(key) for f in filters)] - return hgps + for hgp in self._hgps.values(): + if hgp.backend(backend_name): + return hgp + + raise QiskitBackendNotFoundError( + f"Backend {backend_name} cannot be found in any" + f"hub/group/project for this account." + ) def _discover_backends(self) -> None: """Discovers the remote backends for this account, if not already known.""" @@ -432,32 +465,23 @@ def _discover_backends(self) -> None: def backends( self, name: Optional[str] = None, - filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, min_num_qubits: Optional[int] = None, - input_allowed: Optional[Union[str, List[str]]] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + instance: Optional[str] = None, + filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, **kwargs: Any, ) -> List["ibm_backend.IBMBackend"]: """Return all backends accessible via this account, subject to optional filtering. Args: name: Backend name to filter by. + min_num_qubits: Minimum number of qubits the backend has to have. + instance: This is only supported for legacy runtime and is in the + hub/group/project format. filters: More complex filters, such as lambda functions. For example:: IBMRuntimeService.backends( filters=lambda b: b.configuration().quantum_volume > 16) - min_num_qubits: Minimum number of qubits the backend has to have. - input_allowed: Filter by the types of input the backend supports. - Valid input types are ``job`` (circuit job) and ``runtime`` (Qiskit Runtime). - For example, ``inputs_allowed='runtime'`` will return all backends - that support Qiskit Runtime. If a list is given, the backend must - support all types specified in the list. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. kwargs: Simple filters that specify a ``True``/``False`` criteria in the backend configuration, backends status, or provider credentials. An example to get the operational backends with 5 qubits:: @@ -468,86 +492,52 @@ def backends( The list of available backends that match the filter. Raises: - IBMBackendValueError: If only one or two parameters from `hub`, `group`, - `project` are specified. + IBMInputValueError: If an input is invalid. """ - backends: List["ibm_backend.IBMBackend"] = list() - if all([hub, group, project]): - hgp = self._get_hgp(hub, group, project) - backends = list(hgp.backends.values()) + # TODO filter out input_allowed not having runtime + if self._auth == "legacy": + if instance: + backends = list(self._get_hgp(instance=instance).backends.values()) + else: + backends = list(self._backends.values()) else: + if instance: + raise IBMInputValueError( + "The 'instance' keyword is only supported for legacy runtime." + ) backends = list(self._backends.values()) - # Special handling of the `name` parameter, to support alias resolution. + if name: - aliases = self._aliased_backend_names() - aliases.update(self._deprecated_backend_names()) - name = aliases.get(name, name) kwargs["backend_name"] = name if min_num_qubits: backends = list( filter(lambda b: b.configuration().n_qubits >= min_num_qubits, backends) ) - if input_allowed: - if not isinstance(input_allowed, list): - input_allowed = [input_allowed] - backends = list( - filter( - lambda b: set(input_allowed) - <= set(b.configuration().input_allowed), - backends, - ) - ) return filter_backends(backends, filters=filters, **kwargs) - def my_reservations(self) -> List[BackendReservation]: - """Return your upcoming reservations. - - Returns: - A list of your upcoming reservations. - """ - raw_response = self._default_hgp._api_client.my_reservations() - return convert_reservation_data(raw_response) - - @staticmethod - def _deprecated_backend_names() -> Dict[str, str]: - """Returns deprecated backend names.""" - return { - "ibmqx_qasm_simulator": "ibmq_qasm_simulator", - "ibmqx_hpc_qasm_simulator": "ibmq_qasm_simulator", - "real": "ibmqx1", - } - - @staticmethod - def _aliased_backend_names() -> Dict[str, str]: - """Returns aliased backend names.""" - return { - "ibmq_5_yorktown": "ibmqx2", - "ibmq_5_tenerife": "ibmqx4", - "ibmq_16_rueschlikon": "ibmqx5", - "ibmq_20_austin": "QS1_1", - } - - def active_account(self) -> dict: + def active_account(self) -> Optional[Dict[str, str]]: """Return the IBM Quantum account currently in use for the session. Returns: A dictionary with information about the account currently in the session. """ - return self.account.to_saved_format() + return self._account.to_saved_format() @staticmethod - def delete_account(name: Optional[str]) -> bool: + def delete_account(name: Optional[str] = None, auth: Optional[str] = None) -> bool: """Delete a saved account from disk. Args: - name: Custom name of the saved account. Defaults to "default". + name: Name of the saved account to delete. + auth: Authentication type of the default account to delete. + Ignored if account name is provided. Returns: - True if the account with the given name was deleted. - False if no account was found for the given name. + True if the account was deleted. + False if no account was found. """ - return AccountManager.delete(name=name) + return AccountManager.delete(name=name, auth=auth) @staticmethod def save_account( @@ -569,186 +559,75 @@ def save_account( instance: The CRN (cloud) or hub/group/project (legacy). auth: Authentication type. `cloud` or `legacy`. name: Name of the account to save. - proxies: Proxy configuration for the server. + proxies: Proxy configuration. Supported optional keys are + ``urls`` (a dictionary mapping protocol or protocol and host to the URL of the proxy, + documented at https://docs.python-requests.org/en/latest/api/#requests.Session.proxies), + ``username_ntlm``, ``password_ntlm`` (username and password to enable NTLM user + authentication) verify: Verify the server's TLS certificate. """ - return AccountManager.save( + AccountManager.save( token=token, url=url, instance=instance, auth=auth, name=name, - proxies=proxies, + proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, ) @staticmethod - def saved_accounts() -> dict: + def saved_accounts( + default: Optional[bool] = None, + auth: Optional[str] = None, + name: Optional[str] = None, + ) -> dict: """List the accounts saved on disk. + Args: + default: If set to True, only default accounts are returned. + auth: If set, only accounts with the given authentication type are returned. + name: If set, only accounts with the given name are returned. + Returns: A dictionary with information about the accounts saved on disk. Raises: - IBMProviderCredentialsInvalidUrl: If invalid IBM Quantum - credentials are found on disk. + ValueError: If an invalid account is found on disk. """ - return AccountManager.list() - def get_backend( + return dict( + map( + lambda kv: (kv[0], Account.to_saved_format(kv[1])), + AccountManager.list(default=default, auth=auth, name=name).items(), + ), + ) + + def backend( self, name: str = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **kwargs: Any, + instance: Optional[str] = None, ) -> Backend: """Return a single backend matching the specified filtering. Args: - name (str): name of the backend. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. - **kwargs: dict used for filtering. + name: Name of the backend. + instance: This is only supported for legacy runtime and is in the + hub/group/project format. Returns: - Backend: a backend matching the filtering. + Backend: A backend matching the filtering. Raises: - QiskitBackendNotFoundError: if no backend could be found or - more than one backend matches the filtering criteria. - IBMProviderValueError: If only one or two parameters from `hub`, `group`, - `project` are specified. + QiskitBackendNotFoundError: if no backend could be found. """ # pylint: disable=arguments-differ - backends = self.backends(name, hub=hub, group=group, project=project, **kwargs) - if len(backends) > 1: - raise QiskitBackendNotFoundError( - "More than one backend matches the criteria" - ) + backends = self.backends(name, instance=instance) if not backends: raise QiskitBackendNotFoundError("No backend matches the criteria") return backends[0] - def run_circuits( - self, - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend_name: str, - shots: Optional[int] = None, - initial_layout: Optional[Union[Layout, Dict, List]] = None, - layout_method: Optional[str] = None, - routing_method: Optional[str] = None, - translation_method: Optional[str] = None, - seed_transpiler: Optional[int] = None, - optimization_level: int = 1, - init_qubits: bool = True, - rep_delay: Optional[float] = None, - transpiler_options: Optional[dict] = None, - measurement_error_mitigation: bool = False, - use_measure_esp: Optional[bool] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **run_config: Dict, - ) -> "runtime_job.RuntimeJob": - """Execute the input circuit(s) on a backend using the runtime service. - - Note: - This method uses the IBM Quantum runtime service which is not - available to all accounts. - - Args: - circuits: Circuit(s) to execute. - - backend_name: Name of the backend to execute circuits on. - Transpiler options are automatically grabbed from backend configuration - and properties unless otherwise specified. - - shots: Number of repetitions of each circuit, for sampling. If not specified, - the backend default is used. - - initial_layout: Initial position of virtual qubits on physical qubits. - - layout_method: Name of layout selection pass ('trivial', 'dense', - 'noise_adaptive', 'sabre'). - Sometimes a perfect layout can be available in which case the layout_method - may not run. - - routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre') - - translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') - - seed_transpiler: Sets random seed for the stochastic parts of the transpiler. - - optimization_level: How much optimization to perform on the circuits. - Higher levels generate more optimized circuits, at the expense of longer - transpilation time. - If None, level 1 will be chosen as default. - - init_qubits: Whether to reset the qubits to the ground state for each shot. - - rep_delay: Delay between programs in seconds. Only supported on certain - backends (``backend.configuration().dynamic_reprate_enabled`` ). If supported, - ``rep_delay`` will be used instead of ``rep_time`` and must be from the - range supplied by the backend (``backend.configuration().rep_delay_range``). - Default is given by ``backend.configuration().default_rep_delay``. - - transpiler_options: Additional transpiler options. - - measurement_error_mitigation: Whether to apply measurement error mitigation. - - use_measure_esp: Whether to use excited state promoted (ESP) readout for measurements - which are the final instruction on a qubit. ESP readout can offer higher fidelity - than standard measurement sequences. See - `here `_. - - hub: Name of the hub. - - group: Name of the group. - - project: Name of the project. - - **run_config: Extra arguments used to configure the circuit execution. - - Returns: - Runtime job. - """ - inputs = copy.deepcopy(run_config) # type: Dict[str, Any] - inputs["circuits"] = circuits - inputs["optimization_level"] = optimization_level - inputs["init_qubits"] = init_qubits - inputs["measurement_error_mitigation"] = measurement_error_mitigation - if shots: - inputs["shots"] = shots - if initial_layout: - inputs["initial_layout"] = initial_layout - if layout_method: - inputs["layout_method"] = layout_method - if routing_method: - inputs["routing_method"] = routing_method - if translation_method: - inputs["translation_method"] = translation_method - if seed_transpiler: - inputs["seed_transpiler"] = seed_transpiler - if rep_delay: - inputs["rep_delay"] = rep_delay - if transpiler_options: - inputs["transpiler_options"] = transpiler_options - if use_measure_esp is not None: - inputs["use_measure_esp"] = use_measure_esp - options = {"backend_name": backend_name} - return self.run( - "circuit-runner", - options=options, - inputs=inputs, - result_decoder=RunnerResult, - hub=hub, - group=group, - project=project, - ) - def pprint_programs( self, refresh: bool = False, @@ -834,7 +713,7 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: Raises: RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ if program_id not in self._programs or refresh: try: @@ -844,7 +723,7 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to get program: {ex}") from None + raise IBMRuntimeError(f"Failed to get program: {ex}") from None self._programs[program_id] = self._to_program(response) @@ -888,23 +767,20 @@ def _to_program(self, response: Dict) -> RuntimeProgram: def run( self, program_id: str, - options: Dict, inputs: Union[Dict, ParameterNamespace], + options: Optional[Union[RuntimeOptions, Dict]] = None, callback: Optional[Callable] = None, result_decoder: Optional[Type[ResultDecoder]] = None, - image: Optional[str] = "", - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + instance: Optional[str] = None, ) -> RuntimeJob: """Execute the runtime program. Args: program_id: Program ID. - options: Runtime options that control the execution environment. - Currently the only available option is ``backend_name``, which is required. inputs: Program input parameters. These input values are passed to the runtime program. + options: Runtime options that control the execution environment. See + :class:`RuntimeOptions` for all available options. callback: Callback function to be invoked for any interim results. The callback function will receive 2 positional parameters: @@ -913,67 +789,69 @@ def run( result_decoder: A :class:`ResultDecoder` subclass used to decode job results. ``ResultDecoder`` is used if not specified. - image: The runtime image used to execute the program, specified in the form - of image_name:tag. Not all accounts are authorized to select a different image. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + instance: This is only supported for legacy runtime and is in the + hub/group/project format. Returns: A ``RuntimeJob`` instance representing the execution. Raises: IBMInputValueError: If input is invalid. + RuntimeProgramNotFound: If the program cannot be found. + IBMRuntimeError: An error occurred running the program. """ - if "backend_name" not in options: - raise IBMInputValueError('"backend_name" is required field in "options"') + if instance and self._auth != "legacy": + raise IBMInputValueError( + "The 'instance' keyword is only supported for legacy runtime. " + ) + # If using params object, extract as dictionary if isinstance(inputs, ParameterNamespace): inputs.validate() inputs = vars(inputs) - if image and not re.match( - "[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$", - image, - ): - raise IBMInputValueError('"image" needs to be in form of image_name:tag') - backend_name = options["backend_name"] - if not all([hub, group, project]) and self._default_hgp.get_backend( - backend_name - ): - hgp = self._default_hgp - else: - hgp = self._get_hgp( - hub=hub, - group=group, - project=project, - backend_name=backend_name, - service_name=SERVICE_NAME, - ) - credentials = hgp.credentials - api_client = ( - self._api_client if hgp == self._default_hgp else RuntimeClient(credentials) - ) + if isinstance(options, dict): + options = RuntimeOptions(**options) + options.validate(self.auth) + + backend = None + hgp_name = None + if self._auth == "legacy": + # Find the right hgp + hgp = self._get_hgp(instance=instance, backend_name=options.backend_name) + backend = hgp.backend(options.backend_name) + hgp_name = hgp.name + result_decoder = result_decoder or ResultDecoder - response = api_client.program_run( - program_id=program_id, - credentials=credentials, - backend_name=backend_name, - params=inputs, - image=image, - ) + try: + response = self._api_client.program_run( + program_id=program_id, + backend_name=options.backend_name, + params=inputs, + image=options.image, + hgp=hgp_name, + log_level=options.log_level, + ) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None + raise IBMRuntimeError(f"Failed to run program: {ex}") from None + + if not backend: + backend = self.backend(name=response["backend"]) - backend = self.get_backend(backend_name) job = RuntimeJob( backend=backend, - api_client=api_client, - credentials=credentials, + api_client=self._api_client, + client_params=self._client_params, job_id=response["id"], program_id=program_id, params=inputs, user_callback=callback, result_decoder=result_decoder, - image=image, + image=options.image, ) return job @@ -1025,7 +903,7 @@ def upload_program( IBMInputValueError: If required metadata is missing. RuntimeDuplicateProgramError: If a program with the same name already exists. IBMNotAuthorizedError: If you are not authorized to upload programs. - QiskitRuntimeError: If the upload failed. + IBMRuntimeError: If the upload failed. """ program_metadata = self._read_metadata(metadata=metadata) @@ -1052,7 +930,7 @@ def upload_program( raise IBMNotAuthorizedError( "You are not authorized to upload programs." ) from None - raise QiskitRuntimeError(f"Failed to create program: {ex}") from None + raise IBMRuntimeError(f"Failed to create program: {ex}") from None return response["id"] def _read_metadata(self, metadata: Optional[Union[Dict, str]] = None) -> Dict: @@ -1110,7 +988,7 @@ def update_program( Raises: RuntimeProgramNotFound: If the program doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ if not any([data, metadata, name, description, max_execution_time, spec]): warnings.warn( @@ -1146,7 +1024,7 @@ def update_program( raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to update program: {ex}") from None + raise IBMRuntimeError(f"Failed to update program: {ex}") from None if program_id in self._programs: program = self._programs[program_id] @@ -1178,7 +1056,7 @@ def delete_program(self, program_id: str) -> None: Raises: RuntimeProgramNotFound: If the program doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: self._api_client.program_delete(program_id=program_id) @@ -1187,7 +1065,7 @@ def delete_program(self, program_id: str) -> None: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to delete program: {ex}") from None + raise IBMRuntimeError(f"Failed to delete program: {ex}") from None if program_id in self._programs: del self._programs[program_id] @@ -1201,17 +1079,17 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: If ``False``, make the program visible to just your account. Raises: - RuntimeJobNotFound: if program not found (404) - QiskitRuntimeError: if update failed (401, 403) + RuntimeProgramNotFound: if program not found (404) + IBMRuntimeError: if update failed (401, 403) """ try: self._api_client.set_program_visibility(program_id, public) except RequestsApiError as ex: if ex.status_code == 404: - raise RuntimeJobNotFound(f"Program not found: {ex.message}") from None - raise QiskitRuntimeError( - f"Failed to set program visibility: {ex}" - ) from None + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None + raise IBMRuntimeError(f"Failed to set program visibility: {ex}") from None if program_id in self._programs: program = self._programs[program_id] @@ -1228,14 +1106,14 @@ def job(self, job_id: str) -> RuntimeJob: Raises: RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: response = self._api_client.job_get(job_id) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None + raise IBMRuntimeError(f"Failed to delete job: {ex}") from None return self._decode_job(response) def jobs( @@ -1244,9 +1122,7 @@ def jobs( skip: int = 0, pending: bool = None, program_id: str = None, - hub: str = None, - group: str = None, - project: str = None, + instance: Optional[str] = None, ) -> List[RuntimeJob]: """Retrieve all runtime jobs, subject to optional filtering. @@ -1257,23 +1133,23 @@ def jobs( jobs are included. If ``False``, 'DONE', 'CANCELLED' and 'ERROR' jobs are included. program_id: Filter by Program ID. - hub: Filter by hub - hub, group, and project must all be specified. - group: Filter by group - hub, group, and project must all be specified. - project: Filter by project - hub, group, and project must all be specified. + instance: This is only supported for legacy runtime and is in the + hub/group/project format. Returns: A list of runtime jobs. Raises: - IBMInputValueError: If any but not all of the parameters ``hub``, ``group`` - and ``project`` are given. + IBMInputValueError: If an input value is invalid. """ - if any([hub, group, project]) and not all([hub, group, project]): - raise IBMInputValueError( - "Hub, group and project " - "parameters must all be specified. " - 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) - ) + hub = group = project = None + if instance: + if self._auth == "cloud": + raise IBMInputValueError( + "The 'instance' keyword is only supported for legacy runtime." + ) + hub, group, project = from_instance_format(instance) + job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 offset = skip @@ -1321,14 +1197,14 @@ def delete_job(self, job_id: str) -> None: Raises: RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: self._api_client.job_delete(job_id) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None + raise IBMRuntimeError(f"Failed to delete job: {ex}") from None def _decode_job(self, raw_data: Dict) -> RuntimeJob: """Decode job data received from the server. @@ -1339,26 +1215,20 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: Returns: Decoded job data. """ - hub = raw_data["hub"] - group = raw_data["group"] - project = raw_data["project"] + hub = raw_data.get("hub") + group = raw_data.get("group") + project = raw_data.get("project") + instance = ( + to_instance_format(hub, group, project) + if all([hub, group, project]) + else None + ) # Try to find the right backend try: - backend = self.get_backend( - raw_data["backend"], hub=hub, group=group, project=project - ) + backend = self.backend(raw_data["backend"], instance=instance) except (IBMProviderError, QiskitBackendNotFoundError): backend = ibm_backend.IBMRetiredBackend.from_name( backend_name=raw_data["backend"], - service=self, - credentials=Credentials( - auth="legacy", - token="", - url="", - hub=hub, - group=group, - project=project, - ), api=None, ) @@ -1375,26 +1245,65 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: return RuntimeJob( backend=backend, api_client=self._api_client, - credentials=self._default_hgp.credentials, + client_params=self._client_params, job_id=raw_data["id"], program_id=raw_data.get("program", {}).get("id", ""), params=decoded, creation_date=raw_data.get("created", None), ) - def logout(self) -> None: - """Clears authorization cache on the server. + def least_busy( + self, + min_num_qubits: Optional[int] = None, + instance: Optional[str] = None, + filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, + **kwargs: Any, + ) -> ibm_backend.IBMBackend: + """Return the least busy available backend. - For better performance, the runtime server caches each user's - authorization information. This method is used to force the server - to clear its cache. + Args: + min_num_qubits: Minimum number of qubits the backend has to have. + instance: This is only supported for legacy runtime and is in the + hub/group/project format. + filters: More complex filters, such as lambda functions. + For example:: + + AccountProvider.backends( + filters=lambda b: b.configuration().quantum_volume > 16) - Note: - Invoke this method ONLY when your access level to the runtime - service has changed - for example, the first time your account is - given the authority to upload a program. + kwargs: Simple filters that specify a ``True``/``False`` criteria in the + backend configuration, backends status, or provider credentials. + An example to get the operational backends with 5 qubits:: + + IBMRuntimeService.least_busy(n_qubits=5, operational=True) + + Returns: + The backend with the fewest number of pending jobs. + + Raises: + QiskitBackendNotFoundError: If no backend matches the criteria. + """ + backends = self.backends( + min_num_qubits=min_num_qubits, instance=instance, filters=filters, **kwargs + ) + candidates = [] + for back in backends: + backend_status = back.status() + if not backend_status.operational or backend_status.status_msg != "active": + continue + candidates.append(back) + if not candidates: + raise QiskitBackendNotFoundError("No backend matches the criteria.") + return min(candidates, key=lambda b: b.status().pending_jobs) + + @property + def auth(self) -> str: + """Return the authentication type used. + + Returns: + The authentication type used. """ - self._api_client.logout() + return self._auth def __repr__(self) -> str: return "<{}>".format(self.__class__.__name__) diff --git a/qiskit_ibm_runtime/jupyter/__init__.py b/qiskit_ibm_runtime/jupyter/__init__.py index 491f856983..3c74fbc0b8 100644 --- a/qiskit_ibm_runtime/jupyter/__init__.py +++ b/qiskit_ibm_runtime/jupyter/__init__.py @@ -41,8 +41,8 @@ from qiskit_ibm_runtime import IBMRuntimeService import qiskit_ibm_runtime.jupyter - service = IBMRuntimeService(hub='ibm-q') - backend = service.get_backend('ibmq_vigo') + service = IBMRuntimeService() + backend = service.backend('ibmq_vigo') .. jupyter-execute:: :hide-code: @@ -54,17 +54,6 @@ backend - -IBM Quantum dashboard -====================================== - -.. code-block:: python - - from qiskit_ibm_runtime import IBMRuntimeService - import qiskit_ibm_runtime.jupyter - - %ibm_quantum_dashboard - """ import sys diff --git a/qiskit_ibm_runtime/jupyter/qubits_widget.py b/qiskit_ibm_runtime/jupyter/qubits_widget.py index 2eddd2f69c..558c23d0bf 100644 --- a/qiskit_ibm_runtime/jupyter/qubits_widget.py +++ b/qiskit_ibm_runtime/jupyter/qubits_widget.py @@ -10,6 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. # pylint: disable=invalid-name +# pylint: disable=consider-iterating-dictionary """Widget for qubit properties tab.""" diff --git a/qiskit_ibm_runtime/proxies/__init__.py b/qiskit_ibm_runtime/proxies/__init__.py new file mode 100644 index 0000000000..c0212bfb0d --- /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 0000000000..2e0ea25eb2 --- /dev/null +++ b/qiskit_ibm_runtime/proxies/configuration.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. + +"""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/runner_result.py b/qiskit_ibm_runtime/runner_result.py deleted file mode 100644 index bf609ba6de..0000000000 --- a/qiskit_ibm_runtime/runner_result.py +++ /dev/null @@ -1,74 +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. - -"""Circuit-runner result class""" - -from typing import List, Union -import json - -from qiskit.result import Result, QuasiDistribution -from qiskit.result.postprocess import _hex_to_bin -from qiskit.exceptions import QiskitError - -from .program.result_decoder import ResultDecoder - - -class RunnerResult(Result, ResultDecoder): - """Result class for Qiskit Runtime program circuit-runner.""" - - @classmethod - def decode(cls, data: str) -> "RunnerResult": - """Decoding for results from Qiskit runtime jobs.""" - return cls.from_dict(json.loads(data)) - - def get_quasiprobabilities( - self, experiment: Union[int, List] = None - ) -> Union[QuasiDistribution, List[QuasiDistribution]]: - """Get quasiprobabilites associated with one or more experiments. - - Parameters: - experiment: Indices of experiments to grab quasiprobabilities from. - - Returns: - A single distribution or a list of distributions. - - Raises: - QiskitError: If experiment result doesn't contain quasiprobabilities. - """ - if experiment is None: - exp_keys = range(len(self.results)) - else: - exp_keys = [experiment] # type: ignore[assignment] - - dict_list = [] - for key in exp_keys: - if "quasiprobabilities" in self.data(key).keys(): - shots = self.results[key].shots - hex_quasi = self.results[key].data.quasiprobabilities - bit_lenth = len(self.results[key].header.final_measurement_mapping) - quasi = {} - for hkey, val in hex_quasi.items(): - quasi[_hex_to_bin(hkey).zfill(bit_lenth)] = val - - out = QuasiDistribution(quasi, shots) - out.shots = shots - dict_list.append(out) - else: - raise QiskitError( - 'No quasiprobabilities for experiment "{}"'.format(repr(key)) - ) - - # Return first item of dict_list if size is 1 - if len(dict_list) == 1: - return dict_list[0] - else: - return dict_list diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index 75d439ead9..3ffaf5626e 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -28,14 +28,14 @@ from .exceptions import ( RuntimeJobFailureError, RuntimeInvalidStateError, - QiskitRuntimeError, + IBMRuntimeError, ) from .program.result_decoder import ResultDecoder from .api.clients import RuntimeClient, RuntimeWebsocketClient, WebsocketClientCloseCode from .exceptions import IBMError from .api.exceptions import RequestsApiError from .utils.converters import utc_to_local -from .credentials import Credentials +from .api.client_parameters import ClientParameters logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def __init__( self, backend: Backend, api_client: RuntimeClient, - credentials: Credentials, + client_params: ClientParameters, job_id: str, program_id: str, params: Optional[Dict] = None, @@ -96,7 +96,7 @@ def __init__( Args: backend: The backend instance used to run this job. api_client: Object for connecting to the server. - credentials: Account credentials. + client_params: Parameters used for server connection. job_id: Job ID. program_id: ID of the program this job is for. params: Job parameters. @@ -123,8 +123,8 @@ def __init__( self._ws_client_future = None # type: Optional[futures.Future] self._result_queue = queue.Queue() # type: queue.Queue self._ws_client = RuntimeWebsocketClient( - websocket_url=credentials.runtime_url.replace("https", "wss"), - credentials=credentials, + websocket_url=client_params.url.replace("https", "wss"), + client_params=client_params, job_id=job_id, message_queue=self._result_queue, ) @@ -189,7 +189,7 @@ def cancel(self) -> None: Raises: RuntimeInvalidStateError: If the job is in a state that cannot be cancelled. - QiskitRuntimeError: If unable to cancel job. + IBMRuntimeError: If unable to cancel job. """ try: self._api_client.job_cancel(self.job_id) @@ -198,7 +198,7 @@ def cancel(self) -> None: raise RuntimeInvalidStateError( f"Job cannot be cancelled: {ex}" ) from None - raise QiskitRuntimeError(f"Failed to cancel job: {ex}") from None + raise IBMRuntimeError(f"Failed to cancel job: {ex}") from None self.cancel_result_streaming() self._status = JobStatus.CANCELLED @@ -294,7 +294,7 @@ def logs(self) -> str: Job logs, including standard output and error. Raises: - QiskitRuntimeError: If a network error occurred. + IBMRuntimeError: If a network error occurred. """ if self.status() not in JOB_FINAL_STATES: logger.warning("Job logs are only available after the job finishes.") @@ -303,7 +303,7 @@ def logs(self) -> str: except RequestsApiError as err: if err.status_code == 404: return "" - raise QiskitRuntimeError(f"Failed to get job logs: {err}") from None + raise IBMRuntimeError(f"Failed to get job logs: {err}") from None def _set_status_and_error_message(self) -> None: """Fetch and set status and error message.""" @@ -410,6 +410,9 @@ def _empty_result_queue(self, result_queue: queue.Queue) -> None: except queue.Empty: pass + def __repr__(self) -> str: + return f"<{self.__class__.__name__}('{self._job_id}', '{self._program_id}')>" + @property def job_id(self) -> str: """Return a unique ID identifying the job. diff --git a/qiskit_ibm_runtime/runtime_options.py b/qiskit_ibm_runtime/runtime_options.py new file mode 100644 index 0000000000..7c94dceadd --- /dev/null +++ b/qiskit_ibm_runtime/runtime_options.py @@ -0,0 +1,67 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Runtime options that control the execution environment.""" + +import re +import logging +from dataclasses import dataclass +from typing import Optional + +from .exceptions import IBMInputValueError + + +@dataclass +class RuntimeOptions: + """Class for representing runtime execution options. + + Args: + backend_name: target backend to run on. This is required for legacy runtime. + image: the runtime image used to execute the program, specified in + the form of ``image_name:tag``. Not all accounts are + authorized to select a different image. + log_level: logging level to set in the execution environment. The valid + log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``. + The default level is ``WARNING``. + """ + + backend_name: Optional[str] = None + image: Optional[str] = None + log_level: Optional[str] = None + + def validate(self, auth: str) -> None: + """Validate options. + + Args: + auth: authentication type. + + Raises: + IBMInputValueError: If one or more option is invalid. + """ + if self.image and not re.match( + "[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$", + self.image, + ): + raise IBMInputValueError('"image" needs to be in form of image_name:tag') + + if auth == "legacy" and not self.backend_name: + raise IBMInputValueError( + '"backend_name" is required field in "options" for legacy runtime.' + ) + + if self.log_level and not isinstance( + logging.getLevelName(self.log_level.upper()), int + ): + raise IBMInputValueError( + f"{self.log_level} is not a valid log level. The valid log levels are: `DEBUG`, " + f"`INFO`, `WARNING`, `ERROR`, and `CRITICAL`." + ) diff --git a/qiskit_ibm_runtime/runtime_program.py b/qiskit_ibm_runtime/runtime_program.py index 30ed8bdc3f..6123641e5c 100644 --- a/qiskit_ibm_runtime/runtime_program.py +++ b/qiskit_ibm_runtime/runtime_program.py @@ -17,7 +17,7 @@ from typing import Optional, Dict from types import SimpleNamespace from qiskit_ibm_runtime.exceptions import IBMInputValueError, IBMNotAuthorizedError -from .exceptions import QiskitRuntimeError, RuntimeProgramNotFound +from .exceptions import IBMRuntimeError, RuntimeProgramNotFound from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError @@ -107,7 +107,8 @@ def _format_common(schema: Dict) -> None: formatted.append(" " * 8 + "- " + property_name + ":") for key, value in property_value.items(): formatted.append( - " " * 12 + "{}: {}".format(sentence_case(key), str(value)) + " " * 12 + + "{}: {}".format(camel_to_sentence_case(key), str(value)) ) formatted.append( " " * 12 @@ -115,7 +116,30 @@ def _format_common(schema: Dict) -> None: + str(property_name in schema.get("required", [])) ) - def sentence_case(camel_case_text: str) -> str: + def _format_backend_requirements(schema: Dict) -> None: + """Add backend requirements details to `formatted`.""" + if "min_num_qubits" in schema: + formatted.append( + " " * 4 + + "Minimum number of qubits: {}".format( + str(schema["min_num_qubits"]) + ) + ) + for key, value in schema.items(): + if key not in ["min_num_qubits"]: + formatted.append( + " " * 4 + + "{}: {}".format(snake_to_sentence_case(key), str(value)) + ) + + def snake_to_sentence_case(snake_case_text: str) -> str: + """Converts snake_case to Sentence case""" + snake_case_words = snake_case_text.split("_") + return camel_to_sentence_case( + snake_case_words[0] + "".join(x.title() for x in snake_case_words[1:]) + ) + + def camel_to_sentence_case(camel_case_text: str) -> str: """Converts camelCase to Sentence case""" if camel_case_text == "": return camel_case_text @@ -131,6 +155,12 @@ def sentence_case(camel_case_text: str) -> str: f" Max execution time: {self.max_execution_time}", ] + formatted.append(" Backend requirements:") + if self._backend_requirements: + _format_backend_requirements(self._backend_requirements) + else: + formatted.append(" " * 4 + "none") + formatted.append(" Input parameters:") if self._parameters: _format_common(self._parameters) @@ -298,7 +328,7 @@ def _refresh(self) -> None: Raises: RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: response = self._api_client.program_get(self._id) @@ -307,7 +337,7 @@ def _refresh(self) -> None: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to get program: {ex}") from None + raise IBMRuntimeError(f"Failed to get program: {ex}") from None self._backend_requirements = {} self._parameters = {} self._return_values = {} @@ -328,6 +358,9 @@ def _refresh(self) -> None: self._is_public = response.get("is_public", False) self._data = response.get("data", "") + def __repr__(self) -> str: + return f"<{self.__class__.__name__}('{self._id}')>" + class ParameterNamespace(SimpleNamespace): """A namespace for program parameters with validation. diff --git a/qiskit_ibm_runtime/test/ibm_provider_mock.py b/qiskit_ibm_runtime/test/ibm_provider_mock.py index b6bd5cfcd1..14882a6cdc 100644 --- a/qiskit_ibm_runtime/test/ibm_provider_mock.py +++ b/qiskit_ibm_runtime/test/ibm_provider_mock.py @@ -22,7 +22,7 @@ def mock_get_backend(backend): Note this will set the value of qiskit_ibm_runtime.IBMRuntimeService to a MagicMock object. It is intended to be run as part of docstrings with jupyter-example in a hidden cell so that later examples which rely on ibm quantum devices so that the docs can - be built without requiring configured credentials. If used outside of this + be built without requiring configured accounts. If used outside of this context be aware that you will have to manually restore qiskit_ibm_runtime.IBMRuntimeService the value to qiskit_ibm_runtime.IBMRuntimeService after you finish using your mock. Args: @@ -37,6 +37,6 @@ def mock_get_backend(backend): "The specified backend name is not a valid mock from qiskit.test.mock" ) fake_backend = getattr(backend_mocks, backend)() - mock_ibm_provider.get_backend.return_value = fake_backend + mock_ibm_provider.backend.return_value = fake_backend mock_ibm_provider.return_value = mock_ibm_provider qiskit_ibm_runtime.IBMRuntimeService = mock_ibm_provider diff --git a/qiskit_ibm_runtime/utils/backend.py b/qiskit_ibm_runtime/utils/backend_decoder.py similarity index 61% rename from qiskit_ibm_runtime/utils/backend.py rename to qiskit_ibm_runtime/utils/backend_decoder.py index a16344fbef..800b440f43 100644 --- a/qiskit_ibm_runtime/utils/backend.py +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -12,52 +12,70 @@ """Utilities for working with IBM Quantum backends.""" -from typing import List, Optional, Dict, Union +from typing import List, Dict, Union, Optional +import logging +import traceback import dateutil.parser +from qiskit.providers.models import ( + BackendProperties, + PulseDefaults, +) +from qiskit.providers.models import ( + PulseBackendConfiguration, + QasmBackendConfiguration, +) -from ..backendreservation import BackendReservation -from ..utils.converters import utc_to_local +from .converters import utc_to_local_all +logger = logging.getLogger(__name__) -def convert_reservation_data( - raw_reservations: List, backend_name: Optional[str] = None -) -> List[BackendReservation]: - """Convert a list of raw reservation data to ``BackendReservation`` objects. + +def configuration_from_server_data( + raw_config: Dict, + instance: str = "", +) -> Optional[Union[QasmBackendConfiguration, PulseBackendConfiguration]]: + """Create an IBMBackend instance from raw server data. Args: - raw_reservations: Raw reservation data. - backend_name: Name of the backend. + raw_config: Raw configuration. + instance: Service instance. Returns: - A list of ``BackendReservation`` objects. + Backend configuration. """ - reservations = [] - for raw_res in raw_reservations: - creation_datetime = raw_res.get("creationDate", None) - creation_datetime = ( - utc_to_local(creation_datetime) if creation_datetime else None + # 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." ) - backend_name = backend_name or raw_res.get("backendName", None) - reservations.append( - BackendReservation( - backend_name=backend_name, - start_datetime=utc_to_local(raw_res["initialDate"]), - end_datetime=utc_to_local(raw_res["endDate"]), - mode=raw_res.get("mode", None), - reservation_id=raw_res.get("id", None), - creation_datetime=creation_datetime, - hub_info=raw_res.get("hubInfo", None), - ) + 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 config: %s", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(instance), + traceback.format_exc(), ) - return reservations + return None -def decode_pulse_defaults(defaults: Dict) -> None: +def defaults_from_server_data(defaults: Dict) -> PulseDefaults: """Decode pulse defaults data. Args: - defaults: A ``PulseDefaults`` in dictionary format. + defaults: Raw pulse defaults data. + + Returns: + A ``PulseDefaults`` instance. """ for item in defaults["pulse_library"]: _decode_pulse_library_item(item) @@ -67,12 +85,17 @@ def decode_pulse_defaults(defaults: Dict) -> None: for instr in cmd["sequence"]: _decode_pulse_qobj_instr(instr) + return PulseDefaults.from_dict(defaults) + -def decode_backend_properties(properties: Dict) -> None: +def properties_from_server_data(properties: Dict) -> BackendProperties: """Decode backend properties. Args: - properties: A ``BackendProperties`` in dictionary format. + properties: Raw properties data. + + Returns: + A ``BackendProperties`` instance. """ properties["last_update_date"] = dateutil.parser.isoparse( properties["last_update_date"] @@ -86,8 +109,11 @@ def decode_backend_properties(properties: Dict) -> None: 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: +def _decode_backend_configuration(config: Dict) -> None: """Decode backend configuration. Args: diff --git a/qiskit_ibm_runtime/utils/hgp.py b/qiskit_ibm_runtime/utils/hgp.py new file mode 100644 index 0000000000..46d11928e4 --- /dev/null +++ b/qiskit_ibm_runtime/utils/hgp.py @@ -0,0 +1,43 @@ +# 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/utils.py b/qiskit_ibm_runtime/utils/utils.py index 54a5564c81..afdf86e01d 100644 --- a/qiskit_ibm_runtime/utils/utils.py +++ b/qiskit_ibm_runtime/utils/utils.py @@ -28,6 +28,9 @@ def is_crn(locator: str) -> bool: Args: locator: The value to check. + + Returns: + Whether the input is a CRN. """ return isinstance(locator, str) and locator.startswith("crn:") @@ -38,6 +41,9 @@ def crn_to_api_host(crn: str) -> str: Args: crn: The CRN. + Returns: + API host. + Raises: CannotMapCrnToApiHostError: If the corresponding API host cannot be determined. """ diff --git a/qiskit_ibm_runtime/visualization/interactive/error_map.py b/qiskit_ibm_runtime/visualization/interactive/error_map.py index 00a80d39f8..5652dffbbe 100644 --- a/qiskit_ibm_runtime/visualization/interactive/error_map.py +++ b/qiskit_ibm_runtime/visualization/interactive/error_map.py @@ -10,6 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. # pylint: disable=invalid-name +# pylint: disable=consider-iterating-dictionary """Interactive error map for IBM Quantum devices.""" @@ -67,8 +68,8 @@ def iplot_error_map( from qiskit_ibm_runtime import IBMRuntimeService from qiskit_ibm_runtime.visualization import iplot_error_map - service = IBMRuntimeService(group='open', project='main') - backend = service.get_backend('ibmq_vigo') + service = IBMRuntimeService() + backend = service.backend('ibmq_vigo') iplot_error_map(backend, as_widget=True) """ diff --git a/qiskit_ibm_runtime/visualization/interactive/gate_map.py b/qiskit_ibm_runtime/visualization/interactive/gate_map.py index d89b5d2a43..68c063a0c8 100644 --- a/qiskit_ibm_runtime/visualization/interactive/gate_map.py +++ b/qiskit_ibm_runtime/visualization/interactive/gate_map.py @@ -9,6 +9,7 @@ # 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=consider-iterating-dictionary """Interactive gate map for IBM Quantum devices.""" @@ -71,8 +72,8 @@ def iplot_gate_map( from qiskit_ibm_runtime import IBMRuntimeService from qiskit_ibm_runtime.visualization import iplot_gate_map - service = IBMRuntimeService(group='open', project='main') - backend = service.get_backend('ibmq_vigo') + service = IBMRuntimeService() + backend = service.backend('ibmq_vigo') iplot_gate_map(backend, as_widget=True) """ diff --git a/qiskit_runtime/__init__.py b/qiskit_runtime/__init__.py new file mode 100644 index 0000000000..2ba089a0d2 --- /dev/null +++ b/qiskit_runtime/__init__.py @@ -0,0 +1,19 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""Main entry point for the qiskit_runtime""" + + +try: + from .version import version as __version__ +except ImportError: + __version__ = "0.0.0" diff --git a/qiskit_runtime/circuit_runner/__init__.py b/qiskit_runtime/circuit_runner/__init__.py new file mode 100644 index 0000000000..24326fd26b --- /dev/null +++ b/qiskit_runtime/circuit_runner/__init__.py @@ -0,0 +1,15 @@ +# This code is part of qiskit-runtime. +# +# (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. + +""" +Qiskit circuit runner module +""" diff --git a/qiskit_runtime/circuit_runner/circuit_runner.json b/qiskit_runtime/circuit_runner/circuit_runner.json new file mode 100644 index 0000000000..b32e386920 --- /dev/null +++ b/qiskit_runtime/circuit_runner/circuit_runner.json @@ -0,0 +1,23 @@ +{ + "name": "circuit-runner", + "description": "A runtime program that takes one or more circuits, compiles them, executes them, and optionally applies measurement error mitigation.", + "max_execution_time": 14400, + "version": "1.0", + "parameters": [ + {"name": "circuits", "description": "A circuit or a list of circuits.", "type": "A QuantumCircuit or a list of QuantumCircuits.", "required": true}, + {"name": "shots", "description": "Number of repetitions of each circuit, for sampling. Default: 1024.", "type": "int", "required": false}, + {"name": "initial_layout", "description": "Initial position of virtual qubits on physical qubits.", "type": "dict or list", "required": false}, + {"name": "layout_method", "description": "Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre')", "type": "string", "required": false}, + {"name": "routing_method", "description": "Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre').", "type": "string", "required": false}, + {"name": "translation_method", "description": "Name of translation pass ('unroller', 'translator', 'synthesis').", "type": "string", "required": false}, + {"name": "seed_transpiler", "description": "Sets random seed for the stochastic parts of the transpiler.", "type": "int", "required": false}, + {"name": "optimization_level", "description": "How much optimization to perform on the circuits (0-3). Higher levels generate more optimized circuits. Default is 1.", "type": "int", "required": false}, + {"name": "init_qubits", "description": "Whether to reset the qubits to the ground state for each shot.", "type": "bool", "required": false}, + {"name": "rep_delay", "description": "Delay between programs in seconds.", "type": "float", "required": false}, + {"name": "transpiler_options", "description": "Additional compilation options.", "type": "dict", "required": false}, + {"name": "measurement_error_mitigation", "description": "Whether to apply measurement error mitigation. Default is False.", "type": "bool", "required": false} + ], + "return_values": [ + {"name": "-", "description": "Circuit execution results.", "type": "RunnerResult object"} + ] +} diff --git a/qiskit_runtime/circuit_runner/circuit_runner.py b/qiskit_runtime/circuit_runner/circuit_runner.py new file mode 100644 index 0000000000..82a9aaf7dc --- /dev/null +++ b/qiskit_runtime/circuit_runner/circuit_runner.py @@ -0,0 +1,68 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""Circuit-runner runtime program. + +This is a simplified version of the circuit-runner program. +""" + +from qiskit.compiler import transpile, schedule + + +def main( + backend, + user_messenger, # pylint: disable=unused-argument + circuits, + initial_layout=None, + seed_transpiler=None, + optimization_level=None, + transpiler_options=None, + scheduling_method=None, + schedule_circuit=False, + inst_map=None, + meas_map=None, + measurement_error_mitigation=False, + **kwargs, +): + """Run the circuits on the backend.""" + + # transpiling the circuits using given transpile options + transpiler_options = transpiler_options or {} + circuits = transpile( + circuits, + initial_layout=initial_layout, + seed_transpiler=seed_transpiler, + optimization_level=optimization_level, + backend=backend, + **transpiler_options, + ) + + if schedule_circuit: + circuits = schedule( + circuits=circuits, + backend=backend, + inst_map=inst_map, + meas_map=meas_map, + method=scheduling_method, + ) + + if not isinstance(circuits, list): + circuits = [circuits] + + # Compute raw results + result = backend.run(circuits, **kwargs).result() + + if measurement_error_mitigation: + # Performs measurement error mitigation. + pass + + return result.to_dict() diff --git a/qiskit_runtime/hello_world/__init__.py b/qiskit_runtime/hello_world/__init__.py new file mode 100644 index 0000000000..5ecd538275 --- /dev/null +++ b/qiskit_runtime/hello_world/__init__.py @@ -0,0 +1,13 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""Package containing ``hello-world`` Qiskit quantum program.""" diff --git a/qiskit_runtime/hello_world/hello_world.json b/qiskit_runtime/hello_world/hello_world.json new file mode 100644 index 0000000000..513904206d --- /dev/null +++ b/qiskit_runtime/hello_world/hello_world.json @@ -0,0 +1,41 @@ +{ + "name": "hello-world", + "description": "A sample runtime program.", + "max_execution_time": 300, + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "iterations": { + "type": "integer", + "minimum": 0, + "description": "Number of iterations to run. Each iteration generates a runs a random circuit." + } + }, + "required": [ + "iterations" + ] + }, + "return_values": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "A string that says 'All done!'.", + "type": "string" + }, + "interim_results": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "iteration": { + "type": "integer", + "description": "Iteration number." + }, + "counts": { + "description": "Histogram data of the circuit result.", + "type": "object" + } + } + } + } +} diff --git a/qiskit_runtime/hello_world/hello_world.py b/qiskit_runtime/hello_world/hello_world.py new file mode 100644 index 0000000000..ad265013d5 --- /dev/null +++ b/qiskit_runtime/hello_world/hello_world.py @@ -0,0 +1,55 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""A sample runtime program called hello-world that submits random circuits +for user-specified iterations. +""" + +import random +from typing import Any + +from qiskit import transpile +from qiskit.circuit.random import random_circuit + + +def prepare_circuits(backend): + """Generate a random circuit. + + Args: + backend (qiskit.providers.Backend): Backend used for transpilation. + + Returns: + qiskit.QuantumCircuit: Generated circuit. + """ + circuit = random_circuit(num_qubits=5, depth=4, measure=True, seed=random.randint(0, 1000)) + return transpile(circuit, backend) + + +def main(backend, user_messenger, **kwargs) -> Any: + """Main entry point of the program. + + Args: + backend (qiskit.providers.Backend): Backend to submit the circuits to. + user_messenger (qiskit.providers.ibmq.runtime.UserMessenger): Used to communicate with the + program consumer. + kwargs: User inputs. + + Returns: + Final result of the program. + """ + iterations = kwargs.pop("iterations", 1) + for it in range(iterations): + qc = prepare_circuits(backend) + result = backend.run(qc).result() + user_messenger.publish({"iteration": it, "counts": result.get_counts()}) + + return "All done!" diff --git a/qiskit_runtime/qaoa/qaoa.py b/qiskit_runtime/qaoa/qaoa.py new file mode 100644 index 0000000000..341c3f8a42 --- /dev/null +++ b/qiskit_runtime/qaoa/qaoa.py @@ -0,0 +1,2473 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 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. + +"""A self-contained QAOA runtime with the SWAP strategies.""" + +from typing import Dict, List, Optional, Set, Tuple +from warnings import warn + +import copy +import retworkx as rx +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.algorithms import VQE +from qiskit.algorithms.optimizers import SPSA +from qiskit.circuit import ParameterVector, Parameter, Gate +from qiskit.circuit.library import NLocal, EvolvedOperatorAnsatz +from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.converters import circuit_to_dag +from qiskit.opflow import PauliExpectation, CVaRExpectation, OperatorBase +from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp +from qiskit.utils import QuantumInstance +from qiskit.providers.backend import Backend +from qiskit.dagcircuit import DAGCircuit +from qiskit.ignis.mitigation import TensoredMeasFitter +from qiskit.exceptions import QiskitError +from qiskit.circuit.library.standard_gates.equivalence_library import ( + StandardEquivalenceLibrary as std_eqlib, +) +from qiskit.transpiler import ( + PassManager, + CouplingMap, + AnalysisPass, + TransformationPass, + Layout, +) +from qiskit.transpiler.passes import ( + Collect2qBlocks, + ConsolidateBlocks, + Optimize1qGatesDecomposition, + FullAncillaAllocation, + EnlargeWithAncilla, + ApplyLayout, + BasisTranslator, + UnrollCustomDefinitions, +) +from qiskit.transpiler.passes.calibration.builders import RZXCalibrationBuilderNoEcho +from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( + EchoRZXWeylDecomposition, +) + +from qiskit_optimization import QuadraticProgram + + +class Publisher: + """Class used to publish interim results.""" + + def __init__(self, messenger): + self._messenger = messenger + + def callback(self, *args, **kwargs): + text = list(args) + for k, v in kwargs.items(): + text.append({k: v}) + self._messenger.publish(text) + + +# begin QAOA Gate + + +class QAOAAnsatz(EvolvedOperatorAnsatz): + """A generalized QAOA quantum circuit with a support of custom initial states and mixers. + + References: + + [1]: Farhi et al., A Quantum Approximate Optimization Algorithm. + `arXiv:1411.4028 `_ + """ + + def __init__( + self, + cost_operator=None, + reps: int = 1, + initial_state: Optional[QuantumCircuit] = None, + mixer_operator=None, + name: str = "QAOA", + ): + r""" + Args: + cost_operator (OperatorBase, optional): The operator representing the cost of + the optimization problem, denoted as :math:`U(C, \gamma)` in the original paper. + Must be set either in the constructor or via property setter. + reps (int): The integer parameter p, which determines the depth of the circuit, + as specified in the original paper, default is 1. + initial_state (QuantumCircuit, optional): An optional initial state to use. + If `None` is passed then a set of Hadamard gates is applied as an initial state + to all qubits. + mixer_operator (OperatorBase or QuantumCircuit, optional): An optional custom mixer + to use instead of the global X-rotations, denoted as :math:`U(B, \beta)` + in the original paper. Can be an operator or an optionally parameterized quantum + circuit. + name (str): A name of the circuit, default 'qaoa' + """ + super().__init__(reps=reps, name=name) + + self._cost_operator = None + self._reps = reps + self._initial_state = initial_state + self._mixer = mixer_operator + + # set this circuit as a not-built circuit + self._bounds = None + + # store cost operator and set the registers if the operator is not None + self.cost_operator = cost_operator + + def _check_configuration(self, raise_on_failure: bool = True) -> bool: + valid = True + + if not super()._check_configuration(raise_on_failure): + return False + + if self.cost_operator is None: + valid = False + if raise_on_failure: + raise ValueError( + "The operator representing the cost of the optimization problem is not set" + ) + + if self.initial_state is not None and self.initial_state.num_qubits != self.num_qubits: + valid = False + if raise_on_failure: + raise ValueError( + "The number of qubits of the initial state {} does not match " + "the number of qubits of the cost operator {}".format( + self.initial_state.num_qubits, self.num_qubits + ) + ) + + if self.mixer_operator is not None and self.mixer_operator.num_qubits != self.num_qubits: + valid = False + if raise_on_failure: + raise ValueError( + "The number of qubits of the mixer {} does not match " + "the number of qubits of the cost operator {}".format( + self.mixer_operator.num_qubits, self.num_qubits + ) + ) + + return valid + + @property + def parameter_bounds( + self, + ) -> Optional[List[Tuple[Optional[float], Optional[float]]]]: + """The parameter bounds for the unbound parameters in the circuit. + + Returns: + A list of pairs indicating the bounds, as (lower, upper). None indicates an unbounded + parameter in the corresponding direction. If None is returned, problem is fully + unbounded. + """ + if self._bounds is not None: + return self._bounds + + # if the mixer is a circuit, we set no bounds + if isinstance(self.mixer_operator, QuantumCircuit): + return None + + # default bounds: None for gamma (cost operator), [0, 2pi] for gamma (mixer operator) + beta_bounds = (0, 2 * np.pi) + gamma_bounds = (None, None) + bounds = [] + + if not _is_pauli_identity(self.mixer_operator): + bounds += self.reps * [beta_bounds] + + if not _is_pauli_identity(self.cost_operator): + bounds += self.reps * [gamma_bounds] + + return bounds + + @parameter_bounds.setter + def parameter_bounds( + self, bounds: Optional[List[Tuple[Optional[float], Optional[float]]]] + ) -> None: + """Set the parameter bounds. + + Args: + bounds: The new parameter bounds. + """ + self._bounds = bounds + + @property + def operators(self): + """The operators that are evolved in this circuit. + + Returns: + List[Union[OperatorBase, QuantumCircuit]]: The operators to be evolved (and circuits) + in this ansatz. + """ + return [self.cost_operator, self.mixer_operator] + + @property + def cost_operator(self): + """Returns an operator representing the cost of the optimization problem. + + Returns: + OperatorBase: cost operator. + """ + return self._cost_operator + + @cost_operator.setter + def cost_operator(self, cost_operator) -> None: + """Sets cost operator. + + Args: + cost_operator (OperatorBase, optional): cost operator to set. + """ + self._cost_operator = cost_operator + self.qregs = [QuantumRegister(self.num_qubits, name="q")] + self._invalidate() + + @property + def reps(self) -> int: + """Returns the `reps` parameter, which determines the depth of the circuit.""" + return self._reps + + @reps.setter + def reps(self, reps: int) -> None: + """Sets the `reps` parameter.""" + self._reps = reps + self._invalidate() + + @property + def initial_state(self) -> Optional[QuantumCircuit]: + """Returns an optional initial state as a circuit""" + if self._initial_state is not None: + return self._initial_state + + # if no initial state is passed and we know the number of qubits, then initialize it. + if self.cost_operator is not None: + return _get_default_initial_state(self.cost_operator) + + # otherwise we cannot provide a default + return None + + @initial_state.setter + def initial_state(self, initial_state: Optional[QuantumCircuit]) -> None: + """Sets initial state.""" + self._initial_state = initial_state + self._invalidate() + + # we can't directly specify OperatorBase as a return type, it causes a circular import + # and pylint objects if return type is not documented + @property + def mixer_operator(self): + """Returns an optional mixer operator expressed as an operator or a quantum circuit. + + Returns: + OperatorBase or QuantumCircuit, optional: mixer operator or circuit. + """ + if self._mixer is not None: + return self._mixer + + # if no mixer is passed and we know the number of qubits, then initialize it. + if self.cost_operator is not None: + return _get_default_mixer(self.cost_operator) + + # otherwise we cannot provide a default + return None + + @mixer_operator.setter + def mixer_operator(self, mixer_operator) -> None: + """Sets mixer operator. + + Args: + mixer_operator (OperatorBase or QuantumCircuit, optional): mixer operator or circuit + to set. + """ + self._mixer = mixer_operator + self._invalidate() + + def _build_gate(self): + """Return a QAOAGate based on the current settings.""" + return QAOAGate( + cost_operator=self.cost_operator, + reps=self.reps, + initial_state=self.initial_state, + mixer_operator=self.mixer_operator, + label=self.name, + ) + + def _build(self): + if self._data is not None: + return + + # need to check configuration here to ensure the operators are not None + self._check_configuration() + self._data = [] + num_qubits = self.num_qubits + + qr = QuantumRegister(num_qubits, "q") + if qr.name not in [qreg.name for qreg in self.qregs]: + # if the register already exists, probably because of a previous composition. + # Otherwise, add it. + self.add_register(qr) + + self._append( + self._build_gate(), + qargs=self.qubits, + cargs=[], + ) + + num_cost = 0 if _is_pauli_identity(self.cost_operator) else 1 + if isinstance(self.mixer_operator, QuantumCircuit): + num_mixer = self.mixer_operator.num_parameters + else: + num_mixer = 0 if _is_pauli_identity(self.mixer_operator) else 1 + + betas = ParameterVector("β", self.reps * num_mixer) + gammas = ParameterVector("γ", self.reps * num_cost) + + # Create a permutation to take us from (cost_1, mixer_1, cost_2, mixer_2, ...) + # to (cost_1, cost_2, ..., mixer_1, mixer_2, ...), or if the mixer is a circuit + # with more than 1 parameters, from (cost_1, mixer_1a, mixer_1b, cost_2, ...) + # to (cost_1, cost_2, ..., mixer_1a, mixer_1b, mixer_2a, mixer_2b, ...) + reordered = [] + for rep in range(self.reps): + reordered.extend(gammas[rep * num_cost : (rep + 1) * num_cost]) + reordered.extend(betas[rep * num_mixer : (rep + 1) * num_mixer]) + + self.assign_parameters(reordered, inplace=True) + + +class QAOAGate(Gate): + """A generalized QAOA gate with a support of custom initial states and mixers. + + References: + + [1]: Farhi et al., A Quantum Approximate Optimization Algorithm. + `arXiv:1411.4028 `_ + """ + + def __init__( + self, + cost_operator, + reps: int = 1, + initial_state: Optional[QuantumCircuit] = None, + mixer_operator=None, + label: str = None, + ): + r""" + Args: + cost_operator (OperatorBase, optional): The operator representing the cost of + the optimization problem, denoted as :math:`U(C, \gamma)` in the original paper. + Must be set either in the constructor or via property setter. + reps (int): The integer parameter p, which determines the depth of the circuit, + as specified in the original paper, default is 1. + initial_state (QuantumCircuit, optional): An optional initial state to use. + If `None` is passed then a set of Hadamard gates is applied as an initial state + to all qubits. + mixer_operator (OperatorBase or QuantumCircuit, optional): An optional custom mixer + to use instead of the global X-rotations, denoted as :math:`U(B, \beta)` + in the original paper. Can be an operator or an optionally parameterized quantum + circuit. + label (str): A label. + Raises: + AttributeError: when cost_operator and initial_state are not compatible + """ + # pylint: disable=cyclic-import + from qiskit.opflow import PauliOp, PauliTrotterEvolution + + self.cost_operator = cost_operator + + if initial_state is None: + initial_state = _get_default_initial_state(cost_operator) + else: + if initial_state.num_qubits != cost_operator.num_qubits: + raise AttributeError( + "initial_state and cost_operator has incompatible number of qubits" + ) + + if mixer_operator is None: + mixer_operator = _get_default_mixer(cost_operator) + + self.mixer_operator = mixer_operator + self.operators = [cost_operator, mixer_operator] + self.reps = reps + self.evolution = PauliTrotterEvolution() + self.initial_state = initial_state + + # determine how many parameters the circuit will contain + num_parameters = 0 + for op in self.operators: + if isinstance(op, QuantumCircuit): + num_parameters += op.num_parameters + else: + # check if the operator is just the identity, if yes, skip it + if isinstance(op, PauliOp): + sig_qubits = np.logical_or(op.primitive.x, op.primitive.z) + if sum(sig_qubits) == 0: + continue + num_parameters += 1 + + super().__init__( + "QAOA", + self.operators[0].num_qubits, + params=list(ParameterVector("t", reps * num_parameters)), + label=label, + ) + + def _define(self): + """Build the circuit by evolving the operators and using NLocal for the repetitions.""" + coeff = Parameter("c") + circuits = [] + bind_parameter = [] + for op in self.operators: + # if the operator is already the evolved circuit just append it + if isinstance(op, QuantumCircuit): + circuits.append(op) + bind_parameter.append(False) + else: + evolved_op = self.evolution.convert((coeff * op).exp_i()).reduce() + circuit = evolved_op.to_circuit() + # if the operator was the identity it is amounts only to a global phase and no + # parameter is added + bind_parameter.append(circuit.num_parameters > 0) + circuits.append(circuit) + + self.definition = ( + NLocal( + circuits[0].num_qubits, + rotation_blocks=[], + entanglement_blocks=circuits, + reps=self.reps, + initial_state=self.initial_state, + ) + .decompose() + .assign_parameters(self.params) + ) + + +def _validate_operators(operators): + if not isinstance(operators, list): + operators = [operators] + + if len(operators) > 1: + num_qubits = operators[0].num_qubits + if any(operators[i].num_qubits != num_qubits for i in range(1, len(operators))): + raise ValueError("All operators must act on the same number of qubits.") + + return operators + + +def _validate_prefix(parameter_prefix, operators): + if isinstance(parameter_prefix, str): + return len(operators) * [parameter_prefix] + if len(parameter_prefix) != len(operators): + raise ValueError("The number of parameter prefixes must match the operators.") + + return parameter_prefix + + +def _is_pauli_identity(operator): + from qiskit.opflow import PauliOp + + if isinstance(operator, PauliOp): + return not np.any(np.logical_or(operator.primitive.x, operator.primitive.z)) + return False + + +def _get_default_mixer(cost_operator): + # local imports to avoid circular imports + from qiskit.opflow import I, X + + num_qubits = cost_operator.num_qubits + + # Mixer is just a sum of single qubit X's on each qubit. Evolving by this operator + # will simply produce rx's on each qubit. + active_indices = _active_qubits(cost_operator) + + if len(active_indices) == 0: + return 0 * (I ^ num_qubits) + + mixer_terms = [(I ^ left) ^ X ^ (I ^ (num_qubits - left - 1)) for left in active_indices] + return sum(mixer_terms) + + +def _get_default_initial_state(cost_operator): + initial_state = QuantumCircuit(cost_operator.num_qubits) + active_indices = _active_qubits(cost_operator) + + if len(active_indices) > 0: + # Opflow indices are reversed with respect to circuit indices + active_indices = [cost_operator.num_qubits - 1 - index for index in active_indices] + initial_state.h(active_indices) + + return initial_state + + +def _active_qubits(operator): + from qiskit.opflow import PauliSumOp, PauliOp + + # active qubit selection only supported for PauliSumOps + if isinstance(operator, PauliSumOp): + sparse_pauli = operator.primitive + paulis = sparse_pauli.paulis.to_labels() + elif isinstance(operator, PauliOp): + paulis = [operator.primitive.to_label()] + else: + return list(range(operator.num_qubits)) + + # for each Pauli string get a list which Pauli is the identity (i.e. not active) + is_identity = [list(map(lambda pauli: pauli == "I", pauli_string)) for pauli_string in paulis] + + # use numpy act a logical and on each index across the Pauli strings + idle_qubits = np.all(np.array(is_identity), axis=0) + + # return the indices of the qubits that are not idle + active_indices = [index for index, idle in enumerate(idle_qubits) if not idle] + + return active_indices + + +# end QAOA gate + + +def line_coloring(num_vertices) -> Dict: + """ + Creates an edge coloring of the line graph, corresponding to the optimal + line swap strategy, given as a dictionary where the keys + correspond to the different colors and the values are lists of edges (where edges + are specified as tuples). The graph coloring consists of one color for all even-numbered + edges and one color for all odd-numbered edges. + Args: + num_vertices: The number of vertices in the line graph + Returns: + Graph coloring as a dictionary of edge lists + """ + line_coloring = {} + for i in range(num_vertices - 1): + line_coloring[(i, i + 1)] = i % 2 + line_coloring[(i + 1, i)] = i % 2 + return line_coloring + + +class SwapStrategy: + """A class representing SWAP strategies for coupling maps. + + A swap strategy is a list of swap layers to apply to the physical coupling map. Each swap layer + is specified by a set of tuples which correspond to the edges of the physical coupling map that + are swapped. At each swap layer SWAP gates are applied to the corresponding edges. This class + stores the permutations of the qubits resulting from the swap strategy. + + """ + + def __init__( + self, + coupling_map: CouplingMap, + swap_layers: List[List[Tuple[int, int]]], + edge_coloring: Optional[Dict[Tuple[int, int], int]] = None, + ): + """ + Args: + coupling_map: The coupling map the strategy is implemented for. + swap_layers: The swap layers of the strategy, specified as a list of sets of + edges (edges can be represented as lists, sets or tuples containing two integers). + edge_coloring: (Optional) edge coloring of the coupling map, specified as a set of + sets of edges (edges can be represented as lists, sets or tuples containing two + integers). The edge coloring is used for efficient gate parallelization when + using the swap strategy in a transpiler pass. + """ + self.coupling_map = copy.deepcopy(coupling_map) + self.num_vertices = coupling_map.size() + self.swap_layers = swap_layers + self.edge_coloring = edge_coloring + self._distance_matrix = None + self._inverse_composed_permutation = {0: list(range(self.num_vertices))} + + @property + def coupling_map(self) -> CouplingMap: + """Returns the coupling map of the SWAP strategy.""" + return self._coupling_map + + @coupling_map.setter + def coupling_map(self, coupling_map: CouplingMap) -> None: + """Sets the coupling map of the SWAP strategy.""" + self._coupling_map = coupling_map + self._invalidate() + + @property + def swap_layers(self) -> List: + """Returns the SWAP layers of the SWAP strategy.""" + return self._swap_layers + + @swap_layers.setter + def swap_layers(self, swap_layers: List) -> None: + """Sets the SWAP layers of the SWAP strategy.""" + self._swap_layers = swap_layers + self._invalidate() + + @property + def max_distance(self, qubits: Optional[List] = None) -> Optional[int]: + """ + Return the maximum distance in the SWAP strategy between a list of specified qubits or + all qubits if no list is given. + + Args: + qubits: List of qubits to consider. + + Returns: + The maximum distance between qubits. None if two qubits in the list have no defined + distance. + """ + if qubits is None: + qubits = list(range(self.num_vertices)) + + max_distance = 0 + + for i in range(len(qubits)): + for j in range(i): + qubit1 = qubits[i] + qubit2 = qubits[j] + if self.distance_matrix[qubit1][qubit2] is None: + return None + + max_distance = max(max_distance, self.distance_matrix[qubit1][qubit2]) + + return max_distance + + def __len__(self) -> int: + """Return the length of the strategy as the number of layers.""" + return len(self.swap_layers) + + def __repr__(self) -> str: + """Print the swap strategy.""" + description = f"{self.__class__.__name__} with swap layers:\n" + + for layer in self.swap_layers: + description += f"{layer},\n" + + description += f"on {self.coupling_map} coupling map." + + return description + + @property + def distance_matrix(self) -> List[List]: + """ + Returns the distance matrix of the SWAP strategy as a nested list, where the entry (i,j) + corresponds to the number of SWAP layers that need to be applied to obtain a connection + between physical qubits i and j. + + Returns: + Distance matrix for the SWAP strategy as a nested list. + """ + self._check_configuration(raise_on_failure=True) + + # Only compute the distance matrix if it has not been computed before + if self._distance_matrix is None: + distance_matrix = [[None] * self.num_vertices for _ in range(self.num_vertices)] + + for i in range(self.num_vertices): + distance_matrix[i][i] = 0 + + for i in range(0, len(self.swap_layers) + 1): + for [j, k] in self.swapped_coupling_map(i).get_edges(): + if distance_matrix[j][k] is None: + distance_matrix[j][k] = i + distance_matrix[k][j] = i + + self._distance_matrix = distance_matrix + + return self._distance_matrix + + def permute_labels(self, permutation: List[int], inplace: bool = True): + """ + Permute the labels of the underlying coupling map of the SWAP strategy + and adapt the corresponding SWAP layers accordingly. + + Args: + permutation: The permutation to swap labels by, specified as a list of integers + inplace: Specifies whether the swap strategy object should be modified or a new + swap strategy object created. + + Returns: + A newly created swap strategy if inplace = False, else None + """ + permuted_coupling_map = CouplingMap( + couplinglist=[ + [permutation[edge[0]], permutation[edge[1]]] + for edge in self.coupling_map.get_edges() + ] + ) + permuted_swap_layers = [] + for swap_layer in self.swap_layers: + permuted_swap_layer = [(permutation[i], permutation[j]) for (i, j) in swap_layer] + permuted_swap_layers.append(permuted_swap_layer) + + if inplace: + self.coupling_map = permuted_coupling_map + self.swap_layers = permuted_swap_layers + else: + return SwapStrategy( + coupling_map=permuted_coupling_map, + swap_layers=permuted_swap_layers, + edge_coloring=self.edge_coloring, + ) + + def new_connections(self, idx: int) -> List[Set]: + """ + Returns the new connections obtained after applying the SWAP layer specified by idx, i.e. + a list of qubit pairs that are adjacent to one another after idx steps of the SWAP strategy. + + Args: + idx: The index of the SWAP layer. 1 refers to the first SWAP layer + whereas idx = 0 will return the connections present in the original coupling map + + Returns: + A set of edges representing the new qubit connections + """ + connections = [] + for i in range(self.num_vertices): + for j in range(i): + if self.distance_matrix[i][j] == idx: + connections.append({i, j}) + + return connections + + def missing_couplings(self) -> Set[Tuple[int, int]]: + """Returns the set of couplings that cannot be reached.""" + physical_qubits = list(set(sum(self._coupling_map.get_edges(), ()))) + missed_edges = set() + for i in range(len(physical_qubits)): + for j in range(i + 1, len(physical_qubits)): + missed_edges.add((physical_qubits[i], physical_qubits[j])) + missed_edges.add((physical_qubits[j], physical_qubits[i])) + + for layer_idx in range(len(self) + 1): + for edge in self.new_connections(layer_idx): + for edge_tuple in [tuple(edge), tuple(edge)[::-1]]: + try: + missed_edges.remove(edge_tuple) + except KeyError: + pass + + return missed_edges + + def reaches_full_connectivity(self) -> bool: + """Return True if the swap strategy reaches full connectivity.""" + return len(self.missing_couplings()) == 0 + + def swapped_coupling_map(self, idx: int) -> CouplingMap: + """ + Returns the coupling map after applying a specified number of SWAP layers + from the SWAP strategy. + + Args: + idx: The number of SWAP layers to apply. For idx = 0, the original coupling + map is returned. + + Returns: + The swapped coupling map. + """ + permutation = self.inverse_composed_permutation(idx) + + edges = [[permutation[i], permutation[j]] for [i, j] in self.coupling_map.get_edges()] + + return CouplingMap(couplinglist=edges) + + def apply_swap_layer(self, list_to_swap: List, idx: int) -> List: + """ + Apply SWAPS from a layer specified by idx to a list of elements by + interchanging their positions. + + Args: + list_to_swap: The list of elements to swap + idx: The index of the SWAP layer to apply + + Returns: + The list with swapped elements + """ + x = copy.copy(list_to_swap) + + for edge in self.swap_layers[idx]: + (i, j) = tuple(edge) + x[i], x[j] = x[j], x[i] + + return x + + def composed_permutation(self, idx) -> List[int]: + """ + Returns the composed permutation of all SWAP layers applied up to a specified index. + Permutations are represented by list of integers where the ith element corresponds + to the mapping of i under the permutation. + + Args: + idx: The number of SWAP layers to apply + + Returns: + The permutation as a list of integer values + """ + return self.invert_permutation(self.inverse_composed_permutation(idx)) + + def inverse_composed_permutation(self, idx) -> List[int]: + """ + Returns the inversed composed permutation of all SWAP layers applied up to a specified + index. Permutations are represented by list of integers where the ith element corresponds + to the mapping of i under the permutation. + + Args: + idx: The number of SWAP layers to apply + + Returns: + The inversed permutation as a list of integer values + """ + # Only compute the inverse permutation if it has not been computed before + self._check_configuration(raise_on_failure=True) + try: + return self._inverse_composed_permutation[idx] + + except KeyError: + if idx == 0: + return list(range(self.num_vertices)) + + self._inverse_composed_permutation[idx] = self.apply_swap_layer( + self.inverse_composed_permutation(idx - 1), idx - 1 + ) + + return self._inverse_composed_permutation[idx] + + @staticmethod + def invert_permutation(permutation: List) -> List: + """ + Inverts a permutation specified by a list of integers where the ith element corresponds + to the mapping of i under the permutation. + + Args: + The original permutation + + Returns: + The inverse of the permutation + """ + inverse_permutation = [0] * len(permutation) + for i, j in enumerate(permutation): + if not isinstance(j, int): + raise ValueError(f"Permutation contains non-integer element {j}.") + if j > len(permutation): + raise ValueError( + f"Permutation of length {len(permutation)} contains " + f"element {j} and is therefore invalid." + ) + inverse_permutation[j] = i + return inverse_permutation + + def _invalidate(self) -> None: + """Reset all precomputed properties of the SWAP strategy""" + self._distance_matrix = None + self._inverse_composed_permutation = {} + + def _check_configuration(self, raise_on_failure: bool = True) -> bool: + """ + Check that the configuration of the SWAP strategy is valid + + Args: + raise_on_failure: Specifies whether the function should raise an exception + if the SWAP strategy is invalid. + + Returns: + True if the SWAP strategy is valid, False otherwise (unless an exception is raised) + """ + if self.coupling_map is None: + if raise_on_failure: + raise RuntimeError("The coupling map is None.") + return False + + if self.swap_layers is None: + if raise_on_failure: + raise RuntimeError("SWAP layers are None.") + return False + + edge_set = set(self.coupling_map.get_edges()) + for i, layer in enumerate(self.swap_layers): + for edge in layer: + if edge not in edge_set: + if raise_on_failure: + raise RuntimeError( + f"The {i}th SWAP layer contains the edge {edge} which is not " + f"part of the underlying coupling map with {edge_set} edges." + ) + return False + + return True + + def embed_in(self, coupling_map, vertex_mapping=None, retain_edge_coloring=True): + """ + Given a coupling map and optionally some mapping between vertices of self.coupling_map + and the given coupling map, this function creates a new SWAP strategy by embedding the + existing SWAP strategy in the new coupling map. For instance, this allows us to use the + line strategy in any coupling map that includes a line graph. + + Args: + coupling_map: The new coupling map + vertex_mapping: An optional mapping between vertices of the old and the new coupling + map. If None, a trivial mapping (0 -> 0, 1 -> 1, etc.) is used + retain_edge_coloring: Specifies whether edge coloring of old SWAP strategy should be + used in the embedded strategy. Note that this can lead to an incomplete coloring + in the new strategy. + + Returns: + The new SWAP strategy obtained from embedding the existing SWAP strategy in the new + coupling map + """ + if coupling_map.size() < self.num_vertices: + raise RuntimeError( + f"Cannot embed SWAP strategy for coupling map with {self.num_vertices} " + f"in coupling map with {coupling_map.size()} vertices." + ) + + if vertex_mapping is None: + vertex_mapping = {i: i for i in range(self.num_vertices)} + + swap_layers = [] + for swap_layer in self.swap_layers: + swap_layers.append([(vertex_mapping[i], vertex_mapping[j]) for (i, j) in swap_layer]) + + if retain_edge_coloring and self.edge_coloring is not None: + edge_coloring = { + (vertex_mapping[i], vertex_mapping[j]): self.edge_coloring[(i, j)] + for (i, j) in self.edge_coloring.keys() + } + else: + edge_coloring = None + + return SwapStrategy( + coupling_map=coupling_map, + swap_layers=swap_layers, + edge_coloring=edge_coloring, + ) + + +class LineSwapStrategy(SwapStrategy): + """An optimal SWAP strategy for a line.""" + + def __init__(self, line: List[int], num_swap_layers: int = None): + """ + Creates a swap strategy for a line graph with the specified number of SWAP layers. + This SWAP strategy will use the full line if instructed to do so (i.e. num_variables + is None or equal to num_vertices). If instructed otherwise then the first num_variables + nodes of the line will be used in the swap strategy. + + Args: + line: A line given as a list of nodes, e.g. [0, 2, 3, 4]. + num_swap_layers: Number of swap layers the swap manager should be initialized with + + Returns: + Swap strategy for the line graph + """ + + if num_swap_layers is None: + num_swap_layers = len(line) - 2 + + elif num_swap_layers < 0: + raise ValueError(f"Negative number {num_swap_layers} passed for number of swap layers.") + + swap_layer0 = [(line[i], line[i + 1]) for i in range(0, len(line) - 1, 2)] + swap_layer1 = [(line[i], line[i + 1]) for i in range(1, len(line) - 1, 2)] + + base_layers = [swap_layer0, swap_layer1] + + swap_layers = [] + for i in range(num_swap_layers): + swap_layers.append(base_layers[i % 2]) + + couplings = [] + for idx in range(len(line) - 1): + couplings.append((line[idx], line[idx + 1])) + couplings.append((line[idx + 1], line[idx])) + + super().__init__( + coupling_map=CouplingMap(couplings), + swap_layers=swap_layers, + edge_coloring=line_coloring(num_vertices=len(line)), + ) + + +class FiveQubitTeeSwapStrategy(SwapStrategy): + """A swap strategy for a coupling map of the form + + .. parsed-literal:: + + 0 -- 1 -- 2 + | + 3 + | + 4 + + """ + + def __init__(self): + """Initialize the swap strategy.""" + + swaps = [ + [(1, 3)], + [(0, 1), (3, 4)], + [(1, 3)], + ] + + edges = [[0, 1], [1, 2], [1, 3], [3, 4]] + + coupling_map = CouplingMap(edges) + coupling_map.make_symmetric() + + super().__init__(coupling_map, swaps) + + +class SevenQubitHeavySwapStrategy(SwapStrategy): + """A swap strategy for a coupling map of the form + + .. parsed-literal:: + + 0 -- 1 -- 2 + | + 3 + | + 4 -- 5 -- 6 + + """ + + def __init__(self, n_qubits: int): + """Initialize the swap strategy.""" + + if n_qubits == 7: + swaps = [ + [(0, 1), (3, 5)], + [(1, 3), (4, 5)], + [(0, 1), (3, 5)], + [(1, 2), (5, 6)], + [(3, 5)], + [(1, 3), (5, 6)], + [(3, 5)], + ] + + edges = [[0, 1], [1, 2], [1, 3], [3, 5], [4, 5], [5, 6]] + else: + swaps = [ + [(0, 1), (3, 5)], + [(1, 3), (4, 5)], + [(0, 1), (3, 5)], + [(1, 2)], + [(3, 5)], + [(1, 3)], + [(3, 5)], + ] + + edges = [[0, 1], [1, 2], [1, 3], [3, 5], [4, 5]] + + coupling_map = CouplingMap(edges) + coupling_map.make_symmetric() + + super().__init__(coupling_map, swaps) + + +class DoubleRingSwapStrategy(SwapStrategy): + """Full connectivity swap strategies for a coupling map of the form + + .. parsed-literal:: + + 6 17 + | | + 0 -- 1 -- 4 -- 7 -- 10 -- 12 -- 15 -- 18 -- 21 -- 23 + | | | + 2 13 24 + | | | + 3 -- 5 -- 8 -- 11 -- 14 -- 16 -- 19 -- 22 -- 25 -- 26 + | | + 9 20 + + The swap layers and qubits used depend on the number of qubits. If the number of + qubits is smaller than the length of the longest line then a longest line swap + strategy should be used. For larger number of qubits this class will create the swap + layers to apply. It also uses predefined swap layers and qubits to include which + depend on the number of needed qubits. + """ + + def __init__(self, n_qubits: int) -> None: + """Initialize the swap strategy. + + Args: + n_qubits: The number of qubits for which to generate the swap strategy. + + Raises: + QiskitError: If the number of qubits is smaller than 22 or larger then 27. + In the first case a line swap strategy should be used and in the second + case the backend does not have enough qubits. + """ + + self._lline = [ + 0, + 1, + 2, + 3, + 5, + 8, + 11, + 14, + 16, + 19, + 22, + 25, + 24, + 23, + 21, + 18, + 15, + 12, + 10, + 7, + 6, + ] + + # Create the longest line edges for the coupling map. + # Forward direction + self._longest_line_map = [ + [self._lline[idx], self._lline[idx + 1]] for idx in range(len(self._lline) - 1) + ] + + # Backward direction + self._longest_line_map += [ + [self._lline[idx + 1], self._lline[idx]] for idx in range(len(self._lline) - 1) + ] + + # Defines the extra swaps to apply for a number of qubits. + full_connectivity_strats = { + 22: {"n_line_layers": len(self._lline) + 3, "extra_swaps": {7: [(8, 9)]}}, + 23: { + "n_line_layers": len(self._lline) + 6, + "extra_swaps": {7: [(8, 9), (19, 20)], 17: [(19, 20)]}, + }, + 24: { + "n_line_layers": len(self._lline) + 10, + "extra_swaps": {7: [(8, 9), (19, 20)], 17: [(19, 20)]}, + }, + 25: { + "n_line_layers": len(self._lline) + 10, + "extra_swaps": { + 7: [(8, 9), (19, 20), (14, 13)], + 17: [(8, 9), (19, 20)], + }, + }, + 26: { + "n_line_layers": len(self._lline) + 10, + "extra_swaps": { + 7: [(8, 9), (19, 20), (14, 13)], + 17: [(8, 9), (19, 20), (17, 18)], + }, + }, + 27: { + "n_line_layers": len(self._lline) + 10, + "extra_swaps": { + 7: [(8, 9), (19, 20), (14, 13), (4, 1)], + 17: [(8, 9), (19, 20), (17, 18), (4, 1)], + }, + }, + } + + # The physical qubits to attach to the longest line. + # The key is the number of required qubits and the value is the qubits + # that will be added. + self._qubits_to_add = { + 22: [9], + 23: [9, 20], + 24: [9, 20, 13], + 25: [9, 20, 13, 26], + 26: [9, 20, 13, 26, 17], + 27: [9, 20, 13, 26, 17, 4], + } + + # The physical qubits used. + self._qubits_used = self._lline + self._qubits_to_add[n_qubits] + + self._dangling_qubits_edges = { + 9: [(9, 8), (8, 9)], + 20: [(20, 19), (19, 20)], + 13: [(13, 14), (14, 13), (12, 13), (13, 12)], + 26: [(26, 25), (25, 26)], + 17: [(17, 18), (18, 17)], + 4: [(1, 4), (4, 1), (4, 7), (7, 4)], + } + + # Create the swap strategy. + super().__init__( + self._make_coupling_map(n_qubits), + self._create_swap_layers(**full_connectivity_strats[n_qubits]), + ) + + def _create_swap_layers( + self, n_line_layers: int, extra_swaps: Dict[int, List[Tuple]] + ) -> List[List[Tuple[int, int]]]: + """Creates the swap layers for a given number of line layers and extra swaps. + + Args: + n_line_layers: The number of layers of the line strategy to apply. + extra_swaps: A dict where the swap layer index is the key and the value is a lis + of swaps (specified as tuples to apply.) + + Returns: + The swap layers to apply in the swap strategy. + """ + + # Swap gates on even edges, e.g. (0, 1), (2, 3) + layer1 = [ + (self._lline[idx], self._lline[idx + 1]) for idx in range(0, len(self._lline) - 1, 2) + ] + + # Swap gates on odd edges, e.g. (1, 2) + layer2 = [(self._lline[idx], self._lline[idx + 1]) for idx in range(1, len(self._lline), 2)] + + swap_strat = [] + for idx in range(n_line_layers): + if idx % 2 == 0: + swap_strat.append(layer1) + else: + swap_strat.append(layer2) + + if idx in extra_swaps: + swap_strat.append(extra_swaps[idx]) + + # The swap strategy above has been defined on the physical qubits for clarity but + # for the transpiler pass to work we must define them on the virtual qubits. + virtual_swaps = [] + for layer in swap_strat: + virtual_swaps.append([]) + for swap in layer: + pq0, pq1 = swap[0], swap[1] # physical qubits + virtual_swaps[-1].extend( + [(self.qubits_used.index(pq0), self.qubits_used.index(pq1))] + ) + + return virtual_swaps + + @property + def qubits_used(self) -> List[int]: + """Return the physical qubits that we use.""" + return self._qubits_used + + def _make_coupling_map(self, n_qubits: int) -> CouplingMap: + """Make the coupling map that we will use.""" + + # Add the extra edges to a copy of the longest line coupling map. + coupling_map = [edge for edge in self._longest_line_map] + for qubit in self._qubits_to_add[n_qubits]: + for edge in self._dangling_qubits_edges[qubit]: + coupling_map.append(edge) + + # The coupling map has been defined on the physical qubits for clarity + # however the swap strategy works with virtual qubits so we remap to virtual + virtual_map = [] + for edge in coupling_map: + pq0, pq1 = edge[0], edge[1] # physical qubits + virtual_map.extend([(self.qubits_used.index(pq0), self.qubits_used.index(pq1))]) + + return CouplingMap(virtual_map) + + +def get_swap_strategy( + backend_name: str, n_qubits: Optional[int] = None +) -> Tuple[SwapStrategy, List[int]]: + """Get a swap strategy and the mapping based on the backend. + + Args: + backend_name: the name of the backend as a string. + n_qubits: the number of qubits to use. If not specified then all qubits will be used. + + Returns: + A tuple where the first element is a swap strategy for the given backend and the second + element is the list of physical qubits that the swap strategy applies to. + """ + + if backend_name in [ + "ibmq_belem", + "ibmq_quito", + "ibmq_lime", + "fake_belem", + "fake_quito", + "fake_lime", + ]: + return FiveQubitTeeSwapStrategy(), list(range(5)) + + if backend_name in [ + "fake_lagos", + "ibm_lagos", + "ibmq_casablanca", + "ibm_nairobi", + "ibmq_jakarta", + ]: + return SevenQubitHeavySwapStrategy(n_qubits), list(range(n_qubits)) + + if backend_name in ["ibmq_santiagio", "ibmq_bogota", " ibmq_manila"]: + return LineSwapStrategy(list(range(5))), list(range(5)) + + devices_27q = [ + "ibmq_montreal", + "ibmq_mumbai", + "ibmq_kolkata", + "ibmq_dublin", + "ibm_cairo", + "ibm_hanoi", + "ibmq_toronto", + "ibmq_sydney", + "fake_montreal", + "fake_mumbai", + "fake_kolkata", + "fake_dublin", + "fake_cairo", + "fake_hanoi" "ibmq_toronto", + "fake_sydney", + ] + + if backend_name in devices_27q: + strat = DoubleRingSwapStrategy(n_qubits) + return strat, strat.qubits_used + + raise ValueError(f"No swap strategy found for {backend_name}") + + +class HWQAOAAnsatz(QAOAAnsatz): + """Class for a hardware efficient QAOAAnsatz.""" + + def __init__( + self, + cost_operator=None, + reps: int = 1, + initial_state=None, + mixer_operator=None, + name: str = "QAOA", + swap_strategy: SwapStrategy = None, + initial_layout: Layout = None, + ): + r""" + Args: + cost_operator (OperatorBase, optional): The operator representing the cost of + the optimization problem, denoted as :math:`U(C, \gamma)` in the original paper. + Must be set either in the constructor or via property setter. + reps (int): The integer parameter p, which determines the depth of the circuit, + as specified in the original paper, default is 1. + initial_state (QuantumCircuit, optional): An optional initial state to use. + If `None` is passed then a set of Hadamard gates is applied as an initial state + to all qubits. + mixer_operator (OperatorBase or QuantumCircuit, optional): An optional custom mixer + to use instead of the global X-rotations, denoted as :math:`U(B, \beta)` + in the original paper. Can be an operator or an optionally parameterized quantum + circuit. + name (str): A name of the circuit, default 'qaoa' + swap_strategy (SwapStrategy): The SWAP strategy used for insertion of SWAP layers + initial_layout (Layout): The initial layout mapping logical to physical qubits. If not + specified, a trivial initial layout is used + """ + self._num_logical_qubits = None + self._num_physical_qubits = None + self._swapped_layout = None + self._cost_matrix = None + + super().__init__( + cost_operator=cost_operator, + reps=reps, + initial_state=initial_state, + mixer_operator=mixer_operator, + name=name, + ) + + self.swap_strategy = swap_strategy + self.initial_layout = initial_layout + + @property + def final_layout(self) -> Layout: + """ + Returns: + Returns a Layout object representing the layout after application of the + mapped QAOA circuit. + """ + if self.reps % 2 == 0: + return self.initial_layout + else: + return self._swapped_layout + + @property + def num_qubits(self) -> int: + """ + The number of qubits in the circuit. This is given as the number of physical qubits, i.e. + the size of the coupling map the QAOA circuit is mapped to. If no SWAP strategy and + therefore no coupling map is specified, this defaults to the number of logical qubits. + Returns 0 if no cost operator has been set yet. + + Returns: + The number of qubits + """ + if self._num_physical_qubits is not None: + return self._num_physical_qubits + elif self._num_logical_qubits is not None: + return self._num_logical_qubits + else: + return 0 + + @property + def cost_operator(self): + """Returns an operator representing the cost of the optimization problem. + + We need to override the cost_operator property to set the cost_matrix accordingly. + + Returns: + OperatorBase: cost operator. + """ + return self._cost_operator + + @cost_operator.setter + def cost_operator(self, cost_operator: Optional[OperatorBase]) -> None: + """ + Sets cost operator. + Args: + cost_operator: cost operator to set. + """ + self._cost_operator = cost_operator + self._invalidate() + self._num_logical_qubits = cost_operator.num_qubits if cost_operator else None + self._cost_matrix = self._get_cost_matrix(cost_operator) if cost_operator else None + + @property + def swap_strategy(self) -> Optional[SwapStrategy]: + """Returns the swap strategy used for mapping the circuit to a coupling map.""" + return self._swap_strategy + + @swap_strategy.setter + def swap_strategy(self, swap_strategy: Optional[SwapStrategy]) -> None: + """Sets the swap strategy used for mapping the circuit to a coupling map.""" + self._swap_strategy = swap_strategy + self._num_physical_qubits = swap_strategy.num_vertices if swap_strategy else None + self._invalidate() + + @property + def initial_layout(self) -> Optional[Layout]: + """Returns the initial layout (mapping of physical to logical qubits) of the circuit.""" + return self._initial_layout + + @initial_layout.setter + def initial_layout(self, initial_layout: Optional[Layout]) -> None: + """Sets the initial layout (mapping of physical to logical qubits).""" + self._initial_layout = initial_layout + self._invalidate() + + @staticmethod + def _get_cost_matrix(cost_operator: OperatorBase) -> np.array: + """Returns the cost matrix of a cost operator + + Args: + cost_operator: An operator representing the cost of an optimization problem. + + Returns: + The corresponding cost matrix as a nested list. + """ + # TODO: Add check for cost operator? + quadratic_program = QuadraticProgram() + quadratic_program.from_ising(qubit_op=cost_operator) + size = len(quadratic_program.variables) + objective = quadratic_program.objective + matrix = objective.quadratic.to_array(symmetric=True) + linear_coeff = objective.linear.to_array() + + for i in range(size): + matrix[i][i] += linear_coeff[i] + + return matrix + + def _check_configuration(self, raise_on_failure: bool = True) -> bool: + """Check if the current configuration allows the circuit to be built. + + Args: + raise_on_failure: If True, raise if the configuration is invalid. If False, return + False if the configuration is invalid. + + Returns: + True, if the configuration is valid. Otherwise, depending on the value of + ``raise_on_failure`` an error is raised or False is returned. + """ + # Check standard QAOA configuration requirements + if not super()._check_configuration(raise_on_failure=raise_on_failure): + return False + + if self.swap_strategy is not None and self.cost_operator is not None: + # Check that circuit can be mapped to coupling map of specified SWAP strategy + if self._num_logical_qubits > self._num_physical_qubits: + if raise_on_failure: + raise AttributeError( + f"Cannot map a circuit with {self._num_logical_qubits} qubits to " + f"a physical device with {self._num_physical_qubits} qubits." + ) + return False + + # Check that initial layout is compatible with coupling map and that process terminates! + + # 1) build all possible connections that the swap strategy builds. + possible_edges = set() + for swap_layer_idx in range(len(self._swap_strategy) + 1): + for edge in self._swap_strategy.swapped_coupling_map(swap_layer_idx).get_edges(): + possible_edges.add(edge) + + # 2) Get a list of Pauli strings in the operator, e.g. ["IIZZ", "ZIIZ"] + pauli_strings = [] + for ops in self.operators: + pauli_strings.extend([str(pauli) for pauli in ops.primitive.paulis]) + + # 3) Convert the strings to edges. + required_edges = set() + for pauli_str in pauli_strings: + edge = tuple([i for i, p in enumerate(pauli_str[::-1]) if p != "I"]) + + if len(edge) == 2: + required_edges.add(edge) + + if len(edge) > 2: + if raise_on_failure: + raise ValueError( + f"The Pauli {pauli_str} is non-local (i.e. has more than 2 Z-terms)." + ) + + return False + + # 4) Check that the swap strategy supports all required edges + for edge in required_edges: + if edge not in possible_edges: + if raise_on_failure: + raise ValueError( + f"The edge {edge} is not supported by the SWAP strategy " + f"{self.swap_strategy} which creates edges {possible_edges}." + ) + + return False + + return True + + def _build(self) -> None: + """Build the circuit.""" + + # If the _data property is set the circuit has already been built + if self._data is not None: + return + + # Default to standard QAOAAnsatz if no SWAP strategy has been set + if self.swap_strategy is None: + super()._build() + return + + self._check_configuration() + self._data = [] + + # Set the registers + try: + qr = QuantumRegister(self.num_qubits, "q") + self.add_register(qr) + except CircuitError: + # The register already exists, probably because of a previous composition + pass + + # Set initial layout to trivial layout if not set + if self._initial_layout is None: + self._initial_layout = Layout.generate_trivial_layout(*self.qregs) + + # Order qubit pairs by the minimal number of steps after which they are adjacent during + # the SWAP process, i.e. by the SWAP layer depth after which the corresponding + # interaction can be applied + distance_matrix = self.swap_strategy.distance_matrix + gate_layers = {} + for i in range(self._num_logical_qubits): + for j in range(i): + + rotation_angle = self._cost_matrix[i][j] + + if np.isclose(rotation_angle, 0): + continue + + distance = distance_matrix[self.initial_layout.get_virtual_bits()[self.qubits[i]]][ + self.initial_layout.get_virtual_bits()[self.qubits[j]] + ] + + if distance not in gate_layers.keys(): + gate_layers[distance] = {(i, j): rotation_angle} + else: + gate_layers[distance][(i, j)] = rotation_angle + + max_distance = max(gate_layers.keys()) + final_permutation = self.swap_strategy.composed_permutation(idx=max_distance) + self._swapped_layout = Layout() + self._swapped_layout.from_dict( + { + self.initial_layout.get_physical_bits()[i]: final_permutation[i] + for i in range(self._num_physical_qubits) + } + ) + + # Apply QAOA layers + qaoa_circuit = QuantumCircuit(*self.qregs, name=self.name) + parameters = ParameterVector("t", 2 * self.reps) + for i in range(self.reps): + qaoa_circuit.compose( + self._mapped_qaoa_layer( + mixer_parameter=parameters[2 * i + 1], + cost_parameter=parameters[2 * i], + rzz_layers=gate_layers, + reverse_ops=(i % 2 == 1), + ), + inplace=True, + ) + + # Prepend an initial state (defaults to the equal superposition state if not specified) + if self.initial_state: + qaoa_circuit.compose( + self.initial_state, + front=True, + inplace=True, + qubits=list(range(self._num_logical_qubits)), + ) + + try: + instr = qaoa_circuit.to_gate() + except QiskitError: + instr = qaoa_circuit.to_instruction() + + self.append(instr, self.qubits) + + def _mapped_qaoa_layer( + self, mixer_parameter, cost_parameter, rzz_layers, reverse_ops=False + ) -> QuantumCircuit: + """ + Creates a single mapped QAOA layer with specified parameters and rzz_layers. The mapped + circuit consists of alternating RZZ and SWAP layers. + + Args: + mixer_parameter: Parameter to use for mixer layer + cost_parameter: Parameter to use for cost layer + rzz_layers: RZZ layers specified as a dictionary with integer keys corresponding to the + layer index and values corresponding to the RZZ gates in the layer. RZZ gates are + specified as dictionaries with integer tuples specifying the corresponding qubit + pairs as keys and rotational angles as values. + reverse_ops: Reverses the QAOA layer by reversing the order of the instructions. This + allows us to alternate between even and odd layers. + + Returns: + The mapped QAOA layer as a quantum circuit + """ + + # Build the layer for the cost Hamiltonian + qaoa_cost_layer = QuantumCircuit(self._num_physical_qubits) + + # Applying Ising gates for single qubit rotations + for j in range(self._num_logical_qubits): + rotation_angle = 0 + for k in range(self._num_logical_qubits): + rotation_angle -= self._cost_matrix[j][k] + rotation_angle -= self._cost_matrix[k][j] + + if abs(rotation_angle) > 1.0e-14: + qaoa_cost_layer.rz(rotation_angle * cost_parameter, j) + + # Iterate over and apply gate layers + max_distance = max(rzz_layers.keys()) + current_layout = Layout() + for i in range(max_distance + 1): + # Set the current layout depending on the number of SWAP layers already applied + current_permutation = self.swap_strategy.composed_permutation(idx=i) + current_layout.from_dict( + { + self.initial_layout.get_physical_bits()[j]: current_permutation[j] + for j in range(self._num_physical_qubits) + } + ) + # Determine SWAP gates in upcoming SWAP layer + try: + upcoming_swaps = copy.copy(self.swap_strategy.swap_layers[i]) + except IndexError: + upcoming_swaps = [] + + # Get current layer and replace the problem indices j,k by the corresponding + # positions in the coupling map + rzz_layer = rzz_layers.get(i, {}) + rzz_layer = { + ( + current_layout.get_virtual_bits()[self.qubits[j]], + current_layout.get_virtual_bits()[self.qubits[k]], + ): rotation_angle + for (j, k), rotation_angle in rzz_layer.items() + } + + # Build a list of RZZ gates that overlap with next SWAP layer and should be applied at + # the end of the current RZZ layer to cancel as many CNOT gates as possible. + final_sublayer = {} + layer_new = {} + for (j, k), rotation_angle in rzz_layer.items(): + if (j, k) in upcoming_swaps: + final_sublayer[(j, k)] = rotation_angle + upcoming_swaps.remove((j, k)) + if (k, j) in upcoming_swaps: + final_sublayer[(j, k)] = rotation_angle + upcoming_swaps.remove((k, j)) + else: + layer_new[(j, k)] = rotation_angle + rzz_layer = layer_new + + # Build sub layers according to graph coloring or greedily if no graph + # coloring has been specified TODO: outsource this to a single function? + edge_coloring = self.swap_strategy.edge_coloring + if edge_coloring is not None: + sublayers = [{} for _ in range(max(edge_coloring.values()) + 1)] + for edge, rotation_angle in rzz_layer.items(): + sublayers[edge_coloring[edge]][edge] = rotation_angle + else: + sublayers = [] + while rzz_layer: + current_layer = {} + remaining_gates = {} + blocked_vertices = set() + for edge, rotation_angle in rzz_layer.items(): + if all([j not in blocked_vertices for j in edge]): + current_layer[edge] = rotation_angle + blocked_vertices = blocked_vertices.union(edge) + else: + remaining_gates[edge] = rotation_angle + rzz_layer = remaining_gates + sublayers.append(current_layer) + + # Apply sub-layers + for sublayer in sublayers: + for edge, rotation_angle in sublayer.items(): + (j, k) = map( + lambda vertex: self.initial_layout.get_physical_bits()[vertex], + edge, + ) + qaoa_cost_layer.cx(j, k) + qaoa_cost_layer.rz(rotation_angle * cost_parameter, k) + qaoa_cost_layer.cx(j, k) + + # Apply final sublayer + for edge, rotation_angle in final_sublayer.items(): + (j, k) = map(lambda vertex: self.initial_layout.get_physical_bits()[vertex], edge) + qaoa_cost_layer.cx(j, k) + qaoa_cost_layer.rz(rotation_angle * cost_parameter, k) + + # Add a swap if we are not at the end + if i < max_distance: + qaoa_cost_layer.cx(k, j) + qaoa_cost_layer.cx(j, k) + else: + qaoa_cost_layer.cx(j, k) + + # Apply SWAP gates + if i < max_distance: + for swap in upcoming_swaps: + (j, k) = map( + lambda vertex: self.initial_layout.get_physical_bits()[vertex], + swap, + ) + qaoa_cost_layer.cx(j, k) + qaoa_cost_layer.cx(k, j) + qaoa_cost_layer.cx(j, k) + + # Possibly reverse cost layer and apply mixer Hamiltonian to correct qubits + if reverse_ops: + qaoa_layer = qaoa_cost_layer.reverse_ops() + for i in range(self._num_logical_qubits): + qaoa_layer.rx(2.0 * mixer_parameter, i) + else: + qaoa_layer = qaoa_cost_layer + for i in range(self._num_logical_qubits): + idx = self.initial_layout.get_physical_bits()[ + self._swapped_layout.get_virtual_bits()[self.qubits[i]] + ] + qaoa_layer.rx(2.0 * mixer_parameter, idx) + + return qaoa_layer + + +class QAOASwapPass(TransformationPass): + """A transpiler pass for QAOA circuits.""" + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Replace all QAOAGates with a hardware efficient implementation of QAOA. + + Args: + dag: The DAG to run the transpiler on. + + Returns: + The DAG with :class:`~qiskit.circuit.library.n_local.qaoa_ansatz.QAOAGate`s + replaced by a hardware efficient implementation. + """ + + current_layout = self.property_set.get("layout", None) + if current_layout is None: + current_layout = Layout().generate_trivial_layout(*dag.qregs.values()) + + swap_strategy = self.property_set.get("qaoa_swap_strategy", None) + if swap_strategy is None: + warn(f"{self.__class__.__name__} did not do anything as no swap strategy was found.") + + return dag + + for node in dag.op_nodes(): + op = node.op + if isinstance(op, QAOAGate): + # Generate initial layout for the qubits the QAOA is run on + qaoa_layout = Layout() + qaoa_layout.from_dict({qubit: current_layout[qubit] for qubit in node.qargs}) + + # Create and insert the hardware efficient QAOA in the DAG + qaoa = HWQAOAAnsatz( + cost_operator=op.cost_operator, + reps=op.reps, + initial_state=op.initial_state, + mixer_operator=op.mixer_operator, + swap_strategy=swap_strategy, + initial_layout=qaoa_layout, + ) + + # Reassign the parameters to those of the original circuit. + rebound = qaoa.assign_parameters(op.params) + + dag.substitute_node_with_dag(node, circuit_to_dag(rebound.decompose())) + + mapping = self.property_set.get("qaoa_swap_layout", None) + if mapping is not None: + self.property_set["layout"] = Layout.from_intlist(mapping, *dag.qregs.values()) + + return dag + + +class SwapStrategyCreator(AnalysisPass): + """Create the swap strategy and mapping to physical qubits for QAOA. + + This class analyses the coupling map to first see if a line swap strategy can be used. + The best line is determined based on the fidelity of the two-qubit gates. If the backend + has enough qubits but no simple path can be found (i.e. no line) then a swap strategy + from the swap strategy library is used. + """ + + def __init__( + self, + backend: Backend, + two_qubit_gate: str = "cx", + use_fidelity: bool = True, + swap_strategy: Optional[SwapStrategy] = None, + swap_strategy_qubits: Optional[List[int]] = None, + ): + """ + Args: + backend: The backend that we will analyze. + two_qubit_gate: The name of the two-qubit gate of the backend. + use_fidelity: Whether or not to use the fidelity of the two-qubit gate + to determine the best path. By default this variable is set to True. + swap_strategy: The swap strategy to use. If this variable is not given (i.e. + the default) then the swap strategy will be created based on the problem + size and optionally the gate fidelity. If this variable is given then the + swap_strategy_qubits variable must also be given. + swap_strategy_qubits: A list of integers representing the physical qubits + to run on. If this list is None (the default value) then the qubits + will be determined based on the problem size and optionally the fidelity + of the two-qubit gates. + """ + super().__init__() + + coupling_map = backend.configuration().coupling_map + + if coupling_map is None: + raise QiskitError( + "Cannot create a swap strategy if the backend does not have a coupling map." + ) + + self._coupling_map = CouplingMap(coupling_map) + self._two_qubit_fidelity = {} + self._max_problem_size = backend.configuration().num_qubits + self._name = backend.name() + self._use_fidelity = use_fidelity + + props = backend.properties() + for edge in coupling_map: + self._two_qubit_fidelity[tuple(edge)] = 1 - props.gate_error(two_qubit_gate, edge) + + if swap_strategy is not None and swap_strategy_qubits is not None: + self._swap_strategy = swap_strategy + self._path = swap_strategy_qubits + elif swap_strategy is None and swap_strategy_qubits is None: + self._swap_strategy = None + self._path = None + else: + raise QiskitError( + "Either both swap_strategy and swap_strategy_qubits are None or neither are None." + ) + + def find_path(self, length: int) -> Optional[List[int]]: + """Find the paths of the coupling map with the appropriate length. + + Args: + length: The length of the simple path to find. + + Returns: + The best path that could be found. If no path was found then None is returned. + """ + + paths, size = [], self._coupling_map.size() + + for node1 in range(size): + for node2 in range(node1 + 1, size): + paths.extend( + rx.all_simple_paths( + self._coupling_map.graph, + node1, + node2, + min_depth=length, + cutoff=length, + ) + ) + if len(paths) == 0: + return None + + if not self._use_fidelity: + return paths[0] + + fidelities = [self.evaluate_path(path) for path in paths] + + return self.get_best_path(paths, fidelities) + + @staticmethod + def get_best_path(paths: List[List[int]], fidelities: List[float]) -> List[int]: + """Sort the paths according to their fidelity. + + Args: + paths: The paths on the qubits. Each sublist is a path. + fidelities: The fidelity for each path. + + Returns: + The paths sorted by fidelity. The highest fidelity path is at index 0. + """ + + return min(zip(paths, fidelities), key=lambda x: -x[1])[0] + + def evaluate_path(self, path: List[int]) -> float: + """Compute the fidelity of the path. + + This function uses a heuristic to compute the fidelity of the two-qubit gates on the given + path by multiplying the fidelity of all two-qubit gates on the path. + + Args: + path: The path as a list of qubits. + + Returns: + The fidelity of the path as the product of gate fidelities. + """ + if not path or len(path) == 1: + return 0.0 + + fidelity = 1.0 + for idx in range(len(path) - 1): + fidelity *= self._two_qubit_fidelity[(path[idx], path[idx + 1])] + + return fidelity + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """create a swap strategy on the coupling map for the given problem size. + + First, we try and find a line that can accommodate problems of the given size. + If no such line is found use the full swap strategy for the coupling map taken from + the library of swap strategies. The swap strategy and path are saved in the property + set of the pass manager. + + Args: + dag: the number of qubits in the dag determines the problem size. + """ + + if self._swap_strategy is None or self._path is None: + problem_size = dag.num_qubits() + + if problem_size > self._max_problem_size: + raise ValueError( + f"{self._name} can only handle problems up to size " + f"{self._max_problem_size}. Received {problem_size}" + ) + + self._path = self.find_path(problem_size) + + if self._path is None: + self._swap_strategy, self._path = get_swap_strategy(self._name, problem_size) + else: + self._swap_strategy = LineSwapStrategy(list(range(len(self._path)))) + + self.property_set["qaoa_swap_strategy"] = self._swap_strategy + self.property_set["qaoa_swap_layout"] = self._path + + return dag + + +class InitialQubitMapper(TransformationPass): + """Reorder the decision variables based on the swap strategy. + + This class iteratively reorders the decision variables in a PauliSumOp based on + a swap strategy to reduce the number of two-qubit operations. The swap strategy + defines a matrix of distances :math:`d_{i,j}` that corresponds to the distance + measured in number of swap layers between qubit :math:`i` and :math:`j`. For + example, adjacent qubits in the coupling map have a distance of 0 and next-nearest + neighbours have a distance of 1 if the next swap layer connects them. At each + iteration, the virtual qubit with the largest sum product of distances and pauli + pre-factors to the virtual qubits that have already been mapped to physical qubits + is the next qubit to be mapped. For this qubit, we then identify the unmapped + physical qubit in the coupling map that minimizes the sum-product of distances + and pauli pre-factors. + """ + + def permute_operator(self, cost_op: PauliSumOp, swap_strategy: SwapStrategy) -> PauliSumOp: + """Permute the Paulis in cost_op to minimize the number of CNOT gates when swapping. + + This is the main method of this class. It permutes the operators in the given + cost_op according to the distance matrix of the swap strategy. This helps reduce + the number of CNOT gates that will be needed when applying the swap strategy. This + is only needed for sparse cost operators. + + Args: + cost_op: The cost operator for which to create the mapping based on the + swap_strategy stored in the mapper. + swap_strategy: The swap strategy that provides the distance matrix. + + Returns: + The permuted cost operator. + + Raises: + QiskitError: if the cost operator is too large for the swap strategy. + """ + if len(swap_strategy.distance_matrix) < cost_op.num_qubits: + raise QiskitError( + f"The cost operator with {cost_op.num_qubits} qubits is too large " + f"for the swap strategy {swap_strategy}." + ) + + rotation_angles = self._map_rotation_angles(cost_op) + + # A mapping with the logical qubit as key and the physical qubit as value. + physical_mapping = dict() + + # The distance matrix between the qubits in the swap strategy. + distance_mat = swap_strategy.distance_matrix + + unmapped_virtual_qubits = set(range(cost_op.num_qubits)) + unmapped_physical_qubits = set(range(cost_op.num_qubits)) + + while unmapped_virtual_qubits: + + # Get the next virtual qubit to map using the sum of rotations to the mapped qubits. + v_qubit = self._get_next_qubit_to_map( + list(unmapped_virtual_qubits), physical_mapping, rotation_angles + ) + + # Find the physical qubit to which to map the virtual qubit. + p_qubit = self._min_distance( + v_qubit, + physical_mapping, + unmapped_physical_qubits, + rotation_angles, + distance_mat, + ) + + # Update state variables. + physical_mapping[v_qubit] = p_qubit + unmapped_virtual_qubits.remove(v_qubit) + unmapped_physical_qubits.remove(p_qubit) + + # reorder data to get a permutations list + permutation = [0] * cost_op.num_qubits + for v_qubit, p_qubit in physical_mapping.items(): + permutation[v_qubit] = p_qubit + + return copy.deepcopy(cost_op).permute(permutation) + + def _min_distance( + self, + qubit: int, + physical_mapping: Dict[int, int], + free_positions: Set[int], + rotation_angles: Dict[Tuple, float], + distance_mat: List[List[int]], + ) -> int: + r"""Find the physical qubit to which to map the virtual qubit. + + The cost function that is computed is + + .. math:: + + \sum_{j\in M} d_{v, v(j)}\theta_{ij}. + + Here, :math:`d_{v, v(j)}` is the swap distance between the physical node v + and the position of the physical qubit :math:`v(j)` to which decision variable + :math:`j` has been mapped. :math:`\theta_{ij}` is the angle between the + decision variable to map, i.e. :math:`i`, and the mapped decision variable :math:`j`. + The set :math:`M` is the set of decision variables that have already been mapped + to a physical qubit. + + Args: + qubit: The decision variable to map, i.e. :math:`i`. + physical_mapping: The dictionary of decisions variables (i.e. virtual qubits) as keys + that have already been mapped to physical variables as values. + rotation_angles: A dict connecting the decision variables to their corresponding + rotations. E.g. 4.0*ZIIZ has rotation_angles {(0, 3): 4.0, (3, 0): 4.0}. + distance_mat: The distance matrix from the swap strategy to use. + + Returns: + The index of the physical qubit to which the decision variable (i.e. the qubit) + argument will be mapped. + """ + smallest_cost, smallest_idx = np.Inf, None + + def calculate_cost(pos): + # Compute the sum in the cost function to minimize + cost = 0.0 + for v_qubit, p_qubit in physical_mapping.items(): + theta = rotation_angles.get((qubit, v_qubit), 0) + cost += distance_mat[pos][p_qubit] * theta + return cost + + return min(free_positions, key=calculate_cost) + + @staticmethod + def _map_rotation_angles(cost_op: PauliSumOp) -> Dict[Tuple, float]: + """Return the rotation angle between two qubits. + + Args: + cost_op: A sum of Paulis whose coefficients determine the rotation angles + that will be extracted and put into a dict. + + Returns: + A dictionary where the keys are the decision variable indices, e.g. (2, 3) + and the values are the coefficients of the corresponding pauli Z terms. + The returned dictionary contains keys (A, B) and (B, A) which both point + to the same rotation angle since the RZZGate is symmetric. + """ + + rotation_angles = dict() + + for pauli, coeff in zip(cost_op.primitive.paulis, cost_op.primitive.coeffs): + indices = tuple([idx for idx, char in enumerate(pauli) if str(char) == "Z"]) + + rotation_angles[indices] = coeff + rotation_angles[indices[::-1]] = coeff + + return rotation_angles + + def _get_next_qubit_to_map( + self, + unmapped: List[int], + mapped: Dict[int, int], + rotation_angles: Dict[Tuple, float], + ) -> int: + """Return the next virtual qubit to map to a physical qubit. + + Args: + unmapped: Qubits that have not yet been mapped to a physical qubit + in the initial layout. + mapped: The dictionary of decisions variables (i.e. virtual qubits) as keys + that have already been mapped to physical variables as values. + rotation_angles: A dict connecting the decision variables to their corresponding + rotations. E.g. 4.0*ZIIZ has rotation_angles {(0, 3): 4.0, (3, 0): 4.0}. + + Returns: + A list of sum of rotations between apped and unmapped qubits. + """ + + # If no qubit has been mapped we first consider the qubit with the highest + # number of connections. + if not mapped: + max_rotation_count = 0 + max_qubit = 0 + for idx1, qb1 in enumerate(unmapped): + + rotation_count = 0 + for idx2, qb2 in enumerate(unmapped[idx1 + 1 :]): + if (idx1, idx2) in rotation_angles: + rotation_count += 1 + + if rotation_count > max_rotation_count: + max_qubit = qb1 + + return max_qubit + + # Consider the following qubits using the largest som of rotations. + sum_of_rotations = [ + self._rotation_sum(qubit, list(mapped.keys()), rotation_angles) for qubit in unmapped + ] + + return max(zip(sum_of_rotations, unmapped), key=lambda x: x[0])[1] + + @staticmethod + def _rotation_sum( + qubit: int, other_qubits: List[int], rotation_angles: Dict[Tuple, float] + ) -> float: + """Compute the rotation sum between the given qubit and the other qubits. + + Args: + qubit: The qubit for which we compute the sum of all other rotation angles. + other_qubits: The list of qubits that have already been mapped to physical + qubits. + + Returns: + The sum of the rotation angles between the given qubit and the unmapped qubit. + """ + return sum(rotation_angles.get((qubit, other), 0) for other in other_qubits) + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Reorder the terms in the QAOA cost operator according to a swap strategy. + + This pass does not modify the circuit but simply reorders the terms in the cost + operator of any QAOA gates. + + Args: + dag: The DAG to run the transpiler on. + + Returns: + The DAG in which the :class:`~qiskit.circuit.library.n_local.qaoa_ansatz.QAOAGate`s + cost operators have been remapped. + """ + + swap_strategy = self.property_set.get("qaoa_swap_strategy", None) + if swap_strategy is None: + warn(f"{self.__class__.__name__} did not do anything as no swap strategy was found.") + + return dag + + for node in dag.op_nodes(QAOAGate): + op = node.op + op.cost_operator = self.permute_operator(op.cost_operator, swap_strategy) + + return dag + + +def swap_pass_manager_creator( + backend, + swap_strategy: Optional[SwapStrategy] = None, + swap_strategy_qubits: Optional[List[int]] = None, + use_initial_mapping: bool = False, +) -> PassManager: + """Create a swap strategy pass manager. + + Args: + backend: The backend for which to create the swap strategy pass manager. + swap_strategy: An optional swap strategy that lets users pass in swap strategies + instead of using the default methodology in the :class:`SwapStrategyCreator`. + swap_strategy_qubits: The list of physical qubits that are involved in the swap strategy. + This variable must also be given if the swap_strategy variable is not None. + use_initial_mapping: If True (the default is false), add an initial + mapping to the transpilation passes that will reorganize the Pauli operations in + the cost operator to reduce the number of two-qubit gates that the SWAP strategy will + implement. + """ + + if swap_strategy is not None and swap_strategy_qubits is None: + warn("swap_strategy will be ignored since swap_strategy_qubits is None.") + + basis_gates = backend.configuration().basis_gates + + swap_pm = PassManager() + swap_pm.append( + SwapStrategyCreator( + backend, + swap_strategy=swap_strategy, + swap_strategy_qubits=swap_strategy_qubits, + ) + ) + + if use_initial_mapping: + swap_pm.append(InitialQubitMapper()) + + coupling_map = CouplingMap(backend.configuration().coupling_map) + + swap_pm.append( + [ + QAOASwapPass(), + FullAncillaAllocation(coupling_map), + EnlargeWithAncilla(), + ApplyLayout(), + UnrollCustomDefinitions(std_eqlib, basis_gates), + BasisTranslator(std_eqlib, basis_gates), + Optimize1qGatesDecomposition(basis_gates), + ] + ) + + return swap_pm + + +def pulse_pass_creator(backend) -> PassManager: + """Create a pass manager for pulse-efficient transpilation. + + Args: + backend: The backend for which to create the passes. + + Returns: + The pulse efficient pass manager. + """ + rzx_basis = ["rzx", "rz", "x", "sx"] + # a collection of passes to build the pulse-efficient pass manager. + pulse_efficient_passes = [ + # Consolidate consecutive two-qubit operations. + Collect2qBlocks(), + ConsolidateBlocks(basis_gates=["rz", "sx", "x", "rxx"]), + # Rewrite circuit in terms of Weyl-decomposed echoed RZX gates. + EchoRZXWeylDecomposition(backend.defaults().instruction_schedule_map), + # Attach scaled CR pulse schedules to the RZX gates. + RZXCalibrationBuilderNoEcho(backend), + # Simplify single-qubit gates. + UnrollCustomDefinitions(std_eqlib, rzx_basis), + BasisTranslator(std_eqlib, rzx_basis), + Optimize1qGatesDecomposition(rzx_basis), + ] + return PassManager(pulse_efficient_passes) + + +def main(backend, user_messenger, **kwargs): + """Entry function.""" + # parse inputs + mandatory = {"operator"} + missing = mandatory - set(kwargs.keys()) + if len(missing) > 0: + raise ValueError(f"The following mandatory arguments are missing: {missing}.") + + # Extract the input form the kwargs and build serializable kwargs for book keeping. + serialized_inputs = {} + operator = kwargs["operator"] + + if not isinstance(operator, PauliSumOp): + try: + operator = PauliSumOp.from_list([(str(operator), 1)]) + except QiskitError as err: + raise QiskitError( + f"Cannot convert {operator} of type {type(operator)} to a PauliSumOp." + ) from err + + serialized_inputs["operator"] = operator.primitive.to_list() + + aux_operators = kwargs.get("aux_operators", None) + if aux_operators is not None: + serialized_inputs["aux_operators"] = [] + for op in aux_operators: + if not isinstance(op, PauliSumOp): + try: + op = PauliSumOp.from_list([(str(op), 1)]) + except QiskitError as err: + raise QiskitError( + f"Cannot convert {op} of type {type(op)} to a PauliSumOp" + ) from err + + serialized_inputs["aux_operators"].append(op.primitive.to_list()) + + initial_point = kwargs.get("initial_point", None) + serialized_inputs["initial_point"] = list(initial_point) + + use_initial_mapping = kwargs.get("use_initial_mapping", False) + serialized_inputs["use_initial_mapping"] = use_initial_mapping + + optimizer = kwargs.get("optimizer", SPSA(maxiter=300)) + serialized_inputs["optimizer"] = { + "__class__.__name__": optimizer.__class__.__name__, + "__class__": str(optimizer.__class__), + "settings": getattr(optimizer, "settings", {}), + } + + reps = kwargs.get("reps", 1) + serialized_inputs["reps"] = reps + + shots = kwargs.get("shots", 1024) + serialized_inputs["shots"] = shots + + alpha = kwargs.get("alpha", 1.0) # CVaR expectation + serialized_inputs["alpha"] = alpha + + measurement_error_mitigation = kwargs.get("measurement_error_mitigation", False) + serialized_inputs["measurement_error_mitigation"] = measurement_error_mitigation + + use_swap_strategies = kwargs.get("use_swap_strategies", True) + serialized_inputs["use_swap_strategies"] = use_swap_strategies + + use_pulse_efficient = kwargs.get("use_pulse_efficient", False) + serialized_inputs["use_pulse_efficient"] = use_pulse_efficient + + optimization_level = kwargs.get("optimization_level", 1) + serialized_inputs["optimization_level"] = optimization_level + + # select expectation algorithm + if alpha == 1: + expectation = PauliExpectation() + else: + expectation = CVaRExpectation(alpha, PauliExpectation()) + + # add measurement error mitigation if specified + if measurement_error_mitigation: + # allow for TensoredMeasFitter as soon as runtime runs on latest Terra + measurement_error_mitigation_cls = TensoredMeasFitter + measurement_error_mitigation_shots = shots + else: + measurement_error_mitigation_cls = None + measurement_error_mitigation_shots = None + + # Define the transpiler passes to use. + pass_manager = None + if use_swap_strategies: + pass_manager = swap_pass_manager_creator(backend, use_initial_mapping=use_initial_mapping) + + pulse_passes = pulse_pass_creator(backend) if use_pulse_efficient else None + + # set up quantum instance + quantum_instance = QuantumInstance( + backend, + shots=shots, + measurement_error_mitigation_shots=measurement_error_mitigation_shots, + measurement_error_mitigation_cls=measurement_error_mitigation_cls, + pass_manager=pass_manager, + bound_pass_manager=pulse_passes, + optimization_level=optimization_level, + ) + + quantum_instance.circuit_summary = True + + # publisher for user-server communication + publisher = Publisher(user_messenger) + + # dictionary to store the history of the optimization + history = {"nfevs": [], "params": [], "energy": [], "std": []} + + def store_history_and_forward(nfevs, params, energy, std): + # store information + history["nfevs"].append(nfevs) + history["params"].append(params) + history["energy"].append(energy) + history["std"].append(std) + + # and forward information to users callback + publisher.callback(nfevs, params, energy, std) + + # construct the QAOA instance + qaoa = VQE( + ansatz=QAOAAnsatz(operator, reps), + optimizer=optimizer, + initial_point=initial_point, + expectation=expectation, + callback=store_history_and_forward, + quantum_instance=quantum_instance, + ) + result = qaoa.compute_minimum_eigenvalue(operator, aux_operators) + + serialized_result = { + "optimizer_time": result.optimizer_time, + "optimal_value": result.optimal_value, + "optimal_point": result.optimal_point, + "optimal_parameters": None, # ParameterVectorElement is not serializable + "cost_function_evals": result.cost_function_evals, + "eigenstate": result.eigenstate, + "eigenvalue": result.eigenvalue, + "aux_operator_eigenvalues": result.aux_operator_eigenvalues, + "optimizer_history": history, + "inputs": serialized_inputs, + } + + return serialized_result diff --git a/qiskit_runtime/qaoa/qaoa_metadata.json b/qiskit_runtime/qaoa/qaoa_metadata.json new file mode 100644 index 0000000000..593589a6cf --- /dev/null +++ b/qiskit_runtime/qaoa/qaoa_metadata.json @@ -0,0 +1,118 @@ +{ + "name": "qaoa", + "description": "Qiskit Runtime QAOA program", + "max_execution_time": 18000, + "spec": { + "parameters": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "operator": { + "description": "The cost Hamiltonian, consisting of Pauli I and Z operators, whose smallest eigenvalue we're trying to find. The type must be a PauliSumOp.", + "type": "object" + }, + "optimizer": { + "description": "The classical optimizer used to update the parameters in each iteration. Per default, SPSA with automatic calibration of the learning rate is used. The type must be a qiskit.algorithms.optimizers.Optimizer.", + "type": "object" + }, + "initial_point": { + "description": "Initial parameters of the ansatz. Can be an array or the string ``'random'`` to choose random initial parameters. The type must be numpy.ndarray or str.", + "type": [ + "array", + "string" + ] + }, + "aux_operators": { + "description": "A list of operators to be evaluated at the final, optimized state. This must be a List[PauliSumOp].", + "type": "array" + }, + "reps": { + "description": "The number of QAOA repetitions, i.e. the QAOA depth typically labeled p. This value defaults to 1. This is an integer.", + "type": "integer", + "default": 1 + }, + "shots": { + "description": "The integer number of shots used for each circuit evaluation. Defaults to 1024.", + "type": "integer", + "default": 1024 + }, + "alpha": { + "description": "The fraction of top measurement samples to be used for the expectation value (CVaR expectation). Defaults to 1, i.e. using all samples to construct the expectation value.", + "type": "number" + }, + "measurement_error_mitigation": { + "description": "Whether to apply measurement error mitigation in form of a tensored measurement fitter to the measurements. Defaults to False.", + "type": "boolean", + "default": false + }, + "use_swap_strategies": { + "description": "A boolean on whether or not to use swap strategies when transpiling. This flag is set to True by default. If this is False then the standard transpiler with the given optimization level will run.", + "type": "boolean", + "default": true + }, + "use_pulse_efficient": { + "description": "A boolean on whether or not to use a pulse-efficient transpilation. This flag is set to False by default.", + "type": "boolean", + "default": false + }, + "optimization_level": { + "description": "The optimization level to run if the swap strategies are not used. This value is 1 by default. This is an integer.", + "type": "integer", + "default": 1 + }, + "use_initial_mapping": { + "description": "A boolean flag that, if set to True (the default is False), runs a heuristic algorithm to permute the Paulis in the cost operator to better fit the coupling map and the swap strategy. This is only needed when the optimization problem is sparse and when using swap strategies to transpile.", + "type": "boolean", + "default": false + } + }, + "required": [ + "operator" + ] + }, + "return_values": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "optimizer_time": { + "description": "The total time taken by the optimizer.", + "type": "number" + }, + "optimal_value": { + "description": "The smallest value found during the optimization. Equal to the ``eigenvalue`` attribute.", + "type": "number" + }, + "optimal_point": { + "description": "The optimal parameter values found during the optimization.", + "type": "array" + }, + "optimal_parameters": { + "description": "Not supported at the moment, therefore ``None``.", + "type": "null" + }, + "cost_function_evals": { + "description": "The number of cost function (energy) evaluations. This is an integer.", + "type": "number" + }, + "eigenstate": { + "description": "The square root of sampling probabilities for each computational basis state of the circuit with optimal parameters.", + "type": "object" + }, + "eigenvalue": { + "description": "The estimated smallest eigenvalue.", + "type": "number" + }, + "aux_operator_eigenvalues": { + "description": "The expectation values of the auxiliary operators at the optimal state.", + "type": "array" + }, + "optimizer_history": { + "description": "A dictionary containing information about the optimization process: the value objective function, parameters, and a timestamp. The type is Dict[str, Any].", + "type": "object" + }, + "inputs": { + "description": "A dictionary of the serialized input keyword arguments. The type is Dict[str, Any].", + "type": "object" + } + } + } + } +} diff --git a/qiskit_runtime/qka/__init__.py b/qiskit_runtime/qka/__init__.py new file mode 100644 index 0000000000..8531147df5 --- /dev/null +++ b/qiskit_runtime/qka/__init__.py @@ -0,0 +1,43 @@ +# This code is part of qiskit-runtime. +# +# (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. + +""" +Quantum Kernel Alignment modules +================================ + +.. currentmodule:: qiskit_runtime.qka + +Aux files +--------- + +The ``aux_file`` directory contains datasets for binary classification. + +KernelMatrix class +------------------ + +.. autosummary:: + :toctree: ../stubs/ + + KernelMatrix + +FeatureMap class +------------------ + +.. autosummary:: + :toctree: ../stubs/ + + FeatureMap + +""" + +from .kernel_matrix import KernelMatrix +from .featuremaps import FeatureMap diff --git a/qiskit_runtime/qka/aux_file/dataset_graph10.csv b/qiskit_runtime/qka/aux_file/dataset_graph10.csv new file mode 100644 index 0000000000..44ebf4efe6 --- /dev/null +++ b/qiskit_runtime/qka/aux_file/dataset_graph10.csv @@ -0,0 +1,200 @@ +-2.007916541292168455e-01,1.256050442151832869e-01,1.314111543464645260e-02,1.646477350505198833e-01,-3.560597642700872312e-01,-1.695353554892262138e-01,5.994882770776622377e-02,1.592180384397914317e-01,9.348020040604623770e-02,2.469030420366628009e-01,1.727334581588330087e-01,-2.454461286442135293e-01,-3.109430434520887854e-02,2.276514532517784073e-01,-1.263686379290062944e-01,-4.877100848583847126e-02,4.738988751082016115e-01,4.235600882163070846e-02,-2.479262827528456170e-01,1.403230798247892541e-01,1.000000000000000000e+00 +-3.619770471633777520e-01,3.102410201186359684e-01,1.955016471216194651e-01,2.519809753315865875e-01,1.292735649705571421e-01,6.078849113761808365e-02,4.964318280869732536e-02,-1.190043419084264448e-01,-8.907888847960093726e-02,1.806225748809974496e-01,-3.891721410299649880e-02,3.119682589033747622e-01,1.072508355964233162e-01,-1.099388113086208346e-01,-3.845260712817669124e-02,4.212519615885600144e-02,1.856383482499662785e-01,-3.290732894176815471e-01,1.326495303771342282e-01,3.258614622629292523e-02,-1.000000000000000000e+00 +-1.699567981135229555e+00,-2.499023652979625032e-01,-5.942634989765910314e-02,-1.490057110759272163e+00,-2.034813523011680669e-01,-1.341924090873996001e-01,-4.491966344218056156e-02,-2.196993563951041978e-02,2.403923643158739909e-01,1.826132765017626780e-01,1.266957737287774388e-01,-2.635429686288550122e-01,1.070721972466185806e-01,2.189779380592951075e-01,-4.680750353227767224e-01,-2.713874573953329872e-01,2.929498505299598099e-01,-1.069986603149688797e-01,-2.833521453233959764e-01,1.322852304793778744e-01,1.000000000000000000e+00 +-3.380223718184083292e-01,1.370041451716388914e-01,5.901130877775062622e-02,1.253411046206272805e-01,4.827749586157329409e-01,2.863104589419421808e-01,6.332412929986380723e-02,2.267424279624475625e-01,-5.708031373436329992e-02,-3.523595850569669063e-02,4.112363501220001050e-02,4.658254689076685717e-01,1.899918493528713614e-01,-1.686181006797268456e+00,-1.470504435177556246e+00,2.292900865442913383e-01,1.291532701981612796e-01,-1.561609060412599614e+00,-7.667967188191603112e-02,4.761345531711647094e-03,-1.000000000000000000e+00 +-2.806172170557401091e-01,2.345528404427950075e-01,2.786230084666336815e-01,8.324341648692490381e-02,-1.087067331237027423e-01,-2.470171436270433563e-01,-1.105264854755428350e-01,2.233875130486916993e-01,3.031562242994962908e-01,2.550889741263078214e-01,2.005462828553836174e-01,-1.498809094605016157e-01,7.245672564705890606e-02,-1.429262924191095685e+00,-1.782486420238048819e+00,1.298841118026229091e-01,3.343505737727016092e-01,-1.794784374883469757e+00,-2.153012737029798285e-01,2.977441142038910193e-01,1.000000000000000000e+00 +-2.346721068161321533e-01,1.194903595863296142e-01,5.917843371820045995e-02,2.835171835455025380e-01,1.699647496214566345e-01,-1.272769149921026299e+00,-1.789720273338611722e+00,2.264168273447128912e-02,-1.251693019916260696e-01,-1.355586874585725354e+00,-4.668978510920989561e-02,2.510663893922865375e-01,3.253702075238998903e-01,-2.664341538171773882e-01,-2.546299148618048624e-01,-2.764602557068271405e-01,1.857795514152297822e-01,-7.640354454908623860e-02,9.936511318800467230e-05,9.440223825573818006e-02,-1.000000000000000000e+00 +-1.814810242586567313e+00,-1.997525669651355473e-01,1.969548223903713202e-01,-1.583230326698438439e+00,-1.150166153676587671e-01,-9.435141913297197258e-02,-5.347941540267860117e-02,1.300284229724378848e-01,3.364188457449710512e-01,2.781038237105381006e-01,1.529703121157700851e-01,-2.680246024070674360e-01,2.148996979174693234e-01,-1.394628059165991196e+00,-1.484846598147639307e+00,2.773773581400869326e-02,2.998946494828771403e-01,-1.703850060811981759e+00,-2.422215832158954862e-01,1.761989570765227220e-01,1.000000000000000000e+00 +-3.068878993243701614e-01,1.458429151629054010e-01,7.900337902815834923e-02,2.413892675648690034e-01,2.008437205135009396e-01,-1.255590334176830858e+00,-1.321669830791672551e+00,3.592599513955692381e-02,-1.231808282789535053e-01,-1.429931887152628311e+00,-1.301734423857004441e-01,9.290559734769254785e-02,1.535010499455963795e-01,-1.627845519807247010e+00,-1.798953197751451683e+00,1.450263369791208456e-01,3.040604037053139530e-01,-1.647861822886954153e+00,7.039330985744604585e-02,8.906782256419291943e-02,-1.000000000000000000e+00 +-2.475185490887347184e-01,9.420921282229396954e-02,5.737282722007587543e-02,3.067577078253846609e-02,-2.072072091156420548e-01,-1.568386399799159558e-01,3.625790937428378241e-02,3.160429162302552664e-02,-5.688512450148203170e-02,7.932343149769234547e-02,2.075570007682833729e-02,-2.379648165439725571e-01,-1.027001489524781430e-03,1.311064025402823296e-01,-1.412349284243120673e-01,-1.631336636555825059e+00,-1.378349038814703276e+00,2.156114329793494822e-01,-1.942461747151418283e-01,-1.372603263961538644e+00,1.000000000000000000e+00 +-3.712681993563175520e-01,2.447203123167054906e-01,1.674824609253608432e-01,-1.446717305613078430e+00,-1.420563357980569430e+00,-3.609458750875077504e-01,1.540679229703752900e-02,-1.625051620109700057e+00,-5.736007283963888426e-02,9.804019182766415819e-02,-8.996342455644670832e-02,2.026924048741309825e-01,3.653067457918324212e-01,-2.225456451124414037e-01,-1.408978414783976507e-01,-2.002353048192771490e-01,1.324373334310313677e-01,-1.255958370812784719e-01,9.761506581143294947e-02,1.122444049740187882e-02,-1.000000000000000000e+00 +-1.814997372005986653e+00,-1.941450819739447553e-01,1.653741602290772494e-01,-1.588775077627082366e+00,-9.586166821127925508e-02,3.122259066613891632e-02,2.145107584268255707e-01,4.641439422694794426e-02,2.989727635230137182e-01,1.449741841347681748e-01,1.679038267000977636e-01,-5.138476561718791080e-02,1.807859838991984214e-01,1.237539920076329292e-01,7.541819841429758808e-02,-1.632146375789857107e+00,-1.312653953351071934e+00,1.741179444086803085e-01,-2.423301838396551833e-01,-1.451627771151446478e+00,1.000000000000000000e+00 +-1.913286986041110582e-01,2.450407650849422692e-01,3.545361380153044251e-01,-1.436025934901113299e+00,-1.358754408733316765e+00,-3.345361517269413998e-01,9.228629189717228731e-02,-1.627704820726677992e+00,-9.910154579885172010e-03,1.213180773019647829e-01,9.271637029225321402e-02,1.930591674110951339e-01,2.607463373998891720e-01,-1.584432208669410036e+00,-1.914545203619924862e+00,7.672603635086822083e-02,2.618090199653315131e-01,-1.797289647501532972e+00,6.244643382207429316e-02,1.646627836234521303e-01,-1.000000000000000000e+00 +-2.827169272843018111e-01,-8.938278041821995368e-02,8.689099131522518005e-02,8.801569791382071739e-02,-2.091786619535809799e-01,-1.932951335710459684e-01,-3.421476103753014197e-02,1.458947150913895707e-01,1.416774496008315898e-01,2.267086822419316361e-01,1.930543033835727951e-01,-2.612848042443172147e-01,1.645347232003737947e-01,-1.377589491351909334e+00,-1.684355193891810876e+00,-1.222844897880694059e+00,-1.274121514402086763e+00,-1.373542996575246589e+00,-3.515611588040133961e-01,-1.292597459201656251e+00,1.000000000000000000e+00 +-2.898068069005064751e-01,3.362530106259646878e-01,4.039937736397130053e-01,-1.292134742263436831e+00,-1.327694121309146658e+00,-1.781255923277610176e+00,-1.474615078949816471e+00,-1.707491897761346689e+00,-6.347522520753053066e-02,-1.556758761129486857e+00,3.139049175431805200e-02,1.615493989436921352e-01,1.323517093486887286e-01,-9.163479094230082833e-02,-8.476455173744942839e-02,-8.388291365994691995e-02,-6.105412411432364905e-03,-1.115762012557646732e-01,-2.509796522546856035e-02,4.234376927324820516e-02,-1.000000000000000000e+00 +-1.814790777736557059e+00,-9.654571037546932910e-02,-1.547101539374872714e-01,-1.492103527123674001e+00,-2.653854804365027853e-01,-1.556251496039434690e-01,-1.524839474212562607e-01,-6.663369304520916359e-02,2.793381062230014256e-01,7.889239734507461077e-02,2.769616381192322452e-01,-8.894663379787948188e-02,6.234700937376988494e-02,-1.340385343202203039e+00,-1.556828923430347178e+00,-1.453807041951613588e+00,-1.236379895728524581e+00,-1.475549128646258534e+00,-2.682858355304210130e-01,-1.285611067987255218e+00,1.000000000000000000e+00 +-3.908878157228444783e-01,2.113366908644658537e-01,2.228160196256315229e-01,-1.481040542017491690e+00,-1.383617867835357451e+00,-1.721604745865471298e+00,-1.414072410664353185e+00,-1.615445833161077926e+00,-4.576111109960588619e-02,-1.632679036459264932e+00,-1.234401773580307005e-03,2.811519385075775257e-01,2.014740758147050559e-01,-1.620445921842751158e+00,-1.724348080122484417e+00,1.510452929498979913e-01,1.141521430830667733e-01,-1.655542359199389191e+00,-3.369205777448615324e-02,7.371810551873218820e-02,-1.000000000000000000e+00 +-2.030544519688619043e-01,-1.406268088042871733e+00,-1.646983584535837597e+00,3.689334738254190782e-02,-3.421474950398201709e-01,-1.694136682326066179e+00,1.021401297592262059e-01,3.099079545410267714e-02,2.477327019364135852e-01,1.890797137836061625e-01,2.360554195655827359e-01,-3.439769556005214390e-01,1.309297254528933308e-01,3.071109644024896390e-01,-9.727248652127777617e-02,2.445132909854294545e-02,2.597117486340805859e-01,-2.995306108773132525e-01,-1.144873034107466298e-01,8.332311310990911379e-02,1.000000000000000000e+00 +-2.620082274319464610e-01,1.309241126975579095e-01,1.550173404465169380e-01,2.290351928486012434e-01,1.355830014974737097e-01,1.841756991634939666e-01,3.740495859304824450e-02,1.432554810241620236e-01,-1.318531366877148947e-01,-5.250125578672842908e-02,-1.872262906681710498e-02,2.208668508096660876e-01,1.719656748930894485e-01,-2.664136995765063742e-02,-3.048994777487798458e-02,-1.554169790273430829e-01,3.189090560269032260e-01,-1.618584889809290051e+00,-1.417869631432152167e+00,2.926453270514067784e-02,-1.000000000000000000e+00 +-1.906075541058226630e+00,-1.684024007286910596e+00,-1.340778937578472885e+00,-1.469986073671453619e+00,-2.579063985079173560e-01,-1.619864788609417738e+00,-1.533062362767425024e-01,1.024990390979742955e-01,2.722139247877681245e-01,3.416922464760236977e-01,1.998637733527559024e-01,-1.085948848370048153e-01,2.513993049009481395e-01,3.083956725607817084e-01,4.168154406519475330e-02,-1.614654912656046104e-01,2.233822286873882179e-01,-1.082611399000658892e-01,-3.102175660124854017e-01,2.767709743371943865e-01,1.000000000000000000e+00 +-1.403865864129003604e-01,1.399259893403922705e-01,1.822987714289338868e-01,2.233337833692906893e-01,2.711380196075012794e-01,4.136960628999039047e-01,-1.278568634968769296e-02,-7.653098140936862026e-02,-2.572331698025254881e-02,1.707020466287472726e-01,2.065920513285800508e-01,1.183980052005483008e-01,1.075900892651548701e-01,-1.555623109767140466e+00,-1.909353313594174706e+00,7.389214672735844314e-02,6.165284928008352150e-02,2.672516810167926971e-02,-1.437578316133034884e+00,1.044033025676337212e-02,-1.000000000000000000e+00 +-3.408276321069046988e-01,-1.146037249935432101e+00,-1.501258508232188316e+00,2.618804000103412688e-03,-3.833992773836243928e-01,-1.786375783453783894e+00,2.824428717436953257e-02,5.969626703842215165e-02,3.398492171004255380e-01,3.196012159857406498e-01,1.573379930471782706e-01,-2.098223058925809681e-01,1.593055053841970214e-01,-1.347342479348194288e+00,-1.674166194778130334e+00,1.528908359522206106e-01,8.505383672976472853e-02,-1.729846339704632197e+00,-2.337434662739074076e-01,8.752659306713833631e-02,1.000000000000000000e+00 +-1.412783597316017437e-01,4.731801517553746450e-02,2.343150309962709898e-01,2.573249258932783778e-01,1.910417264810912175e-01,-1.448915033225833371e+00,-1.668482896109561464e+00,-2.185696090003557968e-01,-2.576158986434173154e-02,-1.564639023958924513e+00,-3.033109236920639856e-02,1.986130184165910417e-01,2.692243894750914746e-01,-2.989017527663618418e-02,-2.447858565867271352e-01,-1.546116379421747788e-01,1.911562244413266831e-01,-1.761274715944235458e+00,-1.558771609949650250e+00,1.185904653993593988e-01,-1.000000000000000000e+00 +-1.869606538605044443e+00,-1.742161305177921893e+00,-1.705962237545518567e+00,-1.680726774343433316e+00,-2.857918908775195499e-01,-1.933229486682525478e+00,-7.713614059712470272e-02,-7.657536195635973597e-02,1.924996180568824267e-01,3.080269644621186353e-03,2.458123927851449952e-01,-2.384431090062408276e-01,4.062257989091383470e-02,-1.269543731853680413e+00,-1.805576190862282493e+00,1.471755939521104672e-01,5.569043668847070694e-02,-1.617454615027889675e+00,-2.240814913791160934e-01,8.825325484191345882e-02,1.000000000000000000e+00 +-9.196904305524630785e-02,5.969966196298605310e-02,1.086359809752615313e-01,2.512606763871440552e-01,2.094744769886715818e-01,-1.303860010467098540e+00,-1.617071325959459349e+00,4.671505936492224842e-02,1.082264663551268935e-02,-1.347863119666699250e+00,-3.034928686809768017e-04,2.294002941270299745e-01,1.181027073768831509e-01,-1.664669319623928612e+00,-1.804692453724187073e+00,8.403241356683452401e-02,1.735037831530633623e-01,5.715596369227590734e-02,-1.544565301931799528e+00,-3.264092251927003946e-02,-1.000000000000000000e+00 +-1.555022682862563510e-01,-1.125508257373301069e+00,-1.501457806107648896e+00,-7.326900178082657034e-02,-1.295022601664582163e-01,-1.531828174481691507e+00,-6.302335743966225312e-02,-1.634728460917521109e-01,-1.367145896989113707e-02,1.641630582463626531e-01,2.708179693392485099e-01,-3.283178875772138072e-01,3.473293642587648167e-01,3.770264645556749006e-01,-1.048516243520047075e-01,-1.735934660954378206e+00,-1.405843359387919600e+00,2.119896565479701955e-01,-2.917055976828555330e-01,-1.556421047832049043e+00,1.000000000000000000e+00 +-1.535012995537096414e-01,1.646959501643460166e-01,2.722442285579929622e-01,-1.675350761225579799e+00,-1.429210405398552153e+00,-1.925582100503623051e-01,1.795411376065435272e-01,-1.719388905193311334e+00,-1.287123594519922753e-01,-1.698818447393583841e-02,-4.319585086121631445e-03,2.068592938283637661e-01,6.763226153019955000e-02,-1.143541532515680720e-01,-2.263818919984550704e-01,-2.028328628885033014e-02,1.711518704530328727e-01,-1.576459216968581600e+00,-1.535806894782790755e+00,3.407970358393891397e-02,-1.000000000000000000e+00 +-1.786388176514004167e+00,-1.685889696824095507e+00,-1.608441581101702322e+00,-1.665274141382779849e+00,-3.107298980188865078e-01,-1.703074441527982597e+00,-8.020214110244330841e-02,1.159612282370702380e-01,2.790597354042892442e-01,1.195713541401621227e-01,1.827992363200610093e-01,-2.172429686024237583e-01,-6.056191698649612820e-02,2.799762459115179847e-01,-1.049038409106583247e-01,-1.702234460492283885e+00,-1.309815879622063362e+00,6.573030944695713862e-02,-1.000791266829980791e-01,-1.361385183472438865e+00,1.000000000000000000e+00 +-1.407177086790832587e-01,2.852591968528649380e-01,2.951756655992362477e-01,-1.449733370168583058e+00,-1.449509169095344108e+00,-1.880505444262600412e-01,1.166729570154840523e-01,-1.522909106459823469e+00,-2.239815175130627223e-01,8.318864492281358158e-02,2.137627597733297857e-01,1.992375630005557563e-01,4.048339239098861508e-01,-1.739775740934404125e+00,-1.837135735844621198e+00,2.133512564098599407e-01,3.844720367327983945e-01,9.980635896652981232e-02,-1.400939648441999807e+00,-3.883190660159061003e-02,-1.000000000000000000e+00 +-2.126020741230422451e-01,-1.311872790849536230e+00,-1.391669019449700340e+00,-7.875753174027555481e-04,-2.396346173070124375e-01,-1.754607623443531939e+00,-1.075734915011122839e-01,1.146509379456045127e-02,2.952091447636051025e-01,1.383247610116168369e-01,2.415999236601309441e-01,-9.295107545531325344e-02,3.205064148694648785e-02,-1.377446146155375084e+00,-1.696033905545283549e+00,-1.524333008493155894e+00,-1.431227615493637861e+00,-1.586872455940936399e+00,-2.421020614860027176e-01,-1.465509609927389656e+00,1.000000000000000000e+00 +-9.756487538296562678e-02,1.905828894672605966e-01,1.843750990526454858e-01,-1.360775204799180793e+00,-1.388809615833678679e+00,-1.758455596000101151e+00,-1.598231564606595656e+00,-1.603536979723443778e+00,-9.221819699563088557e-02,-1.475907296959872328e+00,4.609333441812505511e-02,2.021473772991954077e-01,1.269652458713305399e-01,-2.684483074984664319e-01,-1.962621436883517090e-01,-7.669821748926668181e-02,2.410952035134545368e-01,-1.701232758668220768e+00,-1.621400018873538684e+00,1.450006659205423376e-01,-1.000000000000000000e+00 +-1.919357411192960550e+00,-1.746523422285083393e+00,-1.589260229764755339e+00,-1.442571556730520399e+00,-2.789995926641056778e-01,-1.764857351249993656e+00,1.630000458435644228e-01,6.571362908834758182e-03,3.549137380973362843e-01,1.399597894133332576e-01,3.685141050889269021e-01,-3.130230034828292984e-01,3.053191735768333581e-01,-1.308207621013695476e+00,-1.577611713332105481e+00,-1.422370698021114377e+00,-1.478454732679892025e+00,-1.350695461184106438e+00,-2.198106740649748980e-01,-1.431188114865378891e+00,1.000000000000000000e+00 +-1.959723628626568459e-01,1.167913563581796932e-01,1.824795050519394946e-01,-1.511100532387372741e+00,-1.362183845710490449e+00,-1.920632789045093380e+00,-1.579189158682562599e+00,-1.484211511568479969e+00,-5.702889149716390549e-02,-1.454884880955336079e+00,6.729963140093969676e-02,3.690251353496150077e-01,2.324761940787350434e-01,-1.767268937057435263e+00,-1.766247949277564899e+00,1.532858233068019604e-01,1.552477914085325650e-01,-4.242737087494632964e-03,-1.555376670312583531e+00,-1.315702222326894377e-01,-1.000000000000000000e+00 +1.186726607285704116e-02,4.155599789751587281e-01,-1.251641913720423493e-02,-6.365542085251635340e-02,-1.321759374937407638e-01,-1.401692109535647712e-01,-4.792549716968761558e-02,-1.717425529678146212e+00,-1.346413395291601534e+00,-1.262823367737433700e-01,2.916611676991012203e-01,-1.729744498154153209e+00,1.027791152745445857e-01,1.846903559336730005e-01,-1.720817935394969300e-02,-1.813911094811911096e-01,1.743076865679060172e-01,-9.950741364948334500e-03,-4.235305658076960467e-01,1.543634301802584152e-01,1.000000000000000000e+00 +-1.839914152790248725e+00,-2.990889535071097338e-01,1.680357158788539640e-01,-1.447544727917205121e+00,2.176590145518261243e-01,1.325996131777733567e-01,1.390231493227850612e-01,-3.977803235383932379e-02,-1.532318405957663887e-02,1.224971194701602178e-01,3.141260429582210523e-01,2.573758769608737174e-01,2.382882287407745159e-01,-8.074346680799386611e-02,-2.279345605647102313e-01,-1.086430140186502163e-01,4.225392268433366127e-01,-1.186557328974662678e-01,-9.009329746143089623e-02,9.710452121918558555e-02,-1.000000000000000000e+00 +-1.612823155584582668e+00,-1.492408620970711031e-01,-2.927947714038789395e-02,-1.581898585174331684e+00,-2.639617439540696275e-01,4.074645484559386022e-02,-2.427247458169240826e-02,-1.681546120251358634e+00,-1.354123195036015304e+00,-8.456689840799687385e-02,1.220819181697277850e-01,-1.756485518145850966e+00,1.962228551288944522e-01,1.859644055105026117e-01,-1.552383657208578904e-01,-6.000494554658519775e-02,1.782292462513375697e-01,-4.014661907783578787e-02,3.379121773648191551e-02,1.016106829555737945e-01,1.000000000000000000e+00 +-1.780081046958263746e+00,-4.192030499949445099e-02,1.515788495426312610e-01,-1.427950548919541873e+00,2.592200930100108658e-01,1.312811868218668465e-01,1.718496148904553184e-02,-5.089705078785578679e-03,-1.266770518198281625e-01,1.002672490239872038e-01,4.249914927660260883e-02,2.590657667105403239e-01,2.273463138642181913e-01,-1.734370842420827730e+00,-1.651225611069535582e+00,-2.144473951662861921e-02,5.717543232942556841e-02,-1.556967000434823500e+00,-3.302190069655809579e-02,1.313883514775121553e-01,-1.000000000000000000e+00 +-1.336939134088614978e-01,2.876079839638858715e-01,2.395292122218867226e-02,7.718235769613168706e-04,-1.210079550689721201e-01,-6.948296810014226166e-02,1.107304529536503046e-01,-1.449098811951648802e+00,-1.416850769923243991e+00,-1.894821220905087145e-01,2.056007501673507987e-01,-1.629376976711315095e+00,2.652108869293851034e-01,-1.349109149383652451e+00,-1.626720009842021808e+00,9.262742226531375800e-02,1.952059358058028382e-01,-1.777446761957121035e+00,-1.366881397742658599e-01,1.802715457876161997e-01,1.000000000000000000e+00 +-1.796272690921347559e+00,-1.774123902044983658e-01,2.670581478373795781e-01,-1.278963068583510942e+00,1.469453996846848132e-01,-1.414317002840989490e+00,-1.376532910749186600e+00,1.581883384851158481e-01,-2.392557560679413331e-02,-1.485883854601798282e+00,1.397769915317580991e-01,3.184880880991732832e-01,2.909126178028541565e-01,5.254223392782449154e-02,-1.501833053874064572e-01,-1.967275627066273347e-01,3.699415999172078195e-01,-1.406049116031088952e-01,6.622133603538822577e-02,1.961873639760809973e-01,-1.000000000000000000e+00 +-1.785220638062096077e+00,-2.295243944030855021e-01,3.824649826658470253e-02,-1.648181044644347892e+00,-2.742777977271766976e-01,-2.575974150183087952e-01,-1.566125524681920636e-02,-1.429465606904201769e+00,-1.281389396059050778e+00,-2.009397256986373004e-01,2.219921338796986554e-01,-1.587302010170268396e+00,1.701491631286058959e-01,-1.451422498647342074e+00,-1.629706576250242733e+00,4.765660089118081699e-02,1.513431476593253278e-01,-1.744954124120697614e+00,-3.000077540398696230e-01,1.473663347261450762e-01,1.000000000000000000e+00 +-1.915984716237936869e+00,-2.334096645682603444e-01,2.969182944477763986e-01,-1.462364225027299458e+00,-9.038357273379871160e-04,-1.501601560571436567e+00,-1.569491762709352756e+00,-1.413126429675328095e-03,-2.823024453240063208e-02,-1.627235235307024297e+00,1.158073992136935126e-01,2.766491873201362139e-01,1.606535465287846354e-01,-1.612968800083773635e+00,-1.704458739669437284e+00,1.641455785950739488e-01,3.710519682415764908e-01,-1.704170907609168450e+00,-4.815258597647618932e-02,-4.618627193132339437e-02,-1.000000000000000000e+00 +-1.432041439121052584e-01,1.176013730967366833e-01,1.209900020741062621e-01,-4.888365609557257518e-04,-2.535302243252841592e-01,-2.654853813955358743e-02,7.763789683125185692e-02,-1.745798948827284480e+00,-1.583027137084846103e+00,-1.364820093782884713e-01,2.849165470997524485e-01,-1.641591214804483334e+00,9.071169690031330446e-02,2.004624515894348757e-01,-8.350256926870230911e-02,-1.665780049054627288e+00,-1.480118862690187775e+00,1.138372271329863294e-01,-4.213227756064114038e-03,-1.480969809958756933e+00,1.000000000000000000e+00 +-1.947212966606190365e+00,-1.109567050567978608e-01,9.189978066414331015e-02,1.377949703620761357e-01,-1.358465210982832128e+00,-1.191888379927300290e-01,1.348297913431806966e-02,-1.628476840873991360e+00,8.208056027268331722e-03,5.754932724803086402e-02,1.502609168270740425e-01,9.015234033881530662e-02,2.712580623103985622e-01,-7.811883730421693983e-02,-1.209705944878042005e-01,-1.897155308102837223e-01,3.321993044877320500e-01,-1.423230162477970018e-01,-4.922241589906117820e-04,1.276779005151182922e-01,-1.000000000000000000e+00 +-1.643142401212486892e+00,-4.452179782252499440e-02,2.761625880228188445e-02,-1.444505141847691521e+00,-1.864488011141737789e-01,4.235134262320812415e-02,-1.060818489425193190e-01,-1.530886704442798951e+00,-1.290949591393974583e+00,-1.578542625770643082e-01,2.071111419625968342e-01,-1.736294468528262191e+00,1.881918380973065519e-01,2.112289351330886022e-01,-5.637226055998517671e-02,-1.734874621224534508e+00,-1.383487644273780015e+00,1.199881250258862941e-01,-1.480144127027813172e-01,-1.548442094397331381e+00,1.000000000000000000e+00 +-1.849688848902060645e+00,-4.571354376676887987e-02,8.791423855935898146e-02,9.320985568107194308e-02,-1.209440457784467071e+00,-2.541410595060626942e-01,1.167608598191240132e-01,-1.472532643300827893e+00,-1.303253406076957110e-01,-4.200988326309663956e-02,-2.265506228267986644e-01,1.786626037264705258e-01,2.812036326502925787e-01,-1.662884083493961329e+00,-1.847554130340076650e+00,3.404476020084349908e-01,2.714841486400764170e-01,-1.642850792893414846e+00,-1.127204049092180166e-01,5.515944350723923917e-02,-1.000000000000000000e+00 +-1.059080862694527225e-01,2.413223144436135448e-01,1.188793573122913783e-01,2.759458307752131190e-01,-3.155105305463620224e-01,-4.618161741368533718e-02,-1.787389365572205069e-01,-1.675692373916033651e+00,-1.367666161849348638e+00,-2.735719631365116644e-01,2.681588822287698237e-01,-1.810320595153056988e+00,2.234518121411552816e-01,-1.309036186758603559e+00,-1.629338507535421199e+00,-1.586640938538630419e+00,-1.252224138790772212e+00,-1.510137892836192597e+00,-7.003442782371488429e-02,-1.457702130539146435e+00,1.000000000000000000e+00 +-1.825734321933270454e+00,-1.988173085426351294e-01,2.184221411006121993e-01,1.520609502758123732e-01,-1.243887567058084453e+00,-1.710232011557398790e+00,-1.424856931155371820e+00,-1.590944974346635732e+00,7.050679230094457350e-02,-1.371683718348912562e+00,-6.861154885317624630e-02,1.581601008181294232e-01,2.681787861269692708e-01,-1.504471581425083637e-01,-3.471875534253440754e-02,-9.232204486171889657e-02,1.687579589557818283e-01,-1.528039563690852809e-03,2.036078932836379307e-01,1.589408223412197008e-01,-1.000000000000000000e+00 +-1.821739796368849973e+00,-1.186406610554362334e-01,1.657895857813671547e-01,-1.623415940529338908e+00,-2.527373807759313307e-01,-4.807783132028257272e-02,-1.885058399457829187e-02,-1.612100802806384348e+00,-1.318830896830129662e+00,-2.655886226639870840e-01,2.087351429592352869e-01,-1.836812016330601871e+00,5.679997717382348488e-02,-1.326177945238083744e+00,-1.741631677579875470e+00,-1.552712102848185394e+00,-1.450288170305659596e+00,-1.572891868987263297e+00,-1.505080380159262154e-01,-1.401630510513969696e+00,1.000000000000000000e+00 +-1.925496555102242491e+00,-1.906305748726313254e-01,1.280547991103812289e-01,2.654419405709938506e-01,-1.239396153611779461e+00,-1.881188922805395958e+00,-1.387309755378072440e+00,-1.618537046170873017e+00,-4.292296268015079880e-02,-1.684758651153038134e+00,1.233955963793083588e-01,3.570822832340531106e-01,1.706324194092417867e-02,-1.774329777974420486e+00,-1.659496535286899777e+00,8.980207268961742928e-02,1.570836997909211830e-01,-1.883374127297818390e+00,-8.317299220505475210e-03,1.339917436991019395e-01,-1.000000000000000000e+00 +-4.604935331407342125e-02,-1.234022535552339317e+00,-1.489861091426315731e+00,-4.913043052586447967e-03,-2.590156368024213451e-01,-1.662104163159621617e+00,-5.207369989469273069e-02,-1.665437349206309259e+00,-1.229647888137037626e+00,-2.130412650900186111e-01,2.536501330803943577e-01,-1.787062084880876034e+00,6.970503853135476269e-02,3.262479990769734384e-01,-1.865786699508110991e-01,9.903207563649846223e-03,1.576147272749244821e-01,2.427976713302304468e-03,-1.703172656802548202e-01,8.404738727319956137e-02,1.000000000000000000e+00 +-1.871247213242097862e+00,-1.585637251903615763e-01,6.992527411208426802e-02,-1.475375407303459063e+00,-6.213881796473447183e-02,-7.554214817526197501e-02,2.442784591842216690e-01,2.511980873992987126e-02,9.329472155898771435e-03,1.454098576025833478e-02,-4.576900179080329450e-02,1.619136782862597157e-01,6.664832475728971195e-02,-1.353540837371562944e-01,-1.792257851943249436e-01,3.541029725012719009e-02,1.706196530629354724e-01,-1.548850570319505904e+00,-1.566653235345892847e+00,-9.476332446157373357e-02,-1.000000000000000000e+00 +-1.664312032916881012e+00,-1.792424242934206102e+00,-1.550040679881295125e+00,-1.722628866135070158e+00,-3.358656399764347045e-01,-1.609050084232439115e+00,-3.260669242273393081e-02,-1.399839822461237615e+00,-1.517878066986744434e+00,-1.829662077058151481e-01,3.590013195660626821e-02,-1.872548082063439701e+00,6.671205940344417107e-02,-3.332763700905849724e-02,-1.357451803972037074e-01,-8.369038624500416135e-02,2.200551866090619257e-02,-1.573374340703240226e-01,-1.710462813366873946e-01,1.267990236431605355e-01,1.000000000000000000e+00 +-1.810067284149239564e+00,-3.578680540845750047e-01,1.076401651684563421e-01,-1.298810633698214367e+00,1.221107915355371665e-01,2.726840583892822800e-01,1.445993161010468431e-03,-1.968125426327854144e-01,-2.781251465871359407e-01,1.331973805238755859e-01,-1.338553699277940934e-01,1.552493371473668837e-01,1.242063675351776919e-01,-1.747457590161428653e+00,-1.886239978448203347e+00,1.475060927519487863e-01,1.903783085105796857e-01,-2.908784561962698412e-01,-1.347455131304474696e+00,-2.255556498385771214e-01,-1.000000000000000000e+00 +-2.351463841078031136e-01,-1.356221143936683804e+00,-1.548648917862450070e+00,-1.615190690304739787e-02,-2.329305829899785962e-01,-1.563544415731793702e+00,9.712065439353889662e-02,-1.691415670496308588e+00,-1.388081797844022081e+00,-1.632355704138472863e-01,2.233080087874433906e-01,-1.875345470231006839e+00,-1.420174321688624786e-02,-1.478479012048540042e+00,-1.703585643539405226e+00,9.877765948046887678e-02,1.713826636563931527e-01,-1.684931314778971290e+00,-1.740550633523509361e-01,9.656289639812394576e-02,1.000000000000000000e+00 +-1.807051206113242658e+00,-1.702030573975785166e-01,1.457697719140259862e-01,-1.401432021185573307e+00,1.345289163726789416e-01,-1.356331314153615741e+00,-1.633447265801356130e+00,1.312671853011658854e-02,-2.688291691020219809e-02,-1.553230978892023328e+00,1.363204035600718012e-02,2.054077132999896360e-01,1.865197473453200638e-01,-8.720626685161123048e-02,-1.437604573095554172e-01,-1.211220257470444622e-01,1.462310180151815686e-01,-1.580157720731842019e+00,-1.349661545793155115e+00,-7.460394207961704161e-02,-1.000000000000000000e+00 +-1.931195335870454999e+00,-1.835012547290038487e+00,-1.563035808850954167e+00,-1.365951090765915588e+00,-2.419776476979298985e-01,-1.507160458977945705e+00,-8.698048833069951802e-02,-1.647913694272572949e+00,-1.427110558973703647e+00,-6.158962399442696745e-02,2.602267054057313400e-01,-1.645387388779374405e+00,1.823394304296815649e-01,-1.178329356963998098e+00,-1.590525171370741120e+00,8.104795490439978267e-02,8.563665889620825089e-02,-1.774907163470250460e+00,-2.473909510286317892e-01,1.307471694166303378e-01,1.000000000000000000e+00 +-1.861598019139751425e+00,-6.684922331319176347e-03,1.222846918474984612e-01,-1.346678003138850332e+00,4.520385029215109873e-02,-1.267055205377054738e+00,-1.427484046466195089e+00,-1.127116818049106323e-01,-3.035474687264514554e-02,-1.582916251647630279e+00,3.008096301293413516e-02,1.793840427488923583e-01,3.840534720938503233e-01,-1.621601357089720974e+00,-1.602307822388481906e+00,1.143111850867933044e-01,1.964837344560850796e-01,-6.220973587830265494e-02,-1.465073997817349927e+00,-7.554554908694857684e-02,-1.000000000000000000e+00 +-1.678736409827504550e-01,-1.397225422498413305e+00,-1.365152865990226561e+00,-1.024009527884461868e-01,-1.690446713422619207e-01,-1.516140030873107447e+00,9.681822913903759265e-02,-1.476257944029218949e+00,-1.411731881032226577e+00,-1.703173430826560031e-01,7.916337182364427405e-02,-1.768682251963919372e+00,7.807644879584546560e-03,2.850324494224795169e-01,-2.784570630307360573e-02,-1.552327257047553477e+00,-1.278326320403236460e+00,1.918599970261308241e-01,-1.350110322304288180e-01,-1.295089347496607868e+00,1.000000000000000000e+00 +-1.698609511154017593e+00,-1.719161528378636561e-01,1.470416765064267994e-01,2.653323661514429554e-01,-1.364127946851214856e+00,-2.333175690266216240e-01,-7.861458406690191580e-02,-1.426970427975007283e+00,1.117479189963837688e-01,9.526638619662716745e-02,2.269126506552598399e-01,2.995679068958879143e-01,1.814027885997889222e-01,1.185629160547931049e-02,-1.884169765307397992e-01,-1.145857514130308175e-01,3.129948381144793679e-01,-1.723617348532140880e+00,-1.384166647916821979e+00,-1.045716089535642102e-02,-1.000000000000000000e+00 +-1.658897420735486294e+00,-1.620726660547902265e+00,-1.660113144619751901e+00,-1.460868895984654303e+00,-2.255437947025988321e-01,-1.744884626955741647e+00,-1.362731967233296315e-01,-1.457680464175254986e+00,-1.303540666045219121e+00,-1.718893334632473346e-01,2.164523847625174358e-01,-1.884134174199312284e+00,1.430050120792410451e-01,1.974210530718532863e-01,-1.147592315053487799e-01,-1.699573877831422886e+00,-1.438276927437653452e+00,2.286490534459723056e-01,-3.407135222878493463e-01,-1.631851369298568777e+00,1.000000000000000000e+00 +-1.750902203433937432e+00,-1.890080437519559564e-01,1.785743931325385614e-01,2.414132558003883045e-01,-1.517025238295976575e+00,-3.302434795072037299e-02,7.619542695607210703e-03,-1.543139691965131455e+00,-2.701194395846782115e-01,1.521473262932916681e-01,-3.177843420423827336e-03,2.443282642050864373e-01,7.253439665310934537e-02,-1.493056011791469295e+00,-1.759651362925155338e+00,1.136859516285110977e-01,2.436539507012564498e-01,-1.175244628411053344e-01,-1.509998795837532715e+00,-9.186531512934625887e-02,-1.000000000000000000e+00 +-1.417466391479324517e-01,-1.346903524100661720e+00,-1.530929088287875217e+00,-8.903360448265199012e-02,-2.719380145026851214e-01,-1.783814970375273479e+00,7.056489037692566940e-03,-1.350262034702866876e+00,-1.405801340336546135e+00,-1.825652336646692853e-01,2.381831403450938855e-01,-1.639463560709983003e+00,1.586699744579586624e-02,-1.426067408359480426e+00,-1.774135337473782048e+00,-1.263571548561680746e+00,-1.423664858305446090e+00,-1.341604123953090477e+00,-1.745310855853467458e-01,-1.260971454265339631e+00,1.000000000000000000e+00 +-1.919642635345555792e+00,-1.214332625549610356e-01,2.029412211540288224e-01,1.199843114974788738e-01,-1.335700473640089481e+00,-1.815504579463949719e+00,-1.513510260756883996e+00,-1.745957621058216835e+00,-1.207211194117849551e-01,-1.306597022005618225e+00,-1.397984640592999372e-01,2.918508859246253251e-01,1.222937644932370754e-01,-4.161281462589887237e-02,-2.428571363426271223e-01,-2.448264522598121717e-02,7.152417885188167013e-03,-1.717509711129774574e+00,-1.641718810659424932e+00,-6.696670886160294156e-02,-1.000000000000000000e+00 +-1.961262659443256950e+00,-1.867099466739700286e+00,-1.544511259144617954e+00,-1.555113136470289925e+00,-3.633517516101433253e-01,-1.575628606715051339e+00,-1.169198757416956003e-01,-1.541564375167977952e+00,-1.384660207425590706e+00,-2.087540513422468103e-01,2.039514156118009125e-01,-1.587861423462168409e+00,2.521155683909973577e-01,-1.372270524029966632e+00,-1.550817814164088260e+00,-1.426026796856582290e+00,-1.365768584159129384e+00,-1.311865436581222788e+00,-1.850893218702704124e-01,-1.637070564823309038e+00,1.000000000000000000e+00 +-1.747474111846821110e+00,-2.290166316437901584e-01,2.180988537522543513e-01,2.694592974029106358e-01,-1.211201451859689193e+00,-1.767984885951759289e+00,-1.502625876119413562e+00,-1.438947562452083551e+00,8.065893288718263554e-02,-1.528688378021011207e+00,1.087299778884808776e-01,3.198929781779950021e-01,1.039774594364287486e-01,-1.798111513407557371e+00,-1.545989842407716974e+00,9.064067535532020770e-02,2.432173424281315610e-01,-2.081961545829282001e-01,-1.659416484977199513e+00,-9.793454098837411914e-02,-1.000000000000000000e+00 +-1.541891116190167443e-01,1.198641927962546261e-01,-9.696410479856587550e-03,-1.477477922413563460e+00,-1.597051621274383271e+00,2.406839794158164292e-01,1.407643727635641273e-01,-1.553446610635859315e+00,2.989015888209632932e-01,3.165877637380919429e-01,1.169824338328576685e-01,-2.008026315114123383e-01,-6.220300738583128819e-02,2.915046298468972896e-01,-1.616502083500595544e-01,-1.022217754874521073e-01,9.035042558882541708e-02,-1.332018434095425530e-01,-3.146826509327730337e-01,6.291195189475287664e-02,1.000000000000000000e+00 +-1.901571954471936166e-01,2.036693401068984388e-01,-8.558809588610771790e-04,1.799349081329099098e-01,9.517185955184281898e-02,3.590268720909888245e-01,1.130070230940566217e-01,-1.649686655919097245e+00,-1.567966253893172901e+00,5.390840436566476934e-02,1.478514498943867550e-01,-1.164278779706381162e+00,2.523227821439278373e-01,-1.771175375557790255e-01,-1.835251468954054443e-01,-7.142510660604928230e-02,1.156365524626673247e-01,1.752520883288299913e-02,1.580674384399360921e-01,1.595989854848350875e-01,-1.000000000000000000e+00 +-1.977287914640140531e+00,-1.086192057027827729e-01,1.128753655542010237e-01,7.440106077279728947e-02,-1.829164392143819207e+00,1.001733311909408508e-01,1.656542203717096262e-01,-1.530841270169906565e+00,1.540936542463245962e-01,6.463618023479822394e-02,4.251644257692598350e-01,-1.355725998615462391e-01,1.214504897439154657e-01,4.189501652900345929e-01,-1.659671719591041539e-01,-2.173559202300396342e-01,2.715598643706897408e-01,4.647816024550471825e-02,-2.818481592934465874e-01,-3.720732940340260031e-02,1.000000000000000000e+00 +-1.533720518381189724e-01,3.892743879497452819e-01,4.180672620449811527e-02,1.013359898957980343e-01,7.252934689412099400e-02,2.300045152220870637e-01,-2.934514337618384511e-02,-1.349080214839665226e+00,-1.695051566604298010e+00,-2.374758368466359515e-01,3.977900521952173335e-02,-1.247430214337799992e+00,6.920324582981349226e-02,-1.651284782420658681e+00,-1.725574998237646929e+00,1.484493211461310747e-01,1.961649923164716092e-01,-1.743295333073816789e+00,-7.901003630137488909e-02,-6.681325434829828158e-02,-1.000000000000000000e+00 +-2.096774839232835308e-01,3.133813251872709094e-01,4.719834877226380954e-03,-1.486312632979153836e+00,-1.556459141653098399e+00,4.912658260630371754e-02,7.744509731644938499e-02,-1.568503548805284309e+00,2.819467580128540152e-01,2.525988221138925072e-01,1.034772777057548221e-01,-7.926864226769231303e-02,2.438139918066015988e-01,-1.101530864062478798e+00,-1.641623168577236003e+00,-1.198354932584080879e-01,1.378072651587314845e-01,-1.785790733459343693e+00,-3.457637697154141154e-01,1.918905251071924556e-01,1.000000000000000000e+00 +-2.750489793675787720e-01,6.148307212179922321e-02,1.736431353070554684e-01,1.579328863818725837e-01,2.462079915353442006e-01,-1.411825664811311221e+00,-1.534168967146540163e+00,-1.551232148260871746e+00,-1.535742722413438566e+00,-1.770318007937173865e+00,1.751673676029186222e-02,-1.190055568191122015e+00,2.059950403699849497e-01,-1.012485079418455336e-01,-2.060936796710344243e-01,3.384030592975885432e-02,2.505534977666529217e-01,2.009368311663959072e-01,2.824259611451782614e-01,8.170939704749366483e-02,-1.000000000000000000e+00 +-1.917642069761332779e+00,-2.926585656134242375e-01,1.528819448980941464e-01,1.815071978415521148e-01,-1.882235658081898855e+00,1.575446717586391765e-01,-1.408146613731111989e-02,-1.571190358338920401e+00,1.657830368464734416e-01,1.461993811029305701e-01,2.901607518443575229e-01,-2.063763375229137609e-01,9.105827938715693737e-02,-1.402960325339824843e+00,-1.809441313322212919e+00,5.135189814511180451e-02,2.236863619589410657e-01,-1.729600227087127795e+00,-1.870865326701982301e-01,2.039074972169292455e-01,1.000000000000000000e+00 +-4.366449421615858917e-01,-1.003944793806665903e-02,1.918228198993621181e-03,2.955270038624893125e-01,1.648973129255825454e-01,-1.169406948177582839e+00,-1.631843085827449524e+00,-1.802517558474467707e+00,-1.706914345186495652e+00,-1.690513861068017265e+00,7.390782350365482367e-02,-1.409653456133216576e+00,3.350455036101138151e-01,-1.564931056937633347e+00,-1.688642083380686554e+00,7.406743163755136194e-02,1.813613607643461134e-01,-1.772481085071356821e+00,3.485842607448004504e-02,1.003860651007464078e-01,-1.000000000000000000e+00 +-1.798900272653050914e-01,2.389637741664693005e-01,1.663524608467675803e-02,-1.353765703336621495e+00,-1.959743191110952631e+00,2.264950660320298459e-01,1.806454108733758779e-01,-1.559512852094719459e+00,3.648320019934763181e-01,2.358935068061861207e-01,3.078967316911840890e-01,-3.361268876809028749e-01,7.867525194677538192e-03,3.361952823430025550e-01,1.096649834255997713e-01,-1.651150588517939832e+00,-1.265011938517930368e+00,1.393637110926818079e-02,-2.419700909072255435e-01,-1.331374404824197111e+00,1.000000000000000000e+00 +-3.309211948171192352e-01,2.421839490070105683e-01,6.136022709953796450e-02,-1.201853652373070069e+00,-1.436604493724723097e+00,-1.198067495246635250e-01,1.474701755038044515e-01,1.895277933144060289e-01,-1.638327916991755062e+00,-1.563818191245952005e-01,-1.077378310694679930e-01,-1.403821945296813034e+00,1.368941449902785490e-01,-3.477763887439616619e-03,7.226365175294849341e-02,-6.143754737558303897e-02,2.795623814128017637e-01,-1.289968880168595733e-01,-3.133117511980514081e-02,-1.017084218553041403e-01,-1.000000000000000000e+00 +-1.686938541095525990e+00,-5.872095876893711208e-02,2.443208985248077059e-01,9.212828068375636392e-02,-1.739992588081891389e+00,-8.489541302770341191e-02,-4.399330434624062341e-02,-1.530747659062598354e+00,2.738022830851473688e-01,9.055475238662805126e-02,6.543081035374409549e-02,2.659068042593432102e-02,2.048421980933542441e-01,3.147989394625860804e-01,-1.865517304471441262e-01,-1.775850268471311999e+00,-1.600212304956398990e+00,4.336575018149410599e-02,-9.172339110144461793e-02,-1.218500129682053457e+00,1.000000000000000000e+00 +-2.147425078722564606e-01,1.363834862484123445e-01,6.828257638775982186e-02,-1.481775071019213907e+00,-1.351991532810939312e+00,-3.283521414091315482e-01,-3.722540922456851059e-02,3.703523150812216419e-02,-1.661559154803918359e+00,-7.850714527956696720e-02,7.419368308494322939e-03,-1.244710516344766704e+00,2.207972406114958874e-01,-1.606888032720614534e+00,-1.770546800386025632e+00,1.274118906779919680e-03,1.911757525444973127e-01,-1.672845872229138164e+00,1.400165475014619743e-01,-1.208990314961668616e-01,-1.000000000000000000e+00 +-2.772045361255325302e-01,1.711467383161677236e-01,2.451393212895625756e-02,-1.587166372966360095e+00,-1.577276868896203776e+00,1.972544845751455778e-01,-1.032205822818711427e-01,-1.672327308636493370e+00,2.545592984995520558e-01,1.816840435404763476e-01,5.871646934329571854e-02,-2.502838370781583222e-01,2.337932822664689214e-01,-1.254707426736235476e+00,-1.578635681096223875e+00,-1.264382673090489906e+00,-1.367165619666614651e+00,-1.566260933111969456e+00,-1.542597882273265830e-01,-1.271568528877850435e+00,1.000000000000000000e+00 +-1.192345410485343982e-01,2.876314065052951729e-01,1.673852107512977250e-01,-1.390331194293130679e+00,-1.455016824134779840e+00,-1.768413949953279607e+00,-1.450095613466445554e+00,1.650535627965662722e-01,-1.561735774153465428e+00,-1.470408862817280893e+00,2.583023118478519967e-02,-1.241302664921004606e+00,1.513177057589079055e-01,-3.912346900213074796e-02,1.172815152082321011e-02,-1.029777163955075681e-02,2.244996134321211312e-01,-1.300648754226581272e-02,7.335149050516499847e-02,5.772273621679507860e-02,-1.000000000000000000e+00 +-1.813573345588654018e+00,-2.220478181890921665e-01,5.467008543926200892e-02,3.689254172690206723e-02,-1.665402596540724867e+00,4.922176964755629186e-02,-1.284888778791378472e-01,-1.414102000818379556e+00,1.580772852890507674e-01,1.775797928577861062e-01,1.632271718891248613e-01,-1.012449705210102247e-01,1.195179981520401447e-01,-1.362363523651744623e+00,-1.796199810051825718e+00,-1.432072896554568597e+00,-1.283539847349572938e+00,-1.532459423013815769e+00,-3.101370512901745480e-01,-1.425056629662605934e+00,1.000000000000000000e+00 +-2.321712416461661488e-01,1.197055710457237271e-01,2.331169061402619258e-01,-1.359777347085656896e+00,-1.205652424485054386e+00,-1.656023067673558780e+00,-1.663543935883512015e+00,-4.702894733069958033e-02,-1.640070771258239635e+00,-1.355343200009919924e+00,-8.415678213258656615e-03,-1.486702329052909999e+00,2.426856412005025387e-01,-1.659145659484962065e+00,-1.741035399782207449e+00,2.753416158423056537e-01,4.119729903099007551e-01,-1.851604568082790614e+00,9.514474688383869894e-02,3.355685959805613239e-01,-1.000000000000000000e+00 +-1.500485907047857115e-01,-1.221950377757007544e+00,-1.442912336766244419e+00,-1.681670222929709801e+00,-1.788854353140276299e+00,-1.454613944941973269e+00,2.198577103874525124e-02,-1.560819643228220199e+00,3.205792112120700033e-01,1.354250606290548220e-01,2.327374602498822609e-01,-4.973251672414213953e-02,1.334910473910854434e-01,2.103981666313979315e-01,-1.271344326814805581e-01,-4.364511088781411474e-02,2.178793215798139060e-01,-2.499218596906974676e-01,-2.186440282503921972e-01,1.744750978160619914e-01,1.000000000000000000e+00 +-2.192300878144892440e-01,2.129718927342666435e-01,3.987026058767699499e-02,2.000062748130256585e-01,2.628984287197382863e-01,2.270013246051293954e-01,-8.614670694217999025e-02,-1.543148031218893435e+00,-1.776428408697541705e+00,-3.957910129421529644e-02,-1.445144355756989851e-01,-1.229806842034160796e+00,1.711086066476480427e-01,-5.751169330957496550e-02,-1.759722175187910798e-01,-8.966529641510720405e-03,2.386008593071037298e-02,-1.414976449392172819e+00,-1.633366652713292622e+00,-1.020480016423235825e-01,-1.000000000000000000e+00 +-1.797731426999819693e+00,-1.752605155053432062e+00,-1.548023547333538863e+00,-1.775979434727979062e-02,-1.752352156931570981e+00,-1.478995192351079568e+00,-3.220116500219568495e-02,-1.565956927518345143e+00,1.036726623198800656e-01,2.016167245776253059e-01,2.007474174373383158e-01,-2.181649250102777327e-01,1.803902425467817372e-01,2.805682531273903102e-01,-3.578751536483423118e-02,-3.153716615350411479e-01,3.526011840434244426e-01,-1.555955680862944890e-01,-1.788745791555167719e-01,2.078745575009158519e-01,1.000000000000000000e+00 +-1.490027622720656031e-01,2.289246899079383502e-01,4.028490913735319201e-01,3.967488799718776882e-02,3.347084845734267233e-01,2.145937412022381197e-01,2.485608235905084618e-01,-1.579188017895288620e+00,-1.762261402419999401e+00,-8.870698298634535628e-02,-4.629015332510245245e-02,-1.174944430751200697e+00,2.415701140845060158e-01,-1.916344927146877319e+00,-1.677645663723282832e+00,1.576777221398396600e-01,1.798218039140481450e-01,-1.175696772797820699e-01,-1.526183409926597845e+00,-1.406353750619999954e-02,-1.000000000000000000e+00 +-2.804450381031173278e-01,-1.388453345596431854e+00,-1.416961652978935504e+00,-1.605161445656936747e+00,-1.755528150368922180e+00,-1.524795721013195937e+00,-1.240352383476334697e-01,-1.636548475370386946e+00,2.790995590094988987e-01,4.497646242908202863e-02,1.997987723294708806e-01,-3.019569124547694106e-01,8.327915621047257688e-02,-1.287148835552201165e+00,-1.625334029447224937e+00,1.748064638310643515e-01,2.640000800107265011e-01,-1.725001420021360810e+00,-2.696708474908042219e-01,1.930095222890825801e-01,1.000000000000000000e+00 +-1.926953285475565270e-01,2.189375352722924828e-01,4.539331996887961684e-02,3.008432471831398169e-01,2.565262844758790517e-01,-1.293581987033126879e+00,-1.548045675190745873e+00,-1.451162655981990124e+00,-1.525072498757130024e+00,-1.758901982256172669e+00,-3.355741212560588854e-02,-1.307502230564176804e+00,2.578444830799039855e-01,-8.432854820882182412e-02,-1.144569608876211675e-01,9.693908609789249342e-02,3.263164181899512184e-01,-1.789185589176669255e+00,-1.470419019157990004e+00,-4.516418069697201998e-02,-1.000000000000000000e+00 +-1.681524041973221362e+00,-1.757453944303776217e+00,-1.708761911120806110e+00,-1.549405097104009088e-01,-1.902663562777817674e+00,-1.548997546776542000e+00,7.351945697999465179e-02,-1.605305977413101326e+00,1.995461886745794333e-01,-4.181048681232629738e-02,3.660708897173430287e-01,-6.655161461117785338e-02,2.064950518813776226e-01,-1.398963379454520872e+00,-1.715046108526779189e+00,2.116278766484822604e-01,8.234758695042038168e-02,-1.915033149590992467e+00,-1.661268372122445269e-01,1.262595222157170127e-01,1.000000000000000000e+00 +-1.962283649628915050e-01,1.122058887764890850e-01,1.381278041680962521e-01,8.728184706616783084e-02,1.127894522815892503e-01,-1.609889772273879860e+00,-1.525538454944675548e+00,-1.598660103027582879e+00,-1.769575663994145120e+00,-1.600323143094793954e+00,2.014436967072089357e-01,-1.416089356524305787e+00,2.288057989463968267e-01,-1.556363659198443550e+00,-1.692201985594345981e+00,1.091619765004213927e-01,1.858051152871127432e-01,-3.681708277874619351e-02,-1.506373588134407582e+00,8.912630388338534104e-02,-1.000000000000000000e+00 +-5.188785821684105404e-02,-1.374675205996179317e+00,-1.369049881942256075e+00,-1.535576187239738433e+00,-1.788853910025122618e+00,-1.607163819785809356e+00,-1.372988750116458656e-01,-1.451845451849335111e+00,1.683149804906874658e-01,5.818964701482091939e-02,2.432364208586114407e-01,-1.366346592229962797e-01,2.882246812514612233e-01,2.323272716510418334e-01,-1.156633142470376435e-01,-1.722224223048894443e+00,-1.356919905977978402e+00,2.339505846669169831e-01,-2.983554805679451971e-01,-1.444471577260626693e+00,1.000000000000000000e+00 +-1.723282596120939703e-02,9.922894674221888200e-03,2.114255707134858886e-01,-1.256611435471116200e+00,-1.338172796419482058e+00,-1.393884748518721406e-01,-1.422037572426880780e-01,7.951209810074390261e-02,-1.701304200230044028e+00,-1.278531280046300567e-01,1.665352764802276175e-02,-1.531460413957198563e+00,1.201004855627265944e-01,9.143562050819743769e-02,-8.495073340818597163e-02,-8.640815974854335058e-02,3.051292050533321865e-01,-1.782691346137809996e+00,-1.541673503964391179e+00,4.359225158923388244e-02,-1.000000000000000000e+00 +-1.759731435909149821e+00,-1.722540069232149307e+00,-1.350575599538822669e+00,1.530087231441150664e-01,-1.919227638739745778e+00,-1.522311312217412116e+00,2.663977705038157034e-02,-1.462895398911590927e+00,2.346870079004382825e-01,5.298448739890479486e-02,3.208235596105920018e-01,-3.225522417942412456e-01,2.151378677463345324e-01,2.244448666307395646e-01,-1.351102521335871276e-01,-1.674098601469833580e+00,-1.321534060738071670e+00,2.232323694424712124e-01,-2.410302168203579953e-01,-1.419962320981015935e+00,1.000000000000000000e+00 +-2.633866754547229960e-01,1.358270758478960505e-02,2.574503224364339538e-01,-1.443596796605898014e+00,-1.397195964349720265e+00,-2.066669973860401355e-01,-1.758563845619880994e-01,5.331874892815188782e-02,-1.852381209734429879e+00,-1.805032153887715096e-01,1.195129378885405880e-01,-1.364271966209453790e+00,2.664623692271294031e-01,-1.600743433266965665e+00,-1.788286601561143163e+00,3.068265358594896020e-01,2.559739465456911667e-01,-1.971470390451849075e-01,-1.650667372783046982e+00,8.264199620782243516e-02,-1.000000000000000000e+00 +-2.481697005315772475e-01,-1.381967198312306300e+00,-1.512006400315489163e+00,-1.617024483795716172e+00,-1.836827134394403815e+00,-1.545026569114187343e+00,-1.582826646629847955e-02,-1.507168721185889737e+00,1.310109632937027091e-01,1.921808605290821204e-01,1.145040726005610437e-01,-1.219314979512694519e-01,2.975686261079176775e-01,-1.410124262462820521e+00,-1.564041871941007189e+00,-1.616683010822192346e+00,-1.563536729174665130e+00,-1.367834179879528822e+00,-1.919167707969222370e-01,-1.321645793082277809e+00,1.000000000000000000e+00 +-2.250694852470974394e-01,2.586746931010264205e-01,1.270736451133486211e-01,-1.453749431420937599e+00,-1.331400637263621345e+00,-1.916989105138284133e+00,-1.470519389589342474e+00,-2.575670503203331130e-02,-1.546322707545896735e+00,-1.678294797276697059e+00,1.804925820963580729e-01,-1.338470925613644669e+00,9.358071893679681397e-02,-2.050571700633527783e-01,-2.592183686402053100e-01,-2.760682876197858016e-01,2.474594164336252622e-01,-1.622977875468254227e+00,-1.600052899883098068e+00,-2.545593658316831764e-02,-1.000000000000000000e+00 +-2.008138185897252459e+00,-1.769840576784453168e+00,-1.594166575905737071e+00,1.744045374285137229e-02,-1.788144034741654620e+00,-1.604207237451122481e+00,7.450619461755142448e-02,-1.570630280276348323e+00,2.986456564471063890e-01,1.557489853546121794e-01,1.807124548561135913e-01,-3.018706573418425410e-01,1.530498899233971588e-02,-1.296057054467206537e+00,-1.703412346621872908e+00,-1.300735058435303459e+00,-1.236030663661329188e+00,-1.545097966404944456e+00,-2.497731242540399133e-01,-1.288660072959546321e+00,1.000000000000000000e+00 +-2.017963239069477388e-01,2.254262838857801210e-01,2.351494609794932966e-01,-1.293976900602812563e+00,-1.611052001077982299e+00,-1.690434313162011648e+00,-1.662958075946455505e+00,4.345128772439012244e-02,-1.672573879733247004e+00,-1.547695599399579569e+00,3.607367517426384590e-02,-1.305287577992165327e+00,2.629757329469552540e-01,-1.620581609283257007e+00,-1.849230605960164198e+00,4.143028843267702876e-02,1.501687731134105919e-01,-2.245417608825056588e-01,-1.591117214434435212e+00,-2.607642312533986484e-02,-1.000000000000000000e+00 +-4.239786628194559537e-01,1.954984361132021675e-01,-1.001112260934661768e-01,-1.521021702018586108e+00,-1.847664734870663183e+00,3.476951476860069834e-02,3.908534827535986578e-02,-3.441650628764165304e-02,-1.461542283580969714e+00,-4.302677154180577224e-02,1.998236051372857813e-01,-1.747447584324808734e+00,2.825811816454654180e-01,2.453530971542590700e-01,-1.326706774301590896e-01,-6.007190044516801108e-02,2.910221889601137812e-01,-2.185449015932421912e-01,4.480722461949049329e-02,8.659013180298130719e-02,1.000000000000000000e+00 +-1.801324665127946645e+00,-1.719544891428557709e-01,1.088482489085384614e-01,-1.299025138906894750e+00,2.763992571685174870e-01,2.530370326242789569e-01,1.448570234705879867e-01,-1.611682407748750157e+00,-1.743856501784443847e+00,-1.956733507539220041e-01,-1.178153849953956361e-02,-1.489959017649406636e+00,1.875208794936734369e-01,5.437841947870207526e-02,-1.402454379129729478e-01,-1.203440513115870830e-01,2.307435421811548926e-01,-2.925237475086465144e-01,1.891221967490180206e-02,1.194696285086674498e-01,-1.000000000000000000e+00 +-1.846673338107843509e+00,-3.023959190237470729e-01,2.286684893430069587e-01,5.411855215442593070e-02,-1.960626821316767021e+00,3.792700526303909303e-01,-8.648872687750591703e-02,2.606519216137613559e-02,-1.380530914991796365e+00,-1.484316578276552445e-01,7.369935626330553768e-02,-1.661572947509190268e+00,9.848980253666997364e-02,2.517583000120153236e-01,-9.123950666640436025e-02,-1.318329915585552681e-01,2.108636616192697266e-01,-2.370676094466734884e-01,-4.348157446128733117e-01,2.992036293923540646e-01,1.000000000000000000e+00 +-1.883699443687262276e+00,-1.882364363868459511e-01,2.543184252246958876e-01,-1.382661717471148188e+00,2.087119397685269773e-01,1.230869317918208139e-01,-1.017833311620702230e-01,-1.621497538584965925e+00,-1.533846599591235815e+00,-9.491133323523429088e-02,3.604890941637624302e-02,-1.593771289307023498e+00,2.864784080541166178e-01,-1.413364263952484379e+00,-1.781620287627969201e+00,1.188810739693684504e-01,3.699356811418870183e-01,-1.680023429240815380e+00,1.637000803747315214e-01,1.490366681935645088e-01,-1.000000000000000000e+00 +-2.710907429413343950e-01,2.994346243336623026e-01,-2.260029579715211456e-02,-1.492730556851320056e+00,-1.746468660413026175e+00,-1.010087151397107141e-02,-5.373222621421234330e-02,1.648961498104867252e-01,-1.496528298955463798e+00,-2.121406732334990553e-03,2.401872943645969871e-01,-1.619673633875661656e+00,2.162543120494445081e-01,-1.387091666676599333e+00,-1.638646216342895423e+00,3.141293775056863691e-02,2.319621855529500343e-01,-1.730590469786037477e+00,-1.413321507921310727e-01,4.549543055515198331e-01,1.000000000000000000e+00 +-1.778511147832783301e+00,-1.013988032028912412e-01,2.823156451699946334e-01,-1.485894784482977737e+00,1.917891809315158680e-01,-1.207316588191012396e+00,-1.580868066414439221e+00,-1.395163889088425702e+00,-1.649384640267517854e+00,-1.687806656840818098e+00,-1.896454383487397866e-01,-1.222417558878858390e+00,2.100555357210017182e-01,9.232630392069701708e-03,-2.889446551393878293e-01,-1.061335260786943485e-01,3.086988443948957550e-01,-5.761031263168545774e-02,-8.365443100916615526e-02,-2.975471396689306769e-02,-1.000000000000000000e+00 +-1.902045354412928546e+00,-2.251341199966401529e-01,-2.937591749939713615e-02,5.633827003579487680e-02,-1.667111122422912040e+00,6.912288299899774224e-03,4.347368841819030522e-02,-2.944643422612748807e-01,-1.394011698904371599e+00,-1.911564700887848911e-01,2.281464495289923788e-01,-1.561766753820800879e+00,1.966269197021561221e-01,-1.180009473192372926e+00,-1.712197917581733231e+00,2.464873639021550322e-01,1.919760488022198286e-01,-1.663469084195813963e+00,-1.645175843233691593e-01,1.209532845560358227e-01,1.000000000000000000e+00 +-1.833114143413420383e+00,-6.171028862943107873e-02,5.754822885933238985e-02,-1.560724420388548861e+00,3.632564576682422164e-01,-1.481945268653765746e+00,-1.279587882018693001e+00,-1.656645719030719555e+00,-1.736443148111497914e+00,-1.729863960812176193e+00,-6.536101742447317231e-02,-1.330954960290977374e+00,2.008390928839592915e-01,-1.718026687430719956e+00,-1.745088642128966860e+00,8.660695288671575731e-02,8.378037007097807742e-02,-1.719867788414559451e+00,7.806008967015282130e-02,2.804763975134820508e-02,-1.000000000000000000e+00 +-2.768067646498343914e-01,2.125317705033215876e-01,1.271250686981756362e-01,-1.582895884622139926e+00,-1.906587196338887846e+00,2.675521541774043138e-02,2.390045238483756718e-02,5.588645404529107852e-03,-1.503449110022147872e+00,-2.881893140624296468e-01,3.390313987583383759e-01,-1.628713365152743453e+00,-1.375870843740442662e-01,2.395937847036929413e-01,-2.358512458605327711e-01,-1.754802431243140504e+00,-1.361949088710851852e+00,1.429327083128897058e-01,-4.043767937558144787e-01,-1.490996219627224839e+00,1.000000000000000000e+00 +-1.761864421231023936e+00,-2.778956981999408660e-01,2.267530183011868539e-01,1.389414089794701324e-01,-1.488586038940487688e+00,-1.930455733800497398e-01,7.982816361017525120e-02,9.220722791774814409e-02,-1.646106816827008990e+00,-2.116877029822330503e-01,-6.571921097744903850e-02,-1.295791224367208283e+00,3.388574340213011959e-01,-4.880479281179687934e-02,-5.330984855600520711e-02,-1.394783381758671892e-01,2.125230646922146027e-01,-8.815444589911280993e-02,5.209478083777161622e-02,2.513044830152091902e-01,-1.000000000000000000e+00 +-1.873191616492320577e+00,-2.056873989135975522e-01,1.788792433776593138e-01,6.440261287751487818e-02,-1.639983345842225937e+00,7.431984483779473238e-02,-6.737981502640266740e-03,-9.228866042587285934e-02,-1.423709368258853081e+00,-3.519541367219973793e-01,1.929667786579778721e-01,-1.741194061661679227e+00,-8.842165688775438515e-03,3.301823485436581884e-01,-1.892267665999154413e-01,-1.723186355638004041e+00,-1.297342676980458576e+00,2.243686521156938174e-01,-1.225553682741842010e-01,-1.322983351201722391e+00,1.000000000000000000e+00 +-1.845933991953369935e+00,-1.018255445810124504e-01,1.283198692785773587e-01,2.944784400217750897e-01,-1.276526946838344667e+00,-2.567000966561716058e-01,1.870128664907933641e-02,9.175820873300864766e-02,-1.624991653992264418e+00,-7.030674276811187118e-02,2.318219308332492359e-02,-1.103949973968188836e+00,1.916707891925400475e-01,-1.833345479214313567e+00,-1.548042311660148052e+00,1.195313668833518361e-01,1.679486183195668114e-01,-1.705850794960609873e+00,2.550563100626263013e-02,-1.997707596345787984e-02,-1.000000000000000000e+00 +-2.446585241405007105e-01,2.309835849814308173e-01,1.443057578971227439e-01,-1.515667559477737569e+00,-1.619770971134667059e+00,1.187375512054550664e-01,6.649109859940552369e-02,7.432639227162841922e-02,-1.380992404316611077e+00,-6.001711401765588705e-02,2.792694154722341615e-01,-1.763976851410502311e+00,6.032378343654248853e-03,-1.387988888669036358e+00,-1.723065599013226423e+00,-1.243252983977291226e+00,-1.257042860606533363e+00,-1.321430117125417691e+00,-3.429405920406701780e-01,-1.226405993804544181e+00,1.000000000000000000e+00 +-1.865169228647927779e+00,-1.237879373561084628e-01,7.405335204670369931e-02,6.890642354928330771e-02,-1.498597461518269114e+00,-1.590548669947394078e+00,-1.461212144711625749e+00,3.490170736471687185e-02,-1.573357413030800789e+00,-1.537814114421869771e+00,2.186615813812642084e-01,-1.292038367648431718e+00,8.948357088198743980e-02,-8.928658184420408539e-02,-8.429750889904033540e-02,-2.334090783460711638e-01,3.705548010630302924e-01,-2.252870064453939836e-01,-2.091505472947417787e-02,-1.204439228438229648e-02,-1.000000000000000000e+00 +-1.707915085551401813e+00,-2.014512879346425089e-01,-4.914441854039292013e-02,-9.839895374084701996e-02,-1.726056384131820831e+00,9.440942940517532689e-03,1.001794835526170491e-01,5.070658569607315591e-02,-1.242348225775620874e+00,-1.159321605234600316e-01,3.127579004505120497e-01,-1.840778784331079887e+00,-1.658154698399136318e-02,-1.316138131065869743e+00,-1.894058600534813674e+00,-1.433057168579106255e+00,-1.278327361537103357e+00,-1.507053632706953916e+00,4.855031501400158822e-02,-1.410377662069259408e+00,1.000000000000000000e+00 +-1.927330015855815537e+00,-3.023244728403503556e-01,-1.004440616059082414e-02,2.475147107586016804e-01,-1.521246841214893841e+00,-1.832048881318087741e+00,-1.634658137038252690e+00,-1.202191394984148259e-01,-1.720221659787346091e+00,-1.575718727875419445e+00,4.137234899508839686e-02,-1.114256397670778220e+00,3.386845536114191368e-01,-1.552188726862925972e+00,-1.737857642087943022e+00,1.996806574321181027e-01,6.313713665147491216e-02,-1.632778985039999409e+00,-9.376998928433753766e-02,3.399858858802459749e-02,-1.000000000000000000e+00 +-1.599149871765311592e-01,-1.394954387635932491e+00,-1.431275230204140270e+00,-1.658557878945149389e+00,-1.665066556585239388e+00,-1.382801562304887710e+00,-1.903937519451994742e-01,-3.984804340760764868e-03,-1.498487313441081659e+00,-1.414217101692847312e-01,1.861058267637868135e-01,-1.770039703316339974e+00,2.855961150446482688e-01,2.631672011561230207e-01,-2.365965900204080041e-01,-1.170920995135496523e-02,2.603230639002764857e-01,-1.464618248912564757e-01,-3.436496738455320221e-01,1.613615754372600131e-01,1.000000000000000000e+00 +-1.901763756315645981e+00,-1.140130436048093776e-01,-1.589885895296563878e-02,-1.347714557200887731e+00,1.559893111253480291e-01,2.943525095945195846e-01,-8.762013730972015302e-02,-1.698111418051260424e+00,-1.582255231623669767e+00,-8.344383709300481899e-02,-3.070355736728235668e-02,-1.428494191647693201e+00,1.429522525186431858e-01,-1.034098271437848549e-01,-4.141065512465620657e-02,-2.107235320287753622e-03,2.025146775974647206e-01,-1.682346242604868536e+00,-1.622323666331404901e+00,-1.107978489145008821e-01,-1.000000000000000000e+00 +-1.662023350391828025e+00,-1.654770176908459289e+00,-1.557597998770770875e+00,1.203487712134641702e-01,-1.838685072399005804e+00,-1.463835343836004110e+00,-6.355507345375289996e-02,7.890762379367925045e-03,-1.342721534829666385e+00,-2.374024144962470495e-01,1.993536497136917662e-01,-1.710146411338983707e+00,7.197835586250633799e-02,2.953825947571455668e-01,-1.829940855642144815e-01,-3.553528407017891855e-04,1.407134797205625565e-01,-1.308717950203963365e-01,-3.207728021164623078e-01,3.767952619169859241e-02,1.000000000000000000e+00 +-1.887184419707484961e+00,-1.853757098381225432e-01,2.872420446140629968e-01,-1.233903244711152869e+00,4.264280874515810060e-01,1.539815503152855702e-01,-2.511728916094144864e-02,-1.430601943163283485e+00,-1.794794334882835241e+00,2.279592536379622392e-02,6.505452523012145161e-02,-1.304092811701644594e+00,3.616317939265475956e-02,-1.684500566958359480e+00,-1.748425444466124912e+00,1.320542591078140704e-01,1.988450335786769163e-01,-1.457390216866339960e-01,-1.681082243555220623e+00,-1.444260182732841624e-01,-1.000000000000000000e+00 +-2.147591247930463187e-01,-1.295880574644723060e+00,-1.414181058229858623e+00,-1.761577153389257244e+00,-1.816617960661231157e+00,-1.499391787906862561e+00,-9.587358913735103891e-03,6.155900339266405508e-02,-1.315616638581871056e+00,-2.276183033416775969e-01,3.757495554385740233e-01,-1.970598391636864566e+00,1.298988905924748360e-01,-1.210652947605888263e+00,-1.654496270186754714e+00,1.786756798855035910e-01,2.257526784963599653e-01,-1.563851404462621053e+00,-9.310080301164969097e-02,-1.898015221483945192e-03,1.000000000000000000e+00 +-1.757629487888254349e+00,2.750813333173868225e-03,1.909472985348114882e-01,-1.413755901540125759e+00,3.334691618878731223e-01,-1.487572786021322813e+00,-1.704150413544582410e+00,-1.438446898524446071e+00,-1.587959853361427420e+00,-1.756280186303360802e+00,-5.018627473955431656e-02,-1.418801834989586785e+00,7.241102316773673220e-02,-1.254257779836668307e-01,-2.277330462722834681e-01,-1.127309666191127768e-01,3.136523642408932666e-01,-1.717027655520484197e+00,-1.574448929584145285e+00,-7.734777858761476832e-03,-1.000000000000000000e+00 +-1.673765524340555588e+00,-1.873599457951749203e+00,-1.669534734460675462e+00,7.532090305687616505e-02,-1.779359154952696365e+00,-1.588579491977466729e+00,-4.716969564667664733e-02,1.002003555818729991e-01,-1.351159720441112988e+00,-2.036155985690527026e-01,1.352720472680977726e-01,-1.795540273054498925e+00,2.419025125333562620e-01,-1.355743421377280544e+00,-1.701051084926724188e+00,7.121744646704183945e-02,4.833416624330702516e-02,-1.771424242246609326e+00,-1.325963547716367819e-01,1.863729830672161969e-01,1.000000000000000000e+00 +-1.824610374325886353e+00,-2.596331126462229077e-01,1.971523564757886038e-01,-1.243699053737473248e+00,2.473200182543848491e-01,-1.414641461934736055e+00,-1.463653921120485091e+00,-1.723371048229100477e+00,-1.524784161373843983e+00,-1.485507312847800021e+00,1.495881799387448474e-01,-1.369066816825430521e+00,4.668849072457398330e-02,-1.608590022098949079e+00,-1.786438967324652571e+00,1.124377585141832503e-01,2.755700291238519783e-01,-1.233772338920482020e-01,-1.591254715929807162e+00,9.133259280459660179e-03,-1.000000000000000000e+00 +-2.180542142296966468e-01,-1.270062896352505577e+00,-1.495328868863751604e+00,-1.769646165049767905e+00,-1.840127173811692662e+00,-1.615188554748361094e+00,-1.359222778364617967e-01,-3.831082836164435845e-02,-1.445589010003945463e+00,-2.321568870857112499e-01,9.598219204448811548e-02,-1.625105004571937073e+00,1.327266192783912169e-01,2.573210482151609413e-01,-2.440372059621254697e-01,-1.733700222071140606e+00,-1.371266470740743815e+00,1.259516749085466680e-01,-2.967927586243156446e-01,-1.583235986500352688e+00,1.000000000000000000e+00 +-1.780336768017378724e+00,-1.154440834181787029e-01,7.668296338194463546e-03,2.831619677306277394e-01,-1.452473720257523970e+00,-4.285690418951215719e-03,1.654404361349363084e-01,1.514950521350017128e-03,-1.549922508433306589e+00,-1.957878405107431052e-01,4.364706251310906948e-02,-1.346951740306844059e+00,1.799600142998423191e-01,-1.325391087201296392e-01,-1.359185952055554369e-01,-5.762326559486587868e-02,2.491579101794483553e-01,-1.439385722027020531e+00,-1.545230148793086622e+00,-1.970738583918421050e-01,-1.000000000000000000e+00 +-1.629813547646396632e+00,-1.722040278342896746e+00,-1.439454653795339878e+00,8.464614837008242743e-02,-2.052730565713883681e+00,-1.407304653711857645e+00,-3.951692706296879021e-02,1.626117039329012526e-01,-1.242088494892461981e+00,-2.128027926490991262e-02,2.370403982931431064e-01,-1.860509553164634955e+00,1.115267716483113181e-01,1.591178760324968022e-01,-1.357813641817019745e-02,-1.514209434610088501e+00,-1.410136060264903124e+00,1.080091317138559270e-01,-4.215093146274401237e-02,-1.466606840271246659e+00,1.000000000000000000e+00 +-1.912817834617272172e+00,-6.515174009513001463e-02,9.790766084813876113e-02,1.054421055807980984e-01,-1.279485933635208683e+00,-4.091736000026738873e-01,-7.667769932443077585e-02,-1.375716133940347585e-01,-1.576176057217403059e+00,-8.098090482344666441e-05,1.224971632641589092e-01,-1.331235313536060172e+00,2.196543467199380817e-01,-1.620085556824799600e+00,-1.821855582416168406e+00,6.413801000505675587e-02,1.377073901576458814e-01,-9.338564267780208317e-03,-1.650633297537569710e+00,7.399495933207961285e-03,-1.000000000000000000e+00 +-9.984736554360375271e-02,-1.320494716944914604e+00,-1.330790725290356358e+00,-1.697057370467569104e+00,-1.840069058158641191e+00,-1.647605116110829604e+00,6.373820669563598496e-02,2.957061551848822606e-02,-1.389983119018888491e+00,-3.466061775014659951e-01,2.756236428698094487e-01,-1.954052051555470237e+00,1.912412080119063329e-01,-1.217490196055438290e+00,-1.623270425196640776e+00,-1.414692981415264450e+00,-1.321947685066984457e+00,-1.388634985873986816e+00,-2.403447451229275555e-01,-1.448571437282810015e+00,1.000000000000000000e+00 +-1.883952031352472911e+00,-1.122532631133231762e-01,2.764486074714272434e-01,1.901019606590734767e-01,-1.422934513119327171e+00,-1.843347631771780959e+00,-1.468065739422759197e+00,-6.261002636477708098e-03,-1.618981661560015883e+00,-1.592125152649291220e+00,1.044350868181647130e-01,-1.464179489802784451e+00,1.490295794475857694e-01,-1.047113205882685277e-01,-2.568733682170444288e-01,-2.402027069120222236e-01,3.714837850013332776e-01,-1.819701471860130093e+00,-1.382362341255056970e+00,-1.035735002863291815e-01,-1.000000000000000000e+00 +-1.650019870464853655e+00,-1.806658027340098061e+00,-1.403797902110349716e+00,-6.474476174803717177e-02,-1.926903459829577114e+00,-1.415870302696234262e+00,2.319884728917572664e-02,1.186141708858362703e-01,-1.434183175589905934e+00,-2.050432041023633190e-01,-2.093555481806824714e-02,-1.936321547958794076e+00,5.434852774581955259e-02,-1.179570740375065263e+00,-1.587208761533996926e+00,-1.440860384378481740e+00,-1.644263688499265852e+00,-1.523879281006476960e+00,-2.027536844388049819e-01,-1.524534179686675817e+00,1.000000000000000000e+00 +-1.802990477740712860e+00,-1.352501645045940670e-01,2.440960711464473243e-01,-4.801656576218121808e-02,-1.361297888556253222e+00,-1.691875451030229627e+00,-1.486751210467184281e+00,4.885939370215053312e-02,-1.629492514719542484e+00,-1.713586694934849053e+00,-3.252431019550108132e-02,-1.488537331702065591e+00,1.993187636567363141e-01,-1.778658670455917745e+00,-1.727060553958040368e+00,1.165695389493317385e-01,1.687186825675073276e-01,-1.373241103838188093e-02,-1.409423765488930780e+00,-1.015870841581417716e-01,-1.000000000000000000e+00 +-2.439795576160389834e-01,1.152532094054011663e-01,2.168617152173532847e-01,1.866894323907272030e-02,-1.646879238323857852e-01,-6.997772582165057154e-02,-9.108176612808183484e-03,1.158183025451648279e-01,2.417068706899839770e-01,2.718057870858650582e-02,1.950730446043487387e-01,-1.709516556698534584e-01,2.483579463474902549e-01,2.704376787301095431e-01,-4.982668000162603777e-02,-1.441543163316200138e-01,1.534048820910890731e-01,-1.870535299139338470e+00,-1.771469223944440285e+00,-1.662769152132258510e-01,1.000000000000000000e+00 +-2.341795754986746025e-01,4.762168186027798678e-02,1.693464442342933196e-02,2.483461373386978699e-01,1.253448620658552670e-01,2.081338345781823251e-01,1.332734179988863676e-01,1.108470358722858928e-01,-6.370999672153768167e-02,-1.650066915995046068e+00,-1.334546688506071499e+00,-1.650498092965406172e-01,2.878891556507078286e-01,-1.575315876357988198e+00,-2.287549874178484710e-01,-3.197358082530338264e-02,1.652975458202473324e-01,2.154126319311222559e-03,2.234113834455967451e-01,2.111174464899304160e-01,-1.000000000000000000e+00 +-1.808980495989261694e+00,-2.212655903719411565e-01,-4.289748993112732500e-02,-1.527312149745716052e+00,-1.889086907783862901e-01,-9.630992923883216483e-02,1.303469199482651941e-01,9.399689441044666027e-02,9.766995616252455159e-02,8.890142659765526423e-02,2.541773127400676624e-01,-2.817778219832813846e-01,2.081404177732021354e-01,2.030926442196815951e-01,-1.355886793329663109e-01,-3.622955470645472520e-02,8.246131517108407083e-02,-1.909910293441634543e+00,-1.832820919420656836e+00,-1.482993850504104871e-01,1.000000000000000000e+00 +-1.786743623700731698e-01,2.333610978623791854e-01,-2.317769286988272115e-02,2.678056858207210622e-01,2.830420863340221693e-01,2.583360251014666376e-01,6.007902334857953552e-02,-1.676753537738113475e-01,-1.351090423571608967e-01,-1.439543104631452630e+00,-1.680794493945489521e+00,-2.969694657694614826e-01,1.250456463485115632e-01,-2.179542371792486666e-01,-1.459125680687157534e+00,1.568189547032813602e-01,2.312646421745751524e-01,-1.766893625688763514e+00,-8.111903637227746433e-03,7.916568950765279222e-02,-1.000000000000000000e+00 +-2.670670863779148752e-01,3.040045732407791745e-01,7.814293641534225676e-02,4.659231234598722182e-02,-1.617743354729917482e-01,-2.300903445949606940e-01,-1.621687556751581771e-01,8.252131990860941263e-03,3.992821677531311897e-01,3.517318483013078811e-01,2.383671602944574530e-01,-1.963604713128287438e-01,1.819678838763174600e-01,-1.228480995979723822e+00,-1.723203244659578193e+00,-4.365578367003825067e-03,2.155098480035778918e-01,-2.278620059362060635e-01,-1.726971638801024556e+00,-1.059862787334183848e-01,1.000000000000000000e+00 +-1.575566994330422910e-01,1.795560211713463916e-01,2.682631552317020496e-02,2.007662517788816603e-01,1.581980924416818424e-01,-1.556207542138783317e+00,-1.449804158237339102e+00,-1.210895562639316414e-01,-5.533071193417959943e-02,-8.570354810994065820e-02,-1.502045301027012503e+00,-2.784264656142161898e-01,1.310384711439979699e-01,-1.488857577924443953e+00,-8.111371166148849399e-02,-1.177031639826650322e-01,2.241191842153236746e-01,-6.670881561524841241e-02,7.480619264484354713e-02,2.173972671173703408e-01,-1.000000000000000000e+00 +-1.787231451201681143e+00,-1.874545583443503194e-01,4.154541687070615469e-02,-1.705146527264772338e+00,-3.286917219278586710e-01,-1.936103546223102434e-01,-8.827279795360024051e-02,1.179923057226739749e-02,1.704145899614943060e-01,1.593071847959947607e-01,2.207179404937528178e-01,-2.696259934960010263e-01,2.648712489501740819e-02,-1.325651556789151808e+00,-1.813693726376160775e+00,1.451873112535447818e-01,1.860007285068369376e-01,-2.763206519063196032e-01,-1.832195978436054418e+00,-3.601298690760837418e-02,1.000000000000000000e+00 +-2.100560422946932682e-01,1.484499628425742512e-01,2.444989647549606349e-01,1.609246664696919737e-01,2.301053747903976376e-01,-1.415057182688518322e+00,-1.408164698486420008e+00,-2.405783550339117838e-01,-5.833681479055863317e-02,8.179086755040124168e-02,-1.551703219796524369e+00,-1.282693526730974698e-01,3.455256040877574542e-01,-1.750968919478370633e-01,-1.687088325495360142e+00,2.791974160174996999e-01,1.056122875882148093e-01,-1.705541528252550343e+00,2.342416190795347519e-02,1.295063049478414885e-01,-1.000000000000000000e+00 +-1.423924576211661464e-01,1.921284319409686248e-01,1.468442950975537287e-01,-5.556648021727071673e-02,-2.208593480039719448e-01,-2.099378733063208546e-01,5.313501441645162443e-02,9.703649684887688531e-02,2.871007898980917350e-01,8.824986422275575559e-02,1.708047887759106542e-01,-2.352331277535596732e-01,2.526984764809652728e-01,2.687840957456584245e-01,3.921526339296696473e-03,-1.685011646696626064e+00,-1.397855565475193407e+00,-1.339423834167504879e+00,-1.674757620603984698e+00,-1.621478794819147806e+00,1.000000000000000000e+00 +-2.260360423722762302e-01,2.008593328590530802e-01,2.431177294893352436e-01,-1.309754257792349552e+00,-1.629830824440629655e+00,-5.815124031047488873e-02,2.445471168193985023e-01,-1.485328451052150278e+00,-2.366648012703968518e-01,-1.366125741782338476e+00,-1.350246501934106558e+00,-2.816137600067232793e-01,3.882081085250954633e-01,-1.569269750155042997e+00,-2.212442823205463538e-01,-1.063440726231710820e-01,8.420960607021235611e-02,-8.748164054977126958e-02,1.772677003261586420e-02,-6.235113992909069314e-02,-1.000000000000000000e+00 +-1.736992284809889098e+00,-1.702226743355233773e-01,1.679423364667675389e-01,-1.522912300366130234e+00,-3.742089925693151131e-01,-1.092724662937819002e-01,1.351180000592946961e-02,1.006382864373599334e-01,3.765244896172081357e-01,1.726545677597331740e-01,1.583330105393303544e-01,6.125501487936368950e-02,2.171387687393007693e-01,1.167521671419302631e-02,-6.763294035732590748e-02,-1.796820751677606465e+00,-1.360450205712884886e+00,-1.402203829698527304e+00,-1.772810900332412620e+00,-1.592077081909663683e+00,1.000000000000000000e+00 +-8.260132643703596567e-02,1.506323137841703852e-01,5.015301880318073779e-02,-1.291683870600369488e+00,-1.476730015821815289e+00,-1.517797963673162553e-01,-1.088438463196348543e-02,-1.496706302959756707e+00,-1.128812469775872240e-01,-1.669594973202173094e+00,-1.353208388937376716e+00,-2.855651264253485366e-01,3.092973662683150549e-01,7.599542725378864716e-02,-1.730541964489813456e+00,2.233934729433458277e-01,3.422320311481372723e-01,-1.565095979484781852e+00,7.633830631677407386e-02,1.336755402681864546e-01,-1.000000000000000000e+00 +-1.404076984799650651e-01,1.191317553918698202e-01,-5.457105978545798153e-02,1.374395884758006503e-01,-1.667339667907631606e-01,-2.253394516688803084e-01,-8.404619183367420532e-02,-4.135243822093048749e-02,3.374401550230359259e-01,1.642256742657378921e-01,2.269022987963117965e-01,-1.905198098429376208e-01,1.607784713243614882e-01,-1.335010754414554190e+00,-1.774638571230396700e+00,-1.316770162827737067e+00,-1.247768977256307421e+00,1.920813975907403326e-01,-1.758890154793151472e+00,-1.682956658738964606e+00,1.000000000000000000e+00 +-2.022386142575156864e-01,1.336806471945872765e-01,2.875705692623020671e-02,-1.565704532131195537e+00,-1.481319864862788105e+00,-1.764617757145244159e+00,-1.488746964039890708e+00,-1.681955182045430375e+00,-9.291659391132862933e-02,6.570542869799977925e-02,-1.521660326132671326e+00,-2.335841231780483029e-01,2.916632795978746362e-01,-1.702229927469582238e+00,-3.194907254166401422e-01,-1.977399998211519516e-02,1.482855204698622364e-01,-1.361706824813516403e-01,1.397701967238868526e-01,-1.785281155111982654e-02,-1.000000000000000000e+00 +-1.710675675150777897e+00,-6.889656381993650158e-02,5.336733035525358798e-02,-1.728259254603254025e+00,-4.287565833351879574e-01,-1.351761449493077416e-01,1.708338029678988235e-01,9.443669862142795024e-02,3.810947838806565402e-01,2.831015210974562324e-01,2.806288730489565619e-01,2.644478370657210586e-02,2.074396752519246934e-01,-1.368615791967153106e+00,-1.721035943584906747e+00,-1.556529571274913648e+00,-1.339529546342336896e+00,2.604816863605534660e-01,-1.604105136993718927e+00,-1.545370663450510218e+00,1.000000000000000000e+00 +-2.539133094953536784e-01,9.952600458798718797e-02,-9.406514181245290152e-02,-1.296232657332213600e+00,-1.370233898875531153e+00,-1.703616285965132482e+00,-1.464099282246143829e+00,-1.694221421276576489e+00,2.256370931205706198e-02,2.214405366647632378e-01,-1.342361032399055798e+00,-8.651427614168735736e-02,3.670801808416190304e-01,-7.240739261390924386e-02,-1.929572319215834941e+00,1.489434662442567781e-01,1.409932506741681246e-01,-1.696455953850054543e+00,3.076169633660839203e-02,1.804048154257257841e-01,-1.000000000000000000e+00 +-3.396623784028023341e-01,-1.459286752093374107e+00,-1.562384720334421218e+00,-4.378878165881100459e-02,-2.317601361455025877e-01,-1.581590712001686105e+00,3.483088885333713441e-02,-1.974632901043600619e-02,3.235861789429929081e-01,1.827132479463205450e-01,1.058161714238032985e-01,4.567541010519204914e-04,-6.143133313296494924e-02,2.801437997963560078e-01,-2.951779216041229559e-02,-2.872110181937215234e-02,2.602588034669344186e-01,-1.653150479042223786e+00,-1.858187304053193545e+00,-1.437127986318458006e-01,1.000000000000000000e+00 +-2.156700822111204396e-01,2.856154781462307835e-01,4.358355008224418237e-02,1.055124854187428074e-02,2.930327847274443731e-01,3.196338488580008907e-01,-1.228284280631395109e-01,4.776292530106697465e-02,-3.383587202693603968e-02,-1.524384588998426215e+00,-1.557843119323771619e+00,-1.150868439085395989e-01,2.526669630902841357e-01,-1.650505190739935291e+00,-1.813030026693628338e-01,-9.010045015269224633e-02,2.665444142050752419e-01,-1.706644508361277790e+00,-1.545969851106116622e+00,-1.522187528667455769e-01,-1.000000000000000000e+00 +-1.801174539362771609e+00,-1.816188763163395503e+00,-1.497895285526630538e+00,-1.812059156867410703e+00,-7.265023004259998918e-02,-1.750380778769931256e+00,4.886934369858730171e-02,-9.685963324469006042e-02,4.122079114066758976e-01,3.623485172182143721e-01,3.723119939019490232e-01,-2.074123149641814434e-01,1.542832311789590904e-01,1.811906386589723084e-01,-1.374469101163109108e-01,-4.452547192311231972e-01,6.082083527296935910e-02,-1.638050369059725675e+00,-1.777153188779318382e+00,-2.300459371093563909e-01,1.000000000000000000e+00 +-4.451531151184778823e-02,9.148420013289577934e-02,2.534058268925715729e-01,2.054960265755059190e-01,2.785636652081447395e-01,7.879173701551790698e-02,1.541296869682636655e-02,4.721268837444195232e-02,-7.916032429184548247e-02,-1.561824407894278677e+00,-1.555743657094234900e+00,-4.816314658093525658e-01,2.938611281680565401e-01,-1.650182324738711237e-01,-1.745215565765939170e+00,2.138468654552170833e-01,2.647899305847716267e-01,-1.885477440951396633e-01,-1.354105012801542607e+00,-1.185313722870132702e-01,-1.000000000000000000e+00 +-3.862363481910549012e-01,-1.484854201284331321e+00,-1.438572421643739885e+00,-4.142546019707953747e-02,-3.348652602872161377e-01,-1.639473204331644007e+00,7.197267147107569429e-02,-1.094711591033338122e-01,1.916100059435999481e-01,1.104510484539653337e-01,3.024398227715986431e-01,-2.130822796708342626e-01,9.486294496163666379e-02,-1.512413978024118810e+00,-1.735754066485621028e+00,3.041004585394775916e-01,3.318818660567996459e-01,-3.089777197151281629e-01,-1.931654961106461510e+00,-1.354386261256982082e-01,1.000000000000000000e+00 +-4.303605809054293907e-01,2.177837137677910428e-01,3.031313748889638560e-01,1.093102325839635514e-01,7.076542489851209150e-02,-1.179183095294744188e+00,-1.505863307879028001e+00,1.365853728016261648e-01,-1.305806890730770442e-01,1.147488698401981644e-01,-1.610618557095213621e+00,-1.918134292915106287e-01,-1.336499559189097130e-02,-1.718240000988179306e+00,-2.040449602330622614e-01,-1.110775901676131772e-01,1.881237390840745927e-01,-1.630215767900932144e+00,-1.695233570779625776e+00,-2.628398216005405075e-02,-1.000000000000000000e+00 +-1.677091039606229739e+00,-1.865202226684187892e+00,-1.449574026374467861e+00,-1.606683272540252228e+00,-2.477549056295799135e-01,-1.606862935006536430e+00,-1.615279137175159307e-01,8.924077723319796818e-02,-9.582226197238646881e-02,8.636847459609645061e-02,1.077270557392905337e-01,-2.009099826210908879e-01,2.933490456390652756e-02,-1.248006919167750528e+00,-1.698874983878829958e+00,5.081814598152770146e-02,1.478842899130003719e-01,-1.493291054343111601e-01,-1.871050668915237081e+00,-1.179997572996087513e-01,1.000000000000000000e+00 +-3.328088271449912705e-01,3.498802263190264883e-02,1.731632906847828668e-01,1.552086898298051520e-01,1.565348255437749581e-01,-1.435732159384214057e+00,-1.366962610615669815e+00,-2.218411404297142286e-01,-9.745524394934233781e-02,1.438923028868028253e-01,-1.542571478877929447e+00,-4.983556069812025702e-02,2.354435482550457370e-01,-2.203854683616549015e-01,-1.646878114276301819e+00,1.502885180994540937e-01,3.258911374658394200e-01,-9.419806122841692386e-02,-1.577419009343649092e+00,-1.793921236206705849e-01,-1.000000000000000000e+00 +-5.430212539709483588e-02,-1.401301184415392642e+00,-1.620363609016238993e+00,-3.140725486911737085e-03,-2.840792002816733830e-01,-1.631557432178066369e+00,-7.907571158916459897e-02,-1.672689249186963545e-02,2.820765846230589569e-01,1.924189493545946927e-02,9.424944259944431635e-02,-2.025209418751356960e-01,8.007159720548398529e-02,2.718485183061024046e-01,-3.356755891540847414e-01,-1.539747283092349317e+00,-1.283979298093514076e+00,-1.379367054633772405e+00,-1.822919127052061272e+00,-1.655120047632869129e+00,1.000000000000000000e+00 +-1.828648407826313771e-01,1.303669714280714931e-01,3.281634341282182454e-01,-1.331218717902154625e+00,-1.440335771332840631e+00,-8.438256293891646664e-02,-3.016506776942890627e-02,-1.635986432710933247e+00,-5.706716830303201760e-03,-1.519308637308587340e+00,-1.538826082544888507e+00,-1.874635769544862052e-01,1.740443531291482604e-01,-1.642323730198749665e+00,-1.818284598289054976e-01,-1.813919099043513616e-01,3.912970162390819739e-01,-1.690514193262345755e+00,-1.560177869678597284e+00,1.489094548564861564e-01,-1.000000000000000000e+00 +-1.719337784952710724e+00,-1.787163526386397194e+00,-1.493037120732656930e+00,-1.415933286885403763e+00,-1.487854202069676668e-01,-1.583751903934811356e+00,6.103681952781319503e-02,-7.524412701058486030e-02,8.375219989444812607e-02,2.142504794038084248e-01,1.438223961253209671e-01,-1.304850311421204001e-01,1.067574007228712235e-01,2.909954696904511473e-01,-1.670366864727289014e-01,-1.689081678638551631e+00,-1.528707618705964943e+00,-1.430534163439965756e+00,-1.645195206065076965e+00,-1.604056159169864548e+00,1.000000000000000000e+00 +-2.083438875860270934e-01,2.335965437908922027e-01,1.071707499091033577e-01,-1.288246750213543512e+00,-1.378517278944689561e+00,-1.707696384816015933e-01,-1.568376077712627659e-01,-1.518355268363408062e+00,-2.629407773909601675e-01,-1.565686260935592600e+00,-1.628133544842195635e+00,-2.354517478541177933e-01,2.634825782470920918e-01,-3.164200285627927212e-01,-1.728366907590010992e+00,3.490475847534628140e-01,1.841402464753418011e-01,-1.677322903246478147e-01,-1.548509155377692847e+00,-1.388963128235166933e-02,-1.000000000000000000e+00 +-8.140862605581608569e-02,-1.531952677721658107e+00,-1.579822628334136114e+00,-2.158986542727301949e-01,-4.753407251577410064e-02,-1.939691485609706056e+00,-9.736562988277902564e-02,1.229744127047200652e-01,3.433432862619288928e-01,2.159351090051743194e-01,9.719607419281767191e-02,-1.291642903895658112e-01,1.461109897296656579e-01,-1.125938608592356616e+00,-1.704452238561950717e+00,-1.517897004328281119e+00,-1.443169289655185761e+00,1.057877446144747657e-02,-1.738438363948879628e+00,-1.854476765398893834e+00,1.000000000000000000e+00 +-2.863243735617415076e-01,2.365670741337101979e-01,8.903467814116583501e-02,-1.457571714714968714e+00,-1.246015694584325262e+00,-1.673825131524844467e+00,-1.612638785923174112e+00,-1.541490244400076648e+00,-7.079531019797660751e-02,1.034955758663737568e-01,-1.377897664362565777e+00,-2.981016076976940044e-01,6.539238018431087207e-02,-1.538001995939093369e+00,-8.463110382048388836e-03,-2.875371666447258667e-02,2.202154047120651326e-01,-1.681889004787793684e+00,-1.442212170651307579e+00,-1.412194893631334713e-01,-1.000000000000000000e+00 +-1.762130690462024862e+00,-1.668951046054449217e+00,-1.674424950970456072e+00,-1.565170102148560227e+00,-2.258421072760552206e-01,-1.724891117061284174e+00,5.276235571442480093e-02,2.270031654449717884e-01,2.078060747906807226e-01,7.275412066186140447e-02,6.531493706493668450e-02,-2.285500410644309699e-01,3.294273728423363634e-02,-1.394424769471787506e+00,-1.697314211487732205e+00,-1.423639524438613524e+00,-1.392870813809807151e+00,1.297739428189970123e-02,-1.737016674483755407e+00,-1.702235737301599272e+00,1.000000000000000000e+00 +-2.829052725322153128e-01,1.053995753974671934e-01,1.243137310869227297e-02,-1.475376154478991619e+00,-1.273230705412238040e+00,-1.788930468374629879e+00,-1.512484092449806861e+00,-1.494579560309224897e+00,-1.107833022791021582e-03,1.922377008658001513e-02,-1.489411245227656666e+00,-2.360759778040930901e-01,6.220074322926733013e-02,3.836805888715498492e-02,-1.638786439357833302e+00,6.454983442961406692e-02,8.443579955664209935e-02,-4.456643119187696234e-02,-1.535275340651167886e+00,-2.510975333924962000e-02,-1.000000000000000000e+00 +-1.888353826195675322e-01,2.331116445443457141e-01,7.209972267287956260e-02,9.936601978177159178e-02,-2.998875281976747242e-01,7.736684513841683708e-02,-4.154085777495976350e-02,-1.620416913858260699e+00,-1.536378078562709693e+00,-4.970986093233982495e-02,8.054914235365649711e-02,-1.866448787992637248e+00,1.631511674540167545e-01,2.540076346857715328e-01,-3.365790456232687494e-02,-7.210663219225983689e-02,2.482153857969794930e-01,-1.883598696167930964e+00,-1.810466289958495345e+00,-4.205707354893116479e-02,1.000000000000000000e+00 +-1.884999296168834215e+00,-3.112843589472975570e-01,2.510927801806384085e-01,-1.626425934354120084e+00,2.906727097445114816e-01,1.446522529476227947e-01,1.749560835749299215e-01,-1.309041609415544094e-01,-4.164120115564299424e-02,-1.612352644191952677e+00,-1.524397432729068580e+00,-3.156040382378184628e-01,2.208343021322482225e-01,-1.509988895926045904e+00,-2.292660974459153067e-01,-8.014049356864991203e-02,3.799991368343439424e-01,-3.734193110346489819e-01,1.073939538345585615e-01,1.177208510405130526e-01,-1.000000000000000000e+00 +-1.680459216951007306e+00,-1.929987354809645228e-01,2.338483618359407656e-02,-1.613930291721018984e+00,-3.415917136958097622e-01,-1.348783215985026596e-01,1.181432289240746669e-01,-1.585137163766282553e+00,-1.534313066984771767e+00,-1.310491011825282714e-01,1.940131849865792124e-01,-1.733134803485204101e+00,9.973672008270378919e-02,2.478634840235067749e-01,-1.556240280163657952e-01,-7.091430903922907314e-02,1.834628758540778060e-01,-1.854266729031440519e+00,-1.714678789261387504e+00,4.735125599413148034e-02,1.000000000000000000e+00 +-1.735701321098774619e+00,-3.029488293090203399e-01,7.823142982724853867e-02,-1.521530706942672229e+00,1.518072634749043082e-01,1.949321976666617273e-01,1.963842715291226615e-01,5.344791979781139679e-02,-2.110863568467451212e-02,-1.562202197067320109e+00,-1.642579250325277629e+00,-2.377295785858298816e-01,2.815042624544672467e-01,-1.414518361758450027e-01,-1.803016334354505812e+00,1.813624402682965375e-01,4.385810848825134611e-01,-1.917785633493478192e+00,1.837256278364261808e-02,-5.286230436914544917e-02,-1.000000000000000000e+00 +-7.199972550916522440e-02,7.255178596017852755e-02,9.750889304507877331e-02,6.081409242016647121e-02,-2.376473500031918706e-01,-8.982997357940228356e-02,6.192461817998318879e-02,-1.572683273506855661e+00,-1.433725385605350677e+00,3.854035867080965461e-02,2.541416204131221601e-01,-1.671030333565145964e+00,2.400026694987877907e-01,-1.278116954806505667e+00,-1.724615716649997577e+00,1.107077198748998292e-01,1.780571421151442291e-01,-1.284590954348128200e-01,-1.723855140182853019e+00,-1.584402831845356241e-01,1.000000000000000000e+00 +-1.833390667392359896e+00,-2.171180825066165443e-01,1.960964068290079465e-01,-1.392986539304137628e+00,1.584083272860981051e-01,-1.397279559595808962e+00,-1.576413536407396565e+00,2.161410269269601475e-02,6.515055618245384206e-02,3.308749079517017688e-03,-1.291117871585367061e+00,-1.183363545055007710e-01,1.098549087884565512e-01,-1.478121029211044268e+00,-1.186132878675661695e-01,-1.259727635628279940e-01,3.409386167645193089e-01,1.399151801230701653e-01,1.518843333971090503e-01,-1.522919555040948003e-01,-1.000000000000000000e+00 +-1.657067455176580939e+00,-1.956006772697657103e-01,4.866738214374304039e-02,-1.520530216719676808e+00,-3.557408683673492500e-01,-4.834124711662751012e-02,-6.597629840328217421e-02,-1.446866591736235552e+00,-1.478399872957024686e+00,3.409000577232640383e-02,1.290969519790106101e-01,-1.647536979880922514e+00,5.373522734854102589e-02,-1.392181127118939710e+00,-1.613254635171154527e+00,1.703774576150753617e-01,2.226799427848425439e-01,-1.377850789387303210e-01,-1.687682883865781136e+00,-1.182185390507711903e-01,1.000000000000000000e+00 +-1.828577673095696188e+00,-1.615210861024604649e-01,1.496379005811858676e-01,-1.408848830726687096e+00,1.157878170169065607e-01,-1.186607809631150978e+00,-1.557056686931114209e+00,-3.318515883453987403e-02,6.565717225075094754e-02,2.149917478930231662e-01,-1.557595536413655557e+00,-3.593265080683860635e-01,1.885300156998934995e-01,1.078510889679324264e-01,-1.708102050101451663e+00,1.581840693686221433e-01,3.807921891383032609e-01,-1.643429295982688831e+00,-5.589858940038970603e-02,1.439733738179163058e-01,-1.000000000000000000e+00 +-2.014535912011794216e-01,1.464013503426886964e-01,1.583118228851058129e-01,-1.311144004189748191e-01,-2.933891499481025567e-01,7.171208358498094848e-02,6.713523942833959923e-02,-1.462855678228510481e+00,-1.258580727846380531e+00,-2.498970101252624421e-01,3.144182986818298553e-01,-1.585877416016334474e+00,7.929365415811465145e-02,5.093170250068383975e-01,5.087171443260374293e-02,-1.835895741151551963e+00,-1.262516885170584136e+00,-1.497584680792570122e+00,-1.802072848342316513e+00,-1.815473843169535861e+00,1.000000000000000000e+00 +-2.065909537021775932e+00,-7.713236544590557076e-02,3.292057384357651717e-01,2.336083146754882600e-01,-1.283071514341488806e+00,-2.146017387445667002e-01,9.831816996616363213e-02,-1.529687239368231699e+00,-6.557823286419892095e-02,-1.555593879643420774e+00,-1.394966934007892512e+00,-2.834226752862956666e-01,4.210734554677593722e-01,-1.763667489644399122e+00,-1.567802012234626174e-01,-1.153855943401591311e-01,2.188752837490041958e-01,3.527404622114355126e-02,1.206088262135300293e-01,1.451652686106577050e-01,-1.000000000000000000e+00 +-1.978367994037992572e+00,-2.661520016824179824e-01,4.640207350525141217e-02,-1.330826471055515992e+00,-9.127315224419021988e-02,1.120521841253903222e-01,-3.989370800105224851e-02,-1.428621944608968741e+00,-1.434193447270862842e+00,-1.710020712058683279e-01,1.776727481698182620e-01,-1.649790298552828993e+00,1.521028295464029734e-01,3.243600900035593093e-01,1.442058524107271700e-02,-1.695752680588687333e+00,-1.436336009194452590e+00,-1.408519458204670283e+00,-1.650603052771111301e+00,-1.542717696333763877e+00,1.000000000000000000e+00 +-1.707933373529028742e+00,-1.442728049153429448e-01,1.577493833657936106e-01,4.482661014397282262e-02,-1.479160225441092491e+00,-3.834935798107614868e-01,-7.472091006248304801e-02,-1.490247557780237653e+00,3.554589831091523044e-02,-1.567078881776201582e+00,-1.411662679994448499e+00,-1.907050100663965075e-01,2.008971762137442552e-01,-3.383766032115673866e-02,-1.602868897263930226e+00,1.111484426669004516e-01,9.252353490794451574e-02,-1.533010258660377101e+00,1.832494954384461527e-01,2.531780987473795097e-01,-1.000000000000000000e+00 +-6.035667714771275616e-02,6.470808836553376686e-05,1.024276970655027263e-01,-4.101435867772194876e-03,-3.071974488665177860e-01,-1.036708515513684176e-01,-1.794222268096470549e-02,-1.542565533807144673e+00,-1.255950023731022824e+00,-1.004970149235214771e-01,3.376773188277004678e-01,-1.816046997765331517e+00,1.724808629832310369e-01,-1.427182072473219598e+00,-1.552065473637629367e+00,-1.388188809964528758e+00,-1.356036369860796675e+00,1.720711068138204847e-01,-1.791030757359659109e+00,-1.734188633268321178e+00,1.000000000000000000e+00 +-1.788632517301677716e+00,-2.617386839599858406e-01,-2.936679162613395344e-02,-2.740391682109855065e-02,-1.297405165664371918e+00,-1.493482798529176536e+00,-1.514862991219524391e+00,-1.501596256478166191e+00,1.119108588338410537e-02,1.660142063933439105e-01,-1.475156599370080812e+00,-1.933276597493817639e-01,3.668878364649680868e-01,-1.640057643036256341e+00,-6.608321478067419319e-03,-1.954839037314018424e-01,7.896595294917038710e-02,-4.431425520113368988e-02,5.677358752661965796e-02,9.539055629951720405e-03,-1.000000000000000000e+00 +-1.856006504665463019e+00,-1.777447493266164913e-01,4.881178249972623756e-02,-1.568636636291946385e+00,-2.852035358681297983e-01,-1.851432584160719041e-01,1.197743117151101877e-01,-1.616994063691078232e+00,-1.383230983243273648e+00,-1.602217973295344267e-01,1.471966636387322114e-01,-1.825824796344011292e+00,4.942638947399052851e-02,-1.251995243989245887e+00,-1.694599822855320292e+00,-1.448203897775477866e+00,-1.420129063946256709e+00,1.197172510960335112e-01,-1.847869231225990561e+00,-1.900423255698547198e+00,1.000000000000000000e+00 +-1.938517563807695243e+00,-1.176138443868801620e-02,9.293952995888341007e-02,1.396608196283463110e-01,-1.335335518405849742e+00,-1.907449672753195102e+00,-1.628182913393507070e+00,-1.574695438705880024e+00,-1.678398946053656915e-01,3.380506491156565207e-01,-1.617627992223649391e+00,-2.401866425100306346e-01,2.642820025649577076e-01,-1.972760935393504111e-02,-1.830708386519253494e+00,4.779916325500892238e-02,2.844369872034545610e-01,-1.536844096857871378e+00,2.901214956634431269e-02,7.841534439478502205e-02,-1.000000000000000000e+00 +-2.696591070101692633e-01,-1.384688193113345678e+00,-1.498195562579526685e+00,-4.585590496562568646e-02,-2.210743617984119169e-01,-1.553133811849615631e+00,-4.612209217966583424e-02,-1.748276247153416119e+00,-1.405630394575534181e+00,-2.789849179701651138e-01,1.456286412021242926e-01,-1.692047959274368019e+00,2.863753688495023209e-01,2.939749539944037648e-01,-1.074634812087749969e-01,-9.361810643094208018e-02,6.991647225105473984e-02,-1.818221290983097038e+00,-1.912601633301234116e+00,-1.175312837766143148e-01,1.000000000000000000e+00 +-1.896447579353508495e+00,-1.097519778964581999e-01,1.029161177080544221e-01,-1.405474213317246646e+00,3.927025135617715490e-01,-9.727119249037760595e-02,8.051521423641112840e-02,-1.760626230279384308e-02,3.297087136498941251e-03,-1.452286395193483015e+00,-1.477747376763081988e+00,-1.297945053187743170e-01,3.227665455531983918e-01,-1.622388934463279808e+00,-1.703479678209234849e-01,-9.740909277515255593e-02,3.072525292795419971e-01,-1.848078716688827727e+00,-1.489256089766898716e+00,-4.441061523815337253e-02,-1.000000000000000000e+00 +-1.737645268732505244e+00,-1.623708628553631517e+00,-1.423996224171091063e+00,-1.405421788882911471e+00,-2.643303970591006635e-01,-1.794543486411528166e+00,-3.986729681823630855e-02,-1.596080866086237338e+00,-1.258205450819601889e+00,-3.024064760459701517e-01,8.855568852874931562e-02,-1.692147575482150001e+00,6.168322431109037052e-02,3.332373407523177566e-01,-5.444064776614818829e-02,-1.589273107450466793e-01,3.395310182394535303e-01,-1.859003577123959250e+00,-1.632951756930816067e+00,-1.503391061938186279e-01,1.000000000000000000e+00 +-2.001237531819056059e+00,-1.888309866090089606e-01,1.327341408731530226e-01,-1.470839520049634475e+00,2.117324793461772270e-01,1.710111718709355078e-01,-5.130752647826371249e-03,1.074725059185548393e-01,6.458821145140310194e-02,-1.524867715437911109e+00,-1.397109700959722423e+00,-1.599785670409428762e-01,2.758900698739424340e-01,2.391694294421903288e-03,-1.747657762294583250e+00,1.388205165123361784e-01,2.935091611745938023e-01,-9.573406562344149995e-02,-1.405575528569151800e+00,-5.505123569376621506e-02,-1.000000000000000000e+00 +-2.415894917066411518e-01,-1.339288850842404788e+00,-1.558080312315683669e+00,-8.794419585011527263e-02,-1.887423150854516951e-01,-1.603137363622819223e+00,8.521341845397489845e-02,-1.616603324363009797e+00,-1.335036905244106675e+00,-2.740610071985419283e-01,2.950184450795598767e-01,-1.871071861455992025e+00,2.602005367930751434e-01,-1.375536276457179463e+00,-1.635367593925601826e+00,2.743069787887959521e-01,2.948093318002342200e-01,-2.163009607736440798e-01,-1.647124865158788420e+00,-6.861962110242694757e-02,1.000000000000000000e+00 +-1.703216504730926006e+00,-6.387152405651808085e-02,1.390947393323525572e-01,-1.476562929876921615e+00,2.312946350175269228e-01,-1.401775340349431742e+00,-1.540237519879024797e+00,-1.737868580789893660e-02,-8.631242477821583770e-02,-1.068932328222133021e-02,-1.299781420830113943e+00,-1.957980018651323817e-01,2.200184664051651195e-01,-1.749701421790878175e+00,-1.370682515420905134e-01,-7.887318520251951348e-02,1.967623844981341019e-01,-1.802102148505244417e+00,-1.492533598037102394e+00,1.176820069900192806e-01,-1.000000000000000000e+00 +-1.668472440053598849e+00,-1.692714191205419771e+00,-1.511862118036053859e+00,-1.536036795174505531e+00,-4.130098449562970009e-01,-1.743609344136944017e+00,1.136747901042646780e-01,-1.348305275896040545e+00,-1.469801700766598440e+00,-1.698886193741123429e-01,2.087590028510214746e-01,-1.521163599292838731e+00,1.115255063517616024e-01,-1.278257121548038278e+00,-1.599898969078416666e+00,-3.734256357462203968e-02,1.714193737288485508e-01,8.378525831000405155e-02,-1.738692901858964746e+00,-2.183929967042638753e-01,1.000000000000000000e+00 +-1.835595193407608017e+00,-1.588688468422716216e-01,1.071038085108794863e-01,-1.431996774396235184e+00,3.389680600264142196e-01,-1.471000951585167416e+00,-1.513097497047408924e+00,1.292609311097401847e-01,-1.994413707680271486e-01,-4.228260372016211244e-02,-1.477577167216317600e+00,-3.847715451458932900e-01,6.221557352316370304e-02,-1.632352539781317668e-02,-1.807777450957433807e+00,1.954061683424508367e-01,1.916242989475189784e-01,-1.637500830595114976e-02,-1.480493939244007962e+00,1.073340210416912216e-01,-1.000000000000000000e+00 +-2.627949199463695540e-01,-1.311601591218620078e+00,-1.475211753932925696e+00,-1.304726359806189229e-01,-2.367698850744863193e-01,-1.608094359485233937e+00,-1.746864918809430955e-01,-1.523101588101070325e+00,-1.397629073881587303e+00,-3.822912186360122311e-01,1.458091856878317716e-01,-1.886788841133814953e+00,1.593097248506974428e-01,3.554702193506375507e-01,-1.074756851113357475e-01,-1.697499802761408327e+00,-1.554898521660907695e+00,-1.524011092956258251e+00,-1.838783882743532994e+00,-1.754451182941514809e+00,1.000000000000000000e+00 +-1.916822041755505523e+00,-1.994469698241685129e-01,-4.933632276222865709e-02,2.863965164922512296e-01,-1.423380359870673351e+00,-2.279957244996119181e-01,9.397472235863760237e-02,-1.716764114105343131e+00,-1.787351583082517070e-01,-1.414650843336660246e+00,-1.617666909739836179e+00,-2.518808571838557775e-01,1.685049104084261895e-01,-1.761630151708398762e+00,-2.776895007447031105e-01,7.729513967419639198e-02,2.379315862066027076e-01,-1.612209955935588468e+00,-1.483619705832376567e+00,-3.020913529259734720e-02,-1.000000000000000000e+00 +-1.867044884773448876e+00,-1.472673527705081753e+00,-1.649374480873115889e+00,-1.670143217194038465e+00,-3.070842398040137811e-01,-1.754939077683911108e+00,-1.423354955399841748e-01,-1.436019218204293457e+00,-1.418730579984796680e+00,1.079839128213427868e-02,4.152375627128810365e-01,-1.795347933011969488e+00,1.158722101302399332e-01,2.850322800467037232e-01,-1.398292267306494030e-01,-1.862325214132927176e+00,-1.441179708403775406e+00,-1.496897251365243431e+00,-1.622765660460842252e+00,-1.735839599488196994e+00,1.000000000000000000e+00 +-1.677393660314144386e+00,-3.394741131965792746e-01,1.944080460453487813e-01,1.723628021977686720e-01,-1.538798771158149226e+00,-6.468438635467047004e-02,4.107668666750106945e-02,-1.655328577280850588e+00,-8.153552575454094120e-02,-1.545242692158941056e+00,-1.250308347300965517e+00,-1.377305074670933382e-01,3.616201693068952228e-01,-5.447383234045161116e-02,-1.614484662310011087e+00,1.439056111517483805e-01,3.715683101161327828e-01,-4.589024722426153502e-02,-1.545033891606071963e+00,1.469928408203699521e-01,-1.000000000000000000e+00 +-2.292484754950680492e-01,-1.404350897526567721e+00,-1.578611367140718258e+00,-7.230342715986382007e-02,-2.232169484525267644e-01,-1.630610638600653806e+00,-1.399195313728806601e-02,-1.517284057248873941e+00,-1.373463738897101383e+00,2.390939499616162589e-03,4.525627012987060160e-02,-1.595028842936575320e+00,2.367456900519744889e-01,-1.294368160546527591e+00,-1.641006081856117405e+00,-1.360542163032010698e+00,-1.333418534206741279e+00,-1.075695980286662801e-01,-1.710501050537750034e+00,-1.680464972887257602e+00,1.000000000000000000e+00 +-1.824320348288306359e+00,-2.242519383694826507e-01,1.939500729578774729e-01,2.580759419678572697e-01,-1.347001387351347779e+00,-1.949872616982624818e+00,-1.565159184620138477e+00,-1.607137737383463438e+00,-1.336710862730201010e-01,9.077958480873264679e-02,-1.500475897103216383e+00,-8.793965232887804295e-02,-5.893354486171925588e-03,-1.552925293547802843e+00,-6.394711786702439449e-02,-1.428272298215615133e-01,2.401458986440296872e-01,-1.599474863735532937e+00,-1.570795285887488513e+00,4.971420756740695113e-02,-1.000000000000000000e+00 +-1.849266502460056527e+00,-1.536793408891875767e+00,-1.450331064273818971e+00,-1.662558656273171565e+00,-4.022276506934110429e-01,-1.613844321683342686e+00,4.982096749266174546e-02,-1.344866860762329974e+00,-1.254560206002134581e+00,-4.927212993065804847e-02,2.232242021749195338e-01,-1.722173281038203330e+00,1.953070056701929624e-01,-1.385138482621590006e+00,-1.547991780235024484e+00,-1.393118208201791886e+00,-1.351351286137255858e+00,9.987190245233887320e-02,-1.744067585086658490e+00,-1.654447997079727006e+00,1.000000000000000000e+00 +-1.864387728549524947e+00,-1.528811130414389763e-01,-4.480088130063339391e-02,2.433468408664729576e-01,-1.337242124972898516e+00,-1.847268380922106656e+00,-1.392291585547845933e+00,-1.696507564704244908e+00,-1.141758842819323161e-01,2.200858804252461831e-01,-1.449409253919071494e+00,-1.095331009378930492e-01,3.895191444116400392e-01,-1.685609108953693702e-02,-1.676026096460398795e+00,4.057755272416286446e-02,1.943936625867365986e-01,-3.801300302824268673e-01,-1.371802968399387090e+00,9.391816282396751869e-02,-1.000000000000000000e+00 +-2.105163915005565378e-01,5.829521326453870889e-02,-2.330801027164454420e-02,-1.343458878646294652e+00,-1.820581971638018492e+00,1.467537468396624134e-01,-8.328161426570454551e-02,-1.639913835556530364e+00,4.188109869227139015e-01,-4.448674629190724206e-02,3.122778846918302653e-01,-2.497090139123326336e-01,1.756516076350515820e-02,3.384156809903209329e-01,-9.083422385048443848e-02,-1.565818393465682468e-01,2.942333949711795626e-01,-1.752967436998053019e+00,-1.810309952758458296e+00,-4.756885371580615862e-03,1.000000000000000000e+00 +-1.697203936453344220e-01,2.164472413569353826e-01,9.900206135490280390e-02,8.897910041792289615e-02,1.450299974962855343e-01,4.101970544519639716e-01,-1.523634557246537208e-02,-1.509339160259876911e+00,-1.668266124724471222e+00,-1.582167767407107961e+00,-1.457214891393150191e+00,-2.022051936238908443e+00,1.819840730643635385e-01,-1.664136752130559005e+00,-1.705964505243858786e-01,-3.848621840533895944e-02,1.598246614759032957e-01,1.169500345506576389e-01,2.116352785084738564e-01,-2.875304415091849142e-02,-1.000000000000000000e+00 +-1.741522805904053062e+00,8.242275280928418324e-02,-6.731059268197489565e-03,1.338939803846624366e-01,-1.600336378396729486e+00,4.710828127688442002e-02,6.274461946128231937e-02,-1.459686109124986864e+00,3.554908433498816800e-01,1.550998708565357875e-01,2.400451279624569723e-01,-1.243357785412936567e-01,3.592931021600022645e-01,1.877076490277039544e-01,-4.788271790708865938e-02,-1.406023283584208750e-01,1.467205421994474679e-02,-1.861514220027873812e+00,-1.764920111008901227e+00,-2.203720929353735480e-01,1.000000000000000000e+00 +-2.979572688018531923e-01,4.381956681169807410e-03,2.399075434135144458e-01,1.491509945883924271e-01,3.286238888827819893e-01,1.205438224858945140e-01,9.462363301464880860e-02,-1.549568351713531245e+00,-1.759306043954263155e+00,-1.641338465834012883e+00,-1.680553559052160084e+00,-1.711162544065447655e+00,1.604917534636884080e-01,-1.945737205269550824e-02,-1.804900487344063276e+00,1.830046723190849600e-01,2.744771128864742504e-01,-1.716962203553597188e+00,-3.405020573273205564e-02,1.375886700854809241e-01,-1.000000000000000000e+00 +-1.955229329530519811e-01,1.478730638909200867e-01,-1.201783668794771665e-02,-1.560107922479264664e+00,-1.924336397026852774e+00,1.066808076913345615e-01,-8.443806862302799532e-02,-1.577717476909032968e+00,2.284650283079793232e-01,4.632076426126892355e-02,1.730652179581867456e-01,-1.300486417009283913e-01,1.911590199077310703e-01,-1.242333931901333788e+00,-1.526271776844993822e+00,3.141662632691351320e-02,1.167175183099673497e-01,-7.679233401081934918e-02,-1.768567765354970955e+00,-2.365405085762902759e-01,1.000000000000000000e+00 +-9.054096414312204355e-02,1.452948527338082663e-01,2.888070720707515138e-01,2.884057962486949456e-02,4.326722243748587493e-01,-1.364266619381323320e+00,-1.778410777044421742e+00,-1.739871896155328335e+00,-1.805356701043865142e+00,-1.150138933454133705e-01,-1.650812990334628472e+00,-1.537598752479355513e+00,2.521751071627383878e-01,-1.451461304705234934e+00,-7.738008865931172986e-02,-4.056494846509665908e-02,2.136542078638204167e-01,-1.842982905436176821e-01,6.353070317174616677e-02,1.409348599713760031e-01,-1.000000000000000000e+00 +-1.678331761292732871e+00,-2.896760072346654113e-01,1.000901919821304137e-01,-2.819962035219550356e-02,-1.774843789401664873e+00,-1.008161454806341673e-02,-1.379590036791146224e-01,-1.499320264123623403e+00,4.195404364322782853e-02,7.414101326892059973e-02,3.442438587840405129e-01,-1.055178104561328434e-01,1.166994867852243628e-01,-1.522151470880838575e+00,-1.667038946022956658e+00,6.227841045719036128e-02,1.199008504703146932e-01,-1.739190013501349663e-01,-1.865603480026541083e+00,-1.899886295331251751e-01,1.000000000000000000e+00 +-1.113579885006178716e-01,2.124980417005476185e-01,2.538609187701522663e-01,3.590323973709488858e-02,2.331874689876990425e-01,-1.494376707227117240e+00,-1.509753543203158799e+00,-1.741928532826658493e+00,-1.709555712420977924e+00,6.682890234843028021e-02,-1.508294795406126321e+00,-1.506992237086572928e+00,9.519961497824627750e-02,-1.369108901517802623e-01,-1.775749068029529365e+00,7.062975084623734912e-02,1.866130611863734134e-01,-1.686654783698201321e+00,-4.405234855229853141e-02,1.726240929876382835e-01,-1.000000000000000000e+00 diff --git a/qiskit_runtime/qka/aux_file/dataset_graph7.csv b/qiskit_runtime/qka/aux_file/dataset_graph7.csv new file mode 100644 index 0000000000..06cee93dc3 --- /dev/null +++ b/qiskit_runtime/qka/aux_file/dataset_graph7.csv @@ -0,0 +1,128 @@ +-1.935744789981031500e-01,1.139791551437831785e-01,-5.926731841626675656e-03,3.009571667669095341e-01,-3.586033583511695211e-01,-8.786632376030925617e-02,-1.562259205070140322e-01,3.424416004906711275e-01,-1.600323779808836278e-02,1.431132975838603216e-01,2.564224733863315620e-01,-1.641254743615789824e-01,-1.367425919556901903e-01,1.467435020857448327e-02,1.000000000000000000e+00 +-1.000062547092753218e-01,2.430830704236164319e-03,2.442175729383090355e-01,1.268699051167839487e-01,-6.389123846329397560e-02,-8.558800399203210507e-02,7.249021145835105040e-02,4.298628554943320634e-02,-5.271419625106030932e-02,1.975425030271986404e-02,-1.593141663325880197e-01,-4.099908516596876273e-01,-1.996152652940482164e-01,5.384521498929881189e-02,-1.000000000000000000e+00 +-1.774447698492219727e+00,-4.764210619965775217e-02,-2.587953371292733840e-02,2.527078163863590299e-01,-3.506894908219360119e-01,-1.604509432277618775e+00,-1.148743176969121427e-01,3.476306938160337867e-01,5.950125588821331135e-02,-1.689561894348156068e-01,3.510138375728891758e-01,-1.285863892217228022e-01,9.889725276835834022e-02,-4.779867105539917227e-02,1.000000000000000000e+00 +-2.115847659490593113e-01,-4.378197457659226827e-02,-1.560226191263545115e+00,1.851037692264281831e-02,-5.186740647855887787e-02,-1.285080578288320674e-01,2.186092565573694302e-01,-7.563234413279935897e-02,-1.836555867319568658e-01,-1.715292381708927838e+00,-1.053609967823944532e-01,-3.007580256026237686e-01,-5.664314466742134124e-01,4.654207660509027122e-02,-1.000000000000000000e+00 +-1.059648825563055119e-01,2.083871134971554129e-01,-3.896191369327062226e-03,1.638163863683159416e-01,-3.325412330756547519e-01,-1.594674707958482429e+00,-1.889321220144114299e+00,-2.519374096022733989e-01,3.744445401896578929e-01,-1.431062364019081157e+00,2.905878641430151843e-01,1.188508934529553251e-01,4.879893502549231821e-02,-7.302283441518868379e-02,1.000000000000000000e+00 +-7.892925337285790210e-02,-1.425061922732849817e+00,-5.135339515188619419e-02,-1.063343810050886851e-01,-1.337699140567889744e+00,2.135462731273148618e-01,9.071650828203833838e-02,-1.555947836611493162e+00,-2.523557403743938998e-01,-1.186849304841148672e-01,-1.597332163655802995e-01,-1.787685024795254485e+00,1.745797844065838378e-03,2.254499206850570370e-01,-1.000000000000000000e+00 +-1.634685003002452675e+00,-3.251862351339719348e-01,-2.205331884740385773e-02,3.742916305651294850e-01,-1.476130033379834672e-01,1.032155617257237423e-01,-1.809921763154535901e+00,-1.758857849397117978e-01,4.028084449511597914e-02,-1.624796041221835630e+00,1.664756781513079820e-01,-1.071422644886549924e-01,1.558875726640141235e-01,-4.165906302948692297e-02,1.000000000000000000e+00 +-6.893588852403798040e-02,-1.502351679030673459e+00,-1.583392212954673361e+00,2.964158753223027770e-02,-1.679927090025262526e+00,1.960003535628031812e-01,1.493346616410116123e-01,-1.573801522268912567e+00,-3.857005873887964786e-01,-1.602374783816816084e+00,-1.260749596503021597e-01,-1.700132611113932102e+00,-2.914614680654354362e-01,1.977030193570133709e-01,-1.000000000000000000e+00 +-8.520846789723049430e-02,-2.651197730292323418e-02,-2.640371058762110146e-02,-3.274202584091251422e-03,-2.816433303798683840e-01,3.562988748599811573e-02,-3.048970621335784781e-01,2.403931662676393743e-01,5.712739777555907172e-02,-1.551001213505004861e+00,3.965997947137003821e-01,-1.509685871126274503e-01,-1.420543153268830849e+00,-3.946372188686265992e-02,1.000000000000000000e+00 +-1.668893144344563373e+00,1.919130586646038972e-01,1.022447000210930657e-01,5.905500448133854663e-02,-7.747440192085087995e-02,-1.740098495699613945e+00,2.704241472111564293e-01,-1.738207255835940712e-01,7.007512455348459401e-02,-1.715623810450799813e-01,1.464674195565787707e-02,-2.574836095767138455e-01,-2.205749926081212731e-01,2.136271854472032716e-01,-1.000000000000000000e+00 +-1.824841436379284199e+00,-1.748777344081649809e-01,6.115091744109193983e-03,2.462803020292880229e-01,-2.487724873459193076e-01,-1.450250498290739154e+00,-4.697050359614601822e-02,2.511430594108938763e-01,5.617082693944049077e-02,-1.450274777049656239e+00,1.015770576039708994e-01,-2.411988640947981544e-01,-1.607422220292852755e+00,-7.604821669259544809e-03,1.000000000000000000e+00 +-1.703817562252395001e+00,1.326709061849910554e-01,-1.517563722971078999e+00,1.060016902241523729e-01,-2.537611221091851110e-02,-1.625499260459003725e+00,1.413230949241252310e-01,-1.874853551714396604e-01,-1.370225761448278790e-01,-1.640928883789254389e+00,-4.872232664044925249e-02,-3.512887869343963021e-01,-1.330782873438719482e-01,1.884054816212058225e-01,-1.000000000000000000e+00 +-4.602927011417328185e-01,9.114648978546381619e-02,-1.142681042952753273e-01,1.181998566641369736e-01,-3.744886720748129005e-01,-1.683306709233135745e+00,-1.857226218067269841e+00,-1.306773969295676741e-01,8.745669333004414681e-02,1.081474503508523810e-01,3.039225988385292898e-01,-4.511980020317890583e-02,-1.668770004433653753e+00,5.705645149867304144e-02,1.000000000000000000e+00 +-1.609292721928712089e+00,-1.530845752972931129e+00,-8.278815713149145772e-02,-2.373194652547248318e-01,-1.642259528918827094e+00,-1.550507000140835645e+00,2.096919128243851516e-01,-1.647554901046019982e+00,-2.522533002550081949e-01,-9.368579713617050664e-02,6.839489592974815069e-02,-1.993235444764836606e+00,-1.704810005505085768e-01,7.884782078520123638e-02,-1.000000000000000000e+00 +-1.858420224161734247e+00,-1.734005282820396676e-01,9.726655340758569301e-02,1.248959239748611560e-01,-2.843455210772053432e-01,-2.268106966669906563e-02,-1.729137928685011705e+00,-2.638388678031160506e-01,2.428805271409836264e-01,1.609752903428374737e-01,5.216967446545667242e-01,-7.752550718208928160e-02,-1.454779407934555735e+00,-3.368056274862356314e-02,1.000000000000000000e+00 +-1.671006873242029567e+00,-1.355035901028246670e+00,-1.546876931567384483e+00,-4.882903868976315453e-02,-1.431346086050258259e+00,-1.412521778718294874e+00,2.408521092393056395e-01,-1.658598241196473033e+00,-1.382017859742168953e-01,-1.657214374755794140e+00,-2.481908330508241639e-02,-1.762757266728576244e+00,2.383848738496055497e-02,1.455584988399447055e-01,-1.000000000000000000e+00 +-1.902126388006115942e-01,1.996213562281251575e-02,1.008372071407214743e-01,-1.460752336310695121e+00,-1.718442887948363751e-01,5.289151945715515196e-02,-1.988938641605968449e-01,-1.404678157366375135e+00,-1.426704526380021543e+00,1.836450345611355395e-02,1.031901470487577022e-01,-1.214539315444648354e-01,6.219952114504016111e-02,-1.654616533531131140e+00,1.000000000000000000e+00 +-7.099264735690004324e-02,1.750364477230531568e-02,-1.062663212368153143e-01,-3.318805470065910868e-01,-2.759776562556524523e-02,-1.603913436870445519e+00,-1.353532462950584492e+00,-1.395546155641066988e-03,-1.210691504980217770e-01,-1.511115253680355153e+00,-1.480633014803731107e-01,-2.012839088420497746e-01,-1.997788643348255844e-01,1.007769414455348678e-01,-1.000000000000000000e+00 +-1.732361623182581800e+00,-2.102480727713798569e-01,4.589408489794362389e-02,-1.185027255246496658e+00,-1.496015716906844428e-01,-1.529090647151525317e+00,-3.767531196928636805e-01,-1.228746858778276163e+00,-1.513836205519304201e+00,1.311451637954682559e-01,5.498183059552660401e-01,8.708600070403887949e-03,6.129938060113643283e-02,-1.559814424680894263e+00,1.000000000000000000e+00 +-2.835840568649770910e-02,-2.146285735581612841e-01,-1.579610604303776533e+00,1.664476210172569082e-02,-5.099273752013008809e-02,-1.725949610073979512e+00,-1.467398823985591960e+00,9.482147057008569191e-02,-3.186590355189626578e-02,-4.781462003049682941e-02,-3.124998176502554736e-01,-2.756619479040776644e-01,-2.832558409756105178e-01,1.619123252324499995e-01,-1.000000000000000000e+00 +-2.367772758105469166e-01,2.386147726201319208e-01,-1.737780012137604135e-01,-1.355794095407284461e+00,-2.679290745175850752e-01,-1.644752444669923097e+00,-1.939455022430933262e+00,-1.868695451010987663e+00,-1.400941658776029719e+00,-1.688002662345677640e+00,3.606506533467307429e-01,-7.625979515244935003e-03,-4.330127823671142684e-02,-1.553362446245406980e+00,1.000000000000000000e+00 +4.032978658428024166e-02,-1.676820021646022507e+00,1.047351288964794014e-01,-1.274758592562561521e-01,-1.655047118035765985e+00,-1.346487776545435500e+00,-1.527216883896012289e+00,-1.410111606579167454e+00,-2.539982374704825852e-02,-1.713267267046358455e+00,-1.014092723436062216e-01,-1.806922107887470208e+00,-9.253453197220207205e-02,1.441451858524561147e-01,-1.000000000000000000e+00 +-1.731129921112224634e+00,-2.901473907450236034e-01,1.180327861648885340e-02,-1.348904830804334765e+00,-2.564662506036542444e-01,2.657611077804407709e-02,-1.800769790691219896e+00,-1.725256531502176394e+00,-1.582781276073582655e+00,-1.587734247659576381e+00,1.846722494212687804e-01,-8.745442992928353920e-02,2.461445513982963088e-02,-1.550950765867309844e+00,1.000000000000000000e+00 +-2.283456737923411917e-01,-1.581192929304472417e+00,-1.698892334002197302e+00,2.936243047887290847e-02,-1.475603940072048648e+00,-1.481309278271474517e+00,-1.344846732616987506e+00,-1.423359469725386628e+00,-7.429594089515155486e-02,-2.249300418856977068e-01,-8.150346857611970308e-02,-1.783421326054325684e+00,-1.956288978727144245e-01,3.423808630989662039e-01,-1.000000000000000000e+00 +-1.392029844521847359e-01,1.585516293481104011e-02,1.649686505433222983e-01,-1.455942057796049349e+00,-2.565270886891928837e-01,-2.995841363766689816e-02,-2.225188616804514574e-01,-1.304550654947434385e+00,-1.543543743723497785e+00,-1.625506112240582501e+00,2.646686934361995869e-01,3.536394651466978623e-02,-1.596770214722755821e+00,-1.718505474909242325e+00,1.000000000000000000e+00 +-1.709571844246771555e+00,7.258692646837865137e-02,-6.937956961166810710e-02,-1.759107096036754969e-02,1.023153468347885037e-01,-1.396731656103666064e-01,-1.290324032684116107e+00,1.744754247071788056e-01,-2.274937312973350501e-02,-1.542470932907623160e+00,-2.143952135731703179e-01,-2.178828468681655861e-01,-4.685982140614411429e-02,3.202479980416738092e-01,-1.000000000000000000e+00 +-1.783884668386667149e+00,-1.272375533214497589e-01,-6.483306964860351129e-02,-1.317393170180484407e+00,-1.785553028497116612e-01,-1.380977492719456912e+00,-2.398079458622670024e-01,-1.290907093889349877e+00,-1.351572903045891394e+00,-1.550242759707908036e+00,4.198473156596549960e-01,-1.242770486723988876e-01,-1.525877099216195854e+00,-1.554149498054001377e+00,1.000000000000000000e+00 +-1.734648507628440139e+00,2.192826484018607358e-01,-1.489106692551441569e+00,5.007721728629293406e-02,1.398957448036743448e-02,-2.921742187204966895e-02,-1.320288395167840356e+00,3.292664544724834830e-01,-1.795623271947691391e-01,-1.466962564866716723e-01,-2.662897463892865274e-02,-1.738436898994596969e-01,-3.817963162764551055e-02,6.503463516527510646e-02,-1.000000000000000000e+00 +-3.029885861559754590e-01,2.550953935420102736e-01,-1.103307825250942481e-01,-1.420240129037396049e+00,-4.057700325150146448e-01,-1.432037726658133003e+00,-1.742574743379801561e+00,-1.706346924550034450e+00,-1.610482645334571306e+00,2.052113966044370463e-01,2.691827908860374707e-01,-5.868454248888337749e-02,-1.688925815284796350e+00,-1.731488086207674471e+00,1.000000000000000000e+00 +-1.587169441767515288e+00,-1.449215424064308477e+00,1.056750713765475247e-01,1.005523967565410681e-01,-1.643903047784339266e+00,5.393450286841017238e-02,-1.355966372117012320e+00,-1.480728708640227875e+00,-1.192444720770064470e-01,-1.617205607701764691e+00,-2.001940707974544931e-01,-1.854276215264260674e+00,-2.120968756661331667e-01,1.249548214378189137e-01,-1.000000000000000000e+00 +-1.659231896786515836e+00,-2.734026343955329175e-01,1.614267437122218207e-01,-1.308580366437199105e+00,-2.585744813783290574e-01,-7.277887054484513674e-02,-1.964579895708931545e+00,-1.988637864745677053e+00,-1.417206261440844273e+00,-1.064945755041092068e-01,2.014194076204649519e-01,-1.354914430258996605e-01,-1.491698880763357549e+00,-1.551242130728441859e+00,1.000000000000000000e+00 +-1.720446547882567012e+00,-1.541241393438607110e+00,-1.630467902235066324e+00,-7.411202833128890943e-03,-1.654541032018014945e+00,2.623612216276595133e-01,-1.480764894592184522e+00,-1.460466737184188180e+00,-3.295613141155256387e-01,-1.335075346053341272e-01,-1.273070084023267268e-01,-2.127679633637005630e+00,-2.464723373152804542e-01,8.022582804182054605e-02,-1.000000000000000000e+00 +-2.109079585833683601e-01,-1.555892998862504095e+00,1.107885884130413306e-01,1.177215642018687169e-01,-1.903635417489270898e+00,6.149415741520453116e-02,-3.799748269154470948e-01,-1.353483645674147029e+00,-4.930015796167842856e-02,1.531146828385572256e-01,2.740872821745550247e-01,-1.725855934989520168e+00,1.813254786623140491e-01,-7.054318243692112023e-02,1.000000000000000000e+00 +-9.734287508391470389e-02,-4.678911477042982381e-03,-2.354664895714689635e-02,5.100745385274149668e-03,-1.331449935855308631e-01,-1.598625887398838596e+00,2.778746322995367390e-01,4.813444104940975410e-02,-1.625933685672424356e-01,-6.697790788327151468e-02,-1.749605469280097747e+00,2.623302391327739791e-01,-2.634792860014013027e-01,1.665935370787189895e-01,-1.000000000000000000e+00 +-1.817353774439702363e+00,-1.561445054303381763e+00,2.588251245924956534e-02,2.035376981561484999e-01,-1.766443070947268268e+00,-1.296972269680546797e+00,-2.543772414319446229e-01,-1.398205211604792941e+00,2.092399351858046930e-01,1.687208519899038306e-01,2.441626287880789281e-01,-1.858209074295685825e+00,-1.966273036325641155e-01,4.292932778109619096e-02,1.000000000000000000e+00 +2.703596192017793109e-03,-2.329838366977103892e-01,-1.380333551207818088e+00,2.044434338582787891e-01,-4.081958202166395544e-02,-1.681344439141456926e+00,1.476627032024580011e-01,-4.993020226228646330e-02,-1.875771998174172728e-01,-1.762007651436396705e+00,-1.557788011049577648e+00,2.837909499650285161e-01,-5.845342207235090792e-02,-9.376632748452173871e-02,-1.000000000000000000e+00 +-2.606686354658470095e-01,-1.408856249495505031e+00,1.880920528500927280e-01,1.835990601756801177e-02,-1.861442820461840064e+00,-1.652352721661132007e+00,-1.866726146333868464e+00,-1.805367024170868362e+00,-4.902227545481459281e-02,-1.560841106272784495e+00,2.205571318753309085e-01,-1.592737578886485927e+00,-2.845762836725848621e-02,1.370102237967169845e-01,1.000000000000000000e+00 +-9.844964916686858858e-02,-1.561368624864145183e+00,-3.914124367162109819e-02,-1.080476455056877255e-02,-1.722930338042492515e+00,-1.544141333393409976e+00,1.157126277790064411e-01,-1.727777618033043527e+00,-1.670565305629349573e-01,2.974758626022222519e-02,-1.581477342471812042e+00,-1.323120858919411447e+00,-2.100339313983373701e-01,1.448187770874282965e-01,-1.000000000000000000e+00 +-1.957458263369451723e+00,-1.694274989097745543e+00,5.454911108419973481e-02,1.805943278212934489e-01,-1.756092263705557599e+00,-8.595899367681159031e-02,-1.696067414366745130e+00,-1.712023576270820469e+00,2.060492194970154900e-01,-1.398335988329474810e+00,4.410001979147892825e-01,-1.719686354629639036e+00,-6.051566718506100340e-02,3.339095417045597619e-02,1.000000000000000000e+00 +-7.866696516093840685e-03,-1.616787244372446697e+00,-1.740116896609308572e+00,8.579584135912943221e-02,-1.439416796296014933e+00,-1.523227070758474966e+00,3.758797173705020200e-01,-1.769922243342483492e+00,-2.134407608641714915e-01,-1.517898778848180585e+00,-1.492718695300921272e+00,-1.078979009657375254e+00,-8.830119739724537342e-02,2.032437865555704604e-01,-1.000000000000000000e+00 +-2.053848989919285495e-01,-1.323498679920936505e+00,1.600594600689232427e-01,2.127530971190854658e-01,-1.812974538182991369e+00,-9.828391746720832600e-02,-3.237482397158925052e-01,-1.376107670949456407e+00,1.857257065594705159e-01,-1.536261310236082256e+00,3.252889388876294974e-01,-1.582426747967575320e+00,-1.644393972918431368e+00,-7.704793882180768771e-02,1.000000000000000000e+00 +-1.669671522325552715e+00,-4.738520888333819236e-03,-9.011259086863371293e-02,-2.387991865509341005e-01,-5.806701719645110393e-02,-1.206243669493144155e-01,3.369023832387631479e-01,-7.667184510194216540e-02,-6.384074432003430943e-02,-2.220560303315098627e-02,-1.677397347136823980e+00,2.831200735921008960e-01,-2.766936859144445426e-01,1.560540392900901796e-01,-1.000000000000000000e+00 +-1.777587007008249564e+00,-1.704171978040527335e+00,5.913101480819421552e-02,1.428869347821797309e-01,-1.901771407341925402e+00,-1.759160581014954428e+00,-2.726838542570871327e-01,-1.341989046626985482e+00,1.149581111517015558e-01,-1.541115658685354717e+00,1.677606151171127657e-01,-1.464461059124410713e+00,-1.691688504084269074e+00,-3.104441766574880454e-02,1.000000000000000000e+00 +-1.741418645816155575e+00,2.527459606765928291e-01,-1.635744431626482864e+00,-4.135823844505452651e-02,1.560523028071861273e-01,-9.996675928048688597e-02,3.331306303388317236e-01,-1.182858312212287932e-01,5.816274462819263913e-02,-1.764772416562902180e+00,-1.477910347247076261e+00,2.989494469385867159e-01,-1.051717978886456129e-01,1.921403438897218707e-01,-1.000000000000000000e+00 +-3.065731389712608168e-01,-1.342546641072029701e+00,5.964527988909403000e-02,1.711393678184557343e-01,-1.761287550914799827e+00,-1.611029149319431752e+00,-1.765853023869925487e+00,-1.888379046610273448e+00,1.002820626583308672e-01,-1.109509776410169124e-01,2.188709029554854468e-01,-1.576157910757060021e+00,-1.521945969532204845e+00,2.070355351194339347e-02,1.000000000000000000e+00 +-1.623549583779014815e+00,-1.350051115786569111e+00,-8.869050426022510522e-03,-1.874223112652556866e-01,-1.633442654667898397e+00,1.375420098898255117e-01,1.168110294924096237e-01,-1.569377840081284781e+00,-1.602564120101649947e-01,-1.325093610505566621e-01,-1.438045060031879707e+00,-1.050581181443586365e+00,-2.893936297597720642e-01,7.855108334561576588e-02,-1.000000000000000000e+00 +-1.658154207372490641e+00,-1.789134629255389797e+00,4.054766945845136861e-02,4.274914633188270363e-03,-1.797182622929081264e+00,2.559107562684073689e-02,-1.718806878310818353e+00,-1.755749373633363364e+00,7.060323412260165188e-02,-1.084276109706693719e-02,3.609575922405105497e-01,-1.714998993682882000e+00,-1.574951641335678110e+00,1.149572272760127495e-01,1.000000000000000000e+00 +-1.847961507423650840e+00,-1.473921222549971821e+00,-1.617837932133893375e+00,-7.103102034326941316e-02,-1.571069807864609036e+00,3.573476889499199727e-02,1.970289265713811022e-01,-1.739229085965009469e+00,-2.775349152229639182e-02,-1.680739634226039003e+00,-1.587025168513149342e+00,-1.236748239033638841e+00,6.935436000199549089e-02,2.040125069256005230e-01,-1.000000000000000000e+00 +-1.722904466803558410e-01,-1.411617617088636312e+00,2.916437094735713220e-04,-1.406253287690810927e+00,-1.615679981746658056e+00,-2.763953409871925445e-03,-3.950897247081973651e-01,2.430954492310867798e-01,-1.311681842017352828e+00,-2.200217028509704811e-02,1.703654951829411679e-01,-1.720433172383417197e+00,1.308044939654900291e-03,-1.417741158285068082e+00,1.000000000000000000e+00 +-6.888987672868050749e-02,-2.206762982686303332e-01,-1.750101396499676581e-02,-3.727240288538255508e-03,-1.083627958668931801e-01,-8.696675784860415093e-02,-1.319049022372315028e+00,5.257817621846899714e-02,-1.711129698146304778e-01,-1.555886570316086903e+00,-1.677814988070778224e+00,3.915202101818402447e-01,5.543780834425612869e-02,1.390389113262543719e-01,-1.000000000000000000e+00 +-1.727238257218281170e+00,-1.615530576147521602e+00,7.869056225858209186e-02,-1.423059514455122798e+00,-1.761192441644305884e+00,-1.630316956634427994e+00,-2.789120381103245006e-01,1.941822238716390814e-01,-1.518121723396330669e+00,-1.238462931609404100e-02,2.742496492662739316e-01,-1.645835021526846909e+00,3.721937042066567752e-02,-1.627505454659611495e+00,1.000000000000000000e+00 +1.470635122441772691e-02,-2.056113108150927626e-01,-1.764448694346967272e+00,1.968965126762830531e-01,-1.287913227649226389e-01,-1.238240475078797309e-02,-1.315648596190898134e+00,2.225161798906181432e-01,-1.704424821427150682e-01,-1.000365041999348586e-01,-1.533319399388676008e+00,2.921635897646819857e-01,-5.860574376450541911e-02,2.784987048414280242e-01,-1.000000000000000000e+00 +-2.494620197685982754e-01,-1.448566913248975796e+00,7.057694579524713407e-02,-1.273212303503205467e+00,-1.679047110450799574e+00,-1.588867586909664009e+00,-1.771914972690083134e+00,-2.517244837878088726e-01,-1.480580985639242897e+00,-1.669982779328623934e+00,3.614806337794052582e-01,-1.633837644043817372e+00,4.781815759110948083e-02,-1.560544856181362405e+00,1.000000000000000000e+00 +6.107929177820334288e-02,-1.534902656447697966e+00,-8.607572626113366154e-02,-1.053562469886166753e-01,-1.463402905170154700e+00,2.486780937748925002e-01,-1.327669916737666655e+00,-1.444079856760119851e+00,-2.251288416680337356e-02,-1.548686440706024792e+00,-1.576795558854126700e+00,-1.162050860905266214e+00,-1.797664644595173833e-01,8.235556289785103679e-02,-1.000000000000000000e+00 +-1.620915957675557850e+00,-1.759205929598766538e+00,6.587282363154096931e-02,-1.260937479341356138e+00,-1.825365113928211436e+00,-5.420203464332983695e-02,-1.856265590647504293e+00,-3.232448348203816324e-01,-1.506331929169810291e+00,-1.790950030579560037e+00,2.915247309809219645e-01,-1.553405508313238981e+00,1.064510860731728847e-01,-1.593081305610142051e+00,1.000000000000000000e+00 +-4.612223926753210301e-02,-1.491967959290764867e+00,-1.557513752337976953e+00,-5.320920254984169706e-02,-1.565674533919902078e+00,-6.933620921740490939e-03,-1.436743443340090298e+00,-1.483034265310440913e+00,-1.958793561744313072e-01,-5.822026887820264812e-02,-1.749475100938908234e+00,-1.367204286344534836e+00,-3.377935618256747152e-02,1.192202087137585464e-01,-1.000000000000000000e+00 +-4.031583925895292353e-01,-1.532565253996989396e+00,9.855479329093051744e-03,-1.389311833487676839e+00,-1.760960705542248483e+00,-1.210071189044274048e-01,-1.758028352391565741e-01,2.497081893375624473e-01,-1.526797317251125996e+00,-1.580968414753146645e+00,3.132766109992452530e-01,-1.604830716095155818e+00,-1.399353857373611509e+00,-1.618918810056891644e+00,1.000000000000000000e+00 +-1.752387993950347056e+00,-2.153427027813585581e-02,3.324646213538123146e-02,-1.248963934916344776e-01,6.501245133315829844e-03,-1.692417971345222094e+00,-1.397884025322287727e+00,1.653510492758513140e-01,-9.216492629239791956e-03,-1.832012590804132479e+00,-1.872767888166050554e+00,2.915105817056929394e-01,-1.079166206186459309e-01,2.912668576802653342e-01,-1.000000000000000000e+00 +-1.806184649482642346e+00,-1.773788783419828707e+00,7.595038272615574348e-02,-1.353747587535946373e+00,-1.926395246773682013e+00,-1.608648402785678888e+00,-2.940518864491442619e-02,1.536771172595196577e-01,-1.600446038430091589e+00,-1.568954173464251145e+00,2.377243129796963450e-01,-1.702954340893876317e+00,-1.540268035867445651e+00,-1.525964682609035838e+00,1.000000000000000000e+00 +-1.681853001192156416e+00,2.025248851261629568e-02,-1.507927323068977588e+00,6.015652368434671465e-02,6.787768755262589548e-02,-1.797346668678856396e+00,-1.328492471023882127e+00,1.083374242081695804e-01,-6.807194630225105181e-02,-1.536472371808336179e-01,-1.544991427545987506e+00,2.496879419231946562e-01,-2.586500999091325692e-01,2.472464564743200865e-01,-1.000000000000000000e+00 +-6.512889928647674909e-02,-1.280157641621820330e+00,8.548411526008756200e-03,-1.308195710131301981e+00,-1.759788721990152682e+00,-1.442917604990261449e+00,-1.893052923952196265e+00,-1.913558088187841955e-01,-1.400433029598109114e+00,-5.406451812426182196e-02,2.908001571900953874e-01,-1.654973814245627617e+00,-1.488252776308071290e+00,-1.629965511123137256e+00,1.000000000000000000e+00 +-1.600806204135150956e+00,-1.511861641530125588e+00,-4.237261964877586662e-02,-1.027349323790079094e-01,-1.534775695208126400e+00,-1.609318802955511662e+00,-1.415221590280331787e+00,-1.349255135992462717e+00,-1.603016048133207860e-01,-1.709813196142691449e+00,-1.498028189087612372e+00,-1.354158043503653763e+00,-1.324975882836215146e-01,2.711132403917941303e-01,-1.000000000000000000e+00 +-1.815694150620314362e+00,-1.841070891138648191e+00,-2.135129424367295248e-01,-1.451394165585441653e+00,-1.747533020562963335e+00,-1.200370342065606132e-02,-1.923975840908812129e+00,3.494210435125721936e-02,-1.425449270516573108e+00,-4.246354558705554838e-03,1.056439022865369171e-01,-1.676809418735673018e+00,-1.449035505955657666e+00,-1.531577706593846289e+00,1.000000000000000000e+00 +-1.604075385534005527e+00,-1.247560546127565617e+00,-1.672255369508738987e+00,1.065211685963571020e-01,-1.756483006388495305e+00,-1.617196120846714269e+00,-1.389449269607853088e+00,-1.586122669296846199e+00,-4.219247823196448088e-02,-1.204773546433365317e-01,-1.559777708682754449e+00,-1.272675445601217925e+00,-1.568985920919612476e-01,2.787010337220571787e-02,-1.000000000000000000e+00 +-8.034254508214096202e-02,6.880725407078074063e-02,2.094215261303540299e-01,1.446253521446881907e-01,-2.494568563956204299e-01,-1.612589456454816883e+00,-1.591140610263561961e-01,1.892714292746733307e-01,2.185783413107370288e-01,9.944979984667769168e-02,-1.197593196312220387e+00,8.774288695775299407e-02,1.763860410608001916e-01,1.613257325364919892e-01,1.000000000000000000e+00 +-1.886644372382341728e-01,-1.619309640274923801e-01,2.683354162221169431e-02,-1.413968591452252621e-01,9.808653926713208071e-02,-7.657823144474243393e-02,1.303211970396588870e-01,4.133184237415543638e-03,2.793182479116006200e-02,-1.535301780469503274e+00,-1.300594688837936885e-01,-2.232958274315878788e-01,-1.681883951636097274e+00,-2.720573586526732202e-01,-1.000000000000000000e+00 +-1.778715650654681291e+00,-1.262469228272111998e-01,6.962258263356069232e-02,1.095996415049501210e-01,-1.988037976727128342e-01,-1.175735577159129908e-01,-2.309365762557876178e-01,1.224684282068461416e-01,2.462409602188055102e-02,4.908526452607276519e-02,-1.363465905985415283e+00,-1.910421864649460011e-02,-5.795484461945044075e-02,-1.270035108333279483e-01,1.000000000000000000e+00 +-4.142652836984381248e-03,-6.013365015874117675e-02,-1.703420334445887141e+00,3.742735829120897445e-02,-4.025439783818521994e-02,1.289073532965347157e-02,3.107700852026719085e-01,-2.259629875830789780e-01,1.670993057174276208e-02,-1.019608591323673125e-01,-4.382862033882690272e-02,-4.585080608863484342e-01,-1.692172413371813899e+00,-5.792533591758292078e-02,-1.000000000000000000e+00 +-3.935154334802061782e-01,3.638872533473028192e-02,1.395259957821806951e-01,1.664661237197593568e-01,-3.103391820041769833e-01,-2.005443684678374638e-01,-1.884110053262168627e+00,-1.345063215107585031e-01,2.550919602531565022e-01,-1.361261227364313431e+00,-1.290216119102986614e+00,1.336215441564989337e-01,6.254152091303144534e-03,2.100087729950954329e-02,1.000000000000000000e+00 +-9.669110204168082645e-02,-1.775363854018477516e+00,1.069742929339808635e-01,-1.322016069705599928e-01,-1.547590907751123179e+00,4.749105067501178251e-02,1.252294878250418686e-01,-1.594537320930422553e+00,-2.024532740445836865e-01,-1.485978417205851887e+00,-1.276331802522947534e-01,-1.692798225641222842e+00,-1.719467001867377220e+00,-2.401686334724826843e-01,-1.000000000000000000e+00 +-1.818404423502115286e+00,-1.613776695434923358e-01,-2.963013171964887227e-03,1.003119539558565421e-01,-5.117550258241547745e-01,-1.840998639084066912e+00,-1.622614867210045109e+00,-1.900972555314071055e-01,1.886811273742927675e-01,-1.576598230931666178e+00,-1.384814828596028446e+00,2.764715054184119225e-02,-1.234786186862628088e-01,-2.848869597594009748e-02,1.000000000000000000e+00 +-8.409014969526823480e-02,-1.523030598750639619e+00,-1.651004473613457479e+00,2.050129427916006497e-01,-1.662422657414257543e+00,4.900743176672640172e-02,2.953259378908753563e-01,-1.727332438860938346e+00,-1.736397564513021963e-01,-1.754096263969709690e-01,-1.773807575012419369e-01,-1.803500231186919267e+00,-1.772739215935786206e+00,-3.388655478452351488e-02,-1.000000000000000000e+00 +-3.504893168320986074e-01,1.287453279306145226e-01,-9.912367241549739638e-02,5.441291793956634648e-02,-3.067496111818439619e-01,-1.847740748782476627e+00,-2.809401432452648950e-01,2.606615198081184692e-01,-8.298507598924294293e-02,-1.528360789793670182e+00,-1.243673834577912807e+00,8.332409430469578382e-02,-1.536772762431679151e+00,-2.276252096846678452e-01,1.000000000000000000e+00 +-1.669135017685616562e+00,1.878572457368295479e-01,-1.109103344102813082e-01,1.084848744675842580e-02,-8.542400125995765203e-02,-1.858098624717458680e+00,-1.107316092311189104e-02,-4.160138555491198209e-02,-2.961452456263555799e-01,-1.711925191657831302e+00,-1.727054823269065509e-01,-3.765547362240309059e-01,-1.915823137520314301e+00,-1.315770328525295851e-01,-1.000000000000000000e+00 +-1.800479249082186151e+00,-3.386831473970310924e-01,2.229926830865746057e-01,1.062297083684143884e-02,-2.752908599739183892e-01,3.925282309845710971e-02,-3.015683531867457412e-01,2.297082100678827232e-01,1.058116123622843130e-01,-1.433791716055095034e+00,-1.166489686173616480e+00,-4.472131141237700724e-02,-1.571037642506694976e+00,-4.200183994858083109e-02,1.000000000000000000e+00 +-1.615602691154683912e+00,-3.740395205227235731e-02,-1.741864658683469180e+00,-8.026571595103948464e-02,-6.875727441416802643e-02,-1.691643264385989998e+00,1.540923994518740570e-01,-6.881789309428396062e-02,-6.992666548691262030e-02,-1.090237072062237716e-01,-6.974526401931734065e-02,-2.892980571982610161e-01,-1.755724205511214553e+00,-1.814918365608780837e-01,-1.000000000000000000e+00 +-2.677256404895123065e-01,1.835013192157280426e-01,-5.410002376291028758e-02,1.960243382670116552e-01,-1.888170511251534012e-01,-5.086120994193638750e-02,-1.848774520891739437e+00,-2.360222779043457952e-01,6.986521036030590714e-02,3.710310692206895966e-02,-1.267617760230665969e+00,7.705794214828859401e-02,-1.624174807600006210e+00,7.589070361533122033e-02,1.000000000000000000e+00 +-1.483100626972129632e+00,-1.498241130023366896e+00,-1.839484030280719717e-01,-1.599276150165117594e-01,-1.677524565791744582e+00,-1.244818282776545715e+00,1.844648780999630366e-01,-1.531863635483006636e+00,-1.738348777911911669e-01,-1.734127336249420948e+00,-1.460549832600117337e-01,-1.760490064524646625e+00,-1.703402789107563597e+00,-7.417356541098861500e-02,-1.000000000000000000e+00 +-1.714556915953274085e+00,-1.098793838346878715e-02,1.397241074190134769e-01,1.188655975385968788e-01,-3.606397251872860421e-01,-1.623217860346481745e+00,-1.843379238712378632e+00,-2.198912233817513218e-01,5.596523583124383971e-02,1.984229244991772556e-01,-1.302687102631844951e+00,1.540032728930624817e-01,-1.707395301387358844e+00,8.229962282112589234e-02,1.000000000000000000e+00 +-1.552023699997773010e+00,-1.589629870687183866e+00,-1.546490125968891016e+00,-9.512728416417828847e-03,-1.623098593056410577e+00,-1.543080591010138392e+00,3.550992655544662480e-01,-1.651747859497800341e+00,2.138715259196787011e-02,-4.614252337635053880e-02,-1.099106707827380153e-01,-1.797091809321512024e+00,-1.723231873567492078e+00,-1.951141050461503434e-01,-1.000000000000000000e+00 +-1.606564658516367594e-01,1.619450152250831165e-01,1.865756135283968553e-01,-1.459656607146854412e+00,-1.715882654233442106e-01,-1.434470999144190406e+00,-1.193565190757426703e-01,-1.293034381978339198e+00,-1.521215319236361108e+00,-6.652759862985353767e-02,-1.407943939743753869e+00,8.327266281168387019e-02,-9.784064273089676522e-02,-1.498740980415620871e+00,1.000000000000000000e+00 +-1.483323083342633075e-03,6.967090893741754998e-02,6.278870422176768828e-02,-3.086306037876339337e-02,4.902401205912280957e-02,-1.521000431313429102e+00,-1.264142252532288513e+00,9.533858173133910596e-02,-1.011741668662529819e-01,3.310564112257621816e-02,-8.191935135946362689e-02,-3.186349310486205622e-01,-1.898669358964544029e+00,-4.496879339874823578e-02,-1.000000000000000000e+00 +-1.573620891016235035e+00,-1.329982948840280343e-01,2.399503310453289795e-01,-1.330488862416024265e+00,-2.765375378185703781e-01,8.808199743137788695e-02,-2.648386870994752784e-01,-1.379876580550749576e+00,-1.383938581042170268e+00,-8.501291019743140165e-02,-1.165802340721067143e+00,1.507153745526593203e-01,1.395678131681016287e-01,-1.418622966305338906e+00,1.000000000000000000e+00 +-2.414752338762555239e-01,6.263748091812453012e-02,-1.582288633664975963e+00,-2.138551415080261475e-02,-1.432209397597079270e-01,-1.564218586364576780e+00,-1.325323773803723570e+00,8.635628717131471621e-02,-9.389897569821684109e-02,-1.765087260484488496e+00,-6.942818194977197488e-02,-3.223149105900656175e-01,-1.783911969651682083e+00,-1.724922661524661005e-01,-1.000000000000000000e+00 +-2.663953337290222434e-01,2.367978735580491545e-01,-6.125756833378044608e-02,-1.600320213409630377e+00,-1.746247378962873520e-01,-3.198386486924113814e-02,-1.769758135789326392e+00,-1.706661585217408961e+00,-1.625355656722051734e+00,-1.457244178579490068e+00,-1.335913049256137963e+00,1.196030340183451696e-01,-1.642401908248225795e-01,-1.456435876411096952e+00,1.000000000000000000e+00 +-1.232307318054817469e-01,-1.646559127435431868e+00,-1.688108478516482103e-01,8.679061400071028110e-02,-1.626846068315488836e+00,-1.472854188583361923e+00,-1.365579138009763049e+00,-1.466056801914429553e+00,-1.148785375351140531e-01,-9.280445394721349717e-02,-2.086263845777263509e-02,-1.798345062543814654e+00,-1.807806298910443310e+00,-2.312401086807840955e-01,-1.000000000000000000e+00 +-1.797916284381434915e+00,-2.911877350873054437e-01,9.552965566583154766e-02,-1.199357917186429034e+00,-2.857742411791293069e-01,-1.605346495511491778e+00,-1.772284905664825549e+00,-1.665468436189193158e+00,-1.587105210501043695e+00,-1.713909513833273612e+00,-1.395969565879321861e+00,2.700363356400100612e-01,-3.133153261923687927e-02,-1.466512092594051575e+00,1.000000000000000000e+00 +-2.816570040640491157e-02,-1.490270920705007551e+00,-1.697566414689200442e+00,1.066246706088792573e-01,-1.536164302589434261e+00,-1.538328845280117152e+00,-1.356330561620534558e+00,-1.514472207116063451e+00,-1.094905748070687523e-01,-1.800177760814607675e+00,-1.216267770704610451e-01,-1.651142758533824972e+00,-1.955366473810422168e+00,-1.231567332270106924e-01,-1.000000000000000000e+00 +-2.882849109278932676e-01,1.983093618883552678e-01,-1.463093206844333360e-02,-1.322092918898141845e+00,-4.602704639099099992e-01,-1.605476279094690151e+00,-3.759189111112950510e-01,-1.369264970446639085e+00,-1.660121345876027110e+00,-1.739860774927726172e+00,-1.344866286821288837e+00,1.542177775336922019e-01,-1.668878289511315227e+00,-1.520740909468941915e+00,1.000000000000000000e+00 +-1.732333604683580441e+00,9.111658731908463260e-02,-4.803802635457201514e-02,-1.279143932227881919e-01,-2.649329842482005953e-02,8.165733683507830909e-02,-1.263156309110212039e+00,6.513957347289628386e-02,4.103908537035586002e-03,-5.453058692077440012e-02,-8.022016489145190887e-02,-1.727359390460022626e-01,-1.734239058057392757e+00,-3.904902587740946007e-01,-1.000000000000000000e+00 +-1.694631322106838534e+00,-2.318305759969209179e-01,1.452105664682052100e-01,-1.296445994927566847e+00,-9.002133292421310440e-02,-6.386742495464487954e-03,-2.279265489238869458e-01,-1.223730498180746640e+00,-1.310785738689320024e+00,-1.641340256361092376e+00,-1.230315848916744281e+00,1.856264504335765064e-01,-1.656945810802020302e+00,-1.748667142064466384e+00,1.000000000000000000e+00 +-1.450854206908660071e+00,7.828524456119041741e-02,-1.578406784248261419e+00,-2.512896826671837114e-02,-1.843895802506676229e-01,-1.191981600187149992e-01,-1.336143794401053242e+00,5.477238872298689082e-02,-2.040918138447603281e-01,-1.578286250190459761e+00,1.322295879957060549e-01,-1.209625784490078182e-01,-1.599368150749488127e+00,-1.874933429591453538e-01,-1.000000000000000000e+00 +-1.045059878192874936e-01,3.284108220258633581e-01,-1.804137053924599610e-02,-1.415434285098199352e+00,-4.356646779711194073e-01,4.788784486837571741e-02,-1.877641497993017383e+00,-1.743155189450987796e+00,-1.585436495856220152e+00,-2.573786078104545877e-02,-1.387310204173998418e+00,1.943702255628812492e-02,-1.487659000524529818e+00,-1.554790847506030582e+00,1.000000000000000000e+00 +-1.777631063812934009e+00,-1.474989866021839457e+00,-1.378492663939756557e-01,9.719130114846494251e-02,-1.544659298121808133e+00,-4.345709155336191509e-03,-1.300914268229148796e+00,-1.342089156350444235e+00,-1.443842580417471766e-02,1.101393450782136851e-01,-4.458906771815638465e-02,-1.947807337208174339e+00,-1.783904632762413067e+00,-1.539862301179337478e-01,-1.000000000000000000e+00 +-1.875221005130098373e+00,-3.027948236787822434e-02,1.577189260360888357e-01,-1.297525857832602103e+00,-4.466360637113593879e-01,-1.507951520190433659e+00,-1.781636723359887986e+00,-1.742255382993029400e+00,-1.490422196791556297e+00,-2.957936848575772396e-02,-1.097956120195445751e+00,1.158530889140248904e-01,-1.599658517211244968e+00,-1.684814893443490114e+00,1.000000000000000000e+00 +-1.413746274172867690e+00,-1.439223735272604943e+00,-1.606212425722667536e+00,2.463671345926384815e-01,-1.672731456423659946e+00,2.268676841522668242e-03,-1.316526726628725941e+00,-1.262398307739135195e+00,-1.777197614280082993e-01,-1.813590259802646987e+00,1.313756346090966254e-02,-1.618523643863773342e+00,-1.859687251220742299e+00,-2.495201522744032452e-01,-1.000000000000000000e+00 +-3.297252099680703097e-01,-1.371755504998362296e+00,-2.079422813263466507e-02,3.886685963712142033e-01,-1.856985700819249185e+00,-1.739845605796722872e+00,-2.518312292333283464e-01,-1.274371212320373870e+00,-4.745129115520065055e-02,4.382091814791814338e-02,-1.278635974363860894e+00,-1.420689039846026347e+00,-3.444506503819735888e-03,-2.982589230851817944e-02,1.000000000000000000e+00 +-2.124491174803492255e-01,-2.139723931994451100e-01,-3.322687787579704788e-02,7.088355859889972077e-02,-1.097689220635640478e-01,-1.607445440968523709e+00,4.407797109396910940e-01,-1.427730048091011839e-01,-9.409292144042250006e-03,-1.654717190782132308e+00,-1.578590319863247160e+00,3.807029358492156623e-01,-1.860152216807540171e+00,-7.885114569420309527e-02,-1.000000000000000000e+00 +-1.653082354576353996e+00,-1.504985772276845823e+00,8.862611021251846910e-02,-8.201785062421099171e-02,-1.879265445719659366e+00,-5.944675608816845508e-02,-3.709557306042644553e-01,-1.315055201264324181e+00,7.171794880851997678e-02,-5.131578626564396728e-02,-1.251328659172288083e+00,-1.482978491176007774e+00,6.440516665073076075e-02,-5.049820195005234186e-03,1.000000000000000000e+00 +-8.791229388322152616e-02,-5.836882405768635540e-02,-1.481081774714787969e+00,-6.391971646885230562e-02,-6.486649251415627093e-02,-1.506758250413897215e+00,1.770408338306008744e-01,-1.313950103499249289e-01,-1.531950314854812478e-01,6.990859955920894486e-02,-1.626637234267066745e+00,5.929477893373875652e-01,-1.546528241399860448e+00,-1.603662961096674056e-01,-1.000000000000000000e+00 +-3.396285678353226367e-01,-1.528294409412719546e+00,1.338140976218210321e-01,3.178166295579417100e-01,-1.884779252162981145e+00,7.587564002972220500e-02,-1.801052541146987984e+00,-1.802455789509802342e+00,5.629959864233883421e-02,-1.528386690144768467e+00,-1.196946559723040426e+00,-1.496014076337103482e+00,-1.459000822332437441e-02,-4.514110158813214313e-02,1.000000000000000000e+00 +-2.122826033075854713e-01,-1.742457408344193714e+00,-5.994940210951479193e-03,-7.429000399707801072e-02,-1.573847815175543285e+00,-1.427765538136147594e+00,-2.177683539857089290e-03,-1.848090509302166007e+00,-2.462976151736861463e-01,-1.507496047961274011e+00,-1.694486745172459230e+00,-1.084089586155583929e+00,-1.881101533930195080e+00,-4.045707885772964474e-01,-1.000000000000000000e+00 +-1.821486828404217828e+00,-1.785783773911563177e+00,8.694550389372397714e-02,1.018824281446641250e-01,-1.693691149603142154e+00,-1.484847003659224729e+00,-1.765063793949422033e+00,-1.802234392814523023e+00,5.522426840259311853e-02,-1.612363985088490770e+00,-1.317573593524661524e+00,-1.431744863444093685e+00,4.777887984338642646e-02,9.428109634203124534e-02,1.000000000000000000e+00 +-1.355231699667234502e-01,-1.478286893948838276e+00,-1.745691703278433149e+00,2.120014411317283631e-01,-1.397098658957899087e+00,-1.501016599025400300e+00,3.430488942349717574e-01,-1.593148440623272544e+00,-2.454628769550215583e-03,-2.170799739660791150e-01,-1.775361995629232137e+00,-1.198240303294508369e+00,-1.559487754720733532e+00,-1.240689768389830194e-01,-1.000000000000000000e+00 +-9.313034480929896497e-02,-1.548711135573551667e+00,3.748586304684338455e-02,2.709530778596377365e-01,-1.896236382869262327e+00,-1.485636556670600683e+00,-3.161759750274304448e-01,-1.164643173374209084e+00,2.586141464409828783e-02,-1.451717591710380173e+00,-1.196062416447940757e+00,-1.289322134291910560e+00,-1.532448668231899402e+00,-1.185949432151032212e-01,1.000000000000000000e+00 +-1.798587046249422450e+00,1.869445715679594888e-02,-1.044510995144790066e-01,5.572535109843471113e-03,2.050689463626149955e-01,-4.569373395860657633e-02,2.411256194966787947e-01,-1.233356642157531907e-01,-2.070072436122830095e-01,-1.795951024666027296e+00,-1.566054540405366291e+00,3.927205796742158639e-01,-1.603352638892656667e+00,-1.273094642344743677e-01,-1.000000000000000000e+00 +-1.695174531165744680e+00,-1.901057242239872380e+00,4.447986228189836078e-02,1.909527852487359345e-01,-1.747763271347229086e+00,-5.746031256144605581e-02,-3.742409088612788093e-01,-1.155721528626410866e+00,1.726663169414113241e-01,-1.451530504363050422e+00,-1.379182285647580386e+00,-1.538823892180034125e+00,-1.778675306065086392e+00,-1.389632348775922399e-01,1.000000000000000000e+00 +-1.516154332717399411e+00,2.948078711775095617e-01,-1.563462614010797669e+00,4.653471419431301492e-02,-1.000266205608066339e-01,-1.635299494656577979e-01,1.981747060035120023e-01,-2.439225329817111265e-01,-2.040020500696499428e-01,-3.313004356918086701e-02,-1.662464812462594477e+00,2.660385912486399884e-01,-1.808148641442627769e+00,-8.892180870150626082e-02,-1.000000000000000000e+00 +-1.897814348096344694e-01,-1.366965856206360463e+00,2.847573068305468691e-02,7.793816993692186113e-02,-1.847043456045497356e+00,1.384223405335286393e-02,-1.735776206540201150e+00,-1.960193423500923959e+00,2.393677744169735166e-02,4.390804650419367905e-02,-1.346637422342611190e+00,-1.525863026477373374e+00,-1.312007080002479231e+00,8.494577091091098930e-02,1.000000000000000000e+00 +-1.718634311223755873e+00,-1.610442672944327303e+00,2.038515277567406331e-02,-3.736477568872324101e-02,-1.742524571488221818e+00,1.347297168283280444e-02,2.893177315601560551e-01,-1.604696419948735020e+00,-2.371395839510001546e-02,-1.463682789949578567e+00,-1.652413527193833831e+00,-1.441831057552340978e+00,-1.703409524414448928e+00,-2.498822282863613631e-02,-1.000000000000000000e+00 +-1.637188078103448463e+00,-1.603885569860671767e+00,9.449692346724855585e-02,1.853561669661850375e-01,-1.904633031994200199e+00,-1.573753785839371311e+00,-1.746192613065815635e+00,-1.605014692948754984e+00,1.884122078598251537e-01,2.130393423176967782e-01,-1.313215595620440101e+00,-1.375569192665423213e+00,-1.609605564479541062e+00,1.032090234096018702e-02,1.000000000000000000e+00 +-1.463932539775016695e+00,-1.396927527135983116e+00,-1.597124513244271737e+00,1.700606987739440301e-01,-1.593214258358096158e+00,7.898635524713594480e-02,1.460646252191022954e-01,-1.727752340910720941e+00,-1.397253179256417455e-01,-6.391578604999410829e-02,-1.506917714065532099e+00,-1.327124410192860093e+00,-1.868621401392190240e+00,-4.814873314166542850e-02,-1.000000000000000000e+00 +-2.453302913512000472e-01,-1.381393475649339120e+00,2.820336751702119671e-02,-1.315876624107797266e+00,-1.824739999228144560e+00,-1.602121961177756182e+00,-3.705984461049904866e-01,2.088605174089669669e-01,-1.388530441999721976e+00,-1.614360672904696092e-01,-1.382764604531399932e+00,-1.468531559001070574e+00,4.112328007139420816e-02,-1.581333600149080088e+00,1.000000000000000000e+00 +2.713805008108567673e-02,-4.492060815494731962e-02,1.321227763640236974e-01,3.839966895412061376e-02,-1.796176035095834456e-01,-1.215735672995639227e-01,-1.373018785594215529e+00,3.902518928637641604e-01,-1.707055539118201293e-01,-2.122842042683150410e-01,-1.625022535456478234e+00,2.678538992717404144e-01,-1.770618558854318403e+00,-3.741509762172745557e-03,-1.000000000000000000e+00 +-1.578884567282865081e+00,-1.899409259283551643e+00,9.479623447999144248e-02,-1.121556247336875867e+00,-1.760989393365797628e+00,1.773819367306432304e-01,-1.958317720905401182e-01,1.349862208361168603e-01,-1.450112157788013389e+00,-1.357031782377540741e-02,-1.241624569528410049e+00,-1.484921610939185177e+00,1.376233665493970526e-01,-1.556604996631108184e+00,1.000000000000000000e+00 +-3.537691289734849059e-02,4.560153415536132016e-02,-1.594171868075497489e+00,3.781513593400184087e-02,-6.310239035113980599e-02,1.094511092510719319e-02,-1.370207269419601692e+00,1.199799911955920506e-02,-1.145156303849539092e-01,-1.601907832583182367e+00,-1.623516799310357195e+00,3.320668925349491429e-01,-1.861623716837563558e+00,-1.508498673571756210e-01,-1.000000000000000000e+00 +-1.393561281424530174e-01,-1.374758695615320159e+00,-7.759579169863080328e-02,-1.376195546802910563e+00,-1.938362542912492881e+00,2.884745655059433103e-02,-2.006920996823445602e+00,-1.004405064435997530e-01,-1.516342257132262672e+00,-1.596307634773844519e+00,-1.299829520439901742e+00,-1.381582888541074139e+00,-1.757494128895809860e-01,-1.272303335421770765e+00,1.000000000000000000e+00 +-1.960094070973704139e-01,-1.697496330090407568e+00,-5.048449415743674895e-02,-7.752022277990533450e-02,-1.662512304274329278e+00,1.033730136420900059e-01,-1.325909631133677991e+00,-1.454292015846018638e+00,-1.190555544626873269e-01,-8.137424270029648687e-02,-1.756864415556221193e+00,-1.241075488481948152e+00,-1.762882077075956788e+00,-2.177474284367885149e-01,-1.000000000000000000e+00 +-1.716506103836173969e+00,-1.693682981881873273e+00,1.236654366300238284e-01,-1.574135526811256902e+00,-1.664471526834966575e+00,-1.570844244304912207e+00,-1.774862866839263642e+00,-1.427638352974658076e-01,-1.373032160890863329e+00,-1.546979483717454640e+00,-1.022122703105811548e+00,-1.601236489560692533e+00,1.529056596888132558e-01,-1.520223067771249159e+00,1.000000000000000000e+00 +1.240211700589305688e-01,-1.673066155807911848e+00,-1.707600454721348804e+00,4.689498168707974951e-02,-1.699469709394447303e+00,2.441835890795615860e-01,-1.358180419249774618e+00,-1.729888665634490730e+00,-5.395068874283309723e-02,-1.573572313118200672e+00,-1.697368928119671549e+00,-1.296355045903686998e+00,-1.784341285676739997e+00,-1.336320934654017800e-01,-1.000000000000000000e+00 +-2.732278629720036323e-01,-1.400079234667832129e+00,1.347668265396879672e-01,-1.391620773220448415e+00,-1.758071744450004470e+00,-1.606464752742112978e+00,-3.402099865624137465e-01,1.174714134753892802e-01,-1.401180205989532190e+00,-1.721918597261670181e+00,-1.287308539237927096e+00,-1.591594651268959160e+00,-1.593165458194905293e+00,-1.557546955013530354e+00,1.000000000000000000e+00 +-1.598971646227494103e+00,1.236046246409897020e-01,3.431877784083394584e-02,-6.969119693131110849e-02,-8.563580320125260825e-02,-1.603697888322635379e+00,-1.329024396720724255e+00,1.682855646235556224e-01,-2.028225234038001645e-01,6.861652215427307966e-02,-1.602857773622162751e+00,3.625317017837687006e-01,-1.797304123293491429e+00,-5.321588608481897520e-02,-1.000000000000000000e+00 +-1.639644981974852200e+00,-1.854171895099718581e+00,-2.051973173553672813e-02,-1.366762673081192592e+00,-1.770892170133351984e+00,-4.244494930413263492e-03,-1.908786714563566189e-01,4.024445837550703065e-01,-1.370617000966698251e+00,-1.721377477636856446e+00,-1.252054711962235167e+00,-1.454787972043852795e+00,-1.453855134864529886e+00,-1.612771266479338106e+00,1.000000000000000000e+00 +-1.860920327172206346e+00,-3.041931909818444690e-02,-1.688612074945298192e+00,7.305155118052963914e-03,6.657171394361634242e-02,-1.711844099602422897e+00,-1.393033245878821713e+00,-8.345334352642244657e-02,5.571952925660876577e-02,-1.518301027691250749e+00,-1.576734468942531997e+00,2.081936884977613755e-01,-1.813402519878215990e+00,-1.063016573519409841e-01,-1.000000000000000000e+00 +-3.165671461358449679e-01,-1.599647375778868286e+00,1.133291167418244011e-01,-1.413005648690241367e+00,-1.761543553015034513e+00,-4.481829010622573328e-02,-1.810204724753768701e+00,-2.899966671567328347e-01,-1.566337292939382397e+00,-2.779789300881632674e-01,-1.298274720101524116e+00,-1.490768568518090698e+00,-1.663696415234289461e+00,-1.562536267185302030e+00,1.000000000000000000e+00 +-1.489405518472745715e+00,-1.564369280865593348e+00,-1.253944555836074802e-02,2.348296659373619311e-02,-1.559396690984679479e+00,-1.477378163711451586e+00,-1.423427973423906590e+00,-1.385349421852070328e+00,-1.112533298319890868e-01,8.818716506551810275e-02,-1.501800184321577802e+00,-1.270020039620027852e+00,-1.645927092564823946e+00,-1.069863646429739246e-01,-1.000000000000000000e+00 +-1.744035853108975909e+00,-1.852529322203081863e+00,-4.882570838350687448e-02,-1.300941334332897270e+00,-1.853250573941688906e+00,-1.660621744728149718e+00,-1.767231625151308050e+00,-1.626395347022064342e-01,-1.314747526695378310e+00,-5.551483382275135270e-02,-1.308293869447473945e+00,-1.485197846757616569e+00,-1.535382907086665805e+00,-1.519174727187065921e+00,1.000000000000000000e+00 +-1.722786422183899990e+00,-1.501959903195960289e+00,-1.365952932099750550e+00,-7.992587737987744134e-02,-1.527679953029977167e+00,-1.344128265453815585e+00,-1.358802884399927136e+00,-1.409152429298882403e+00,-3.089803980877695033e-01,-1.561449503771455749e+00,-1.702209857888720146e+00,-1.354103718271319812e+00,-1.798914482103099344e+00,-3.756717063139381474e-01,-1.000000000000000000e+00 diff --git a/qiskit_runtime/qka/featuremaps.py b/qiskit_runtime/qka/featuremaps.py new file mode 100644 index 0000000000..dcab2ca8d3 --- /dev/null +++ b/qiskit_runtime/qka/featuremaps.py @@ -0,0 +1,132 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""The FeatureMap class.""" + +import json + +import numpy as np + +from qiskit import QuantumCircuit, QuantumRegister + + +class FeatureMap: + """Mapping data with the feature map.""" + + def __init__(self, feature_dimension, entangler_map=None): + """ + Args: + feature_dimension (int): number of features (twice the number of qubits for this + encoding) + entangler_map (list[list]): connectivity of qubits with a list of [source, target], + or None for full entanglement. Note that the order in the list is the order of + applying the two-qubit gate. + + Raises: + ValueError: If the value of ``feature_dimension`` is odd. + """ + + if isinstance(feature_dimension, int): + if feature_dimension % 2 == 0: + self._feature_dimension = feature_dimension + else: + raise ValueError("Feature dimension must be an even integer.") + else: + raise ValueError("Feature dimension must be an even integer.") + + self._num_qubits = int(feature_dimension / 2) + + if entangler_map is None: + self._entangler_map = [ + [i, j] for i in range(self._num_qubits) for j in range(i + 1, self._num_qubits) + ] + else: + self._entangler_map = entangler_map + + self._num_parameters = self._num_qubits + + def construct_circuit(self, x=None, parameters=None, q=None, inverse=False, name=None): + """Construct the feature map circuit. + + Args: + x (numpy.ndarray): data vector of size feature_dimension + parameters (numpy.ndarray): optional parameters in feature map + q (QauntumRegister): the QuantumRegister object for the circuit + inverse (bool): whether or not to invert the circuit + name (str): The name to use for the constructed ``QuantumCircuit`` object + + Returns: + QuantumCircuit: a quantum circuit transforming data x + + Raises: + ValueError: If the input parameters or vector are invalid + """ + + if parameters is not None: + if isinstance(parameters, (int, float)): + raise ValueError("Parameters must be a list.") + if len(parameters) == 1: + parameters = parameters * np.ones(self._num_qubits) + else: + if len(parameters) != self._num_parameters: + raise ValueError( + "The number of feature map parameters must be {}.".format( + self._num_parameters + ) + ) + + if len(x) != self._feature_dimension: + raise ValueError( + "The input vector must be of length {}.".format(self._feature_dimension) + ) + + if q is None: + q = QuantumRegister(self._num_qubits, name="q") + + circuit = QuantumCircuit(q, name=name) + + for i in range(self._num_qubits): + circuit.ry(-parameters[i], q[i]) + + for source, target in self._entangler_map: + circuit.cz(q[source], q[target]) + + for i in range(self._num_qubits): + circuit.rz(-2 * x[2 * i + 1], q[i]) + circuit.rx(-2 * x[2 * i], q[i]) + + if inverse: + return circuit.inverse() + else: + return circuit + + def to_json(self): + """Return JSON representation of this object. + + Returns: + str: JSON string representing this object. + """ + return json.dumps( + {"feature_dimension": self._feature_dimension, "entangler_map": self._entangler_map} + ) + + @classmethod + def from_json(cls, data): + """Return an instance of this class from the JSON representation. + + Args: + data (str): JSON string representing an object. + + Returns: + FeatureMap: An instance of this class. + """ + return cls(**json.loads(data)) diff --git a/qiskit_runtime/qka/kernel_matrix.py b/qiskit_runtime/qka/kernel_matrix.py new file mode 100644 index 0000000000..c12a8ea998 --- /dev/null +++ b/qiskit_runtime/qka/kernel_matrix.py @@ -0,0 +1,138 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""The KernelMatrix class.""" + +import itertools + +import numpy as np + +from qiskit.compiler import transpile + + +class KernelMatrix: + """Build the kernel matrix from a quantum feature map.""" + + def __init__(self, feature_map, backend, initial_layout=None): + """ + Args: + feature_map (int): the feature map object + backend (Backend): the backend instance + initial layout (list or dict): initial position of virtual qubits on the physical + qubits of the quantum device + """ + + self._feature_map = feature_map + self._feature_map_circuit = self._feature_map.construct_circuit # the feature map circuit + self._backend = backend + self._initial_layout = initial_layout + + self.results = {} # store the results object (program_data) + + def construct_kernel_matrix(self, x1_vec, x2_vec, parameters=None): + """Create the kernel matrix for a given feature map and input data. + + With the qasm simulator or real backends, compute order 'n^2' + states Phi^dag(y)Phi(x)|0> for input vectors x and y. + + Args: + x1_vec (numpy.ndarray): NxD array of training data or test data, where N is the + number of samples and D is the feature dimension + x2_vec (numpy.ndarray): MxD array of training data or support vectors, where M + is the number of samples and D is the feature dimension + parameters (numpy.ndarray): optional parameters in feature map + + Returns: + numpy.ndarray: the kernel matrix + """ + + is_identical = False + if np.array_equal(x1_vec, x2_vec): + is_identical = True + + experiments = [] + + measurement_basis = "0" * self._feature_map._num_qubits + + if is_identical: + + my_product_list = list( + itertools.combinations(range(len(x1_vec)), 2) + ) # all pairwise combos of datapoint indices + for index_1, index_2 in my_product_list: + + circuit_1 = self._feature_map_circuit( + x=x1_vec[index_1], parameters=parameters, name="{}_{}".format(index_1, index_2) + ) + circuit_2 = self._feature_map_circuit( + x=x1_vec[index_2], parameters=parameters, inverse=True + ) + circuit = circuit_1.compose(circuit_2) + circuit.measure_all() + + experiments.append(circuit) + + program_data = self._run_circuits(experiments) + self.results["program_data"] = program_data + + mat = np.eye( + len(x1_vec), len(x1_vec) + ) # kernel matrix element on the diagonal is always 1 + for experiment, [index_1, index_2] in enumerate(my_product_list): + + counts = program_data.get_counts(experiment=experiment) + shots = sum(counts.values()) + + mat[index_1][index_2] = ( + counts.get(measurement_basis, 0) / shots + ) # kernel matrix element is the probability of measuring all 0s + mat[index_2][index_1] = mat[index_1][index_2] # kernel matrix is symmetric + + return mat + + else: + + for index_1, point_1 in enumerate(x1_vec): + for index_2, point_2 in enumerate(x2_vec): + + circuit_1 = self._feature_map_circuit( + x=point_1, parameters=parameters, name="{}_{}".format(index_1, index_2) + ) + circuit_2 = self._feature_map_circuit( + x=point_2, parameters=parameters, inverse=True + ) + circuit = circuit_1.compose(circuit_2) + circuit.measure_all() + + experiments.append(circuit) + + program_data = self._run_circuits(experiments) + self.results["program_data"] = program_data + + mat = np.zeros((len(x1_vec), len(x2_vec))) + i = 0 + for index_1, _ in enumerate(x1_vec): + for index_2, _ in enumerate(x2_vec): + + counts = program_data.get_counts(experiment=i) + shots = sum(counts.values()) + + mat[index_1][index_2] = counts.get(measurement_basis, 0) / shots + i += 1 + + return mat + + def _run_circuits(self, circuits): + """Execute the input circuits.""" + + transpiled = transpile(circuits, backend=self._backend, initial_layout=self._initial_layout) + return self._backend.run(transpiled, shots=8192).result() diff --git a/qiskit_runtime/qka/qka.json b/qiskit_runtime/qka/qka.json new file mode 100644 index 0000000000..fe697433dc --- /dev/null +++ b/qiskit_runtime/qka/qka.json @@ -0,0 +1,19 @@ +{ + "name": "quantum-kernel-alignment", + "description": "Quantum kernel alignment algorithm that learns, on a given dataset, a quantum kernel maximizing the SVM classification margin.", + "max_execution_time": 28800, + "version": "1.0", + "parameters": [ + {"name": "feature_map", "description": "An instance of FeatureMap in dictionary format used to map classical data into a quantum state space.", "type": "dict", "required": true}, + {"name": "data", "description": "NxD array of training data, where N is the number of samples and D is the feature dimension.", "type": "numpy.ndarray", "required": true}, + {"name": "labels", "description": "Nx1 array of +/-1 labels of the N training samples.", "type": "numpy.ndarray", "required": true}, + {"name": "initial_kernel_parameters", "description": "Initial parameters of the quantum kernel. If not specified, an array of randomly generated numbers is used.", "type": "numpy.ndarray", "required": false}, + {"name": "maxiters", "description": "Number of SPSA optimization steps. Default is 1.", "type": "int", "required": false}, + {"name": "C", "description": "Penalty parameter for the soft-margin support vector machine. Default is 1.", "type": "float", "required": false}, + {"name": "initial_layout", "description": "Initial position of virtual qubits on the physical qubits of the quantum device. Default is None.", "type": "list or dict", "required": false} + ], + "return_values": [ + {"name": "aligned_kernel_parameters", "description": "The optimized kernel parameters found from quantum kernel alignment.", "type": "numpy.ndarray"}, + {"name": "aligned_kernel_matrix", "description": "The aligned quantum kernel matrix evaluated with the optimized kernel parameters on the training data.", "type": "numpy.ndarray"} + ] +} \ No newline at end of file diff --git a/qiskit_runtime/qka/qka.py b/qiskit_runtime/qka/qka.py new file mode 100644 index 0000000000..bb3e87a48c --- /dev/null +++ b/qiskit_runtime/qka/qka.py @@ -0,0 +1,518 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""Source code for the QKA Qiskit Runtime program.""" + +# pylint: disable=invalid-name + +import itertools +import json +import numpy as np +from numpy.random import RandomState +from qiskit import QuantumCircuit, QuantumRegister +from qiskit.compiler import transpile +from cvxopt import matrix, solvers # pylint: disable=import-error + + +class FeatureMap: + """Mapping data with the feature map.""" + + def __init__(self, feature_dimension, entangler_map=None): + """ + Args: + feature_dimension (int): number of features, twice the number + of qubits for this encoding + entangler_map (list[list]): connectivity of qubits with a list of [source, target], + or None for full entanglement. Note that the order in + the list is the order of applying the two-qubit gate. + Raises: + ValueError: If the value of ``feature_dimension`` is not an even integer. + """ + + if isinstance(feature_dimension, int): + if feature_dimension % 2 == 0: + self._feature_dimension = feature_dimension + else: + raise ValueError("Feature dimension must be an even integer.") + else: + raise ValueError("Feature dimension must be an even integer.") + + self._num_qubits = int(feature_dimension / 2) + + if entangler_map is None: + self._entangler_map = [ + [i, j] + for i in range(self._feature_dimension) + for j in range(i + 1, self._feature_dimension) + ] + else: + self._entangler_map = entangler_map + + self._num_parameters = self._num_qubits + + def construct_circuit(self, x=None, parameters=None, q=None, inverse=False, name=None): + """Construct the feature map circuit. + + Args: + x (numpy.ndarray): data vector of size feature_dimension + parameters (numpy.ndarray): optional parameters in feature map + q (QauntumRegister): the QuantumRegister object for the circuit + inverse (bool): whether or not to invert the circuit + name (str): name of circuit + + Returns: + QuantumCircuit: a quantum circuit transforming data x + Raises: + ValueError: If the input parameters or vector are invalid + """ + + if parameters is not None: + if isinstance(parameters, (int, float)): + raise ValueError("Parameters must be a list.") + if len(parameters) == 1: + parameters = parameters * np.ones(self._num_qubits) + else: + if len(parameters) != self._num_parameters: + raise ValueError( + "The number of feature map parameters must be {}.".format( + self._num_parameters + ) + ) + + if len(x) != self._feature_dimension: + raise ValueError( + "The input vector must be of length {}.".format(self._feature_dimension) + ) + + if q is None: + q = QuantumRegister(self._num_qubits, name="q") + + circuit = QuantumCircuit(q, name=name) + + for i in range(self._num_qubits): + circuit.ry(-parameters[i], q[i]) + + for source, target in self._entangler_map: + circuit.cz(q[source], q[target]) + + for i in range(self._num_qubits): + circuit.rz(-2 * x[2 * i + 1], q[i]) + circuit.rx(-2 * x[2 * i], q[i]) + + if inverse: + return circuit.inverse() + else: + return circuit + + def to_json(self): + """Return JSON representation of this object. + + Returns: + str: JSON string representing this object. + """ + return json.dumps( + {"feature_dimension": self._feature_dimension, "entangler_map": self._entangler_map} + ) + + @classmethod + def from_json(cls, data): + """Return an instance of this class from the JSON representation. + + Args: + data (str): JSON string representing an object. + + Returns: + cls: An instance of this class. + """ + return cls(**json.loads(data)) + + +class KernelMatrix: + """Build the kernel matrix from a quantum feature map.""" + + def __init__(self, feature_map, backend, initial_layout=None): + """ + Args: + feature_map: the feature map object + backend (Backend): the backend instance + initial_layout (list or dict): initial position of virtual + qubits on the physical qubits + of the quantum device + """ + + self._feature_map = feature_map + self._feature_map_circuit = self._feature_map.construct_circuit + self._backend = backend + self._initial_layout = initial_layout + + self.results = {} + + def construct_kernel_matrix(self, x1_vec, x2_vec, parameters=None): + """Create the kernel matrix for a given feature map and input data. + + With the qasm simulator or real backends, compute order 'n^2' + states Phi^dag(y)Phi(x)|0> for input vectors x and y. + + Args: + x1_vec (numpy.ndarray): NxD array of training data or test data, + where N is the number of samples + and D is the feature dimension + x2_vec (numpy.ndarray): MxD array of training data or support + vectors, where M is the number of samples + and D is the feature dimension + parameters (numpy.ndarray): optional parameters in feature map + + Returns: + numpy.ndarray: the kernel matrix + """ + + is_identical = False + if np.array_equal(x1_vec, x2_vec): + is_identical = True + + experiments = [] + + measurement_basis = "0" * self._feature_map._num_qubits + + if is_identical: + + my_product_list = list( + itertools.combinations(range(len(x1_vec)), 2) + ) # all pairwise combos of datapoint indices + + for index_1, index_2 in my_product_list: + + circuit_1 = self._feature_map_circuit( + x=x1_vec[index_1], parameters=parameters, name="{}_{}".format(index_1, index_2) + ) + circuit_2 = self._feature_map_circuit( + x=x1_vec[index_2], parameters=parameters, inverse=True + ) + circuit = circuit_1.compose(circuit_2) + circuit.measure_all() + experiments.append(circuit) + + experiments = transpile( + experiments, backend=self._backend, initial_layout=self._initial_layout + ) + program_data = self._backend.run(experiments, shots=8192).result() + + self.results["program_data"] = program_data + + mat = np.eye( + len(x1_vec), len(x1_vec) + ) # kernel matrix element on the diagonal is always 1 + for experiment, [index_1, index_2] in enumerate(my_product_list): + + counts = program_data.get_counts(experiment=experiment) + shots = sum(counts.values()) + + mat[index_1][index_2] = ( + counts.get(measurement_basis, 0) / shots + ) # kernel matrix element is the probability of measuring all 0s + mat[index_2][index_1] = mat[index_1][index_2] # kernel matrix is symmetric + + return mat + + else: + + for index_1, point_1 in enumerate(x1_vec): + for index_2, point_2 in enumerate(x2_vec): + + circuit_1 = self._feature_map_circuit( + x=point_1, parameters=parameters, name="{}_{}".format(index_1, index_2) + ) + circuit_2 = self._feature_map_circuit( + x=point_2, parameters=parameters, inverse=True + ) + circuit = circuit_1.compose(circuit_2) + circuit.measure_all() + experiments.append(circuit) + + experiments = transpile( + experiments, backend=self._backend, initial_layout=self._initial_layout + ) + program_data = self._backend.run(experiments, shots=8192).result() + + self.results["program_data"] = program_data + + mat = np.zeros((len(x1_vec), len(x2_vec))) + i = 0 + for index_1, _ in enumerate(x1_vec): + for index_2, _ in enumerate(x2_vec): + + counts = program_data.get_counts(experiment=i) + shots = sum(counts.values()) + + mat[index_1][index_2] = counts.get(measurement_basis, 0) / shots + i += 1 + + return mat + + +class QKA: + """The quantum kernel alignment algorithm.""" + + def __init__(self, feature_map, backend, initial_layout=None, user_messenger=None): + """Constructor. + + Args: + feature_map (partial obj): the quantum feature map object + backend (Backend): the backend instance + initial_layout (list or dict): initial position of virtual qubits on + the physical qubits of the quantum device + user_messenger (UserMessenger): used to publish interim results. + """ + + self.feature_map = feature_map + self.feature_map_circuit = self.feature_map.construct_circuit + self.backend = backend + self.initial_layout = initial_layout + self.num_parameters = self.feature_map._num_parameters + + self._user_messenger = user_messenger + self.result = {} + self.kernel_matrix = KernelMatrix( + feature_map=self.feature_map, backend=self.backend, initial_layout=self.initial_layout + ) + + def spsa_parameters(self): + """Return array of precomputed SPSA parameters. + + The i-th optimization step, i>=0, the parameters evolve as + + a_i = a / (i + 1 + A) ** alpha, + c_i = c / (i + 1) ** gamma, + + for fixed coefficents a, c, alpha, gamma, A. + + Returns: + numpy.ndarray: spsa parameters + """ + spsa_params = np.zeros((5)) + spsa_params[0] = 0.05 # a + spsa_params[1] = 0.1 # c + spsa_params[2] = 0.602 # alpha + spsa_params[3] = 0.101 # gamma + spsa_params[4] = 0 # A + + return spsa_params + + def cvxopt_solver(self, K, y, C, max_iters=10000, show_progress=False): + """Convex optimization of SVM objective using cvxopt. + + Args: + K (numpy.ndarray): nxn kernel (Gram) matrix + y (numpy.ndarray): nx1 vector of labels +/-1 + C (float): soft-margin penalty + max_iters (int): maximum iterations for the solver + show_progress (bool): print progress of solver + + Returns: + dict: results from the solver + """ + + if y.ndim == 1: + y = y[:, np.newaxis] + H = np.outer(y, y) * K + f = -np.ones(y.shape) + + n = K.shape[1] # number of training points + + y = y.astype("float") + + P = matrix(H) + q = matrix(f) + G = matrix(np.vstack((-np.eye((n)), np.eye((n))))) + h = matrix(np.vstack((np.zeros((n, 1)), np.ones((n, 1)) * C))) + A = matrix(y, y.T.shape) + b = matrix(np.zeros(1), (1, 1)) + + solvers.options["maxiters"] = max_iters + solvers.options["show_progress"] = show_progress + + ret = solvers.qp(P, q, G, h, A, b, kktsolver="ldl") + + return ret + + def spsa_step_one(self, lambdas, spsa_params, count): + """Evaluate +/- perturbations of kernel parameters (lambdas). + + Args: + lambdas (numpy.ndarray): kernel parameters at step 'count' in SPSA optimization loop + spsa_params (numpy.ndarray): SPSA parameters + count (int): the current step in the SPSA optimization loop + + Returns: + numpy.ndarray: kernel parameters in + direction + numpy.ndarray: kernel parameters in - direction + numpy.ndarray: random vector with elements {-1,1} + """ + + prng = RandomState(count) + + c_spsa = float(spsa_params[1]) / np.power(count + 1, spsa_params[3]) + delta = 2 * prng.randint(0, 2, size=np.shape(lambdas)[0]) - 1 + + lambda_plus = lambdas + c_spsa * delta + lambda_minus = lambdas - c_spsa * delta + + return lambda_plus, lambda_minus, delta + + def spsa_step_two(self, cost_plus, cost_minus, lambdas, spsa_params, delta, count): + """Evaluate one iteration of SPSA on SVM objective function F and + return updated kernel parameters. + + F(alpha, lambda) = 1^T * alpha - (1/2) * alpha^T * Y * K * Y * alpha + + Args: + cost_plus (float): objective function F(alpha_+, lambda_+) + cost_minus (float): objective function F(alpha_-, lambda_-) + lambdas (numpy.ndarray): kernel parameters at step 'count' in SPSA optimization loop + spsa_params (numpy.ndarray): SPSA parameters + delta (numpy.ndarray): random vector with elements {-1,1} + count(int): the current step in the SPSA optimization loop + + Returns: + float: estimate of updated SVM objective function F using average + of F(alpha_+, lambda_+) and F(alpha_-, lambda_-) + numpy.ndarray: updated values of the kernel parameters + after one SPSA optimization step + """ + + a_spsa = float(spsa_params[0]) / np.power(count + 1 + spsa_params[4], spsa_params[2]) + c_spsa = float(spsa_params[1]) / np.power(count + 1, spsa_params[3]) + + g_spsa = (cost_plus - cost_minus) * delta / (2.0 * c_spsa) + + lambdas_new = lambdas - a_spsa * g_spsa + lambdas_new = lambdas_new.flatten() + + cost_final = (cost_plus + cost_minus) / 2 + + return cost_final, lambdas_new + + def align_kernel(self, data, labels, initial_kernel_parameters=None, maxiters=1, C=1): + """Align the quantum kernel. + + Uses SPSA for minimization over kernel parameters (lambdas) and + convex optimization for maximization over lagrange multipliers (alpha): + + min_lambda max_alpha 1^T * alpha - (1/2) * alpha^T * Y * K_lambda * Y * alpha + + Args: + data (numpy.ndarray): NxD array of training data, where N is the + number of samples and D is the feature dimension + labels (numpy.ndarray): Nx1 array of +/-1 labels of the N training samples + initial_kernel_parameters (numpy.ndarray): Initial parameters of the quantum kernel + maxiters (int): number of SPSA optimization steps + C (float): penalty parameter for the soft-margin support vector machine + + Returns: + dict: the results of kernel alignment + """ + + if initial_kernel_parameters is not None: + lambdas = initial_kernel_parameters + else: + lambdas = np.random.uniform(-1.0, 1.0, size=(self.num_parameters)) + + spsa_params = self.spsa_parameters() + + lambda_save = [] + cost_final_save = [] + + for count in range(maxiters): + + lambda_plus, lambda_minus, delta = self.spsa_step_one( + lambdas=lambdas, spsa_params=spsa_params, count=count + ) + + kernel_plus = self.kernel_matrix.construct_kernel_matrix( + x1_vec=data, x2_vec=data, parameters=lambda_plus + ) + kernel_minus = self.kernel_matrix.construct_kernel_matrix( + x1_vec=data, x2_vec=data, parameters=lambda_minus + ) + + ret_plus = self.cvxopt_solver(K=kernel_plus, y=labels, C=C) + cost_plus = -1 * ret_plus["primal objective"] + + ret_minus = self.cvxopt_solver(K=kernel_minus, y=labels, C=C) + cost_minus = -1 * ret_minus["primal objective"] + + cost_final, lambda_best = self.spsa_step_two( + cost_plus=cost_plus, + cost_minus=cost_minus, + lambdas=lambdas, + spsa_params=spsa_params, + delta=delta, + count=count, + ) + + lambdas = lambda_best + + interim_result = {"cost": cost_final, "kernel_parameters": lambdas} + + self._user_messenger.publish(interim_result) + + lambda_save.append(lambdas) + cost_final_save.append(cost_final) + + # Evaluate aligned kernel matrix with optimized set of + # parameters averaged over last 10% of SPSA steps: + num_last_lambdas = int(len(lambda_save) * 0.10) + if num_last_lambdas > 0: + last_lambdas = np.array(lambda_save)[-num_last_lambdas:, :] + lambdas = np.sum(last_lambdas, axis=0) / num_last_lambdas + else: + lambdas = np.array(lambda_save)[-1, :] + + kernel_best = self.kernel_matrix.construct_kernel_matrix( + x1_vec=data, x2_vec=data, parameters=lambdas + ) + + self.result["aligned_kernel_parameters"] = lambdas + self.result["aligned_kernel_matrix"] = kernel_best + + return self.result + + +def main(backend, user_messenger, **kwargs): + """Entry function.""" + + # Reconstruct the feature map object. + feature_map = kwargs.get("feature_map") + fm = FeatureMap.from_json(feature_map) + + data = kwargs.get("data") + labels = kwargs.get("labels") + initial_kernel_parameters = kwargs.get("initial_kernel_parameters", None) + maxiters = kwargs.get("maxiters", 1) + C = kwargs.get("C", 1) + initial_layout = kwargs.get("initial_layout", None) + + qka = QKA( + feature_map=fm, + backend=backend, + initial_layout=initial_layout, + user_messenger=user_messenger, + ) + qka_results = qka.align_kernel( + data=data, + labels=labels, + initial_kernel_parameters=initial_kernel_parameters, + maxiters=maxiters, + C=C, + ) + + return qka_results diff --git a/qiskit_runtime/sample_program/__init__.py b/qiskit_runtime/sample_program/__init__.py new file mode 100644 index 0000000000..0377e07d99 --- /dev/null +++ b/qiskit_runtime/sample_program/__init__.py @@ -0,0 +1,13 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""Package containing ``sample-program`` Qiskit quantum program.""" diff --git a/qiskit_runtime/sample_program/sample_program.json b/qiskit_runtime/sample_program/sample_program.json new file mode 100644 index 0000000000..b6fd9b0e77 --- /dev/null +++ b/qiskit_runtime/sample_program/sample_program.json @@ -0,0 +1,41 @@ +{ + "name": "sample-program", + "description": "(DEPRECATED) A sample runtime program. This runtime program is deprecated, please use hello-world instead.", + "max_execution_time": 300, + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "iterations": { + "type": "integer", + "minimum": 0, + "description": "Number of iterations to run. Each iteration generates a runs a random circuit." + } + }, + "required": [ + "iterations" + ] + }, + "return_values": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "description": "A string that says 'All done!'.", + "type": "string" + }, + "interim_results": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "iteration": { + "type": "integer", + "description": "Iteration number." + }, + "counts": { + "description": "Histogram data of the circuit result.", + "type": "object" + } + } + } + } +} diff --git a/qiskit_runtime/sample_program/sample_program.py b/qiskit_runtime/sample_program/sample_program.py new file mode 100644 index 0000000000..a9e090f8de --- /dev/null +++ b/qiskit_runtime/sample_program/sample_program.py @@ -0,0 +1,53 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""A sample runtime program that submits random circuits for user-specified iterations.""" + +import random +from typing import Any + +from qiskit import transpile +from qiskit.circuit.random import random_circuit + + +def prepare_circuits(backend): + """Generate a random circuit. + + Args: + backend (qiskit.providers.Backend): Backend used for transpilation. + + Returns: + qiskit.QuantumCircuit: Generated circuit. + """ + circuit = random_circuit(num_qubits=5, depth=4, measure=True, seed=random.randint(0, 1000)) + return transpile(circuit, backend) + + +def main(backend, user_messenger, **kwargs) -> Any: + """Main entry point of the program. + + Args: + backend (qiskit.providers.Backend): Backend to submit the circuits to. + user_messenger (qiskit.providers.ibmq.runtime.UserMessenger): Used to communicate with the + program consumer. + kwargs: User inputs. + + Returns: + Final result of the program. + """ + iterations = kwargs.pop("iterations", 5) + for it in range(iterations): + qc = prepare_circuits(backend) + result = backend.run(qc).result() + user_messenger.publish({"iteration": it, "counts": result.get_counts()}) + + return "All done!" diff --git a/qiskit_runtime/version.py b/qiskit_runtime/version.py new file mode 100644 index 0000000000..af762813ae --- /dev/null +++ b/qiskit_runtime/version.py @@ -0,0 +1,5 @@ +# THIS FILE IS GENERATED FROM QISKIT_RUNTIME SETUP.PY +# pylint: disable=missing-module-docstring,invalid-name +short_version = "0.1.0" +version = "0.1.0.dev0+378fb03" +release = False diff --git a/qiskit_runtime/vqe/vqe.py b/qiskit_runtime/vqe/vqe.py new file mode 100644 index 0000000000..65fe6c1b62 --- /dev/null +++ b/qiskit_runtime/vqe/vqe.py @@ -0,0 +1,1164 @@ +# This code is part of qiskit-runtime. +# +# (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. + +"""A generalized SPSA optimizer including support for Hessians.""" + +from abc import ABC, abstractmethod +from typing import Iterator, Optional, Union, Callable, Tuple, List, Dict, Any +import logging +from time import time +import warnings +import sys +import json +import traceback +from collections import deque + +import numpy as np +import scipy + +from qiskit.algorithms.optimizers import Optimizer, OptimizerSupportLevel, SPSA, QNSPSA +from qiskit import Aer +from qiskit.algorithms import VQE, VQEResult +from qiskit.algorithms.exceptions import AlgorithmError +from qiskit.algorithms.minimum_eigen_solvers import MinimumEigensolverResult +from qiskit.circuit import ParameterVector, QuantumCircuit +from qiskit.opflow import ( + StateFn, + CircuitSampler, + PauliExpectation, + ExpectationBase, + OperatorBase, + ListOp, + I, +) +from qiskit.providers import BaseBackend, Backend +from qiskit.utils import QuantumInstance + +from qiskit.ignis.mitigation.measurement import CompleteMeasFitter +from qiskit.providers.ibmq.runtime.utils import RuntimeDecoder + +# the overlap function +OVERLAP = Callable[[np.ndarray, np.ndarray], float] + +# parameters, loss, stepsize, number of function evaluations, accepted +CALLBACK = Callable[[np.ndarray, float, float, int, bool], None] + +logger = logging.getLogger(__name__) + + +# Suppress all warnings +warnings.simplefilter("ignore") + +# pylint: disable=invalid-name + + +# disable check for ansatzs, optimizer setter because of pylint bug +# pylint: disable=no-member +class It(ABC): + """A base class for serializable iterators.""" + + @abstractmethod + def serialize(self) -> Tuple[str, Dict[str, Any]]: + """Serialize the iterator.""" + raise NotImplementedError + + @abstractmethod + def get_iterator(self) -> Iterator[float]: + """Get the iterator.""" + raise NotImplementedError + + @staticmethod + def deserialize(serialized: Tuple[str, Dict[str, Any]]) -> "It": + """Construct the iterator from the serialized data.""" + + name, inputs = serialized + classes = {"Constant": Constant, "Powerlaw": Powerlaw, "Concatenated": Concatenated} + return classes[name](**inputs) + + +class Constant(It): + """An iterator yielding constant values.""" + + def __init__(self, value: float) -> None: + """ + Args: + value: The constant value yielded from this iterator. + """ + self.value = value + + def get_iterator(self) -> Iterator[float]: + def constant_it(): + while True: + yield self.value + + return constant_it + + def serialize(self) -> Tuple[str, Dict[str, Any]]: + return "Constant", {"value": self.value} + + +class Powerlaw(It): + r"""An iterator yielding values from a powerlaw. + + The powerlaw is + + .. math:: + + k(n) = c \left(\frac{1}{n + A}\right)^p, + + where :math:`c` is the constant coeffient (``coeff``), :math:`p` is the exponent + (``power``), :math:`A` is a constant offset (``offset``) and :math:`n` is an integer. + """ + + def __init__(self, coeff, power, offset, skip=0): + """ + Args: + coeff: The coefficient of the powerlaw. + power: The exponent in the powerlaw. + offset: The offset. + skip: How many initial values to skip in the iterator. + """ + self.coeff = coeff + self.power = power + self.offset = offset + self.skip = skip + + def serialize(self) -> Tuple[str, Dict[str, Any]]: + return ( + "Powerlaw", + {"coeff": self.coeff, "power": self.power, "offset": self.offset, "skip": self.skip}, + ) + + def get_iterator(self) -> Iterator[float]: + def powerlaw_it(): + n = 1 + while True: + if n > self.skip: + yield self.coeff / ((n + self.offset) ** self.power) + n += 1 + + return powerlaw_it + + +class Concatenated(It): + """An iterator consisting of concatenated other iterators.""" + + def __init__(self, iterators: List[It], breakpoints: List[int]) -> None: + """ + Args: + iterators: A list of iterators this iterator is made up of. + breakpoints: A list of integers specifying when to use the next iterator. + """ + self.iterators = [] + # deserialize if necessary + for iterator in iterators: + if isinstance(iterator, (list, tuple)): + self.iterators.append(self.deserialize(iterator)) + else: + self.iterators.append(iterator) + + self.breakpoints = breakpoints + + def serialize(self) -> Tuple[str, Dict[str, Any]]: + return ( + "Concatenated", + { + "iterators": [it.serialize() for it in self.iterators], + "breakpoints": self.breakpoints, + }, + ) + + def get_iterator(self) -> Iterator[float]: + iterators = [it.get_iterator()() for it in self.iterators] + breakpoints = self.breakpoints + + def concat(): + i, n = 0, 0 # n counts always up, i is at which iterator/breakpoint pair we are + while True: + if i < len(breakpoints) and n >= breakpoints[i]: + i += 1 + yield next(iterators[i]) + n += 1 + + return concat + + +class _SPSA(Optimizer): + """A generalized SPSA optimizer including support for Hessians.""" + + def __init__( + self, + maxiter: int = 100, + blocking: bool = False, + allowed_increase: Optional[float] = None, + trust_region: bool = False, + learning_rate: Optional[Union[float, Callable[[], Iterator]]] = None, + perturbation: Optional[Union[float, Callable[[], Iterator]]] = None, + resamplings: int = 1, + last_avg: int = 1, + callback: Optional[CALLBACK] = None, + # 2-SPSA arguments + second_order: bool = False, # skip_calibration: bool = False) -> None: + hessian_delay: int = 0, + lse_solver: Optional[Union[str, Callable[[np.ndarray, np.ndarray], np.ndarray]]] = None, + regularization: Optional[float] = None, + perturbation_dims: Optional[int] = None, + initial_hessian: Optional[np.ndarray] = None, + expectation: Optional[ExpectationBase] = None, + backend: Optional[Union[Backend, QuantumInstance]] = None, + ) -> None: + r""" + Args: + maxiter: The maximum number of iterations. + blocking: If True, only accepts updates that improve the loss. + allowed_increase: If blocking is True, this sets by how much the loss can increase + and still be accepted. If None, calibrated automatically to be twice the + standard deviation of the loss function. + trust_region: If True, restricts norm of the random direction to be <= 1. + learning_rate: A generator yielding learning rates for the parameter updates, + :math:`a_k`. + perturbation: A generator yielding the perturbation magnitudes :math:`c_k`. + resamplings: In each step, sample the gradient (and preconditioner) this many times. + last_avg: Return the average of the ``last_avg`` parameters instead of just the + last parameter values. + callback: A callback function passed information in each iteration step. The + information is, in this order: the parameters, the function value, the number + of function evaluations, the stepsize, whether the step was accepted. + second_order: If True, use 2-SPSA instead of SPSA. In 2-SPSA, the Hessian is estimated + additionally to the gradient, and the gradient is preconditioned with the inverse + of the Hessian to improve convergence. + hessian_delay: Start preconditioning only after a certain number of iterations. + Can be useful to first get a stable average over the last iterations before using + the preconditioner. + lse_solver: The method to solve for the inverse of the preconditioner. Per default an + exact LSE solver is used, but can e.g. be overwritten by a minimization routine. + regularization: To ensure the preconditioner is symmetric and positive definite, the + identity times a small coefficient is added to it. This generator yields that + coefficient. + perturbation_dims: The number of dimensions to perturb at once. Per default all + dimensions are perturbed simultaneously. + initial_hessian: The initial guess for the Hessian. By default the identity matrix + is used. + expectation: An expectation converter. + backend: A backend to evaluate the circuits, if the overlap function is provided as + a circuit and the objective function as operator expression. + """ + super().__init__() + + if regularization is None: + regularization = 0.01 + + if lse_solver is None: + lse_solver = np.linalg.solve + + self.history = None + + self.maxiter = maxiter + self.learning_rate = learning_rate + self.perturbation = perturbation + self.blocking = blocking + self.allowed_increase = allowed_increase + self.trust_region = trust_region + self.callback = callback + self.resamplings = resamplings + self.last_avg = last_avg + self.second_order = second_order + self.hessian_delay = hessian_delay + self.lse_solver = lse_solver + self.regularization = regularization + self.perturbation_dims = perturbation_dims + self.initial_hessian = initial_hessian + self.trust_region = trust_region + + # runtime arguments + self.grad_params = None + self.grad_expr = None + self.hessian_params = None + self.hessian_expr = None + self.gradient_expressions = None + + if backend is not None: + self._sampler = CircuitSampler(backend, caching="all") + self._expectation = expectation + else: + self._sampler = None + self._expectation = None + + self._nfev = None + self._moving_avg = None # moving average of the preconditioner + + @staticmethod + def calibrate( + loss: Callable[[np.ndarray], float], + initial_point: np.ndarray, + c: float = 0.1, + stability_constant: float = 0, + target_magnitude: Optional[float] = None, # 2 pi / 10 + alpha: float = 0.602, + gamma: float = 0.101, + modelspace: bool = False, + ) -> Tuple[Iterator[float], Iterator[float]]: + r"""Calibrate SPSA parameters with a powerseries as learning rate and perturbation coeffs. + + The powerseries are: + + .. math:: + + a_k = \frac{a}{(A + k + 1)^\alpha}, c_k = \frac{c}{(k + 1)^\gamma} + + Args: + loss: The loss function. + initial_point: The initial guess of the iteration. + c: The initial perturbation magnitude. + stability_constant: The value of `A`. + target_magnitude: The target magnitude for the first update step. + alpha: The exponent of the learning rate powerseries. + gamma: The exponent of the perturbation powerseries. + modelspace: Whether the target magnitude is the difference of parameter values + or function values (= model space). + + Returns: + tuple(generator, generator): A tuple of powerseries generators, the first one for the + learning rate and the second one for the perturbation. + """ + if target_magnitude is None: + target_magnitude = 2 * np.pi / 10 + + dim = len(initial_point) + + # compute the average magnitude of the first step + steps = 25 + avg_magnitudes = 0 + for _ in range(steps): + # compute the random directon + pert = np.array([1 - 2 * np.random.binomial(1, 0.5) for _ in range(dim)]) + delta = loss(initial_point + c * pert) - loss(initial_point - c * pert) + avg_magnitudes += np.abs(delta / (2 * c)) + + avg_magnitudes /= steps + + if modelspace: + a = target_magnitude / (avg_magnitudes ** 2) + else: + a = target_magnitude / avg_magnitudes + + # compute the rescaling factor for correct first learning rate + if a < 1e-10: + warnings.warn(f"Calibration failed, using {target_magnitude} for `a`") + a = target_magnitude + + def learning_rate(): + return powerseries(a, alpha, stability_constant) + + def perturbation(): + return powerseries(c, gamma) + + return learning_rate, perturbation + + @staticmethod + def estimate_stddev(loss: OperatorBase, initial_point: np.ndarray, avg: int = 25) -> float: + """Estimate the standard deviation of the loss function.""" + losses = [loss(initial_point) for _ in range(avg)] + return np.std(losses) + + # values_dict = {} + # for params in self.grad_params: + # values_dict.update({params[i]: [initial_point[i]] * avg}) + + # # execute at once + # sampled = self._sampler.convert(loss, params=values_dict) + # results = np.real(sampled.eval()) + + def _point_samples(self, loss, x, eps, deltas1, deltas2): + # cache gradient epxressions + if self.gradient_expressions is None: + # sorted loss parameters + sorted_params = sorted(loss.parameters, key=lambda p: p.name) + + # SPSA estimates + theta_p = ParameterVector("th+", len(loss.parameters)) + theta_m = ParameterVector("th-", len(loss.parameters)) + + # 2-SPSA estimates + x_pp = ParameterVector("x++", len(loss.parameters)) + x_pm = ParameterVector("x+-", len(loss.parameters)) + x_mp = ParameterVector("x-+", len(loss.parameters)) + x_mm = ParameterVector("x--", len(loss.parameters)) + + self.grad_expr = [ + loss.assign_parameters(dict(zip(sorted_params, theta_p))), + loss.assign_parameters(dict(zip(sorted_params, theta_m))), + ] + self.grad_params = [theta_p, theta_m] + + # catch QNSPSA case. Could be put in a method to make it a bit nicer + if self.second_order: + if self.hessian_expr is None: + self.hessian_expr = [ + loss.assign_parameters(dict(zip(sorted_params, x_pp))), + loss.assign_parameters(dict(zip(sorted_params, x_pm))), + loss.assign_parameters(dict(zip(sorted_params, x_mp))), + loss.assign_parameters(dict(zip(sorted_params, x_mm))), + ] + self.hessian_params = [x_pp, x_pm, x_mp, x_mm] + + self.gradient_expressions = ListOp(self.grad_expr + self.hessian_expr) + else: + self.gradient_expressions = ListOp(self.grad_expr) + + num_parameters = x.size + resamplings = len(deltas1) + + # SPSA parameters + theta_p_ = np.array([x + eps * delta1 for delta1 in deltas1]) + theta_m_ = np.array([x - eps * delta1 for delta1 in deltas1]) + + # 2-SPSA parameters + x_pp_ = np.array([x + eps * (delta1 + delta2) for delta1, delta2 in zip(deltas1, deltas2)]) + x_pm_ = np.array([x + eps * delta1 for delta1 in deltas1]) + x_mp_ = np.array([x - eps * (delta1 - delta2) for delta1, delta2 in zip(deltas1, deltas2)]) + x_mm_ = np.array([x - eps * delta1 for delta1 in deltas1]) + y_ = np.array([x for _ in deltas1]) + + # build dictionary + values_dict = {} + + if self.second_order: + for params, value_matrix in zip( + self.grad_params + self.hessian_params, + [theta_p_, theta_m_, x_pp_, x_pm_, x_mp_, x_mm_, y_], + ): + values_dict.update( + {params[i]: value_matrix[:, i].tolist() for i in range(num_parameters)} + ) + else: + for params, value_matrix in zip(self.grad_params, [theta_p_, theta_m_]): + values_dict.update( + {params[i]: value_matrix[:, i].tolist() for i in range(num_parameters)} + ) + + # execute at once + sampled = self._sampler.convert(self.gradient_expressions, params=values_dict) + results = np.real(sampled.eval()) + + # put results together + gradient_estimate = np.zeros(x.size) + fval_estimate = 0 + for i in range(resamplings): + self._nfev += 2 + gradient_estimate += (results[i, 0] - results[i, 1]) / (2 * eps) * deltas1[0] + fval_estimate += (results[i, 0] + results[i, 1]) / 2 + + if self.callback is not None: + if self._expectation: + # get estimation error for the function evaluations + variance = np.array( + [ + self._expectation.compute_variance(sampled_op) + for sampled_op in sampled[i][:2] + ] + ) + shots = self._sampler.quantum_instance.run_config.shots + estimation_error = np.sqrt(variance / shots) + else: + estimation_error = [0.0, 0.0] + + self.callback(self._nfev - 1, theta_p_[i, :], results[i, 0], estimation_error[0]) + self.callback(self._nfev, theta_m_[i, :], results[i, 1], estimation_error[1]) + + hessian_estimate = np.zeros((x.size, x.size)) + if self.second_order: + for i in range(resamplings): + self._nfev += 4 + diff = results[i, 2] - results[i, 3] + diff -= results[i, 4] - results[i, 5] + diff /= 2 * eps ** 2 + + rank_one = np.outer(deltas1[i], deltas2[i]) + hessian_estimate += diff * (rank_one + rank_one.T) / 2 + + return ( + gradient_estimate / resamplings, + hessian_estimate / resamplings, + fval_estimate / resamplings, + ) + + def _compute_update(self, loss, x, k, eps): + # compute the perturbations + if isinstance(self.resamplings, dict): + avg = self.resamplings.get(k, 1) + else: + avg = self.resamplings + + gradient = np.zeros(x.size) + preconditioner = np.zeros((x.size, x.size)) + + # accumulate the number of samples + deltas1 = [bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(avg)] + deltas2 = [bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(avg)] + + gradient, preconditioner, fval = self._point_samples(loss, x, eps, deltas1, deltas2) + + # update the exponentially smoothed average + if self.second_order: + smoothed = k / (k + 1) * self._moving_avg + 1 / (k + 1) * preconditioner + self._moving_avg = smoothed + + if k > self.hessian_delay: + # make the preconditioner SPD + spd_preconditioner = _make_spd(smoothed, self.regularization) + + # solve for the gradient update + gradient = np.real(self.lse_solver(spd_preconditioner, gradient)) + + return gradient, fval + + def _minimize(self, loss, initial_point): + # handle circuits case + if not callable(loss): + # sorted loss parameters + sorted_params = sorted(loss.parameters, key=lambda p: p.name) + + def loss_callable(x): + value_dict = dict(zip(sorted_params, x)) + return self._sampler.convert(loss, params=value_dict).eval().real + + else: + loss_callable = loss + + self.history = { + "loss": [], + "params": [], + "time": [], + } + + # ensure learning rate and perturbation are set + # this happens only here because for the calibration the loss function is required + if self.learning_rate is None and self.perturbation is None: + get_learning_rate, get_perturbation = self.calibrate(loss_callable, initial_point) + eta = get_learning_rate() + eps = get_perturbation() + elif self.learning_rate is None or self.perturbation is None: + raise ValueError("If one of learning rate or perturbation is set, both must be set.") + else: + if isinstance(self.learning_rate, float): + eta = constant(self.learning_rate) + else: + eta = self.learning_rate() + + if isinstance(self.perturbation, float): + eps = constant(self.perturbation) + else: + eps = self.perturbation() + + # prepare some initials + x = np.asarray(initial_point) + + if self.initial_hessian is None: + self._moving_avg = np.identity(x.size) + else: + self._moving_avg = self.initial_hessian + + self._nfev = 0 + + # if blocking is enabled we need to keep track of the function values + if self.blocking: + fx = loss_callable(x) + + self._nfev += 1 + if self.allowed_increase is None: + self.allowed_increase = 2 * self.estimate_stddev(loss_callable, x) + + logger.info("=" * 30) + logger.info("Starting SPSA optimization") + start = time() + + # keep track of the last few steps to return their average + last_steps = deque([x]) + + for k in range(1, self.maxiter + 1): + iteration_start = time() + # compute update + update, fx_next = self._compute_update(loss, x, k, next(eps)) + + # trust region + if self.trust_region: + norm = np.linalg.norm(update) + if norm > 1: # stop from dividing by 0 + update = update / norm + + # compute next parameter value + lr = next(eta) + update = update * lr + x_next = x - update + + # blocking + if self.blocking: + fx_next = loss_callable(x_next) + + self._nfev += 1 + if fx + self.allowed_increase <= fx_next: # accept only if loss improved + + self.history["loss"].append(fx_next) + self.history["params"].append(x_next) + self.history["time"].append(time()) + + # if self.callback is not None: + # self.callback(self._nfev, # number of function evals + # x_next, # next parameters + # fx_next, # loss at next parameters + # np.linalg.norm(update), # size of the update step + # False) # not accepted + + logger.info( + "Iteration %s/%s rejected in %s.", + k, + self.maxiter + 1, + time() - iteration_start, + ) + continue + fx = fx_next + + logger.info( + "Iteration %s/%s done in %s.", k, self.maxiter + 1, time() - iteration_start + ) + + # if self.callback is not None: + # self.callback(self._nfev, # number of function evals + # x_next, # next parameters + # fx_next, # loss at next parameters + # np.linalg.norm(update), # size of the update step + # True) # accepted + + self.history["loss"].append(fx_next) + self.history["params"].append(x_next) + self.history["time"].append(time()) + + # update parameters + x = x_next + + # update the list of the last ``last_avg`` parameters + if self.last_avg > 1: + last_steps.append(x_next) + if len(last_steps) > self.last_avg: + last_steps.popleft() + + logger.info("SPSA finished in %s", time() - start) + logger.info("=" * 30) + + if self.last_avg > 1: + x = np.mean(last_steps, axis=0) + + return x, loss_callable(x), self._nfev + + def get_support_level(self): + """Get the support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, # could be supported though + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.required, + } + + def optimize( + self, + num_vars, + objective_function, + gradient_function=None, + variable_bounds=None, + initial_point=None, + ): + return self._minimize(objective_function, initial_point) + + +class _QNSPSA(_SPSA): + """Quantum Natural SPSA.""" + + def __init__( + self, + overlap_fn: Union[OVERLAP, QuantumCircuit], + maxiter: int = 100, + blocking: bool = False, + allowed_increase: Optional[float] = None, + learning_rate: Optional[Union[float, Callable[[], Iterator]]] = None, + perturbation: Optional[Union[float, Callable[[], Iterator]]] = None, + resamplings: int = 1, + callback: Optional[CALLBACK] = None, + # 2-SPSA arguments + hessian_delay: int = 0, + lse_solver: Optional[Union[str, Callable[[np.ndarray, np.ndarray], np.ndarray]]] = None, + regularization: Optional[float] = None, + perturbation_dims: Optional[int] = None, + initial_hessian: Optional[np.ndarray] = None, + expectation: Optional[ExpectationBase] = None, + backend: Optional[Union[Backend, QuantumInstance]] = None, + ) -> None: + r""" + Args: + maxiter: The maximum number of iterations. + blocking: If True, only accepts updates that improve the loss. + allowed_increase: If blocking is True, this sets by how much the loss can increase + and still be accepted. If None, calibrated automatically to be twice the + standard deviation of the loss function. + learning_rate: A generator yielding learning rates for the parameter updates, + :math:`a_k`. + perturbation: A generator yielding the perturbation magnitudes :math:`c_k`. + resamplings: In each step, sample the gradient (and preconditioner) this many times. + callback: A callback function passed information in each iteration step. The + information is, in this order: the parameters, the function value, the number + of function evaluations, the stepsize, whether the step was accepted. + hessian_delay: Start preconditioning only after a certain number of iterations. + Can be useful to first get a stable average over the last iterations before using + the preconditioner. + lse_solver: The method to solve for the inverse of the preconditioner. Per default an + exact LSE solver is used, but can e.g. be overwritten by a minimization routine. + regularization: To ensure the preconditioner is symmetric and positive definite, the + identity times a small coefficient is added to it. This generator yields that + coefficient. + perturbation_dims: The number of dimensions to perturb at once. Per default all + dimensions are perturbed simulatneously. + initial_hessian: The initial guess for the Hessian. By default the identity matrix + is used. + expectation: The Expectation converter for taking the average value of the + Observable over the ansatz state function. + backend: A backend to evaluate the circuits, if the overlap function is provided as + a circuit and the objective function as operator expression. + """ + super().__init__( + maxiter, + blocking, + allowed_increase, + trust_region=False, + learning_rate=learning_rate, + perturbation=perturbation, + resamplings=resamplings, + callback=callback, + second_order=True, + hessian_delay=hessian_delay, + lse_solver=lse_solver, + regularization=regularization, + perturbation_dims=perturbation_dims, + initial_hessian=initial_hessian, + expectation=expectation, + backend=backend, + ) + + self.overlap_fn = overlap_fn + + if not callable(overlap_fn): + sorted_overlap_params = sorted(overlap_fn.parameters, key=lambda p: p.name) + + x_pp = ParameterVector("x++", overlap_fn.num_parameters) + x_pm = ParameterVector("x+-", overlap_fn.num_parameters) + x_mp = ParameterVector("x-+", overlap_fn.num_parameters) + x_mm = ParameterVector("x--", overlap_fn.num_parameters) + y = ParameterVector("y", overlap_fn.num_parameters) + + left = overlap_fn.assign_parameters(dict(zip(sorted_overlap_params, y))) + rights = [ + overlap_fn.assign_parameters(dict(zip(sorted_overlap_params, x_pp))), + overlap_fn.assign_parameters(dict(zip(sorted_overlap_params, x_pm))), + overlap_fn.assign_parameters(dict(zip(sorted_overlap_params, x_mp))), + overlap_fn.assign_parameters(dict(zip(sorted_overlap_params, x_mm))), + ] + + self.hessian_params = [x_pp, x_pm, x_mp, x_mm, y] + self.hessian_expr = [~StateFn(left) @ StateFn(right) for right in rights] + + @staticmethod + def get_overlap(circuit, backend=None, expectation=None): + """Get the overlap function.""" + params_x = ParameterVector("x", circuit.num_parameters) + params_y = ParameterVector("y", circuit.num_parameters) + + expression = ~StateFn(circuit.assign_parameters(params_x)) @ StateFn( + circuit.assign_parameters(params_y) + ) + + if expectation is not None: + expression = expectation.convert(expression) + + if backend is None: + + def overlap_fn(values_x, values_y): + value_dict = dict( + zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) + ) + return -0.5 * np.abs(expression.bind_parameters(value_dict).eval()) ** 2 + + else: + sampler = CircuitSampler(backend) + + def overlap_fn(values_x, values_y): + value_dict = dict( + zip(params_x[:] + params_y[:], values_x.tolist() + values_y.tolist()) + ) + return -0.5 * np.abs(sampler.convert(expression, params=value_dict).eval()) ** 2 + + return overlap_fn + + +class QNSPSAVQE(VQE): + def __init__( + self, + ansatz: Optional[QuantumCircuit] = None, + initial_point: Optional[np.ndarray] = None, + expectation: Optional[ExpectationBase] = None, + callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None, + quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None, + natural_spsa: bool = False, + maxiter: int = 100, + blocking: bool = True, + allowed_increase: float = 0.1, + learning_rate: Optional[float] = None, + perturbation: Optional[float] = None, + regularization: float = 0.01, + resamplings: int = 1, + hessian_delay: int = 0, + initial_hessian: Optional[np.ndarray] = None, + ) -> None: + """ + Args: + ansatz: A parameterized circuit used as Ansatz for the wave function. + initial_point: An optional initial point (i.e. initial parameter values) + for the optimizer. If ``None`` then VQE will look to the variational form for a + preferred point and if not will simply compute a random one. + expectation: The Expectation converter for taking the average value of the + Observable over the ansatz state function. When ``None`` (the default) an + :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select + an appropriate expectation based on the operator and backend. When using Aer + qasm_simulator backend, with paulis, it is however much faster to leverage custom + Aer function for the computation but, although VQE performs much faster + with it, the outcome is ideal, with no shot noise, like using a state vector + simulator. If you are just looking for the quickest performance when choosing Aer + qasm_simulator and the lack of shot noise is not an issue then set `include_custom` + parameter here to ``True`` (defaults to ``False``). + callback: a callback that can access the intermediate data during the optimization. + Four parameter values are passed to the callback as follows during each evaluation + by the optimizer for its current set of parameters as it works towards the minimum. + These are: the evaluation count, the optimizer parameters for the + variational form, the evaluated mean and the evaluated standard deviation.` + quantum_instance: Quantum Instance or Backend + """ + super().__init__( + ansatz=ansatz, + initial_point=initial_point, + quantum_instance=quantum_instance, + expectation=expectation, + ) + + self.natural_spsa = natural_spsa + self.maxiter = maxiter + self.learning_rate = learning_rate + self.perturbation = perturbation + self.allowed_increase = allowed_increase + self.blocking = blocking + self.regularization = regularization + self.resamplings = resamplings + self.hessian_delay = hessian_delay + self.initial_hessian = initial_hessian + + self._ret = VQEResult() + self._eval_time = None + self._callback = callback + + self._eval_count = 0 + + @property + def optimizer(self): # pylint: disable=arguments-differ + raise NotImplementedError( + "The optimizer is a SPSA version with batched circuits and " + "cannot be returned as a standalone." + ) + + @optimizer.setter + def optimizer(self, optimizer): + raise NotImplementedError( + "The optimizer is a SPSA version with batched circuits and " "cannot be set." + ) + + def compute_minimum_eigenvalue( + self, operator: OperatorBase, aux_operators: Optional[List[Optional[OperatorBase]]] = None + ) -> MinimumEigensolverResult: + if self.quantum_instance is None: + raise AlgorithmError( + "A QuantumInstance or Backend " "must be supplied to run the quantum algorithm." + ) + self.quantum_instance.circuit_summary = True + + if operator is None: + raise AlgorithmError("The operator was never provided.") + + self._check_operator_ansatz(operator) + + # We need to handle the array entries being Optional i.e. having value None + if aux_operators: + zero_op = I.tensorpower(operator.num_qubits) * 0.0 + converted = [] + for op in aux_operators: + if op is None: + converted.append(zero_op) + else: + converted.append(op) + + # For some reason Chemistry passes aux_ops with 0 qubits and paulis sometimes. + aux_operators = [zero_op if op == 0 else op for op in converted] + else: + aux_operators = None + + optimizer_settings = { + "maxiter": self.maxiter, + "blocking": self.blocking, + "allowed_increase": self.allowed_increase, + "learning_rate": self.learning_rate, + "perturbation": self.perturbation, + "regularization": self.regularization, + "resamplings": self.resamplings, + "hessian_delay": self.hessian_delay, + "initial_hessian": self.initial_hessian, + "expectation": self.expectation, + "callback": self._callback, + "backend": self._quantum_instance, + } + + if self.natural_spsa: + optimizer = _QNSPSA(overlap_fn=self.ansatz, **optimizer_settings) + else: + optimizer = _SPSA(**optimizer_settings) + + self._eval_count = 0 + # energy_evaluation, expectation = self.get_energy_evaluation( + # operator, return_expectation=True + # ) + + theta = ParameterVector("θ​", self.ansatz.num_parameters) + energy_expectation, expectation = self.construct_expectation( + theta, operator, return_expectation=True + ) + + start_time = time() + opt_params, opt_value, nfev = optimizer.optimize( + num_vars=len(self.initial_point), + objective_function=energy_expectation, + # gradient_function=gradient, + # variable_bounds=bounds, + initial_point=self.initial_point, + ) + eval_time = time() - start_time + + result = VQEResult() + result.optimal_point = opt_params + result.optimal_parameters = dict(zip(self._ansatz_params, opt_params)) + result.optimal_value = opt_value + result.cost_function_evals = nfev + result.optimizer_time = eval_time + result.eigenvalue = opt_value + 0j + result.eigenstate = self._get_eigenstate(result.optimal_parameters) + + logger.info( + "Optimization complete in %s seconds.\nFound opt_params %s in %s evals", + eval_time, + result.optimal_point, + self._eval_count, + ) + + # TODO delete as soon as get_optimal_vector etc are removed + self._ret = result + + if aux_operators is not None: + aux_values = self._eval_aux_ops(opt_params, aux_operators, expectation=expectation) + result.aux_operator_eigenvalues = aux_values[0] + + # return result, None + + return result, optimizer.history + + +# Code from qn-spsa/utils.py + + +def bernoulli_perturbation(dim, perturbation_dims=None): + """Get a Bernoulli random perturbation.""" + if perturbation_dims is None: + return np.array([1 - 2 * np.random.binomial(1, 0.5) for _ in range(dim)]) + + pert = np.array([1 - 2 * np.random.binomial(1, 0.5) for _ in range(perturbation_dims)]) + indices = np.random.choice(list(range(dim)), size=perturbation_dims, replace=False) + result = np.zeros(dim) + result[indices] = pert + + return result + + +def powerseries(eta=0.01, power=2, offset=0): + """Yield a series decreasing by a powerlaw.""" + + n = 1 + while True: + yield eta / ((n + offset) ** power) + n += 1 + + +def constant(eta=0.01): + """Yield a constant series.""" + + while True: + yield eta + + +def _make_spd(matrix, bias=0.01): + identity = np.identity(matrix.shape[0]) + psd = scipy.linalg.sqrtm(matrix.dot(matrix)) + return (1 - bias) * psd + bias * identity + + +class Publisher: + """Class used to publish interim results.""" + + def __init__(self, messenger): + self._messenger = messenger + + def callback(self, *args, **kwargs): + text = list(args) + for k, v in kwargs.items(): + text.append({k: v}) + self._messenger.publish(text) + + +def _parse_optimizer(kwargs): + optimizer = kwargs.get("optimizer", SPSA()) + # backwards compatibility: previously optimizers were dicts + if isinstance(optimizer, dict): + # verify the optimizer and split into name and parameters + optimizer_name = optimizer.pop("name", "SPSA") + if optimizer_name not in ["SPSA", "QN-SPSA"]: + raise ValueError( + f"Unsupported optimizer: {optimizer_name}." + "If specified via dict only SPSA and QN-SPSA are available." + ) + + optimizer_params = optimizer + + # de-serialize learning rate and perturbation if necessary + for attr in ["learning_rate", "perturbation"]: + if attr in optimizer_params.keys(): + if isinstance(optimizer_params[attr], (list, tuple)): # need to de-serialize + iterator_factory = It.deserialize(optimizer_params[attr]) + optimizer_params[attr] = iterator_factory.get_iterator() + + if optimizer_name == "SPSA": + optimizer = _SPSA(**optimizer_params) + else: + optimizer = _QNSPSA(overlap_fn=lambda: None, **optimizer_params) + + return optimizer + + +def main(backend, user_messenger, **kwargs): + """Entry function.""" + # parse inputs + mandatory = {"ansatz", "operator"} + missing = mandatory - set(kwargs.keys()) + if len(missing) > 0: + raise ValueError(f"The following mandatory arguments are missing: {missing}.") + + ansatz = kwargs["ansatz"] + operator = kwargs["operator"] + aux_operators = kwargs.get("aux_operators", None) + initial_point = kwargs.get("initial_point", None) + optimizer = _parse_optimizer(kwargs) + + shots = kwargs.get("shots", 1024) + measurement_error_mitigation = kwargs.get("measurement_error_mitigation", False) + + # set up quantum instance + if measurement_error_mitigation: + _quantum_instance = QuantumInstance( + backend, + shots=shots, + measurement_error_mitigation_shots=shots, + measurement_error_mitigation_cls=CompleteMeasFitter, + ) + else: + _quantum_instance = QuantumInstance(backend, shots=shots) + + publisher = Publisher(user_messenger) + + # verify the initial point + if initial_point == "random" or initial_point is None: + initial_point = np.random.random(ansatz.num_parameters) + elif len(initial_point) != ansatz.num_parameters: + raise ValueError("Mismatching number of parameters and initial point dimension.") + + # construct the VQE instance + if isinstance(optimizer, (SPSA, QNSPSA, _SPSA, _QNSPSA)): + vqe = QNSPSAVQE( + ansatz=ansatz, + initial_point=initial_point, + expectation=PauliExpectation(), + callback=publisher.callback, + quantum_instance=_quantum_instance, + natural_spsa=isinstance(optimizer, QNSPSA), + allowed_increase=optimizer.allowed_increase, + maxiter=optimizer.maxiter, + blocking=optimizer.blocking, + learning_rate=optimizer.learning_rate, + perturbation=optimizer.perturbation, + resamplings=optimizer.resamplings, + regularization=optimizer.regularization, + hessian_delay=optimizer.hessian_delay, + initial_hessian=optimizer.initial_hessian, + ) + result, history = vqe.compute_minimum_eigenvalue(operator, aux_operators) + else: + vqe = VQE( + ansatz=ansatz, + initial_point=initial_point, + expectation=PauliExpectation(), + callback=publisher.callback, + quantum_instance=_quantum_instance, + ) + result = vqe.compute_minimum_eigenvalue(operator, aux_operators) + history = None + + eigenvalues_list = ( + result.aux_operator_eigenvalues.tolist() + if result.aux_operator_eigenvalues is not None + else None + ) + + serialized_result = { + "optimizer_evals": result.optimizer_evals, + "optimizer_time": result.optimizer_time, + "optimal_value": result.optimal_value, + "optimal_point": result.optimal_point, + "optimal_parameters": None, # ParameterVectorElement is not serializable + "cost_function_evals": result.cost_function_evals, + "eigenstate": result.eigenstate, + "eigenvalue": result.eigenvalue, + "aux_operator_eigenvalues": eigenvalues_list, + "optimizer_history": history, + } + + user_messenger.publish(serialized_result, final=True) + + +if __name__ == "__main__": + # the code currently uses Aer instead of runtime provider + _backend = Aer.get_backend("qasm_simulator") + user_params = {} + if len(sys.argv) > 1: + # If there are user parameters. + user_params = json.loads(sys.argv[1], cls=RuntimeDecoder) + try: + main(_backend, **user_params) + except Exception: + print(traceback.format_exc()) diff --git a/qiskit_runtime/vqe/vqe_metadata.json b/qiskit_runtime/vqe/vqe_metadata.json new file mode 100644 index 0000000000..b378e02199 --- /dev/null +++ b/qiskit_runtime/vqe/vqe_metadata.json @@ -0,0 +1,28 @@ +{ + "name": "vqe", + "description": "Variational Quantum Eigensolver (VQE) to find the minimal eigenvalue of a Hamiltonian.", + "max_execution_time": 18000, + "version": "1.1", + "parameters": [ + {"name": "ansatz", "description": "A parameterized quantum circuit preparing the ansatz wavefunction for the VQE. It is assumed that all qubits are initially in the 0 state.", "type": "QuantumCircuit", "required": true}, + {"name": "operator", "description": "The Hamiltonian whose smallest eigenvalue we're trying to find.", "type": "PauliSumOp", "required": true}, + {"name": "optimizer", "description": "The classical optimizer used in to update the parameters in each iteration. Can be either any of Qiskit's optimizer classes. If a dictionary, only SPSA and QN-SPSA are supported and the dictionary must specify the name and options of the optimizer, e.g. ``{'name': 'SPSA', 'maxiter': 100}``.", "type": "Union[Optimizer, dict]", "required": true}, + {"name": "initial_parameters", "description": "Initial parameters of the ansatz. Can be an array or the string ``'random'`` to choose random initial parameters.", "type": "Union[numpy.ndarray, str]", "required": true}, + {"name": "aux_operators", "description": "A list of operators to be evaluated at the final, optimized state.", "type": "List[PauliSumOp]", "required": false}, + {"name": "shots", "description": "The number of shots used for each circuit evaluation. Defaults to 1024.", "type": "int", "required": false}, + {"name": "measurement_error_mitigation", "description": "Whether to apply measurement error mitigation in form of a complete measurement fitter to the measurements. Defaults to False.", "type": "bool", "required": false}, + {"name": "initial_layout", "description": "Initial position of virtual qubits on the physical qubits of the quantum device. Default is None.", "type": "list or dict", "required": false} + ], + "return_values": [ + {"name": "optimizer_evals", "description": "The number of steps of the optimizer.", "type": "int"}, + {"name": "optimizer_time", "description": "The total time taken by the optimizer.", "type": "float"}, + {"name": "optimal_value", "description": "The smallest value found during the optimization. Equal to the ``eigenvalue`` attribute.", "type": "float"}, + {"name": "optimal_point", "description": "The optimal parameter values found during the optimization.", "type": "np.ndarray"}, + {"name": "optimal_parameters", "description": "Not supported at the moment, therefore ``None``.", "type": "NoneType"}, + {"name": "cost_function_evals", "description": "The number of cost function (energy) evaluations", "type": "int"}, + {"name": "eigenstate", "description": "The square root of sampling probabilities for each computational basis state of the circuit with optimal parameters.", "type": "dict"}, + {"name": "eigenvalue", "description": "The estimated eigenvalue.", "type": "complex"}, + {"name": "aux_operator_eigenvalues", "description": "The expectation values of the auxiliary operators at the optimal state.", "type": "np.ndarray"}, + {"name": "optimizer_history", "description": "A dictionary containing information about the optimization process: the value objective function, parameters, and a timestamp.", "type": "dict"} + ] +} diff --git a/releasenotes/archive/0.12/backend-run-circuits-ad446ca9b56209b9.yaml b/releasenotes/archive/0.12/backend-run-circuits-ad446ca9b56209b9.yaml index c06a2e251e..b76da4883e 100644 --- a/releasenotes/archive/0.12/backend-run-circuits-ad446ca9b56209b9.yaml +++ b/releasenotes/archive/0.12/backend-run-circuits-ad446ca9b56209b9.yaml @@ -26,7 +26,7 @@ features: from qiskit.test.reference_circuits import ReferenceCircuits provider = IBMQ.load_account() - backend = provider.get_backend('ibmq_vigo') + backend = provider.backend('ibmq_vigo') circuits = transpile(ReferenceCircuits.bell(), backend=backend) default_shots = backend.options.shots # Returns the backend default of 1024 shots. backend.set_options(shots=2048) # All jobs will now have use 2048 shots. diff --git a/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml b/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml index ad54526302..5604b28de2 100644 --- a/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml +++ b/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml @@ -1,5 +1,6 @@ --- features: - | - You can now pass ``hub``, ``group``, and ``project`` parameters to - :meth:`qiskit_ibm_runtime.IBMRuntimeService.jobs` to filter jobs. \ No newline at end of file + You can now pass ``instance`` parameter in the hub/group/project format to + :meth:`qiskit_ibm_runtime.IBMRuntimeService.jobs` to filter jobs. Currently + only supported for legacy authentication. \ No newline at end of file diff --git a/releasenotes/notes/print-backend-requirements-a682e98ded7977d7.yaml b/releasenotes/notes/print-backend-requirements-a682e98ded7977d7.yaml new file mode 100644 index 0000000000..93cbfa7608 --- /dev/null +++ b/releasenotes/notes/print-backend-requirements-a682e98ded7977d7.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + When printing programs with :meth:`qiskit_ibm_runtime.IBMRuntimeService.pprint_programs`, + ``backend_requirements`` will now be listed. + diff --git a/test/decorators.py b/test/decorators.py deleted file mode 100644 index ba9f1ef28c..0000000000 --- a/test/decorators.py +++ /dev/null @@ -1,363 +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. - -"""Decorators for using with IBM Provider unit tests. - - Environment variables used by the decorators: - * QISKIT_IBM_RUNTIME_API_TOKEN: default API token to use. - * QISKIT_IBM_RUNTIME_API_URL: default API url to use. - * QISKIT_IBM_RUNTIME_HGP: default hub/group/project to use. - * QISKIT_IBM_RUNTIME_PRIVATE_HGP: hub/group/project to use for private jobs. - * QISKIT_IBM_RUNTIME_DEVICE: default device to use. - * QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS: True if use staging credentials. - * QISKIT_IBM_RUNTIME_STAGING_API_TOKEN: staging API token to use. - * QISKIT_IBM_RUNTIME_STAGING_API_URL: staging API url to use. - * QISKIT_IBM_RUNTIME_STAGING_HGP: staging hub/group/project to use. - * QISKIT_IBM_RUNTIME_STAGING_DEVICE: staging device to use. - * QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP: staging hub/group/project to use for private jobs. -""" - -import os -from functools import wraps -from unittest import SkipTest -from typing import Tuple, Optional - -from qiskit.test.testing_options import get_test_options -from qiskit_ibm_runtime import least_busy -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime.credentials import Credentials, discover_credentials -from qiskit_ibm_runtime.hub_group_project import HubGroupProject - - -def requires_qe_access(func): - """Decorator that signals that the test uses the online API. - - It involves: - * determines if the test should be skipped by checking environment - variables. - * if the `QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS` environment variable is - set, it reads the credentials from an alternative set of environment - variables. - * if the test is not skipped, it reads `qe_token` and `qe_url` from - environment variables or qiskitrc. - * if the test is not skipped, it appends `qe_token` and `qe_url` as - arguments to the test function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - def _wrapper(obj, *args, **kwargs): - if get_test_options()["skip_online"]: - raise SkipTest("Skipping online tests") - credentials = _get_credentials() - kwargs.update({"qe_token": credentials.token, "qe_url": credentials.url}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def requires_providers(func): - """Decorator that signals the test uses the online API, via a public and premium hgp. - - This decorator delegates into the `requires_qe_access` decorator and appends a provider, - an open access hub/group/project and a premium hub/group/project to the decorated function. - - Args: - func (callable): Test function to be decorated. - - Returns: - callable: The decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - qe_token = kwargs.pop("qe_token") - qe_url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - # Get open access hgp - open_hgp = _get_open_hgp(service) - if not open_hgp: - raise SkipTest("Requires open access hub/group/project.") - # Get a premium hgp - premium_hub, premium_group, premium_project = _get_custom_hgp() - if not all([premium_hub, premium_group, premium_project]): - raise SkipTest( - "Requires both the open access and premium hub/group/project." - ) - kwargs.update( - { - "service": service, - "hgps": { - "open_hgp": { - "hub": open_hgp.credentials.hub, - "group": open_hgp.credentials.group, - "project": open_hgp.credentials.project, - }, - "premium_hgp": { - "hub": premium_hub, - "group": premium_group, - "project": premium_project, - }, - }, - } - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_provider(func): - """Decorator that signals the test uses the online API, via a custom hub/group/project. - - This decorator delegates into the `requires_qe_access` decorator, but - instead of the credentials it appends a `provider` argument to the decorated - function. It also appends the custom `hub`, `group` and `project` arguments. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - token = kwargs.pop("qe_token") - url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=token, url=url) - hub, group, project = _get_custom_hgp() - kwargs.update( - {"service": service, "hub": hub, "group": group, "project": project} - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_private_provider(func): - """Decorator that signals the test requires a hub/group/project for private jobs. - - This decorator appends `provider`, `hub`, `group` and `project` arguments to the decorated - function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - token = kwargs.pop("qe_token") - url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=token, url=url) - hub, group, project = _get_private_hgp() - kwargs.update( - {"service": service, "hub": hub, "group": group, "project": project} - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_device(func): - """Decorator that retrieves the appropriate backend to use for testing. - - It involves: - * Enable the account using credentials obtained from the - `requires_qe_access` decorator. - * Use the backend specified by `QISKIT_IBM_RUNTIME_STAGING_DEVICE` if - `QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS` is set, otherwise use the backend - specified by `QISKIT_IBM_RUNTIME_DEVICE`. - * if device environment variable is not set, use the least busy - real backend. - * appends arguments `backend` to the decorated function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(obj, *args, **kwargs): - backend_name = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_DEVICE", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_DEVICE", None) - ) - _backend = _get_backend( - qe_token=kwargs.pop("qe_token"), - qe_url=kwargs.pop("qe_url"), - backend_name=backend_name, - ) - kwargs.update({"backend": _backend}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def requires_runtime_device(func): - """Decorator that retrieves the appropriate backend to use for testing. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(obj, *args, **kwargs): - backend_name = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_DEVICE", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_DEVICE", None) - ) - if not backend_name: - raise SkipTest("Runtime device not specified") - _backend = _get_backend( - qe_token=kwargs.pop("qe_token"), - qe_url=kwargs.pop("qe_url"), - backend_name=backend_name, - ) - kwargs.update({"backend": _backend}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def _get_backend(qe_token, qe_url, backend_name): - """Get the specified backend.""" - service = IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - _backend = None - hub, group, project = _get_custom_hgp() - if backend_name: - _backend = service.get_backend( - name=backend_name, hub=hub, group=group, project=project - ) - else: - _backend = least_busy( - service.backends( - simulator=False, min_num_qubits=5, hub=hub, group=group, project=project - ) - ) - if not _backend: - raise Exception("Unable to find a suitable backend.") - return _backend - - -def _get_credentials(): - """Finds the credentials for a specific test and options. - - Returns: - Credentials: set of credentials - - Raises: - Exception: When the credential could not be set and they are needed - for that set of options. - """ - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""): - # Special case: instead of using the standard credentials mechanism, - # load them from different environment variables. This assumes they - # will always be in place, as is used by the CI setup. - return Credentials( - token=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_TOKEN"), - url=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_URL"), - auth_url=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_URL"), - ) - # Attempt to read the standard credentials. - discovered_credentials, _ = discover_credentials() - if discovered_credentials: - # Decide which credentials to use for testing. - if len(discovered_credentials) > 1: - try: - # Attempt to use IBM Quantum credentials. - return discovered_credentials[(None, None, None)] - except KeyError: - pass - # Use the first available credentials. - return list(discovered_credentials.values())[0] - raise Exception("Unable to locate valid credentials.") - - -def _get_open_hgp(service: IBMRuntimeService) -> Optional[HubGroupProject]: - """Get open hub/group/project - - Returns: - Open hub/group/project or ``None``. - """ - hgps = service._get_hgps() - for hgp in hgps: - if hgp.is_open: - return hgp - return None - - -def _get_custom_hgp() -> Tuple[str, str, str]: - """Get a custom hub/group/project - - Gets the hub/group/project set in QISKIT_IBM_RUNTIME_STAGING_HGP for staging env or - QISKIT_IBM_RUNTIME_HGP for production env. - - Returns: - Tuple of custom hub/group/project or ``None`` if not set. - """ - hub = None - group = None - project = None - hgp = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_HGP", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_HGP", None) - ) - if hgp: - hub, group, project = hgp.split("/") - return hub, group, project - - -def _get_private_hgp() -> Tuple[str, str, str]: - """Get a private hub/group/project - - Gets the hub/group/project set in QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP for staging env or - QISKIT_IBM_RUNTIME_PRIVATE_HGP for production env. - - Returns: - Tuple of custom hub/group/project or ``None`` if not set. - - Raises: - SkipTest: requires private provider - """ - hub = None - group = None - project = None - hgp = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_PRIVATE_HGP", None) - ) - if not hgp: - raise SkipTest("Requires private provider.") - hub, group, project = hgp.split("/") - return hub, group, project diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py deleted file mode 100644 index e135d60441..0000000000 --- a/test/ibm/runtime/test_runtime.py +++ /dev/null @@ -1,907 +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. - -"""Tests for runtime service.""" - -import copy -import json -import os -from io import StringIO -from unittest.mock import patch -from unittest import mock, skipIf -import uuid -import time -import random -import subprocess -import tempfile -import warnings -from datetime import datetime -import numpy as np -import scipy.sparse - -from qiskit.algorithms.optimizers import ( - ADAM, - GSLS, - IMFIL, - SPSA, - QNSPSA, - SNOBFIT, - L_BFGS_B, - NELDER_MEAD, -) -from qiskit.result import Result -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.test.reference_circuits import ReferenceCircuits -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import ( - PauliSumOp, - MatrixOp, - PauliOp, - CircuitOp, - EvolvedOp, - TaperedPauliSumOp, - Z2Symmetries, - I, - X, - Y, - Z, - StateFn, - CircuitStateFn, - DictStateFn, - VectorStateFn, - OperatorStateFn, - SparseVectorStateFn, - CVaRMeasurement, - ComposedOp, - SummedOp, - TensoredOp, -) -from qiskit.quantum_info import SparsePauliOp, Pauli, PauliTable, Statevector -from qiskit.providers.jobstatus import JobStatus - -from qiskit_ibm_runtime.exceptions import IBMInputValueError -from qiskit_ibm_runtime import IBMRuntimeService, RuntimeJob, IBMBackend -from qiskit_ibm_runtime.credentials import Credentials -from qiskit_ibm_runtime.hub_group_project import HubGroupProject -from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder -from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm_runtime.exceptions import RuntimeProgramNotFound, RuntimeJobFailureError -from qiskit_ibm_runtime.runtime_program import ParameterNamespace - -from ...ibm_test_case import IBMTestCase -from .fake_runtime_client import ( - BaseFakeRuntimeClient, - FailedRanTooLongRuntimeJob, - FailedRuntimeJob, - CancelableRuntimeJob, - CustomResultRuntimeJob, -) -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types -from ...contextmanagers import mock_ibm_provider - - -class TestRuntime(IBMTestCase): - """Class for testing runtime modules.""" - - DEFAULT_DATA = "def main() {}" - DEFAULT_METADATA = { - "name": "qiskit-test", - "description": "Test program.", - "max_execution_time": 300, - "spec": { - "backend_requirements": {"min_num_qubits": 5}, - "parameters": { - "properties": { - "param1": { - "description": "Desc 1", - "type": "string", - "enum": ["a", "b", "c"], - }, - "param2": {"description": "Desc 2", "type": "integer", "min": 0}, - }, - "required": ["param1"], - }, - "return_values": { - "type": "object", - "description": "Return values", - "properties": { - "ret_val": {"description": "Some return value.", "type": "string"} - }, - }, - "interim_results": { - "properties": { - "int_res": {"description": "Some interim result", "type": "string"} - } - }, - }, - } - - def setUp(self): - """Initial test setup.""" - super().setUp() - with mock_ibm_provider(): - self.service = IBMRuntimeService(auth="legacy", token="abc") - self.service._programs = {} - self.service._default_hgp = mock.MagicMock(spec=HubGroupProject) - self.service._default_hgp.credentials = Credentials( - token="", url="", services={"runtime": "https://quantum-computing.ibm.com"} - ) - - def get_backend(backend_name, hub=None, group=None, project=None): - # pylint: disable=unused-argument - return mock.MagicMock(spec=IBMBackend) - - self.service.get_backend = get_backend - self.service._api_client = BaseFakeRuntimeClient() - - def test_coder(self): - """Test runtime encoder and decoder.""" - result = Result( - backend_name="ibmqx2", - backend_version="1.1", - qobj_id="12345", - job_id="67890", - success=False, - results=[], - ) - - data = { - "string": "foo", - "float": 1.5, - "complex": 2 + 3j, - "array": np.array([[1, 2, 3], [4, 5, 6]]), - "result": result, - "sclass": SerializableClass("foo"), - } - encoded = json.dumps(data, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - decoded["sclass"] = SerializableClass.from_json(decoded["sclass"]) - - decoded_result = decoded.pop("result") - data.pop("result") - - decoded_array = decoded.pop("array") - orig_array = data.pop("array") - - self.assertEqual(decoded, data) - self.assertIsInstance(decoded_result, Result) - self.assertTrue((decoded_array == orig_array).all()) - - def test_coder_qc(self): - """Test runtime encoder and decoder for circuits.""" - bell = ReferenceCircuits.bell() - unbound = EfficientSU2(num_qubits=4, reps=1, entanglement="linear") - subtests = (bell, unbound, [bell, unbound]) - for circ in subtests: - with self.subTest(circ=circ): - encoded = json.dumps(circ, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - if not isinstance(circ, list): - decoded = [decoded] - self.assertTrue( - all(isinstance(item, QuantumCircuit) for item in decoded) - ) - - def test_coder_operators(self): - """Test runtime encoder and decoder for operators.""" - x = Parameter("x") - y = x + 1 - qc = QuantumCircuit(1) - qc.h(0) - coeffs = np.array([1, 2, 3, 4, 5, 6]) - table = PauliTable.from_labels(["III", "IXI", "IYY", "YIZ", "XYZ", "III"]) - op = 2.0 * I ^ I - z2_symmetries = Z2Symmetries( - [Pauli("IIZI"), Pauli("ZIII")], - [Pauli("IIXI"), Pauli("XIII")], - [1, 3], - [-1, 1], - ) - isqrt2 = 1 / np.sqrt(2) - sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=y), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), - PauliSumOp.from_list( - [("II", -1.052373245772859), ("IZ", 0.39793742484318045)] - ), - PauliSumOp(SparsePauliOp(table, coeffs), coeff=10), - MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=x), - PauliOp(primitive=Pauli("Y"), coeff=x), - CircuitOp(qc, coeff=x), - EvolvedOp(op, coeff=x), - TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), - StateFn(qc, coeff=x), - CircuitStateFn(qc, is_measurement=True), - DictStateFn("1" * 3, is_measurement=True), - VectorStateFn(np.ones(2 ** 3, dtype=complex)), - OperatorStateFn(CircuitOp(QuantumCircuit(1))), - SparseVectorStateFn(sparse), - Statevector([1, 0]), - CVaRMeasurement(Z, 0.2), - ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), - SummedOp([X ^ X * 2, Y ^ Y], 2), - TensoredOp([(X ^ Y), (Z ^ I)]), - (Z ^ Z) ^ (I ^ 2), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(op, decoded) - - @skipIf(os.name == "nt", "Test not supported on Windows") - def test_coder_optimizers(self): - """Test runtime encoder and decoder for optimizers.""" - subtests = ( - (ADAM, {"maxiter": 100, "amsgrad": True}), - (GSLS, {"maxiter": 50, "min_step_size": 0.01}), - (IMFIL, {"maxiter": 20}), - (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), - (SNOBFIT, {"maxiter": 200, "maxfail": 20}), - (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), - # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 - (L_BFGS_B, {}), - (NELDER_MEAD, {}), - ) - for opt_cls, settings in subtests: - with self.subTest(opt_cls=opt_cls): - optimizer = opt_cls(**settings) - encoded = json.dumps(optimizer, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertTrue(isinstance(decoded, opt_cls)) - for key, value in settings.items(): - self.assertEqual(decoded.settings[key], value) - - def test_encoder_datetime(self): - """Test encoding a datetime.""" - subtests = ( - {"datetime": datetime.now()}, - {"datetime": datetime(2021, 8, 4)}, - {"datetime": datetime.fromtimestamp(1326244364)}, - ) - for obj in subtests: - encoded = json.dumps(obj, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(decoded, obj) - - def test_encoder_callable(self): - """Test encoding a callable.""" - with warnings.catch_warnings(record=True) as warn_cm: - encoded = json.dumps({"fidelity": lambda x: x}, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertIsNone(decoded["fidelity"]) - self.assertEqual(len(warn_cm), 1) - - def test_decoder_import(self): - """Test runtime decoder importing modules.""" - script = """ -import sys -import json -from qiskit_ibm_runtime import RuntimeDecoder -if __name__ == '__main__': - obj = json.loads(sys.argv[1], cls=RuntimeDecoder) - print(obj.__class__.__name__) -""" - temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(script) - temp_fp.close() - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - DictStateFn("1" * 3, is_measurement=True), - Statevector([1, 0]), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - cmd = ["python", temp_fp.name, encoded] - proc = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - check=True, - ) - self.assertIn(op.__class__.__name__, proc.stdout) - - def test_list_programs(self): - """Test listing programs.""" - program_id = self._upload_program() - programs = self.service.programs() - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_id, all_ids) - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - program_1 = self._upload_program() - program_2 = self._upload_program() - program_3 = self._upload_program() - programs = self.service.programs(limit=2, skip=1) - all_ids = [prog.program_id for prog in programs] - self.assertNotIn(program_1, all_ids) - self.assertIn(program_2, all_ids) - self.assertIn(program_3, all_ids) - programs = self.service.programs(limit=3) - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_1, all_ids) - - def test_list_program(self): - """Test listing a single program.""" - program_id = self._upload_program() - program = self.service.program(program_id) - self.assertEqual(program_id, program.program_id) - - def test_print_programs(self): - """Test printing programs.""" - ids = [] - for idx in range(3): - ids.append(self._upload_program(name=f"name_{idx}")) - - programs = self.service.programs() - with patch("sys.stdout", new=StringIO()) as mock_stdout: - self.service.pprint_programs() - stdout = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout) - self.assertIn(prog.name, stdout) - self.assertNotIn(str(prog.max_execution_time), stdout) - self.service.pprint_programs(detailed=True) - stdout_detailed = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout_detailed) - self.assertIn(prog.name, stdout_detailed) - self.assertIn(str(prog.max_execution_time), stdout_detailed) - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program( - max_execution_time=max_execution_time, is_public=is_public - ) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - def test_update_program(self): - """Test updating program.""" - new_data = "def main() {foo=bar}" - new_metadata = copy.deepcopy(self.DEFAULT_METADATA) - new_metadata["name"] = "test_update_program" - new_name = "name2" - new_description = "some other description" - new_cost = self.DEFAULT_METADATA["max_execution_time"] + 100 - new_spec = copy.deepcopy(self.DEFAULT_METADATA["spec"]) - new_spec["backend_requirements"] = {"input_allowed": "runtime"} - - sub_tests = [ - {"data": new_data}, - {"metadata": new_metadata}, - {"data": new_data, "metadata": new_metadata}, - {"metadata": new_metadata, "name": new_name}, - { - "data": new_data, - "metadata": new_metadata, - "description": new_description, - }, - {"max_execution_time": new_cost, "spec": new_spec}, - ] - - for new_vals in sub_tests: - with self.subTest(new_vals=new_vals.keys()): - program_id = self._upload_program() - self.service.update_program(program_id=program_id, **new_vals) - updated = self.service.program(program_id, refresh=True) - if "data" in new_vals: - raw_program = self.service._api_client.program_get(program_id) - self.assertEqual(new_data, raw_program["data"]) - if "metadata" in new_vals and "name" not in new_vals: - self.assertEqual(new_metadata["name"], updated.name) - if "name" in new_vals: - self.assertEqual(new_name, updated.name) - if "description" in new_vals: - self.assertEqual(new_description, updated.description) - if "max_execution_time" in new_vals: - self.assertEqual(new_cost, updated.max_execution_time) - if "spec" in new_vals: - raw_program = self.service._api_client.program_get(program_id) - self.assertEqual(new_spec, raw_program["spec"]) - - def test_update_program_no_new_fields(self): - """Test updating a program without any new data.""" - program_id = self._upload_program() - with warnings.catch_warnings(record=True) as warn_cm: - self.service.update_program(program_id=program_id) - self.assertEqual(len(warn_cm), 1) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.delete_program(program_id) - - def test_run_program(self): - """Test running program.""" - params = {"param1": "foo"} - job = self._run_program(inputs=params) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - - def test_run_program_with_custom_runtime_image(self): - """Test running program.""" - params = {"param1": "foo"} - image = "name:tag" - job = self._run_program(inputs=params, image=image) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - self.assertEqual(job.image, image) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program_id = self._upload_program(name="qiskit-test") - self.service.programs() - program = self.service.program(program_id) - self.assertEqual(program.data, self.DEFAULT_DATA) - self._validate_program(program) - - def test_program_params_validation(self): - """Test program parameters validation process""" - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA - ) - program = self.service.program(program_id) - params: ParameterNamespace = program.parameters() - params.param1 = "Hello, World" - # Check OK params - params.validate() - # Check OK params - contains unnecessary param - params.param3 = "Hello, World" - params.validate() - # Check bad params - missing required param - params.param1 = None - with self.assertRaises(IBMInputValueError): - params.validate() - params.param1 = "foo" - - def test_program_params_namespace(self): - """Test running a program using parameter namespace.""" - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA - ) - params = self.service.program(program_id).parameters() - params.param1 = "Hello World" - self._run_program(program_id, inputs=params) - - def test_run_program_failed(self): - """Test a failed program execution.""" - job = self._run_program(job_classes=FailedRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual( - API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - job = self._run_program(job_classes=FailedRanTooLongRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual( - API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( - job.job_id, job_result_raw - ), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job(self): - """Test retrieving a job.""" - program_id = self._upload_program() - params = {"param1": "foo"} - job = self._run_program(program_id, inputs=params) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(program_id, rjob.program_id) - - def test_jobs_no_limit(self): - """Test retrieving jobs without limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(25): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(limit=None) - self.assertEqual(25, len(rjobs)) - - def test_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - job_count = 25 - program_id = self._upload_program() - for _ in range(job_count): - jobs.append(self._run_program(program_id)) - - limits = [21, 30] - for limit in limits: - with self.subTest(limit=limit): - rjobs = self.service.jobs(limit=limit) - self.assertEqual(min(limit, job_count), len(rjobs)) - - def test_jobs_skip(self): - """Test retrieving jobs with skip.""" - jobs = [] - program_id = self._upload_program() - for _ in range(5): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(skip=4) - self.assertEqual(1, len(rjobs)) - - def test_jobs_skip_limit(self): - """Test retrieving jobs with skip and limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(10): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(skip=4, limit=2) - self.assertEqual(2, len(rjobs)) - - def test_jobs_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - rjobs = self.service.jobs(pending=True) - self.assertEqual(pending_jobs_count, len(rjobs)) - - def test_jobs_limit_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 4 - rjobs = self.service.jobs(limit=limit, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - skip = 4 - rjobs = self.service.jobs(skip=skip, pending=True) - self.assertEqual(pending_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 2 - skip = 3 - rjobs = self.service.jobs(limit=limit, skip=skip, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - rjobs = self.service.jobs(pending=False) - self.assertEqual(returned_jobs_count, len(rjobs)) - - def test_jobs_limit_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 6 - rjobs = self.service.jobs(limit=limit, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - skip = 4 - rjobs = self.service.jobs(skip=skip, pending=False) - self.assertEqual(returned_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 6 - skip = 2 - rjobs = self.service.jobs(limit=limit, skip=skip, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_filter_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - program_id_1 = self._upload_program() - job = self._run_program(program_id=program_id) - job_1 = self._run_program(program_id=program_id_1) - job.wait_for_final_state() - job_1.wait_for_final_state() - rjobs = self.service.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - program_id = self._upload_program() - job = self._run_program( - program_id=program_id, - hub="defaultHub", - group="defaultGroup", - project="defaultProject", - ) - job.wait_for_final_state() - rjobs = self.service.jobs( - program_id=program_id, - hub="defaultHub", - group="defaultGroup", - project="defaultProject", - ) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.service.jobs( - program_id=program_id, hub="test", group="test", project="test" - ) - self.assertFalse(rjobs) - with self.assertRaises(IBMInputValueError): - self.service.jobs(hub="defaultHub") - - def test_cancel_job(self): - """Test canceling a job.""" - job = self._run_program(job_classes=CancelableRuntimeJob) - time.sleep(1) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_final_result(self): - """Test getting final result.""" - job = self._run_program() - result = job.result() - self.assertTrue(result) - - def test_interim_results(self): - """Test getting interim results.""" - job = self._run_program() - interim_results = job.interim_results() - self.assertTrue(interim_results) - - def test_job_status(self): - """Test job status.""" - job = self._run_program() - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - inputs = {"param1": "foo", "param2": "bar"} - job = self._run_program(inputs=inputs) - self.assertEqual(inputs, job.inputs) - - def test_job_program_id(self): - """Test job program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - self.assertEqual(program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_result_decoder(self): - """Test result decoder.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - sub_tests = [(SerializableClassDecoder, None), (None, SerializableClassDecoder)] - for result_decoder, decoder in sub_tests: - with self.subTest(decoder=decoder): - job = self._run_program(job_classes=job_cls, decoder=result_decoder) - result = job.result(decoder=decoder) - self.assertIsInstance(result["serializable_class"], SerializableClass) - - def test_get_result_twice(self): - """Test getting results multiple times.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - job = self._run_program(job_classes=job_cls) - _ = job.result() - _ = job.result() - - def test_program_metadata(self): - """Test program metadata.""" - file_name = "test_metadata.json" - with open(file_name, "w") as file: - json.dump(self.DEFAULT_METADATA, file) - self.addCleanup(os.remove, file_name) - - sub_tests = [file_name, self.DEFAULT_METADATA] - - for metadata in sub_tests: - with self.subTest(metadata_type=type(metadata)): - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=metadata - ) - program = self.service.program(program_id) - self.service.delete_program(program_id) - self._validate_program(program) - - def test_different_providers(self): - """Test retrieving job submitted with different provider.""" - program_id = self._upload_program() - job = self._run_program(program_id) - cred = Credentials( - token="", - url="", - hub="hub2", - group="group2", - project="project2", - services={"runtime": "https://quantum-computing.ibm.com"}, - ) - self.service._default_hgp.credentials = cred - rjob = self.service.job(job.job_id) - self.assertIsNotNone(rjob.backend) - - def _upload_program( - self, name=None, max_execution_time=300, is_public: bool = False - ): - """Upload a new program.""" - name = name or uuid.uuid4().hex - data = self.DEFAULT_DATA - metadata = copy.deepcopy(self.DEFAULT_METADATA) - metadata.update(name=name) - metadata.update(is_public=is_public) - metadata.update(max_execution_time=max_execution_time) - program_id = self.service.upload_program(data=data, metadata=metadata) - return program_id - - def _run_program( - self, - program_id=None, - inputs=None, - job_classes=None, - final_status=None, - decoder=None, - image="", - hub=None, - group=None, - project=None, - ): - """Run a program.""" - options = {"backend_name": "some_backend"} - if final_status is not None: - self.service._api_client.set_final_status(final_status) - elif job_classes: - self.service._api_client.set_job_classes(job_classes) - elif all([hub, group, project]): - self.service._api_client.set_hgp(hub, group, project) - if program_id is None: - program_id = self._upload_program() - with patch( - "qiskit_ibm_runtime.ibm_runtime_service.RuntimeClient", - return_value=self.service._api_client, - ): - job = self.service.run( - program_id=program_id, - options=options, - inputs=inputs, - result_decoder=decoder, - image=image, - ) - return job - - def _populate_jobs_with_all_statuses(self, jobs, program_id): - pending_jobs_count = 0 - returned_jobs_count = 0 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status="RUNNING")) - pending_jobs_count += 1 - for _ in range(4): - jobs.append(self._run_program(program_id, final_status="COMPLETED")) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status="QUEUED")) - pending_jobs_count += 1 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status="FAILED")) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status="CANCELLED")) - returned_jobs_count += 1 - return (jobs, pending_jobs_count, returned_jobs_count) - - def _validate_program(self, program): - """Validate a program.""" - self.assertEqual(self.DEFAULT_METADATA["name"], program.name) - self.assertEqual(self.DEFAULT_METADATA["description"], program.description) - self.assertEqual( - self.DEFAULT_METADATA["max_execution_time"], program.max_execution_time - ) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["backend_requirements"], - program.backend_requirements, - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["parameters"], program.parameters().metadata - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["return_values"], program.return_values - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["interim_results"], program.interim_results - ) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py deleted file mode 100644 index eb9ed6badd..0000000000 --- a/test/ibm/runtime/test_runtime_integration.py +++ /dev/null @@ -1,779 +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. - -"""Tests for runtime service.""" - -import copy -import unittest -import os -import uuid -import time -import random -from contextlib import suppress -import tempfile - -from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit.test.reference_circuits import ReferenceCircuits - -from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError -from qiskit_ibm_runtime.runtime_program import RuntimeProgram -from qiskit_ibm_runtime.exceptions import ( - RuntimeDuplicateProgramError, - RuntimeProgramNotFound, - RuntimeJobFailureError, - RuntimeInvalidStateError, - RuntimeJobNotFound, -) - -from ...ibm_test_case import IBMTestCase -from ...decorators import requires_runtime_device -from ...proxy_server import MockProxyServer, use_proxies -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types - - -class TestRuntimeIntegration(IBMTestCase): - """Integration tests for runtime modules.""" - - RUNTIME_PROGRAM = """ -import random -import time -import warnings - -from qiskit import transpile -from qiskit.circuit.random import random_circuit - -def prepare_circuits(backend): - circuit = random_circuit(num_qubits=5, depth=4, measure=True, - seed=random.randint(0, 1000)) - return transpile(circuit, backend) - -def main(backend, user_messenger, **kwargs): - iterations = kwargs['iterations'] - sleep_per_iteration = kwargs.pop('sleep_per_iteration', 0) - interim_results = kwargs.pop('interim_results', {}) - final_result = kwargs.pop("final_result", {}) - for it in range(iterations): - time.sleep(sleep_per_iteration) - qc = prepare_circuits(backend) - user_messenger.publish({"iteration": it, "interim_results": interim_results}) - backend.run(qc).result() - - user_messenger.publish(final_result, final=True) - print("this is a stdout message") - warnings.warn("this is a stderr message") - """ - - RUNTIME_PROGRAM_METADATA = { - "max_execution_time": 600, - "description": "Qiskit test program", - } - PROGRAM_PREFIX = "qiskit-test" - - @classmethod - @requires_runtime_device - def setUpClass(cls, backend): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.backend = backend - cls.poll_time = 1 if backend.configuration().simulator else 5 - cls.service = backend.provider() - metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) - metadata["name"] = cls._get_program_name() - try: - cls.program_id = cls.service.upload_program( - data=cls.RUNTIME_PROGRAM, metadata=metadata - ) - except RuntimeDuplicateProgramError: - pass - except IBMNotAuthorizedError: - raise unittest.SkipTest("No upload access.") - - @classmethod - def tearDownClass(cls) -> None: - """Class level teardown.""" - super().tearDownClass() - with suppress(Exception): - cls.service.delete_program(cls.program_id) - - def setUp(self) -> None: - """Test level setup.""" - super().setUp() - self.to_delete = [] - self.to_cancel = [] - self.proxy_process = None - - def tearDown(self) -> None: - """Test level teardown.""" - super().tearDown() - # Delete programs - for prog in self.to_delete: - with suppress(Exception): - self.service.delete_program(prog) - - # Cancel and delete jobs. - for job in self.to_cancel: - with suppress(Exception): - job.cancel() - with suppress(Exception): - self.service.delete_job(job.job_id) - - def test_list_programs(self): - """Test listing programs.""" - programs = self.service.programs() - self.assertTrue(programs) - found = False - for prog in programs: - self._validate_program(prog) - if prog.program_id == self.program_id: - found = True - self.assertTrue(found, f"Program {self.program_id} not found!") - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - self._upload_program() - self._upload_program() - self._upload_program() - programs = self.service.programs(limit=3, refresh=True) - all_ids = [prog.program_id for prog in programs] - self.assertEqual(len(all_ids), 3) - programs = self.service.programs(limit=2, skip=1) - some_ids = [prog.program_id for prog in programs] - self.assertEqual(len(some_ids), 2) - self.assertNotIn(all_ids[0], some_ids) - self.assertIn(all_ids[1], some_ids) - self.assertIn(all_ids[2], some_ids) - - def test_list_program(self): - """Test listing a single program.""" - program = self.service.program(self.program_id) - self.assertEqual(self.program_id, program.program_id) - self._validate_program(program) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program = self.service.program(self.program_id) - self.assertEqual(self.RUNTIME_PROGRAM, program.data) - self._validate_program(program) - - def test_retrieve_unauthorized_program_data(self): - """Test retrieving program data when user is not the program author""" - program = self.service.program("sample-program") - self._validate_program(program) - with self.assertRaises(IBMNotAuthorizedError): - return program.data - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - program_id = self._upload_program(max_execution_time=max_execution_time) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - - def test_upload_program_file(self): - """Test uploading a program using a file.""" - temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(self.RUNTIME_PROGRAM) - temp_fp.close() - - program_id = self._upload_program(data=temp_fp.name) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - - @unittest.skipIf( - not os.environ.get("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""), - "Only runs on staging", - ) - def test_upload_public_program(self): - """Test uploading a public program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program( - max_execution_time=max_execution_time, is_public=is_public - ) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - @unittest.skipIf( - not os.environ.get("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""), - "Only runs on staging", - ) - def test_set_visibility(self): - """Test setting the visibility of a program.""" - program_id = self._upload_program() - # Get the initial visibility - prog: RuntimeProgram = self.service.program(program_id) - start_vis = prog.is_public - # Flip the original value - self.service.set_program_visibility(program_id, not start_vis) - # Get the new visibility - prog: RuntimeProgram = self.service.program(program_id, refresh=True) - end_vis = prog.is_public - # Verify changed - self.assertNotEqual(start_vis, end_vis) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.delete_program(program_id) - - def test_update_program_data(self): - """Test updating program data.""" - program_v1 = """ -def main(backend, user_messenger, **kwargs): - return "version 1" - """ - program_v2 = """ -def main(backend, user_messenger, **kwargs): - return "version 2" - """ - program_id = self._upload_program(data=program_v1) - self.assertEqual(program_v1, self.service.program(program_id).data) - self.service.update_program(program_id=program_id, data=program_v2) - self.assertEqual(program_v2, self.service.program(program_id).data) - - def test_update_program_metadata(self): - """Test updating program metadata.""" - program_id = self._upload_program() - original = self.service.program(program_id) - new_metadata = { - "name": self._get_program_name(), - "description": "test_update_program_metadata", - "max_execution_time": original.max_execution_time + 100, - "spec": { - "return_values": {"type": "object", "description": "Some return value"} - }, - } - self.service.update_program(program_id=program_id, metadata=new_metadata) - updated = self.service.program(program_id, refresh=True) - self.assertEqual(new_metadata["name"], updated.name) - self.assertEqual(new_metadata["description"], updated.description) - self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) - self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) - - def test_run_program(self): - """Test running a program.""" - job = self._run_program(final_result="foo") - result = job.result() - self.assertEqual(JobStatus.DONE, job.status()) - self.assertEqual("foo", result) - - def test_run_program_failed(self): - """Test a failed program execution.""" - options = {"backend_name": self.backend.name()} - job = self.service.run(program_id=self.program_id, inputs={}, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn( - API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError) as err_cm: - job.result() - self.assertIn("KeyError", str(err_cm.exception)) - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - max_execution_time = 60 - inputs = {"iterations": 1, "sleep_per_iteration": 60} - program_id = self._upload_program(max_execution_time=max_execution_time) - options = {"backend_name": self.backend.name()} - job = self.service.run(program_id=program_id, inputs=inputs, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn( - API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( - job.job_id, job_result_raw - ), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job_queued(self): - """Test retrieving a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, rjob.program_id) - - def test_retrieve_job_running(self): - """Test retrieving a running job.""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_job_done(self): - """Test retrieving a finished job.""" - job = self._run_program() - job.wait_for_final_state() - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_all_jobs(self): - """Test retrieving all jobs.""" - job = self._run_program() - rjobs = self.service.jobs() - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Job {job.job_id} not returned.") - - def test_retrieve_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - for _ in range(3): - jobs.append(self._run_program()) - - rjobs = self.service.jobs(limit=2) - self.assertEqual(len(rjobs), 2) - job_ids = {job.job_id for job in jobs} - rjob_ids = {rjob.job_id for rjob in rjobs} - self.assertTrue(rjob_ids.issubset(job_ids)) - - def test_retrieve_pending_jobs(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjobs = self.service.jobs(pending=True) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Pending job {job.job_id} not retrieved.") - - def test_retrieve_returned_jobs(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - job = self._run_program() - job.wait_for_final_state() - rjobs = self.service.jobs(pending=False) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") - - def test_retrieve_jobs_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.service.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - default_hgp = self.service._default_hgp - hub = default_hgp.credentials.hub - group = default_hgp.credentials.group - project = default_hgp.credentials.project - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.service.jobs( - program_id=program_id, hub=hub, group=group, project=project - ) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.service.jobs( - program_id=program_id, hub="test", group="test", project="test" - ) - self.assertFalse(rjobs) - - def test_cancel_job_queued(self): - """Test canceling a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_running(self): - """Test canceling a running job.""" - job = self._run_program(iterations=3) - self._wait_for_status(job, JobStatus.RUNNING) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_done(self): - """Test canceling a finished job.""" - job = self._run_program() - job.wait_for_final_state() - with self.assertRaises(RuntimeInvalidStateError): - job.cancel() - - def test_delete_job(self): - """Test deleting a job.""" - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING, JobStatus.DONE] - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program(iterations=2) - self._wait_for_status(job, status) - self.service.delete_job(job.job_id) - with self.assertRaises(RuntimeJobNotFound): - self.service.job(job.job_id) - - def test_interim_result_callback(self): - """Test interim result callback.""" - - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result["iteration"] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result["interim_results"] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "foo" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program( - iterations=iterations, interim_results=int_res, callback=result_callback - ) - job.wait_for_final_state() - self.assertEqual(iterations - 1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results(self): - """Test stream_results method.""" - - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result["iteration"] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result["interim_results"] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "bar" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program(iterations=iterations, interim_results=int_res) - job.stream_results(result_callback) - job.wait_for_final_state() - self.assertEqual(iterations - 1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results_done(self): - """Test streaming interim results after job is done.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal called_back - called_back = True - - called_back = False - job = self._run_program(interim_results="foobar") - job.wait_for_final_state() - job._status = JobStatus.RUNNING # Allow stream_results() - job.stream_results(result_callback) - time.sleep(2) - self.assertFalse(called_back) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_retrieve_interim_results(self): - """Test retrieving interim results with API endpoint""" - int_res = "foo" - job = self._run_program(interim_results=int_res) - job.wait_for_final_state() - interim_results = job.interim_results() - self.assertIn(int_res, interim_results[0]) - - def test_callback_error(self): - """Test error in callback method.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - if interim_result["iteration"] == 0: - raise ValueError("Kaboom!") - nonlocal final_it - final_it = interim_result["iteration"] - - final_it = 0 - iterations = 3 - with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as err_cm: - job = self._run_program( - iterations=iterations, interim_results="foo", callback=result_callback - ) - job.wait_for_final_state() - - self.assertIn("Kaboom", ", ".join(err_cm.output)) - self.assertEqual(iterations - 1, final_it) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_callback_cancel_job(self): - """Test canceling a running job while streaming results.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal final_it - final_it = interim_result["iteration"] - - final_it = 0 - iterations = 3 - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] - - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program( - iterations=iterations, - interim_results="foo", - callback=result_callback, - ) - self._wait_for_status(job, status) - job.cancel() - time.sleep(3) # Wait for cleanup - self.assertIsNotNone(job._ws_client._server_close_code) - self.assertLess(final_it, iterations) - - def test_final_result(self): - """Test getting final result.""" - final_result = get_complex_types() - job = self._run_program(final_result=final_result) - result = job.result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, result) - - rresults = self.service.job(job.job_id).result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, rresults) - - def test_job_status(self): - """Test job status.""" - job = self._run_program(iterations=1) - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - interim_results = get_complex_types() - inputs = {"iterations": 1, "interim_results": interim_results} - options = {"backend_name": self.backend.name()} - job = self.service.run( - program_id=self.program_id, inputs=inputs, options=options - ) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - self.assertEqual(inputs, job.inputs) - rjob = self.service.job(job.job_id) - rinterim_results = rjob.inputs["interim_results"] - self._assert_complex_types_equal(interim_results, rinterim_results) - - def test_job_backend(self): - """Test job backend.""" - job = self._run_program() - self.assertEqual(self.backend, job.backend) - - def test_job_program_id(self): - """Test job program ID.""" - job = self._run_program() - self.assertEqual(self.program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_logout(self): - """Test logout.""" - self.service.logout() - # Make sure we can still do things. - self._upload_program() - _ = self._run_program() - - def test_run_circuit(self): - """Test run_circuits""" - job = self.service.run_circuits( - ReferenceCircuits.bell(), backend_name=self.backend.name(), shots=100 - ) - counts = job.result().get_counts() - self.assertEqual(100, sum(counts.values())) - - def test_job_creation_date(self): - """Test job creation date.""" - job = self._run_program(iterations=1) - self.assertTrue(job.creation_date) - rjob = self.service.job(job.job_id) - self.assertTrue(rjob.creation_date) - rjobs = self.service.jobs(limit=2) - for rjob in rjobs: - self.assertTrue(rjob.creation_date) - - def test_websocket_proxy(self): - """Test connecting to websocket via proxy.""" - - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - MockProxyServer(self, self.log).start() - callback_called = False - - with use_proxies(self.service._default_hgp, MockProxyServer.VALID_PROXIES): - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - - self.assertTrue(callback_called) - - def test_websocket_proxy_invalid_port(self): - """Test connecting to websocket via invalid proxy port.""" - - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - callback_called = False - invalid_proxy = { - "https": "http://{}:{}".format( - MockProxyServer.PROXY_IP_ADDRESS, MockProxyServer.INVALID_PROXY_PORT - ) - } - with use_proxies(self.service._default_hgp, invalid_proxy): - with self.assertLogs("qiskit_ibm_runtime", "WARNING") as log_cm: - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - self.assertIn("WebsocketError", ",".join(log_cm.output)) - self.assertFalse(callback_called) - - def test_job_logs(self): - """Test job logs.""" - job = self._run_program(final_result="foo") - with self.assertLogs("qiskit_ibm_runtime", "WARN"): - job.logs() - job.wait_for_final_state() - job_logs = job.logs() - self.assertIn("this is a stdout message", job_logs) - self.assertIn("this is a stderr message", job_logs) - - def _validate_program(self, program): - """Validate a program.""" - self.assertTrue(program) - self.assertTrue(program.name) - self.assertTrue(program.program_id) - self.assertTrue(program.description) - self.assertTrue(program.max_execution_time) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - - def _upload_program( - self, name=None, max_execution_time=300, data=None, is_public: bool = False - ): - """Upload a new program.""" - name = name or self._get_program_name() - data = data or self.RUNTIME_PROGRAM - metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) - metadata["name"] = name - metadata["max_execution_time"] = max_execution_time - metadata["is_public"] = is_public - program_id = self.service.upload_program(data=data, metadata=metadata) - self.to_delete.append(program_id) - return program_id - - @classmethod - def _get_program_name(cls): - """Return a unique program name.""" - return cls.PROGRAM_PREFIX + "_" + uuid.uuid4().hex - - def _assert_complex_types_equal(self, expected, received): - """Verify the received data in complex types is expected.""" - if "serializable_class" in received: - received["serializable_class"] = SerializableClass.from_json( - received["serializable_class"] - ) - self.assertEqual(expected, received) - - def _run_program( - self, - program_id=None, - iterations=1, - interim_results=None, - final_result=None, - callback=None, - ): - """Run a program.""" - inputs = { - "iterations": iterations, - "interim_results": interim_results or {}, - "final_result": final_result or {}, - } - pid = program_id or self.program_id - options = {"backend_name": self.backend.name()} - job = self.service.run( - program_id=pid, inputs=inputs, options=options, callback=callback - ) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - return job - - def _wait_for_status(self, job, status): - """Wait for job to reach a certain status.""" - wait_time = 1 if status == JobStatus.QUEUED else self.poll_time - while job.status() not in JOB_FINAL_STATES + (status,): - time.sleep(wait_time) - if job.status() != status: - self.skipTest(f"Job {job.job_id} unable to reach status {status}.") diff --git a/test/ibm/test_basic_server_paths.py b/test/ibm/test_basic_server_paths.py deleted file mode 100644 index 0f49e30803..0000000000 --- a/test/ibm/test_basic_server_paths.py +++ /dev/null @@ -1,55 +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. - -"""Tests that hit all the basic server endpoints using both a public and premium provider.""" - -from datetime import datetime, timedelta - -from ..decorators import requires_providers -from ..ibm_test_case import IBMTestCase - - -class TestBasicServerPaths(IBMTestCase): - """Test the basic server endpoints using both a public and premium provider.""" - - @classmethod - @requires_providers - def setUpClass(cls, service, hgps): - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service # Dict[str, IBMRuntimeService] - cls.hgps = hgps - cls.last_week = datetime.now() - timedelta(days=7) - - def test_device_properties_and_defaults(self): - """Test the properties and defaults for an open pulse device.""" - for desc, hgp in self.hgps.items(): - pulse_backends = self.service.backends( - open_pulse=True, operational=True, **hgp - ) - if not pulse_backends: - raise self.skipTest( - "Skipping pulse test since no pulse backend " - 'found for "{}"'.format(desc) - ) - - pulse_backend = pulse_backends[0] - with self.subTest(desc=desc, backend=pulse_backend): - self.assertIsNotNone(pulse_backend.properties()) - self.assertIsNotNone(pulse_backend.defaults()) - - def test_device_status(self): - """Test device status.""" - for desc, hgp in self.hgps.items(): - backend = self.service.backends(simulator=False, operational=True, **hgp)[0] - with self.subTest(desc=desc, backend=backend): - self.assertTrue(backend.status()) diff --git a/test/ibm/test_filter_backends.py b/test/ibm/test_filter_backends.py deleted file mode 100644 index 0443fe8f5e..0000000000 --- a/test/ibm/test_filter_backends.py +++ /dev/null @@ -1,197 +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. - -"""Backends Filtering Test.""" - -from unittest import mock -from datetime import datetime - -from dateutil import tz -from qiskit_ibm_runtime import least_busy -from qiskit_ibm_runtime import IBMError - -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_provider, requires_device - - -class TestBackendFilters(IBMTestCase): - """Qiskit Backend Filtering Tests.""" - - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - - @requires_device - def test_filter_config_properties(self, backend): - """Test filtering by configuration properties.""" - # Use the default backend as a reference for the filter. - n_qubits = backend.configuration().n_qubits - - filtered_backends = self.service.backends( - n_qubits=n_qubits, - local=False, - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for filtered_backend in filtered_backends[:5]: - with self.subTest(filtered_backend=filtered_backend): - self.assertEqual(n_qubits, filtered_backend.configuration().n_qubits) - self.assertFalse(filtered_backend.configuration().local) - - def test_filter_status_dict(self): - """Test filtering by dictionary of mixed status/configuration properties.""" - filtered_backends = self.service.backends( - operational=True, # from status - local=False, - simulator=True, # from configuration - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertTrue(backend.status().operational) - self.assertFalse(backend.configuration().local) - self.assertTrue(backend.configuration().simulator) - - def test_filter_config_callable(self): - """Test filtering by lambda function on configuration properties.""" - filtered_backends = self.service.backends( - filters=lambda x: ( - not x.configuration().simulator and x.configuration().n_qubits >= 5 - ), - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertFalse(backend.configuration().simulator) - self.assertGreaterEqual(backend.configuration().n_qubits, 5) - - def test_filter_least_busy(self): - """Test filtering by least busy function.""" - backends = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - least_busy_backend = least_busy(backends) - self.assertTrue(least_busy_backend) - - def test_filter_least_busy_reservation(self): - """Test filtering by least busy function, with reservations.""" - backend = reservations = None - for backend in self.service.backends( - simulator=False, - operational=True, - status_msg="active", - hub=self.hub, - group=self.group, - project=self.project, - ): - reservations = backend.reservations() - if reservations: - break - - if not reservations: - self.skipTest("Test case requires reservations.") - - reserv = reservations[0] - now = datetime.now(tz=tz.tzlocal()) - window = 60 - if reserv.start_datetime > now: - window = (reserv.start_datetime - now).seconds * 60 - self.assertRaises(IBMError, least_busy, [backend], window) - - self.assertEqual(least_busy([backend], None), backend) - - backs = [backend] - for back in self.service.backends( - simulator=False, - operational=True, - status_msg="active", - hub=self.hub, - group=self.group, - project=self.project, - ): - if back.name() != backend.name(): - backs.append(back) - break - self.assertTrue(least_busy(backs, window)) - - def test_filter_least_busy_paused(self): - """Test filtering by least busy function, with paused backend.""" - backends = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - if len(backends) < 2: - self.skipTest("Test needs at least 2 backends.") - paused_backend = backends[0] - paused_status = paused_backend.status() - paused_status.status_msg = "internal" - paused_status.pending_jobs = 0 - paused_backend.status = mock.MagicMock(return_value=paused_status) - - least_busy_backend = least_busy(backends) - self.assertTrue(least_busy_backend) - self.assertNotEqual(least_busy_backend.name(), paused_backend.name()) - self.assertEqual(least_busy_backend.status().status_msg, "active") - - def test_filter_min_num_qubits(self): - """Test filtering by minimum number of qubits.""" - filtered_backends = self.service.backends( - min_num_qubits=5, - simulator=False, - filters=lambda b: b.configuration().quantum_volume >= 10, - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertGreaterEqual(backend.configuration().n_qubits, 5) - self.assertTrue(backend.configuration().quantum_volume, 10) - - def test_filter_input_allowed(self): - """Test filtering by input allowed""" - subtests = ("job", ["job"], ["job", "runtime"]) - - for input_type in subtests: - with self.subTest(input_type=input_type): - filtered = self.service.backends( - input_allowed=input_type, - hub=self.hub, - group=self.group, - project=self.project, - ) - self.assertTrue(filtered) - if not isinstance(input_type, list): - input_type = [input_type] - for backend in filtered[:5]: - self.assertTrue( - set(input_type) <= set(backend.configuration().input_allowed) - ) diff --git a/test/ibm/test_ibm_backend.py b/test/ibm/test_ibm_backend.py deleted file mode 100644 index 9a98545f1e..0000000000 --- a/test/ibm/test_ibm_backend.py +++ /dev/null @@ -1,129 +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. - -"""IBMBackend Test.""" - -from datetime import timedelta, datetime - -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_device, requires_provider - - -class TestIBMBackend(IBMTestCase): - """Test ibm_backend module.""" - - @classmethod - @requires_device - def setUpClass(cls, backend): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.backend = backend - - def test_backend_status(self): - """Check the status of a real chip.""" - self.assertTrue(self.backend.status().operational) - - def test_backend_properties(self): - """Check the properties of calibration of a real chip.""" - self.assertIsNotNone(self.backend.properties()) - - def test_backend_pulse_defaults(self): - """Check the backend pulse defaults of each backend.""" - service = self.backend.provider() - for backend in service.backends(): - with self.subTest(backend_name=backend.name()): - defaults = backend.defaults() - if backend.configuration().open_pulse: - self.assertIsNotNone(defaults) - - def test_backend_reservations(self): - """Test backend reservations.""" - service = self.backend.provider() - backend = reservations = None - for backend in service.backends( - simulator=False, - operational=True, - hub=self.backend.hub, - group=self.backend.group, - project=self.backend.project, - ): - reservations = backend.reservations() - if reservations: - break - - if not reservations: - self.skipTest("Test case requires reservations.") - - reserv = reservations[0] - self.assertGreater(reserv.duration, 0) - self.assertTrue(reserv.mode) - before_start = reserv.start_datetime - timedelta(seconds=30) - after_start = reserv.start_datetime + timedelta(seconds=30) - before_end = reserv.end_datetime - timedelta(seconds=30) - after_end = reserv.end_datetime + timedelta(seconds=30) - - # Each tuple contains the start datetime, end datetime, whether a - # reservation should be found, and the description. - sub_tests = [ - (before_start, after_end, True, "before start, after end"), - (before_start, before_end, True, "before start, before end"), - (after_start, before_end, True, "after start, before end"), - (before_start, None, True, "before start, None"), - (None, after_end, True, "None, after end"), - (before_start, before_start, False, "before start, before start"), - (after_end, after_end, False, "after end, after end"), - ] - - for start_dt, end_dt, should_find, name in sub_tests: - with self.subTest(name=name): - f_reservs = backend.reservations( - start_datetime=start_dt, end_datetime=end_dt - ) - found = False - for f_reserv in f_reservs: - if f_reserv == reserv: - found = True - break - self.assertEqual( - found, - should_find, - "Reservation {} found={}, used start datetime {}, end datetime {}".format( - reserv, found, start_dt, end_dt - ), - ) - - -class TestIBMBackendService(IBMTestCase): - """Test ibm_backend_service module.""" - - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - cls.last_week = datetime.now() - timedelta(days=7) - - def test_my_reservations(self): - """Test my_reservations method""" - reservations = self.service.my_reservations() - for reserv in reservations: - for attr in reserv.__dict__: - self.assertIsNotNone( - getattr(reserv, attr), - "Reservation {} is missing attribute {}".format(reserv, attr), - ) diff --git a/test/ibm/test_ibm_provider.py b/test/ibm/test_ibm_provider.py deleted file mode 100644 index 4ecf25c45b..0000000000 --- a/test/ibm/test_ibm_provider.py +++ /dev/null @@ -1,251 +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. - -"""Tests for the IBMRuntimeService class.""" - -from datetime import datetime -from unittest import mock - -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.models.backendproperties import BackendProperties - -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime import hub_group_project -from qiskit_ibm_runtime.api.clients import AccountClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError -from qiskit_ibm_runtime.exceptions import ( - IBMProviderCredentialsInvalidUrl, -) -from qiskit_ibm_runtime.ibm_backend import IBMSimulator, IBMBackend -from ..decorators import requires_qe_access, requires_provider -from ..ibm_test_case import IBMTestCase - -API_URL = "https://api.quantum-computing.ibm.com/api" -AUTH_URL = "https://auth.quantum-computing.ibm.com/api" - - -class TestIBMProviderEnableAccount(IBMTestCase): - """Tests for IBMRuntimeService.""" - - # Enable Account Tests - - @requires_qe_access - def test_provider_init_token(self, qe_token, qe_url): - """Test initializing IBMRuntimeService with only API token.""" - # pylint: disable=unused-argument - service = IBMRuntimeService(auth="legacy", token=qe_token) - self.assertIsInstance(service, IBMRuntimeService) - self.assertEqual(service._default_hgp.credentials.token, qe_token) - - @requires_qe_access - def test_pass_unreachable_proxy(self, qe_token, qe_url): - """Test using an unreachable proxy while enabling an account.""" - proxies = { - "urls": { - "http": "http://user:password@127.0.0.1:5678", - "https": "https://user:password@127.0.0.1:5678", - } - } - with self.assertRaises(RequestsApiError) as context_manager: - IBMRuntimeService( - auth="legacy", token=qe_token, url=qe_url, proxies=proxies - ) - self.assertIn("ProxyError", str(context_manager.exception)) - - def test_provider_init_non_auth_url(self): - """Test initializing IBMRuntimeService with a non-auth URL.""" - qe_token = "invalid" - qe_url = API_URL - - with self.assertRaises(IBMProviderCredentialsInvalidUrl) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - self.assertIn("authentication URL", str(context_manager.exception)) - - def test_provider_init_non_auth_url_with_hub(self): - """Test initializing IBMRuntimeService with a non-auth URL containing h/g/p.""" - qe_token = "invalid" - qe_url = API_URL + "/Hubs/X/Groups/Y/Projects/Z" - - with self.assertRaises(IBMProviderCredentialsInvalidUrl) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - self.assertIn("authentication URL", str(context_manager.exception)) - - @requires_qe_access - def test_discover_backend_failed(self, qe_token, qe_url): - """Test discovering backends failed.""" - with mock.patch.object( - AccountClient, - "list_backends", - return_value=[{"backend_name": "bad_backend"}], - ): - with self.assertLogs( - hub_group_project.logger, level="WARNING" - ) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - self.assertIn("bad_backend", str(context_manager.output)) - - -class TestIBMProviderHubGroupProject(IBMTestCase): - """Tests for IBMRuntimeService HubGroupProject related methods.""" - - @requires_qe_access - def _initialize_provider(self, qe_token=None, qe_url=None): - """Initialize and return provider.""" - return IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - def setUp(self): - """Initial test setup.""" - super().setUp() - - self.service = self._initialize_provider() - self.credentials = self.service._default_hgp.credentials - - def test_get_hgp(self): - """Test get single hgp.""" - hgp = self.service._get_hgp( - hub=self.credentials.hub, - group=self.credentials.group, - project=self.credentials.project, - ) - self.assertEqual(self.service._default_hgp, hgp) - - def test_get_hgps_with_filter(self): - """Test get hgps with a filter.""" - hgp = self.service._get_hgps( - hub=self.credentials.hub, - group=self.credentials.group, - project=self.credentials.project, - )[0] - self.assertEqual(self.service._default_hgp, hgp) - - def test_get_hgps_no_filter(self): - """Test get hgps without a filter.""" - hgps = self.service._get_hgps() - self.assertIn(self.service._default_hgp, hgps) - - -class TestIBMProviderServices(IBMTestCase): - """Tests for services provided by the IBMRuntimeService class.""" - - @requires_provider - def setUp(self, service, hub, group, project): - """Initial test setup.""" - # pylint: disable=arguments-differ - super().setUp() - self.service = service - self.hub = hub - self.group = group - self.project = project - qr = QuantumRegister(1) - cr = ClassicalRegister(1) - self.qc1 = QuantumCircuit(qr, cr, name="circuit0") - self.qc1.h(qr[0]) - self.qc1.measure(qr, cr) - - def test_remote_backends_exist_real_device(self): - """Test if there are remote backends that are devices.""" - remotes = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - self.assertTrue(remotes) - - def test_remote_backends_exist_simulator(self): - """Test if there are remote backends that are simulators.""" - remotes = self.service.backends( - simulator=True, hub=self.hub, group=self.group, project=self.project - ) - self.assertTrue(remotes) - - def test_remote_backends_instantiate_simulators(self): - """Test if remote backends that are simulators are an ``IBMSimulator`` instance.""" - remotes = self.service.backends( - simulator=True, hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - with self.subTest(backend=backend): - self.assertIsInstance(backend, IBMSimulator) - - def test_remote_backend_status(self): - """Test backend_status.""" - remotes = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - _ = backend.status() - - def test_remote_backend_configuration(self): - """Test backend configuration.""" - remotes = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - _ = backend.configuration() - - def test_remote_backend_properties(self): - """Test backend properties.""" - remotes = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - properties = backend.properties() - if backend.configuration().simulator: - self.assertEqual(properties, None) - - def test_aliases(self): - """Test that display names of devices map the regular names.""" - aliased_names = self.service._aliased_backend_names() - - for display_name, backend_name in aliased_names.items(): - with self.subTest(display_name=display_name, backend_name=backend_name): - try: - backend_by_name = self.service.get_backend( - backend_name, - hub=self.hub, - group=self.group, - project=self.project, - ) - except QiskitBackendNotFoundError: - # The real name of the backend might not exist - pass - else: - backend_by_display_name = self.service.get_backend(display_name) - self.assertEqual(backend_by_name, backend_by_display_name) - self.assertEqual(backend_by_display_name.name(), backend_name) - - def test_remote_backend_properties_filter_date(self): - """Test backend properties filtered by date.""" - backends = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - - datetime_filter = datetime(2019, 2, 1).replace(tzinfo=None) - for backend in backends: - with self.subTest(backend=backend): - properties = backend.properties(datetime=datetime_filter) - if isinstance(properties, BackendProperties): - last_update_date = properties.last_update_date.replace(tzinfo=None) - self.assertLessEqual(last_update_date, datetime_filter) - else: - self.assertEqual(properties, None) - - def test_provider_backends(self): - """Test provider_backends have correct attributes.""" - provider_backends = { - back - for back in dir(self.service) - if isinstance(getattr(self.service, back), IBMBackend) - } - backends = {back.name().lower() for back in self.service._backends.values()} - self.assertEqual(provider_backends, backends) diff --git a/test/ibm/test_utils.py b/test/ibm/test_utils.py deleted file mode 100644 index b83c820698..0000000000 --- a/test/ibm/test_utils.py +++ /dev/null @@ -1,47 +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. - -"""Tests for utils.""" - -from qiskit_ibm_runtime.utils import is_crn, crn_to_api_host -from qiskit_ibm_runtime import CannotMapCrnToApiHostError - -from ..ibm_test_case import IBMTestCase - - -CRN_HOST_TUPLES = [ - [ - "crn:v1:bluemix:public:quantum-computing:us-east:a/...::", - "https://us-east.quantum-computing.cloud.ibm.com", - ] -] - - -class TestUtils(IBMTestCase): - """Tests for utility functions.""" - - def test_is_crn(self): - """Tests detection of CRN values.""" - self.assertFalse(is_crn("abc")) - for entry in CRN_HOST_TUPLES: - self.assertTrue(is_crn(entry[0])) - - def test_map_to_api_host(self): - """Tests mapping of CRN values to API hosts.""" - for entry in CRN_HOST_TUPLES: - print(entry) - print(crn_to_api_host(entry[0])) - self.assertEqual(crn_to_api_host(entry[0]), entry[1]) - - self.assertRaises( - CannotMapCrnToApiHostError, lambda: crn_to_api_host("invalid") - ) diff --git a/test/ibm_test_case.py b/test/ibm_test_case.py index a0af410f91..763e999e7b 100644 --- a/test/ibm_test_case.py +++ b/test/ibm_test_case.py @@ -13,17 +13,22 @@ """Custom TestCase for IBM Provider.""" import os +import copy import logging import inspect - -from qiskit.test.base import BaseQiskitTestCase +import unittest +from contextlib import suppress +from collections import defaultdict from qiskit_ibm_runtime import QISKIT_IBM_RUNTIME_LOGGER_NAME +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError -from .utils import setup_test_logging +from .utils.utils import setup_test_logging +from .utils.decorators import requires_cloud_legacy_services +from .utils.templates import RUNTIME_PROGRAM, RUNTIME_PROGRAM_METADATA, PROGRAM_PREFIX -class IBMTestCase(BaseQiskitTestCase): +class IBMTestCase(unittest.TestCase): """Custom TestCase for use with the Qiskit IBM Runtime.""" @classmethod @@ -56,3 +61,140 @@ def _set_logging_level(cls, logger: logging.Logger) -> None: ): logger.addHandler(logging.StreamHandler()) logger.propagate = False + + +class IBMIntegrationTestCase(IBMTestCase): + """Custom integration test case for use with the Qiskit IBM Runtime.""" + + @classmethod + @requires_cloud_legacy_services + def setUpClass(cls, services): + """Initial class level setup.""" + # pylint: disable=arguments-differ + super().setUpClass() + cls.services = services + + def setUp(self) -> None: + """Test level setup.""" + super().setUp() + self.to_delete = defaultdict(list) + self.to_cancel = defaultdict(list) + + def tearDown(self) -> None: + """Test level teardown.""" + super().tearDown() + # Delete programs + for service in self.services: + for prog in self.to_delete[service.auth]: + with suppress(Exception): + service.delete_program(prog) + + # Cancel and delete jobs. + for service in self.services: + for job in self.to_cancel[service.auth]: + with suppress(Exception): + job.cancel() + with suppress(Exception): + service.delete_job(job.job_id) + + def _upload_program( + self, + service, + name=None, + max_execution_time=300, + data=None, + is_public: bool = False, + ): + """Upload a new program.""" + name = name or PROGRAM_PREFIX + data = data or RUNTIME_PROGRAM + metadata = copy.deepcopy(RUNTIME_PROGRAM_METADATA) + metadata["name"] = name + metadata["max_execution_time"] = max_execution_time + metadata["is_public"] = is_public + program_id = service.upload_program(data=data, metadata=metadata) + self.to_delete[service.auth].append(program_id) + return program_id + + +class IBMIntegrationJobTestCase(IBMIntegrationTestCase): + """Custom integration test case for job-related tests.""" + + @classmethod + def setUpClass(cls): + """Initial class level setup.""" + # pylint: disable=arguments-differ + # pylint: disable=no-value-for-parameter + super().setUpClass() + cls._create_default_program() + cls._find_sim_backends() + + @classmethod + def tearDownClass(cls) -> None: + """Class level teardown.""" + super().tearDownClass() + # Delete default program. + with suppress(Exception): + for service in cls.services: + service.delete_program(cls.program_ids[service.auth]) + cls.log.debug( + "Deleted %s program %s", service.auth, cls.program_ids[service.auth] + ) + + @classmethod + def _create_default_program(cls): + """Create a default program.""" + metadata = copy.deepcopy(RUNTIME_PROGRAM_METADATA) + metadata["name"] = PROGRAM_PREFIX + cls.program_ids = {} + cls.sim_backends = {} + for service in cls.services: + try: + prog_id = service.upload_program( + data=RUNTIME_PROGRAM, metadata=metadata + ) + cls.log.debug("Uploaded %s program %s", service.auth, prog_id) + cls.program_ids[service.auth] = prog_id + except IBMNotAuthorizedError: + raise unittest.SkipTest("No upload access.") + + @classmethod + def _find_sim_backends(cls): + """Find a simulator backend for each service.""" + for service in cls.services: + cls.sim_backends[service.auth] = service.backends(simulator=True)[0].name() + + def _run_program( + self, + service, + program_id=None, + iterations=1, + inputs=None, + interim_results=None, + final_result=None, + callback=None, + backend=None, + log_level=None, + ): + """Run a program.""" + self.log.debug("Running program on %s", service.auth) + inputs = ( + inputs + if inputs is not None + else { + "iterations": iterations, + "interim_results": interim_results or {}, + "final_result": final_result or {}, + } + ) + pid = program_id or self.program_ids[service.auth] + backend_name = ( + backend if backend is not None else self.sim_backends[service.auth] + ) + options = {"backend_name": backend_name, "log_level": log_level} + job = service.run( + program_id=pid, inputs=inputs, options=options, callback=callback + ) + self.log.info("Runtime job %s submitted.", job.job_id) + self.to_cancel[service.auth].append(job) + return job diff --git a/test/jobtestcase.py b/test/jobtestcase.py deleted file mode 100644 index 7b6d184eb3..0000000000 --- a/test/jobtestcase.py +++ /dev/null @@ -1,37 +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. - -"""Custom TestCase for Jobs.""" - -import time - -from qiskit.providers import JobStatus - -from .ibm_test_case import IBMTestCase - - -class JobTestCase(IBMTestCase): - """Include common functionality when testing jobs.""" - - def wait_for_initialization(self, job, timeout=1): - """Waits until job progresses from `INITIALIZING` to other status.""" - waited = 0 - wait = 0.1 - while job.status() is JobStatus.INITIALIZING: - time.sleep(wait) - waited += wait - if waited > timeout: - self.fail( - msg="The JOB is still initializing after timeout ({}s)".format( - timeout - ) - ) diff --git a/test/ibm/runtime/__init__.py b/test/mock/__init__.py similarity index 94% rename from test/ibm/runtime/__init__.py rename to test/mock/__init__.py index 99de8ba789..86c34a4a3a 100644 --- a/test/ibm/runtime/__init__.py +++ b/test/mock/__init__.py @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Runtime related tests.""" +"""Test mock classes.""" diff --git a/test/mock/fake_account_client.py b/test/mock/fake_account_client.py new file mode 100644 index 0000000000..4fecc0bbf0 --- /dev/null +++ b/test/mock/fake_account_client.py @@ -0,0 +1,112 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# 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.""" + +from typing import List, Dict, Any, Optional +from datetime import datetime as python_datetime + +from qiskit.test.mock.backends import FakeLima + + +class FakeApiBackend: + """Fake backend.""" + + def __init__(self, config_update=None, status_update=None): + fake_backend = FakeLima() + self.properties = fake_backend.properties().to_dict() + self.defaults = fake_backend.defaults().to_dict() + + self.configuration = fake_backend.configuration().to_dict() + self.configuration["online_date"] = python_datetime.now().isoformat() + if config_update: + self.configuration.update(**config_update) + self.name = self.configuration["backend_name"] + + self.status = fake_backend.status().to_dict() + if status_update: + self.status.update(**status_update) + + +class BaseFakeAccountClient: + """Base class for faking the AccountClient.""" + + def __init__( + self, + hgp: Optional[str] = None, + num_backends: int = 2, + specs: List[Dict] = None, + ): + """Initialize a fake account client. + + Args: + num_backends: Number of backends. Ignored if ``specs`` is specified. + specs: Backend specs. This is a dictionary of overwritten backend + configuration / status. For example:: + + specs = [ {"configuration": {"backend_name": "backend1"}, + "status": {"operational": False}} + ] + """ + self._hgp = hgp + self._fake_backend = FakeLima() + self._backends = [] + if not specs: + specs = [{}] * num_backends + + for idx, backend_spec in enumerate(specs): + config = backend_spec.get("configuration", {}) + status = backend_spec.get("status", {}) + if "backend_name" not in config: + config["backend_name"] = f"backend{idx}" + self._backends.append(FakeApiBackend(config, status)) + + def list_backends(self, *args, **kwargs) -> List[Dict[str, Any]]: + """Return backends available for this provider.""" + # pylint: disable=unused-argument + return [back.configuration.copy() for back in self._backends] + + def backend_status(self, backend_name: str) -> Dict[str, Any]: + """Return the status of the backend.""" + for back in self._backends: + if back.name == backend_name: + return back.status.copy() + raise ValueError(f"Backend {backend_name} not found") + + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: + """Return the properties of the backend.""" + # pylint: disable=unused-argument + for back in self._backends: + if back.name == backend_name: + return back.properties.copy() + raise ValueError(f"Backend {backend_name} not found") + + def backend_pulse_defaults(self, backend_name: str) -> Dict: + """Return the pulse defaults of the backend.""" + for back in self._backends: + if back.name == backend_name: + return back.defaults.copy() + raise ValueError(f"Backend {backend_name} not found") + + # Test-only methods. + + @property + def backend_names(self): + """Return names of the backends.""" + return [back.name for back in self._backends] + + @property + def hgp(self): + """Return hub/group/project.""" + return self._hgp diff --git a/test/mock/fake_legacy_auth_client.py b/test/mock/fake_legacy_auth_client.py new file mode 100644 index 0000000000..417636053a --- /dev/null +++ b/test/mock/fake_legacy_auth_client.py @@ -0,0 +1,75 @@ +# 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 legacy AuthClient.""" + +from typing import Dict, List, Union, Optional + + +class BaseFakeAuthClient: + """Base class for faking the runtime client.""" + + def __init__(self, *args, **kwargs): + """Initialize a auth runtime client.""" + pass + + 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. + """ + return { + "http": "http://127.0.0.1", + "ws": "ws://127.0.0.1", + "services": {"runtime": "http://127.0.0.1"}, + } + + def user_hubs(self) -> List[Dict[str, str]]: + """Retrieve the hub/group/project sets available to the user.""" + + hubs = [] + for idx in range(2): + hubs.append( + {"hub": f"hub{idx}", "group": f"group{idx}", "project": f"project{idx}"} + ) + return hubs + + def api_version(self) -> Dict[str, Union[str, bool]]: + """Return the version of the API. + + Returns: + API version. + """ + return {"new_api": True, "api-auth": "0.1"} + + def current_access_token(self) -> Optional[str]: + """Return the current access token. + + Returns: + The access token in use. + """ + return "123" + + def current_service_urls(self) -> Dict[str, str]: + """Return the current service URLs. + + Returns: + A dict with the base URLs for the services, in the same + format as :meth:`user_urls()`. + """ + return {"runtime": "http://127.0.0.1"} diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/mock/fake_runtime_client.py similarity index 79% rename from test/ibm/runtime/fake_runtime_client.py rename to test/mock/fake_runtime_client.py index fa16c46dbc..a80f875af9 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/mock/fake_runtime_client.py @@ -18,10 +18,25 @@ import base64 from typing import Optional, Dict from concurrent.futures import ThreadPoolExecutor +from functools import wraps -from qiskit_ibm_runtime.credentials import Credentials from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.utils import RuntimeEncoder +from qiskit_ibm_runtime.utils.hgp import from_instance_format + +from .fake_account_client import BaseFakeAccountClient + + +def cloud_only(func): + """Decorator that runs a test using both legacy and cloud services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + if self._auth_type != "cloud": + raise ValueError(f"Method {func} called by a legacy client!") + return func(self, *args, **kwargs) + + return _wrapper class BaseFakeProgram: @@ -98,6 +113,7 @@ def __init__( final_status, params, image, + log_level=None, ): """Initialize a fake job.""" self._job_id = job_id @@ -110,6 +126,7 @@ def __init__( self._params = params self._image = image self._interim_results = json.dumps("foo") + self.log_level = log_level if final_status is None: self._future = self._executor.submit(self._auto_progress) self._result = None @@ -230,30 +247,19 @@ def _auto_progress(self): class BaseFakeRuntimeClient: """Base class for faking the runtime client.""" - def __init__( - self, - job_classes=None, - final_status=None, - job_kwargs=None, - hub=None, - group=None, - project=None, - ): + def __init__(self, *args, **kwargs): """Initialize a fake runtime client.""" + # pylint: disable=unused-argument + test_options = kwargs.pop("test_options", {}) self._programs = {} self._jobs = {} - self._job_classes = job_classes or [] - self._final_status = final_status - self._job_kwargs = job_kwargs or {} - self._hub = hub - self._group = group - self._project = project - - def set_hgp(self, hub, group, project): - """Set hub, group and project""" - self._hub = hub - self._group = group - self._project = project + self._job_classes = test_options.get("job_classes", []) + self._final_status = test_options.get("final_status") + self._job_kwargs = test_options.get("job_kwargs", {}) + self._backend_client = test_options.get( + "backend_client", BaseFakeAccountClient() + ) + self._auth_type = test_options.get("auth_type", "legacy") def set_job_classes(self, classes): """Set job classes to use.""" @@ -313,9 +319,7 @@ def program_update( spec: Optional[Dict] = None, ) -> None: """Update a program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) - program = self._programs[program_id] + program = self._get_program(program_id) program._data = program_data or program._data program._name = name or program._name program._description = description or program._description @@ -339,21 +343,28 @@ def program_get(self, program_id: str): def program_run( self, program_id: str, - credentials: Credentials, - backend_name: str, - params: str, - image: Optional[str] = "", + backend_name: Optional[str], + params: Dict, + image: str, + hgp: Optional[str], + log_level: Optional[str], ): """Run the specified program.""" + _ = self._get_program(program_id) job_id = uuid.uuid4().hex job_cls = ( self._job_classes.pop(0) if len(self._job_classes) > 0 else BaseFakeRuntimeJob ) - hub = self._hub or credentials.hub - group = self._group or credentials.group - project = self._project or credentials.project + if hgp: + hub, group, project = from_instance_format(hgp) + else: + hub = group = project = None + + if backend_name is None: + backend_name = self.list_backends()[0] + job = job_cls( job_id=job_id, program_id=program_id, @@ -364,15 +375,15 @@ def program_run( params=params, final_status=self._final_status, image=image, - **self._job_kwargs + log_level=log_level, + **self._job_kwargs, ) self._jobs[job_id] = job - return {"id": job_id} + return {"id": job_id, "backend": backend_name} def program_delete(self, program_id: str) -> None: """Delete the specified program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) + self._get_program(program_id) del self._programs[program_id] def job_get(self, job_id): @@ -421,7 +432,8 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: public: If ``True``, make the program visible to all. If ``False``, make the program visible to just your account. """ - self._programs[program_id]._is_public = public + program = self._get_program(program_id) + program._is_public = public def job_results(self, job_id): """Get the results of a program job.""" @@ -440,8 +452,51 @@ def job_delete(self, job_id): self._get_job(job_id) del self._jobs[job_id] + def _get_program(self, program_id): + """Get program.""" + if program_id not in self._programs: + raise RequestsApiError("Program not found", status_code=404) + return self._programs[program_id] + def _get_job(self, job_id): """Get job.""" if job_id not in self._jobs: raise RequestsApiError("Job not found", status_code=404) return self._jobs[job_id] + + @cloud_only + def list_backends(self): + """Return IBM Cloud backends""" + self._check_cloud_only() + return self._backend_client.backend_names + + @cloud_only + def backend_configuration(self, backend_name: str): + """Return the configuration of the IBM Cloud backend.""" + configs = self._backend_client.list_backends() + for conf in configs: + if conf["backend_name"] == backend_name: + return conf + raise ValueError(f"Backend {backend_name} not found.") + + @cloud_only + def backend_status(self, backend_name: str): + """Return the status of the IBM Cloud backend.""" + return self._backend_client.backend_status(backend_name) + + @cloud_only + def backend_properties(self, backend_name: str, datetime=None): + """Return the properties of the IBM Cloud backend.""" + if datetime: + raise NotImplementedError("'datetime' is not supported with cloud runtime.") + return self._backend_client.backend_properties(backend_name) + + @cloud_only + def backend_pulse_defaults(self, backend_name: str): + """Return the pulse defaults of the IBM Cloud backend.""" + return self._backend_client.backend_pulse_defaults(backend_name) + + @cloud_only + def _check_cloud_only(self): + if self._auth_type != "cloud": + raise ValueError("A backend method is called by a legacy client!") diff --git a/test/mock/fake_runtime_service.py b/test/mock/fake_runtime_service.py new file mode 100644 index 0000000000..cdd639b92b --- /dev/null +++ b/test/mock/fake_runtime_service.py @@ -0,0 +1,126 @@ +# 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. + +"""Context managers for using with IBM Provider unit tests.""" + +from typing import Dict +from collections import OrderedDict +from unittest import mock + +from qiskit_ibm_runtime.ibm_runtime_service import IBMRuntimeService +from qiskit_ibm_runtime.hub_group_project import HubGroupProject +from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.clients import AuthClient + +from .fake_account_client import BaseFakeAccountClient +from .fake_runtime_client import BaseFakeRuntimeClient + + +class FakeRuntimeService(IBMRuntimeService): + """Creates an IBMRuntimeService instance with mocked hub/group/project. + + By default there are 2 h/g/p - `hub0/group0/project0` and `hub1/group1/project1`. + Each h/g/p has 2 backends - `common_backend` and `unique_backend_`. + + There are a few test options available, via the `test_options` input: + + - num_hgps: Number of hub/group/project. + - account_client: Custom account (backend) client to use. + """ + + DEFAULT_HGPS = ["hub0/group0/project0", "hub1/group1/project1"] + DEFAULT_COMMON_BACKEND = "common_backend" + DEFAULT_UNIQUE_BACKEND_PREFIX = "unique_backend_" + + def __init__(self, *args, **kwargs): + test_options = kwargs.pop("test_options", {}) + self._test_num_hgps = test_options.get("num_hgps", 2) + self._fake_account_client = test_options.get("account_client") + + with mock.patch( + "qiskit_ibm_runtime.ibm_runtime_service.RuntimeClient", + new=BaseFakeRuntimeClient, + ): + super().__init__(*args, **kwargs) + + def _authenticate_legacy_account(self, client_params: ClientParameters): + """Mock authentication.""" + return FakeAuthClient() + + def _initialize_hgps( + self, + auth_client: AuthClient, + ) -> Dict: + """Mock hgp initialization.""" + + hgps = OrderedDict() + + for idx in range(self._test_num_hgps): + hgp_name = self.DEFAULT_HGPS[idx] + + hgp_params = ClientParameters( + auth_type="legacy", + token="some_token", + url="some_url", + instance=hgp_name, + ) + hgp = HubGroupProject(client_params=hgp_params, instance=hgp_name) + fake_account_client = self._fake_account_client + if not fake_account_client: + specs = [ + {"configuration": {"backend_name": self.DEFAULT_COMMON_BACKEND}}, + { + "configuration": { + "backend_name": self.DEFAULT_UNIQUE_BACKEND_PREFIX + + str(idx) + } + }, + ] + fake_account_client = BaseFakeAccountClient(specs=specs, hgp=hgp_name) + hgp._api_client = fake_account_client + hgps[hgp_name] = hgp + + return hgps + + def _discover_cloud_backends(self): + """Mock discovery cloud backends.""" + if not self._fake_account_client: + specs = [{"configuration": {"backend_name": self.DEFAULT_COMMON_BACKEND}}] + for idx in range(self._test_num_hgps): + specs.append( + { + "configuration": { + "backend_name": self.DEFAULT_UNIQUE_BACKEND_PREFIX + + str(idx) + } + } + ) + self._fake_account_client = BaseFakeAccountClient(specs=specs) + + test_options = { + "backend_client": self._fake_account_client, + "auth_type": "cloud", + } + self._api_client = BaseFakeRuntimeClient(test_options=test_options) + return super()._discover_cloud_backends() + + +class FakeAuthClient: + """Fake auth client.""" + + def current_service_urls(self): + """Return service urls.""" + return {"http": "legacy_api_url", "services": {"runtime": "legacy_runtime_url"}} + + def current_access_token(self): + """Return access token.""" + return "some_token" diff --git a/test/http_server.py b/test/mock/http_server.py similarity index 100% rename from test/http_server.py rename to test/mock/http_server.py diff --git a/test/proxy_server.py b/test/mock/proxy_server.py similarity index 90% rename from test/proxy_server.py rename to test/mock/proxy_server.py index 93f304e433..a50b8141b5 100644 --- a/test/proxy_server.py +++ b/test/mock/proxy_server.py @@ -15,6 +15,8 @@ import subprocess from contextlib import contextmanager +from qiskit_ibm_runtime.proxies import ProxyConfiguration + class MockProxyServer: """Local proxy server for testing.""" @@ -52,10 +54,10 @@ def stop(self): @contextmanager -def use_proxies(hgp, proxies): +def use_proxies(service, proxies): """Context manager to set and restore proxies setting.""" try: - hgp.credentials.proxies = {"urls": proxies} + service._client_params.proxies = ProxyConfiguration(urls=proxies) yield finally: - hgp.credentials.proxies = None + service._client_params.proxies = None diff --git a/test/ibm/runtime/ws_handler.py b/test/mock/ws_handler.py similarity index 100% rename from test/ibm/runtime/ws_handler.py rename to test/mock/ws_handler.py diff --git a/test/ws_server.py b/test/mock/ws_server.py similarity index 100% rename from test/ws_server.py rename to test/mock/ws_server.py diff --git a/test/test_account.py b/test/test_account.py new file mode 100644 index 0000000000..cf36914587 --- /dev/null +++ b/test/test_account.py @@ -0,0 +1,551 @@ +# 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. + +"""Tests for the account functions.""" + +import json +import logging +import os +import uuid +from typing import Any +from unittest import skipIf + +from qiskit_ibm_runtime.accounts import AccountManager, Account, management +from qiskit_ibm_runtime.proxies import ProxyConfiguration +from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.account import ( + get_account_config_contents, + temporary_account_config_file, + no_envs, + custom_envs, +) + +_TEST_LEGACY_ACCOUNT = Account( + auth="legacy", + token="token-x", + url="https://auth.quantum-computing.ibm.com/api", + instance="ibm-q/open/main", +) + +_TEST_CLOUD_ACCOUNT = Account( + auth="cloud", + token="token-y", + url="https://cloud.ibm.com", + instance="crn:v1:bluemix:public:quantum-computing:us-east:a/...::", + proxies=ProxyConfiguration( + username_ntlm="bla", password_ntlm="blub", urls={"https": "127.0.0.1"} + ), +) + + +class TestAccount(IBMTestCase): + """Tests for Account class.""" + + dummy_token = "123" + dummy_cloud_url = "https://us-east.quantum-computing.cloud.ibm.com" + dummy_legacy_url = "https://auth.quantum-computing.ibm.com/api" + + def test_invalid_auth(self): + """Test invalid values for auth parameter.""" + + with self.assertRaises(ValueError) as err: + invalid_auth: Any = "phantom" + Account( + auth=invalid_auth, token=self.dummy_token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `auth` value.", str(err.exception)) + + def test_invalid_token(self): + """Test invalid values for token parameter.""" + + invalid_tokens = [1, None, ""] + for token in invalid_tokens: + with self.subTest(token=token): + with self.assertRaises(ValueError) as err: + Account( + auth="cloud", token=token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `token` value.", str(err.exception)) + + def test_invalid_url(self): + """Test invalid values for url parameter.""" + + subtests = [ + {"auth": "cloud", "url": 123}, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account(**params, token=self.dummy_token).validate() + self.assertIn("Invalid `url` value.", str(err.exception)) + + def test_invalid_instance(self): + """Test invalid values for instance parameter.""" + + subtests = [ + {"auth": "cloud", "instance": ""}, + {"auth": "cloud"}, + {"auth": "legacy", "instance": "no-hgp-format"}, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account( + **params, token=self.dummy_token, url=self.dummy_cloud_url + ).validate() + self.assertIn("Invalid `instance` value.", str(err.exception)) + + def test_invalid_proxy_config(self): + """Test invalid values for proxy configuration.""" + + subtests = [ + { + "proxies": ProxyConfiguration(**{"username_ntlm": "user-only"}), + }, + { + "proxies": ProxyConfiguration(**{"password_ntlm": "password-only"}), + }, + { + "proxies": ProxyConfiguration(**{"urls": ""}), + }, + ] + for params in subtests: + with self.subTest(params=params): + with self.assertRaises(ValueError) as err: + Account( + **params, + auth="legacy", + token=self.dummy_token, + url=self.dummy_cloud_url, + ).validate() + self.assertIn("Invalid proxy configuration", str(err.exception)) + + +# NamedTemporaryFiles not supported in Windows +@skipIf(os.name == "nt", "Test not supported in Windows") +class TestAccountManager(IBMTestCase): + """Tests for AccountManager class.""" + + @temporary_account_config_file(contents={}) + @no_envs(["QISKIT_IBM_API_TOKEN"]) + def test_save_get(self): + """Test save and get.""" + + # Each tuple contains the + # - account to save + # - the name passed to AccountManager.save + # - the name passed to AccountManager.get + sub_tests = [ + # verify accounts can be saved and retrieved via custom names + (_TEST_LEGACY_ACCOUNT, "acct-1", "acct-1"), + (_TEST_CLOUD_ACCOUNT, "acct-2", "acct-2"), + # verify default account name handling for cloud accounts + (_TEST_CLOUD_ACCOUNT, None, management._DEFAULT_ACCOUNT_NAME_CLOUD), + (_TEST_CLOUD_ACCOUNT, None, None), + # verify default account name handling for legacy accounts + (_TEST_LEGACY_ACCOUNT, None, management._DEFAULT_ACCOUNT_NAME_LEGACY), + # verify account override + (_TEST_LEGACY_ACCOUNT, "acct", "acct"), + (_TEST_CLOUD_ACCOUNT, "acct", "acct"), + ] + for account, name_save, name_get in sub_tests: + with self.subTest( + f"for account type '{account.auth}' " + f"using `save(name={name_save})` and `get(name={name_get})`" + ): + AccountManager.save( + token=account.token, + url=account.url, + instance=account.instance, + auth=account.auth, + proxies=account.proxies, + verify=account.verify, + name=name_save, + ) + self.assertEqual(account, AccountManager.get(name=name_get)) + + @temporary_account_config_file( + contents=json.dumps( + { + "cloud": _TEST_CLOUD_ACCOUNT.to_saved_format(), + "legacy": _TEST_LEGACY_ACCOUNT.to_saved_format(), + } + ) + ) + def test_list(self): + """Test list.""" + + with temporary_account_config_file( + contents={ + "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), + "key2": _TEST_LEGACY_ACCOUNT.to_saved_format(), + } + ), self.subTest("non-empty list of accounts"): + accounts = AccountManager.list() + + self.assertEqual(len(accounts), 2) + self.assertEqual(accounts["key1"], _TEST_CLOUD_ACCOUNT) + self.assertTrue(accounts["key2"], _TEST_LEGACY_ACCOUNT) + + with temporary_account_config_file(contents={}), self.subTest( + "empty list of accounts" + ): + self.assertEqual(len(AccountManager.list()), 0) + + with temporary_account_config_file( + contents={ + "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), + "key2": _TEST_LEGACY_ACCOUNT.to_saved_format(), + management._DEFAULT_ACCOUNT_NAME_CLOUD: Account( + "cloud", "token-cloud", instance="crn:123" + ).to_saved_format(), + management._DEFAULT_ACCOUNT_NAME_LEGACY: Account( + "legacy", "token-legacy" + ).to_saved_format(), + } + ), self.subTest("filtered list of accounts"): + accounts = list(AccountManager.list(auth="cloud").keys()) + self.assertEqual(len(accounts), 2) + self.assertListEqual( + accounts, ["key1", management._DEFAULT_ACCOUNT_NAME_CLOUD] + ) + + accounts = list(AccountManager.list(auth="legacy").keys()) + self.assertEqual(len(accounts), 2) + self.assertListEqual( + accounts, ["key2", management._DEFAULT_ACCOUNT_NAME_LEGACY] + ) + + accounts = list(AccountManager.list(auth="cloud", default=True).keys()) + self.assertEqual(len(accounts), 1) + self.assertListEqual(accounts, [management._DEFAULT_ACCOUNT_NAME_CLOUD]) + + accounts = list(AccountManager.list(auth="cloud", default=False).keys()) + self.assertEqual(len(accounts), 1) + self.assertListEqual(accounts, ["key1"]) + + accounts = list(AccountManager.list(name="key1").keys()) + self.assertEqual(len(accounts), 1) + self.assertListEqual(accounts, ["key1"]) + + @temporary_account_config_file( + contents={ + "key1": _TEST_CLOUD_ACCOUNT.to_saved_format(), + management._DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT.to_saved_format(), + management._DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT.to_saved_format(), + } + ) + def test_delete(self): + """Test delete.""" + + with self.subTest("delete named account"): + self.assertTrue(AccountManager.delete(name="key1")) + self.assertFalse(AccountManager.delete(name="key1")) + + with self.subTest("delete default legacy account"): + self.assertTrue(AccountManager.delete(auth="legacy")) + + with self.subTest("delete default cloud account"): + self.assertTrue(AccountManager.delete()) + + self.assertTrue(len(AccountManager.list()) == 0) + + +MOCK_PROXY_CONFIG_DICT = { + "urls": {"https": "127.0.0.1", "username_ntlm": "", "password_ntlm": ""} +} +# NamedTemporaryFiles not supported in Windows +@skipIf(os.name == "nt", "Test not supported in Windows") +class TestEnableAccount(IBMTestCase): + """Tests for IBMRuntimeService enable account.""" + + def test_enable_account_by_name(self): + """Test initializing account by name.""" + name = "foo" + token = uuid.uuid4().hex + with temporary_account_config_file(name=name, token=token): + service = FakeRuntimeService(name=name) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + + def test_enable_account_by_auth(self): + """Test initializing account by auth.""" + for auth in ["cloud", "legacy"]: + with self.subTest(auth=auth), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + with temporary_account_config_file(auth=auth, token=token): + service = FakeRuntimeService(auth=auth) + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + + def test_enable_account_by_token_url(self): + """Test initializing account by token or url.""" + token = uuid.uuid4().hex + subtests = [ + {"token": token}, + {"url": "some_url"}, + {"token": token, "url": "some_url"}, + ] + for param in subtests: + with self.subTest(param=param): + with self.assertRaises(ValueError): + _ = FakeRuntimeService(**param) + + def test_enable_account_by_name_and_other(self): + """Test initializing account by name and other.""" + subtests = [ + {"auth": "cloud"}, + {"token": "some_token"}, + {"url": "some_url"}, + {"auth": "cloud", "token": "some_token", "url": "some_url"}, + ] + + name = "foo" + token = uuid.uuid4().hex + for param in subtests: + with self.subTest(param=param), temporary_account_config_file( + name=name, token=token + ): + with self.assertLogs("qiskit_ibm_runtime", logging.WARNING) as logged: + service = FakeRuntimeService(name=name, **param) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertIn("are ignored", logged.output[0]) + + def test_enable_cloud_account_by_auth_token_url(self): + """Test initializing cloud account by auth, token, url.""" + # Enable account will fail due to missing CRN. + urls = [None, "some_url"] + for url in urls: + with self.subTest(url=url), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + with self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(auth="cloud", token=token, url=url) + self.assertIn("instance", str(err.exception)) + + def test_enable_legacy_account_by_auth_token_url(self): + """Test initializing legacy account by auth, token, url.""" + urls = [(None, LEGACY_API_URL), ("some_url", "some_url")] + for url, expected in urls: + with self.subTest(url=url), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + service = FakeRuntimeService(auth="legacy", token=token, url=url) + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, expected) + + def test_enable_account_by_auth_url(self): + """Test initializing legacy account by auth, token, url.""" + subtests = ["legacy", "cloud"] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + with temporary_account_config_file(auth=auth, token=token), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + with self.assertLogs( + "qiskit_ibm_runtime", logging.WARNING + ) as logged: + service = FakeRuntimeService(auth=auth, url="some_url") + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + expected = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + self.assertEqual(service._account.url, expected) + self.assertIn("url", logged.output[0]) + + def test_enable_account_by_only_auth(self): + """Test initializing account with single saved account.""" + subtests = ["legacy", "cloud"] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + with temporary_account_config_file(auth=auth, token=token), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + service = FakeRuntimeService() + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + expected = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + self.assertEqual(service._account.url, expected) + self.assertEqual(service._account.auth, auth) + + def test_enable_account_both_auth(self): + """Test initializing account with both saved types.""" + token = uuid.uuid4().hex + contents = get_account_config_contents(auth="cloud", token=token) + contents.update( + get_account_config_contents(auth="legacy", token=uuid.uuid4().hex) + ) + with temporary_account_config_file(contents=contents), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + service = FakeRuntimeService() + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, CLOUD_API_URL) + self.assertEqual(service._account.auth, "cloud") + + def test_enable_account_by_env_auth(self): + """Test initializing account by environment variable and auth.""" + subtests = ["legacy", "cloud", None] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "h/g/p" if auth == "legacy" else "crn:123", + } + with custom_envs(envs): + service = FakeRuntimeService(auth=auth) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, url) + auth = auth or "cloud" + self.assertEqual(service._account.auth, auth) + + def test_enable_account_by_env_token_url(self): + """Test initializing account by environment variable and extra.""" + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "my_crn", + } + subtests = [{"token": token}, {"url": url}, {"token": token, "url": url}] + for extra in subtests: + with self.subTest(extra=extra): + with custom_envs(envs) as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(**extra) + self.assertIn("token", str(err.exception)) + + def test_enable_account_bad_name(self): + """Test initializing account by bad name.""" + name = "phantom" + with temporary_account_config_file() as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(name=name) + self.assertIn(name, str(err.exception)) + + def test_enable_account_bad_auth(self): + """Test initializing account by bad name.""" + auth = "phantom" + with temporary_account_config_file() as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(auth=auth) + self.assertIn("auth", str(err.exception)) + + def test_enable_account_by_name_pref(self): + """Test initializing account by name and preferences.""" + name = "foo" + subtests = [ + {"proxies": MOCK_PROXY_CONFIG_DICT}, + {"verify": False}, + {"instance": "bar"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "bar"}, + ] + for extra in subtests: + with self.subTest(extra=extra): + with temporary_account_config_file( + name=name, verify=True, proxies="some proxies" + ): + service = FakeRuntimeService(name=name, **extra) + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_auth_pref(self): + """Test initializing account by auth and preferences.""" + subtests = [ + {"proxies": MOCK_PROXY_CONFIG_DICT}, + {"verify": False}, + {"instance": "h/g/p"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "h/g/p"}, + ] + for auth in ["cloud", "legacy"]: + for extra in subtests: + with self.subTest( + auth=auth, extra=extra + ), temporary_account_config_file( + auth=auth, verify=True, proxies="some proxies" + ), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + service = FakeRuntimeService(auth=auth, **extra) + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_env_pref(self): + """Test initializing account by environment variable and preferences.""" + subtests = [ + {"proxies": MOCK_PROXY_CONFIG_DICT}, + {"verify": False}, + {"instance": "bar"}, + {"proxies": MOCK_PROXY_CONFIG_DICT, "verify": False, "instance": "bar"}, + ] + for extra in subtests: + with self.subTest(extra=extra): + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "my_crn", + } + with custom_envs(envs): + service = FakeRuntimeService(**extra) + + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_name_input_instance(self): + """Test initializing account by name and input instance.""" + name = "foo" + instance = uuid.uuid4().hex + with temporary_account_config_file(name=name, instance="stored-instance"): + service = FakeRuntimeService(name=name, instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def test_enable_account_by_auth_input_instance(self): + """Test initializing account by auth and input instance.""" + instance = uuid.uuid4().hex + with temporary_account_config_file(auth="cloud", instance="bla"): + service = FakeRuntimeService(auth="cloud", instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def test_enable_account_by_env_input_instance(self): + """Test initializing account by env and input instance.""" + instance = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": "some_token", + "QISKIT_IBM_API_URL": "some_url", + "QISKIT_IBM_INSTANCE": "some_instance", + } + with custom_envs(envs): + service = FakeRuntimeService(auth="cloud", instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def _verify_prefs(self, prefs, account): + if "proxies" in prefs: + self.assertEqual(account.proxies, ProxyConfiguration(**prefs["proxies"])) + if "verify" in prefs: + self.assertEqual(account.verify, prefs["verify"]) + if "instance" in prefs: + self.assertEqual(account.instance, prefs["instance"]) diff --git a/test/ibm/test_account_client.py b/test/test_account_client.py similarity index 74% rename from test/ibm/test_account_client.py rename to test/test_account_client.py index a1d381d9ed..97399c7719 100644 --- a/test/ibm/test_account_client.py +++ b/test/test_account_client.py @@ -14,46 +14,22 @@ import re -from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit_ibm_runtime.api.clients import AccountClient, AuthClient from qiskit_ibm_runtime.api.exceptions import ApiError, RequestsApiError +from qiskit_ibm_runtime.api.client_parameters import ClientParameters -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_qe_access, requires_provider -from ..contextmanagers import custom_envs, no_envs -from ..http_server import SimpleServer, ClientErrorHandler +from .ibm_test_case import IBMTestCase +from .mock.http_server import SimpleServer, ClientErrorHandler +from .utils.account import custom_envs, no_envs +from .utils.decorators import requires_qe_access class TestAccountClient(IBMTestCase): """Tests for AccountClient.""" - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - def setUp(self): """Initial test setup.""" super().setUp() - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - self.qc1 = QuantumCircuit(qr, cr, name="qc1") - self.qc2 = QuantumCircuit(qr, cr, name="qc2") - self.qc1.h(qr) - self.qc2.h(qr[0]) - self.qc2.cx(qr[0], qr[1]) - self.qc1.measure(qr[0], cr[0]) - self.qc1.measure(qr[1], cr[1]) - self.qc2.measure(qr[0], cr[0]) - self.qc2.measure(qr[1], cr[1]) - self.seed = 73846087 - self.fake_server = None def tearDown(self) -> None: @@ -65,7 +41,10 @@ def tearDown(self) -> None: def _get_client(self): """Helper for instantiating an AccountClient.""" # pylint: disable=no-value-for-parameter - return AccountClient(self.service._default_hgp.credentials) + params = ClientParameters( + auth_type="legacy", url=SimpleServer.URL, token="foo", instance="h/g/p" + ) + return AccountClient(params) def test_custom_client_app_header(self): """Check custom client application header.""" @@ -90,7 +69,7 @@ def test_client_error(self): client = self._get_client() self.fake_server = SimpleServer(handler_class=ClientErrorHandler) self.fake_server.start() - client.account_api.session.base_url = SimpleServer.URL + # client.account_api.session.base_url = SimpleServer.URL sub_tests = [ {"error": "Bad client input"}, @@ -114,7 +93,7 @@ class TestAuthClient(IBMTestCase): @requires_qe_access def test_valid_login(self, qe_token, qe_url): """Test valid authentication.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) self.assertTrue(client.access_token) @requires_qe_access @@ -122,33 +101,33 @@ def test_url_404(self, qe_token, qe_url): """Test login against a 404 URL""" url_404 = re.sub(r"/api.*$", "/api/TEST_404", qe_url) with self.assertRaises(ApiError): - _ = AuthClient(qe_token, url_404) + _ = self._init_auth_client(qe_token, url_404) @requires_qe_access def test_invalid_token(self, qe_token, qe_url): """Test login using invalid token.""" qe_token = "INVALID_TOKEN" with self.assertRaises(ApiError): - _ = AuthClient(qe_token, qe_url) + _ = self._init_auth_client(qe_token, qe_url) @requires_qe_access def test_url_unreachable(self, qe_token, qe_url): """Test login against an invalid (malformed) URL.""" qe_url = "INVALID_URL" with self.assertRaises(ApiError): - _ = AuthClient(qe_token, qe_url) + _ = self._init_auth_client(qe_token, qe_url) @requires_qe_access def test_api_version(self, qe_token, qe_url): """Check the version of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) version = client.api_version() self.assertIsNotNone(version) @requires_qe_access def test_user_urls(self, qe_token, qe_url): """Check the user urls of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) user_urls = client.user_urls() self.assertIsNotNone(user_urls) self.assertTrue("http" in user_urls and "ws" in user_urls) @@ -156,7 +135,7 @@ def test_user_urls(self, qe_token, qe_url): @requires_qe_access def test_user_hubs(self, qe_token, qe_url): """Check the user hubs of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) user_hubs = client.user_hubs() self.assertIsNotNone(user_hubs) for user_hub in user_hubs: @@ -164,3 +143,8 @@ def test_user_hubs(self, qe_token, qe_url): self.assertTrue( "hub" in user_hub and "group" in user_hub and "project" in user_hub ) + + def _init_auth_client(self, token, url): + """Return an AuthClient.""" + params = ClientParameters(auth_type="legacy", token=token, url=url) + return AuthClient(params) diff --git a/test/test_backend_retrieval.py b/test/test_backend_retrieval.py new file mode 100644 index 0000000000..ba871bae46 --- /dev/null +++ b/test/test_backend_retrieval.py @@ -0,0 +1,240 @@ +# 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. + +"""Backends Filtering Test.""" + +from qiskit.test.mock.backends import FakeLima +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from .ibm_test_case import IBMTestCase +from .mock.fake_account_client import BaseFakeAccountClient +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestBackendFilters(IBMTestCase): + """Qiskit Backend Filtering Tests.""" + + @run_legacy_and_cloud_fake + def test_no_filter(self, service): + """Test no filtering.""" + # FakeRuntimeService by default creates 3 backends. + backend_name = [back.name() for back in service.backends()] + self.assertEqual(len(backend_name), 3) + + @run_legacy_and_cloud_fake + def test_filter_by_name(self, service): + """Test filtering by name.""" + for name in [ + FakeRuntimeService.DEFAULT_COMMON_BACKEND, + FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0", + ]: + with self.subTest(name=name): + backend_name = [back.name() for back in service.backends(name=name)] + self.assertEqual(len(backend_name), 1) + + def test_filter_by_instance_legacy(self): + """Test filtering by instance.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + for hgp in FakeRuntimeService.DEFAULT_HGPS: + with self.subTest(hgp=hgp): + backends = service.backends(instance=hgp) + backend_name = [back.name() for back in backends] + self.assertEqual(len(backend_name), 2) + for back in backends: + self.assertEqual(back._api_client.hgp, hgp) + + def test_filter_config_properties(self): + """Test filtering by configuration properties.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits, local=False), + self._get_specs(n_qubits=n_qubits * 2, local=False), + self._get_specs(n_qubits=n_qubits, local=True), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends(n_qubits=n_qubits, local=False) + self.assertTrue(len(filtered_backends), 1) + self.assertEqual( + n_qubits, filtered_backends[0].configuration().n_qubits + ) + self.assertFalse(filtered_backends[0].configuration().local) + + def test_filter_status_dict(self): + """Test filtering by dictionary of mixed status/configuration properties.""" + fake_backends = [ + self._get_specs(operational=True, simulator=True), + self._get_specs(operational=True, simulator=True), + self._get_specs(operational=True, simulator=False), + self._get_specs(operational=False, simulator=False), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends( + operational=True, # from status + simulator=True, # from configuration + ) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertTrue(backend.status().operational) + self.assertTrue(backend.configuration().simulator) + + def test_filter_config_callable(self): + """Test filtering by lambda function on configuration properties.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits), + self._get_specs(n_qubits=n_qubits * 2), + self._get_specs(n_qubits=n_qubits - 1), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends( + filters=lambda x: (x.configuration().n_qubits >= 5) + ) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertGreaterEqual(backend.configuration().n_qubits, n_qubits) + + def test_filter_least_busy(self): + """Test filtering by least busy function.""" + default_stat = {"pending_jobs": 1, "operational": True, "status_msg": "active"} + fake_backends = [ + self._get_specs( + **{**default_stat, "backend_name": "bingo", "pending_jobs": 5} + ), + self._get_specs(**{**default_stat, "pending_jobs": 7}), + self._get_specs(**{**default_stat, "operational": False}), + self._get_specs(**{**default_stat, "status_msg": "internal"}), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + backend = service.least_busy() + self.assertEqual(backend.name(), "bingo") + + def test_filter_min_num_qubits(self): + """Test filtering by minimum number of qubits.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits), + self._get_specs(n_qubits=n_qubits * 2), + self._get_specs(n_qubits=n_qubits - 1), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends(min_num_qubits=n_qubits) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertGreaterEqual(backend.configuration().n_qubits, n_qubits) + + def test_filter_by_hgp(self): + """Test filtering by hub/group/project.""" + num_backends = 3 + test_options = { + "account_client": BaseFakeAccountClient(num_backends=num_backends), + "num_hgps": 2, + } + legacy_service = FakeRuntimeService( + auth="legacy", + token="my_token", + instance="h/g/p", + test_options=test_options, + ) + backends = legacy_service.backends(instance="hub0/group0/project0") + self.assertEqual(len(backends), num_backends) + + def _get_specs(self, **kwargs): + """Get the backend specs to pass to the fake account client.""" + specs = {"configuration": {}, "status": {}} + status_keys = FakeLima().status().to_dict() + status_keys.pop("backend_name") # name is in both config and status + status_keys = list(status_keys.keys()) + for key, val in kwargs.items(): + if key in status_keys: + specs["status"][key] = val + else: + specs["configuration"][key] = val + return specs + + def _get_services(self, fake_backends): + """Get both cloud and legacy services initialized with fake backends.""" + test_options = {"account_client": BaseFakeAccountClient(specs=fake_backends)} + legacy_service = FakeRuntimeService( + auth="legacy", + token="my_token", + instance="h/g/p", + test_options=test_options, + ) + cloud_service = FakeRuntimeService( + auth="cloud", + token="my_token", + instance="my_instance", + test_options=test_options, + ) + return [legacy_service, cloud_service] + + +class TestGetBackend(IBMTestCase): + """Test getting a backend via legacy api.""" + + def test_get_common_backend(self): + """Test getting a backend that is in default and non-default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = service.backend(FakeRuntimeService.DEFAULT_COMMON_BACKEND) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[0]) + + def test_get_unique_backend_default_hgp(self): + """Test getting a backend in the default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0" + backend = service.backend(backend_name) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[0]) + + def test_get_unique_backend_non_default_hgp(self): + """Test getting a backend in the non default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + backend = service.backend(backend_name) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[1]) + + def test_get_phantom_backend(self): + """Test getting a phantom backend.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(QiskitBackendNotFoundError): + service.backend("phantom") + + def test_get_backend_by_hgp(self): + """Test getting a backend by hgp.""" + hgp = FakeRuntimeService.DEFAULT_HGPS[1] + backend_name = FakeRuntimeService.DEFAULT_COMMON_BACKEND + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = service.backend(backend_name, instance=hgp) + self.assertEqual(backend._api_client.hgp, hgp) + + def test_get_backend_by_bad_hgp(self): + """Test getting a backend not in hgp.""" + hgp = FakeRuntimeService.DEFAULT_HGPS[1] + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(QiskitBackendNotFoundError): + _ = service.backend(backend_name, instance=hgp) diff --git a/test/ibm/test_serialization.py b/test/test_backend_serialization.py similarity index 81% rename from test/ibm/test_serialization.py rename to test/test_backend_serialization.py index 865f7c4a0a..3fa81051bf 100644 --- a/test/ibm/test_serialization.py +++ b/test/test_backend_serialization.py @@ -16,35 +16,30 @@ import dateutil.parser -from ..decorators import requires_provider -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_cloud_legacy_services, run_cloud_legacy_real class TestSerialization(IBMTestCase): """Test data serialization.""" @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): + @requires_cloud_legacy_services + def setUpClass(cls, services): """Initial class level setup.""" # pylint: disable=arguments-differ super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - cls.sim_backend = service.get_backend( - "ibmq_qasm_simulator", hub=cls.hub, group=cls.group, project=cls.project - ) + cls.services = services + cls.instances = {} + for serv in services: + cls.instances[serv.auth] = serv._account.instance - def test_backend_configuration(self): + @run_cloud_legacy_real + def test_backend_configuration(self, service): """Test deserializing backend configuration.""" - backends = self.service.backends( - operational=True, - simulator=False, - hub=self.hub, - group=self.group, - project=self.project, + instance = self.instances[service.auth] if service.auth == "legacy" else None + backends = service.backends( + operational=True, simulator=False, instance=instance ) # Known keys that look like a serialized complex number. @@ -67,14 +62,12 @@ def test_backend_configuration(self): backend.configuration().to_dict(), good_keys, good_keys_prefixes ) - def test_pulse_defaults(self): + @run_cloud_legacy_real + def test_pulse_defaults(self, service): """Test deserializing backend configuration.""" - backends = self.service.backends( - operational=True, - open_pulse=True, - hub=self.hub, - group=self.group, - project=self.project, + instance = self.instances[service.auth] if service.auth == "legacy" else None + backends = service.backends( + operational=True, open_pulse=True, instance=instance ) if not backends: self.skipTest("Need pulse backends.") @@ -86,14 +79,12 @@ def test_pulse_defaults(self): with self.subTest(backend=backend): self._verify_data(backend.defaults().to_dict(), good_keys) - def test_backend_properties(self): + @run_cloud_legacy_real + def test_backend_properties(self, service): """Test deserializing backend properties.""" - backends = self.service.backends( - operational=True, - simulator=False, - hub=self.hub, - group=self.group, - project=self.project, + instance = self.instances[service.auth] if service.auth == "legacy" else None + backends = service.backends( + operational=True, simulator=False, instance=instance ) # Known keys that look like a serialized object. diff --git a/test/test_basic_server_paths.py b/test/test_basic_server_paths.py new file mode 100644 index 0000000000..52be3c2e1a --- /dev/null +++ b/test/test_basic_server_paths.py @@ -0,0 +1,53 @@ +# 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. + +"""Tests that hit all the basic server endpoints using both a public and premium h/g/p.""" + +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_multiple_hgps + + +class TestBasicServerPaths(IBMTestCase): + """Test the basic server endpoints using both a public and premium provider.""" + + @classmethod + @requires_multiple_hgps + def setUpClass(cls, service, open_hgp, premium_hgp): + # pylint: disable=arguments-differ + super().setUpClass() + cls.service = service # Dict[str, IBMRuntimeService] + cls.hgps = [open_hgp, premium_hgp] + + def test_device_properties_and_defaults(self): + """Test device properties and defaults.""" + for hgp in self.hgps: + with self.subTest(hgp=hgp): + pulse_backends = self.service.backends( + simulator=False, operational=True, instance=hgp + ) + if not pulse_backends: + raise self.skipTest( + "Skipping pulse test since no pulse backend " + 'found for "{}"'.format(hgp) + ) + + self.assertIsNotNone(pulse_backends[0].properties()) + self.assertIsNotNone(pulse_backends[0].defaults()) + + def test_device_status(self): + """Test device status.""" + for hgp in self.hgps: + with self.subTest(hgp=hgp): + backend = self.service.backends( + simulator=False, operational=True, instance=hgp + )[0] + self.assertTrue(backend.status()) diff --git a/test/ibm/test_registration.py b/test/test_client_parameters.py similarity index 58% rename from test/ibm/test_registration.py rename to test/test_client_parameters.py index 294dcc6139..3c83c67943 100644 --- a/test/ibm/test_registration.py +++ b/test/test_client_parameters.py @@ -10,41 +10,32 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the registration and credentials modules.""" +"""Tests for ClientParameters.""" -from requests_ntlm import HttpNtlmAuth - -from qiskit_ibm_runtime.credentials import ( - Credentials, -) -from ..ibm_test_case import IBMTestCase +import uuid -IBM_TEMPLATE = "https://localhost/api/Hubs/{}/Groups/{}/Projects/{}" +from requests_ntlm import HttpNtlmAuth +from qiskit_ibm_runtime.proxies import ProxyConfiguration +from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.auth import CloudAuth, LegacyAuth -PROXIES = { - "urls": { - "http": "http://user:password@127.0.0.1:5678", - "https": "https://user:password@127.0.0.1:5678", - } -} +from .ibm_test_case import IBMTestCase -class TestCredentialsKwargs(IBMTestCase): - """Test for ``Credentials.connection_parameters()``.""" +class TestClientParameters(IBMTestCase): + """Test for ``ClientParameters``.""" def test_no_proxy_params(self) -> None: """Test when no proxy parameters are passed.""" no_params_expected_result = {"verify": True} - no_params_credentials = Credentials("dummy_token", "https://dummy_url") + no_params_credentials = self._get_client_params() result = no_params_credentials.connection_parameters() self.assertDictEqual(no_params_expected_result, result) def test_verify_param(self) -> None: """Test 'verify' arg is acknowledged.""" false_verify_expected_result = {"verify": False} - false_verify_credentials = Credentials( - "dummy_token", "https://dummy_url", verify=False - ) + false_verify_credentials = self._get_client_params(verify=False) result = false_verify_credentials.connection_parameters() self.assertDictEqual(false_verify_expected_result, result) @@ -52,8 +43,8 @@ def test_proxy_param(self) -> None: """Test using only proxy urls (no NTLM credentials).""" urls = {"http": "localhost:8080", "https": "localhost:8080"} proxies_only_expected_result = {"verify": True, "proxies": urls} - proxies_only_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies={"urls": urls} + proxies_only_credentials = self._get_client_params( + proxies=ProxyConfiguration(**{"urls": urls}) ) result = proxies_only_credentials.connection_parameters() self.assertDictEqual(proxies_only_expected_result, result) @@ -71,8 +62,8 @@ def test_proxies_param_with_ntlm(self) -> None: "proxies": urls, "auth": HttpNtlmAuth("domain\\username", "password"), } - proxies_with_ntlm_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=proxies_with_ntlm_dict + proxies_with_ntlm_credentials = self._get_client_params( + proxies=ProxyConfiguration(**proxies_with_ntlm_dict) ) result = proxies_with_ntlm_credentials.connection_parameters() @@ -85,19 +76,6 @@ def test_proxies_param_with_ntlm(self) -> None: result.pop("auth") self.assertDictEqual(ntlm_expected_result, result) - def test_malformed_proxy_param(self) -> None: - """Test input with malformed nesting of the proxies dictionary.""" - urls = {"http": "localhost:8080", "https": "localhost:8080"} - malformed_nested_proxies_dict = {"proxies": urls} - malformed_nested_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=malformed_nested_proxies_dict - ) - - # Malformed proxy entries should be ignored. - expected_result = {"verify": True} - result = malformed_nested_credentials.connection_parameters() - self.assertDictEqual(expected_result, result) - def test_malformed_ntlm_params(self) -> None: """Test input with malformed NTLM credentials.""" urls = {"http": "localhost:8080", "https": "localhost:8080"} @@ -106,10 +84,51 @@ def test_malformed_ntlm_params(self) -> None: "username_ntlm": 1234, "password_ntlm": 5678, } - malformed_ntlm_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=malformed_ntlm_credentials_dict + malformed_ntlm_credentials = self._get_client_params( + proxies=malformed_ntlm_credentials_dict ) # Should raise when trying to do username.split('\\', ) # in NTLM credentials due to int not facilitating 'split'. with self.assertRaises(AttributeError): _ = malformed_ntlm_credentials.connection_parameters() + + def test_auth_handler_legacy(self): + """Test getting legacy auth handler.""" + token = uuid.uuid4().hex + params = self._get_client_params(auth_type="legacy", token=token) + handler = params.get_auth_handler() + self.assertIsInstance(handler, LegacyAuth) + self.assertIn(token, handler.get_headers().values()) + + def test_auth_handler_cloud(self): + """Test getting cloud auth handler.""" + token = uuid.uuid4().hex + instance = uuid.uuid4().hex + params = self._get_client_params( + auth_type="cloud", token=token, instance=instance + ) + handler = params.get_auth_handler() + self.assertIsInstance(handler, CloudAuth) + self.assertIn(f"apikey {token}", handler.get_headers().values()) + self.assertIn(instance, handler.get_headers().values()) + + def _get_client_params( + self, + auth_type="legacy", + token="dummy_token", + url="https://dummy_url", + instance=None, + proxies=None, + verify=None, + ): + """Return a custom ClientParameters.""" + if verify is None: + verify = True + return ClientParameters( + auth_type=auth_type, + token=token, + url=url, + instance=instance, + proxies=proxies, + verify=verify, + ) diff --git a/test/test_data_serialization.py b/test/test_data_serialization.py new file mode 100644 index 0000000000..49a273d276 --- /dev/null +++ b/test/test_data_serialization.py @@ -0,0 +1,273 @@ +# 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. + +"""Tests for runtime data serialization.""" + +import json +import os +from unittest import skipIf +import subprocess +import tempfile +import warnings +from datetime import datetime +import numpy as np +import scipy.sparse + +from qiskit.algorithms.optimizers import ( + ADAM, + GSLS, + IMFIL, + SPSA, + QNSPSA, + SNOBFIT, + L_BFGS_B, + NELDER_MEAD, +) +from qiskit.result import Result +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.test.reference_circuits import ReferenceCircuits +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + PauliSumOp, + MatrixOp, + PauliOp, + CircuitOp, + EvolvedOp, + TaperedPauliSumOp, + Z2Symmetries, + I, + X, + Y, + Z, + StateFn, + CircuitStateFn, + DictStateFn, + VectorStateFn, + OperatorStateFn, + SparseVectorStateFn, + CVaRMeasurement, + ComposedOp, + SummedOp, + TensoredOp, +) +from qiskit.quantum_info import SparsePauliOp, Pauli, PauliTable, Statevector + +from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder + +from .ibm_test_case import IBMTestCase +from .utils.serialization import ( + SerializableClass, + SerializableClassDecoder, + get_complex_types, +) +from .utils.program import run_program +from .mock.fake_runtime_service import FakeRuntimeService +from .mock.fake_runtime_client import CustomResultRuntimeJob + + +class TestDataSerialization(IBMTestCase): + """Class for testing runtime data serialization.""" + + def test_coder(self): + """Test runtime encoder and decoder.""" + result = Result( + backend_name="ibmqx2", + backend_version="1.1", + qobj_id="12345", + job_id="67890", + success=False, + results=[], + ) + + data = { + "string": "foo", + "float": 1.5, + "complex": 2 + 3j, + "array": np.array([[1, 2, 3], [4, 5, 6]]), + "result": result, + "sclass": SerializableClass("foo"), + } + encoded = json.dumps(data, cls=RuntimeEncoder) + decoded = json.loads(encoded, cls=RuntimeDecoder) + decoded["sclass"] = SerializableClass.from_json(decoded["sclass"]) + + decoded_result = decoded.pop("result") + data.pop("result") + + decoded_array = decoded.pop("array") + orig_array = data.pop("array") + + self.assertEqual(decoded, data) + self.assertIsInstance(decoded_result, Result) + self.assertTrue((decoded_array == orig_array).all()) + + def test_coder_qc(self): + """Test runtime encoder and decoder for circuits.""" + bell = ReferenceCircuits.bell() + unbound = EfficientSU2(num_qubits=4, reps=1, entanglement="linear") + subtests = (bell, unbound, [bell, unbound]) + for circ in subtests: + with self.subTest(circ=circ): + encoded = json.dumps(circ, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + if not isinstance(circ, list): + decoded = [decoded] + self.assertTrue( + all(isinstance(item, QuantumCircuit) for item in decoded) + ) + + def test_coder_operators(self): + """Test runtime encoder and decoder for operators.""" + x = Parameter("x") + y = x + 1 + qc = QuantumCircuit(1) + qc.h(0) + coeffs = np.array([1, 2, 3, 4, 5, 6]) + table = PauliTable.from_labels(["III", "IXI", "IYY", "YIZ", "XYZ", "III"]) + op = 2.0 * I ^ I + z2_symmetries = Z2Symmetries( + [Pauli("IIZI"), Pauli("ZIII")], + [Pauli("IIXI"), Pauli("XIII")], + [1, 3], + [-1, 1], + ) + isqrt2 = 1 / np.sqrt(2) + sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) + + subtests = ( + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=y), + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), + PauliSumOp.from_list( + [("II", -1.052373245772859), ("IZ", 0.39793742484318045)] + ), + PauliSumOp(SparsePauliOp(table, coeffs), coeff=10), + MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=x), + PauliOp(primitive=Pauli("Y"), coeff=x), + CircuitOp(qc, coeff=x), + EvolvedOp(op, coeff=x), + TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), + StateFn(qc, coeff=x), + CircuitStateFn(qc, is_measurement=True), + DictStateFn("1" * 3, is_measurement=True), + VectorStateFn(np.ones(2 ** 3, dtype=complex)), + OperatorStateFn(CircuitOp(QuantumCircuit(1))), + SparseVectorStateFn(sparse), + Statevector([1, 0]), + CVaRMeasurement(Z, 0.2), + ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), + SummedOp([X ^ X * 2, Y ^ Y], 2), + TensoredOp([(X ^ Y), (Z ^ I)]), + (Z ^ Z) ^ (I ^ 2), + ) + for op in subtests: + with self.subTest(op=op): + encoded = json.dumps(op, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertEqual(op, decoded) + + @skipIf(os.name == "nt", "Test not supported on Windows") + def test_coder_optimizers(self): + """Test runtime encoder and decoder for optimizers.""" + subtests = ( + (ADAM, {"maxiter": 100, "amsgrad": True}), + (GSLS, {"maxiter": 50, "min_step_size": 0.01}), + (IMFIL, {"maxiter": 20}), + (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), + (SNOBFIT, {"maxiter": 200, "maxfail": 20}), + (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), + # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 + (L_BFGS_B, {}), + (NELDER_MEAD, {}), + ) + for opt_cls, settings in subtests: + with self.subTest(opt_cls=opt_cls): + optimizer = opt_cls(**settings) + encoded = json.dumps(optimizer, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertTrue(isinstance(decoded, opt_cls)) + for key, value in settings.items(): + self.assertEqual(decoded.settings[key], value) + + def test_encoder_datetime(self): + """Test encoding a datetime.""" + subtests = ( + {"datetime": datetime.now()}, + {"datetime": datetime(2021, 8, 4)}, + {"datetime": datetime.fromtimestamp(1326244364)}, + ) + for obj in subtests: + encoded = json.dumps(obj, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertEqual(decoded, obj) + + def test_encoder_callable(self): + """Test encoding a callable.""" + with warnings.catch_warnings(record=True) as warn_cm: + encoded = json.dumps({"fidelity": lambda x: x}, cls=RuntimeEncoder) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertIsNone(decoded["fidelity"]) + self.assertEqual(len(warn_cm), 1) + + def test_decoder_import(self): + """Test runtime decoder importing modules.""" + script = """ +import sys +import json +from qiskit_ibm_runtime import RuntimeDecoder +if __name__ == '__main__': + obj = json.loads(sys.argv[1], cls=RuntimeDecoder) + print(obj.__class__.__name__) +""" + temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) + self.addCleanup(os.remove, temp_fp.name) + temp_fp.write(script) + temp_fp.close() + + subtests = ( + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), + DictStateFn("1" * 3, is_measurement=True), + Statevector([1, 0]), + ) + for op in subtests: + with self.subTest(op=op): + encoded = json.dumps(op, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + cmd = ["python", temp_fp.name, encoded] + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + ) + self.assertIn(op.__class__.__name__, proc.stdout) + + def test_result_decoder(self): + """Test result decoder.""" + custom_result = get_complex_types() + job_cls = CustomResultRuntimeJob + job_cls.custom_result = custom_result + legacy_service = FakeRuntimeService(auth="legacy", token="some_token") + + sub_tests = [(SerializableClassDecoder, None), (None, SerializableClassDecoder)] + for result_decoder, decoder in sub_tests: + with self.subTest(decoder=decoder): + job = run_program( + service=legacy_service, job_classes=job_cls, decoder=result_decoder + ) + result = job.result(decoder=decoder) + self.assertIsInstance(result["serializable_class"], SerializableClass) diff --git a/test/test_integration_backend.py b/test/test_integration_backend.py new file mode 100644 index 0000000000..ba138ab2a9 --- /dev/null +++ b/test/test_integration_backend.py @@ -0,0 +1,92 @@ +# 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. + +"""Tests for backend functions using real runtime service.""" + +from unittest import SkipTest + +from .ibm_test_case import IBMIntegrationTestCase +from .utils.decorators import ( + run_cloud_legacy_real, + requires_cloud_legacy_devices, +) + + +class TestIntegrationBackend(IBMIntegrationTestCase): + """Integration tests for backend functions.""" + + @run_cloud_legacy_real + def test_backends(self, service): + """Test getting all backends.""" + backends = service.backends() + self.assertTrue(backends) + backend_names = [back.name() for back in backends] + self.assertEqual( + len(backend_names), + len(set(backend_names)), + f"backend_names={backend_names}", + ) + + @run_cloud_legacy_real + def test_get_backend(self, service): + """Test getting a backend.""" + backends = service.backends() + backend = service.backend(backends[0].name()) + self.assertTrue(backend) + + +class TestIBMBackend(IBMIntegrationTestCase): + """Test ibm_backend module.""" + + @classmethod + @requires_cloud_legacy_devices + def setUpClass(cls, devices): + """Initial class level setup.""" + # pylint: disable=arguments-differ + # pylint: disable=no-value-for-parameter + super().setUpClass() + cls.devices = devices + + def test_backend_status(self): + """Check the status of a real chip.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + self.assertTrue(backend.status().operational) + + def test_backend_properties(self): + """Check the properties of calibration of a real chip.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + if backend.configuration().simulator: + raise SkipTest("Skip since simulator does not have properties.") + self.assertIsNotNone(backend.properties()) + + def test_backend_pulse_defaults(self): + """Check the backend pulse defaults of each backend.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + if backend.configuration().simulator: + raise SkipTest("Skip since simulator does not have defaults.") + self.assertIsNotNone(backend.defaults()) + + def test_backend_configuration(self): + """Check the backend configuration of each backend.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + self.assertIsNotNone(backend.configuration()) + + def test_backend_run(self): + """Check one cannot do backend.run""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + with self.assertRaises(RuntimeError): + backend.run() diff --git a/test/test_integration_interim_results.py b/test/test_integration_interim_results.py new file mode 100644 index 0000000000..a904229e20 --- /dev/null +++ b/test/test_integration_interim_results.py @@ -0,0 +1,200 @@ +# 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. + +"""Tests for job functions using real runtime service.""" + +import time + +from qiskit.providers.jobstatus import JobStatus + +from .ibm_test_case import IBMIntegrationJobTestCase +from .utils.decorators import run_cloud_legacy_real +from .utils.utils import cancel_job_safe, wait_for_status +from .mock.proxy_server import MockProxyServer, use_proxies + + +class TestIntegrationInterimResults(IBMIntegrationJobTestCase): + """Integration tests for interim result functions.""" + + @run_cloud_legacy_real + def test_interim_result_callback(self, service): + """Test interim result callback.""" + + def result_callback(job_id, interim_result): + nonlocal final_it + final_it = interim_result["iteration"] + nonlocal callback_err + if job_id != job.job_id: + callback_err.append(f"Unexpected job ID: {job_id}") + if interim_result["interim_results"] != int_res: + callback_err.append(f"Unexpected interim result: {interim_result}") + + int_res = "foo" + final_it = 0 + callback_err = [] + iterations = 3 + job = self._run_program( + service, + iterations=iterations, + interim_results=int_res, + callback=result_callback, + ) + job.wait_for_final_state() + self.assertEqual(iterations - 1, final_it) + self.assertFalse(callback_err) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_stream_results(self, service): + """Test stream_results method.""" + + def result_callback(job_id, interim_result): + nonlocal final_it + final_it = interim_result["iteration"] + nonlocal callback_err + if job_id != job.job_id: + callback_err.append(f"Unexpected job ID: {job_id}") + if interim_result["interim_results"] != int_res: + callback_err.append(f"Unexpected interim result: {interim_result}") + + int_res = "bar" + final_it = 0 + callback_err = [] + iterations = 3 + job = self._run_program(service, iterations=iterations, interim_results=int_res) + job.stream_results(result_callback) + job.wait_for_final_state() + self.assertEqual(iterations - 1, final_it) + self.assertFalse(callback_err) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_stream_results_done(self, service): + """Test streaming interim results after job is done.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + nonlocal called_back + called_back = True + + called_back = False + job = self._run_program(service, interim_results="foobar") + job.wait_for_final_state() + job._status = JobStatus.RUNNING # Allow stream_results() + job.stream_results(result_callback) + time.sleep(2) + self.assertFalse(called_back) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_retrieve_interim_results(self, service): + """Test retrieving interim results with API endpoint""" + int_res = "foo" + job = self._run_program(service, interim_results=int_res) + job.wait_for_final_state() + interim_results = job.interim_results() + self.assertIn(int_res, interim_results[0]) + + @run_cloud_legacy_real + def test_callback_error(self, service): + """Test error in callback method.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + if interim_result["iteration"] == 0: + raise ValueError("Kaboom!") + nonlocal final_it + final_it = interim_result["iteration"] + + final_it = 0 + iterations = 3 + with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as err_cm: + job = self._run_program( + service, + iterations=iterations, + interim_results="foo", + callback=result_callback, + ) + job.wait_for_final_state() + + self.assertIn("Kaboom", ", ".join(err_cm.output)) + self.assertEqual(iterations - 1, final_it) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_callback_cancel_job(self, service): + """Test canceling a running job while streaming results.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + nonlocal final_it + final_it = interim_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, iterations=10) + + job = self._run_program( + service=service, + iterations=iterations, + 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) + + @run_cloud_legacy_real + def test_websocket_proxy(self, service): + """Test connecting to websocket via proxy.""" + + def result_callback(job_id, interim_result): # pylint: disable=unused-argument + nonlocal callback_called + callback_called = True + + MockProxyServer(self, self.log).start() + callback_called = False + + with use_proxies(service, MockProxyServer.VALID_PROXIES): + job = self._run_program(service, iterations=1, callback=result_callback) + job.wait_for_final_state() + + self.assertTrue(callback_called) + + @run_cloud_legacy_real + def test_websocket_proxy_invalid_port(self, service): + """Test connecting to websocket via invalid proxy port.""" + + def result_callback(job_id, interim_result): # pylint: disable=unused-argument + nonlocal callback_called + callback_called = True + + callback_called = False + invalid_proxy = { + "https": "http://{}:{}".format( + MockProxyServer.PROXY_IP_ADDRESS, MockProxyServer.INVALID_PROXY_PORT + ) + } + # TODO - verify WebsocketError in output log. For some reason self.assertLogs + # doesn't always work even when the error is clearly logged. + with use_proxies(service, invalid_proxy): + job = self._run_program(service, iterations=2, callback=result_callback) + job.wait_for_final_state() + self.assertFalse(callback_called) diff --git a/test/test_integration_job.py b/test/test_integration_job.py new file mode 100644 index 0000000000..ac60746217 --- /dev/null +++ b/test/test_integration_job.py @@ -0,0 +1,248 @@ +# 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. + +"""Tests for job functions using real runtime service.""" + +import time +import random + +from qiskit.providers.jobstatus import JobStatus +from qiskit.test.decorators import slow_test + +from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE +from qiskit_ibm_runtime.exceptions import ( + RuntimeJobFailureError, + RuntimeInvalidStateError, + RuntimeJobNotFound, +) + +from .ibm_test_case import IBMIntegrationJobTestCase +from .utils.decorators import run_cloud_legacy_real +from .utils.serialization import ( + get_complex_types, + SerializableClassDecoder, + SerializableClass, +) +from .utils.utils import cancel_job_safe, wait_for_status, get_real_device + + +class TestIntegrationJob(IBMIntegrationJobTestCase): + """Integration tests for job functions.""" + + @run_cloud_legacy_real + def test_run_program(self, service): + """Test running a program.""" + job = self._run_program(service, final_result="foo") + result = job.result() + self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("foo", result) + + @slow_test + @run_cloud_legacy_real + def test_run_program_real_device(self, service): + """Test running a program.""" + device = get_real_device(service) + job = self._run_program(service, final_result="foo", backend=device) + result = job.result() + self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("foo", result) + + def test_run_program_cloud_no_backend(self): + """Test running a cloud program with no backend.""" + service = [serv for serv in self.services if serv.auth == "cloud"][0] + job = self._run_program(service, backend="") + self.assertTrue(job.backend, f"Job {job.job_id} has no backend.") + + @run_cloud_legacy_real + def test_run_program_log_level(self, service): + """Test running with a custom log level.""" + levels = ["INFO", "ERROR"] + for level in levels: + with self.subTest(level=level): + job = self._run_program(service, log_level=level) + job.wait_for_final_state() + expect_info_msg = level == "INFO" + self.assertEqual( + "info log" in job.logs(), + expect_info_msg, + f"Job log is {job.logs()}", + ) + + @run_cloud_legacy_real + def test_run_program_failed(self, service): + """Test a failed program execution.""" + job = self._run_program(service, inputs={}) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertIn( + API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError) as err_cm: + job.result() + self.assertIn("KeyError", str(err_cm.exception)) + + @run_cloud_legacy_real + def test_run_program_failed_ran_too_long(self, service): + """Test a program that failed since it ran longer than maximum execution time.""" + max_execution_time = 60 + inputs = {"iterations": 1, "sleep_per_iteration": 60} + program_id = self._upload_program( + service, max_execution_time=max_execution_time + ) + job = self._run_program(service, program_id=program_id, inputs=inputs) + + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertIn( + API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( + job.job_id, job_result_raw + ), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_cloud_legacy_real + def test_cancel_job_queued(self, service): + """Test canceling a queued job.""" + real_device = get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + wait_for_status(job, JobStatus.QUEUED) + if not cancel_job_safe(job, self.log): + return + time.sleep(10) # Wait a bit for DB to update. + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_cloud_legacy_real + def test_cancel_job_running(self, service): + """Test canceling a running job.""" + job = self._run_program(service, iterations=3) + wait_for_status(job, JobStatus.RUNNING) + if not cancel_job_safe(job, self.log): + return + time.sleep(10) # Wait a bit for DB to update. + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_cloud_legacy_real + def test_cancel_job_done(self, service): + """Test canceling a finished job.""" + job = self._run_program(service) + job.wait_for_final_state() + with self.assertRaises(RuntimeInvalidStateError): + job.cancel() + + @run_cloud_legacy_real + def test_delete_job(self, service): + """Test deleting a job.""" + sub_tests = [JobStatus.RUNNING, JobStatus.DONE] + for status in sub_tests: + with self.subTest(status=status): + job = self._run_program(service, iterations=2) + wait_for_status(job, status) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) + + @run_cloud_legacy_real + def test_delete_job_queued(self, service): + """Test deleting a queued job.""" + real_device = get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + wait_for_status(job, JobStatus.QUEUED) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) + + @run_cloud_legacy_real + def test_final_result(self, service): + """Test getting final result.""" + final_result = get_complex_types() + job = self._run_program(service, final_result=final_result) + result = job.result(decoder=SerializableClassDecoder) + self.assertEqual(final_result, result) + + rresults = service.job(job.job_id).result(decoder=SerializableClassDecoder) + self.assertEqual(final_result, rresults) + + @run_cloud_legacy_real + def test_job_status(self, service): + """Test job status.""" + job = self._run_program(service, iterations=1) + time.sleep(random.randint(1, 5)) + self.assertTrue(job.status()) + + @run_cloud_legacy_real + def test_job_inputs(self, service): + """Test job inputs.""" + interim_results = get_complex_types() + inputs = {"iterations": 1, "interim_results": interim_results} + job = self._run_program(service, inputs=inputs) + self.assertEqual(inputs, job.inputs) + rjob = service.job(job.job_id) + rinterim_results = rjob.inputs["interim_results"] + self._assert_complex_types_equal(interim_results, rinterim_results) + + @run_cloud_legacy_real + def test_job_backend(self, service): + """Test job backend.""" + job = self._run_program(service) + self.assertEqual(self.sim_backends[service.auth], job.backend.name()) + + @run_cloud_legacy_real + def test_job_program_id(self, service): + """Test job program ID.""" + job = self._run_program(service) + self.assertEqual(self.program_ids[service.auth], job.program_id) + + @run_cloud_legacy_real + def test_wait_for_final_state(self, service): + """Test wait for final state.""" + job = self._run_program(service) + job.wait_for_final_state() + self.assertEqual(JobStatus.DONE, job.status()) + + @run_cloud_legacy_real + def test_job_creation_date(self, service): + """Test job creation date.""" + job = self._run_program(service, iterations=1) + self.assertTrue(job.creation_date) + rjob = service.job(job.job_id) + self.assertTrue(rjob.creation_date) + rjobs = service.jobs(limit=2) + for rjob in rjobs: + self.assertTrue(rjob.creation_date) + + @run_cloud_legacy_real + def test_job_logs(self, service): + """Test job logs.""" + job = self._run_program(service, final_result="foo") + with self.assertLogs("qiskit_ibm_runtime", "WARN"): + job.logs() + job.wait_for_final_state() + job_logs = job.logs() + self.assertIn("this is a stdout message", job_logs) + self.assertIn("this is a stderr message", job_logs) + + def _assert_complex_types_equal(self, expected, received): + """Verify the received data in complex types is expected.""" + if "serializable_class" in received: + received["serializable_class"] = SerializableClass.from_json( + received["serializable_class"] + ) + self.assertEqual(expected, received) diff --git a/test/test_integration_program.py b/test/test_integration_program.py new file mode 100644 index 0000000000..e50c6bdd36 --- /dev/null +++ b/test/test_integration_program.py @@ -0,0 +1,215 @@ +# 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. + +"""Tests for runtime service.""" + +import unittest +import os +import tempfile + +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError +from qiskit_ibm_runtime.runtime_program import RuntimeProgram +from qiskit_ibm_runtime.exceptions import ( + RuntimeProgramNotFound, +) + +from .ibm_test_case import IBMIntegrationTestCase +from .utils.decorators import run_cloud_legacy_real +from .utils.templates import RUNTIME_PROGRAM, PROGRAM_PREFIX + + +class TestIntegrationProgram(IBMIntegrationTestCase): + """Integration tests for runtime modules.""" + + @run_cloud_legacy_real + def test_list_programs(self, service): + """Test listing programs.""" + program_id = self._upload_program(service) + programs = service.programs() + self.assertTrue(programs) + found = False + for prog in programs: + self._validate_program(prog) + if prog.program_id == program_id: + found = True + self.assertTrue(found, f"Program {program_id} not found!") + + @run_cloud_legacy_real + def test_list_programs_with_limit_skip(self, service): + """Test listing programs with limit and skip.""" + for _ in range(4): + self._upload_program(service) + programs = service.programs(limit=3, refresh=True) + all_ids = [prog.program_id for prog in programs] + self.assertEqual(len(all_ids), 3, f"Retrieved programs: {all_ids}") + programs = service.programs(limit=2, skip=1) + some_ids = [prog.program_id for prog in programs] + self.assertEqual(len(some_ids), 2, f"Retrieved programs: {some_ids}") + self.assertNotIn(all_ids[0], some_ids) + self.assertIn(all_ids[1], some_ids) + self.assertIn(all_ids[2], some_ids) + + @run_cloud_legacy_real + def test_list_program(self, service): + """Test listing a single program.""" + program_id = self._upload_program(service) + program = service.program(program_id) + self.assertEqual(program_id, program.program_id) + self._validate_program(program) + + @run_cloud_legacy_real + def test_retrieve_program_data(self, service): + """Test retrieving program data""" + program_id = self._upload_program(service) + program = service.program(program_id) + self.assertEqual(RUNTIME_PROGRAM, program.data) + self._validate_program(program) + + @run_cloud_legacy_real + def test_retrieve_unauthorized_program_data(self, service): + """Test retrieving program data when user is not the program author""" + programs = service.programs() + not_mine = None + for prog in programs: + if prog.is_public: + not_mine = prog + break + if not_mine is None: + self.skipTest("Cannot find a program that's not mine!") + with self.assertRaises(IBMNotAuthorizedError): + return not_mine.data + + @run_cloud_legacy_real + def test_upload_program(self, service): + """Test uploading a program.""" + max_execution_time = 3000 + program_id = self._upload_program( + service, max_execution_time=max_execution_time + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + + @run_cloud_legacy_real + def test_upload_program_file(self, service): + """Test uploading a program using a file.""" + temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) + self.addCleanup(os.remove, temp_fp.name) + temp_fp.write(RUNTIME_PROGRAM) + temp_fp.close() + + program_id = self._upload_program(service, data=temp_fp.name) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + + @unittest.skip("Skip until authorized to upload public on cloud") + @unittest.skipIf( + not os.environ.get("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""), + "Only runs on staging", + ) + @run_cloud_legacy_real + def test_upload_public_program(self, service): + """Test uploading a public program.""" + max_execution_time = 3000 + is_public = True + program_id = self._upload_program( + service, max_execution_time=max_execution_time, is_public=is_public + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + self.assertEqual(program.is_public, is_public) + + @unittest.skip("Skip until authorized to upload public on cloud") + @unittest.skipIf( + not os.environ.get("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""), + "Only runs on staging", + ) + @run_cloud_legacy_real + def test_set_visibility(self, service): + """Test setting the visibility of a program.""" + program_id = self._upload_program(service) + # Get the initial visibility + prog: RuntimeProgram = service.program(program_id) + start_vis = prog.is_public + # Flip the original value + service.set_program_visibility(program_id, not start_vis) + # Get the new visibility + prog: RuntimeProgram = service.program(program_id, refresh=True) + end_vis = prog.is_public + # Verify changed + self.assertNotEqual(start_vis, end_vis) + + @run_cloud_legacy_real + def test_delete_program(self, service): + """Test deleting program.""" + program_id = self._upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.program(program_id, refresh=True) + + @run_cloud_legacy_real + def test_double_delete_program(self, service): + """Test deleting a deleted program.""" + program_id = self._upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.delete_program(program_id) + + @run_cloud_legacy_real + def test_update_program_data(self, service): + """Test updating program data.""" + program_v1 = """ +def main(backend, user_messenger, **kwargs): + return "version 1" + """ + program_v2 = """ +def main(backend, user_messenger, **kwargs): + return "version 2" + """ + program_id = self._upload_program(service, data=program_v1) + self.assertEqual(program_v1, service.program(program_id).data) + service.update_program(program_id=program_id, data=program_v2) + self.assertEqual(program_v2, service.program(program_id).data) + + @run_cloud_legacy_real + def test_update_program_metadata(self, service): + """Test updating program metadata.""" + program_id = self._upload_program(service) + original = service.program(program_id) + new_metadata = { + "name": PROGRAM_PREFIX, + "description": "test_update_program_metadata", + "max_execution_time": original.max_execution_time + 100, + "spec": { + "return_values": {"type": "object", "description": "Some return value"} + }, + } + service.update_program(program_id=program_id, metadata=new_metadata) + updated = service.program(program_id, refresh=True) + self.assertEqual(new_metadata["name"], updated.name) + self.assertEqual(new_metadata["description"], updated.description) + self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) + self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) + + def _validate_program(self, program): + """Validate a program.""" + self.assertTrue(program) + self.assertTrue(program.name) + self.assertTrue(program.program_id) + self.assertTrue(program.description) + self.assertTrue(program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) diff --git a/test/test_integration_retrieve_job.py b/test/test_integration_retrieve_job.py new file mode 100644 index 0000000000..8c4e730370 --- /dev/null +++ b/test/test_integration_retrieve_job.py @@ -0,0 +1,144 @@ +# 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. + +"""Tests for job functions using real runtime service.""" + +import uuid + +from qiskit.providers.jobstatus import JobStatus + +from .ibm_test_case import IBMIntegrationJobTestCase +from .utils.decorators import run_cloud_legacy_real +from .utils.utils import wait_for_status, get_real_device + + +class TestIntegrationRetrieveJob(IBMIntegrationJobTestCase): + """Integration tests for job retrieval functions.""" + + @run_cloud_legacy_real + def test_retrieve_job_queued(self, service): + """Test retrieving a queued job.""" + real_device = get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + wait_for_status(job, JobStatus.QUEUED) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_job_running(self, service): + """Test retrieving a running job.""" + job = self._run_program(service, iterations=10) + wait_for_status(job, JobStatus.RUNNING) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_job_done(self, service): + """Test retrieving a finished job.""" + job = self._run_program(service) + job.wait_for_final_state() + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_all_jobs(self, service): + """Test retrieving all jobs.""" + job = self._run_program(service) + rjobs = service.jobs() + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + self.assertTrue(found, f"Job {job.job_id} not returned.") + + @run_cloud_legacy_real + def test_retrieve_jobs_limit(self, service): + """Test retrieving jobs with limit.""" + jobs = [] + for _ in range(3): + jobs.append(self._run_program(service)) + + rjobs = service.jobs(limit=2, program_id=self.program_ids[service.auth]) + self.assertEqual(len(rjobs), 2, f"Retrieved jobs: {[j.job_id for j in rjobs]}") + job_ids = {job.job_id for job in jobs} + rjob_ids = {rjob.job_id for rjob in rjobs} + self.assertTrue( + rjob_ids.issubset(job_ids), f"Submitted: {job_ids}, Retrieved: {rjob_ids}" + ) + + @run_cloud_legacy_real + def test_retrieve_pending_jobs(self, service): + """Test retrieving pending jobs (QUEUED, RUNNING).""" + job = self._run_program(service, iterations=10) + wait_for_status(job, JobStatus.RUNNING) + rjobs = service.jobs(pending=True) + after_status = job.status() + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + + self.assertTrue( + found or after_status == JobStatus.RUNNING, + f"Pending job {job.job_id} not retrieved.", + ) + + @run_cloud_legacy_real + def test_retrieve_returned_jobs(self, service): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" + job = self._run_program(service) + job.wait_for_final_state() + rjobs = service.jobs(pending=False) + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") + + @run_cloud_legacy_real + def test_retrieve_jobs_by_program_id(self, service): + """Test retrieving jobs by Program ID.""" + program_id = self._upload_program(service) + job = self._run_program(service, program_id=program_id) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs), f"Retrieved jobs: {[j.job_id for j in rjobs]}") + + def test_jobs_filter_by_hgp(self): + """Test retrieving jobs by hgp.""" + service = [serv for serv in self.services if serv.auth == "legacy"][0] + default_hgp = list(service._hgps.keys())[0] + program_id = self._upload_program(service) + job = self._run_program(service, program_id=program_id) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id, instance=default_hgp) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs), f"Retrieved jobs: {[j.job_id for j in rjobs]}") + + uuid_ = uuid.uuid4().hex + fake_hgp = f"{uuid_}/{uuid_}/{uuid_}" + rjobs = service.jobs(program_id=program_id, instance=fake_hgp) + self.assertFalse(rjobs) diff --git a/test/test_job_retrieval.py b/test/test_job_retrieval.py new file mode 100644 index 0000000000..c7c96bfa70 --- /dev/null +++ b/test/test_job_retrieval.py @@ -0,0 +1,256 @@ +# 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. + +"""Tests for runtime job retrieval.""" + +from qiskit_ibm_runtime.exceptions import IBMInputValueError + +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.program import run_program, upload_program +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestRetrieveJobs(IBMTestCase): + """Class for testing job retrieval.""" + + def setUp(self): + """Initial test setup.""" + super().setUp() + self._legacy_service = FakeRuntimeService(auth="legacy", token="my_token") + + @run_legacy_and_cloud_fake + def test_retrieve_job(self, service): + """Test retrieving a job.""" + program_id = upload_program(service) + params = {"param1": "foo"} + job = run_program(service=service, program_id=program_id, inputs=params) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(program_id, rjob.program_id) + + @run_legacy_and_cloud_fake + def test_jobs_no_limit(self, service): + """Test retrieving jobs without limit.""" + program_id = upload_program(service) + + jobs = [] + for _ in range(25): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(limit=None) + self.assertEqual(25, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_limit(self, service): + """Test retrieving jobs with limit.""" + program_id = upload_program(service) + + jobs = [] + job_count = 25 + for _ in range(job_count): + jobs.append(run_program(service, program_id)) + + limits = [21, 30] + for limit in limits: + with self.subTest(limit=limit): + rjobs = service.jobs(limit=limit) + self.assertEqual(min(limit, job_count), len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_skip(self, service): + """Test retrieving jobs with skip.""" + program_id = upload_program(service) + + jobs = [] + for _ in range(5): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(skip=4) + self.assertEqual(1, len(rjobs)) + + def test_jobs_skip_limit(self): + """Test retrieving jobs with skip and limit.""" + service = self._legacy_service + program_id = upload_program(service) + + jobs = [] + for _ in range(10): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(skip=4, limit=2) + self.assertEqual(2, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_pending(self, service): + """Test retrieving pending jobs (QUEUED, RUNNING).""" + program_id = upload_program(service) + + _, pending_jobs_count, _ = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + rjobs = service.jobs(pending=True) + self.assertEqual(pending_jobs_count, len(rjobs)) + + def test_jobs_limit_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with limit.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 4 + rjobs = service.jobs(limit=limit, pending=True) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_skip_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with skip.""" + service = self._legacy_service + program_id = upload_program(service) + + _, pending_jobs_count, _ = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + skip = 4 + rjobs = service.jobs(skip=skip, pending=True) + self.assertEqual(pending_jobs_count - skip, len(rjobs)) + + def test_jobs_limit_skip_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with limit and skip.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 2 + skip = 3 + rjobs = service.jobs(limit=limit, skip=skip, pending=True) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" + service = self._legacy_service + program_id = upload_program(service) + + _, _, returned_jobs_count = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + rjobs = service.jobs(pending=False) + self.assertEqual(returned_jobs_count, len(rjobs)) + + def test_jobs_limit_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 6 + rjobs = service.jobs(limit=limit, pending=False) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_skip_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with skip.""" + service = self._legacy_service + program_id = upload_program(service) + + _, _, returned_jobs_count = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + skip = 4 + rjobs = service.jobs(skip=skip, pending=False) + self.assertEqual(returned_jobs_count - skip, len(rjobs)) + + def test_jobs_limit_skip_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit and skip.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 6 + skip = 2 + rjobs = service.jobs(limit=limit, skip=skip, pending=False) + self.assertEqual(limit, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_filter_by_program_id(self, service): + """Test retrieving jobs by Program ID.""" + program_id = upload_program(service) + program_id_1 = upload_program(service) + + job = run_program(service=service, program_id=program_id) + job_1 = run_program(service=service, program_id=program_id_1) + job.wait_for_final_state() + job_1.wait_for_final_state() + rjobs = service.jobs(program_id=program_id) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + + def test_jobs_filter_by_instance(self): + """Test retrieving jobs by instance.""" + service = self._legacy_service + program_id = upload_program(service) + instance = FakeRuntimeService.DEFAULT_HGPS[1] + + job = run_program(service=service, program_id=program_id, instance=instance) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id, instance=instance) + self.assertTrue(rjobs) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + rjobs = service.jobs( + program_id=program_id, instance="nohub1/nogroup1/noproject1" + ) + self.assertFalse(rjobs) + + def test_jobs_bad_instance(self): + """Test retrieving jobs with bad instance values.""" + service = self._legacy_service + with self.assertRaises(IBMInputValueError): + _ = service.jobs(instance="foo") + + def test_different_hgps(self): + """Test retrieving job submitted with different hgp.""" + # Initialize with hgp0 + service = FakeRuntimeService( + auth="legacy", + token="some_token", + instance=FakeRuntimeService.DEFAULT_HGPS[0], + ) + program_id = upload_program(service) + + # Run with hgp1 backend. + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + job = run_program(service, program_id=program_id, backend_name=backend_name) + + rjob = service.job(job.job_id) + self.assertIsNotNone(rjob.backend) + + def _populate_jobs_with_all_statuses(self, service, program_id): + """Populate the database with jobs of all statuses.""" + jobs = [] + pending_jobs_count = 0 + returned_jobs_count = 0 + status_count = { + "RUNNING": 3, + "COMPLETED": 4, + "QUEUED": 2, + "FAILED": 3, + "CANCELLED": 2, + } + pending_status = ["RUNNING", "QUEUED"] + for stat, count in status_count.items(): + for _ in range(count): + jobs.append( + run_program( + service=service, program_id=program_id, final_status=stat + ) + ) + if stat in pending_status: + pending_jobs_count += 1 + else: + returned_jobs_count += 1 + return jobs, pending_jobs_count, returned_jobs_count diff --git a/test/test_jobs.py b/test/test_jobs.py new file mode 100644 index 0000000000..0929f38002 --- /dev/null +++ b/test/test_jobs.py @@ -0,0 +1,270 @@ +# 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. + +"""Tests for job related runtime functions.""" + +import time +import random + +from qiskit.providers.jobstatus import JobStatus +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from qiskit_ibm_runtime import RuntimeJob +from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE +from qiskit_ibm_runtime.exceptions import ( + RuntimeJobFailureError, + RuntimeJobNotFound, + RuntimeProgramNotFound, + IBMInputValueError, +) + +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_client import ( + FailedRuntimeJob, + FailedRanTooLongRuntimeJob, + CancelableRuntimeJob, + CustomResultRuntimeJob, +) +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.program import run_program, upload_program +from .utils.serialization import get_complex_types +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestRuntimeJob(IBMTestCase): + """Class for testing runtime jobs.""" + + @run_legacy_and_cloud_fake + def test_run_program(self, service): + """Test running program.""" + params = {"param1": "foo"} + job = run_program(service=service, inputs=params) + self.assertTrue(job.job_id) + self.assertIsInstance(job, RuntimeJob) + self.assertIsInstance(job.status(), JobStatus) + self.assertEqual(job.inputs, params) + job.wait_for_final_state() + self.assertEqual(job.status(), JobStatus.DONE) + self.assertTrue(job.result()) + + @run_legacy_and_cloud_fake + def test_run_phantom_program(self, service): + """Test running a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + _ = run_program(service=service, program_id="phantom_program") + + @run_legacy_and_cloud_fake + def test_run_program_phantom_backend(self, service): + """Test running on a phantom backend.""" + with self.assertRaises(QiskitBackendNotFoundError): + _ = run_program(service=service, backend_name="phantom_backend") + + def test_run_program_missing_backend_legacy(self): + """Test running a legacy program with no backend.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, backend_name="") + + def test_run_program_missing_backend_cloud(self): + """Test running a cloud program with no backend.""" + service = FakeRuntimeService(auth="cloud", token="my_token", instance="crn:123") + job = run_program(service=service, backend_name="") + self.assertTrue(job.backend) + + def test_run_program_default_hgp_backend(self): + """Test running a program with a backend in default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_COMMON_BACKEND + default_hgp = list(service._hgps.values())[0] + self.assertIn(backend, default_hgp.backends.keys()) + job = run_program(service=service, backend_name=backend) + self.assertEqual(job.backend.name(), backend) + self.assertEqual( + job.backend._api_client.hgp, FakeRuntimeService.DEFAULT_HGPS[0] + ) + + def test_run_program_non_default_hgp_backend(self): + """Test running a program with a backend in non-default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + default_hgp = list(service._hgps.values())[0] + self.assertNotIn(backend, default_hgp.backends.keys()) + job = run_program(service=service, backend_name=backend) + self.assertEqual(job.backend.name(), backend) + + def test_run_program_by_hgp_backend(self): + """Test running a program with both backend and hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_COMMON_BACKEND + non_default_hgp = list(service._hgps.keys())[1] + job = run_program( + service=service, backend_name=backend, instance=non_default_hgp + ) + self.assertEqual(job.backend.name(), backend) + self.assertEqual(job.backend._api_client.hgp, non_default_hgp) + + def test_run_program_by_hgp_bad_backend(self): + """Test running a program with backend not in hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + default_hgp = list(service._hgps.values())[0] + self.assertNotIn(backend, default_hgp.backends.keys()) + with self.assertRaises(QiskitBackendNotFoundError): + _ = run_program( + service=service, backend_name=backend, instance=default_hgp.name + ) + + def test_run_program_by_phantom_hgp(self): + """Test running a program with a phantom hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, instance="h/g/p") + + def test_run_program_by_bad_hgp(self): + """Test running a program with a bad hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, instance="foo") + + @run_legacy_and_cloud_fake + def test_run_program_with_custom_runtime_image(self, service): + """Test running program with a custom image.""" + params = {"param1": "foo"} + image = "name:tag" + job = run_program(service=service, inputs=params, image=image) + self.assertTrue(job.job_id) + self.assertIsInstance(job, RuntimeJob) + self.assertIsInstance(job.status(), JobStatus) + self.assertEqual(job.inputs, params) + job.wait_for_final_state() + self.assertEqual(job.status(), JobStatus.DONE) + self.assertTrue(job.result()) + self.assertEqual(job.image, image) + + @run_legacy_and_cloud_fake + def test_run_program_with_custom_log_level(self, service): + """Test running program with a custom image.""" + job = run_program(service=service, log_level="DEBUG") + job_raw = service._api_client._get_job(job.job_id) + self.assertEqual(job_raw.log_level, "DEBUG") + + @run_legacy_and_cloud_fake + def test_run_program_failed(self, service): + """Test a failed program execution.""" + job = run_program(service=service, job_classes=FailedRuntimeJob) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual( + API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_legacy_and_cloud_fake + def test_run_program_failed_ran_too_long(self, service): + """Test a program that failed since it ran longer than maximum execution time.""" + job = run_program(service=service, job_classes=FailedRanTooLongRuntimeJob) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual( + API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( + job.job_id, job_result_raw + ), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_legacy_and_cloud_fake + def test_program_params_namespace(self, service): + """Test running a program using parameter namespace.""" + program_id = upload_program(service) + params = service.program(program_id).parameters() + params.param1 = "Hello World" + run_program(service, program_id, inputs=params) + + @run_legacy_and_cloud_fake + def test_cancel_job(self, service): + """Test canceling a job.""" + job = run_program(service, job_classes=CancelableRuntimeJob) + time.sleep(1) + job.cancel() + self.assertEqual(job.status(), JobStatus.CANCELLED) + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_legacy_and_cloud_fake + def test_final_result(self, service): + """Test getting final result.""" + job = run_program(service) + result = job.result() + self.assertTrue(result) + + @run_legacy_and_cloud_fake + def test_interim_results(self, service): + """Test getting interim results.""" + job = run_program(service) + # TODO maybe a bit more validation on the returned interim results + interim_results = job.interim_results() + self.assertTrue(interim_results) + + @run_legacy_and_cloud_fake + def test_job_status(self, service): + """Test job status.""" + job = run_program(service) + time.sleep(random.randint(1, 5)) + self.assertTrue(job.status()) + + @run_legacy_and_cloud_fake + def test_job_inputs(self, service): + """Test job inputs.""" + inputs = {"param1": "foo", "param2": "bar"} + job = run_program(service, inputs=inputs) + self.assertEqual(inputs, job.inputs) + + @run_legacy_and_cloud_fake + def test_job_program_id(self, service): + """Test job program ID.""" + program_id = upload_program(service) + job = run_program(service, program_id=program_id) + self.assertEqual(program_id, job.program_id) + + @run_legacy_and_cloud_fake + def test_wait_for_final_state(self, service): + """Test wait for final state.""" + job = run_program(service) + job.wait_for_final_state() + self.assertEqual(JobStatus.DONE, job.status()) + + @run_legacy_and_cloud_fake + def test_get_result_twice(self, service): + """Test getting results multiple times.""" + custom_result = get_complex_types() + job_cls = CustomResultRuntimeJob + job_cls.custom_result = custom_result + + job = run_program(service=service, job_classes=job_cls) + _ = job.result() + _ = job.result() + + @run_legacy_and_cloud_fake + def test_delete_job(self, service): + """Test deleting a job.""" + params = {"param1": "foo"} + job = run_program(service=service, inputs=params) + self.assertTrue(job.job_id) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) diff --git a/test/ibm/test_jupyter.py b/test/test_jupyter.py similarity index 95% rename from test/ibm/test_jupyter.py rename to test/test_jupyter.py index c65b4a62b3..bc18263584 100644 --- a/test/ibm/test_jupyter.py +++ b/test/test_jupyter.py @@ -12,6 +12,8 @@ """Tests for Jupyter tools.""" +import unittest + from qiskit_ibm_runtime.jupyter.qubits_widget import qubits_tab from qiskit_ibm_runtime.jupyter.config_widget import config_tab from qiskit_ibm_runtime.jupyter.gates_widget import gates_tab @@ -19,10 +21,11 @@ from qiskit_ibm_runtime.jupyter.dashboard.backend_widget import make_backend_widget from qiskit_ibm_runtime.jupyter.dashboard.utils import BackendWithProviders -from ..decorators import requires_provider -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_provider +@unittest.skip("Skip until jupyter is done") class TestBackendInfo(IBMTestCase): """Test backend information Jupyter widget.""" @@ -70,6 +73,7 @@ def test_error_map_tab(self): iplot_error_map(backend) +@unittest.skip("Skip until jupyter is done") class TestIBMDashboard(IBMTestCase): """Test backend information Jupyter widget.""" diff --git a/test/ibm/test_ibm_logger.py b/test/test_logger.py similarity index 99% rename from test/ibm/test_ibm_logger.py rename to test/test_logger.py index 8a44b1775d..c4810b0ab6 100644 --- a/test/ibm/test_ibm_logger.py +++ b/test/test_logger.py @@ -20,7 +20,7 @@ from qiskit_ibm_runtime import QISKIT_IBM_RUNTIME_LOG_LEVEL, QISKIT_IBM_RUNTIME_LOG_FILE from qiskit_ibm_runtime.utils.utils import setup_logger -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase class TestLogger(IBMTestCase): diff --git a/test/test_programs.py b/test/test_programs.py new file mode 100644 index 0000000000..4f63e5358e --- /dev/null +++ b/test/test_programs.py @@ -0,0 +1,260 @@ +# 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. + +"""Tests for program related runtime functions.""" + +import copy +import json +import os +from io import StringIO +from unittest.mock import patch +import warnings +import tempfile + +from qiskit_ibm_runtime.exceptions import IBMInputValueError +from qiskit_ibm_runtime.exceptions import RuntimeProgramNotFound +from qiskit_ibm_runtime.runtime_program import ParameterNamespace + +from .ibm_test_case import IBMTestCase +from .utils.program import upload_program, DEFAULT_DATA, DEFAULT_METADATA +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestPrograms(IBMTestCase): + """Class for testing runtime modules.""" + + @run_legacy_and_cloud_fake + def test_list_programs(self, service): + """Test listing programs.""" + program_id = upload_program(service) + programs = service.programs() + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + + @run_legacy_and_cloud_fake + def test_list_programs_with_limit_skip(self, service): + """Test listing programs with limit and skip.""" + program_ids = [] + for _ in range(3): + program_ids.append(upload_program(service)) + programs = service.programs(limit=2, skip=1) + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_ids[0], all_ids) + self.assertIn(program_ids[1], all_ids) + self.assertIn(program_ids[2], all_ids) + programs = service.programs(limit=3) + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_ids[0], all_ids) + + @run_legacy_and_cloud_fake + def test_list_program(self, service): + """Test listing a single program.""" + program_id = upload_program(service) + program = service.program(program_id) + self.assertEqual(program_id, program.program_id) + + @run_legacy_and_cloud_fake + def test_print_programs(self, service): + """Test printing programs.""" + ids = [] + for idx in range(3): + ids.append(upload_program(service, name=f"name_{idx}")) + + programs = service.programs() + with patch("sys.stdout", new=StringIO()) as mock_stdout: + service.pprint_programs() + stdout = mock_stdout.getvalue() + for prog in programs: + self.assertIn(prog.program_id, stdout) + self.assertIn(prog.name, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) + self.assertNotIn("Backend requirements", stdout) + service.pprint_programs(detailed=True) + stdout_detailed = mock_stdout.getvalue() + for prog in programs: + self.assertIn(prog.program_id, stdout_detailed) + self.assertIn(prog.name, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) + self.assertIn("Backend requirements", stdout_detailed) + + @run_legacy_and_cloud_fake + def test_upload_program(self, service): + """Test uploading a program.""" + max_execution_time = 3000 + is_public = True + program_id = upload_program( + service=service, max_execution_time=max_execution_time, is_public=is_public + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + self.assertEqual(program.is_public, is_public) + + @run_legacy_and_cloud_fake + def test_update_program(self, service): + """Test updating program.""" + new_data = "def main() {foo=bar}" + new_metadata = copy.deepcopy(DEFAULT_METADATA) + new_metadata["name"] = "test_update_program" + new_name = "name2" + new_description = "some other description" + new_cost = DEFAULT_METADATA["max_execution_time"] + 100 + new_spec = copy.deepcopy(DEFAULT_METADATA["spec"]) + new_spec["backend_requirements"] = {"input_allowed": "runtime"} + + sub_tests = [ + {"data": new_data}, + {"metadata": new_metadata}, + {"data": new_data, "metadata": new_metadata}, + {"metadata": new_metadata, "name": new_name}, + { + "data": new_data, + "metadata": new_metadata, + "description": new_description, + }, + {"max_execution_time": new_cost, "spec": new_spec}, + ] + + for new_vals in sub_tests: + with self.subTest(new_vals=new_vals.keys()): + program_id = upload_program(service) + service.update_program(program_id=program_id, **new_vals) + updated = service.program(program_id, refresh=True) + if "data" in new_vals: + raw_program = service._api_client.program_get(program_id) + self.assertEqual(new_data, raw_program["data"]) + if "metadata" in new_vals and "name" not in new_vals: + self.assertEqual(new_metadata["name"], updated.name) + if "name" in new_vals: + self.assertEqual(new_name, updated.name) + if "description" in new_vals: + self.assertEqual(new_description, updated.description) + if "max_execution_time" in new_vals: + self.assertEqual(new_cost, updated.max_execution_time) + if "spec" in new_vals: + raw_program = service._api_client.program_get(program_id) + self.assertEqual(new_spec, raw_program["spec"]) + + @run_legacy_and_cloud_fake + def test_update_program_no_new_fields(self, service): + """Test updating a program without any new data.""" + program_id = upload_program(service) + with warnings.catch_warnings(record=True) as warn_cm: + service.update_program(program_id=program_id) + self.assertEqual(len(warn_cm), 1) + + @run_legacy_and_cloud_fake + def test_update_phantom_program(self, service): + """Test updating a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + service.update_program("phantom_program", name="foo") + + @run_legacy_and_cloud_fake + def test_delete_program(self, service): + """Test deleting program.""" + program_id = upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.program(program_id, refresh=True) + + @run_legacy_and_cloud_fake + def test_double_delete_program(self, service): + """Test deleting a deleted program.""" + program_id = upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.delete_program(program_id) + + @run_legacy_and_cloud_fake + def test_retrieve_program_data(self, service): + """Test retrieving program data""" + program_id = upload_program(service, name="qiskit-test") + service.programs() + program = service.program(program_id) + self.assertEqual(program.data, DEFAULT_DATA) + self._validate_program(program) + + @run_legacy_and_cloud_fake + def test_program_params_validation(self, service): + """Test program parameters validation process""" + program_id = upload_program(service) + program = service.program(program_id) + params: ParameterNamespace = program.parameters() + params.param1 = "Hello, World" + # Check OK params + params.validate() + # Check OK params - contains unnecessary param + params.param3 = "Hello, World" + params.validate() + # Check bad params - missing required param + params.param1 = None + with self.assertRaises(IBMInputValueError): + params.validate() + params.param1 = "foo" + + @run_legacy_and_cloud_fake + def test_program_metadata(self, service): + """Test program metadata.""" + temp_fp = tempfile.NamedTemporaryFile(mode="w+", delete=False) + json.dump(DEFAULT_METADATA, temp_fp) + temp_fp.close() + + sub_tests = [temp_fp.name, DEFAULT_METADATA] + try: + for metadata in sub_tests: + with self.subTest(metadata_type=type(metadata)): + program_id = service.upload_program( + data=DEFAULT_DATA, metadata=metadata + ) + program = service.program(program_id) + service.delete_program(program_id) + self._validate_program(program) + finally: + os.remove(temp_fp.name) + + @run_legacy_and_cloud_fake + def test_set_program_visibility(self, service): + """Test setting program visibility.""" + program_id = upload_program(service, is_public=False) + service.set_program_visibility(program_id, True) + program = service.program(program_id) + self.assertTrue(program.is_public) + + @run_legacy_and_cloud_fake + def test_set_program_visibility_phantom_program(self, service): + """Test setting program visibility for a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + service.set_program_visibility("foo", True) + + def _validate_program(self, program): + """Validate a program.""" + self.assertEqual(DEFAULT_METADATA["name"], program.name) + self.assertEqual(DEFAULT_METADATA["description"], program.description) + self.assertEqual( + DEFAULT_METADATA["max_execution_time"], program.max_execution_time + ) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) + self.assertEqual( + DEFAULT_METADATA["spec"]["backend_requirements"], + program.backend_requirements, + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["parameters"], program.parameters().metadata + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["return_values"], program.return_values + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["interim_results"], program.interim_results + ) diff --git a/test/ibm/test_proxies.py b/test/test_proxies.py similarity index 57% rename from test/ibm/test_proxies.py rename to test/test_proxies.py index 36bd72d336..c2cc2dfa12 100644 --- a/test/ibm/test_proxies.py +++ b/test/test_proxies.py @@ -10,19 +10,21 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Tests for the AuthClient and VersionClient proxy support.""" +"""Tests for the proxy support.""" -import urllib import subprocess +import urllib from requests.exceptions import ProxyError from qiskit_ibm_runtime import IBMRuntimeService +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 -from qiskit_ibm_runtime.credentials import Credentials -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_qe_access +from qiskit_ibm_runtime.proxies import ProxyConfiguration +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_qe_access, requires_cloud_service ADDRESS = "127.0.0.1" PORT = 8085 @@ -53,9 +55,44 @@ def tearDown(self): # wait for the process to terminate self.proxy_process.wait() + @requires_cloud_service + def test_proxies_cloud_runtime_client(self, service, instance): + """Should reach the proxy using RuntimeClient.""" + # pylint: disable=unused-argument + params = service._client_params + params.proxies = ProxyConfiguration(urls=VALID_PROXIES) + client = RuntimeClient(params) + client.list_programs(limit=1) + api_line = pproxy_desired_access_log_line(params.url) + self.proxy_process.terminate() # kill to be able of reading the output + proxy_output = self.proxy_process.stdout.read().decode("utf-8") + self.assertIn(api_line, proxy_output) + + @requires_qe_access + def test_proxies_legacy_runtime_client(self, qe_token, qe_url): + """Should reach the proxy using RuntimeClient.""" + service = IBMRuntimeService( + auth="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": VALID_PROXIES}, + ) + service.programs(limit=1) + + auth_line = pproxy_desired_access_log_line(qe_url) + api_line = list(service._hgps.values())[0]._api_client._session.base_url + api_line = pproxy_desired_access_log_line(api_line) + self.proxy_process.terminate() # kill to be able of reading the output + proxy_output = self.proxy_process.stdout.read().decode("utf-8") + + # Check if the authentication call went through proxy. + self.assertIn(auth_line, proxy_output) + # Check if the API call (querying providers list) went through proxy. + self.assertIn(api_line, proxy_output) + @requires_qe_access - def test_proxies_ibm_account(self, qe_token, qe_url): - """Should reach the proxy using account.enable.""" + def test_proxies_account_client(self, qe_token, qe_url): + """Should reach the proxy using AccountClient.""" service = IBMRuntimeService( auth="legacy", token=qe_token, @@ -66,7 +103,8 @@ def test_proxies_ibm_account(self, qe_token, qe_url): self.proxy_process.terminate() # kill to be able of reading the output auth_line = pproxy_desired_access_log_line(qe_url) - api_line = pproxy_desired_access_log_line(service._default_hgp.credentials.url) + api_line = list(service._hgps.values())[0]._api_client._session.base_url + api_line = pproxy_desired_access_log_line(api_line) proxy_output = self.proxy_process.stdout.read().decode("utf-8") # Check if the authentication call went through proxy. @@ -78,8 +116,14 @@ def test_proxies_ibm_account(self, qe_token, qe_url): def test_proxies_authclient(self, qe_token, qe_url): """Should reach the proxy using AuthClient.""" pproxy_desired_access_log_line_ = pproxy_desired_access_log_line(qe_url) + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls=VALID_PROXIES), + ) - _ = AuthClient(qe_token, qe_url, proxies=VALID_PROXIES) + _ = AuthClient(params) self.proxy_process.terminate() # kill to be able of reading the output self.assertIn( @@ -102,11 +146,31 @@ def test_proxies_versionclient(self, qe_token, qe_url): self.proxy_process.stdout.read().decode("utf-8"), ) + @requires_qe_access + def test_invalid_proxy_port_runtime_client(self, qe_token, qe_url): + """Should raise RequestApiError with ProxyError using RuntimeClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), + ) + with self.assertRaises(RequestsApiError) as context_manager: + client = RuntimeClient(params) + client.list_programs(limit=1) + self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access def test_invalid_proxy_port_authclient(self, qe_token, qe_url): """Should raise RequestApiError with ProxyError using AuthClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), + ) with self.assertRaises(RequestsApiError) as context_manager: - _ = AuthClient(qe_token, qe_url, proxies=INVALID_PORT_PROXIES) + _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -120,11 +184,32 @@ def test_invalid_proxy_port_versionclient(self, qe_token, qe_url): self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access + def test_invalid_proxy_address_runtime_client(self, qe_token, qe_url): + """Should raise RequestApiError with ProxyError using RuntimeClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), + ) + with self.assertRaises(RequestsApiError) as context_manager: + client = RuntimeClient(params) + client.list_programs(limit=1) + + self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access def test_invalid_proxy_address_authclient(self, qe_token, qe_url): """Should raise RequestApiError with ProxyError using AuthClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), + ) with self.assertRaises(RequestsApiError) as context_manager: - _ = AuthClient(qe_token, qe_url, proxies=INVALID_ADDRESS_PROXIES) + _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -144,16 +229,18 @@ def test_proxy_urls(self, qe_token, qe_url): test_urls = [ "http://{}:{}".format(ADDRESS, PORT), "//{}:{}".format(ADDRESS, PORT), - "http:{}:{}".format(ADDRESS, PORT), "http://user:123@{}:{}".format(ADDRESS, PORT), ] for proxy_url in test_urls: with self.subTest(proxy_url=proxy_url): - credentials = Credentials( - qe_token, qe_url, proxies={"urls": {"https": proxy_url}} + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies=ProxyConfiguration(urls={"https": proxy_url}), ) version_finder = VersionClient( - credentials.base_url, **credentials.connection_parameters() + params.url, **params.connection_parameters() ) version_finder.version() diff --git a/test/ibm/runtime/test_runtime_ws.py b/test/test_runtime_ws.py similarity index 94% rename from test/ibm/runtime/test_runtime_ws.py rename to test/test_runtime_ws.py index 6c0499f1f5..b819800f85 100644 --- a/test/ibm/runtime/test_runtime_ws.py +++ b/test/test_runtime_ws.py @@ -16,13 +16,13 @@ from qiskit.test.mock.fake_qasm_simulator import FakeQasmSimulator -from qiskit_ibm_runtime.credentials import Credentials from qiskit_ibm_runtime import RuntimeJob from qiskit_ibm_runtime.exceptions import RuntimeInvalidStateError +from qiskit_ibm_runtime.api.client_parameters import ClientParameters -from ...ibm_test_case import IBMTestCase -from ...ws_server import MockWsServer -from .ws_handler import ( +from .ibm_test_case import IBMTestCase +from .mock.ws_server import MockWsServer +from .mock.ws_handler import ( websocket_handler, JOB_ID_PROGRESS_DONE, JOB_ID_ALREADY_DONE, @@ -30,7 +30,7 @@ JOB_ID_RETRY_FAILURE, JOB_PROGRESS_RESULT_COUNT, ) -from .fake_runtime_client import BaseFakeRuntimeClient +from .mock.fake_runtime_client import BaseFakeRuntimeClient class TestRuntimeWebsocketClient(IBMTestCase): @@ -187,13 +187,13 @@ def result_callback(job_id, interim_result): def _get_job(self, callback=None, job_id=JOB_ID_PROGRESS_DONE): """Get a runtime job.""" - cred = Credentials( - token="my_token", url="", services={"runtime": MockWsServer.VALID_WS_URL} + params = ClientParameters( + auth_type="legacy", token="my_token", url=MockWsServer.VALID_WS_URL ) job = RuntimeJob( backend=FakeQasmSimulator(), api_client=BaseFakeRuntimeClient(), - credentials=cred, + client_params=params, job_id=job_id, program_id="my-program", user_callback=callback, diff --git a/test/ibm/test_tutorials.py b/test/test_tutorials.py similarity index 95% rename from test/ibm/test_tutorials.py rename to test/test_tutorials.py index 44c938a5e0..49f460a69d 100644 --- a/test/ibm/test_tutorials.py +++ b/test/test_tutorials.py @@ -12,7 +12,7 @@ """Tests for the tutorials, copied from ``qiskit-iqx-tutorials``.""" -from unittest import skipIf +from unittest import skipIf, skip import os import glob import warnings @@ -24,7 +24,7 @@ from qiskit_ibm_runtime.utils.utils import to_python_identifier -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase TUTORIAL_PATH = "docs/tutorials/**/*.ipynb" @@ -51,6 +51,7 @@ def test_function(self): return type.__new__(mcs, name, bases, dict_) +@skip("Skip until we have tutorials") @skipIf(not TEST_OPTIONS["run_slow"], "Skipping slow tests.") class TestTutorials(IBMTestCase, metaclass=TutorialsTestCaseMeta): """Tests for tutorials.""" diff --git a/test/ibm/__init__.py b/test/utils/__init__.py similarity index 92% rename from test/ibm/__init__.py rename to test/utils/__init__.py index 1fa91ead63..c576b47d5b 100644 --- a/test/ibm/__init__.py +++ b/test/utils/__init__.py @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Tests for IBM Quantum Provider.""" +"""Test utility functions.""" diff --git a/test/contextmanagers.py b/test/utils/account.py similarity index 56% rename from test/contextmanagers.py rename to test/utils/account.py index c58f7bb542..a649d7ad9e 100644 --- a/test/contextmanagers.py +++ b/test/utils/account.py @@ -12,16 +12,15 @@ """Context managers for using with IBM Provider unit tests.""" +import json import os -from contextlib import ContextDecorator, contextmanager -from typing import Optional, Dict +import uuid +from contextlib import ContextDecorator +from tempfile import NamedTemporaryFile from unittest.mock import patch -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime.credentials import Credentials -from qiskit_ibm_runtime.credentials.environ import VARIABLES_MAP - -CREDENTIAL_ENV_VARS = VARIABLES_MAP.keys() +from qiskit_ibm_runtime.accounts import management +from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL class custom_envs(ContextDecorator): @@ -99,34 +98,64 @@ def side_effect(self, filename_): return self.isfile_original(filename_) -def _mock_initialize_hgps( - self, credentials: Credentials, preferences: Optional[Dict] = None -) -> None: - """Mock ``_initialize_hgps()``, just storing the credentials.""" - hgp = dict() - hgp["credentials"] = credentials - self._hgp = hgp - self._hgps = {} - if preferences: - credentials.preferences = preferences.get(credentials.unique_id(), {}) - - -@contextmanager -def mock_ibm_provider(): - """Mock the initialization of ``IBMRuntimeService``, so it does not query the API.""" - patcher = patch.object( - IBMRuntimeService, - "_initialize_hgps", - side_effect=_mock_initialize_hgps, - autospec=True, - ) - patcher2 = patch.object( - IBMRuntimeService, - "_check_api_version", - return_value={"new_api": True, "api-auth": "0.1"}, - ) - patcher.start() - patcher2.start() - yield - patcher2.stop() - patcher.stop() +class temporary_account_config_file(ContextDecorator): + """Context manager that uses a temporary qiskitrc.""" + + # pylint: disable=invalid-name + + def __init__(self, contents=None, **kwargs): + # Create a temporary file with the contents. + contents = ( + contents if contents is not None else get_account_config_contents(**kwargs) + ) + + self.tmp_file = NamedTemporaryFile(mode="w+") + json.dump(contents, self.tmp_file) + self.tmp_file.flush() + self.account_config_json_backup = management._DEFAULT_ACCOUNT_CONFIG_JSON_FILE + + def __enter__(self): + # Temporarily modify the default location of the configuration file. + management._DEFAULT_ACCOUNT_CONFIG_JSON_FILE = self.tmp_file.name + return self + + def __exit__(self, *exc): + # Delete the temporary file and restore the default location. + self.tmp_file.close() + management._DEFAULT_ACCOUNT_CONFIG_JSON_FILE = self.account_config_json_backup + + +def get_account_config_contents( + name=None, + auth="cloud", + token=None, + url=None, + instance=None, + verify=None, + proxies=None, +): + """Generate qiskitrc content""" + if instance is None: + instance = "some_instance" if auth == "cloud" else "hub/group/project" + token = token or uuid.uuid4().hex + if name is None: + name = ( + management._DEFAULT_ACCOUNT_NAME_CLOUD + if auth == "cloud" + else management._DEFAULT_ACCOUNT_NAME_LEGACY + ) + if url is None: + url = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + out = { + name: { + "auth": auth, + "url": url, + "token": token, + "instance": instance, + } + } + if verify is not None: + out["verify"] = verify + if proxies is not None: + out["proxies"] = proxies + return out diff --git a/test/utils/decorators.py b/test/utils/decorators.py new file mode 100644 index 0000000000..6cbbe79980 --- /dev/null +++ b/test/utils/decorators.py @@ -0,0 +1,298 @@ +# 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. + +"""Decorators for using with IBM Provider unit tests. + + Environment variables used by the decorators: + * QISKIT_IBM_API_TOKEN: default API token to use. + * QISKIT_IBM_API_URL: default API url to use. + * QISKIT_IBM_HGP: default hub/group/project to use. + * QISKIT_IBM_PRIVATE_HGP: hub/group/project to use for private jobs. + * QISKIT_IBM_DEVICE: default device to use. + * QISKIT_IBM_USE_STAGING_CREDENTIALS: True if use staging credentials. + * QISKIT_IBM_STAGING_API_TOKEN: staging API token to use. + * QISKIT_IBM_STAGING_API_URL: staging API url to use. + * QISKIT_IBM_STAGING_HGP: staging hub/group/project to use. + * QISKIT_IBM_STAGING_DEVICE: staging device to use. + * QISKIT_IBM_STAGING_PRIVATE_HGP: staging hub/group/project to use for private jobs. +""" + +import os +from functools import wraps +from unittest import SkipTest +from typing import Optional, List, Union + +from qiskit.test.testing_options import get_test_options +from qiskit_ibm_runtime import IBMRuntimeService + +from ..mock.fake_runtime_service import FakeRuntimeService + + +def requires_online_access(func): + """Decorator that signals whether online access is needed.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + if get_test_options()["skip_online"]: + raise SkipTest("Skipping online tests") + return func(*args, **kwargs) + + return _wrapper + + +def requires_qe_access(func): + """Test requires legacy access.""" + + @wraps(func) + def _wrapper(obj, *args, **kwargs): + token, url, _ = _get_token_url_instance("legacy") + kwargs.update({"qe_token": token, "qe_url": url}) + return func(obj, *args, **kwargs) + + return _wrapper + + +def requires_multiple_hgps(func): + """Test requires a public and premium hgp.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + service = _get_service("legacy") + hgps = list(service._hgps.keys()) + if len(hgps) < 2: + raise SkipTest("Test require at least 2 hub/group/project.") + + # Get open access hgp + open_hgp = hgps[-1] + premium_hgp = hgps[0] + kwargs.update( + { + "service": service, + "open_hgp": open_hgp, + "premium_hgp": premium_hgp, + } + ) + return func(*args, **kwargs) + + return _wrapper + + +def requires_legacy_service(func): + """Test requires legacy online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + token, url, instance = _get_token_url_instance("legacy") + service = IBMRuntimeService( + auth="legacy", token=token, url=url, instance=instance + ) + kwargs.update({"service": service, "instance": instance}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_service(func): + """Test requires cloud online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + token, url, instance = _get_token_url_instance("cloud") + service = IBMRuntimeService( + auth="cloud", token=token, url=url, instance=instance + ) + kwargs.update({"service": service, "instance": instance}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_legacy_services(func): + """Test requires cloud online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + cloud_token, cloud_url, cloud_instance = _get_token_url_instance("cloud") + cloud_service = IBMRuntimeService( + auth="cloud", token=cloud_token, url=cloud_url, instance=cloud_instance + ) + legacy_token, legacy_url, legacy_instance = _get_token_url_instance("legacy") + legacy_service = IBMRuntimeService( + auth="legacy", token=legacy_token, url=legacy_url, instance=legacy_instance + ) + + kwargs.update({"services": [cloud_service, legacy_service]}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_provider(func): + """Decorator that signals the test uses the online API, via a custom hub/group/project. + + This decorator delegates into the `requires_qe_access` decorator, but + instead of the credentials it appends a `provider` argument to the decorated + function. It also appends the custom `hub`, `group` and `project` arguments. + + Args: + func (callable): test function to be decorated. + + Returns: + callable: the decorated function. + """ + + @wraps(func) + @requires_qe_access + def _wrapper(*args, **kwargs): + token = kwargs.pop("qe_token") + url = kwargs.pop("qe_url") + service = IBMRuntimeService(auth="legacy", token=token, url=url) + hub, group, project = _get_custom_hgp() + kwargs.update( + {"service": service, "hub": hub, "group": group, "project": project} + ) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_legacy_devices(func): + """Test requires both cloud and legacy devices.""" + + @wraps(func) + def _wrapper(obj, *args, **kwargs): + + devices = [] + token, url, instance = _get_token_url_instance("cloud") + service = IBMRuntimeService( + auth="cloud", token=token, url=url, instance=instance + ) + # TODO use real device when cloud supports it + devices.append(service.least_busy(min_num_qubits=5)) + + token, url, instance = _get_token_url_instance("legacy") + service = IBMRuntimeService( + auth="legacy", token=token, url=url, instance=instance + ) + devices.append( + service.least_busy(simulator=False, min_num_qubits=5, instance=instance) + ) + + kwargs.update({"devices": devices}) + return func(obj, *args, **kwargs) + + return _wrapper + + +@requires_online_access +def _get_token_url_instance(auth): + # TODO: Change this once we start using different environments + if auth == "cloud": + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""): + return ( + os.getenv("QISKIT_IBM_STAGING_CLOUD_TOKEN"), + os.getenv("QISKIT_IBM_STAGING_CLOUD_URL"), + os.getenv("QISKIT_IBM_STAGING_CLOUD_CRN"), + ) + + return ( + os.getenv("QISKIT_IBM_CLOUD_TOKEN"), + os.getenv("QISKIT_IBM_CLOUD_URL"), + os.getenv("QISKIT_IBM_CLOUD_CRN"), + ) + + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""): + # Special case: instead of using the standard credentials mechanism, + # load them from different environment variables. This assumes they + # will always be in place, as is used by the CI setup. + return ( + os.getenv("QISKIT_IBM_STAGING_API_TOKEN"), + os.getenv("QISKIT_IBM_STAGING_API_URL"), + os.getenv("QISKIT_IBM_STAGING_HGP"), + ) + + return ( + os.getenv("QISKIT_IBM_API_TOKEN"), + os.getenv("QISKIT_IBM_API_URL"), + os.getenv("QISKIT_IBM_HGP"), + ) + + +def _get_service(auth: str) -> Union[List, IBMRuntimeService]: + """Return service(s). + + Args: + auth: Service type, ``cloud``, ``legacy``, or ``both``. + + Returns: + Runtime service(s) + """ + if auth in ["cloud", "legacy"]: + token, url, instance = _get_token_url_instance(auth) + return IBMRuntimeService(auth=auth, token=token, url=url, instance=instance) + + services = [] + for auth_ in ["cloud", "legacy"]: + token, url, instance = _get_token_url_instance(auth_) + services.append( + IBMRuntimeService(auth=auth_, token=token, url=url, instance=instance) + ) + return services + + +def _get_custom_hgp() -> Optional[str]: + """Get a custom hub/group/project + + Gets the hub/group/project set in QISKIT_IBM_STAGING_HGP for staging env or + QISKIT_IBM_HGP for production env. + + Returns: + Custom hub/group/project or ``None`` if not set. + """ + hgp = ( + os.getenv("QISKIT_IBM_STAGING_HGP", None) + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", "") + else os.getenv("QISKIT_IBM_HGP", None) + ) + return hgp + + +def run_legacy_and_cloud_fake(func): + """Decorator that runs a test using both legacy and cloud fake services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + legacy_service = FakeRuntimeService( + auth="legacy", token="my_token", instance="h/g/p" + ) + cloud_service = FakeRuntimeService( + auth="cloud", token="my_token", instance="crn:123" + ) + for service in [legacy_service, cloud_service]: + with self.subTest(service=service.auth): + kwargs["service"] = service + func(self, *args, **kwargs) + + return _wrapper + + +def run_cloud_legacy_real(func): + """Decorator that runs a test using both legacy and cloud real services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + for service in self.services: + with self.subTest(service=service.auth): + kwargs["service"] = service + func(self, *args, **kwargs) + + return _wrapper diff --git a/test/utils/program.py b/test/utils/program.py new file mode 100644 index 0000000000..4d2078fd01 --- /dev/null +++ b/test/utils/program.py @@ -0,0 +1,93 @@ +# 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. + +"""Utility functions for runtime testing.""" + +import uuid +import copy + + +DEFAULT_DATA = "def main() {}" +DEFAULT_METADATA = { + "name": "qiskit-test", + "description": "Test program.", + "max_execution_time": 300, + "spec": { + "backend_requirements": {"min_num_qubits": 5}, + "parameters": { + "properties": { + "param1": { + "description": "Desc 1", + "type": "string", + "enum": ["a", "b", "c"], + }, + "param2": {"description": "Desc 2", "type": "integer", "min": 0}, + }, + "required": ["param1"], + }, + "return_values": { + "type": "object", + "description": "Return values", + "properties": { + "ret_val": {"description": "Some return value.", "type": "string"} + }, + }, + "interim_results": { + "properties": { + "int_res": {"description": "Some interim result", "type": "string"} + } + }, + }, +} + + +def upload_program(service, name=None, max_execution_time=300, is_public: bool = False): + """Upload a new program.""" + name = name or uuid.uuid4().hex + data = DEFAULT_DATA + metadata = copy.deepcopy(DEFAULT_METADATA) + metadata.update(name=name) + metadata.update(is_public=is_public) + metadata.update(max_execution_time=max_execution_time) + program_id = service.upload_program(data=data, metadata=metadata) + return program_id + + +def run_program( + service, + program_id=None, + inputs=None, + job_classes=None, + final_status=None, + decoder=None, + image="", + instance=None, + backend_name=None, + log_level=None, +): + """Run a program.""" + backend_name = backend_name if backend_name is not None else "common_backend" + options = {"backend_name": backend_name, "image": image, "log_level": log_level} + if final_status is not None: + service._api_client.set_final_status(final_status) + elif job_classes: + service._api_client.set_job_classes(job_classes) + if program_id is None: + program_id = upload_program(service) + job = service.run( + program_id=program_id, + options=options, + inputs=inputs, + result_decoder=decoder, + instance=instance, + ) + return job diff --git a/test/ibm/runtime/utils.py b/test/utils/serialization.py similarity index 96% rename from test/ibm/runtime/utils.py rename to test/utils/serialization.py index 2f164d5f19..2d43b3a342 100644 --- a/test/ibm/runtime/utils.py +++ b/test/utils/serialization.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Utility functions for runtime testing.""" +"""Serialization utility functions for runtime testing.""" import json diff --git a/test/utils/templates.py b/test/utils/templates.py new file mode 100644 index 0000000000..a30b38bc44 --- /dev/null +++ b/test/utils/templates.py @@ -0,0 +1,52 @@ +# 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. + +"""Templates for use with unit tests.""" + +RUNTIME_PROGRAM = """ +import random +import time +import warnings +import logging + +from qiskit import transpile +from qiskit.circuit.random import random_circuit + +logger = logging.getLogger("qiskit-test") + +def prepare_circuits(backend): + circuit = random_circuit(num_qubits=5, depth=4, measure=True, + seed=random.randint(0, 1000)) + return transpile(circuit, backend) + +def main(backend, user_messenger, **kwargs): + iterations = kwargs['iterations'] + sleep_per_iteration = kwargs.pop('sleep_per_iteration', 0) + interim_results = kwargs.pop('interim_results', {}) + final_result = kwargs.pop("final_result", {}) + for it in range(iterations): + time.sleep(sleep_per_iteration) + qc = prepare_circuits(backend) + user_messenger.publish({"iteration": it, "interim_results": interim_results}) + backend.run(qc).result() + + user_messenger.publish(final_result, final=True) + print("this is a stdout message") + warnings.warn("this is a stderr message") + logger.info("this is an info log") + """ + +RUNTIME_PROGRAM_METADATA = { + "max_execution_time": 600, + "description": "Qiskit test program", +} +PROGRAM_PREFIX = "qiskit-test" diff --git a/test/utils.py b/test/utils/utils.py similarity index 60% rename from test/utils.py rename to test/utils/utils.py index 8a818619c4..484cece80a 100644 --- a/test/utils.py +++ b/test/utils/utils.py @@ -14,16 +14,17 @@ import os import logging -from typing import Optional +import time +import unittest from qiskit import QuantumCircuit -from qiskit.qobj import QasmQobj -from qiskit.compiler import assemble, transpile -from qiskit.test.reference_circuits import ReferenceCircuits -from qiskit.pulse import Schedule +from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus +from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit_ibm_runtime.hub_group_project import HubGroupProject from qiskit_ibm_runtime import IBMRuntimeService from qiskit_ibm_runtime.ibm_backend import IBMBackend +from qiskit_ibm_runtime.runtime_job import RuntimeJob +from qiskit_ibm_runtime.exceptions import RuntimeInvalidStateError def setup_test_logging(logger: logging.Logger, filename: str): @@ -53,36 +54,6 @@ def setup_test_logging(logger: logging.Logger, filename: str): logger.setLevel(os.getenv("LOG_LEVEL", "DEBUG")) -def most_busy_backend( - service: IBMRuntimeService, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, -) -> IBMBackend: - """Return the most busy backend for the provider given. - - Return the most busy available backend for those that - have a `pending_jobs` in their `status`. Backends such as - local backends that do not have this are not considered. - - Args: - service: IBM Quantum account provider. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. - - Returns: - The most busy backend. - """ - backends = service.backends( - simulator=False, operational=True, hub=hub, group=group, project=project - ) - return max( - [b for b in backends if b.configuration().n_qubits >= 5], - key=lambda b: b.status().pending_jobs, - ) - - def get_large_circuit(backend: IBMBackend) -> QuantumCircuit: """Return a slightly larger circuit that would run a bit longer. @@ -102,38 +73,6 @@ def get_large_circuit(backend: IBMBackend) -> QuantumCircuit: return circuit -def bell_in_qobj(backend: IBMBackend, shots: int = 1024) -> QasmQobj: - """Return a bell circuit in Qobj format. - - Args: - backend: Backend to use for transpiling the circuit. - shots: Number of shots. - - Returns: - A bell circuit in Qobj format. - """ - return assemble( - transpile(ReferenceCircuits.bell(), backend=backend), - backend=backend, - shots=shots, - ) - - -def get_pulse_schedule(backend: IBMBackend) -> Schedule: - """Return a pulse schedule.""" - config = backend.configuration() - defaults = backend.defaults() - inst_map = defaults.instruction_schedule_map - - # Run 2 experiments - 1 with x pulse and 1 without - x = inst_map.get("x", 0) - measure = inst_map.get("measure", range(config.n_qubits)) << x.duration - ground_sched = measure - excited_sched = x | measure - schedules = [ground_sched, excited_sched] - return schedules - - def get_hgp(qe_token: str, qe_url: str, default: bool = True) -> HubGroupProject: """Return a HubGroupProject for the account. @@ -159,3 +98,42 @@ def get_hgp(qe_token: str, qe_url: str, default: bool = True) -> HubGroupProject hgp_to_return = hgp break return hgp_to_return + + +def cancel_job_safe(job: RuntimeJob, logger: logging.Logger) -> bool: + """Cancel a runtime job.""" + try: + job.cancel() + status = job.status() + assert ( + status is JobStatus.CANCELLED + ), "cancel() was successful for job {} but its " "status is {}.".format( + job.job_id, status + ) + return True + except RuntimeInvalidStateError: + if job.status() in JOB_FINAL_STATES: + logger.warning("Unable to cancel job because it's already done.") + return False + raise + + +def wait_for_status(job, status, poll_time=1, time_out=20): + """Wait for job to reach a certain status.""" + wait_time = 1 if status == JobStatus.QUEUED else poll_time + while job.status() not in JOB_FINAL_STATES + (status,) and time_out > 0: + time.sleep(wait_time) + time_out -= wait_time + if job.status() != status: + raise unittest.SkipTest(f"Job {job.job_id} unable to reach status {status}.") + + +def get_real_device(service): + """Get a real device for the service.""" + try: + # TODO: Remove filters when ibmq_berlin is removed + return service.least_busy( + simulator=False, filters=lambda b: b.name() != "ibmq_berlin" + ).name() + except QiskitBackendNotFoundError: + raise unittest.SkipTest("No real device") # cloud has no real device diff --git a/tutorials/00_introduction.ipynb b/tutorials/00_introduction.ipynb new file mode 100644 index 0000000000..b3597b3489 --- /dev/null +++ b/tutorials/00_introduction.ipynb @@ -0,0 +1,344 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2b3fb1b1", + "metadata": {}, + "source": [ + "# Qiskit Runtime" + ] + }, + { + "cell_type": "markdown", + "id": "8fb23fc2", + "metadata": {}, + "source": [ + "Qiskit Runtime is a new architecture offered by IBM Quantum that streamlines computations requiring many iterations. These experiments will execute significantly faster within this improved hybrid quantum/classical process.\n", + "\n", + "Using Qiskit Runtime, for example, a research team at IBM Quantum was able to achieve \n", + "[120x speed up](https://research.ibm.com/blog/120x-quantum-speedup) in their lithium hydride simulation. \n", + "\n", + "Qiskit Runtime allows authorized users to upload their Qiskit quantum programs for themselves or \n", + "others to use. A Qiskit quantum program, also called a Qiskit runtime program, is a piece of Python code that takes certain inputs, performs\n", + "quantum and maybe classical computation, and returns the processing results. The same or other\n", + "authorized users can then invoke these quantum programs by simply passing in the required input parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "233c286a", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import IBMQ\n", + "\n", + "IBMQ.load_account()\n", + "provider = IBMQ.get_provider(project='qiskit-runtime') # Change this to your provider." + ] + }, + { + "cell_type": "markdown", + "id": "1612dac1", + "metadata": {}, + "source": [ + "\n", + "If you don't have an IBM Quantum account, you can sign up for one on the [IBM Quantum](https://quantum-computing.ibm.com/) page." + ] + }, + { + "cell_type": "markdown", + "id": "61d9f293", + "metadata": {}, + "source": [ + "## Listing programs " + ] + }, + { + "cell_type": "markdown", + "id": "2ed6ce38", + "metadata": {}, + "source": [ + "The `provider.runtime` object is an instance of the [`IBMRuntimeService`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService) class and serves as the main entry point to using the runtime service. It has three methods that can be used to find metadata of available programs:\n", + "- `pprint_programs()`: pretty prints summary metadata of available programs\n", + "- `programs()`: returns a list of `RuntimeProgram` instances\n", + "- `program()`: returns a single `RuntimeProgram` instance\n", + "\n", + "The metadata of a runtime program includes its ID, name, description, maximum execution time, backend requirements, input parameters, return values, and interim results. Maximum execution time is the maximum amount of time, in seconds, a program can run before being forcibly terminated." + ] + }, + { + "cell_type": "markdown", + "id": "2abfb988", + "metadata": {}, + "source": [ + "To print the summary metadata of the programs (by default first 20 programs are displayed):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a420f91c", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "==================================================\nqasm3-runner:\n Name: qasm3-runner\n Description: A runtime program that takes one or more circuits, converts them to OpenQASM3, compiles them, executes them, and optionally applies measurement error mitigation. This program can also take and execute one or more OpenQASM3 strings. Note that this program can only run on a backend that supports OpenQASM3.\n==================================================\nsampler:\n Name: sampler\n Description: Sample distributions generated by given circuits executed on the target backend.\n==================================================\nestimator:\n Name: estimator\n Description: Expectation value estimator. A runtime program that estimates the value of an observable for an input quantum circuit. This program is in beta mode and is only available to select accounts.\n==================================================\nsample-expval:\n Name: sample-expval\n Description: A sample expectation value program.\n==================================================\nvqe:\n Name: vqe\n Description: Variational Quantum Eigensolver (VQE) to find the minimal eigenvalue of a Hamiltonian.\n==================================================\ncircuit-runner:\n Name: circuit-runner\n Description: A runtime program that takes one or more circuits, compiles them, executes them, and optionally applies measurement error mitigation.\n==================================================\nhello-world:\n Name: hello-world\n Description: A sample runtime program.\n==================================================\nquantum-kernel-alignment:\n Name: quantum-kernel-alignment\n Description: Quantum kernel alignment algorithm that learns, on a given dataset, a quantum kernel maximizing the SVM classification margin.\n" + } + ], + "source": [ + "provider.runtime.pprint_programs()" + ] + }, + { + "cell_type": "markdown", + "id": "0a5ca5f5", + "metadata": {}, + "source": [ + "You can use the `limit` and `skip` parameters in `pprint_programs()` and `programs()` to page through the remaining programs. You can pass `detailed = True` parameter to `pprint_programs()` to view all the metadata for the programs. The program metadata once fetched, is cached for performance reasons, so you can pass `refresh = True` parameter to `pprint_programs()` or `programs()` methods in order to get the latest programs from the server. To print the metadata of the program `hello-world`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f8302b63", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "hello-world:\n Name: hello-world\n Description: A sample runtime program.\n Creation date: 2021-07-02T13:45:13Z\n Update date: 2021-07-02T13:45:13Z\n Max execution time: 300\n Input parameters:\n Properties:\n - iterations:\n Description: Number of iterations to run. Each iteration generates a runs a random circuit.\n Minimum: 0\n Type: integer\n Required: True\n Interim results:\n Properties:\n - counts:\n Description: Histogram data of the circuit result.\n Type: object\n Required: False\n - iteration:\n Description: Iteration number.\n Type: integer\n Required: False\n Returns:\n Description: A string that says 'All done!'.\n Type: string\n" + } + ], + "source": [ + "program = provider.runtime.program('hello-world')\n", + "print(program)" + ] + }, + { + "cell_type": "markdown", + "id": "ec37a66c", + "metadata": {}, + "source": [ + "As you can see from above, the program `hello-world` is a simple program that has only 1 input parameter `iterations`, which indicates how many iterations to run. For each iteration it generates and runs a random 5-qubit circuit and returns the counts as well as the iteration number as the interim results. When the program finishes, it returns the sentence `All done!`. This program can only run for 300 seconds (5 minutes), and requires a backend that has at least 5 qubits." + ] + }, + { + "cell_type": "markdown", + "id": "8097db3a", + "metadata": {}, + "source": [ + "## Invoking a runtime program " + ] + }, + { + "cell_type": "markdown", + "id": "b6d680a3", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.run()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.run) method to invoke a runtime program. This method takes the following parameters:\n", + "\n", + "- `program_id`: ID of the program to run\n", + "- `inputs`: Program input parameters. These input values are passed to the runtime program.\n", + "- `options`: Runtime options. These options control the execution environment. Currently the only available option is `backend_name`, which is required.\n", + "- `callback`: Callback function to be invoked for any interim results. The callback function will receive 2 positional parameters: job ID and interim result.\n", + "- `result_decoder`: Optional class used to decode job result." + ] + }, + { + "cell_type": "markdown", + "id": "abe247c4", + "metadata": {}, + "source": [ + "Before we run a quantum program, we may want to define a callback function that would process interim results, which are intermediate data provided by a program while its still running. \n", + "\n", + "As we saw earlier, the metadata of `hello-world` says that its interim results are the iteration number and the counts of the randomly generated circuit. Here we define a simple callback function that just prints these interim results:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "92f46214", + "metadata": {}, + "outputs": [], + "source": [ + "def interim_result_callback(job_id, interim_result):\n", + " print(f\"interim result: {interim_result}\")" + ] + }, + { + "cell_type": "markdown", + "id": "3f9e793f", + "metadata": {}, + "source": [ + "The following example runs the `hello-world` program with 3 iterations on `ibmq_montreal` and waits for its result. You can also use a different backend that supports Qiskit Runtime:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d622e53c", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "job id: c618jdik2ih5ha3l6mog\ninterim result: {'iteration': 0, 'counts': {'00000': 31, '00001': 11, '10000': 10, '10001': 10, '10010': 19, '10011': 11, '10100': 4, '10101': 6, '10110': 11, '10111': 8, '11000': 16, '11001': 4, '11010': 45, '11011': 16, '11100': 8, '11101': 9, '11110': 18, '11111': 8, '00010': 104, '00011': 47, '00100': 5, '00101': 10, '00110': 25, '00111': 10, '01000': 60, '01001': 35, '01010': 260, '01011': 119, '01100': 13, '01101': 12, '01110': 39, '01111': 40}}\ninterim result: {'iteration': 1, 'counts': {'00000': 99, '00001': 64, '10000': 13, '10001': 12, '10010': 9, '10011': 4, '10100': 21, '10101': 89, '10110': 6, '10111': 19, '11000': 19, '11001': 9, '11010': 5, '11011': 5, '11100': 26, '11101': 61, '11110': 11, '11111': 17, '00010': 37, '00011': 23, '00100': 73, '00101': 13, '00110': 20, '00111': 3, '01000': 105, '01001': 83, '01010': 30, '01011': 26, '01100': 79, '01101': 11, '01110': 22, '01111': 10}}\ninterim result: {'iteration': 2, 'counts': {'00000': 30, '00001': 5, '10000': 8, '10001': 3, '10010': 1, '10011': 3, '10100': 7, '10101': 5, '10110': 3, '11000': 22, '11001': 6, '11010': 1, '11011': 3, '11100': 66, '11101': 8, '11110': 11, '11111': 7, '00010': 2, '00011': 2, '00100': 51, '00101': 5, '00110': 5, '00111': 2, '01000': 136, '01001': 16, '01010': 11, '01011': 2, '01100': 534, '01101': 31, '01110': 31, '01111': 7}}\nAll done!\n" + } + ], + "source": [ + "backend = provider.get_backend('ibmq_montreal')\n", + "program_inputs = {\n", + " 'iterations': 3\n", + "}\n", + "options = {'backend_name': backend.name()}\n", + "job = provider.runtime.run(program_id=\"hello-world\",\n", + " options=options,\n", + " inputs=program_inputs,\n", + " callback=interim_result_callback\n", + " )\n", + "print(f\"job id: {job.job_id()}\")\n", + "result = job.result()\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "1cb7b160", + "metadata": {}, + "source": [ + "The `run()` method returns a [`RuntimeJob`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.RuntimeJob.html#qiskit.providers.ibmq.runtime.RuntimeJob) instance, which is similar to the `Job` instance returned by regular `backend.run()`. Some of the `RuntimeJob` methods:\n", + "\n", + "- `status()`: Return job status.\n", + "- `result()`: Wait for the job to finish and return the final result.\n", + "- `cancel()`: Cancel the job.\n", + "- `wait_for_final_state()`: Wait for the job to finish.\n", + "- `stream_results()`: Stream interim results. This can be used to start streaming the interim results if a `callback` function was not passed to the `run()` method. This method can also be used to reconnect a lost websocket connection.\n", + "- `job_id()`: Return the job ID.\n", + "- `backend()`: Return the backend where the job is run.\n", + "- `logs()`: Return job logs.\n", + "- `error_message()`: Returns the reason if the job failed and `None` otherwise.\n", + "\n", + "Refer to the [`RuntimeJob` API documentation](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.RuntimeJob.html#qiskit.providers.ibmq.runtime.RuntimeJob) for a full list of methods and usage. " + ] + }, + { + "cell_type": "markdown", + "id": "present-creature", + "metadata": {}, + "source": [ + "
\n", + "Note: To ensure fairness, there is a maximum execution time for each Qiskit Runtime job. If a job exceeds this time limit, it is forcibly terminated. The maximum execution time is calculated based on 1) a maximum system limit, 2) the `max_execution_time` defined by the program, and 3) the fair-share value in your hub/group/project. \n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "821630b0", + "metadata": {}, + "source": [ + "## Retrieving old jobs" + ] + }, + { + "cell_type": "markdown", + "id": "ed6efcd4", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.job()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.job) method to retrieve a previously executed runtime job. Attributes of this [`RuntimeJob`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.RuntimeJob.html#qiskit.providers.ibmq.runtime.RuntimeJob) instance can tell you about the execution:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4336b881", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Job c618jdik2ih5ha3l6mog is an execution instance of runtime program hello-world.\nThis job ran on backend ibmq_montreal and had input parameters {'iterations': 3}\n" + } + ], + "source": [ + "retrieved_job = provider.runtime.job(job.job_id())\n", + "print(f\"Job {retrieved_job.job_id()} is an execution instance of runtime program {retrieved_job.program_id}.\")\n", + "print(f\"This job ran on backend {retrieved_job.backend()} and had input parameters {retrieved_job.inputs}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5c00514e", + "metadata": {}, + "source": [ + "Similarly, you can use [`IBMRuntimeService.jobs()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.jobs) to get a list of jobs. You can specify a limit on how many jobs to return. The default limit is 10:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "919862b8", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "c618jdik2ih5ha3l6mog\n" + } + ], + "source": [ + "retrieved_jobs = provider.runtime.jobs(limit=1)\n", + "for rjob in retrieved_jobs:\n", + " print(rjob.job_id())" + ] + }, + { + "cell_type": "markdown", + "id": "d6f8f1d2", + "metadata": {}, + "source": [ + "## Deleting a job" + ] + }, + { + "cell_type": "markdown", + "id": "81234913", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.delete_job()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.delete_job) method to delete a job. You can only delete your own jobs, and this action cannot be reversed. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b1095852", + "metadata": {}, + "outputs": [], + "source": [ + "provider.runtime.delete_job(job.job_id())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/01_circuit_runner.ipynb b/tutorials/01_circuit_runner.ipynb new file mode 100644 index 0000000000..8f1a3d3348 --- /dev/null +++ b/tutorials/01_circuit_runner.ipynb @@ -0,0 +1,623 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f041e8ea", + "metadata": {}, + "source": [ + "# Circuit runner program" + ] + }, + { + "cell_type": "markdown", + "id": "d16ddf88", + "metadata": {}, + "source": [ + "## `run_circuits` convenience method\n", + "\n", + "Evaluating quantum circuits is the fundamental operation on quantum computing systems. The `circuit-runner` program facilitates compiling, executing, and (possibly) post-processing circuit entirely in the cloud. The easiest way to use the `circuit-runner` is via the convenience method accessible through a provider with `runtime` access:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fafcaba8", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import IBMQ, QuantumCircuit\n", + "from qiskit.visualization import plot_histogram\n", + "from qiskit.quantum_info import hellinger_fidelity\n", + "from qiskit.ignis.mitigation.expval import expectation_value" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1b07aacd", + "metadata": {}, + "outputs": [], + "source": [ + "IBMQ.load_account();" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9b552135", + "metadata": {}, + "outputs": [], + "source": [ + "# Replace by your provider\n", + "provider = IBMQ.get_provider(project='qiskit-runtime')" + ] + }, + { + "cell_type": "markdown", + "id": "ee1e3511", + "metadata": {}, + "source": [ + "The runner is accessbile via the `run_circuits` method of the provider, and supports the same functionality that transpiling and executing a circuit in Qiskit does, as well as a few additional arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "84785ee9", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Execute the input circuit(s) on a backend using the runtime service.\n\n Note:\n This method uses the IBM Quantum runtime service which is not\n available to all accounts.\n\n Args:\n circuits: Circuit(s) to execute.\n\n backend_name: Name of the backend to execute circuits on.\n Transpiler options are automatically grabbed from backend configuration\n and properties unless otherwise specified.\n\n shots: Number of repetitions of each circuit, for sampling. If not specified,\n the backend default is used.\n\n initial_layout: Initial position of virtual qubits on physical qubits.\n\n layout_method: Name of layout selection pass ('trivial', 'dense',\n 'noise_adaptive', 'sabre').\n Sometimes a perfect layout can be available in which case the layout_method\n may not run.\n\n routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre')\n\n translation_method: Name of translation pass ('unroller', 'translator', 'synthesis')\n\n seed_transpiler: Sets random seed for the stochastic parts of the transpiler.\n\n optimization_level: How much optimization to perform on the circuits.\n Higher levels generate more optimized circuits, at the expense of longer\n transpilation time.\n If None, level 1 will be chosen as default.\n\n init_qubits: Whether to reset the qubits to the ground state for each shot.\n\n rep_delay: Delay between programs in seconds. Only supported on certain\n backends (``backend.configuration().dynamic_reprate_enabled`` ). If supported,\n ``rep_delay`` will be used instead of ``rep_time`` and must be from the\n range supplied by the backend (``backend.configuration().rep_delay_range``).\n Default is given by ``backend.configuration().default_rep_delay``.\n\n transpiler_options: Additional transpiler options.\n\n measurement_error_mitigation: Whether to apply measurement error mitigation.\n\n use_measure_esp: Whether to use excited state promoted (ESP) readout for measurements\n which are the final instruction on a qubit. ESP readout can offer higher fidelity\n than standard measurement sequences. See\n `here `_.\n\n **run_config: Extra arguments used to configure the circuit execution.\n\n Returns:\n Runtime job.\n \n" + } + ], + "source": [ + "print(provider.run_circuits.__doc__)" + ] + }, + { + "cell_type": "markdown", + "id": "65f667d6", + "metadata": {}, + "source": [ + "of the possible arguments, `measurement_error_mitigation` is one that is unique to the Qiskit Runtime, and will be used below.\n", + "\n", + "We now construct an example circuit to execute via the circuit runner:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e01c0503", + "metadata": { + "tags": [ + "nbsphinx-thumbnail" + ] + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "
", + "image/svg+xml": "\n\n\n \n \n \n \n 2021-11-02T14:33:38.178360\n image/svg+xml\n \n \n Matplotlib v3.4.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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\n" + }, + "metadata": {}, + "execution_count": 5 + } + ], + "source": [ + "N = 6\n", + "qc = QuantumCircuit(N)\n", + "\n", + "qc.x(range(0, N))\n", + "qc.h(range(0, N))\n", + "\n", + "for kk in range(N//2,0,-1):\n", + " qc.ch(kk, kk-1)\n", + "for kk in range(N//2, N-1):\n", + " qc.ch(kk, kk+1)\n", + "qc.measure_all()\n", + "qc.draw('mpl',fold=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f36f8eb8", + "metadata": {}, + "outputs": [], + "source": [ + "exact_dist = {'000000': 0.015624999999999986,\n", + " '000001': 0.015624999999999986,\n", + " '000011': 0.031249999999999965,\n", + " '000111': 0.06249999999999992,\n", + " '100000': 0.015624999999999986,\n", + " '100001': 0.015624999999999986,\n", + " '100011': 0.031249999999999965,\n", + " '100111': 0.06249999999999992,\n", + " '110000': 0.031249999999999965,\n", + " '110001': 0.031249999999999965,\n", + " '110011': 0.06249999999999992,\n", + " '110111': 0.12499999999999982,\n", + " '111111': 0.4999999999999991}" + ] + }, + { + "cell_type": "markdown", + "id": "f8186fc9", + "metadata": {}, + "source": [ + "Having defined our circuit(s), we can now compile and execute them on a given backend via the `run_circuits` method:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "08de9773", + "metadata": {}, + "outputs": [], + "source": [ + "backend = provider.backend.ibmq_montreal" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8356e156", + "metadata": {}, + "outputs": [], + "source": [ + "job = provider.run_circuits(qc, backend.name(), shots=2048, initial_layout=[0,1,4,7,10,12])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "738dbad1", + "metadata": {}, + "outputs": [], + "source": [ + "res = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4cf377ee", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA98AAAGXCAYAAACqZ+OYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAACdH0lEQVR4nOzdd3wUdf7H8dc32SSbELrU0HtvRhGFiAri6Z2oZz8PFRU90Dt7B9QD9WyniF1PrFhPQU8R8QREODCgIKB0+AHSeyQ9398f38262Wwg2WQ3JLyfj8c+YGfnM98yk5n5znznO8Zai4iIiIiIiIhETkxlZ0BERERERESkulPjW0RERERERCTC1PgWERERERERiTA1vkVEREREREQiTI1vERERERERkQhT41tEREREREQkwtT4FhEREREREYmwqDe+jTEjjTHrjDFZxpiFxpgBh5k/3hjzgC8m2xjzf8aYvwbN80djzHLf78uNMedGthQiIiIiIiIipRfVxrcx5iLgKeBBoDcwF/jcGNPiEGHvAGcAI4COwAXAkoBl9gPeBd4Cevn+fd8Y0zcCRRAREREREREpM2OtjV5ixswHllhrrwmYtgr4wFp7V4j5TwfeB9paa3eWsMx3gXrW2sEB02YAO6y1l1R0GURERERERETKKmp3vo0x8cCxwPSgn6YDJ5YQdg7wHXCzMWaTMWaVMWaCMSY5YJ5+IZb5xSGWKSIiIiIiIhJVniimdQwQC2wLmr4NGFRCTBugP5AN/BGoAzwNNAXO983TuIRlNg61QGPMCFwXdpKSko5t1qwZAAkJCcTGxnLw4EEAPB4PiYmJHDhwoDCO5ORkDh48SH5+PgA1atQgNzeXnJwcALxeL8YYMjMzAYiLiyMhIYGMjAwAYmJiqFGjRoUs49dff6WgoACA5ORksrOzyc3NBSAxMRFrLVlZWQDEx8cTFxfHr7/+CkBsbCxJSUkVsoyMjAwKe0/UrFmTzMxM8vLy8NUv+fn5ZGdnl6qOK2IZWk9aT1pPWk9aT1pPWk9aT1pPWk9aT6Wv45UrV+601jbgEBYuXNjQ4/G8DHRDg3aXxBpj9uXn579aUFDw3LHHHpsTPEM0G9/hiAEscKm1dh+AMeZ64AtjTCNrbXCj+7CstS8CLwKkpqba9PT0isyviIiIiIhIlWGM2XC4eTwez8uNGzfu3KBBgz0xMTHRe265CrHWkpOTE/fLL7/csH///j7A5cHzRPOqxU4gH2gUNL0RsLWEmC3A5sKGt89Pvn8LB2nbWsZlioiIiIiISOl1a9CgwX41vEtmjCEhISG3ZcuW+3C9t4uJWuPbWpsDLAQGB/00GDfqeSjfAk2DnvHu4Pu38ArNvDIuU0REREREREovRg3v0vHVU2zI36KclyeAK4wxVxtjOhtjnsI9v/08gDHmdWPM6wHzvw3sAl41xnQ1xpyEe1XZB9ba7b55ngJONcbcaYzpZIy5CzgFeDJKZRIRERERERE5pKg+822tfdcYUx+4F2gCLAXOtNYW3sVuETR/hjFmEG6Qte+APcDHwJ0B88w1xlwMjAMeANYAF1lr50e4OCIiIiIiIiKlEvUB16y1zwLPlvDbwBDTVgCnH2aZHwAfVET+REREREREpGTXPMmxkVz+SzeyMJLLrywaJl5EREREREQkwtT4FhERERERkSqv8N3oRyo1vkVERERERKRKSklJ6X7PPfc07tChQ5caNWr0uf3225s0b968W40aNXq3bdu26+uvv16ncN6mTZt2/+abb5IAnnvuuXrGmGPT09O9AP/85z+PGTRoUNtI5lWNbxEREREREamyPvzww3qfffbZqt27d3/fqVOnrG+++WbF/v37v7/zzjt/ufbaa1tv2LAhDqBv374HZsyYURNg9uzZNZs1a5b91VdfFX5PHjBgwIFI5lONbxEREREREamyrrvuum3t2rXLTU5OtsOHD9/TqlWr3NjYWK655po9LVu2zP7mm29qAKSlpWV88803NQHmz5+ffNNNN20N+F7ztNNOy4hkPtX4FhERERERkSqrZcuW/oe9J06cWL9Tp05datas2atmzZq9Vq9enbhjxw4PwODBgw+kp6cnb9iwIa6goMBcccUVu9PT05NXrFgRf+DAgdh+/fodjGQ+o/6qMREREREREZGKYoyxACtXroy/+eabW37yyScrTzvttAyPx0OnTp26WGsB6NatW7bX6y149NFHG/bt2/dAvXr1Co455pjcp59+ukFqampGbGxsRPOpO98iIiIiIiJS5R04cCDGGEPjxo1zAZ566qn6q1evTgycp2/fvgcmTZrUMC0t7QDASSeddGDSpEkN+/fvH9HnvUF3vkVERERERKQMXrqRhZWdh1COPfbYrBEjRmxLS0vrHBMTY//4xz/u6t27d5HnuNPS0g58+umn9QYPHpwBMHDgwAMvvvhio1NOOSWiz3sDmMJb8Eej1NRUm56eXtnZEBERERERqRTGmIXW2tRDzbN48eL1PXv23BmtPFV1ixcvPqZnz56tgqer27mIiIiIiIhIhKnxLSIiIiIiIhJhanyLiIiIiIiIRJga3yIiIiIiIiIRpsa3iIiIiIiISISp8S0iIiIiIiISYWp8i4iIiIiIiESYGt8iIiIiIiIiEeap7AyIiIiIiIhI1bGm433HRnL5bVfctzCSy68suvMtIiIiIiIiUgp//OMfW/31r39tGk6sGt8iIiIiIiIiEabGt4iIiIiIiFRZ69evjxsyZEjbunXr9kxJSek+bty4htu2bYtt1KhRj7fffrs2wL59+2JatGjRbeLEifUB3nnnndqdO3fukpyc3Ltx48Y9br755iJ3s7/44ovk3r17d6pZs2avxo0b95gwYUL9xx577JgpU6bUe+655xonJSX1PvXUU9uVJZ965ltERERERESqpPz8fM4666x2v/vd7/ZOmTJl7dq1a+NOP/30jp07d8567rnn1o8YMaL1ySefvOyWW25J6dKly8Hrr79+F0BycnLBa6+9tu7YY4/NTE9PTzzzzDM79O7d++Cf//znvStXrow/77zz2j/xxBMbrrjiij179uyJWbt2bfyJJ56YOW/evOSUlJScCRMm/FLWvKrxLSIiIiIiIlXSrFmzauzevdvz2GOPbQHo0qVLzp///OcdkydPrvfBBx+snzJlyu5TTjmlw969ez1LlixZVhj3+9///kDh//v27Zs5dOjQ3TNnzqz55z//ee+rr75a78QTT9x/7bXX7gZo3LhxfuPGjTPLm1d1OxcRERERESmFadOm0bFjR9q1a8fDDz9c7PdJkybRoEEDevXqRa9evXj55Zf9v7322mu0b9+e9u3b89prr/mnL1y4kO7du9OuXTv++te/Yq2NSlmqi7Vr18bv2LEjvmbNmr0KPxMmTGiyY8cOD8CoUaN2rlq1KvGiiy7a1bhx4/zCuP/+9781+vbt26Fu3bo9a9as2eutt95qsGvXLg/Axo0b41u3bp1d0XlV41tEREREROQw8vPzGTVqFJ9//jnLly9n8uTJLF++vNh8F110ET/88AM//PADV199NQC7d+/m/vvvZ/78+SxYsID777+fPXv2APCX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d82hS5cu5Ofns2LFCgBSUlJo1qwZ8+fPByA5OZnU1FTmzZtHdnY2AP3792flypVs374dgG7dupGdnc2qVasAaN68OY0aNSI9PR2AWrVq0adPH+bMmUNeXh4AaWlpLFu2jF27dgHQs2dPDhw4wNq1awFo1aoV9erVY9GiRQDUrVuXnj17MmvWLKy1GGM4+eSTWbx4MXv27AGgT58+7N69m/Xr12s9aT1pPWk9aT1pPWk9aT1pPWk9VYn1FEdxM2fOPGLXk0SHsdZGJyFjmgKbgZOttbMDpo8B/mSt7RgipjswAzjBWrvOGHMfcL61tlvAPP2ADsBioCbwN+BMoKe1dtWh8pSammoLN1oREREREZGKsKbjfcWmtV1RfNqRwBiz0FqbWtn5OBpE7c53WRljEoB3gVuttetKms9aO4+A7ujGmLm4u983AH+NcDZFREREREREDiuaje+dQD6uC3mgRkCoZ7SbAJ2BV40xr/qmxQDGGJMHnGmtnR4cZK3NN8akA+0rLOciIiIiIiIi5RC1AdestTnAQmBw0E+DcaOeB9sMdAd6BXyeB1b7/h8qBmOMAXrgBnITERERERERqXTR7nb+BPCGMWYB8C1wHdAU16jGGPM6gLV2mLU2Fwh+p/d2INtauzRg2ljgf8AqoBauq3kPDj+CuoiIiIiIiEhURLXxba191xhTH7gX1618Ka77+AbfLId833cJ6gAvAo2BfcD3QJq1dkH5cywiIiIiIiJSflEfcM1a+yzwbAm/DTxM7H3AfUHTbgJuqpjciYiIiIiIiFS8qD3zLSIiIiIiInK0UuNbREREREREJMLU+BYRERERERGJMDW+RURERERERCJMjW8RERERERGRCFPjW0RERERERCTC1PgWERERERERiTA1vkVEREREREQiTI1vERERERERkQhT41tEREREREQkwtT4FhEREREREYkwNb5FREREREREIkyNbxEREREREZEIU+NbREREREREJMLU+BYRERERERGJMDW+RURERERERCJMjW8RERERERGRCFPjW0RERERERCTC1PgWERERERERiTA1vkVEREREREQiTI1vERERERERkQhT41tEREREREQkwtT4FhEREREREYkwNb5FREREREREIkyNbxEREREREZEIU+NbREREREREJMLU+BYRERERERGJMDW+RURERERERCJMjW8RERERERGRCFPjW0RERERERCTC1PgWERERERERiTA1vkVEREREREQiTI1vERERERERkQhT41tEREREREQkwtT4FhEREREREYmwMjW+jTExxpiYgO+NjTFXG2NOqvisiYiIiIiIiFQPZb3z/R/gBgBjTDKQDjwKzDTGDKvgvImIiIiIiIhUC2VtfKcC//X9/zxgP9AQuAa4tQLzJSIiIiIiIlJtlLXxnQzs9f3/dOAja20urkHetgLzJSIiIiIiIlJtlLXx/X/AScaYGsAQ4Evf9HrAwYrMmIiIiIiIiEh1UdbG9xPAG8AmYDMw2zc9DfixAvMlIiIiIiJSacaNG0d8fDxxcXGcccYZxX6/9NJL8Xq9JCYmUqtWLaZOnQrAww8/TLdV4+myahzdVo3n5d1z/TF/+9vfSExMxOv10rdv36iVRY4MZWp8W2tfAPoBw4H+1toC309rgNEVnDcREREREZGoy8nJ4f7772f69Ons2bOHWbNm+RvXhR555BGysrLIzMzkhhtuYPjw4QC0bNmSd5oPZ3n7e3mi8Xk8snMGAKtWreKZZ55h0aJFZGVlsXPnTh599NGol00qT5nf822tTbfWfmStzQiY9h9r7bcVmzUREREREZHomzRpErVr12bgwIEkJyeTlpbGM888U2SeZs2a+f+/f/9+jDEAXHLJJXTzNgFgUI2OWCwH8rOYO3cutWrVonPnzgAMHjyY1157LUolkiNBmRvfxpiRxphlxpiDxpg2vml3GGMurPjsiYiIiIiIRNeKFSuoX7++/3vr1q3ZsmVLsfkuvPBC4uLieO6553j77beL/f7wzhnUivFSM9ZLWloa+/btY86cOWRlZfHJJ5+wY8eOiJZDjixlanwbY24E7gVeBEzAT78A11dctkRERERERI5s7733Hrm5uVx77bVcd911RX778sDPvLZ3PhObuHuUrVu35u677+aMM86gQYMGNGnShJiYMt8LlSqsrGv7OuAaa+1TQF7A9EVA1wrLlYiIiIiISCXp2LEju3bt8n9ft24dTZo0KXH+p556irVr1/q/L87czA1b3mdMwzPoV6O1f/rf//53MjIyOHDgAJ06daJ58+aRKYAckcra+G4JLA0xPRdILH92REREREREKtewYcPYt28fs2fPJiMjg9mzZzNy5Mgi83z55Zf+/99///0kJSUBsGHDBi7dNIkr65zAn+ocVyRm2bJlgGvMf/jhh/z973+PcEnkSOIp4/xrgT7AhqDpZwLLKyRHIiIiIiIilcjr9XLvvfcyaNAgrLUMHDiQoUOHkpaWxoABAxg/fjy33XYbP//8M8YYvF6v/5nvq666imybx+v7FvD6vgUAfNziGtoCZ555Jlu3bgXg2muvZciQIZVVRKkExlpb+pmNuRIYB9wOvABcC7TzfR9urX03EpmMlNTUVJuenl7Z2RARERERkWpkTcf7ik1ru6L4tCOBMWahtTa1svNxNCjTnW9r7avGGA/wIJAEvIEbbO2vVa3hLSIiIiIiIhItZe12jrX2JeAlY8wxQIy1dnvFZ0tERERERESk+ihz47uQtXZnRWZEREREREREpLo6bOPbGLMEONlau8cY8yNQ4kPi1toeFZk5ERERERERkeqgNHe+PwSyA/5f+hHaREREREREROTwjW9r7f0B/78vorkRERERERERqYZiyjKzMea/xpg6IabXMsb8t8JyJSIiIiIiIlKNlKnxDQwE4kNM9wIDyp0bERERERERkWqoVKOdG2P6BHztYYzZHfA9FhgCbK7IjImIiIiIiIhUF6V91Vg6bqA1C0wP8XsmcENFZUpERERERKSquObJot/vrJRcyJGutI3v1oAB1gLHAzsCfssBtltr8ys4byIiIiIiIiLVQqka39baDb7/lvUZcREREREREZGj3mEb38aY84BPrLW5vv+XyFr77wrLmYiIiIiIiEg1UZo73x8AjYHtvv+XxOIGXxMRERERERGRAIdtfFtrY0L9X0RERERERERKR41pERERERERkQgr7TPfpaJnvkVERERERESKK+0z36WhZ75FREREREREQijTM98iIiIiIiIiUnZqWIuIiIiIiIhEmN7zLSIiIiIiIhJhpbnz/QFQN+D/JX3eL02CxpiRxph1xpgsY8xCY8yAQ8x7sjFmrjFmlzEm0xjzszHm1hDz/dEYs9wYk+3799zS5EVEREREREQkGg7b+LbWxlhrtwf8v6TPYQdbM8ZcBDwFPAj0BuYCnxtjWpQQkgFMANKALsA44H5jzMiAZfYD3gXeAnr5/n3fGNP3cPkRERERERERiYZoP/N9MzDJWvuStfYna+0NwBbgL6FmttYutNa+Y61dZq1dZ619E/gCCLxbfiPwtbV2vG+Z44GZvukiIiIiIiIi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8huv18u9997LoEGDqFu3Lv3796dt27YsXryYe+65B3AN67vvvps1a9bwz3/+E6/XC8BXX33Fe++9x6WXXkqXLl248MIL+eMf/wjA8ccfT0yMO33v2LEjAwcOBOCee+6hR48e3HTTTf5Bu0TkyBPVxre19l3gRuBe3Kjk/YEzrbUbfLO08H0Cfe/7DAD+4Pv/ZwG/1wFeBH7CjZyeAqRZaxdEogwiIiIiIoczduxYcnJyyM3N5csvvwTg97//PePHjwdgwIABjBo1CmstBQUFZGVlAXDnnXfy5z//mbfffpvMzEzefPNN/zKnTJlC+/btmTdvHgUFBcycOZOHHnqIn3/+me+++47du3fzj3/8I/qFFZFSidqrxgpZa58Fni3ht4EhppnDLO8m4KYKyZyIiIiISASkpKT4X4MFsGnTJlJSUkodv3//fs466yzGjx/PCSec4J/epEkTwL0668orr+Sxx4Lf6isiR4oqP9q5iIiIiMiR7rjjjmPVqlWsW7eOnJwc3nnnHc4+++xSxebk5HDuuecybNgwzj///CK/bdnihjmy1vLxxx/TrVu3Cs+7iFQMNb5FRERERCLM4/EwceJEhgwZQufOnbnwwgvp2rUrY8aMYerUqQB89913NGvWjPfff59rr72Wrl27AvDee+8xe/ZsJk2aVOyVYn/605/o3r073bt3Z+fOndx7772VVUQROQxjra3sPFSa1NRUm56eXtnZEBERERGRKuyaJ4t+v/O5+4rN03ZF8WlHAmPMQmttamXn42gQ9We+RURERETECW60Abx0Y7RzISLRoG7nIiIiIiIiIhGmxreIiIiIiIhIhKnbuYiIiIjIEWRNx/uKTTtSnxcWkdLTnW8RERERERGRCFPjW0RERERERCTC1PgWERERERERiTA1vkVEREREREQiTI1vERERERERkQhT41tEREREREQkwtT4FhEREREREYkwNb5FREREREREIkyNbxEREREREZEIU+NbREREREREJMLU+BYRERERERGJMDW+RURERERERCJMjW8RERERERGRCFPjW0RERERERCTC1PgWERERkagaN24c8fHxxMXFccYZZxT7ff/+/bRo0YK4uDiSk5OZM2cOACNHjiQxMdH/Mcbw7rvv8ssvvxSZHhMTQ+/evaNdLBGRQ1LjW0RERESiJicnh/vvv5/p06ezZ88eZs2axdSpU4vMM2LECJKTk8nNzWX48OFceumlADz77LNkZmaSmZnJm2++icfj4aKLLqJp06b+6ZmZmXi9XoYPH14ZxRMRKZEa3yIiIiISNZMmTaJ27doMHDiQ5ORk0tLSeOaZZ4rMM2PGDG688UYAHnnkETZt2kRBQUGReR5//HGOP/74Ysv/4osvyMnJYdSoURErg4hIONT4FhEREZGoWbFiBfXr1/d/b926NVu2bCkyT0ZGhr/buNfrJTY2llWrVhWZ57vvvuPWW28ttvx//OMf9OrVi5gYneaKyJHFU9kZEBEREREpi1deeYXY2FjOPffcYr99++23vPLKK5WQKxGRQ9MlQRERERGJmo4dO7Jr1y7/93Xr1tGkSZMi8yQnJ/P9998DkJWVRX5+Pu3bt/f//swzz5CWllZs2e+99x7WWi677LII5V5EJHxqfIuIiIhI1AwbNox9+/Yxe/ZsMjIymD17NiNHjiwyz6mnnsqTTz4JwO23305KSoq/G3leXh6LFy/m7rvvLrbsJ554gn79+kW8DCIi4VC3cxERERGJGq/Xy7333sugQYOw1jJw4ECGDh1KWloaAwYMYPz48bz44ot069aNuLg4EhIS+PTTT/3xEydOJDExkYEDBxZb9sKFC5kyZUoUSyMiUnq68y3iM23aNDp27Ei7du14+OGHi/2enZ3NRRddRLt27ejbty/r16/3/7ZkyRL69etH165d6d69O1lZWQCcccYZ9OzZk65du3LdddeRn58freKIiBxVtA+vWsaOHUtOTg65ubl8+eWXANx999188MEHtGvXjueff55NmzaRm5tLRkYGAwcO9K/DiRMn0rVr15DrsEOHDtxxxx1ahyJyRFLjWwTIz89n1KhRfP755yxfvpzJkyezfPnyIvO88sor1K1bl9WrV3PTTTdxxx13AK7722WXXcbzzz/PsmXLmDlzJnFxcYB79mzx4sUsXbqUHTt28P7770e9bCIi1Z324VWf1qGIHA3U+BYBFixYQLt27WjTpg3x8fFcfPHFxbqtTZkyhcsvvxyA888/n6+++gprLdOnT6dHjx707NkTgPr16xMbGwtArVq1AHdikJOTgzEmiqUSETk6aB9e9WkdisjRQI1vOSqNGzeO+Ph44uLiOOOMM9i8eTPNmzf3/16/fn0efPBB4uLiSE5OZs6cOWzevJktW7Zg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iaFfv35FYnJycrjqqqv8V6rbt2/vj9u1axennHIKycnJXH/99cXiRowYQYcOHejUqRMffvhhRMosIlKVTZkyhaVLl9KgQYMi+++EhARmzJhBbm4uTzzxBNZa/va3vzF69Gjatm3LE088wYYNG7juuuvo0aMHMTEx1K5dm5NOOont27dzzjnnMHz4cM444wzatWtHnz59KCgo8A88BG4ffvLJJzNx4kT++Mc/kpmZSZs2bbj22mvLvA+/5sniHzlyhDs2wapVq6hbty7GGHr06FEkpk6dOsTHx5OYmMirtyWyZ8sy/299zijaYwLwj/VSGBs41svAgQMBeP7558nKyiIzM7PYWC/BvTCkegk1xsTIkSOLzHPqqacWeTtOSkoKMTExbNiwgbS0NG688UZ/wxsgJiaG5s2b89RTTwHw0ksv+XtMilQHanxHUfCBND8/n1GjRvH555+zfPly3nrrLZo0aVLkQPrKK6/w008/sWbNGhYsWED79u3Jz8/nzjvvZNKkSUyYMIGVK1cSExPjH+Tn2muvZfPmzfz+978nKSmJ1atX06tXL8Dd0fn73/8OuAOm1+ulS5cu5OTkMH78eBo2bMjKlStZvnw5J598cljlXNPxviIfEZEjRajRwIOfQ01OTub7778HfusCecIJJ7Br1y6+++47LrjgAjp27Mj+/fv9++/Jkyfj9XpZs2YNZ511Fm+++Sb5+fkcPHiQ+vXrc9lll/G3v/2Nzp0787///Y9du3Zx//33c+mll7J06VLq16/PRx99RJ06dfj111+56aabuPTSS1mzZg3du3dn69atgNuHjx492p+3goICMjIyaNasWYXsw4P339qHV47yjCJdu3Ztxo8fzyWXXBJy2S+//DKZmZlc+WgmdZt0LfJbuOte283RKdQYE0OHDi3ydpwXX3yxyNtx3njjDQCuuuoqsrKyePrpp0lMTCQxMdE/EOTkyZN54IEHSExM5N///jfvv/9+pZVRpKKp8R0loQ6kjz/+OO3ataNNmzbEx8djjCE/P7/IgfSjjz5i+fLl/O1vf+O8884jLy+Pxx57jPbt23PhhReSnJzMwYMHMcaQkJDA8OHDeeONN8jLy+Oyyy7jwQcf5KKLLiIjIwNrLTVq1KB///488MADXHfddRw8eJB9+/Zx66238q9//Yu77roLcFcedaVRRKqbcO/UXH75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\n", + "text/plain": [ + "
" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_histogram([res.get_counts(), exact_dist], legend=['raw', 'exact'], figsize=(15,6))" + ] + }, + { + "cell_type": "markdown", + "id": "6c9901a9", + "metadata": {}, + "source": [ + "The overall fidelity between the ideal and raw distributions is:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3dfd52a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.839082737475692" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hellinger_fidelity(res.get_counts(), exact_dist)" + ] + }, + { + "cell_type": "markdown", + "id": "b0707ef3", + "metadata": {}, + "source": [ + "## The circuit-runner program\n", + "\n", + "The `run_circuits` method executes a `circuit-runner` for you internally. It is also possible to directly call this program via the `program_id`. First, let's print the program description:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "23de57a7", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "circuit-runner:\n Name: circuit-runner\n Description: A runtime program that takes one or more circuits, compiles them, executes them, and optionally applies measurement error mitigation.\n Creation date: 2021-07-02T13:46:10Z\n Update date: 2021-07-02T13:46:10Z\n Max execution time: 14400\n Input parameters:\n Properties:\n - seed_transpiler:\n Type: integer\n Description: Sets random seed for the stochastic parts of the transpiler.\n Required: False\n - translation_method:\n Enum: ['unroller', 'translator', 'synthesis']\n Type: string\n Description: Name of translation pass ('unroller', 'translator', 'synthesis').\n Required: False\n - transpiler_options:\n Type: object\n Description: Additional compilation options.\n Required: False\n - shots:\n Default: 1024\n Description: Number of repetitions of each circuit, for sampling. Default: 1024.\n Type: integer\n Required: False\n - circuits:\n Description: A circuit or a list of QuantumCircuits.\n Type: ['object', 'array']\n Required: True\n - init_qubits:\n Type: boolean\n Description: Whether to reset the qubits to the ground state for each shot.\n Required: False\n - layout_method:\n Enum: ['trivial', 'dense', 'noise_adaptive', 'sabre']\n Type: string\n Description: Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre')\n Required: False\n - initial_layout:\n Description: Initial position of virtual qubits on physical qubits.\n Type: ['object', 'array']\n Required: False\n - rep_delay:\n Type: number\n Description: Delay between programs in seconds.\n Required: False\n - routing_method:\n Enum: ['basic', 'lookahead', 'stochastic', 'sabre']\n Type: string\n Description: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre').\n Required: False\n - optimization_level:\n Min: 0\n Type: integer\n Default: 1\n Description: How much optimization to perform on the circuits (0-3). Higher levels generate more optimized circuits. Default is 1.\n Max: 3\n Required: False\n - measurement_error_mitigation:\n Type: boolean\n Default: False\n Description: Whether to apply measurement error mitigation. Default is False.\n Required: False\n Interim results:\n none\n Returns:\n Description: Circuit execution results in a RunnerResult object.\n Type: object\n" + } + ], + "source": [ + "program = provider.runtime.program('circuit-runner')\n", + "print(program)" + ] + }, + { + "cell_type": "markdown", + "id": "19df4562", + "metadata": {}, + "source": [ + "To run the program, we need the custom circuit runner results class, `RunnerResult`, from the IBM Quantum package:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "204aad90", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.providers.ibmq import RunnerResult\n", + "\n", + "# Specify the program inputs here.\n", + "program_inputs = {\n", + " 'circuits': qc,\n", + " 'shots': 2048,\n", + " 'optimization_level': 0,\n", + " 'initial_layout': [0,1,4,7,10,12],\n", + " 'measurement_error_mitigation': False\n", + "}\n", + "# Specify the backend.\n", + "options = {'backend_name': backend.name()}\n", + "\n", + "# Send circuits to the cloud for execution by the circuit-runner program.\n", + "job2 = provider.runtime.run(program_id=\"circuit-runner\",\n", + " options=options,\n", + " inputs=program_inputs,\n", + " result_decoder=RunnerResult\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9955d4d3", + "metadata": {}, + "outputs": [], + "source": [ + "res2 = job2.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "55c59d81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.794535071278472" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hellinger_fidelity(res2.get_counts(), exact_dist)" + ] + }, + { + "cell_type": "markdown", + "id": "b35132ff", + "metadata": {}, + "source": [ + "## Measurement error mitigation in the Cloud\n", + "\n", + "One of the unique options for the Qiskit Runtime circuit-runner is the ability to correct for measurement errors automatically in the cloud. To enable this, just set `measurement_error_mitigation=True` in the `run_circuits` method or `measurement_error_mitigation: True` in the `program_inputs` for the `circuit-runner` program." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b2c1990e", + "metadata": {}, + "outputs": [], + "source": [ + "job3 = provider.run_circuits(qc, backend, shots=2048, initial_layout=[0,1,4,7,10,12], \n", + " measurement_error_mitigation=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "e0b476e8", + "metadata": {}, + "outputs": [], + "source": [ + "res3 = job3.result()" + ] + }, + { + "cell_type": "markdown", + "id": "adb9712b", + "metadata": {}, + "source": [ + "The mitigated results are returned as quasiprobabilities; a distribution that may contain negative values but nevertheless sums to one. These can be accessed similar to counts, using the `get_quasiprobabilities` method:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "fb8676ac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'000000': 0.015020697020227713,\n", + " '000001': 0.01503968527760154,\n", + " '000010': -0.00012627678196427706,\n", + " '000011': 0.030574525060850996,\n", + " '000100': 0.000659834155234161,\n", + " '000101': -0.001063355958793566,\n", + " '000110': -0.0004775999777916211,\n", + " '000111': 0.06031549309142881,\n", + " '001000': 0.0003777367742875846,\n", + " '001001': -0.00018911714421758157,\n", + " '001011': -0.0005497404302188685,\n", + " '001111': 0.002569807761895156,\n", + " '010000': 0.0009733012442038954,\n", + " '010001': 0.0011091309301552864,\n", + " '010011': 0.0028983164662437686,\n", + " '010101': 0.0006953842032121027,\n", + " '010111': 0.0062289131505383925,\n", + " '011011': 0.0005211534611385364,\n", + " '011101': 1.8085574676836907e-05,\n", + " '011110': 0.00010457485981474027,\n", + " '011111': 0.009008930948687141,\n", + " '100000': 0.020757733894230336,\n", + " '100001': 0.013550397423734102,\n", + " '100010': 0.00022053106121490537,\n", + " '100011': 0.038644305848702515,\n", + " '100100': 0.0003334666950873213,\n", + " '100101': -6.858956101837708e-05,\n", + " '100110': -0.0005943967601188724,\n", + " '100111': 0.05970117602149442,\n", + " '101000': -0.0004924095356996335,\n", + " '101001': 0.0008826058626688284,\n", + " '101011': -0.0013611362001075304,\n", + " '101101': 0.001737025111214627,\n", + " '101110': 0.00023664577533407804,\n", + " '101111': 0.0011200992316059554,\n", + " '110000': 0.03567791385094045,\n", + " '110001': 0.030199327570111845,\n", + " '110010': 0.0012285103619610381,\n", + " '110011': 0.07161663042226933,\n", + " '110100': 0.00021431875863761756,\n", + " '110101': 0.0019190227680874988,\n", + " '110110': 0.0006237863586354301,\n", + " '110111': 0.12786449020370413,\n", + " '111000': 0.0020301695201953808,\n", + " '111001': 0.0029065130997377527,\n", + " '111010': 0.00025079508226566666,\n", + " '111011': 0.022803871645841876,\n", + " '111100': 0.006655111831611918,\n", + " '111101': 0.0024129361534944403,\n", + " '111110': 0.002880228496461319,\n", + " '111111': 0.4123394393204907}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "quasi = res3.get_quasiprobabilities()\n", + "quasi_binary = quasi.binary_probabilities()\n", + "quasi_binary" + ] + }, + { + "cell_type": "markdown", + "id": "240bc428", + "metadata": {}, + "source": [ + "Quasiprobabilities can be directly used to compute things like expectation values:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "fd8b7452", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Raw expectation value: 0.2158203125\n", + "Mitigated expectation value: 0.32066344764961213\n", + "Exact expectation value: 0.4374999999999999\n" + ] + } + ], + "source": [ + "print(\"Raw expectation value:\", expectation_value(res3.get_counts())[0])\n", + "print(\"Mitigated expectation value:\", expectation_value(quasi_binary)[0])\n", + "print(\"Exact expectation value:\", expectation_value(exact_dist)[0])" + ] + }, + { + "cell_type": "markdown", + "id": "5d22a386", + "metadata": {}, + "source": [ + "It is also possible to compute the closest true probability distribution (in terms of the Euclidean norm):" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "c5daccf0", + "metadata": {}, + "outputs": [], + "source": [ + "nearest_probs = quasi.nearest_probability_distribution().binary_probabilities()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "e5c7f20e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/AAAAGSCAYAAABE/Cp2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAACjhklEQVR4nOzdd3gVVf7H8fdJbiohJAQCpFCS0EFaEAuGSLdhQ2QtWEAXsKy6LrIrUuw/dRUVXNe22FHRFWwsyAqBFemggIYWhNANJYT05Pz+uMk1N7kBAqnweT3PfZI7d84958zMnZnvnDNnjLUWEREREREREandvGq6ACIiIiIiIiJyYgrgRUREREREROoABfAiIiIiIiIidYACeBEREREREZE6QAG8iIiIiIiISB2gAF5ERERERESkDnDUdAGqSqNGjWzLli1ruhgiIiIiIiI1YtWqVb9ZaxufYJ5wh8PxBtAJNfDWtEJgfX5+/qgePXrs9zTDGRvAt2zZkpUrV9Z0MURERERERGqEMebXE83jcDjeaNq0afvGjRsf8vLystVRLvGssLDQHDhwoMPevXvfAIZ4mkdXWERERERERM5enRo3bpyu4L3meXl52caNGx/B2RvC8zzVWB4RERERERGpXbwUvNceReui3DhdAbyIiIiIiIhIHaAAXkRERERERKQOOGMHsRMREREREZGKuWMqPary+1+/j1VV+f1nOrXAi4iIiIiISK2Wl5dX00WoFRTAi4iIiIiISK0TGRnZ+eGHH27apk2bDvXq1es+bty4ZtHR0Z3q1avXLTY2tuM777wTUjxvRERE58WLFwcC/OMf/2hojOmxcuVKf4AXXnihUf/+/WNrqBqVSgG8iIiIiIiI1Eqffvppw6+//nrzwYMH17Rr1y578eLFyenp6WvGjx+/+49//GOrX3/91QegV69eR7/99tv6AElJSfWjoqJyFixYUPw+6KKLLjpak/WoLArgRUREREREpFYaPXr0vri4uLygoCB7++23H2rZsmWet7c3d9xxx6EWLVrkLF68uB5AQkJCxuLFi+sDLFu2LOj+++/fW+J9/X79+mXUZD0qiwJ4ERERERERqZVatGjhuvl92rRpYe3atetQv379rvXr1++6ZcuWgAMHDjgABgwYcHTlypVBv/76q09hYaG59dZbD65cuTIoOTnZ9+jRo97nn39+Zs3VovJoFHoRERERERGplYwxFmDTpk2+DzzwQIsvvvhiU79+/TIcDgft2rXrYK0FoFOnTjn+/v6Fzz77bHivXr2ONmzYsLBRo0Z5L7/8cuP4+PgMb2/vGq1HZVELvIiIiIiIiNRqR48e9TLG0LRp0zyAF198MWzLli0BJefp1avX0RkzZoQnJCQcBbjwwguPzpgxI7x3795nxP3voABeREREREREarkePXpk33nnnfsSEhLaN27cuMtPP/0U0K1bN7f72hMSEo4eO3bMa8CAARkAiYmJR48dO+Z18cUXnxH3vwOY4i4HZ5r4+Hi7cuXKmi6GiIiIiIhIjTDGrLLWxh9vnnXr1m3v0qXLb9VVJjmxdevWNerSpUtLT5+pBV5ERERERESkDlAALyIiIiIiIlIHKIAXERERERERqQMUwIuIiIiIiIjUAQrgRUREREREROoABfAiIiIiIiIidYACeBEREREREZE6QAG8iIiIiIiISB2gAF5ERERERETqrM2bN/sGBgZ2y8/PL3eewMDAbhs3bvStxmJ5lJyc7GuM6ZGXl3dK6R2VXB4RERERERGpo7a2ndyjKr8/Nnnyqsr+ztatW+dmZmauKX5/7rnnth0+fHjaAw888FvxtJKfn44HHnggYuvWrX6zZ89OqYzvqyi1wIuIiIiIiIjUAQrgRUREREREpNaJjIzs/MgjjzRp06ZNh4CAgG7Dhg1rsXPnTkdCQkLrevXqdbvgggvaHDhwwLtkt/R77rknctWqVUF//etfmwcGBnYbMWJEcwBjTI/169f7Aezdu9e7b9++cUFBQd06derU/t57743o0aNH2+J8b7vttuimTZueExQU1K1jx47t586dGwQwa9as4JdffrnpV199FRoYGNitbdu2HQDS0tK8hw0b1qJx48bnhIeHn3PvvfdGFHfnz8/P584774wKDQ3tEhUV1fmzzz5rcDrLRAG8iIiIiIiI1Epz5swJXbBgwaaNGzeu//bbb0MGDRrU+qmnnko9cODA2sLCQp5++unwkvO//PLLu3r06JHx1FNP7cjMzFzzzjvv7Cj9naNGjWoRGBhYuGfPnnVvv/12yscff9yo5Oc9e/Y8tnbt2g2HDh1aM3To0IM33XRTbGZmphk6dGj6Pffcs/eyyy47lJmZuSY5OXkjwPDhw1s6HA62bt26fs2aNRu/++67Bi+88EIjgOeff77x/PnzG6xYsWLjqlWrNn7++eehp7M8FMCLiIiIiIhIrTR69Oj90dHR+a1atcrr2bNnRrdu3Y5deOGFWYGBgfaKK644vG7dusCKfF9+fj5z584NeeKJJ3bVr1+/sEePHtnDhg37reQ8Y8eOPdi0adMCHx8fpkyZsi83N9esW7fO39P37dy507Fo0aIGr7322o7g4ODCyMjI/LvvvnvfrFmzGgJ89tlnoWPGjNkfFxeX16RJk4KHHnpo76kvDQ1iJyIiIiIiIrVUs2bNXMO1+/v7FzZp0sQ11HxAQEBhZmamd0W+b/fu3Y6CggITExPj+t7o6OjckvNMnDixyXvvvdfowIEDPgDHjh3z3r9/v8fYecuWLb75+fmmWbNmXYqnWWtN06ZNcwH27dvn07x5c9f3x8bG5lSkvKWpBV5ERERERM56c+fOpW3btsTFxfH000+XO9+nn36KMYaVK1cCkJaWxsUXX0xQUBB33323a77MzEwuu+wy2rVrR8eOHRk/fnyV10GcjDG2vM8iIiLyvb29bUpKik/xtJ07d7oeLzd37tygadOmNZ05c+a2w4cPrz169OjaoKCgAmutx++OiYnJ8/X1tQcPHlx79OjRtUePHl2bkZGxZsuWLRsAwsPD83bs2OH6/m3btvmdTt0UwIuIiIiIyFmtoKCAu+66i2+++YaNGzfy4YcfsnHjxjLzHT16lBdffJFevXq5pvn7+/PYY4/x3HPPlZn/wQcf5JdffmHNmjX873//45tvvqnSeohT48aN88sLlB0OB4MGDTr88MMPRxw9etRrzZo1/p988klY8edHjhzxdjgctmnTpnl5eXnmwQcfbHbs2DFXK3+TJk3yU1NTfQsKCgBo0aJF3oUXXnjkzjvvjD548KBXQUEBGzZs8Pvqq6+CAK655ppD//znP8O3bt3qc+DAAe9nnnmm6enUTQG8iIiIiIic1ZYvX05cXBwxMTH4+voyfPhwZs+eXWa+Rx55hIceegh//99vh65Xrx69e/d2mwYQGBjIxRdfDICvry/du3cnNTW1aisiANx33337vvzyy9Dg4OCut956a3Tpz19//fUdR48e9W7WrFmXm2++udVVV1110NfX1wJce+21R/r06ZPeoUOHztHR0Z39/f0Li7vDA4wYMeIgQGhoaNcOHTq0B/j444+35+bmmvbt23cKCQnpOnTo0Nhdu3b5ADzwwAMHEhMT03v06NGxa9euHYYMGXLodOpmirsCnGni4+NtcbcWERERERGR8syaNYu5c+fyxhtvAPDuu++ybNkypk2b5ppn9erVPPHEE3z66ackJiby3HPPER8f7/p8xowZrFy50i1NscOHD9O9e3e+/fZbYmJiqr5CRYwxq6y18cebZ926ddu7dOny2/HmOdONGTMmct++fT6fffbZ9pouC8C6desadenSpaWnz9QCLyIiIiIichyFhYU88MAD/P3vf69w2vz8fP7whz9w7733VmvwLuVbs2aN/7JlywIKCwv57rvvAmfOnNno6quvPlzT5ToZGoVeRERERETOapGRkezcudP1PjU1lcjISNf7o0ePsn79ehITEwHYu3cvQ4YMYc6cOW6t8J7ceeedtG7dmvvuu68qii6nID093evmm2+OOXDggE/Dhg3zR48eve/GG288XNPlOhkK4EVERERE5KzWs2dPNm/eTEpKCpGRkcycOZMPPvjA9XmDBg347bffe5l76kLvyYQJEzhy5Iira77UDn369MncsWPH+poux6lQAC8iIiIiImc1h8PBtGnTGDRoEAUFBdx+++107NiRiRMnEh8fz5AhQ46bvmXLlqSnp5Obm8vnn3/OvHnzCA4O5oknnqBdu3Z0794dgLvvvptRo0ZVR5XkDKVB7ERERERERM5AJzmI3bbOnTsf8vLyOjMDwzqmsLDQ/PTTT6FdunTxOGCCBrETERERERE5e60/cOBAg8LCQlPTBTnbFRYWmgMHDjQAyu3ery70IiIiIiIiZ6n8/PxRe/fufWPv3r2dUANvTSsE1ufn55d7n4UCeBERERERkbNUjx499gPHv8lfag1dYRERERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOUAAvIiIiIiIiUgcogBcRERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gBHTRdARERERESkrtjadnKZabHJZaeJVAW1wIuIiIiIiIjUAQrgRUREREREROoABfAiIiIiIiIidUC1B/DGmLHGmBRjTLYxZpUx5qKTTNfbGJNvjFlf1WUUERERERERqW2qNYA3xlwPvAg8CXQDvge+McY0P0G6UOAdYEGVF1JERERERESkFqruFvgHgBnW2tettT9ba+8B9gBjTpDuTeBtYGlVF1BERERERESkNqq2AN4Y4wv0AOaV+mgecMFx0o0FmgCPV13pRERERERERGq36nwOfCPAG9hXavo+oL+nBMaYzsAk4DxrbYEx5rgZGGPuBO4EiIiIYOHChQDExMRQv3591q1bB0BYWBgdO3YkKSkJAIfDQe/evVm9ejXp6ekAxMfHs2/fPnbu3AlA69at8fPzY/165y344eHhtGnThiVLlgDg5+fH+eefz8qVK8nIyACgV69epKamsmvXLgDatm2Lt7c3GzduBKBp06a0atWKpUudHQsCAgLo1asXy5YtIysrC4Dzzz+flJQU9u7dC0CHDh0oKCggOTkZgMjISKKioli2bBkAQUFBxMfHs3TpUnJycgDo3bs3mzZtYv/+/QB06tSJnJwcNm/eDEB0dDRNmjRh5cqVAAQHB9O9e3eWLFlCfn4+AAkJCWzYsIG0tDQAunTpwtGjR9m2bRsALVu2pGHDhqxevRqA0NBQunTpwqJFi7DWYoyhT58+rFu3jkOHDgHQvXt3Dh48yPbt27WetJ60nrSetJ60nrSetJ60nrSe6sR68qGshQsX1tr1JGcWY62tnoyMiQB2AX2stUklpk8EbrTWti01vx+wBnjKWvtu0bTJwFBrbacT5RcfH2+LN3wREREREZHKsLXt5DLTYpPLTqsNjDGrrLXxNV0OqTzV2QL/G1CAszt8SU2AvR7mbwa0B/5ljPlX0TQvwBhj8oFLrbWlu+OLiIiIiIiInJGq7R54a20usAoYUOqjAThHoy9tF9AZ6Fri9Sqwpeh/T2lEREREREREzkjV2QIP8DzwrjFmOfA/YDQQgTMwxxjzDoC1doS1Ng9we+a7MWY/kGOt1bPgRURERERE5KxSrQG8tfYjY0wYMAFnF/n1OLvC/1o0y3GfBy8iIiIiIiJytqruFnista8Ar5TzWeIJ0k4GJld6oURERERERERquWq7B15ERERERERETp0CeBEREREREZE6QAG8iIiIiIiISB2gAF5ERERERESkDlAALyIiIiIiIlIHKIAXERERERERqQMUwIuIiIiIiIjUAQrgRURERETkrPL444/j6+uLj48PgwcPLvP5DTfcgL+/PwEBAQQHBzNnzhy3z9dk7SRu0xTuSP3ANe2qq67C398ff39/WrRoweHDh6u6GnIWUgAvIiIiIiJnjdzcXKZMmcK8efM4dOgQixYtKhOgP/PMM2RnZ5OVlcU999zD7bff7vb53bs/oYl3fdf7lStX8tVXX7F7926ys7MpLCzkwQcfrJb6yNlFAbyIiIiIiJw1ZsyYQYMGDUhMTCQoKIiEhASmT5/uNk9UVJTr//T0dIwxrvfPHviWcEcQLXwauqWx1nLo0CGys7PJzc0lNja2aisiZyUF8CIiIiIictZITk4mLCzM9b5Vq1bs2bOnzHzDhg3Dx8eHf/zjH3zwgbOr/N69e3n38Apei7zBbd74+HiGDBlCXFwcgYGBBAQE8Ne//rVqKyJnJQXwIiIiIiIipXz88cfk5eXxxz/+kdGjRwNw+eWXM6xBNxo7gtzmTUlJ4bvvvmPjxo1kZGSQk5PDmDFjaqLYcoZz1HQBREREREREqkvbtm15++23Xe9TUlJo1qxZufO/+OKL+Pj4ALB582bWph/l3cPLKcBicN4P3/7ldBo3bkz79u0BuOKKK1i8eHGV1kPOTmqBFxERERGRs8aIESM4cuQISUlJZGRkkJSUxNixY93mmT9/vuv/KVOmEBgYCMCRI0dIbjOR5DYTOde/BYmBrZkWcR0dO3Zkx44d/PbbbxQWFrJgwQLatWtXrfWSs4MCeBEREREROWv4+/szYcIE+vfvT2hoKL179+bKK68kISGBhx9+GIC//OUvrsfITZs2zXUPfHlGjhxJr169iIyMJDAwEGstb731VnVUR84yxlpb02WoEvHx8XblypU1XQwRERERETmDbG07ucy02OSy02oDY8wqa218TZdDKo9a4EVERERERETqAAXwIiIiIiIiInWAAngRERERERGROkABvIiIiIiIiEgdoABeREREREREpA5QAC8iIiIiIiJSByiAFxEREREREakDFMCLiIiIiIiI1AEK4EVERERERETqAAXwIiIiIiIiInWAAngRERERERGROkABvIiIiIiIiEgd4KjpAoiIiIiIiNRGd0wtO218tZdC5HdqgRcRERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOUAAvIiIiIiIiUgcogBcRERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOUAAvIiIiIiIiUgcogBcRERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOUAAvIiIiIiIiUgcogBcRERERERGpAyoUwBtjvIwxXiXeNzXGjDLGXFj5RRMRERERERGRYhVtgf8KuAfAGBMErASeBRYaY0aczBcYY8YaY1KMMdnGmFXGmIuOM28fY8z3xpg0Y0yWMeYXY8yDFSyziIiIiIiISJ1X0QA+Hvhv0f/XAOlAOHAHcMLA2hhzPfAi8CTQDfge+MYY07ycJBnAS0AC0AF4HJhijBlbwXKLiIiIiIiI1GkVDeCDgMNF/w8E/m2tzcMZ1MeeRPoHgBnW2tettT9ba+8B9gBjPM1srV1lrZ1prd1grU2x1r4H/Acot9VeRERERERE5ExU0QB+B3ChMaYeMAiYXzS9IZB5vITGGF+gBzCv1EfzgAtOJnNjTLeieRdVoMwiIiIiIiIidZ6jgvM/D7yLs2v7r0BS0fQE4KcTpG0EeAP7Sk3fB/Q/XkJjTCrQuKi8U6y1r5Yz353AnQAREREsXLgQgJiYGOrXr8+6desACAsLo2PHjiQlOYvvcDjo3bs3q1evJj09HYD4+Hj27dvHzp07AWjdujV+fn6sX78egPDwcNq0acOSJUsA8PPz4/zzz2flypVkZGQA0KtXL1JTU9m1axcAbdu2xdvbm40bNwLQtGlTWrVqxdKlSwEICAigV69eLFu2jKysLADOP/98UlJS2Lt3LwAdOnSgoKCA5ORkACIjI4mKimLZsmUABAUFER8fz9KlS8nJyQGgd+/ebNq0if379wPQqVMncnJy2Lx5MwDR0dE0adKElStXAhAcHEz37t1ZsmQJ+fn5ACQkJLBhwwbS0tIA6NKlC0ePHmXbtm0AtGzZkoYNG7J69WoAQkND6dKlC4sWLcJaizGGPn36sG7dOg4dOgRA9+7dOXjwINu3b9d60nrSetJ60nrSetJ60nrSetJ6qnXr6WQtXLiw1q4nObMYa23FEhgTD0QD8621GUXTLgMOW2v/d5x0EcAuoI+1NqnE9InAjdbatsdJ2wpn9/3zgP8D/mStffd45YyPj7fFG76IiIiIiEhF3TG17LTx/5hcZlpsctlptYExZpW1Nr6myyGVp6It8FhrV+Icfb7ktK9OIulvQAHQpNT0JsDeE+SZUvTvT8aYJsBknD0BRERERERERM4KFb0HvvgxcBuMMZnGmJiiaQ8ZY4YdL521NhdYBQwo9dEAnKPRnywvwK8iZRYRERERERGp6yoUwBtj7gMmAK8BpsRHu4G7T+IrngduNcaMMsa0N8a8CEQArxZ9/zvGmHdK5HePMeZyY0zrotdInI+re68i5RYRERERERGp6yrahX40cIe19itjzOMlpq8GOp4osbX2I2NMGM6LAM2A9cCl1tpfi2Yp/Tx4b5z3vLcE8oGtwHiKAn4RERERERGRs0VFA/gWOIPu0vKAgJP5AmvtK8Ar5XyWWOr9VGBqRQooIiIiIiIiciaq6D3w24DuHqZfCmw8/eKIiIiIiIiIiCcVbYF/DphmjAnEeQ/8+caYm4FxwO2VXTgRERERERERcapQAG+t/ZcxxgE8CQTifJTbbuBea+1HVVA+EREREREREeHUngP/OvC6MaYR4GWt3V/5xRIRERERERGRkiocwBez1v5WmQURERERERERkfKdMIA3xvwI9LHWHjLG/ATY8ua11p5TmYUTEREREREREaeTaYH/FMgp8X+5AbyIiIiIiIiIVI0TBvDW2ikl/p9cpaUREREREREREY8q9Bx4Y8x/jTEhHqYHG2P+W2mlEhERERERERE3FQrggUTA18N0f+Ci0y6NiIiIiIiIiHh0UqPQG2O6l3h7jjHmYIn33sAgYFdlFkxEREREREREfneyj5FbiXPwOgvM8/B5FnBPZRVKRERERERERNydbADfCjDANuBc4ECJz3KB/dbagkoum4iIiIiIiIgUOakA3lr7a9G/Fb1nXkREREREREQqwQkDeGPMNcAX1tq8ov/LZa39rNJKJiIiIiIiIiIuJ9MCPwtoCuwv+r88FueAdiIiIiIiIiJSyU4YwFtrvTz9LyIiIiIiIiLVRwG5iIiIiIiISB1wsvfAnxTdAy8iIiIiIiJSNU72HviToXvgRURERERERKpIhe6BFxEREREREZGaoeBcREREREREpA7Qc+BFRERERERE6gA9B15ERERERESkDtBz4EVERERERETqAAXkIiIiIiIiInVAhQN4Y0x3Y8w7xpiVRa93jTHdq6JwIiIiIiIiIuJUoQDeGHMjsAJoBnxd9GoCLDfG3FT5xRMREREREREROLlB7Ep6AnjEWvtkyYnGmL8CjwPvVVbBREREREREROR3Fe1C3xj42MP0T4Dw0y+OiIiIiIiIiHhS0QD+OyDRw/REYNHpFkZEREREREREPDthF3pjzDUl3n4DPGWMiQd+KJp2HnANMLnSSyciIiIiIiIiwMndAz/Lw7Q7i14lvQy8ctolEhEREREREZEyThjAW2v1rHgRERERERGRGqbgXERERERERKQOqOhj5DDGhAKXAM0B35KfWWsfraRyiYiIiIiIiEgJFQrgjTHnAV8BOTgfKbcLaFb0fjugAF5ERERERESkClS0C/2zwPtAJJAN9MXZEr8S+L/KLZqIiIiIiIiIFKtoAH8OMM1aa4ECwM9auw94CD1GTkRERERERKTKVDSAzy3x/z6gRdH/GUBEpZRIRERERERERMqo6CB2q4GewCZgIfC4MaYJcBPwY+UWTURERERERESKVbQF/mFgd9H/E4ADwMtAKHBnJZZLREREREREREqoUAu8tXZlif8P4HycnIiIiIiIiIhUsQo/Bx7AGBMLtC96u9Fau63yiiQiIiIiIiIipVX0OfBhwJvAEKDw98nmS+B2a21aJZdPRERERERERKj4PfBvAHHARYB/0SsBaAW8XrlFExEREREREZFiFe1CPwjoZ61dWmLa/4wxfwS+rbxiiYiIiIiIiEhJFW2BPwAc8zA9E1D3eREREREREZEqUtEA/lFgqjEmsnhC0f9/L/pMRERERERERKrACbvQG2N+AmyJSa2A7caYXUXvI4FsIBznPfIiIiIiIiIiUslO5h74WVVeChERERERERE5rhMG8NbaKdVREBEREREREREpX0VHoQfAGNMX6ICza/0Ga+3CyiyUiIiIiIiIiLirUABfNGDdv4EewO6iyRHGmJXA1dba3eUmFhEREREREZFTVtFR6F8CCoA4a220tTYaaF007aXKLpyIiIiIiIiIOFU0gB8A3GWtTSmeYK3dBtxb9NkJGWPGGmNSjDHZxphVxpiLjjPvNcaYecaYA8aYo8aYZcaYIRUss4iIiIiIiEidV9EAHtwfKXe8aWUYY64HXgSeBLoB3wPfGGOal5OkD/Bf4LKi+b8G/n28oF9ERERERETkTFTRAH4B8LIxJrp4QlHwPbXosxN5AJhhrX3dWvuztfYeYA8wxtPM1to/WWufttYut9ZuKRoRfxVwVQXLLSIiIiIiIlKnVTSAvxeoB2wzxvxqjPkV2Fo07d7jJTTG+OIc/G5eqY/mARdUoAz1gUMVmF9ERERERESkzqvoY+TSgHOBRKBd0bSfrbXfnkTaRoA3sK/U9H1A/5PJ3BhzFxAFvFvO53cCdwJERESwcOFCAGJiYqhfvz7r1q0DICwsjI4dO5KUlASAw+Ggd+/erF69mvT0dADi4+PZt28fO3fuBKB169b4+fmxfv16AMLDw2nTpg1LliwBwM/Pj/PPP5+VK1eSkZEBQK9evUhNTWXXrl0AtG3bFm9vbzZu3AhA06ZNadWqFUuXLgUgICCAXr16sWzZMrKysgA4//zzSUlJYe/evQB06NCBgoICkpOTAYiMjCQqKoply5YBEBQURHx8PEuXLiUnJweA3r17s2nTJvbv3w9Ap06dyMnJYfPmzQBER0fTpEkTVq5cCUBwcDDdu3dnyZIl5OfnA5CQkMCGDRtIS0sDoEuXLhw9epRt27YB0LJlSxo2bMjq1asBCA0NpUuXLixatAhrLcYY+vTpw7p16zh0yHn9pXv37hw8eJDt27drPWk9aT1pPWk9aT1pPWk9aT1pPdW69XSyFi5cWGvXk5xZjLUndfs6xhhvIBvoYq3dWOGMjIkAdgF9rLVJJaZPBG601rY9QfprcQbu11trvzhRfvHx8bZ4wxcREREREamoO6aWnTb+H5PLTItNLjutNjDGrLLWxtd0OaTynHQXemttAfAr4HuKef2G83FzTUpNbwLsPV5CY8xQnMH7iJMJ3kVERERERETONBW9B/4x4GljTKOKZmStzcU5AF3px80NwDkavUfGmGE4g/dbrbWzKpqviIiIiIiIyJmgovfAPwi0AnYZY1KBYyU/tNaec4L0zwPvGmOWA/8DRgMRwKsAxph3ir5nRNH74TiD9weBJGNM06LvybXWHqxg2UVERERERETqrIoG8LNwPvPdnEpm1tqPjDFhwASgGbAeuNRa+2vRLKWfBz+6qIxTi17FFuEcSE9ERERERETkrHBSAbwxJhB4Fufz131wPvP9HmvtbxXN0Fr7CvBKOZ8lHu+9iIiIiIiIyNnqZO+BnwLcCnwFfIjzsW//qKIyiYiIiIiIiEgpJ9uF/hpgpLV2JoAx5n3gf8YY76LR6UVERERERESkCp1sC3w0sLj4jbV2OZCPcwA6EREREREREaliJxvAewO5pablU/FB8ERERERERETkFJxsAG6A94wxOSWm+QOvG2MyiydYa4dUZuFERERERERExOlkA/i3PUx7rzILIiIiIiIiIiLlO6kA3lp7W1UXRERERERERETKd7L3wIuIiIiIiIhIDVIALyIiIiIiIlIHKIAXERERERERqQMUwIuIiIiIiIjUAQrgRUREREREROoABfAiIiIiIiIidYACeBEREREREZE6QAG8iIiIiIiISB2gAF5ERERERESkDlAALyIiIiIiIlIHKIAXERERERERqQMUwIuIiIiIiIjUAQrgRUREREREROoABfAiIiIiIiIidYACeBEREREREZE6QAG8iIiIiIiISB2gAF5ERERERESkDlAALyIiIiIiIlIHKIAXERERESnl8ccfx9fXFx8fHwYPHlzm85dffpnAwECMMfz5z392Tf/f//5HYGAgAQEB+Pv7c8MNN7g+e++99/D398fHx4cuXbpQWFhYLXURkTOHAngRERERkRJyc3OZMmUK8+bN49ChQyxatIg5c+a4zdOtWzfeeecdYmJi3KZ36dKFvXv3kpWVxfbt2/n4449ZvXo1AKNHj2batGnk5OSwe/duHn/88Wqrk4icGRTAi4iIiIiUMGPGDBo0aEBiYiJBQUEkJCQwffp05s6dS9u2bYmLi2PJkiUMHToUY4wrXVJSEgkJCTRs2JBZs2Zx9OhRAJKTkznnnHPIzMzkpZde4pNPPmH48OF8+OGHLFiwgO7du9O1a1d69+7Nli1baqraIlIHKIAXERERESkhOTmZsLAw1/tWrVqxe/du7rrrLr755hs2btzIhx9+yMaNG93SNW/enBkzZjBw4EBuuOEG2rRpwzXXXEOPHj246aabCAkJYe7cudx3331ER0dz8OBBxowZw/vvv8/atWu54YYb1CovIselAF5ERERE5AQyMzOJi4sjJiYGX19fhg8fzuzZs93madmyJeeccw7h4eF88MEHrFq1iq+//prc3FyaNm0KQEREBOHh4WRkZABgjCE9PR2AI0eOEBERUb0VE5E6xVHTBRARERERqU3atm3L22+/7XqfkpJCUFAQ0dHRrmlRUVEsW7bsuN/TvXt3IiIi+Ne//sXNN9/MsWPHWL58Obm5uRw8eJCGDRvy2muvcemllxIQEEBwcDA//PBDldVLROo+tcCLiIiIiJQwYsQIjhw5QlJSEhkZGSQlJXkcid6TFStWkJOTAzgD/+3bt3PBBRfQtWtXvL29GTJkCG+++SYfffQRw4cP54UXXuDrr78mNTWV2267jQceeKAqqyYidZwCeBERERGREvz9/ZkwYQL9+/cnNDSU3r17c9VVV/Hpp5/y8MMPAzBnzhxeeeUVtm7dygsvvIC/vz8ACxYs4OOPP+aGG26gQ4cODBs2jGuvvZb09HTCw8NJS0ujT58+NGvWjNGjR7Nu3Tp69eoFwPXXX8/3339fY/UWkdpPAbyIiIiISCmTJk0iNzeXvLw85s+fT8+ePQkNDWXUqFHk5uayadMmfvrpJ6y1FBYWkp2dDcD48eO5+eab+eCDD8jKyuK9994jNzeXq6++mvvuu4+8vDzy8vL48ccfCQsL48iRI2zatAmA+fPn0759+5qstojUcroHXkRERETkBBwOB9OmTWPQoEEUFBRw++2307FjRyZOnEh8fDxDhgxhxYoVXH311Rw6dIgvvviCSZMmsWHDBj7++GOSkpJIS0tjxowZgPNRdV27duX111/n2muvxcvLi9DQUN56662araiI1GrGWlvTZagS8fHxduXKlTVdDBERERERqaPumFp22vh/TC4zLTa57LTawBizylobX9PlkMqjLvQiIiIiIiIidYACeBEREREREZE6QAG8iIiIiIiISB2gQexERERERE6Rp3ukX7+vukshImcLtcCLiIiIiIiI1AFqgRcRERERqURb204uM622jlIuInWLWuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOUAAvIiIiIiIiUgcogBcRERERERGpAxTAi4iIiIiIiNQBCuBFRERERERE6gAF8CIiIiIiIiJ1gAJ4ERERERERkTpAAbyIiIiIiIhIHaAAXkRERERERKQOqPYA3hgz1hiTYozJNsasMsZcdJx5mxljPjDG/GKMKTDGzKjGooqIiIiIiIjUGtUawBtjrgdeBJ4EugHfA98YY5qXk8QP+A14GlhWLYUUERERERERqYWquwX+AWCGtfZ1a+3P1tp7gD3AGE8zW2u3W2vvtdbOAA5WYzlFREREREREapVqC+CNMb5AD2BeqY/mARdUVzlERERE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IiIicrIUwIuU4umEFnRSKyIiIiIiNUvPgRcRkbPS3Llzadu2LXFxcTz99NNlPs/JyeH6668nLi6OXr16sX37dgCWL19O165d6dq1K126dOHf//63K82LL75Ip06d6NixI1OnTq2mmoiIiMjZQgG8iIic8R5//HF8fX3x8fFh8ODBFBQUcNddd/HNN9+wceNG3n//fZo1a4aPjw9BQUEsWbKEN998k/3797Nr1y7WrFlDbGwsDz30EJ06deLxxx8nOTmZ5ORkrrnmGowx9OvXj9dff53ly5ezbt06vvzyS7Zs2VLTVRcREZEziAJ4ERE5o+Xm5jJlyhTmzZvHoUOHWLRoEX//+9+Ji4sjJiYGX19fjDEUFBSQl5fH7bffzg033MDs2bMZN24chw4dIjMzk+DgYJ555hm8vLy4/PLLycrK4ueffyY8PBxjDImJifTq1YvAwEAcDgd9+vThs88+q+nqSx1SFb1CDh8+zNChQ2nXrh3t27dn6dKl1VUdqWNKX+gsLT09nebNm7td6AT417/+RUBAgOv10EMPudI4HA78/f0JCAigXr161VYXkTOZAngRETmjzZgxgwYNGpCYmEhQUBAJCQm8//77REdHu+ZJSUmhY8eOADzzzDOkpqaya9cuOnfujL+/Pw6Hg8DAQNf8y5Yto2PHjnTu3JkBAwYQFBTE0KFDWbx4MWlpaWRmZvL111+zc+fOaq+v1E2le4V8+OGHbNy40W2eN998k9DQULZs2cL999/vCpQ6derEypUrWbt2LXPnzuWPf/wj+fn5APzpT39i8ODB/PLLL6xbt4727dtXe92k9vN0oXPOnDlu89x5550EBQW5XegEuOKKKzh06BBZWVksXbqUZ599luzsbFe6devWkZWVxbFjx6q1TiJnKgXwIiJyRktOTiYsLMz1vlWrVhw+fNhtnuzsbBo3bgyAv78/3t7e5ObmAs6gyd/fn927d3P33Xfj7+9Pr1692LBhAytWrGDWrFn079+f9u3b89BDDzFw4EAGDx5M165d8fb2rrZ6St22fPlyt14hw4cPZ/bs2W7zzJ49m1tuuQWAoUOHsmDBAqy1rl4f4NyWjTEAHDlyhKSkJEaOHAmAr68vISEh1VcpqTM8XeicPn262zzffvst9913H/D7hc7CwkIaNWqEv78/4GylF5GqpQBe3FRF970TfaeISHVz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4Yf4+voSExPD0KFDWbBgAaGhoSxYsIDhw4e7uuFt376dSy+9FGMMU6dOdbWsJycnc9ttt9G3b1/q16+PMYbU1FS+/PJLV0vabbfd5joBKdmSlpubyyOPPOJqLfnuu+9cJ0GvvPIKd999N++99x7e3t788MMPWGtZvXo1a9asYd++fSxatKjM76Hkstq2bRtffvklc+bMqdLt6r///a9bV/aZM2eWuSDYotMQNi93dmWfNWuW6+LOkCFDmDlzJjk5OaSkpLB582bOPfdcALKO7gcg49AOtv/4GUPqdy63jJWpNuzvPv30U4+/Bfh9f5efn1+hfWVVbBuno2S+AwcOZMqUKTz++OO0bNmSefPmMWLEiCpfVp7S3nDDDdx111389a9/ZcaMGRQUFHjsvnvkyBGsta7uu5MnT2bdunV8++23vPnmm2zbto2IiAi++OILFixYQPv27dmyZQvJycn87W9/Y+PGjWWW84mWe/GFg7i4OFfgUay4pbR47IxiSR/eznuPhPPp/53e2DWnE1iW17r73nvv4evrS8uWLXnrrbf48ssv+XX9HLcyn87vaOvWrWRlZbl+CytXrmTv3r38/e9/57nnniMnJwdjDElJSW69zGbNmkV+fj5+fn5MmjSJRx99lICAAJYvX87//vc/rLU8+uijGGNo1KgRo0eP5pNPPqGgoIDvv/+eDz74gO7du/P9999zxx13UFhY6DqeFN+2mJmZSWFhYZnHVHpqkX738DK34++GDRsYNmwY4H4xet++fQQGBrqCcF9fX1atWuV6ctF//vMf19hI3t7evPDCCzgcDlq2bMmxY8cICQnxeBy8/vrrmT59Oo0aNXI9brPkcfDnn3/GGFNt50nFGjRoQG5uLklJSVhrycrKomHDhixatAhrLdOmTcPHx4djx45x7bXXMm7cOCIiIvjss89o1aqVa0DU22+/ndzcXBo2bEhubi6ff/656ze0detWHnnkEZ588kmMMXTu3JmCggJ+/PFH17JKS0tzXZQODw9nwIABtG7dml69erEvZQk+fkEAFBbkUViQhx7uKDWqZMvFmfTq0aOHrY1GvVD2taXNpDKvyk6bk5NjHQ6H/e677+zRo0etv7+//eyzz2xMTIzdunWrzcnJseecc47dsGHDCfO9cNg/bYhXgCu/3oExtndgjFuasLAw+89//tNaa21WVpY1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L/Xq1WPUqFHk5uYye/Zsvv/+e7eTniFDhriu9D/88MNcffXV7N69m3vvvZfly5dz+PBhNm7cSL169QgICMBaS8+ePQkKCuLPf/4zkydPpqCggDVr1jB37lzy8/NJTEwkIiKCzp07Ex0dzVVXXVXu9nHo0CEuvvhiFixYADhHl77xxhuZMWMGGRkZPPTQQ+zbt4+//vWvfPDBByQmJroeM5aSkkKPHj3YsmULEydOpGPHjtx+++1ERkbSsmVLvv76a7799lsaN27M9u3bOeecc1xdKWNiYk7593Cqz4Iu7/dQclkVd1+sq7+HmlBb9nfl/RZK5uvl5VWj28bpKJ2vj48PhYWF/Pjjj67fQteuXd3yrYpl5Slt//79ycjIcN3CApTbfTcpKYl33nmHzp07k56ezqRJk5g5cyadOnUiPDycoKAgzj33XNq0acOvv/5K+/bteeCBBzwu5+LbgI633E/laQ6V5XQCy/J6Oe3YscO1TbZr185jmSv7vKFnz55kZGS48vPx8fF4i9PMmTOZMGECAwcO5NChQ1x00UWMHz+eJ554goceeoiUlBT8/f3Jy8vD4XDw2muvcfnll/PAAw9w5ZVX0qVLF9577z369+/PI488wt69ewFo3749AQEBRERE0KNHD3pc8qhb3p5apEc2vICkpCTuuusucnNzsda6nn5Q+mL0xRdf7Bocb8qUKQwdOpSwsDDef/99/Pz8OHbsGImJifztb3/j3HPP5cILL+SDDz4o9zhYskfYe++9x/79+yv1OFjZevbsecqDql599dU0aNCAgQMH0rp1a3x8fBg+fDgrv36Eb14dSEiT9lx88wfkZKYRFxfn1nuu9LKaPn063t7eZKbv4evpF/PpM+cw+4WeRLYdQN+gNmXKLVJtqvume2AskAJkA6uAi04wf5+i+bKBbcDok8lHg9i5y8rKsg6Hwy5atMg1yNGnn35qW7VqZbdt2+YarGT9+vUnle+9DftYB17WGy97YUAru6XNJHvRRRfZv/3tb9Zaaw8dOmQjIyOtw+Gw9erVs999953r+/r27WsB63A47COPPOLKt3nz5tbLy8sC1svLy954442nXObKrm91LevqSPvKK6+c1EBloaGh1hhjvb29bf/+/a211rZo0cKGhITYNm3a2I8//thGRkZab29vC7gGy+nSpYtt0qSJBSxgW7VqZc85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\n", + "text/plain": [ + "
" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_histogram([res3.get_counts(),nearest_probs], legend=['raw', 'mitigated'], figsize=(15,6))" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "32e72f51", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.9255895675305238" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hellinger_fidelity(nearest_probs, exact_dist)" + ] + }, + { + "cell_type": "markdown", + "id": "a59a1f7c", + "metadata": {}, + "source": [ + "### Additional information on measurement mitigation\n", + "\n", + "From the results object, it is also possible to determine the execution time of the mitigation process (not including calibration time), on the per experiment level:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6ffd5b04", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.008146638981997967" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res3.results[0].header.measurement_mitigation_time" + ] + }, + { + "cell_type": "markdown", + "id": "c1973f47", + "metadata": {}, + "source": [ + "It is also possible to view the final measurment mapping that shows which physical qubit measurements correspond to classical bit values:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "1251442e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'0': 0, '1': 1, '4': 2, '7': 3, '10': 4, '12': 5}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res3.results[0].header.final_measurement_mapping" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8f2a61e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/02_uploading_program.ipynb b/tutorials/02_uploading_program.ipynb new file mode 100644 index 0000000000..34e6470075 --- /dev/null +++ b/tutorials/02_uploading_program.ipynb @@ -0,0 +1,654 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "55f13cc7", + "metadata": {}, + "source": [ + "# Uploading a Qiskit runtime program" + ] + }, + { + "cell_type": "markdown", + "id": "4ff8d2da", + "metadata": {}, + "source": [ + "
\n", + "Note: Qiskit Runtime allows authorized users to upload runtime programs. Access to the Qiskit Runtime service may not mean you have access to upload a runtime program.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "2077b996", + "metadata": {}, + "source": [ + "Here we provide an overview on how to construct and upload a runtime program. A runtime program is a piece of Python code that lives in the cloud and can be invoked by passing in just its parameters. Runtime programs are private by default, which means only you can see and access your programs. Some authorized users can also mark their programs as public, making them visible and accessible by everyone." + ] + }, + { + "cell_type": "markdown", + "id": "cf42076e", + "metadata": {}, + "source": [ + "## Constructing a runtime program" + ] + }, + { + "cell_type": "markdown", + "id": "e282eccc", + "metadata": {}, + "source": [ + "Below is a template of a runtime program. You can find the template file in the \n", + "[`qiskit-ibmq-provider`](https://github.com/Qiskit/qiskit-ibmq-provider/blob/master/qiskit/providers/ibmq/runtime/program/program_template.py) repository." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1206f612", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import json\n", + "\n", + "from qiskit.providers.ibmq.runtime import UserMessenger, ProgramBackend\n", + "\n", + "\n", + "def program(backend: ProgramBackend, user_messenger: UserMessenger, **kwargs):\n", + " \"\"\"Function that does classical-quantum calculation.\"\"\"\n", + " # UserMessenger can be used to publish interim results.\n", + " user_messenger.publish(\"This is an interim result.\")\n", + " return \"final result\"\n", + "\n", + "\n", + "def main(backend: ProgramBackend, user_messenger: UserMessenger, **kwargs):\n", + " \"\"\"This is the main entry point of a runtime program.\n", + "\n", + " The name of this method must not change. It also must have ``backend``\n", + " and ``user_messenger`` as the first two positional arguments.\n", + "\n", + " Args:\n", + " backend: Backend for the circuits to run on.\n", + " user_messenger: Used to communicate with the program user.\n", + " kwargs: User inputs.\n", + " \"\"\"\n", + " # Massage the input if necessary.\n", + " result = program(backend, user_messenger, **kwargs)\n", + " # Final result can be directly returned\n", + " return result\n" + ] + }, + { + "cell_type": "markdown", + "id": "bb4cfa8b", + "metadata": {}, + "source": [ + "Each runtime program must have a `main()` function, which serves as the entry point to the program. This function must have `backend` and `user_messenger` as the first two positional arguments:\n", + "\n", + "- `backend` is an instance of [`ProgramBackend`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.ProgramBackend.html#qiskit.providers.ibmq.runtime.ProgramBackend) and has a [`run()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.ProgramBackend.run.html#qiskit.providers.ibmq.runtime.ProgramBackend.run) method that can be used to submit circuits.\n", + "- `user_messenger` is an instance of [`UserMessenger`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.UserMessenger.html#qiskit.providers.ibmq.runtime.UserMessenger) and has a [`publish()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.UserMessenger.publish.html#qiskit.providers.ibmq.runtime.UserMessenger.publish) method that can be used to send interim and final results to the program user. This method takes a parameter `final` that indicates whether it's a final result. However, it is recommended to return the final result directly from the `main()` function. Currently only final results are stored after a program execution finishes." + ] + }, + { + "cell_type": "markdown", + "id": "705ccef3", + "metadata": {}, + "source": [ + "There are several runtime programs in the `qiskit_runtime` directory in this repository. `qiskit_runtime/hello_world/hello_world.py` is one of them. It is a sample runtime program that submits random circuits for user-specified iterations:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "181916df", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"A sample runtime program that submits random circuits for user-specified iterations.\"\"\"\n", + "\n", + "import random\n", + "\n", + "from qiskit import transpile\n", + "from qiskit.circuit.random import random_circuit\n", + "\n", + "\n", + "def prepare_circuits(backend):\n", + " \"\"\"Generate a random circuit.\n", + "\n", + " Args:\n", + " backend: Backend used for transpilation.\n", + "\n", + " Returns:\n", + " Generated circuit.\n", + " \"\"\"\n", + " circuit = random_circuit(num_qubits=5, depth=4, measure=True,\n", + " seed=random.randint(0, 1000))\n", + " return transpile(circuit, backend)\n", + "\n", + "\n", + "def main(backend, user_messenger, **kwargs):\n", + " \"\"\"Main entry point of the program.\n", + "\n", + " Args:\n", + " backend: Backend to submit the circuits to.\n", + " user_messenger: Used to communicate with the program consumer.\n", + " kwargs: User inputs.\n", + " \"\"\"\n", + " iterations = kwargs.pop('iterations', 5)\n", + " for it in range(iterations):\n", + " qc = prepare_circuits(backend)\n", + " result = backend.run(qc).result()\n", + " user_messenger.publish({\"iteration\": it, \"counts\": result.get_counts()})\n", + "\n", + " return \"All done!\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "1281b655", + "metadata": {}, + "source": [ + "## Data serialization" + ] + }, + { + "cell_type": "markdown", + "id": "9edb1023", + "metadata": {}, + "source": [ + "Runtime programs live in the cloud, and JSON is the standard way of passing data to and from cloud services. Therefore, when a user invokes a runtime program, the input parameters must first be serialized into the JSON format and then deserialized once received by the server. By default, this serialization and deserialization is done automatically using the [`RuntimeEncoder`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.RuntimeEncoder.html#qiskit.providers.ibmq.runtime.RuntimeEncoder) and [`RuntimeDecoder`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.RuntimeDecoder.html#qiskit.providers.ibmq.runtime.RuntimeDecoder) classes.\n" + ] + }, + { + "cell_type": "markdown", + "id": "8b4aeb2b", + "metadata": {}, + "source": [ + "### Custom classes" + ] + }, + { + "cell_type": "markdown", + "id": "1d7fbdf5", + "metadata": {}, + "source": [ + "`RuntimeEncoder` and `RuntimeDecoder` only support types commonly used in Qiskit, such as complex numbers and numpy arrays. If your program uses custom Python classes for input or output, these two methods only have partial support for that. \n", + "\n", + "Your custom class should have the following methods:\n", + "\n", + "- a `to_json()` method that returns a JSON string representation of the object\n", + "- a `from_json()` class method that accepts a JSON string and returns the corresponding object. \n", + "\n", + "When `RuntimeEncoder` serializes a Python object, it checks whether the object has a `to_json()` method. If so, it calls the method to serialize the object. `RuntimeDecoder`, however, does _not_ invoke `from_json()` to convert the data back because it doesn't know how to import your custom class. Therefore the deserialization needs to be done explicitly. " + ] + }, + { + "cell_type": "markdown", + "id": "c3e2d6e2", + "metadata": {}, + "source": [ + "Here is an example of serializing and deserializing a custom class. First we define the class `MyCustomClass`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ca9047fc", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "class MyCustomClass:\n", + " \n", + " def __init__(self, foo, bar):\n", + " self._foo = foo\n", + " self._bar = bar\n", + " \n", + " def to_json(self):\n", + " \"\"\"Convert this instance to a JSON string.\"\"\"\n", + " return json.dumps({\"foo\": self._foo, \"bar\": self._bar})\n", + " \n", + " @classmethod\n", + " def from_json(cls, json_str):\n", + " \"\"\"Return a MyCustomClass instance based on the input JSON string.\"\"\"\n", + " return cls(**json.loads(json_str))" + ] + }, + { + "cell_type": "markdown", + "id": "31f967a4", + "metadata": {}, + "source": [ + "Note that it has the `to_json()` method that converts a `MyCustomClass` instance to a JSON string, and a `from_json()` class method that converts a JSON string back to a `MyCustomClass` instance." + ] + }, + { + "cell_type": "markdown", + "id": "2fdf8941", + "metadata": {}, + "source": [ + "Here is how one would use `MyCustomClass` as an **input** to your program:" + ] + }, + { + "cell_type": "markdown", + "id": "258fcd02", + "metadata": {}, + "source": [ + "```\n", + "program_inputs = {\n", + " 'my_obj': MyCustomClass(\"my foo\", \"my bar\")\n", + "}\n", + "\n", + "options = {\"backend_name\": \"ibmq_qasm_simulator\"}\n", + "job = provider.runtime.run(program_id=\"some-program\",\n", + " options=options,\n", + " inputs=program_inputs\n", + " )\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "dc86356b", + "metadata": {}, + "source": [ + "Since `MyCustomClass` has a `to_json()` method, the method is automatically called to convert the instance to a JSON string when `provider.runtime.run()` is invoked. \n", + "\n", + "Your program can then use the `from_json()` method to restore the JSON string back to a `MyCustomClass` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "681a1798", + "metadata": {}, + "outputs": [], + "source": [ + "def main(backend, user_messenger, **kwargs):\n", + " \"\"\"Main entry point of the program.\"\"\"\n", + " my_obj_str = kwargs.pop('my_obj')\n", + " my_obj = MyCustomClass.from_json(my_obj_str)" + ] + }, + { + "cell_type": "markdown", + "id": "b71c40bc", + "metadata": {}, + "source": [ + "Similarly, if you pass a `MyCustomClass` instance as an **output** of your program, it is automatically converted to a JSON string (via the `to_json()` method):" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f4cf18b4", + "metadata": {}, + "outputs": [], + "source": [ + "def main(backend, user_messenger, **kwargs):\n", + " \"\"\"Main entry point of the program.\"\"\"\n", + " return MyCustomClass(\"this foo\", \"that bar\")" + ] + }, + { + "cell_type": "markdown", + "id": "8e0fdb4a", + "metadata": {}, + "source": [ + "Now when the user of this program calls `job.result()`, they will receive a JSON string rather than a `MyCustomClass` instance. The user can convert the string back to `MyCustomClass` themselves:" + ] + }, + { + "cell_type": "markdown", + "id": "abf149cc", + "metadata": {}, + "source": [ + "```\n", + "output_str = job.result()\n", + "output = MyCustomClass.from_json(output_str)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "32cd0c7e", + "metadata": {}, + "source": [ + "Alternatively, you can provide a decoder for the users. Your decoder class should inherit [`ResultDecoder`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.ResultDecoder.html#qiskit.providers.ibmq.runtime.ResultDecoder) and overwrites the `decode()` method:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a2f8564d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.providers.ibmq.runtime import ResultDecoder\n", + "\n", + "class MyResultDecoder(ResultDecoder):\n", + "\n", + " @classmethod\n", + " def decode(cls, data):\n", + " data = super().decoded(data) # Perform any preprocessing.\n", + " return MyCustomClass.from_json(data)" + ] + }, + { + "cell_type": "markdown", + "id": "3d0de7d0", + "metadata": {}, + "source": [ + "Your user can then use this `MyResultDecoder` to decode the result of your program:\n", + "\n", + "```\n", + "output = job.result(decoder=MyResultDecoder)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "ecdda3bf", + "metadata": {}, + "source": [ + "## Testing your runtime program" + ] + }, + { + "cell_type": "markdown", + "id": "2a18f49e", + "metadata": {}, + "source": [ + "You can test your runtime program using a local simulator or a real backend before uploading it. Simply import and invoke the `main()` function of your program and pass the following parameters:\n", + "\n", + "- the `backend` instance you want to use\n", + "- a new `UserMessenger` instance.\n", + "- program input parameters that are serialized and then deserialized using the correct encoder and decoder. While this may seem redundant, it is to ensure input parameters can be passed to your program properly once it's uploaded to the cloud.\n" + ] + }, + { + "cell_type": "markdown", + "id": "f197d48e", + "metadata": {}, + "source": [ + "The following example tests the `hello-world` program we saw earlier. It uses the `qasm_simulator` from Qiskit Aer as the test backend. It serializes and deserializes input data using `RuntimeEncoder` and `RuntimeDecoder`, which are the default en/decoders used by runtime." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "12c32ba7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"iteration\": 0, \"counts\": {\"01000\": 4, \"00000\": 12, \"00011\": 872, \"01011\": 136}}\n", + "{\"iteration\": 1, \"counts\": {\"01000\": 6, \"00000\": 19, \"00011\": 871, \"01011\": 128}}\n", + "{\"iteration\": 2, \"counts\": {\"00001\": 1024}}\n" + ] + }, + { + "data": { + "text/plain": [ + "'All done!'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys\n", + "sys.path.insert(0, '..') # Add qiskit_runtime directory to the path\n", + "\n", + "from qiskit_runtime.hello_world import hello_world\n", + "from qiskit import Aer\n", + "from qiskit.providers.ibmq.runtime.utils import RuntimeEncoder, RuntimeDecoder\n", + "from qiskit.providers.ibmq.runtime import UserMessenger\n", + "\n", + "inputs = {\"iterations\": 3}\n", + "\n", + "backend = Aer.get_backend('qasm_simulator')\n", + "user_messenger = UserMessenger()\n", + "serialized_inputs = json.dumps(inputs, cls=RuntimeEncoder)\n", + "deserialized_inputs = json.loads(serialized_inputs, cls=RuntimeDecoder)\n", + "\n", + "hello_world.main(backend, user_messenger, **deserialized_inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "501a66f3", + "metadata": {}, + "source": [ + "## Defining program metadata" + ] + }, + { + "cell_type": "markdown", + "id": "f98640c1", + "metadata": {}, + "source": [ + "Program metadata helps users to understand how to use your program. It includes:\n", + "\n", + "- `name`: Name of the program.\n", + "- `max_execution_time`: Maximum amount of time, in seconds, a program can run before being forcibly terminated.\n", + "- `description`: Describes the program.\n", + "- `spec`: Detailed information about the program, which includes the following attributes:\n", + " - `backend_requirements`: Describes the backend attributes needed to run the program.\n", + " - `parameters`: Describes the program input parameters as a JSON schema\n", + " - `return_values`: Describes the return values as a JSON schema\n", + " - `interim_results`: Describes the interim results as a JSON schema\n", + "\n", + "When uploading a program, you must specify at least `name`, `max_execution_time`, and `description`. It is strongly encouraged to also specify `parameters`, `return_values`, and `interim_results` within `spec` if the program has them." + ] + }, + { + "cell_type": "markdown", + "id": "55db2a39", + "metadata": {}, + "source": [ + "Below shows the metadata JSON file of the `hello-world` program as an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3cdce276", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"name\": \"hello-world\",\n", + " \"description\": \"A sample runtime program.\",\n", + " \"max_execution_time\": 300,\n", + " \"spec\": {\n", + " \"backend_requirements\": {\n", + " \"min_num_qubits\": 5\n", + " },\n", + " \"parameters\": {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"properties\": {\n", + " \"iterations\": {\n", + " \"type\": \"integer\",\n", + " \"minimum\": 0,\n", + " \"description\": \"Number of iterations to run. Each iteration generates a runs a random circuit.\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"iterations\"\n", + " ]\n", + " },\n", + " \"return_values\": {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"description\": \"A string that says 'All done!'.\",\n", + " \"type\": \"string\"\n", + " },\n", + " \"interim_results\": {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"properties\": {\n", + " \"iteration\": {\n", + " \"type\": \"integer\",\n", + " \"description\": \"Iteration number.\"\n", + " },\n", + " \"counts\": {\n", + " \"description\": \"Histogram data of the circuit result.\",\n", + " \"type\": \"object\"\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "import os\n", + "\n", + "hello_world_json = os.path.join(os.getcwd(), \"../qiskit_runtime/hello_world/hello_world.json\")\n", + "\n", + "with open(hello_world_json, 'r') as file:\n", + " data = file.read()\n", + "\n", + "print(data)" + ] + }, + { + "cell_type": "markdown", + "id": "43dd6a77", + "metadata": {}, + "source": [ + "## Uploading a program" + ] + }, + { + "cell_type": "markdown", + "id": "d1865c58", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.upload_program()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.upload_program) method to upload your program. In the example below, the program data lives in the file `hello_world.py`, and its metadata, as described above, is in `hello_world.json`. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "864ab085", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hello-world-nQ9dgRjGEe\n" + ] + } + ], + "source": [ + "import os\n", + "from qiskit import IBMQ\n", + "\n", + "IBMQ.load_account()\n", + "provider = IBMQ.get_provider(project='qiskit-runtime') # Substitute with your provider.\n", + "\n", + "hello_world_data = os.path.join(os.getcwd(), \"../qiskit_runtime/hello_world/hello_world.py\")\n", + "hello_world_json = os.path.join(os.getcwd(), \"../qiskit_runtime/hello_world/hello_world.json\")\n", + "\n", + "program_id = provider.runtime.upload_program(\n", + " data=hello_world_data,\n", + " metadata=hello_world_json\n", + ")\n", + "print(program_id)" + ] + }, + { + "cell_type": "markdown", + "id": "45ffd6c8", + "metadata": {}, + "source": [ + "`upload_program()` returns a program ID, which uniquely identifies the program. It is derived from the program name, usually with a randomly-generated suffix. Program ID is needed for users to invoke the program" + ] + }, + { + "cell_type": "markdown", + "id": "chubby-infection", + "metadata": {}, + "source": [ + "## Updating a program" + ] + }, + { + "cell_type": "markdown", + "id": "measured-catalyst", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.update_program()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.update_program.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.update_program) method to update the source code and/or metadata of a program:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "southern-farmer", + "metadata": {}, + "outputs": [], + "source": [ + "provider.runtime.update_program(program_id=program_id, description=\"A new description.\")" + ] + }, + { + "cell_type": "markdown", + "id": "becoming-badge", + "metadata": {}, + "source": [ + "This method allows you to make changes to your program while retaining the same program ID." + ] + }, + { + "cell_type": "markdown", + "id": "f841041c", + "metadata": {}, + "source": [ + "## Deleting a program" + ] + }, + { + "cell_type": "markdown", + "id": "a20cd296", + "metadata": {}, + "source": [ + "You can use the [`IBMRuntimeService.delete_program()`](https://qiskit.org/documentation/stubs/qiskit.providers.ibmq.runtime.IBMRuntimeService.html#qiskit.providers.ibmq.runtime.IBMRuntimeService.delete_program) method to delete a program. Only the person who uploaded the program can delete it. \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/images/chip.png b/tutorials/images/chip.png new file mode 100644 index 0000000000..0fa4a271f6 Binary files /dev/null and b/tutorials/images/chip.png differ diff --git a/tutorials/images/subgraphs.png b/tutorials/images/subgraphs.png new file mode 100644 index 0000000000..7ccd9e3e22 Binary files /dev/null and b/tutorials/images/subgraphs.png differ diff --git a/tutorials/qka.ipynb b/tutorials/qka.ipynb new file mode 100644 index 0000000000..c55b47081b --- /dev/null +++ b/tutorials/qka.ipynb @@ -0,0 +1,634 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Kernel Alignment with Qiskit Runtime\n", + "\n", + "
\n", + "\n", + "**Classification with Support Vector Machines**
\n", + "Classification problems are widespread in machine learning applications. Examples include credit card risk, handwriting recognition, and medical diagnosis. One approach to tackling classification problems is the support vector machine (SVM) [1,2]. This supervised learning algorithm uses labeled data samples to train a model that can predict to which class a test sample belongs. It does this by finding a separating hyperplane maximizing the margin between data classes. Often, data is not linearly separable in the original space. In these cases, the kernel trick is used to implicitly encode a transformation of the data into a higher-dimensional feature space, through the inner product between pairs of data points, where the data may become separable.\n", + "\n", + "**Quantum Kernels**
\n", + "Quantum computers can be used to encode classical data in a quantum-enhanced feature space. In 2019, IBM introduced an algorithm called the quantum kernel estimator (QKE) for computing quantum kernels [3]. This algorithm uses quantum circuits with data provided classically and offers an efficient way to evaluate inner products between data in a quantum feature space. For two data samples $\\theta$ and $\\theta'$, the kernel matrix is given as\n", + "\n", + "$$\n", + " K(\\theta, \\theta') = \\lvert\\langle 0^n \\rvert U^\\dagger(\\theta) U(\\theta') \\lvert 0^n \\rangle \\rvert^2,\n", + "$$\n", + "\n", + "where $U(\\theta)$ prepares the quantum feature state. Quantum kernels used in a classification framework inherit the convex optimization program of the SVM and avoid common limitations of variational quantum classifiers. A key observation of this paper was that a necessary condition for a computational advantage requires quantum circuits for the kernel that are hard to simulate classically. More recently, IBM proved that quantum kernels can offer superpolynomial speedups over any classical learner on a learning problem based on the hardness of the discrete logarithm problem [4]. This means that quantum kernels can someday offer quantum advantage on suitable problems. \n", + "\n", + "\n", + "**Quantum Kernels that Exploit Structure in Data**
\n", + "An important approach in the search for practical quantum advantage in machine learning is to identify quantum kernels for learning problems that have underlying structure in the data. We've taken a step in this direction in our recent paper [5], where we introduced a broad class of quantum kernels that exploit group structure in data. Examples of learning problems for data with group structure could include learning permutations or classifying translations. We call this new class of kernels _covariant quantum kernels_ as they are related to covariant quantum measurements. The quantum feature map is defined by a unitary representation $D(\\theta)$ of a group $G$ for some element $\\theta \\in G$, and a fiducial reference state $\\lvert\\psi\\rangle = V\\lvert0^n\\rangle$ prepared by a unitary circuit $V$. The kernel matrix is given as\n", + "\n", + "$$\n", + " K(\\theta, \\theta') = \\vert\\langle 0^n \\rvert V^\\dagger D^\\dagger(\\theta) D(\\theta') V \\lvert 0^n \\rangle \\rvert^2. \\qquad (1)\n", + "$$\n", + "\n", + "In general, the choice of the fiducial state is not known _a priori_ and can significantly impact the performance of the classifier. Here, we use a method called quantum kernel alignment (QKA) to find a good fiducial state for a given group.\n", + "\n", + "**Aligning Quantum Kernels on a Dataset**
\n", + "In practice, SVMs require a choice of the kernel function. Sometimes, symmetries in the data can inform this selection, other times it is chosen in an ad hoc manner. Kernel alignment is one approach to learning a kernel on a given dataset by iteratively adapting it to have high similarity to a target kernel informed from the underlying data distribution [6]. As a result, the SVM with an aligned kernel will likely generalize better to new data than with an unaligned kernel. Using this concept, we introduced in [5] an algorithm for quantum kernel alignment, which provides a way to learn a quantum kernel from a family of kernels. Specifically, the algorithm optimizes the parameters in a quantum circuit to maximize the alignment of a kernel while converging to the maximum SVM margin. In the context of covariant quantum kernels, we extend Eq. $(1)$ to\n", + "\n", + "$$\n", + " K_\\lambda(\\theta,\\theta') = \\lvert\\langle 0^n \\rvert V^\\dagger_\\lambda D^\\dagger(\\theta) D(\\theta') V_\\lambda \\lvert 0^n \\rangle \\rvert^2, \\qquad (2)\n", + "$$\n", + "\n", + "and use QKA to learn a good fiducial state parametrized by $\\lambda$ for a given group. \n", + "\n", + "\n", + "**Covariant Quantum Kernels on a Specific Learning Problem**
\n", + "Let's try out QKA on a learning problem. In the following, we'll consider a binary classification problem we call _labeling cosets with error_ [5]. In this problem, we will use a group and a subgroup to form two cosets, which will represent our data classes. We take the group $G = SU(2)^{\\otimes n}$ for $n$ qubits, which is the special unitary group of $2\\times2$ matrices and has wide applicability in nature, for example, the Standard Model of particle physics and in many condensed matter systems. We take the graph-stabilizer subgroup $S_{\\mathrm{graph}} \\in G$ with $S_{\\mathrm{graph}} = \\langle \\{ X_i \\otimes_{k:(k,i) \\in \\mathcal{E}} Z_k \\}_{i \\in \\mathcal{V}} \\rangle$ for a graph $(\\mathcal{E},\\mathcal{V})$ with edges $\\mathcal{E}$ and vertices $\\mathcal{V}$. Note that the stabilizers fix a stabilizer state such that $D_s \\lvert \\psi\\rangle = \\lvert \\psi\\rangle$. This observation will be useful a bit later. \n", + "\n", + "To generate the dataset, we write the rotations of the group as $D(\\theta_1, \\theta_2, 0)=\\exp(i \\theta_1 X) \\exp(i \\theta_2 Z) \\in SU(2)$, so that each qubit is parametrized by the first two Euler angles (the third we set to zero). Then, we draw randomly two sets of angles $\\mathbf{\\theta}_\\pm \\in [-\\pi/4, \\pi/4]^{2n}$ for the $n$-qubit problem. From these two sets, we construct a binary classification problem by forming two left-cosets (representing the two classes) with those angles, $C_\\pm = D(\\mathbf{\\theta}_\\pm) S_{\\mathrm{graph}}$ where $D(\\mathbf{\\theta}_\\pm) = \\otimes_{k=1}^n D(\\theta_\\pm^{2k-1}, \\theta_\\pm^{2k}, 0)$. Note that the elements of the cosets can again be written in terms of Euler angles. We build training and testing sets by randomly drawing elements from $C_\\pm$ such that the dataset has samples $i=1,...,m$ containing the first two Euler angles for each qubit $\\mathbf{\\theta}_{y_i} = (\\theta_{y_i}^{1}, \\theta_{y_i}^{2}, \\theta_{y_i}^{3}, \\theta_{y_i}^{4}, ..., \\theta_{y_i}^{2n-1}, \\theta_{y_i}^{2n})$ and labels $y_i \\in \\{-1,1\\}$ that indicate to which coset a sample belongs.\n", + "\n", + "Next, we select a fiducial state. A natural candidate is the stabilizer state we encountered above. Why? Because this is a subgroup invariant state, $D_s\\lvert\\psi\\rangle = \\lvert\\psi\\rangle$, which causes the data for a given coset to be mapped to a unique state: $D(\\mathbf{\\theta}_\\pm)D_s \\lvert\\psi\\rangle = D(\\mathbf{\\theta}_\\pm) \\lvert\\psi\\rangle$. This means the classifier only needs to distinguish the _two_ states $D(\\mathbf{\\theta}_\\pm) \\lvert\\psi\\rangle \\langle \\psi\\rvert D^\\dagger(\\mathbf{\\theta}_\\pm)$ for every element of the coset. In this tutorial, we will add a small Gaussian error with variance $0.01$ to the Euler angles of the dataset. This noise will perturb these two states, but if the variance is sufficiently small, we expect the states will still be classified correctly. Let's consider a parametrized version of the stabilizer state, associated with the coupling graph $(\\mathcal{E},\\mathcal{V})$ given by the device connectivity, as our fiducial state and then use kernel alignment to find its optimal parameters. Specifically, we'll replace the initial layers of Hadamards in the graph state with $y$-rotations by an angle $\\lambda$,\n", + "\n", + "$$\n", + "\\lvert \\psi_\\lambda\\rangle = V_\\lambda \\lvert 0^n\\rangle = \\prod_{(k,t) \\in \\mathcal{E}} CZ_{k,t} \\prod_{k \\in \\mathcal{V}} \\exp\\left(i \\frac{\\lambda}{2} Y_k\\right)\\lvert 0^n\\rangle,\n", + "$$\n", + "\n", + "where $CZ=\\mathrm{diag}(1,1,1,-1)$. Then, given two samples from our dataset, $\\mathbf{\\theta}$ and $\\mathbf{\\theta}'$, the kernel matrix is evaluated as in Eq. $(2)$. If we initialize the kernel with $\\lambda \\approx 0$, we expect the quantum kernel alignment algorithm to converge towards the optimal $\\lambda = \\pi/2$ and the classifier to yield 100\\% test accuracy.\n", + "\n", + "Let's define two specific problem instances to test these ideas out. We'll be using the quantum device `ibmq_montreal`, with coupling map shown below:\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "We'll pick two different subgraphs, one for 7 qubits and one for 10, to define our problem instances. Using these subgraphs, we'll generate the corresponding datasets as described above, and then align the quantum kernel with QKA to learn a good fiducial state.\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "**Speeding up Algorithms with Qiskit Runtime**
\n", + "QKA is an iterative quantum-classical algorithm, in which quantum hardware is used to execute parametrized quantum circuits for evaluating the quantum kernel matrices with QKE, while a classical optimizer tunes the parameters of those circuits to maximize the alignment. Iterative algorithms of this type can be slow due to latency between the quantum and classical calculations. Qiskit Runtime is a new architecture that can speed up iterative algorithms like QKA by co-locating classical computations with the quantum hardware executions. In this tutorial, we'll use QKA with Qiskit Runtime to learn a good quantum kernel for the _labeling cosets with error_ problem defined above.\n", + "\n", + "
\n", + "\n", + "**References**
\n", + "[1] B. E. Boser, I. M. Guyon, and V. N. Vapnik, Proceedings of the Fifth Annual Workshop on Computational Learning Theory, COLT ’92 (Association for Computing Machinery, New York, NY, USA, 1992) pp. 144-152 [link](https://doi.org/10.1145/130385.130401)
\n", + "[2] V. Vapnik, The Nature of Statistical Learning Theory, Information Science and Statistics (Springer New York, 2013) [link](https://books.google.com/books?id=EqgACAAAQBAJ)
\n", + "[3] V. Havlíček, A. D. Córcoles, K. Temme, A. W. Harrow, A. Kandala, J. M. Chow, and J. M. Gambetta, Nature 567, 209-212 (2019) [link](https://doi.org/10.1038/s41586-019-0980-2)
\n", + "[4] Y. Liu, S. Arunachalam, and K. Temme, arXiv:2010.02174 (2020) [link](https://arxiv.org/abs/2010.02174)
\n", + "[5] J. R. Glick, T. P. Gujarati, A. D. Córcoles, Y. Kim, A. Kandala, J. M. Gambetta, K. Temme, arXiv:2105.03406 (2021) [link](https://arxiv.org/abs/2105.03406)
\n", + "[6] N. Cristianini, J. Shawe-taylor, A. Elisseeff, and J. Kandola, Advances in Neural Information Processing Systems 14 (2001) [link](https://proceedings.neurips.cc/paper/2001/file/1f71e393b3809197ed66df836fe833e5-Paper.pdf)
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Load your IBM Quantum account and get the quantum backend\n", + "\n", + "We'll be using the 27-qubit device `ibmq_montreal` for this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '..') # Add qiskit_runtime directory to the path\n", + "\n", + "from qiskit import IBMQ\n", + "IBMQ.load_account()\n", + "provider = IBMQ.get_provider(project='qiskit-runtime') # Change this to your provider.\n", + "backend = provider.get_backend('ibmq_montreal')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Invoke the Quantum Kernel Alignment program\n", + "\n", + "Before executing the runtime program for QKA, we need to prepare the dataset and configure the input parameters for the algorithm.\n", + "\n", + "### 1. Prepare the dataset\n", + "\n", + "First, we load the dataset from the `csv` file and then extract the labeled training and test samples. Here, we'll look at the 7-qubit problem, shown above in subfigure a). A second dataset is also available for the 10-qubit problem in b)." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.read_csv('../qiskit_runtime/qka/aux_file/dataset_graph7.csv',sep=',', header=None) # alterative problem: dataset_graph10.csv\n", + "data = df.values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at the data to see how it's formatted. Each row of the dataset contains a list of Euler angles, followed by the class label $\\pm1$ in the last column. For an $n$-qubit problem, there are $2n$ features corresponding to the first two Euler angles for each qubit (recall discussion above). The rows alternate between class labels." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 0 1 2 3 4 5 6 \\\n", + "0 -0.193574 0.113979 -0.005927 0.300957 -0.358603 -0.087866 -0.156226 \n", + "1 -0.100006 0.002431 0.244218 0.126870 -0.063891 -0.085588 0.072490 \n", + "2 -1.774448 -0.047642 -0.025880 0.252708 -0.350689 -1.604509 -0.114874 \n", + "3 -0.211585 -0.043782 -1.560226 0.018510 -0.051867 -0.128508 0.218609 \n", + "\n", + " 7 8 9 10 11 12 13 14 \n", + "0 0.342442 -0.016003 0.143113 0.256422 -0.164125 -0.136743 0.014674 1.0 \n", + "1 0.042986 -0.052714 0.019754 -0.159314 -0.409991 -0.199615 0.053845 -1.0 \n", + "2 0.347631 0.059501 -0.168956 0.351014 -0.128586 0.098897 -0.047799 1.0 \n", + "3 -0.075632 -0.183656 -1.715292 -0.105361 -0.300758 -0.566431 0.046542 -1.0 \n" + ] + } + ], + "source": [ + "print(df.head(4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's explicitly construct the training and test samples (denoted `x`) and their labels (denoted `y`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# choose number of training and test samples per class:\n", + "num_train = 10\n", + "num_test = 10\n", + "\n", + "# extract training and test sets and sort them by class label\n", + "train = data[:2*num_train, :]\n", + "test = data[2*num_train:2*(num_train+num_test), :]\n", + "\n", + "ind=np.argsort(train[:,-1])\n", + "x_train = train[ind][:,:-1]\n", + "y_train = train[ind][:,-1]\n", + "\n", + "ind=np.argsort(test[:,-1])\n", + "x_test = test[ind][:,:-1]\n", + "y_test = test[ind][:,-1]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Configure the QKA algorithm\n", + "\n", + "The first task is to set up the feature map and its entangler map, which specifies the arrangement of $CZ$ gates in the fiducial state. We will choose this to match the connectivity of the problem subgraph, pictured above. We also initialize the fiducial state parameter $\\lambda$ with `initial_point`." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_runtime.qka import FeatureMap\n", + "\n", + "d = np.shape(data)[1]-1 # feature dimension is twice the qubit number\n", + "\n", + "em = [[0,2],[3,4],[2,5],[1,4],[2,3],[4,6]] # we'll match this to the 7-qubit graph \n", + "# em = [[0,1],[2,3],[4,5],[6,7],[8,9],[1,2],[3,4],[5,6],[7,8]] # we'll match this to the 10-qubit graph\n", + "\n", + "fm = FeatureMap(feature_dimension=d, entangler_map=em) # define the feature map\n", + "initial_point = [0.1] # set the initial parameter for the feature map" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's print out the circuit for the feature map (the circuit for the kernel will be a feature map for one data sample composed with an inverse feature map for a second sample). The first part of the feature map is the fiducial state, which is prepared with a layer of $y$ rotations followed by $CZ$s. Then, the last two layers of $z$ and $x$ rotations in the circuit denote the group representation $D(\\theta)$ for a data sample $\\theta$. Note that a single-qubit rotation is defined as $RP(\\phi) = \\exp(- i [\\phi/2] P)$ for $P \\in {X, Y, Z}$." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
     ┌──────────┐   ┌──────────────┐ ┌────────────┐                                                 \n",
+       "q_0: ┤ RY(-0.1) ├─■─┤ RZ(-0.38383) ├─┤ RX(3.3378) ├─────────────────────────────────────────────────\n",
+       "     ├──────────┤ │ └──────────────┘ └────────────┘┌──────────────┐ ┌──────────────┐                \n",
+       "q_1: ┤ RY(-0.1) ├─┼────────────────────────■───────┤ RZ(-0.11811) ├─┤ RX(-0.20449) ├────────────────\n",
+       "     ├──────────┤ │                        │       └──────────────┘ └┬────────────┬┘┌─────────────┐ \n",
+       "q_2: ┤ RY(-0.1) ├─■────────■───────────────┼──────────────■──────────┤ RZ(3.4802) ├─┤ RX(0.15495) ├─\n",
+       "     ├──────────┤          │               │              │         ┌┴────────────┤ ├─────────────┴┐\n",
+       "q_3: ┤ RY(-0.1) ├─■────────┼───────────────┼──────────────■─────────┤ RZ(0.34764) ├─┤ RX(-0.54085) ├\n",
+       "     ├──────────┤ │        │               │                        ├─────────────┤ ├──────────────┤\n",
+       "q_4: ┤ RY(-0.1) ├─■────────┼───────────────■──────────────■─────────┤ RZ(0.34312) ├─┤ RX(-0.14015) ├\n",
+       "     ├──────────┤          │        ┌─────────────┐       │        ┌┴─────────────┴┐└──────────────┘\n",
+       "q_5: ┤ RY(-0.1) ├──────────■────────┤ RZ(0.51497) ├───────┼────────┤ RX(-0.029293) ├────────────────\n",
+       "     ├──────────┤                   └─────────────┘       │        └┬──────────────┤┌─────────────┐ \n",
+       "q_6: ┤ RY(-0.1) ├─────────────────────────────────────────■─────────┤ RZ(-0.42725) ├┤ RX(0.44115) ├─\n",
+       "     └──────────┘                                                   └──────────────┘└─────────────┘ 
" + ], + "text/plain": [ + " ┌──────────┐ ┌──────────────┐ ┌────────────┐ \n", + "q_0: ┤ RY(-0.1) ├─■─┤ RZ(-0.38383) ├─┤ RX(3.3378) ├─────────────────────────────────────────────────\n", + " ├──────────┤ │ └──────────────┘ └────────────┘┌──────────────┐ ┌──────────────┐ \n", + "q_1: ┤ RY(-0.1) ├─┼────────────────────────■───────┤ RZ(-0.11811) ├─┤ RX(-0.20449) ├────────────────\n", + " ├──────────┤ │ │ └──────────────┘ └┬────────────┬┘┌─────────────┐ \n", + "q_2: ┤ RY(-0.1) ├─■────────■───────────────┼──────────────■──────────┤ RZ(3.4802) ├─┤ RX(0.15495) ├─\n", + " ├──────────┤ │ │ │ ┌┴────────────┤ ├─────────────┴┐\n", + "q_3: ┤ RY(-0.1) ├─■────────┼───────────────┼──────────────■─────────┤ RZ(0.34764) ├─┤ RX(-0.54085) ├\n", + " ├──────────┤ │ │ │ ├─────────────┤ ├──────────────┤\n", + "q_4: ┤ RY(-0.1) ├─■────────┼───────────────■──────────────■─────────┤ RZ(0.34312) ├─┤ RX(-0.14015) ├\n", + " ├──────────┤ │ ┌─────────────┐ │ ┌┴─────────────┴┐└──────────────┘\n", + "q_5: ┤ RY(-0.1) ├──────────■────────┤ RZ(0.51497) ├───────┼────────┤ RX(-0.029293) ├────────────────\n", + " ├──────────┤ └─────────────┘ │ └┬──────────────┤┌─────────────┐ \n", + "q_6: ┤ RY(-0.1) ├─────────────────────────────────────────■─────────┤ RZ(-0.42725) ├┤ RX(0.44115) ├─\n", + " └──────────┘ └──────────────┘└─────────────┘ " + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.tools.visualization import circuit_drawer\n", + "circuit_drawer(fm.construct_circuit(x=x_train[0], parameters=initial_point), \n", + " output='text', fold=200)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we set the values for the SVM soft-margin penalty `C` and the number of SPSA iterations `maxiters` we use to align the quantum kernel." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "C = 1 # SVM soft-margin penalty\n", + "maxiters = 10 # number of SPSA iterations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we decide how to map the virtual qubits of our problem graph to the physical qubits of the hardware. For example, in the 7-qubit problem, we can directly map the virtual qubits `[0, 1, 2, 3, 4, 5, 6]` to the physical qubits `[10, 11, 12, 13, 14, 15, 16]` of the device. This allows us to avoid introducing SWAP gates for qubits that are not connected, which can increase the circuit depth. " + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "initial_layout = [10, 11, 12, 13, 14, 15, 16] # see figure above for the 7-qubit graph\n", + "# initial_layout = [9, 8, 11, 14, 16, 19, 22, 25, 24, 23] # see figure above for the 10-qubit graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Set up and run the program\n", + "\n", + "We're almost ready to run the program. First, let's take a look at the program metadata, which includes a description of the input parameters and their default values." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "quantum-kernel-alignment:\n", + " Name: quantum-kernel-alignment\n", + " Description: Quantum kernel alignment algorithm that learns, on a given dataset, a quantum kernel maximizing the SVM classification margin.\n", + " Version: 1\n", + " Creation date: 2021-05-06T14:56:53Z\n", + " Max execution time: 28800\n", + " Input parameters:\n", + " - feature_map:\n", + " Description: An instance of FeatureMap in dictionary format used to map classical data into a quantum state space.\n", + " Type: dict\n", + " Required: True\n", + " - data:\n", + " Description: NxD array of training data, where N is the number of samples and D is the feature dimension.\n", + " Type: numpy.ndarray\n", + " Required: True\n", + " - labels:\n", + " Description: Nx1 array of +/-1 labels of the N training samples.\n", + " Type: numpy.ndarray\n", + " Required: True\n", + " - initial_kernel_parameters:\n", + " Description: Initial parameters of the quantum kernel. If not specified, an array of randomly generated numbers is used.\n", + " Type: numpy.ndarray\n", + " Required: False\n", + " - maxiters:\n", + " Description: Number of SPSA optimization steps. Default is 1.\n", + " Type: int\n", + " Required: False\n", + " - C:\n", + " Description: Penalty parameter for the soft-margin support vector machine. Default is 1.\n", + " Type: float\n", + " Required: False\n", + " - initial_layout:\n", + " Description: Initial position of virtual qubits on the physical qubits of the quantum device. Default is None.\n", + " Type: list or dict\n", + " Required: False\n", + " Interim results:\n", + " none\n", + " Returns:\n", + " - aligned_kernel_parameters:\n", + " Description: The optimized kernel parameters found from quantum kernel alignment.\n", + " Type: numpy.ndarray\n", + " - aligned_kernel_matrix:\n", + " Description: The aligned quantum kernel matrix evaluated with the optimized kernel parameters on the training data.\n", + " Type: numpy.ndarray\n" + ] + } + ], + "source": [ + "print(provider.runtime.program('quantum-kernel-alignment'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that this program has several input parameters, which we'll configure below. To run the program, we'll set up its two main components: `inputs` (the input parameters from the program metadata) and `options` (the quantum backend). We'll also define a callback function so that the intermediate results of the algorithm will be printed as the program runs. Note that each step of the algorithm for the settings we've selected here takes approximately 11 minutes." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def interim_result_callback(job_id, interim_result):\n", + " print(f\"interim result: {interim_result}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "scrolled": false, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "c2at64rhejjp7co0uc9g\n", + "interim result: {'cost': 10.870283985111303, 'kernel_parameters': array([0.24000164])}\n", + "\n", + "interim result: {'cost': 10.333206932017534, 'kernel_parameters': array([0.42813036])}\n", + "\n", + "interim result: {'cost': 9.080271557433964, 'kernel_parameters': array([0.62392269])}\n", + "\n", + "interim result: {'cost': 7.651520327865867, 'kernel_parameters': array([0.76737064])}\n", + "\n", + "interim result: {'cost': 6.6448212932491355, 'kernel_parameters': array([0.91182299])}\n", + "\n", + "interim result: {'cost': 5.958753300709191, 'kernel_parameters': array([1.03833353])}\n", + "\n", + "interim result: {'cost': 5.265464439204466, 'kernel_parameters': array([1.11397698])}\n", + "\n", + "interim result: {'cost': 4.899536249549028, 'kernel_parameters': array([1.15494826])}\n", + "\n", + "interim result: {'cost': 4.848342921952558, 'kernel_parameters': array([1.1975977])}\n", + "\n", + "interim result: {'cost': 4.743013044149239, 'kernel_parameters': array([1.221689])}\n", + "\n" + ] + } + ], + "source": [ + "program_inputs = {\n", + " 'feature_map': fm,\n", + " 'data': x_train,\n", + " 'labels': y_train,\n", + " 'initial_kernel_parameters': initial_point,\n", + " 'maxiters': maxiters,\n", + " 'C': C,\n", + " 'initial_layout': initial_layout\n", + "}\n", + "\n", + "options = {'backend_name': backend.name()}\n", + "\n", + "job = provider.runtime.run(program_id=\"quantum-kernel-alignment\",\n", + " options=options,\n", + " inputs=program_inputs,\n", + " callback=interim_result_callback,\n", + " )\n", + "\n", + "print(job.job_id())\n", + "result = job.result()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Retrieve the results of the program\n", + "\n", + "Now that we've run the program, we can retrieve the output, which is the aligned kernel parameter and the aligned kernel matrix. Let's also plot this kernel matrix (we'll subtract off the diagonal to show the contrast between the remaining entries). The kernel matrix is expected to have a block-diagonal structure. This reflects the fact that the kernel maps the input data effectively to just two states (modulo the small noise we added to the data; recall the discussion above). That is, data in the same coset (same class label) have a larger overlap than do data from different cosets." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "aligned_kernel_parameters: [1.221689]\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "print(f\"aligned_kernel_parameters: {result['aligned_kernel_parameters']}\")\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from pylab import cm\n", + "plt.rcParams['font.size'] = 20\n", + "plt.imshow(result['aligned_kernel_matrix']-np.identity(2*num_train), cmap=cm.get_cmap('bwr', 20))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Use the results of the program to test an SVM on new data\n", + "\n", + "Equipped with the aligned kernel and its optimized parameter, we can use the `sklearn` package to train an SVM and then evaluate its classification accuracy on new test points. Note that a second kernel matrix built from the test points is needed for the SVM decision function." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "accuracy test: 1.0\n" + ] + } + ], + "source": [ + "from qiskit_runtime.qka import KernelMatrix\n", + "from sklearn.svm import SVC\n", + "from sklearn import metrics\n", + "\n", + "# train the SVM with the aligned kernel matrix:\n", + "\n", + "kernel_aligned = result['aligned_kernel_matrix']\n", + "model = SVC(C=C, kernel='precomputed')\n", + "model.fit(X=kernel_aligned, y=y_train)\n", + "\n", + "# test the SVM on new data:\n", + "\n", + "km = KernelMatrix(feature_map=fm, backend=backend, initial_layout=initial_layout)\n", + "kernel_test = km.construct_kernel_matrix(x1_vec=x_test, x2_vec=x_train, parameters=result['aligned_kernel_parameters'])\n", + "labels_test = model.predict(X=kernel_test)\n", + "accuracy_test = metrics.balanced_accuracy_score(y_true=y_test, y_pred=labels_test)\n", + "print(f\"accuracy test: {accuracy_test}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Version Information

Qiskit SoftwareVersion
Qiskit0.25.0
Terra0.17.0
Aer0.8.0
Ignis0.6.0
Aqua0.9.0
IBM Q Provider0.13.0
System information
Python3.7.10 (default, Feb 26 2021, 10:16:00) \n", + "[Clang 10.0.0 ]
OSDarwin
CPUs4
Memory (Gb)16.0
Mon May 03 13:47:57 2021 EDT
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "

This code is a part of Qiskit

© Copyright IBM 2017, 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.

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import qiskit.tools.jupyter\n", + "%qiskit_version_table\n", + "%qiskit_copyright" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:envruntime2]", + "language": "python", + "name": "conda-env-envruntime2-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tutorials/sample_expval_program/qiskit_runtime_expval_program.ipynb b/tutorials/sample_expval_program/qiskit_runtime_expval_program.ipynb new file mode 100644 index 0000000000..53c723b7fa --- /dev/null +++ b/tutorials/sample_expval_program/qiskit_runtime_expval_program.ipynb @@ -0,0 +1,823 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "33be72af", + "metadata": {}, + "source": [ + "# Custom Expectation Value Program for the Qiskit Runtime\n", + "\n", + "\n", + "

\n", + "Paul Nation\n", + "

\n", + "

\n", + "IBM Quantum Partners Technical Enablement Team\n", + "

\n", + "\n", + "Here we will show how to make a program that takes a circuit, or list of circuits, and computes the expectation values of one or more diagonal operators." + ] + }, + { + "cell_type": "markdown", + "id": "118e72f0", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- You must have Qiskit 0.32+ installed.\n", + "- You must have an IBM Quantum Experience account with the ability to upload a Runtime program. You can upload a program if you have access to more than just the open hub/group/project (ibm-q/open/main)." + ] + }, + { + "cell_type": "markdown", + "id": "6368c90a", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "The primary method by which information is obtained from quantum computers is via expectation values. Indeed, the samples that come from executing a quantum circuit multiple times, once converted to probabilities, can be viewed as just a finite sample approximation to the expectation value for the projection operators corresponding to each bitstring. More practically, many quantum algorithms require computing expectation values over Pauli operators, e.g. Variational Quantum Eigensolvers, and thus having a runtime program that computes these quantities is of fundamental importance. Here we look at one such example, where an user passes one or more circuits and expectation operators and gets back the computed expectation values, and possibly error bounds.\n", + "\n", + "### Expectation value of a diagonal operator\n", + "\n", + "Consider a generic observable given by the tensor product of diagonal operators over $N$ qubits $O = O_{N-1}\\dots O_{0}$ where the subscript indicates the qubit on which the operator acts. Then for a set of observed $M$ bitstrings $\\{b_{0}, \\dots b_{M-1}\\}$, where $M \\leq 2^N $, with corresponding approximate probabilites $p_{m}$ the expectation value is given by\n", + "\n", + "$$\n", + "\\langle O\\rangle \\simeq \\sum_{m=0}^{M-1} p_{m}\\prod_{n=0}^{N-1}O_{n}[b_{m}[N-n-1], b_{m}[N-n-1]],\n", + "$$\n", + "\n", + "where $O_{n}[b_{m}[N-n-1], b_{m}[N-n-1]]$ is the diagonal element of $O_{n}$ specified by the $N-n-1$th bit in bitstring $b_{m}$. The reason for the complicated indexing in `b_{m}` is because Qiskit uses least-sginificant bit indexing where the zeroth element of the bit-strings is given by the right-most bit.\n", + "\n", + "Here we will use built-in routines to compute these expectation values. However, it is not hard to do yourself, with plenty of examples to be found." + ] + }, + { + "cell_type": "markdown", + "id": "42df9e62", + "metadata": {}, + "source": [ + "## Main program\n", + "\n", + "Here we define our main function for the expectation value runtime program. As always, our program must start with the `backend`, and `user_messenger` arguements, followed by the actual inputs we pass to the program. Here our options are quite simple:\n", + "\n", + "- `circuits`: A single QuantumCircuit or list of QuantumCircuits to be executed on the target backend.\n", + "\n", + "\n", + "- `expectation_operators`: The operators we want to evaluate. These can be strings of diagonal Pauli's, eg, `ZIZZ`, or custom operators defined by dictionarys. For example, the projection operator on the all ones state of 4 qubits is `{'1111': 1}`.\n", + "\n", + "\n", + "- `shots`: Howe many times to sample each circuit.\n", + "\n", + "\n", + "- `transpiler_config`: A dictionary that passes additional arguments on to the transpile function, eg. `optimization_level`.\n", + "\n", + "\n", + "- `run_config`: A dictionary that passes additional arguments on to `backend.run()`.\n", + "\n", + "\n", + "- `skip_transpilation`: A flag to skip transpilation altogether and just run the circuits. This is useful for situations where you need to transpile parameterized circuits once, but must bind parameters multiple times and evaluate. \n", + "\n", + "\n", + "- `return_stddev`: Flag to return bound on standard deviation. If using measurement mitigation this adds some overhead to the computation.\n", + "\n", + "\n", + "- `use_measurement_mitigation`: Use M3 measurement mitigation and compute expecation value and standard deviation bound from quasi-probabilities.\n", + "\n", + "At the top of the cell below you will see a commented out `%%writefile sample_expval.py`. We will use this to convert the cell to a Python module named `sample_expval.py` to upload." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "06b48e43", + "metadata": {}, + "outputs": [], + "source": [ + "#%%writefile sample_expval.py\n", + "import mthree\n", + "from qiskit import transpile\n", + "\n", + "# The entrypoint for our Runtime Program\n", + "def main(backend, user_messenger,\n", + " circuits,\n", + " expectation_operators='',\n", + " shots = 8192,\n", + " transpiler_config={},\n", + " run_config={},\n", + " skip_transpilation=False,\n", + " return_stddev=False,\n", + " use_measurement_mitigation=False,\n", + " ):\n", + " \n", + " \"\"\"Compute expectation \n", + " values for a list of operators after\n", + " executing a list of circuits on the target backend.\n", + " \n", + " Parameters:\n", + " backend (ProgramBackend): Qiskit backend instance.\n", + " user_messenger (UserMessenger): Used to communicate with the program user.\n", + " circuits: (QuantumCircuit or list): A single list of QuantumCircuits.\n", + " expectation_operators (str or dict or list): Expectation values to evaluate.\n", + " shots (int): Number of shots to take per circuit.\n", + " transpiler_config (dict): A collection of kwargs passed to transpile().\n", + " run_config (dict): A collection of kwargs passed to backend.run().\n", + " skip_transpilation (bool): Skip transpiling of circuits, default=False.\n", + " return_stddev (bool): Return upper bound on standard devitation,\n", + " default=False. \n", + " use_measurement_mitigation (bool): Improve resulting using measurement\n", + " error mitigation, default=False.\n", + " \n", + " Returns:\n", + " array_like: Returns array of expectation values or a list of (expval, stddev)\n", + " tuples if return_stddev=True.\n", + " \"\"\"\n", + " \n", + " # transpiling the circuits using given transpile options\n", + " if not skip_transpilation:\n", + " trans_circuits = transpile(circuits, backend=backend,\n", + " **transpiler_config)\n", + " # Make sure everything is a list\n", + " if not isinstance(trans_circuits, list):\n", + " trans_circuits = [trans_circuits]\n", + " # If skipping set circuits -> trans_circuits\n", + " else:\n", + " if not isinstance(circuits, list):\n", + " trans_circuits = [circuits]\n", + " else:\n", + " trans_circuits = circuits\n", + "\n", + " # If we are given a single circuit but requesting multiple expectation\n", + " # values, then set flag to make multiple pointers to same result.\n", + " duplicate_results = False\n", + " if isinstance(expectation_operators, list):\n", + " if len(expectation_operators) and len(trans_circuits) == 1:\n", + " duplicate_results = True\n", + " \n", + " # If doing measurement mitigation we must build and calibrate a\n", + " # mitigator object. Will also determine which qubits need to be\n", + " # calibrated.\n", + " if use_measurement_mitigation:\n", + " # Get an the measurement mappings at end of circuits\n", + " meas_maps = mthree.utils.final_measurement_mapping(trans_circuits)\n", + " # Get an M3 mitigator\n", + " mit = mthree.M3Mitigation(backend)\n", + " # Calibrate over the set of qubits measured in the transpiled circuits.\n", + " mit.cals_from_system(meas_maps)\n", + "\n", + " # Compute raw results\n", + " result = backend.run(trans_circuits, shots=shots, **run_config).result()\n", + " raw_counts = result.get_counts()\n", + "\n", + " # When using measurement mitigation we need to apply the correction and then\n", + " # compute the expectation values from the computed quasi-probabilities.\n", + " if use_measurement_mitigation:\n", + " quasi_dists = mit.apply_correction(raw_counts, meas_maps,\n", + " return_mitigation_overhead=return_stddev)\n", + " \n", + " if duplicate_results:\n", + " quasi_dists = mthree.classes.QuasiCollection(\n", + " [quasi_dists]*len(expectation_operators))\n", + " # There are two different calls depending on what we want returned.\n", + " if return_stddev:\n", + " return quasi_dists.expval_and_stddev(expectation_operators)\n", + " return quasi_dists.expval(expectation_operators)\n", + " \n", + " # If the program didn't return in the mitigation loop above it means\n", + " # we are processing the raw_counts data. We do so here using the\n", + " # mthree utilities\n", + " if duplicate_results:\n", + " raw_counts = [raw_counts]*len(expectation_operators)\n", + " if return_stddev:\n", + " return mthree.utils.expval_and_stddev(raw_counts, expectation_operators)\n", + " return mthree.utils.expval(raw_counts, expectation_operators)" + ] + }, + { + "cell_type": "markdown", + "id": "edadd3f8", + "metadata": {}, + "source": [ + "## Local testing\n", + "\n", + "Here we test with a local \"Fake\" backend that mimics the noise properties of a real system and a 4-qubit GHZ state." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2a25b3ac", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.test.mock import FakeSantiago\n", + "from qiskit.providers.ibmq.runtime import UserMessenger\n", + "msg = UserMessenger()\n", + "backend = FakeSantiago()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6339a144", + "metadata": {}, + "outputs": [], + "source": [ + "qc = QuantumCircuit(4)\n", + "qc.h(2)\n", + "qc.cx(2, 1)\n", + "qc.cx(1, 0)\n", + "qc.cx(2, 3)\n", + "qc.measure_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3966f447", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.974614 , 1. , 0.02428596])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "main(backend, msg,\n", + " qc,\n", + " expectation_operators=['ZZZZ', 'IIII', 'IZZZ'],\n", + " transpiler_config={'optimization_level':3, 'layout_method': 'sabre',\n", + " 'routing_method': 'sabre'},\n", + " run_config={},\n", + " skip_transpilation=False,\n", + " return_stddev=False,\n", + " use_measurement_mitigation=True\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "fafe3295", + "metadata": {}, + "source": [ + "If we have done our job correctly, the above should print out two expectation values close to one and a final expectation value close to zero." + ] + }, + { + "cell_type": "markdown", + "id": "f1fa15d4", + "metadata": {}, + "source": [ + "## Program metadata\n", + "\n", + "Next we add the needed program data to a dictionary for uploading with our program." + ] + }, + { + "cell_type": "code", + "execution_count": 297, + "id": "cdb8037d", + "metadata": {}, + "outputs": [], + "source": [ + "meta = {\n", + " \"name\": \"sample-expval\",\n", + " \"description\": \"A sample expectation value program.\",\n", + " \"max_execution_time\": 1000,\n", + " \"spec\": {}\n", + "}\n", + "\n", + "meta[\"spec\"][\"parameters\"] = {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"properties\": {\n", + " \"circuits\": {\n", + " \"description\": \"A single or list of QuantumCircuits.\",\n", + " \"type\": [\n", + " \"array\",\n", + " \"object\"\n", + " ]\n", + " },\n", + " \"expectation_operators\": {\n", + " \"description\": \"One or more expectation values to evaluate.\",\n", + " \"type\": [\n", + " \"string\",\n", + " \"object\",\n", + " \"array\"\n", + " ]\n", + " },\n", + " \"shots\": {\n", + " \"description\": \"Number of shots per circuit.\",\n", + " \"type\": \"integer\"\n", + " },\n", + " \"transpiler_config\": {\n", + " \"description\": \"A collection of kwargs passed to transpile.\",\n", + " \"type\": \"object\"\n", + " },\n", + " \"run_config\": {\n", + " \"description\": \"A collection of kwargs passed to backend.run. Default is False.\",\n", + " \"type\": \"object\",\n", + " \"default\": False\n", + " },\n", + " \"return_stddev\": {\n", + " \"description\": \"Return upper-bound on standard deviation. Default is False.\",\n", + " \"type\": \"boolean\",\n", + " \"default\": False\n", + " },\n", + " \"use_measurement_mitigation\": {\n", + " \"description\": \"Use measurement mitigation to improve results. Default is False.\",\n", + " \"type\": \"boolean\",\n", + " \"default\": False\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"circuits\"\n", + " ]\n", + "}\n", + "\n", + "meta[\"spec\"][\"return_values\"] = {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"description\": \"A list of expectation values and optionally standard deviations.\",\n", + " \"type\": \"array\"\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "09521947", + "metadata": {}, + "source": [ + "## Upload the program\n", + "\n", + "We are now in a position to upload the program. To do so we first uncomment and excute the line `%%writefile sample_expval.py` giving use the `sample_expval.py` file we need to upload. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "935a21d6", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import IBMQ\n", + "IBMQ.load_account();" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d93c23d6", + "metadata": {}, + "outputs": [], + "source": [ + "provider = IBMQ.get_provider(group='deployed')" + ] + }, + { + "cell_type": "code", + "execution_count": 276, + "id": "59a3e697", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "'sample-expval-KLGD4Kbn97'" + }, + "execution_count": 276, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "program_id = provider.runtime.upload_program(data='sample_expval.py', metadata=meta)\n", + "program_id" + ] + }, + { + "cell_type": "markdown", + "id": "58430b3e", + "metadata": {}, + "source": [ + "### Delete program if needed" + ] + }, + { + "cell_type": "code", + "execution_count": 255, + "id": "4ec662be", + "metadata": {}, + "outputs": [], + "source": [ + "#provider.runtime.delete_program(program_id)" + ] + }, + { + "cell_type": "markdown", + "id": "8542a282", + "metadata": {}, + "source": [ + "## Wrapping the runtime program\n", + "\n", + "As always, it is best to wrap the call to the runtime program with a function (or possibly a class) that makes input easier and does some validation." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c685ef09", + "metadata": {}, + "outputs": [], + "source": [ + "def expectation_value_runner(backend,\n", + " circuits,\n", + " expectation_operators='',\n", + " shots = 8192,\n", + " transpiler_config={},\n", + " run_config={},\n", + " skip_transpilation=False,\n", + " return_stddev=False,\n", + " use_measurement_mitigation=False):\n", + " \n", + " \"\"\"Compute expectation values for a list of operators after\n", + " executing a list of circuits on the target backend.\n", + " \n", + " Parameters:\n", + " backend (Backend or str): Qiskit backend instance or name.\n", + " circuits: (QuantumCircuit or list): A single or list of QuantumCircuits.\n", + " expectation_operators (str or dict or list): Expectation values to evaluate.\n", + " shots (int): Number of shots to take per circuit.\n", + " transpiler_config (dict): A collection of kwargs passed to transpile().\n", + " run_config (dict): A collection of kwargs passed to backend.run().\n", + " return_stddev (bool): Return upper bound on standard devitation,\n", + " default=False. \n", + " skip_transpilation (bool): Skip transpiling of circuits, default=False.\n", + " use_measurement_mitigation (bool): Improve resulting using measurement\n", + " error mitigation, default=False.\n", + " \n", + " Returns:\n", + " array_like: Returns array of expectation values or a list of (expval, stddev)\n", + " pairs if return_stddev=True.\n", + " \"\"\"\n", + " if not isinstance(backend, str):\n", + " backend = backend.name()\n", + " options = {'backend_name': backend}\n", + " \n", + " if isinstance(circuits, list) and len(circuits) != 1:\n", + " if isinstance(expectation_operators, list):\n", + " if len(circuits) != 1 and len(expectation_operators) == 1:\n", + " expectation_operators = expectation_operators*len(circuits)\n", + " elif len(circuits) != len(expectation_operators): \n", + " raise ValueError('Number of circuits must match number of expectation \\\n", + " values if more than one of each')\n", + " inputs = {}\n", + " inputs['circuits'] = circuits\n", + " inputs['expectation_operators'] = expectation_operators\n", + " inputs['shots'] = shots\n", + " inputs['transpiler_config'] = transpiler_config\n", + " inputs['run_config'] = run_config\n", + " inputs['return_stddev'] = return_stddev\n", + " inputs['skip_transpilation'] = skip_transpilation\n", + " inputs['use_measurement_mitigation'] = use_measurement_mitigation\n", + " \n", + " return provider.runtime.run('sample-expval', options=options, inputs=inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "766a8961", + "metadata": {}, + "source": [ + "### Trying it out\n", + "\n", + "Because we made our program public anyone can try it out. Lets do so here with our previously made GHZ state and running on the simulator." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b9f2a955", + "metadata": {}, + "outputs": [], + "source": [ + "backend = provider.backend.ibmq_qasm_simulator\n", + "\n", + "all_zeros_proj = {'0000': 1}\n", + "all_ones_proj = {'1111': 1}\n", + "job = expectation_value_runner(backend, qc, [all_zeros_proj, all_ones_proj, 'ZZZZ'])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "beb8550b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.50012207, 0.49987793, 1. ])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "29268312", + "metadata": {}, + "source": [ + "The first two projectors should be nearly $0.50$ as they tell use the probability of being in the all zeros and ones states, respectively, which should be 50/50 for our GHZ state. The final expectation value of `ZZZZ` should be one since this is a GHZ over an even number of qubits. It should go close to zero for an odd number." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5494d586", + "metadata": {}, + "outputs": [], + "source": [ + "qc2 = QuantumCircuit(3)\n", + "qc2.h(2)\n", + "qc2.cx(2, 1)\n", + "qc2.cx(1, 0)\n", + "qc2.measure_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2c326f9d", + "metadata": {}, + "outputs": [], + "source": [ + "all_zeros_proj = {'000': 1}\n", + "all_ones_proj = {'111': 1}\n", + "job2 = expectation_value_runner(backend, qc2, [all_zeros_proj, all_ones_proj, 'ZZZ'])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "58d9a637", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.50415039, 0.49584961, 0.00830078])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job2.result()" + ] + }, + { + "cell_type": "markdown", + "id": "05ff6e0d", + "metadata": {}, + "source": [ + "## Quantum Volume as an expectation value\n", + "\n", + "Here we formulate QV as an expectation value of a projector onto the heavy-output elements on a distribution. We can then use our expectation value routine to compute whether a given circuit has passed the QV metric.\n", + "\n", + "QV is defined in terms of heavy-ouputs of a distribution. Heavy-outputs are those bit-strings that are those that have probabilities above the median value of the distribution. Below we define the projection operator onto the set of bit-strings that are heavy-outputs for a given distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "778b6a77", + "metadata": {}, + "outputs": [], + "source": [ + "def heavy_projector(qv_probs):\n", + " \"\"\"Forms the projection operator onto the heavy-outputs of a given probability distribution.\n", + "\n", + " Parameters:\n", + " qv_probs (dict): A dictionary of bitstrings and associated probabilities.\n", + "\n", + " Returns:\n", + " dict: Projector onto the heavy-set.\n", + " \"\"\"\n", + " median_prob = np.median(list(qv_probs.values()))\n", + " heavy_strs = {}\n", + " for key, val in qv_probs.items():\n", + " if val > median_prob:\n", + " heavy_strs[key] = 1\n", + " return heavy_strs" + ] + }, + { + "cell_type": "markdown", + "id": "1c05e1b2", + "metadata": {}, + "source": [ + "Now we generate 10 QV circuits as our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "20fab8af", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qiskit.quantum_info import Statevector\n", + "from qiskit.circuit.library import QuantumVolume\n", + "# Generate QV circuits\n", + "N = 10\n", + "qv_circs = [QuantumVolume(5) for _ in range(N)]" + ] + }, + { + "cell_type": "markdown", + "id": "927a0946", + "metadata": {}, + "source": [ + "Next, we have to determine the heavy-set of each circuit from the ideal answer, and then pass this along to our heavy-set projector function that we defined above." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "73d822cc", + "metadata": {}, + "outputs": [], + "source": [ + "ideal_probs = [Statevector.from_instruction(circ).probabilities_dict() for circ in qv_circs]\n", + "heavy_projectors = [heavy_projector(probs) for probs in ideal_probs]" + ] + }, + { + "cell_type": "markdown", + "id": "33559bc5", + "metadata": {}, + "source": [ + "QV circuits have no meaasurements on them so need to add them:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "bf818575", + "metadata": {}, + "outputs": [], + "source": [ + "circs = [circ.measure_all(inplace=False) for circ in qv_circs]" + ] + }, + { + "cell_type": "markdown", + "id": "dd9579fb", + "metadata": {}, + "source": [ + "With a list of circuits and projection operators we now need only to pass both sets to our above expection value runner targeting the desired backend. We will also set the best transpiler arguments to give us a sporting chance of getting some passing scores." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "8d692921", + "metadata": {}, + "outputs": [], + "source": [ + "backend = provider.backend.ibmq_manila" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "5c169661", + "metadata": {}, + "outputs": [], + "source": [ + "job3 = expectation_value_runner(backend, circs, heavy_projectors,\n", + " transpiler_config={'optimization_level':3, 'layout_method': 'sabre',\n", + " 'routing_method': 'sabre'})" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "98f6efd9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.52392578, 0.60400391, 0.57189941, 0.57897949, 0.7734375 ,\n", + " 0.65844727, 0.56225586, 0.73706055, 0.69030762, 0.61193848])" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qv_scores = job3.result()\n", + "qv_scores" + ] + }, + { + "cell_type": "markdown", + "id": "947b5149", + "metadata": {}, + "source": [ + "A passing QV score is one where the value of the heavy-set projector is above $2/3$. So let us see who passed:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "3f6394d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([False, False, False, False, True, False, False, True, True,\n", + " False])" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qv_scores > 2/3" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "0a8fe223", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

This code is a part of Qiskit

© Copyright IBM 2017, 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.

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.tools.jupyter import *\n", + "%qiskit_copyright" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c482e31", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/sample_expval_program/sample_expval.py b/tutorials/sample_expval_program/sample_expval.py new file mode 100644 index 0000000000..72ba1edd69 --- /dev/null +++ b/tutorials/sample_expval_program/sample_expval.py @@ -0,0 +1,91 @@ +import mthree +from qiskit import transpile + +# The entrypoint for our Runtime Program +def main(backend, user_messenger, + circuits, + expectation_operators='', + shots = 8192, + transpiler_config={}, + run_config={}, + skip_transpilation=False, + return_stddev=False, + use_measurement_mitigation=False, + ): + + """Compute expectation values for a list of operators after + executing a list of circuits on the target backend. + + Parameters: + backend (ProgramBackend): Qiskit backend instance. + user_messenger (UserMessenger): Used to communicate with the program user. + circuits: (QuantumCircuit or list): A single list of QuantumCircuits. + expectation_operators (str or dict or list): Expectation values to evaluate. + shots (int): Number of shots to take per circuit. + transpiler_config (dict): A collection of kwargs passed to transpile(). + run_config (dict): A collection of kwargs passed to backend.run(). + skip_transpilation (bool): Skip transpiling of circuits, default=False. + return_stddev (bool): Return upper bound on standard devitation, + default=False. + use_measurement_mitigation (bool): Improve resulting using measurement + error mitigation, default=False. + + Returns: + array_like: Returns array of expectation values or a list of (expval, stddev) + tuples if return_stddev=True. + """ + + # transpiling the circuits using given transpile options + if not skip_transpilation: + trans_circuits = transpile(circuits, backend=backend, + **transpiler_config) + + if not isinstance(trans_circuits, list): + trans_circuits = [trans_circuits] + # If skipping set circuits -> trans_circuits + else: + if not isinstance(circuits, list): + trans_circuits = [circuits] + else: + trans_circuits = circuits + + # If we are given a single circuit but requesting multiple expectation values + # Then set flag to make multiple pointers to same result. + duplicate_results = False + if isinstance(expectation_operators, list): + if len(expectation_operators) and len(trans_circuits) == 1: + duplicate_results = True + + if use_measurement_mitigation: + # Get an the measurement mappings at end of circuits + meas_maps = mthree.utils.final_measurement_mapping(trans_circuits) + # Get an M3 mitigator + mit = mthree.M3Mitigation(backend) + # Calibrate over the set of qubits measured in the transpiled circuits. + mit.cals_from_system(meas_maps) + + # Compute raw results + result = backend.run(trans_circuits, shots=shots, **run_config).result() + raw_counts = result.get_counts() + + # When using measurement mitigation we need to apply the correction and then + # compute the expectation values from the computed quasi-probabilities. + if use_measurement_mitigation: + quasi_dists = mit.apply_correction(raw_counts, meas_maps, + return_mitigation_overhead=return_stddev) + + if duplicate_results: + quasi_dists = mthree.classes.QuasiCollection([quasi_dists]*len(expectation_operators)) + # There are two different calls depending on what we want returned. + if return_stddev: + return quasi_dists.expval_and_stddev(expectation_operators) + return quasi_dists.expval(expectation_operators) + + # If the program didn't return in the mitigation loop above it means + # we are processing the raw_counts data. We do so here using the + # mthree utilities + if duplicate_results: + raw_counts = [raw_counts]*len(expectation_operators) + if return_stddev: + return mthree.utils.expval_and_stddev(raw_counts, expectation_operators) + return mthree.utils.expval(raw_counts, expectation_operators) diff --git a/tutorials/sample_vqe_program/qiskit_runtime_vqe_program.ipynb b/tutorials/sample_vqe_program/qiskit_runtime_vqe_program.ipynb new file mode 100644 index 0000000000..4c3ab084a9 --- /dev/null +++ b/tutorials/sample_vqe_program/qiskit_runtime_vqe_program.ipynb @@ -0,0 +1,1381 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "98d4d396", + "metadata": {}, + "source": [ + "# Creating Custom Programs for Qiskit Runtime\n", + "\n", + "

\n", + "Paul Nation\n", + "

\n", + "

\n", + "IBM Quantum Partners Technical Enablement Team\n", + "

\n", + "\n", + "Here we will demonstrate how to create, upload, and use a custom Program for Qiskit Runtime. As the utility of the Runtime execution engine lies in its ability to execute many quantum circuits with low latencies, this tutorial will show how to create your own Variational Quantum Eigensolver (VQE) program from scratch." + ] + }, + { + "cell_type": "markdown", + "id": "96b86d47", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- You must have Qiskit 0.30+ installed.\n", + "- You must have an IBM Quantum account with the ability to upload a runtime program. You have this ability if you belong to more than one provider." + ] + }, + { + "cell_type": "markdown", + "id": "46817643", + "metadata": {}, + "source": [ + "## Current limitations\n", + "\n", + "The runtime execution engine currently has the following limitations that must be kept in mind:\n", + "\n", + "- The Docker images used by the runtime include only Qiskit and its dependencies, with few exceptions. One exception is the inclusion of the `mthree` measurement mitigation package.\n", + "\n", + "\n", + "- For security reasons, the runtime cannot make internet calls outside of the environment.\n", + "\n", + "\n", + "- Your runtime program name must not contain an underscore`_`, otherwise it will cause an error when you try to execute it.\n", + "\n", + "As Qiskit Runtime matures, these limitations will be removed." + ] + }, + { + "cell_type": "markdown", + "id": "98e4b3ac", + "metadata": {}, + "source": [ + "## Simple VQE\n", + "\n", + "VQE is an hybrid quantum-classical optimization procedure that finds the lowest eigenstate and eigenenergy of a linear system defined by a given Hamiltonian of Pauli operators. For example, consider the following two-qubit Hamiltonian:\n", + "\n", + "\n", + "$$\n", + "H = A X_{1}\\otimes X_{0} + A Y_{1}\\otimes Y_{0} + A Z_{1}\\otimes Z_{0},\n", + "$$\n", + "\n", + "where $A$ is numerical coefficient and the subscripts label the qubits on which the operators act. The zero index being farthest right is the ordering used in Qiskit. The Pauli operators tell us which measurement basis to to use when measuring each of the qubits.\n", + "\n", + "We want to find the ground state (lowest energy state) of this Hamiltonian, and the associated eigenvector. To do this we must start at a given initial state and iteratively vary the parameters that define this state using a classical optimizer, such that the computed energies of subsequent steps are nominally lower than those previously. The parameterized state of the system is defined by an ansatz quantum circuit that should have non-zero support in the direction of the ground state. Because in general we do not know the solution, the choice of ansatz circuit can be highly problem-specific with a form dictated by additional information. For further information about variational algorithms, we point the reader to [Nature Reviews Physics volume 3, 625 (2021)](https://doi.org/10.1038/s42254-021-00348-9).\n", + "\n", + "\n", + "Thus we need at least the following inputs to create our VQE quantum program:\n", + "\n", + "1. A representation of the Hamiltonian that specifies the problem.\n", + "\n", + "\n", + "2. A choice of parameterized ansatz circuit, and the ability to pass configuration options, if any.\n", + "\n", + "\n", + "However, the following are also beneficial inputs that users might want to have:\n", + "\n", + "3. Add the ability to pass an initial state.\n", + "\n", + "\n", + "4. Vary the number of shots that are taken.\n", + "\n", + "\n", + "5. Ability to select which classical optimizer is used, and set configuraton values, if any. \n", + "\n", + "\n", + "6. Ability to turn on and off measurement mitigation.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d15ad0da", + "metadata": {}, + "source": [ + "## Specifying the form of the input values\n", + "\n", + "All inputs to runtime programs must be serializable objects. That is to say, whatever you pass into a runtime program must be able to be converted to JSON format. It is thus beneficial to keep inputs limited to basic data types and structures unless you have experience with custom object serialization, or they are common Qiskit types such as ``QuantumCircuit`` etc that the built-in `RuntimeEncoder` can handle. Fortunately, the VQE program described above can be made out of simple Python components.\n", + "\n", + "First, it is possible to represent any Hamiltonian using a list of values with each containing the numerical coefficeint for each term and the string representation for the Pauli operators. For the above example, the ground state energy with $A=1$ is $-3$ and we can write it as:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "78b7a519", + "metadata": {}, + "outputs": [], + "source": [ + "H = [(1, 'XX'), (1, 'YY'), (1, 'ZZ')]" + ] + }, + { + "cell_type": "markdown", + "id": "fca6d7c5", + "metadata": {}, + "source": [ + "Next we have to provide the ability to specify the parameterized Ansatz circuit. Here we will take advange of the fact that many ansatz circuits are pre-defined in the Qiskit Circuit Library. Examples can be found in the [N-local circuits section](https://qiskit.org/documentation/apidoc/circuit_library.html#n-local-circuits).\n", + "\n", + "We would like the user to be able to select between ansatz options such as: `NLocal`, `TwoLocal`, and `EfficientSU2`. We could have the user pass the whole ansatz circuit to the program; however, in order to reduce the size of the upload we will pass the ansatz by name. In the runtime program, we can take this name and get the class that it corresponds to from the library using, for example, " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "421f2f51", + "metadata": {}, + "outputs": [], + "source": [ + "import qiskit.circuit.library.n_local as lib_local\n", + "\n", + "ansatz = getattr(lib_local, 'EfficientSU2')" + ] + }, + { + "cell_type": "markdown", + "id": "48c7ebae", + "metadata": {}, + "source": [ + "For the ansatz configuration, we will pass a simple `dict` of values." + ] + }, + { + "cell_type": "markdown", + "id": "a592ac05", + "metadata": {}, + "source": [ + "### Optionals \n", + "\n", + "- If we want to add the ability to pass an initial state, then we will need to add the ability to pass a 1D list/ NumPy array. Because the number of parameters depends on the ansatz and its configuration, the user would have to know what ansatz they are doing ahead of time.\n", + "\n", + "\n", + "- Selecting a number of shots requires simply passing an integer value.\n", + "\n", + "\n", + "- Here we will allow selecting a classical optimizer by name from those in SciPy, and a `dict` of configuration parameters. Note that for execution on an actual system, the noise inherent in today's quantum systems makes having a stochastic optimizer crucial to success. SciPy does not have such a choice, and the one built into Qiskit is wrapped in such a manner as to make it difficult to use elsewhere. As such, here we will use an SPSA optimizer written to match the style of those in SciPy. This function is given in [Appendix A](#Appendix-A)." + ] + }, + { + "cell_type": "markdown", + "id": "8c5964c1", + "metadata": {}, + "source": [ + "- Finally, for measurement error mitigation we can simply pass a boolean (True/False) value." + ] + }, + { + "cell_type": "markdown", + "id": "00df4e79", + "metadata": {}, + "source": [ + "## Main program\n", + "\n", + "We are now in a position to start building our main program. However, before doing so we point out that it makes the code cleaner to make a separate fuction that takes strings of Pauli operators that define our Hamiltonian and convert them to a list of circuits with single-qubit gates that change the measurement basis for each qubit, if needed. This function is given in [Appendix B](#Appendix-B)." + ] + }, + { + "cell_type": "markdown", + "id": "3ddbadd8", + "metadata": {}, + "source": [ + "### Required signature\n", + "\n", + "Every runtime program is defined via the `main` function, and must have the following input signature:\n", + "\n", + "```\n", + "main(backend, user_message, *args, **kwargs)\n", + "```\n", + "\n", + "where `backend` is the backend that the program is to be executed on, and `user_message` is the class by which interim (and possibly final) results are communicated back to the user. After these two items, we add our program-specific arguments and keyword arguments." + ] + }, + { + "cell_type": "markdown", + "id": "28ba84cc", + "metadata": {}, + "source": [ + "### The main VQE program\n", + "\n", + "Here is the main program for our sample VQE. What each element of the function does is written in the comments before the element appears." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "96bb7811", + "metadata": {}, + "outputs": [], + "source": [ + "# Grab functions and modules from dependencies\n", + "import numpy as np\n", + "import scipy.optimize as opt\n", + "from scipy.optimize import OptimizeResult\n", + "import mthree\n", + "\n", + "# Grab functions and modules from Qiskit needed\n", + "from qiskit import QuantumCircuit, transpile\n", + "import qiskit.circuit.library.n_local as lib_local\n", + "\n", + "# The entrypoint for our Runtime Program\n", + "def main(backend, user_messenger,\n", + " hamiltonian,\n", + " ansatz='EfficientSU2',\n", + " ansatz_config={},\n", + " x0=None,\n", + " optimizer='SPSA',\n", + " optimizer_config={'maxiter': 100},\n", + " shots = 8192,\n", + " use_measurement_mitigation=False\n", + " ):\n", + " \n", + " \"\"\"\n", + " The main sample VQE program.\n", + " \n", + " Parameters:\n", + " backend (ProgramBackend): Qiskit backend instance.\n", + " user_messenger (UserMessenger): Used to communicate with the\n", + " program user.\n", + " hamiltonian (list): Hamiltonian whose ground state we want to find.\n", + " ansatz (str): Optional, name of ansatz quantum circuit to use,\n", + " default='EfficientSU2'\n", + " ansatz_config (dict): Optional, configuration parameters for the\n", + " ansatz circuit.\n", + " x0 (array_like): Optional, initial vector of parameters.\n", + " optimizer (str): Optional, string specifying classical optimizer,\n", + " default='SPSA'.\n", + " optimizer_config (dict): Optional, configuration parameters for the\n", + " optimizer.\n", + " shots (int): Optional, number of shots to take per circuit.\n", + " use_measurement_mitigation (bool): Optional, use measurement mitigation,\n", + " default=False.\n", + " \n", + " Returns:\n", + " OptimizeResult: The result in SciPy optimization format. \n", + " \"\"\"\n", + " \n", + " # Split the Hamiltonian into two arrays, one for coefficients, the other for\n", + " # operator strings\n", + " coeffs = np.array([item[0] for item in hamiltonian], dtype=complex)\n", + " op_strings = [item[1] for item in hamiltonian]\n", + " # The number of qubits needed is given by the number of elements in the strings\n", + " # the defiune the Hamiltonian. Here we grab this data from the first element.\n", + " num_qubits = len(op_strings[0])\n", + " \n", + " # We grab the requested ansatz circuit class from the Qiskit circuit library\n", + " # n_local module and configure it using the number of qubits and options\n", + " # passed in the ansatz_config.\n", + " ansatz_instance = getattr(lib_local, ansatz)\n", + " ansatz_circuit = ansatz_instance(num_qubits, **ansatz_config)\n", + " \n", + " # Here we use our convenence function from Appendix B to get measurement circuits\n", + " # with the correct single-qubit rotation gates.\n", + " meas_circs = opstr_to_meas_circ(op_strings)\n", + " \n", + " # When computing the expectation value for the energy, we need to know if we\n", + " # evaluate a Z measurement or and identity measurement. Here we take and X and Y\n", + " # operator in the strings and convert it to a Z since we added the rotations\n", + " # with the meas_circs.\n", + " meas_strings = [string.replace('X', 'Z').replace('Y', 'Z') for string in op_strings]\n", + " \n", + " # Take the ansatz circuits, add the single-qubit measurement basis rotations from\n", + " # meas_circs, and finally append the measurements themselves.\n", + " full_circs = [ansatz_circuit.compose(mcirc).measure_all(inplace=False) for mcirc in meas_circs]\n", + " \n", + " # Get the number of parameters in the ansatz circuit.\n", + " num_params = ansatz_circuit.num_parameters\n", + " \n", + " # Use a given initial state, if any, or do random initial state.\n", + " if x0:\n", + " x0 = np.asarray(x0, dtype=float)\n", + " if x0.shape[0] != num_params:\n", + " raise ValueError('Number of params in x0 ({}) does not match number \\\n", + " of ansatz parameters ({})'. format(x0.shape[0],\n", + " num_params))\n", + " else:\n", + " x0 = 2*np.pi*np.random.rand(num_params)\n", + " \n", + " # Because we are in general targeting a real quantum system, our circuits must be transpiled\n", + " # to match the system topology and, hopefully, optimize them.\n", + " # Here we will set the transpiler to the most optimal settings where 'sabre' layout and\n", + " # routing are used, along with full O3 optimization.\n", + "\n", + " # This works around a bug in Qiskit where Sabre routing fails for simulators (Issue #7098)\n", + " trans_dict = {}\n", + " if not backend.configuration().simulator:\n", + " trans_dict = {'layout_method': 'sabre', 'routing_method': 'sabre'}\n", + " trans_circs = transpile(full_circs, backend, optimization_level=3, **trans_dict)\n", + " \n", + " # If using measurement mitigation we need to find out which physical qubits our transpiled\n", + " # circuits actually measure, construct a mitigation object targeting our backend, and\n", + " # finally calibrate our mitgation by running calibration circuits on the backend.\n", + " if use_measurement_mitigation:\n", + " maps = mthree.utils.final_measurement_mapping(trans_circs)\n", + " mit = mthree.M3Mitigation(backend)\n", + " mit.cals_from_system(maps)\n", + " \n", + " # Here we define a callback function that will stream the optimizer parameter vector\n", + " # back to the user after each iteration. This uses the `user_messenger` object.\n", + " # Here we convert to a list so that the return is user readable locally, but\n", + " # this is not required.\n", + " def callback(xk):\n", + " user_messenger.publish(list(xk))\n", + " \n", + " # This is the primary VQE function executed by the optimizer. This function takes the \n", + " # parameter vector as input and returns the energy evaluated using an ansatz circuit\n", + " # bound with those parameters.\n", + " def vqe_func(params):\n", + " # Attach (bind) parameters in params vector to the transpiled circuits.\n", + " bound_circs = [circ.bind_parameters(params) for circ in trans_circs]\n", + " \n", + " # Submit the job and get the resultant counts back\n", + " counts = backend.run(bound_circs, shots=shots).result().get_counts()\n", + " \n", + " # If using measurement mitigation apply the correction and\n", + " # compute expectation values from the resultant quasiprobabilities\n", + " # using the measurement strings.\n", + " if use_measurement_mitigation:\n", + " quasi_collection = mit.apply_correction(counts, maps)\n", + " expvals = quasi_collection.expval(meas_strings)\n", + " # If not doing any mitigation just compute expectation values\n", + " # from the raw counts using the measurement strings.\n", + " # Since Qiskit does not have such functionality we use the convenence\n", + " # function from the mthree mitigation module.\n", + " else:\n", + " expvals = mthree.utils.expval(counts, meas_strings)\n", + " \n", + " # The energy is computed by simply taking the product of the coefficients\n", + " # and the computed expectation values and summing them. Here we also\n", + " # take just the real part as the coefficients can possibly be complex,\n", + " # but the energy (eigenvalue) of a Hamiltonian is always real.\n", + " energy = np.sum(coeffs*expvals).real\n", + " return energy\n", + " \n", + " # Here is where we actually perform the computation. We begin by seeing what\n", + " # optimization routine the user has requested, eg. SPSA verses SciPy ones,\n", + " # and dispatch to the correct optimizer. The selected optimizer starts at\n", + " # x0 and calls 'vqe_func' everytime the optimizer needs to evaluate the cost\n", + " # function. The result is returned as a SciPy OptimizerResult object.\n", + " # Additionally, after every iteration, we use the 'callback' function to\n", + " # publish the interm results back to the user. This is important to do\n", + " # so that if the Program terminates unexpectedly, the user can start where they\n", + " # left off.\n", + " \n", + " # Since SPSA is not in SciPy need if statement\n", + " if optimizer == 'SPSA':\n", + " res = fmin_spsa(vqe_func, x0, args=(), **optimizer_config,\n", + " callback=callback)\n", + " # All other SciPy optimizers here\n", + " else:\n", + " res = opt.minimize(vqe_func, x0, method=optimizer,\n", + " options=optimizer_config, callback=callback)\n", + " # Return result. OptimizeResult is a subclass of dict.\n", + " return res" + ] + }, + { + "cell_type": "markdown", + "id": "8ab3432a", + "metadata": {}, + "source": [ + "## Local testing\n", + "\n", + "
\n", + "Important: You need to execute the code blocks in Appendices A and B before continuing.\n", + "
\n", + "\n", + "We can test whether our routine works by simply calling the `main` function with a backend instance, a `UserMessenger`, and sample arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c146a02e", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.providers.ibmq.runtime import UserMessenger\n", + "msg = UserMessenger()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ea291c28", + "metadata": {}, + "outputs": [], + "source": [ + "# Use the local Aer simulator\n", + "from qiskit import Aer\n", + "backend = Aer.get_backend('qasm_simulator')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3f071e0f", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1.419780432710152, 2.3984284215892018, 1.1306533554149105, 1.8357672762510684, 5.414120644000338, 6.107301966755861, -0.013391355872252708, 5.615586607539193, 4.211781149943555, 1.792388243059789, 4.203949657158362, 0.1038271369149637, 2.4220098073658884, 4.617958787629208, 2.9969591661895865, 1.5490655190231735]\n", + "[2.1084925021737537, 3.0871404910528035, 0.4419412859513089, 2.52447934571467, 4.725408574536736, 5.418589897292259, -0.7021034253358543, 6.3042986770027944, 3.523069080479953, 1.1036761735961873, 3.5152375876947604, 0.7925392063785653, 3.11072187682949, 5.30667085709281, 3.685671235653188, 0.8603534495595718]\n", + "[1.7365578685005831, 3.459075124725974, 0.8138759196244794, 2.8964139793878405, 4.353473940863566, 5.046655263619089, -1.0740380590090248, 5.932364043329624, 3.1511344468067826, 1.475610807269358, 3.8871722213679307, 1.1644738400517358, 2.73878724315632, 4.934736223419639, 4.057605869326359, 1.2322880832327423]\n", + "[1.7839871181735734, 3.4116458750529834, 0.766446669951489, 2.84898472971485, 4.306044691190576, 5.094084513292079, -1.0266088093360346, 5.884934793656634, 3.198563696479773, 1.5230400569423481, 3.8397429716949403, 1.1170445903787456, 2.6913579934833294, 4.887306973746649, 4.105035118999349, 1.2797173329057325]\n", + "[1.122687940285629, 4.072945052940928, 1.4277458478394336, 2.1876855518269056, 3.6447455133026314, 5.755383691180024, -1.687907987223979, 6.546233971544579, 2.5372645185918286, 2.1843392348302926, 4.501042149582885, 1.7783437682666903, 3.352657171371274, 4.226007795858704, 4.766334296887294, 0.618418155017788]\n" + ] + }, + { + "data": { + "text/plain": [ + " fun: -1.72705078125\n", + " message: 'Optimization terminated successfully.'\n", + " nfev: 10\n", + " nit: 5\n", + " success: True\n", + " x: array([ 1.12268794, 4.07294505, 1.42774585, 2.18768555, 3.64474551,\n", + " 5.75538369, -1.68790799, 6.54623397, 2.53726452, 2.18433923,\n", + " 4.50104215, 1.77834377, 3.35265717, 4.2260078 , 4.7663343 ,\n", + " 0.61841816])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Execute the main routine for our simple two-qubit Hamiltonian H, and perform 5 iterations of the SPSA solver.\n", + "main(backend, msg, H, optimizer_config={'maxiter': 5})" + ] + }, + { + "cell_type": "markdown", + "id": "1460bc50", + "metadata": {}, + "source": [ + "Having executed the above, we see that there are 5 parameter arrays returned, one for each callback, along with the final optimization result. The parameter arrays are the interim results, and the `UserMessenger` object prints these values to the cell output. The output itself is the answer we obtained, expressed as a SciPy `OptimizerResult` object." + ] + }, + { + "cell_type": "markdown", + "id": "d5d0151f", + "metadata": {}, + "source": [ + "## Program metadata\n", + "\n", + "Program metadata is essentially the docstring for a runtime program. It describes overall program information such as the program `name`, `description`, and the `max_execution_time` the program is allowed to run, as well as details the inputs and the outputs the program expects. At a bare minimum the values described above are required" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2b3aa8f2", + "metadata": {}, + "outputs": [], + "source": [ + "meta = {\n", + " \"name\": \"sample-vqe\",\n", + " \"description\": \"A sample VQE program.\",\n", + " \"max_execution_time\": 100000,\n", + " \"spec\": {}\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "be4e4dd2", + "metadata": {}, + "source": [ + "It is important to set the `max_execution_time` high enough so that your program does not get terminated unexpectedly. Additionally, one should make sure that interim results are sent back to the user so that, if something does happen, the user can start where they left off.\n", + "\n", + "It is, however, good form to detail the parameters and return types, as well as interim results. That being said, if making a runtime intended to be used by others, this information would also likely be mirrored in the signature of a function or class that the user would interact with directly; end users should not directly call runtime programs. We will see why below. Nevertheless, let us add to our metadata. First, the `parameters` section details the inputs the user is able to pass:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d10c1b1a", + "metadata": {}, + "outputs": [], + "source": [ + "meta[\"spec\"][\"parameters\"] = {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"properties\": {\n", + " \"hamiltonian\": {\n", + " \"description\": \"Hamiltonian whose ground state we want to find.\", \n", + " \"type\": \"array\"\n", + " },\n", + " \"ansatz\": {\n", + " \"description\": \"Name of ansatz quantum circuit to use, default='EfficientSU2'\",\n", + " \"type\": \"string\",\n", + " \"default\": \"EfficientSU2\"\n", + " },\n", + " \"ansatz_config\": {\n", + " \"description\": \"Configuration parameters for the ansatz circuit.\",\n", + " \"type\": \"object\"\n", + " },\n", + " \"optimizer\": {\n", + " \"description\": \"Classical optimizer to use, default='SPSA'.\",\n", + " \"type\": \"string\",\n", + " \"default\": \"SPSA\"\n", + " },\n", + " \"x0\": {\n", + " \"description\": \"Initial vector of parameters. This is a numpy array.\", \n", + " \"type\": \"array\"\n", + " },\n", + " \"optimizer_config\": {\n", + " \"description\": \"Configuration parameters for the optimizer.\", \n", + " \"type\": \"object\"\n", + " },\n", + " \"shots\": {\n", + " \"description\": \"The number of shots used for each circuit evaluation.\",\n", + " \"type\": \"integer\"\n", + " },\n", + " \"use_measurement_mitigation\": {\n", + " \"description\": \"Use measurement mitigation, default=False.\",\n", + " \"type\": \"boolean\",\n", + " \"default\": False\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"hamiltonian\"\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "0ee9bb79", + "metadata": {}, + "source": [ + "Next, the `return_values` section tells about the return types:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "404e6d1b", + "metadata": {}, + "outputs": [], + "source": [ + "meta[\"spec\"][\"return_values\"] = {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"description\": \"Final result in SciPy optimizer format\",\n", + " \"type\": \"object\"\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "affde208", + "metadata": {}, + "source": [ + "and finally let us specify what comes back when an interim result is returned:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "99a56745", + "metadata": {}, + "outputs": [], + "source": [ + "meta[\"spec\"][\"interim_results\"] = {\n", + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\n", + " \"description\": \"Parameter vector at current optimization step. This is a numpy array.\", \n", + " \"type\": \"array\"\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "c55bbf04", + "metadata": {}, + "source": [ + "## Uploading the program\n", + "\n", + "We now have all the ingredients needed to upload our program. To do so we need to collect all of our code in one file, here called `sample_vqe.py` for uploading. This limitation will be removed in later versions of Qiskit Runtime. Alternatively, if the entire code is contained within a single Jupyter notebook cell, this can be done using the magic function\n", + "\n", + "```\n", + "%%writefile my_program.py\n", + "```\n", + "\n", + "To actually upload the program we need to get a Provider from our IBM Quantum account:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9166cb5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import IBMQ\n", + "IBMQ.load_account()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c2da132b", + "metadata": {}, + "outputs": [], + "source": [ + "provider = IBMQ.get_provider(group='deployed')" + ] + }, + { + "cell_type": "markdown", + "id": "cf539336", + "metadata": {}, + "source": [ + "### Program upload\n", + "\n", + "The call to `program_upload` takes the target Python file as `data` and the metadata as inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "46ce62a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "'sample-vqe-G3YBjmvlPr'" + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "program_id = provider.runtime.upload_program(data='sample_vqe.py', metadata=meta)\n", + "program_id" + ] + }, + { + "cell_type": "markdown", + "id": "0d73dc6e", + "metadata": {}, + "source": [ + "Here the `upload_program()` method returns a `program_id`, which is how you should reference your program." + ] + }, + { + "cell_type": "markdown", + "id": "375651ac", + "metadata": {}, + "source": [ + "### Program information\n", + "\n", + "We can query the program for information and see that our metadata is correctly being attached:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "0e08a4d0", + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "sample-vqe-G3YBjmvlPr:\n Name: sample-vqe\n Description: A sample VQE program.\n Creation date: 2021-11-10T17:10:18.903742Z\n Update date: 2021-11-10T17:10:18.903742Z\n Max execution time: 100000\n Input parameters:\n Properties:\n - ansatz:\n Default: EfficientSU2\n Description: Name of ansatz quantum circuit to use, default='EfficientSU2'\n Type: string\n Required: False\n - ansatz_config:\n Description: Configuration parameters for the ansatz circuit.\n Type: object\n Required: False\n - hamiltonian:\n Description: Hamiltonian whose ground state we want to find.\n Type: array\n Required: True\n - optimizer:\n Default: SPSA\n Description: Classical optimizer to use, default='SPSA'.\n Type: string\n Required: False\n - optimizer_config:\n Description: Configuration parameters for the optimizer.\n Type: object\n Required: False\n - shots:\n Description: The number of shots used for each circuit evaluation.\n Type: integer\n Required: False\n - use_measurement_mitigation:\n Default: False\n Description: Use measurement mitigation, default=False.\n Type: boolean\n Required: False\n - x0:\n Description: Initial vector of parameters. This is a numpy array.\n Type: array\n Required: False\n Interim results:\n Description: Parameter vector at current optimization step. This is a numpy array.\n Type: array\n Returns:\n Description: Final result in SciPy optimizer format\n Type: object\n" + } + ], + "source": [ + "prog = provider.runtime.program(program_id)\n", + "print(prog)" + ] + }, + { + "cell_type": "markdown", + "id": "70a9e564", + "metadata": {}, + "source": [ + "### Deleting a program\n", + "\n", + "If you make a mistake and need to delete and/or re-upload the program, you can run the following, passing the `program_id`:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ec56e36b", + "metadata": {}, + "outputs": [], + "source": [ + "#provider.runtime.delete_program(program_id)" + ] + }, + { + "cell_type": "markdown", + "id": "4025eb01", + "metadata": {}, + "source": [ + "## Running the program\n", + "\n", + "### Specify parameters\n", + "\n", + "To run the program we need to specify the `options` that are used in the runtime environment (not the program variables). At present, only the `backend_name` is required." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2efca1b3", + "metadata": {}, + "outputs": [], + "source": [ + "backend = provider.backend.ibmq_qasm_simulator\n", + "options = {'backend_name': backend.name()}" + ] + }, + { + "cell_type": "markdown", + "id": "c8b0b300", + "metadata": {}, + "source": [ + "The `inputs` dictionary is used to pass arguments to the `main` function itself. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "381794fa", + "metadata": {}, + "outputs": [], + "source": [ + "inputs = {}\n", + "inputs['hamiltonian'] = H\n", + "inputs['optimizer_config']={'maxiter': 10}" + ] + }, + { + "cell_type": "markdown", + "id": "8443e1ae", + "metadata": {}, + "source": [ + "### Execute the program\n", + "\n", + "We now can execute the program and grab the result." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "3b419193", + "metadata": {}, + "outputs": [], + "source": [ + "job = provider.runtime.run(program_id, options=options, inputs=inputs)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "0addcac0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'fun': -1.93994140625,\n", + " 'x': array([-1.28984461, 3.73974929, 3.52327612, 1.74979783, 3.13519544,\n", + " 2.43577395, 1.30425595, 0.04847941, 6.17766827, 1.92879213,\n", + " 1.95707213, 2.8097762 , 1.95108352, 1.20067124, 7.01868106,\n", + " 4.36507161]),\n", + " 'nit': 10,\n", + " 'nfev': 20,\n", + " 'message': 'Optimization terminated successfully.',\n", + " 'success': True}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.result()" + ] + }, + { + "cell_type": "markdown", + "id": "9e6e0ec0", + "metadata": {}, + "source": [ + "A few things need to be pointed out. First, we did not get back any interim results, and second, the return object is a plain dictionary. This is because we did not listen for the return results, and we did not tell the job how to format the return result." + ] + }, + { + "cell_type": "markdown", + "id": "917857da", + "metadata": {}, + "source": [ + "### Listening for interim results\n", + "\n", + "To listen for interm results we need to pass a callback function to `provider.runtime.run` that stores the results. The callback takes two arguments `job_id` and the returned data:" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "358bdc84", + "metadata": {}, + "outputs": [], + "source": [ + "interm_results = []\n", + "def vqe_callback(job_id, data):\n", + " interm_results.append(data)" + ] + }, + { + "cell_type": "markdown", + "id": "9ed2dabd", + "metadata": {}, + "source": [ + "Executing again we get:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "34b61c28", + "metadata": {}, + "outputs": [], + "source": [ + "job2 = provider.runtime.run(program_id, options=options, inputs=inputs, callback=vqe_callback)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "8d331f69", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'fun': -2.635986328125,\n", + " 'x': array([ 1.39625003, 3.10967996, 2.46291361, -0.09150619, 1.89013366,\n", + " 0.48872864, 5.60656903, 1.12770301, 4.04603538, 2.85551118,\n", + " 0.45677689, 3.46054286, 4.10740117, 4.163728 , 1.53949656,\n", + " 3.46634995]),\n", + " 'nit': 10,\n", + " 'nfev': 20,\n", + " 'message': 'Optimization terminated successfully.',\n", + " 'success': True}" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job2.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "e83bf7ba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[1.1839280526666394, 2.391820224610454, 2.7491281736833244, 0.5771768054969294, 2.349087960882593, 0.20251406828095217, 5.3527505036344865, 1.80726551800796, 2.8686317344166947, 2.4545878612072003, -0.04047464122825306, 4.2780676963333795, 3.27599724292225, 3.5527489679560844, 2.1472927005219273, 3.1637626657075555], [1.1855194978035488, 2.3902287794735444, 2.750719618820234, 0.5755853603600198, 2.3506794060195024, 0.20092262314404263, 5.351159058497577, 1.8088569631448694, 2.870223179553604, 2.452996416070291, -0.04206608636516258, 4.27647625119647, 3.2775886880591596, 3.554340413092994, 2.148884145658837, 3.165354110844465], [1.0411904999135912, 2.534557777363502, 2.8950486167101914, 0.7199143582499773, 2.206350408129545, 0.05659362525408518, 5.206830060607619, 1.664527965254912, 3.0145521774435617, 2.5973254139602484, 0.10226291152479487, 4.420805249086427, 3.133259690169202, 3.6986694109829514, 2.004555147768879, 3.0210251129545074], [1.005580093753927, 2.5701681835231662, 2.9306590228698557, 0.7555247644096416, 2.241960814289209, 0.020983219094420913, 5.242440466767284, 1.7001383714145764, 3.050162583603226, 2.561715007800584, 0.13787331768445915, 4.456415655246091, 3.0976492840095378, 3.663059004823287, 2.0401655539285435, 3.0566355191141716], [1.07047876838977, 2.6350668581590093, 2.8657603482340126, 0.8204234390454845, 2.177062139653366, 0.08588189373026392, 5.307339141403126, 1.6352396967787333, 2.985263908967383, 2.496816333164741, 0.20277199232030216, 4.521314329881934, 3.162547958645381, 3.7279576794591303, 1.9752668792927004, 2.9917368444783285], [1.3994411335364108, 2.96402922330565, 3.1947227133806533, 0.4914610738988439, 2.5060245048000067, -0.2430804714163767, 5.636301506549767, 1.3062773316320926, 3.3142262741140236, 2.8257786983113817, -0.12619037282633846, 4.192351964735293, 3.4915103237920215, 3.3989953143124896, 2.304229244439341, 3.3206992096249692], [1.325020213130704, 3.0384501437113567, 3.1203017929749466, 0.5658819943045507, 2.5804454252057134, -0.16865955101066996, 5.710722426955474, 1.231856411226386, 3.3886471945197303, 2.751357777905675, -0.2006112932320452, 4.117931044329586, 3.417089403386315, 3.4734162347181963, 2.2298083240336344, 3.395120130030676], [1.031941029864989, 2.7453709604456416, 2.8272226097092314, 0.2728028110388356, 2.2873662419399983, 0.12441963225504513, 6.003801610221189, 1.524935594492101, 3.6817263777854454, 2.45827859463996, 0.09246789003366987, 3.8248518610638707, 3.71016858665203, 3.7664954179839114, 1.9367291407679192, 3.102040946764961], [1.4127118235825624, 3.126141754163215, 2.446451815991658, -0.10796798267873797, 1.9065954482224248, 0.5051904259726187, 5.623030816503616, 1.1441648007745275, 4.062497171503019, 2.8390493883575334, 0.47323868375124345, 3.444081067346297, 4.090939380369604, 4.147266211701485, 1.5559583470503457, 3.4828117404825343], [1.3962500340466297, 3.1096799646272824, 2.4629136055275906, -0.09150619314280523, 1.890133658686492, 0.4887286364366859, 5.606569026967683, 1.1277030112385948, 4.046035381967086, 2.855511177893466, 0.4567768942153107, 3.46054285688223, 4.107401169905537, 4.163728001237418, 1.539496557514413, 3.4663499509466016]]\n" + ] + } + ], + "source": [ + "print(interm_results)" + ] + }, + { + "cell_type": "markdown", + "id": "43f69e8a", + "metadata": {}, + "source": [ + "### Formatting the returned results\n", + "\n", + "In order to format the return results into the desired format, we need to specify a decoder. This decoder must have a `decode` method that gets called to do the actual conversion. In our case `OptimizeResult` is a simple sub-class of `dict` so the formatting is simple." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "ccdc7af3", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.providers.ibmq.runtime import ResultDecoder\n", + "from scipy.optimize import OptimizeResult\n", + "\n", + "class VQEResultDecoder(ResultDecoder):\n", + " @classmethod\n", + " def decode(cls, data):\n", + " data = super().decode(data) # This is required to preformat the data returned.\n", + " return OptimizeResult(data)" + ] + }, + { + "cell_type": "markdown", + "id": "5e0f0aae", + "metadata": {}, + "source": [ + "We can then use this when returning the job result:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "951eed95", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " fun: -0.645751953125\n", + " message: 'Optimization terminated successfully.'\n", + " nfev: 20\n", + " nit: 10\n", + " success: True\n", + " x: array([ 5.72140052, 2.29687026, 4.13837683, 3.22216958, 4.76184762,\n", + " 1.20943004, 5.74244574, 2.22665936, 4.34308411, 3.8390838 ,\n", + " -0.50949471, 2.15587397, 3.19045035, 5.82751179, 1.95972168,\n", + " 3.75821819])" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job3 = provider.runtime.run(program_id, options=options, inputs=inputs)\n", + "job3.result(decoder=VQEResultDecoder)" + ] + }, + { + "cell_type": "markdown", + "id": "5ca7d039", + "metadata": {}, + "source": [ + "## Simplifying program execution with wrapping functions\n", + "\n", + "While runtime programs are powerful and flexible, they are not the most friendly things to interact with. Therefore, if your program is intended to be used by others, it is best to make wrapper functions and/or classes that simplify the user experience. Moreover, such wrappers allow for validation of user inputs on the client side, which can quickly find errors that would otherwise be raised later during the execution process - something that might have taken hours waiting in queue to get to.\n", + "\n", + "Here we will make two helper routines. First, a job wrapper that allows us to attach and retrieve the interim results directly from the job object itself, as well as decodes for us so that the end user need not worry about formatting the results themselves." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "c3460583", + "metadata": {}, + "outputs": [], + "source": [ + "class RuntimeJobWrapper():\n", + " \"\"\"A simple Job wrapper that attaches interm results directly to the job object itself\n", + " in the `interm_results attribute` via the `_callback` function.\n", + " \"\"\"\n", + " def __init__(self):\n", + " self._job = None\n", + " self._decoder = VQEResultDecoder\n", + " self.interm_results = []\n", + " \n", + " def _callback(self, job_id, xk):\n", + " \"\"\"The callback function that attaches interm results:\n", + " \n", + " Parameters:\n", + " job_id (str): The job ID.\n", + " xk (array_like): A list or NumPy array to attach.\n", + " \"\"\"\n", + " self.interm_results.append(xk)\n", + " \n", + " def __getattr__(self, attr):\n", + " if attr == 'result':\n", + " return self.result\n", + " else:\n", + " if attr in dir(self._job):\n", + " return getattr(self._job, attr)\n", + " raise AttributeError(\"Class does not have {}.\".format(attr))\n", + " \n", + " def result(self):\n", + " \"\"\"Get the result of the job as a SciPy OptimizerResult object.\n", + " \n", + " This blocks until job is done, cancelled, or errors.\n", + " \n", + " Returns:\n", + " OptimizerResult: A SciPy optimizer result object.\n", + " \"\"\"\n", + " return self._job.result(decoder=self._decoder)" + ] + }, + { + "cell_type": "markdown", + "id": "0f336b6d", + "metadata": {}, + "source": [ + "Next, we create the actual function we want users to call to execute our program. To this function we will add a series of simple validation checks (not all checks will be done for simplicity), as well as use the job wrapper defined above to simply the output." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "aa386027", + "metadata": {}, + "outputs": [], + "source": [ + "import qiskit.circuit.library.n_local as lib_local\n", + "\n", + "def vqe_runner(backend, hamiltonian,\n", + " ansatz='EfficientSU2', ansatz_config={},\n", + " x0=None, optimizer='SPSA',\n", + " optimizer_config={'maxiter': 100},\n", + " shots = 8192,\n", + " use_measurement_mitigation=False):\n", + " \n", + " \"\"\"Routine that executes a given VQE problem via the sample-vqe program on the target backend.\n", + " \n", + " Parameters:\n", + " backend (ProgramBackend): Qiskit backend instance.\n", + " hamiltonian (list): Hamiltonian whose ground state we want to find.\n", + " ansatz (str): Optional, name of ansatz quantum circuit to use, default='EfficientSU2'\n", + " ansatz_config (dict): Optional, configuration parameters for the ansatz circuit.\n", + " x0 (array_like): Optional, initial vector of parameters.\n", + " optimizer (str): Optional, string specifying classical optimizer, default='SPSA'.\n", + " optimizer_config (dict): Optional, configuration parameters for the optimizer.\n", + " shots (int): Optional, number of shots to take per circuit.\n", + " use_measurement_mitigation (bool): Optional, use measurement mitigation, default=False.\n", + " \n", + " Returns:\n", + " OptimizeResult: The result in SciPy optimization format. \n", + " \"\"\"\n", + " options = {'backend_name': backend.name()}\n", + " \n", + " inputs = {}\n", + " \n", + " # Validate Hamiltonian is correct\n", + " num_qubits = len(H[0][1])\n", + " for idx, ham in enumerate(hamiltonian):\n", + " if len(ham[1]) != num_qubits:\n", + " raise ValueError('Number of qubits in Hamiltonian term {} does not match {}'.format(idx,\n", + " num_qubits))\n", + " inputs['hamiltonian'] = hamiltonian\n", + " \n", + " # Validate ansatz is in the module\n", + " ansatz_circ = getattr(lib_local, ansatz, None)\n", + " if not ansatz_circ:\n", + " raise ValueError('Ansatz {} not in n_local circuit library.'.format(ansatz))\n", + " \n", + " inputs['ansatz'] = ansatz\n", + " inputs['ansatz_config'] = ansatz_config\n", + " \n", + " # If given x0, validate its length against num_params in ansatz:\n", + " if x0:\n", + " x0 = np.asarray(x0)\n", + " ansatz_circ = ansatz_circ(num_qubits, **ansatz_config)\n", + " num_params = ansatz_circ.num_parameters\n", + " if x0.shape[0] != num_params:\n", + " raise ValueError('Length of x0 {} does not match number of params in ansatz {}'.format(x0.shape[0],\n", + " num_params))\n", + " inputs['x0'] = x0\n", + " \n", + " # Set the rest of the inputs\n", + " inputs['optimizer'] = optimizer\n", + " inputs['optimizer_config'] = optimizer_config\n", + " inputs['shots'] = shots\n", + " inputs['use_measurement_mitigation'] = use_measurement_mitigation\n", + " \n", + " rt_job = RuntimeJobWrapper()\n", + " job = provider.runtime.run(program_id, options=options, inputs=inputs, callback=rt_job._callback)\n", + " rt_job._job = job\n", + " \n", + " return rt_job\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "d4c8982f", + "metadata": {}, + "source": [ + "We can now execute our runtime program via this runner function:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "8e81851d", + "metadata": {}, + "outputs": [], + "source": [ + "job4 = vqe_runner(backend, H, optimizer_config={'maxiter': 15})" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "9bf5499c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + " fun: -1.349853515625\n", + " message: 'Optimization terminated successfully.'\n", + " nfev: 30\n", + " nit: 15\n", + " success: True\n", + " x: array([0.09925502, 1.40473727, 1.61291267, 3.45519813, 2.65167136,\n", + " 4.4163485 , 1.98523376, 5.94459488, 6.46103911, 1.76878845,\n", + " 1.96124064, 3.31830748, 2.06192779, 4.28293342, 3.2448137 ,\n", + " 1.63457609])" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job4.result()" + ] + }, + { + "cell_type": "markdown", + "id": "ef281bc5", + "metadata": {}, + "source": [ + "The interim results are now attached to the job `interm_results` attribute and, as expected, we see that the length matches the number of iterations performed." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "958dbc84", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "15" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(job4.interm_results)" + ] + }, + { + "cell_type": "markdown", + "id": "dd78b903", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "We have demonstrated how to create, upload, and use a custom Qiskit Runtime by creating our own VQE solver from scratch. This tutorial was meant to touch upon every aspect of the process for a real-world example. Within the current limitations of the runtime environment, this example should enable readers to develop their own single-file runtime program. This program is also a good starting point for exploring additional flavors of VQE runtime. For example, it is straightforward to vary the number of shots per iteration, increasing shots as the number of iterations increases. Those looking to go deeper can consider implimenting an [adaptive VQE](https://doi.org/10.1038/s41467-019-10988-2), where the ansatz is not fixed at initialization." + ] + }, + { + "cell_type": "markdown", + "id": "0a7a50d4", + "metadata": {}, + "source": [ + "## Appendix A\n", + "\n", + "Here we code a simple simultaneous perturbation stochastic approximation (SPSA) optimizer for use on noisy quantum systems. Most optimizers do not handle fluctuating cost functions well, so this is a needed addition for executing on real quantum hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9d8788af", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.optimize import OptimizeResult\n", + "\n", + "def fmin_spsa(func, x0, args=(), maxiter=100,\n", + " a=1.0, alpha=0.602, c=1.0, gamma=0.101,\n", + " callback=None):\n", + " \"\"\"\n", + " Minimization of scalar function of one or more variables using simultaneous\n", + " perturbation stochastic approximation (SPSA).\n", + "\n", + " Parameters:\n", + " func (callable): The objective function to be minimized.\n", + "\n", + " ``fun(x, *args) -> float``\n", + "\n", + " where x is an 1-D array with shape (n,) and args is a\n", + " tuple of the fixed parameters needed to completely \n", + " specify the function.\n", + "\n", + " x0 (ndarray): Initial guess. Array of real elements of size (n,), \n", + " where ‘n’ is the number of independent variables.\n", + " \n", + " maxiter (int): Maximum number of iterations. The number of function\n", + " evaluations is twice as many. Optional.\n", + "\n", + " a (float): SPSA gradient scaling parameter. Optional.\n", + "\n", + " alpha (float): SPSA gradient scaling exponent. Optional.\n", + "\n", + " c (float): SPSA step size scaling parameter. Optional.\n", + " \n", + " gamma (float): SPSA step size scaling exponent. Optional.\n", + "\n", + " callback (callable): Function that accepts the current parameter vector\n", + " as input.\n", + "\n", + " Returns:\n", + " OptimizeResult: Solution in SciPy Optimization format.\n", + "\n", + " Notes:\n", + " See the `SPSA homepage `_ for usage and\n", + " additional extentions to the basic version implimented here.\n", + " \"\"\"\n", + " A = 0.01 * maxiter\n", + " x0 = np.asarray(x0)\n", + " x = x0\n", + "\n", + " for kk in range(maxiter):\n", + " ak = a*(kk+1.0+A)**-alpha\n", + " ck = c*(kk+1.0)**-gamma\n", + " # Bernoulli distribution for randoms\n", + " deltak = 2*np.random.randint(2, size=x.shape[0])-1\n", + " grad = (func(x + ck*deltak, *args) - func(x - ck*deltak, *args))/(2*ck*deltak)\n", + " x -= ak*grad\n", + " \n", + " if callback is not None:\n", + " callback(x)\n", + "\n", + " return OptimizeResult(fun=func(x, *args), x=x, nit=maxiter, nfev=2*maxiter, \n", + " message='Optimization terminated successfully.',\n", + " success=True)" + ] + }, + { + "cell_type": "markdown", + "id": "40b4d8c2", + "metadata": {}, + "source": [ + "## Appendix B\n", + "\n", + "This is a helper function that converts the Pauli operators in the strings that define the Hamiltonian operators into the appropriate measurements at the end of the circuits. For $X$ operators this involves adding an $H$ gate to the qubits to be measured, whereas a $Y$ operator needs $S^{+}$ followed by a $H$. Other choices of Pauli operators require no additional gates prior to measurement." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "15654ed8", + "metadata": {}, + "outputs": [], + "source": [ + "def opstr_to_meas_circ(op_str):\n", + " \"\"\"Takes a list of operator strings and makes circuit with the correct post-rotations for measurements.\n", + "\n", + " Parameters:\n", + " op_str (list): List of strings representing the operators needed for measurements.\n", + "\n", + " Returns:\n", + " list: List of circuits for measurement post-rotations\n", + " \"\"\"\n", + " num_qubits = len(op_str[0])\n", + " circs = []\n", + " for op in op_str:\n", + " qc = QuantumCircuit(num_qubits)\n", + " for idx, item in enumerate(op):\n", + " if item == 'X':\n", + " qc.h(num_qubits-idx-1)\n", + " elif item == 'Y':\n", + " qc.sdg(num_qubits-idx-1)\n", + " qc.h(num_qubits-idx-1)\n", + " circs.append(qc)\n", + " return circs" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "715a3fec", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

This code is a part of Qiskit

© Copyright IBM 2017, 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.

" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.tools.jupyter import *\n", + "%qiskit_copyright" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8141d8a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/sample_vqe_program/sample_vqe.py b/tutorials/sample_vqe_program/sample_vqe.py new file mode 100644 index 0000000000..684eba51f8 --- /dev/null +++ b/tutorials/sample_vqe_program/sample_vqe.py @@ -0,0 +1,251 @@ +# This code is part of qiskit-runtime. +# +# (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. + +# Grab functions and modules from dependencies +import numpy as np +import scipy.optimize as opt +from scipy.optimize import OptimizeResult +import mthree + +# Grab functions and modules from Qiskit needed +from qiskit import QuantumCircuit, transpile +import qiskit.circuit.library.n_local as lib_local + + +# The entrypoint for our Runtime Program +def main(backend, user_messenger, + hamiltonian, + ansatz='EfficientSU2', + ansatz_config={}, + x0=None, + optimizer='SPSA', + optimizer_config={'maxiter': 100}, + shots = 8192, + use_measurement_mitigation=False + ): + + """ + The main sample VQE program. + + Parameters: + backend (ProgramBackend): Qiskit backend instance. + user_messenger (UserMessenger): Used to communicate with the program user. + hamiltonian (list): Hamiltonian whose ground state we want to find. + ansatz (str): Optional, name of ansatz quantum circuit to use, default='EfficientSU2' + ansatz_config (dict): Optional, configuration parameters for the ansatz circuit. + x0 (array_like): Optional, initial vector of parameters. + optimizer (str): Optional, string specifying classical optimizer, default='SPSA'. + optimizer_config (dict): Optional, configuration parameters for the optimizer. + shots (int): Optional, number of shots to take per circuit. + use_measurement_mitigation (bool): Optional, use measurement mitigation, default=False. + + Returns: + OptimizeResult: The result in SciPy optimization format. + """ + + # Split the Hamiltonian into two arrays, one for coefficients, the other for + # operator strings + coeffs = np.array([item[0] for item in hamiltonian], dtype=complex) + op_strings = [item[1] for item in hamiltonian] + # The number of qubits needed is given by the number of elements in the strings + # the defiune the Hamiltonian. Here we grab this data from the first element. + num_qubits = len(op_strings[0]) + + # We grab the requested ansatz circuit class from the Qiskit circuit library + # n_local module and configure it using the number of qubits and options + # passed in the ansatz_config. + ansatz_instance = getattr(lib_local, ansatz) + ansatz_circuit = ansatz_instance(num_qubits, **ansatz_config) + + # Here we use our convenence function from Appendix B to get measurement circuits + # with the correct single-qubit rotation gates. + meas_circs = opstr_to_meas_circ(op_strings) + + # When computing the expectation value for the energy, we need to know if we + # evaluate a Z measurement or and identity measurement. Here we take and X and Y + # operator in the strings and convert it to a Z since we added the rotations + # with the meas_circs. + meas_strings = [string.replace('X', 'Z').replace('Y', 'Z') for string in op_strings] + + # Take the ansatz circuits, add the single-qubit measurement basis rotations from + # meas_circs, and finally append the measurements themselves. + full_circs = [ansatz_circuit.compose(mcirc).measure_all(inplace=False) for mcirc in meas_circs] + + # Get the number of parameters in the ansatz circuit. + num_params = ansatz_circuit.num_parameters + + # Use a given initial state, if any, or do random initial state. + if x0: + x0 = np.asarray(x0, dtype=float) + if x0.shape[0] != num_params: + raise ValueError('Number of params in x0 ({}) does not match number of ansatz parameters ({})'. format( + x0.shape[0], num_params)) + else: + x0 = 2*np.pi*np.random.rand(num_params) + + # Because we are in general targeting a real quantum system, our circuits must be transpiled + # to match the system topology and, hopefully, optimize them. + # Here we will set the transpiler to the most optimal settings where 'sabre' layout and + # routing are used, along with full O3 optimization. + + # This works around a bug in Qiskit where Sabre routing fails for simulators (Issue #7098) + trans_dict = {} + if not backend.configuration().simulator: + trans_dict = {'layout_method': 'sabre', 'routing_method': 'sabre'} + trans_circs = transpile(full_circs, backend, optimization_level=3, **trans_dict) + + # If using measurement mitigation we need to find out which physical qubits our transpiled + # circuits actually measure, construct a mitigation object targeting our backend, and + # finally calibrate our mitgation by running calibration circuits on the backend. + if use_measurement_mitigation: + maps = mthree.utils.final_measurement_mapping(trans_circs) + mit = mthree.M3Mitigation(backend) + mit.cals_from_system(maps) + + # Here we define a callback function that will stream the optimizer parameter vector + # back to the user after each iteration. This uses the `user_messenger` object. + # Here we convert to a list so that the return is user readable locally, but + # this is not required. + def callback(xk): + user_messenger.publish(list(xk)) + + # This is the primary VQE function executed by the optimizer. This function takes the + # parameter vector as input and returns the energy evaluated using an ansatz circuit + # bound with those parameters. + def vqe_func(params): + # Attach (bind) parameters in params vector to the transpiled circuits. + bound_circs = [circ.bind_parameters(params) for circ in trans_circs] + + # Submit the job and get the resultant counts back + counts = backend.run(bound_circs, shots=shots).result().get_counts() + + # If using measurement mitigation apply the correction and + # compute expectation values from the resultant quasiprobabilities + # using the measurement strings. + if use_measurement_mitigation: + quasi_collection = mit.apply_correction(counts, maps) + expvals = quasi_collection.expval(meas_strings) + # If not doing any mitigation just compute expectation values + # from the raw counts using the measurement strings. + # Since Qiskit does not have such functionality we use the convenence + # function from the mthree mitigation module. + else: + expvals = mthree.utils.expval(counts, meas_strings) + + # The energy is computed by simply taking the product of the coefficients + # and the computed expectation values and summing them. Here we also + # take just the real part as the coefficients can possibly be complex, + # but the energy (eigenvalue) of a Hamiltonian is always real. + energy = np.sum(coeffs*expvals).real + return energy + + # Here is where we actually perform the computation. We begin by seeing what + # optimization routine the user has requested, eg. SPSA verses SciPy ones, + # and dispatch to the correct optimizer. The selected optimizer starts at + # x0 and calls 'vqe_func' everytime the optimizer needs to evaluate the cost + # function. The result is returned as a SciPy OptimizerResult object. + # Additionally, after every iteration, we use the 'callback' function to + # publish the interm results back to the user. This is important to do + # so that if the Program terminates unexpectedly, the user can start where they + # left off. + + # Since SPSA is not in SciPy need if statement + if optimizer == 'SPSA': + res = fmin_spsa(vqe_func, x0, args=(), **optimizer_config, callback=callback) + # All other SciPy optimizers here + else: + res = opt.minimize(vqe_func, x0, method=optimizer, options=optimizer_config, callback=callback) + # Return result. OptimizeResult is a subclass of dict. + return res + + +def opstr_to_meas_circ(op_str): + """Takes a list of operator strings and makes circuit with the correct post-rotations for measurements. + + Parameters: + op_str (list): List of strings representing the operators needed for measurements. + + Returns: + list: List of circuits for measurement post-rotations + """ + num_qubits = len(op_str[0]) + circs = [] + for op in op_str: + qc = QuantumCircuit(num_qubits) + for idx, item in enumerate(op): + if item == 'X': + qc.h(num_qubits-idx-1) + elif item == 'Y': + qc.sdg(num_qubits-idx-1) + qc.h(num_qubits-idx-1) + circs.append(qc) + return circs + + +def fmin_spsa(func, x0, args=(), maxiter=100, + a=1.0, alpha=0.602, c=1.0, gamma=0.101, + callback=None): + """ + Minimization of scalar function of one or more variables using simultaneous + perturbation stochastic approximation (SPSA). + + Parameters: + func (callable): The objective function to be minimized. + + ``fun(x, *args) -> float`` + + where x is an 1-D array with shape (n,) and args is a + tuple of the fixed parameters needed to completely + specify the function. + + x0 (ndarray): Initial guess. Array of real elements of size (n,), + where ‘n’ is the number of independent variables. + + maxiter (int): Maximum number of iterations. The number of function + evaluations is twice as many. Optional. + + a (float): SPSA gradient scaling parameter. Optional. + + alpha (float): SPSA gradient scaling exponent. Optional. + + c (float): SPSA step size scaling parameter. Optional. + + gamma (float): SPSA step size scaling exponent. Optional. + + callback (callable): Function that accepts the current parameter vector + as input. + + Returns: + OptimizeResult: Solution in SciPy Optimization format. + + Notes: + See the `SPSA homepage `_ for usage and + additional extentions to the basic version implimented here. + """ + A = 0.01 * maxiter + x0 = np.asarray(x0) + x = x0 + + for kk in range(maxiter): + ak = a * (kk+1.0+A)**-alpha + ck = c * (kk+1.0)**-gamma + # Bernoulli distribution for randoms + deltak = 2*np.random.randint(2, size=x.shape[0])-1 + grad = (func(x + ck*deltak, *args) - func(x - ck*deltak, *args))/(2*ck*deltak) + x -= ak*grad + + if callback is not None: + callback(x) + + return OptimizeResult(fun=func(x, *args), x=x, nit=maxiter, nfev=2*maxiter, + message='Optimization terminated successfully.', + success=True) diff --git a/tutorials/vqe.ipynb b/tutorials/vqe.ipynb new file mode 100644 index 0000000000..b95c691beb --- /dev/null +++ b/tutorials/vqe.ipynb @@ -0,0 +1,705 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# VQE\n", + "\n", + "The Variational Quantum Eigensolver (VQE) is a central algorithm in many applications from e.g. quantum chemistry or optimization.\n", + "This tutorial shows you how to run the VQE as a Qiskit Runtime program. We'll start off by defining the algorithm settings, such as the Hamiltonian and ansatz, and then run a VQE both locally, on your machine, and remotely, using the Qiskit Runtime.\n", + "\n", + "**Note:** You can find tutorials on solving more comprehensive problems, such as finding the ground state of the lithium hydride molecule, using the VQE (and Qiskit Runtime) within [the tutorials of Qiskit Nature](https://github.com/Qiskit/qiskit-nature/tree/main/docs/tutorials)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## System Hamiltonian" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's start by defining the operator of which we want to determine the ground state. Here we'll chose a simple diagonal Hamiltonian $\\hat H$ acting with Pauli-Z operators on the first two qubits\n", + "\n", + "$$\n", + "\\hat H = \\hat Z_0 \\otimes \\hat Z_1.\n", + "$$\n", + "\n", + "We can construct this Hamiltonian with Qiskit's `opflow` module:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.opflow import Z, I\n", + "\n", + "num_qubits = 4\n", + "hamiltonian = (Z ^ Z) ^ (I ^ (num_qubits - 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This Hamiltonian has a ground state energy of -1." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "target_energy = -1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parameterized Ansatz Circuit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we choose a parameterized quantum circuit $\\hat U(\\theta)$ to prepare the ansatz wavefunction\n", + "\n", + "$$\n", + "|\\psi(\\theta)\\rangle = \\hat U(\\theta)|0\\rangle.\n", + "$$\n", + "\n", + "We'll use the `EfficientSU2` circuit from Qiskit's circuit library, which is a hardware efficient, heuristic ansatz with alternating rotation and entanglement layers." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library import EfficientSU2\n", + "\n", + "# the rotation gates are chosen randomly, so we set a seed for reproducibility\n", + "ansatz = EfficientSU2(num_qubits, reps=1, entanglement='linear', insert_barriers=True) \n", + "ansatz.draw('mpl', style='iqx')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solve with the VQE" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have the problem and ansatz specified we can use the Variational Quantum Eigensolver (VQE) to solve for the minimal eigenvalue of our Hamiltonian.\n", + "\n", + "The VQE requires a classical optimization routine, along with an initial point, to calculate the parameter updates." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qiskit.algorithms.optimizers import SPSA\n", + "\n", + "optimizer = SPSA(maxiter=50)\n", + "\n", + "np.random.seed(10) # seed for reproducibility\n", + "initial_point = np.random.random(ansatz.num_parameters)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To access intermediate information we can pass a callback into the VQE. The callback is given the current number of function evaluations, the current parameters, function values and standard deviation in the expectation evaluation. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "intermediate_info = {\n", + " 'nfev': [],\n", + " 'parameters': [],\n", + " 'energy': [],\n", + " 'stddev': []\n", + "}\n", + "\n", + "def callback(nfev, parameters, energy, stddev):\n", + " intermediate_info['nfev'].append(nfev)\n", + " intermediate_info['parameters'].append(parameters)\n", + " intermediate_info['energy'].append(energy)\n", + " intermediate_info['stddev'].append(stddev)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Local VQE\n", + "\n", + "Before running the Qiskit Runtime VQE program, let's first simulate this system locally using Qiskit's `VQE` class." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.providers.basicaer import QasmSimulatorPy # local simulator\n", + "from qiskit.algorithms import VQE\n", + "\n", + "local_vqe = VQE(ansatz=ansatz,\n", + " optimizer=optimizer,\n", + " initial_point=initial_point,\n", + " quantum_instance=QasmSimulatorPy(),\n", + " callback=callback)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jul/opt/miniconda3/envs/stable/lib/python3.7/site-packages/qiskit/utils/run_circuits.py:564: UserWarning: Option max_credits is not used by this backend\n", + " return backend.run(circuits, **backend_options, **noise_config, **run_config)\n" + ] + } + ], + "source": [ + "local_result = local_vqe.compute_minimum_eigenvalue(hamiltonian)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Eigenvalue: -1.0\n", + "Target: -1\n" + ] + } + ], + "source": [ + "print('Eigenvalue:', local_result.eigenvalue)\n", + "print('Target:', target_energy)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With the information from the callback, we can for instance compute the average of the estimation errors in the expectation evaluations. For an exact (statevector) simulation this would be 0, but for a shot-based readout as we have on real hardware, this will be a small finite error." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean error: 0.013757962876791466\n" + ] + } + ], + "source": [ + "print('Mean error:', np.mean(intermediate_info['stddev']))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Runtime VQE: ``VQEProgram``\n", + "\n", + "To run the VQE using Qiskit Runtime, we only have to do very few changes from the local VQE run and mainly have to replace the `VQE` class by the `VQEProgram` class. Both follow the same `MinimumEigensolver` interface and thus share the `compute_minimum_eigenvalue` method to execute the algorithm and return the same type of result object. Merely the signature of the initializer differs sligthly.\n", + "\n", + "We start by choosing the provider with access to the Qiskit Runtime service and the backend to execute the circuits on. \n", + "\n", + "**Note:** To run this tutorial, replace the provider by your provider." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import IBMQ\n", + "\n", + "IBMQ.load_account()\n", + "provider = IBMQ.get_provider(project='qiskit-runtime') # replace by your designated provider" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're using the IBM Montreal device, but you can replace this with another device that support the Qiskit Runtime." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "backend = provider.get_backend('ibmq_montreal')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's not forget to reset the callback!" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "intermediate_info = {\n", + " 'nfev': [],\n", + " 'parameters': [],\n", + " 'energy': [],\n", + " 'stddev': []\n", + "}\n", + "\n", + "def callback(nfev, parameters, energy, stddev):\n", + " intermediate_info['nfev'].append(nfev)\n", + " intermediate_info['parameters'].append(parameters)\n", + " intermediate_info['energy'].append(energy)\n", + " intermediate_info['stddev'].append(stddev)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the Qiskit Runtime VQE runs on real hardware, we might want to do measurement error mitigation. For this purpose the `VQEProgram` supports a boolean flag `measurement_error_mitigation` that can be set to `True` to enable error mitigation with a complete measurement fitter." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "measurement_error_mitigation = True" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_nature.runtime import VQEProgram\n", + "\n", + "# in this first release, the optimizer must be specified as dictionary\n", + "optimizer = {'name': 'SPSA',\n", + " 'maxiter': 50}\n", + "\n", + "runtime_vqe = VQEProgram(ansatz=ansatz,\n", + " optimizer=optimizer,\n", + " initial_point=initial_point,\n", + " provider=provider,\n", + " backend=backend,\n", + " shots=1024,\n", + " measurement_error_mitigation=measurement_error_mitigation,\n", + " callback=callback)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "runtime_result = runtime_vqe.compute_minimum_eigenvalue(hamiltonian)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Eigenvalue: -0.9756263578485963\n", + "Target: -1\n" + ] + } + ], + "source": [ + "print('Eigenvalue:', runtime_result.eigenvalue)\n", + "print('Target:', target_energy)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean error: (0.012858397132816049+0j)\n" + ] + } + ], + "source": [ + "print('Mean error:', np.mean(intermediate_info['stddev']))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Additionally to final results, the `VQEProgram` also returns the history of the optimization, such that we can visualize the " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "history = runtime_result.optimizer_history\n", + "\n", + "loss = history['loss']\n", + "timestamps = history['time']\n", + "\n", + "runtimes = np.concatenate(([0], np.diff(timestamps)))\n", + "runtimes_in_min = runtimes / 60" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From this, we can extract the times per iteration and statistics on it:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total time taken: 23.09min\n", + "\n", + "Median time of the iterations: 27.12s\n", + "Average time per iteration: 27.71s\n", + "Standard deviation: 5.21s\n" + ] + } + ], + "source": [ + "print(f'Total time taken: {np.sum(runtimes_in_min):.2f}min\\n')\n", + "\n", + "# note that the median and average might differ, since the device get's calibrated every hour\n", + "# resulting in some iteration times that are much larger than the rest\n", + "print(f'Median time of the iterations: {np.median(runtimes):.2f}s')\n", + "print(f'Average time per iteration: {np.mean(runtimes):.2f}s')\n", + "print(f'Standard deviation: {np.std(runtimes):.2f}s')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or plot the loss against the total time taken:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "image/png": "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u/9cxN9n5TKqf3ygwxEREelQu3fvZseOHQwYMIDo6GjMLNAhibQr5xxVVVVs2bIFoM0JvIZzg9ygxBicg+ufWURZpUYgRESkeyssLGTAgAHExMQocZduycyIiYlhwIABFBYWtnl/Je9BLrVXJA9eMpptZVXc8u8leL0u0CGJiIh0mLq6OqKjowMdhkiHi46OPqTSMCXvXcCYjER+duZIPlhZyF9mrw50OCIiIh1KI+7SExzqz7mS9y7i0gkZnH3sAP74XgFzCooCHY6IiIiIBICS9y7CzPjN2UeS06cX339+MZtKKgMdkoiIiByCyy67jDPPPLNVfZ988kni4uL2u339+vWYGQsWLGiv8DpNW/4c5GtK3ruQ6IhQHv7OGDxex/X/XEh1nSfQIYmIiIjfli1buOaaaxg4cCAREREMGDCAq6++ms2bN+/V74EHHuCZZ55pl3MOGjSIbdu2ccwxxwCQl5eHmVFcXHzA/RqS/oZHfHw8EyZM4LXXXmuXuJraX0zt+efQWrW1taSkpPB///d/LW7/61//SkxMDGVlZQB4vV7+/Oc/c8wxxxAdHU3v3r05/vjjeeutt/bar+EztvRYuXJlu34GJe9dTGZKLPedfwzLtuzmF//9KtDhiIiICLBu3Tpyc3NZtmwZTz31FKtXr+aZZ57hq6++YuzYsaxfv76xb3x8PAkJCe1y3tDQUPr27UtY2KHN/v3222+zbds25s+fz7hx4zj33HNZtmxZu8R2MO3559BaERERXHrppTz55JM4t+8kII899hjf/va3iY/3Tc998cUX87Of/Yxrr72Wr776ik8++YSxY8dy5pln8te//nWf/b/66iu2bdu21yMrK6t9P4RzTo9WPsaMGeOCxT1vr3QZd7zunpu/IdChiIiItJvly5cHOoRDctppp7n+/fu7ioqKvdorKipc//793emnn97YNmvWLHfGGWc0vp8zZ44bP368i42Ndb1793Zjx451X375pXPOuSeeeMLFxsY29i0pKXETJ050J598sisvL3fr1q1zgPv8888bXzd9zJo1q8V4m+7XYPfu3Q5wf/rTn/bbxznnAPef//xnrz4vvPCCO/HEE110dLQbMWKEe+edd/ba3lJMzf8cpk2b5q677jr3gx/8wCUmJrqUlBR3//33u+rqanfDDTe4+Ph4N2jQIPf000/vFc/mzZvdBRdc4BISElxCQoI7/fTTXUFBwX6v1bJlyxzgPvjgg73alyxZ4gA3Z84c55xz//rXvxzgXn755X2O8f3vf99FRES4TZs2Oeecmz17tgNcUVHRfs/bkgP9vAMLXAv5aNCPvJvZDWa2zsyqzWyhmU05QN9zzOwdMysysz1mNt/MvtGsz2Vm5lp4RHX8p2k/t5yUzZSsFH7+6ld8sXlXoMMRERHpsUpKSnj77bf57ne/S0xMzF7bYmJiuOGGG3jrrbcoLS3dZ9/6+nq++c1vMnnyZJYuXcr8+fO5+eabCQ0N3afv1q1bmTp1KgMHDuS1114jNjZ2r+2DBg3ixRdfBL4eAX7ggQda9Rnq6up49NFHAd/qn2115513ctNNN7F06VLGjh3LhRdeSHl5eZtj+uc//0mvXr2YP38+P/rRj7j55pv51re+RXZ2NgsWLGDWrFlcddVVbNu2DYDKykpmzJhBVFQUc+bM4ZNPPqFfv36ceOKJVFa2fH/gqFGjGD9+PI8//vhe7Y899hhZWVlMnTq1MZasrCy+9a1v7XOM2267jdra2sbP1pmCeoVVM7sAeAC4AfjQ//yWmY10zm1sYZdpwAfAT4ES4BLgZTOb7pyb16RfJTC06Y7OueoO+AgdJjTEeODCYznrzx9y/TOLeO17k0mKjQh0WCIiIu3ul699xfKtuzv1nCP79+YXZ41qVd9Vq1bhnGPEiBEtH2vkSJxzrFq1inHjxu21bffu3ezatYuzzjqLoUN9qcnw4cP3Ocbq1as56aSTOOWUU3jooYcICdl3/DU0NJSkpCQA0tLSSElJOWjsU6dOJSQkhKqqKrxeL4MHD+b8888/6H7N3XLLLZx11lkA/OY3v+Hpp59myZIlTJ48uU0xjRo1irvuuguAH/zgB/zud78jPDyc73//+wD8/Oc/5/e//z0fffQR3/72t3n++edxzvHEE080Tr34t7/9jbS0NF5//fX9fparrrqKm266ib/85S/Ex8dTU1PDP//5T26//fbGPgUFBfu9pgMGDKB3797k5+fv1Z6ZmbnX+4SEhH3ueThcwT7y/gPgSefco865Fc657wHbgOtb6uyc+75z7nfOuc+cc6udc78EFgLf2rer29700aGfooMkxUbw0CWjKdpTw/efX4xHCziJiIgErYiIfQfZkpKSuOyyyzjllFM444wzuO+++9i4ce/xydraWiZPnszpp5/Oww8/3GLifqieffZZFi9ezKuvvkpWVhaPP/54Y7LdFkcddVTj6/79+wMc0uqhTY9jZqSlpXHkkUc2toWHh5OYmNh47IULF7Ju3Tp69epFXFwccXFxxMfHU1paypo1a/Z7ngsvvJDQ0FCee+45AF555RV2797NrFmz2hRv82s6e/ZslixZ0viYN2/efvY8dEE78m5mEcAY4N5mm94BJrbhUL2A5r+nijazDUAosAT4mXNu8SGGGlBHD0rgl98cxY9f+pL73yvghyfnBDokERGRdtXaEfBAycrKwsxYvnw5Z5999j7bly9fTlhYGIMHD25x/yeeeIKbb76Zt99+m1dffZU777yTV155hVNOOQXwJawnn3wyb775Jhs2bCAjI6PdYh84cCBZWVlkZWURFxfHeeedx/Lly0lJSWn8kuCa3Ni5vxVBm5baNIyAe73eNsfTvGTHzFpsazi21+vlmGOO4fnnn9/nWAf6EhIXF8f555/P448/znXXXcdjjz3GGWecQd++fRv7ZGdns2LFihb337JlC7t37yY7O3uv9sGDB7fqNx6HI5hH3lPwJdc7mrXvAPru231fZvZdYCDwjybN+cAVwDeBi4Bq4CMza/FWYDO7xswWmNmCoqLgXBzpwrGDOD93IH/+YDXvLW/+xyUiIiIdKSkpiVNPPZWHHnponzrryspKHnzwQc4+++zGGUxacvTRR3PHHXeQl5fH9OnTeeqppxq3mRlPPvkkkydPZsaMGfuMzDfVMBLs8bR9Oulp06YxcuRIfvWrXwGQmpoK0FhfDrBkyZI2H/dwYjqY0aNHs3r1alJSUhg2bNhej4P9BuGqq67i888/5/XXX+f999/nqquu2mv7JZdcwqpVq3jllVf22ffuu+8mKiqKCy64oD0/TqsEc/J+WMzsXOAe4GLn3IaGdufcJ865p5xzS/x18BcAa4DvtXQc59wjzrlc51xuww9xsDEzfvXNIxjVvze3/HsJ64srAh2SiIhIj/Lggw/i8Xg48cQT+eCDD9i0aRN5eXmcdNJJhIeH86c//anF/datW8ePfvQjPv74YzZs2MDs2bP54osvGDly5F79QkJCeOqpp5g4cSLTp0/fbwKfkZGBmfHGG29QVFREeXl5mz7HD3/4Qx555BE2bdpEdHQ0EyZM4Pe//z1fffUVH3/8MbfeemubjtceMR3IJZdcQp8+ffjmN7/JnDlzWLduHXPnzuWHP/whq1atOuC+xx13HCNHjmTmzJn07duX0047ba/t5513HhdccAGXXXYZDz/8MOvWrWP58uX86Ec/4i9/+Qt///vfSU5O3mufwsJCtm/fvtejtra23T4vBHfyXgx4gD7N2vsAB6xRN7Nv4xttn+mcO+BqA845D7AAaOdJODtXVLhvAacQM657ZiFVtVrASUREpLMMHjyYBQsWMGrUKC699FIyMzOZMWMGISEhLFmyZK9yjKZiYmIoKCjgvPPOIzs7m1mzZnHJJZdwxx137NO3aQK/vxH4AQMG8Mtf/pI777yTPn36cOONN7bpc5x55plkZmY2LmLUMCPL2LFjufbaa/n1r3/dpuO1R0wHEhMTw9y5cxkyZAjnnXcew4cPZ9asWZSWlpKYmHjQ/a+88kpKS0u57LLL9pnhx8x49tln+dWvfsVf//pXRowYwahRo7j//vuZM2cOl1xyyT7HGzVqFP369dvrMXfu3Hb7vADWtI4p2JjZfGCpc+6aJm0FwIvOuR/vZ5/zgaeAWc65f7fiHIYveV/qnLviQH1zc3NdsC8/PDu/kCue/JzvjM/g/751RKDDERERaZMVK1bsd4aPruYvf/kLP/zhD/nPf/7DN77xjYPvIEGvoKCAGTNmMG3aNJ555pnDvnn4QD/vZrbQOZfbvD2YR94B7gMuM7OrzGyEmT0A9AceBjCzp83s6YbOZnYh8E/gR8BcM+vrfyQ16fMLMzvFzIaY2THAY8BRDcfs6mbkpHHxuHT+9fkmCnd3qdkvRUREupUbb7yRf/zjH3z11VdUVVUFOhxpB9nZ2cyePZvs7OxDqv9vD0E72wyAc+5fZpaMb972fsAy4PQmNezpzXa5Dt9nut//aDAHmO5/nQA8gu+m1zJgMTDVOfdZu3+AALl26lCe+2wjj3+0nh+dtu9csSIiItI5DmXOdAlu2dnZjXPRB0JQJ+8AzrmHgIf2s236gd7vZ59bgFvaI7ZglZ4cw+lH9uOfn27ghhlD6R3V9pXSRERERCT4BHvZjByi66YNZU9NPc/N3/90UiIiIiLStSh576aOGBDP5GEpPPbhOmrqNfOMiIiISHeg5L0bu3baEAr31PDfxVsDHYqIiIiItAMl793Y5GEpjOrfm4fnrsHrDd4pQUVERESkdZS8d2NmxrXThrK2qIJ3V+wIdDgiIiIicpiUvHdzpx/Rl0FJ0Tw8Zw3BvCCXiIiIiByckvduLiw0hKunDGHxxl18vr400OGIiIiIyGFQ8t4DnDdmEEmxEfxtzppAhyIiItLtmNkBH5dddlnAYsvMzOTee+8N2Pml/QX9Ik1y+KIjQpl1XCZ/fK+A/O17yOnbK9AhiYiIdBvbtm1rfP36669z9dVX79UWHR3dpuPV1tYSERHRbvEFs570WduLRt57iJnHZRAdHsojc9cGOhQREZFupW/fvo2PhISEvdoqKiqYOXMmffv2JTY2ltGjR/P666/vtX9mZiZ33XUXV1xxBQkJCVxyySUAPP7446SnpxMTE8NZZ53FQw89hJntte9rr73GmDFjiIqKYvDgwdx5553U1tYCMH36dDZs2MBtt93W+FuA/amtreWOO+5g4MCBxMTEMHbsWP73v/81bs/Ly8PMeP/99xk/fjwxMTHk5uayaNGivY7z8ccfM23aNGJiYhgwYADXX389u3fvbtw+ffp0rr/+em699VZSU1OZNGkSAG+88QY5OTlERUUxdepUnn/+ecyM9evXU1FRQe/evXnhhRf2Ote7775LeHg4O3b0rEk5lLz3EImxEVwwdhD/XbKFrbuqAh2OiIhIj1BeXs5pp53Gu+++y9KlSzn33HM555xzWLly5V797rvvPoYPH86CBQv4zW9+wyeffMJVV13Fd7/7XZYsWcI3vvENfvGLX+y1z//+9z8uueQSbrzxRr766isef/xxXnjhBX7yk58A8NJLLzFw4EB+/vOfs23btr1+G9Dc5Zdfzpw5c3j22WdZtmwZs2bN4qyzzmLp0qV79fvxj3/M7373OxYtWkRycjKXXHJJ44QYX375JSeffDLf+MY3WLp0KS+99BJLlizhiiuu2OsYzzzzDM455s2bx9NPP83GjRs555xzOOOMM1i6dCk33XQTt99+e2P/2NhYLrroIh5//PG9jvP4449z5pln0qdPn1Zeje7BNANJ6+Xm5roFCxYEOoxDtrm0kmn35HH5xEx+eubIQIcjIiKyjxUrVjBixIh92jdcOnOftl6nnUrSxRfjrapi0zXX7rM9/uyzSTjnbOpLS9ly0/f32Z540YX0Pv106rZtY+vtd+y1LeMfTx9S/C+88ALnnXfeAWd4mzBhAmeeeSY//elPAd/I+5FHHslrr73W2Oeiiy6itLSUt99+u7Htmmuu4dFHH2089tSpUznppJP42c9+1tjnlVde4Tvf+Q579uzBzMjMzOTGG2/k1ltv3W88a9asISsri/Xr15Oent7Y/q1vfYv+/fvz0EMPkZeXx4wZM3j77bc55ZRTAPjoo4+YPHkymzZtYuDAgcycOZPw8HAee+yxxmMsWbKEY489lh07dpCWlsb06dMpKSnhiy++aOzz4x//mFdeeYUVK1Y0tv3mN7/hzjvvZN26dWRmZrJgwQImTJjAhg0bGDBgAKWlpfTv35///Oc/nHnmmfu/IEFufz/vAGa20DmX27xdI+89yMDEGM46qh/PfbaRssq6QIcjIiLS7VVUVHD77bczcuRIEhMTiYuLY8GCBWzcuHGvfrm5e+doK1euZNy4cXu1jR8/fq/3Cxcu5P/9v/9HXFxc4+Piiy+moqKC7du3tzrGRYsW4Zxj5MiRex3rjTfeYM2avSe7OOqooxpf9+/fH4DCwsLGeJ555pm9jtFQFtP0OGPGjNnns44dO/aAnzU3N5cjjzySp556CoBnn32WpKQkTjvttFZ/zu5CN6z2MNdOG8orS7byzPwNfHfGsECHIyIi0ioHGgkPiY4+4PawxMQDbg/v1++QR9oP5tZbb+Xtt9/m3nvvJSsri5iYGGbOnNlYl94gNja2zcf2er384he/4LzzzttnW2pqapuOY2Z8/vnnhIeH77Wt+c22Tbc31NB7vd7G56uuuopbbrlln3MMGDCg8fWhfFaAq666igceeICf/OQnPP7448yaNYvQ0NBDOlZXpuS9hxnRrzfTslN54qN1XDl5MFHhPe+HXkREpLN8+OGHzJw5k3PPPReA6upq1qxZQ3Z29gH3Gz58OJ9//vlebZ999tle70ePHs3KlSsZNmz/g3ERERF4PJ4DnuvYY4/FOcf27duZMWPGAfseyOjRo/nqq68OGE9Lhg8fzn//+9+92pp/VoBLLrmE2267jb/85S8sWrSI559//pBj7cpUNtMDXTdtKMXltby4aHOgQxEREenWsrOzefnll1m0aBFffvkl3/nOd6iurj7ofjfddBPvvPMO99xzD6tWreKxxx7j5Zdf3qvPz3/+c5599ll+/vOfs2zZMlauXMkLL7yw182emZmZzJs3jy1btlBcXLzfGC+55BIuu+wyXnjhBdauXcuCBQu49957eemll1r9We+44w4+++wzrrvuOhYvXszq1at5/fXXufbafe9HaOq6665jzZo13HrrreTn5/PSSy/xt7/9DWCvGXISEhI477zz+OEPf8jUqVPJyspqdWzdiZL3HmjCkCSOHhjPo3PX4vHqhmUREZGOct9995GWlsaUKVM47bTTmDBhAlOmTDnofscddxyPPvoof/rTnzjqqKN45ZVXuOOOO4iKimrsc8opp/DGG28we/Zsxo0bx7hx4/jd7363102nv/rVr9i0aRNDhw49YCnNE088weWXX87tt9/O8OHDOfPMM5k7dy4ZGRmt/qxHHXUUc+fOZf369UybNo2jjz6aH//4xwedDSYjI4MXX3yRV199laOPPpo//vGPjTPrNP28AFdeeSW1tbVceeWVrY6ru9FsM23Q1WebaeqtL7dx/T8X8dAlozn9yH6BDkdERAQ48OwbPd0tt9zCe++9x5dffhnoUDrcAw88wM9//nN27dq11+j7v/71L6699lq2bt1KTExMACNsH4cy24xq3nuok0f1ZXBKLA/PWcNpR/Q94MINIiIi0vnuueceTjrpJOLi4njvvfd4+OGH+c1vfhPosDrEgw8+yNixY0lNTeXTTz/l//7v/7jssssa85PKykq2b9/Ob37zG66++upukbgfKpXN9FChIcbVU4bwxeYyPlm7M9DhiIiISDMLFizglFNO4YgjjuCBBx7gt7/9LTfffHOgw+oQq1ev5uyzz2bEiBH87Gc/47rrruOee+5p3H733XeTk5NDUlLSXvPa90Qqm2mD7lQ2A1Bd52Hy72czsn9vnr5i3MF3EBER6WAqm5GeRIs0SZtEhYdy+aRM5hYUsXzr7kCHIyIiIiIHEfTJu5ndYGbrzKzazBaa2QFv0Tazaf5+1Wa21syuO9xjdmffGZ9BbEQof5u75uCdRUREOoGqAqQnONSf86BO3s3sAuAB4DfAscDHwFtmlr6f/oOBN/39jgV+C/zZzM491GN2d/Ex4Vw8Pp3Xv9hGcXlNoMMREZEeLjw8nKqqqkCHIdLhqqqq9lnRtjWCOnkHfgA86Zx71Dm3wjn3PWAbcP1++l8HbHXOfc/f/1HgKeDWwzhmt3fqEf3weB0L1pcGOhQREenh0tLS2LJlC5WVlRqBl27JOUdlZSVbtmwhLS2tzfsH7VSRZhYBjAHubbbpHWDifnY7zr+9qf8Bs8wsHLBDOGa3d8SA3kSEhrBoYymnHtE30OGIiEgP1rt3bwC2bt1KXV1dgKMR6Rjh4eH06dOn8ee9LYI2eQdSgFBgR7P2HcCJ+9mnL/BeC/3D/Mezth7TzK4BrgH2WrGsO4kMC+XIgfEsWF8S6FBERETo3bv3ISU1Ij1BsJfNBJxz7hHnXK5zLvdAywp3dbkZiSzbspvqOk+gQxERERGR/Qjm5L0Y8AB9mrX3AbbvZ5/t++lf7z/eoRyzRxidkUitx8tXW8sCHYqIiIiI7EfQJu/OuVpgIXBSs00n4ZshpiWf7Kf/Audc3SEes0cYnZ4IoJtWRURERIJYMNe8A9wH/MPMPgM+wjebTH/gYQAzexrAOTfT3/9h4EYzux/4GzAJuAy4qLXH7KlSe0WSmRzDwg1K3kVERESCVVAn7865f5lZMvBToB+wDDjdObfB3yW9Wf91ZnY68Ed8Uz9uBW5yzr3YhmP2WKMzEplbUIRzDjMLdDgiIiIi0kxQJ+8AzrmHgIf2s216C21zgNGHesyeLDcjiZcWbWHDzkoyU2IDHY6IiIiINBO0Ne/S+cZk+OreVTojIiIiEpyUvEujrLQ4ekWFsXCjkncRERGRYKTkXRqFhBij0xNZqBlnRERERIJSm5N3MzvNzF43s+VmNsjfdpWZndD+4UlnG5ORSEHhHsqqtCS1iIiISLBpU/JuZpcA/wZWAYOBcP+mUOD29g1NAmFMRiLOwZJNuwIdioiIiIg009aR99uBq51zt+BbtbTBp8Ax7RWUBM4xgxIIMVi4viTQoYiIiIhIM21N3rPwrWLaXDnQ+/DDkUCLjQxjRL/eumlVREREJAi1NXnfCmS30D4VWHP44UgwGJORyJKNu6j3eAMdioiIiIg00dbk/RHgT2Y2yf9+kJnNAu4G/tqukUnAjMlIpKLWw8rtewIdioiIiIg00aYVVp1zd5tZPPAuEAXMBmqAe51zD3ZAfBIADYs1LdpYyhED4gMcjYiIiIg0aPNUkc65O4EUYBwwAUh1zv2svQOTwBmQEE2f3pEs0HzvIiIiIkGlTSPvDZxzlcCCdo5FgoSZkZuRxMINSt5FREREgkmbkncze/VA251z3zi8cCRYjM5I5I0vt7G9rJq+8VGBDkdEREREaHvZzM5mj934FmuaChS3b2gSSA117xp9FxEREQkebb1h9fKW2s3sD/gSeekmRvXvTVR4CAs3lHLGUf0CHY6IiIiIcAg3rO7H34DvttOxJAiEh4Zw1MAELdYkIiIiEkTaK3nPaafjSBDJzUjkqy1lVNV6Ah2KiIiIiND2G1b/1LwJ6AecBjzeXkFJcBiTkUi91/HF5l2MH5Ic6HBEREREery2ThV5ZLP3XqAIuAUl793O6HT/TasbS5W8i4iIiASBtt6wOqOjApHgkxgbwdDUWBZqsSYRERGRoNBeNe/STY3JSGThxlKcc4EORURERKTHO+jI+8EWZmpKizR1P2MyEvn3gs2sLa5gaGpcoMMRERER6dFaUzazs8OjkKA1JiMJgIXrS5W8i4iIiATYQZP3/S3M1NHMLBK4F7gIiAbeB25wzm0+wD4/Bs7BN3VlDfAp8GPn3LImfZ4EZjXbdb5zbkK7foBuYkhKLAkx4SzcUMr5YwcFOhwRERGRHi2Ya97vB87Fl7xPAXoDr5tZ6AH2mQ48BEwEjgfqgffMLKlZv/fwTXHZ8Di9PQPvTkJCjNHpiVqsSURERCQItHWqSMxsBr6EOh2IaLrNOXd8ewRlZvHAlcDlzrl3/W2XAhuAE4H/tbSfc+6UZse5FCgDJgGvNdlU45zb3h6x9gRjMhL5YGUhuyprSYiJOPgOIiIiItIh2jTybmaXAW8BvfCNchcBicBoYHk7xjUGCAfeaWhwzm0CVuAbVW+tXvg+Y/Nh48lmVmhmBWb2qJmlHW7A3dmYDN9874s0+i4iIiISUG0tm7kVuNE5dxFQh6+e/FjgGaC8HePqC3iA4mbtO/zbWusBYAnwSZO2t4GZwAnAD4FxwAf+Gvt9mNk1ZrbAzBYUFRW14dTdx9EDEwgLMRZuUPIuIiIiEkhtTd6H4KsXB98NoQ3Tj/wFuOxgO5vZr83MHeQxvY0x7e9c9wGTgXOdc56Gdufc8865V51zXzrnXgNOw3eD6xktHcc594hzLtc5l5uamtoeoXU50RGhjOrfmwVarElEREQkoNpa874TXykKwBbgCOALIBnfjDAHcz++UfoD2QhMAEKBFHylOQ36APMOdhIz+yNwITDDObf2QH2dc1vNbDOQdbDj9mSjMxJ57rON1Hm8hIcG833OIiIiIt1XW7OwecDJ/tf/Bv5kZk8AzwHvHmxn51yxc27lQR6VwEJ8ZTknNexrZgOBEcDHBzqHmT2A74ba451zKw8Wk5mlAAOAbQfr25ONyUikus7Lim27Ax2KiIiISI/V1uT9RnyJOsBvgXvwjbr/G7iqvYJyzpUBjwF3m9mJZnYs8A98o/wNZTuY2Uozu7HJ+weBy4GLgVIz6+t/xPm3x5nZvWZ2nJll+kt0XgMKgZfbK/7uqOGmVZXOiIiIiAROm8pmnHMlTV57gd+3e0RfuxnfPO3/4utFmmY2rV/HV6ue0uT9Df7n95sd65fAXfhugj0S3w2rCfhG22cD5zvn9rRr9N1Mv/hoBiREs3BjKVcwONDhiIiIiPRIbUrezWwJvhHw55xzWzskIj/nXA3wPf9jf33sQO9b6F8FnHKgPrJ/ozMSWbC+5OAdRURERKRDtLVs5k18pTMbzOw9M5vVUJIi3V9uRiLbyqrZsqsq0KGIiIiI9EhtSt6dcz9xzg0GZgAFwL3ADjN73sxanGpRuo+GunfN9y4iIiISGIc0559z7kPn3A1AP+ACfLXnr7ZnYBJ8hvftRUxEKIuUvIuIiIgERFvneW9kZoPwzepyCTAK+LC9gpLgFBYawjGDEliwQXXvIiIiIoHQppF3M0s0s2vMbA6wDt+sLc8Bg51z0zoiQAkuYzISWbFtDxU19YEORURERKTHaevI+3Z8K57+C7jZObe4/UOSYDYmIxGP17F08y4mDk05+A4iIiIi0m7amryfCbzvn+NdeqBj0/03ra4vVfIuIiIi0snaukjTux0ViHQN8dHhZPeJY+FG3bQqIiIi0tnaWvOeZGZ/NbMCM9tlZrubPjoqSAkuYzKSWLShFK/XBToUERERkR6lrWUzjwHHAo8AWwFlbz3QmIxEnvtsI6uLysnu0yvQ4YiIiIj0GG1N3k8ATnLOze+IYKRraFisacH6UiXvIiIiIp2orYs0FQLlHRGIdB2ZyTEMSIjm1aVbAh2KiIiISI/S1uT9TuBXZhbXEcFI12BmzDwug0/XlrBsS1mgwxERERHpMdqavP8UOBkoNLMVZvZF00cHxCdB6sJx6cREhPL4h+sCHYqIiIhIj9HWmvcXOiQK6XLio8M5P3cQz3y6gdtPHU7f+KhAhyQiIiLS7bV1nvdfdlQg0vVcMWkwT32ynqc/Wc/tpw4PdDgiIiIi3V5by2Ywsygz+7aZ3WFmCf62oWaW1O7RSVBLT47hlJF9+ef8jVTW1gc6HBEREZFur62LNA0DVgIPA/8PaEjYrwfubt/QpCu4aspgyqrqeHGRZp4RERER6WhtHXm/H3gH6ANUNWl/FZjRTjFJFzImI5GjB8bz+IfrtOKqiIiISAdra/I+EbjXOedp1r4R6N8+IUlXYmZcOWUI64or+GBlYaDDEREREenW2lzzDoS30JYOaMLvHuq0I/rSPz6Kv3+4NtChiIiIiHRrbU3e3wF+0OS9M7PewC+BN9otKulSwkNDuGxSphZtEhEREelgbU3efwBMNrN8IAr4F7Ae6Av8qH1Dk67kgrHpxGrRJhEREZEO1abk3Tm3FTgG+D3wN2ABcDsw2jlX1J6BmVmkmf3ZzIrNrMLMXjWzgQfZ5y4zc80e25v1MX+/rWZWZWZ5ZjaqPWPvieKjwzl/7CBeXbqV7WXVgQ5HREREpFtqc827c67KOfe4c+5G59wNzrm/O+eqDr5nm90PnAtcBEwBegOvm1noQfbLB/o1eRzZbPvtwA+B7wFjgULgXTPr1W6R91CXTxyMxzme/mR9oEMRERER6ZbatMKqmc3czyYHVAOrnXOLDzcoM4sHrgQud86962+7FNgAnAj87wC71zvntre0wcwMuBn4nXPuRX/bLHwJ/MX4fpsgh6jpok03Hj+MmIg2/XiJiIiIyEG0Nbt6EIjAN+OM198WAtT5X4eb2WLg1MMsoxnjP8c7DQ3OuU1mtgLfdJUHSt6HmNlWoAaYD/zEOdcwDcpgfPX5TY9bZWZz/cdV8n6YrpoymLe/2s6Li7Zw6YSMQIcjIiIi0q20tWzmfGAxMAnfDatR/tcLgbOBYwED7jvMuPoCHqC4WfsO/7b9mQ9cBpwKXO3v+7GZJTc5bsNxWnVcM7vGzBaY2YKionYt6++WxmQkcvSgBC3aJCIiItIB2pq83wd83zn3iXOu3v/4BN8sNH9wzi3FV0/e4mqrZvbrFm4obf6Yfqgfxjn3lnPu3865L5xz7wFn+j/jrMM45iPOuVznXG5qauqhHqbHMDOumjxYizaJiIiIdIC2ls1kApUttFf6twGsAxL3s//9wDMHOcdGYAIQCqQATYe7+wDzWhUp4JwrN7OvgCx/U0MtfB//eZoet8U6eWm7047oy4CEaP7+4VpOHNkn0OGIiIiIdBttHXn/DLjPzBpLTPyv78VXsgK+RHlzSzs754qdcysP8qjEV4ZTB5zU5DwDgRHAx60N1syigOHANn/TOnxJ+knN+kxpy3HlwMJCQ5g1MUOLNomIiIi0s7Ym71cB/YGNZrbezNbjG8Hu798GEAv8+nCCcs6VAY8Bd5vZiWZ2LPAP4AvgvYZ+ZrbSzG5s8v5eM5tmZoPNbDzwgj+ep/zHdfhG/+8ws3PM7AjgSaAcePZwYpa9adEmERERkfbXprIZ59wqf8J7MpDjb14JvOtPjHHOvdJOsd0M1ONbxTUaeB+Y6ZzzNOmTg6+0psFA4Dm+Lrf5FJjgnNvQpM/d/uM9iK+8Zz5wsnNuTzvFLXy9aNM/PtnA7acOp298VKBDEhEREenyzJ9zSyvk5ua6BQsWBDqMLmPjzkqm3zub66YN5fZThwc6HBEREZEuw8wWOudym7cfdOTdzH4APOScq/a/3i/n3OFOESndSHpyDKeM0qJNIiIiIu2lNdnU9/DVjFf7X++P4/Dnd5du5qopg3lrmRZtEhEREWkPB03enXODW3ot0hqj079etOmScemEhFigQxIRERHpslo124yZfWxmCU3e/9bMkpq8TzGzjS3uLD2aFm0SERERaT+tnSpyAhDR5P13gYQm70PxzfQiso+GRZvuf7+A5Vt3BzocERERkS6rrfO8N2ip9kHT1kiLwkJDuO2UHAq2l3P6n+bxjb98yDOfbmB3dV2gQxMRERHpUlo1VaSZeYG+zrlC//s9wNHOubX+932Arc650I4MNtA0VeThKa2o5ZUlW/jX55tYuX0PUeEhnH5EP84fO4jxg5MwUz28iIiICBzGVJF+jn1H1jXSLm2SGBvB5ZMGc9nETL7cUsa/Pt/Eq0u28tLiLQxOieW83IF8e/RA0nprQScRERGRlrRl5P1doMbfdBowB6j0v48ETtTIu7RVVa2HN7/cxr8WbOKzdSWEhhgzclK5YGw6M3JSCQs91MouERERka5rfyPvrU3en2jNSZxzlx9CbF2GkveOta64gn8v2MQLCzdTtKeGtF6RPDozl6MHJQQ6NBEREZFOdVjJu/goee8c9R4vs/OL+Nkry0jtFcl/vztJ88OLiIhIj7K/5F01CRJ0wkJDOGlkH3502nC+3FLGS4u3BDokERERkaCg5F2C1jeO7s/RgxK4538rqaytD3Q4IiIiIgGn5F2CVkiI8fMzR7Bjdw0Pz1kb6HBEREREAk7JuwS1MRlJnHlUPx6Zu4ZtZVWBDkdEREQkoJS8S9D70WnD8Tq4++38QIciIiIiElBK3iXoDUyM4arJg3l58RaWbNoV6HBEREREAkbJu3QJN8wYRkpcJL9+fTma3lRERER6KiXv0iXERYZx68nZLNhQyhtfbgt0OCIiIiIBoeRduozzcgcxol9vfvfWSqrrPIEOR0RERKTTKXmXLiM0xPjZGSPYXFrF4x+tC3Q4IiIiIp1Oybt0KROHpXDiiD48NHsNRXtqAh2OiIiISKcK2uTdzCLN7M9mVmxmFWb2qpkNPMg+683MtfB4o0mfu1rYvr3jP5G0l5+cPpzqOg/3vaupI0VERKRnCdrkHbgfOBe4CJgC9AZeN7PQA+wzFujX5DEacMC/m/XLb9bvyPYMXDrWkNQ4Zh6Xyb8+38SKbbsDHY6IiIhIpwkLdAAtMbN44Ergcufcu/62S4ENwInA/1razzlX1Ow4VwK72Td5r3fOabS9C/v+CVm8tHgzv35jOc9cOR4za/MxNpVU8sriLTggMizE9wgP9b/2P4eHEBHatD2EjORYQkPafj4RERGRwxWUyTswBggH3mlocM5tMrMVwET2k7w3Zb5s7krgGedcVbPNQ8xsK1ADzAd+4pxb217BS8eLjwnn5hOyuOu15XywspATRvRp9b47y2v48wer+ef8DdR52j5n/NjMRJ6+YjzREQf6JZCIiIhI+wvW5L0v4AGKm7Xv8G9rjZOAwcCjzdrnA5cBK4E04KfAx2Y2yjm3s/lBzOwa4BqA9PT0Vp5aOsMlEzL4x6cb+H9vrmBqdirhoQeuAiuvqefv89by6Ny1VNV5OD93EN8/MYvUuEhq6r3+h4eauiav673UNmnfUFLJ3W+v5Pp/LuSRS3OJCAvmyjMRERHpbjo1eTezXwN3HqTbjHY63dXA5865pU0bnXNvNYvpU2AtMAu4r/lBnHOPAI8A5ObmamnPIBIeGsKdZ4zgiicX8MynG7h80uAW+9XUe3hu/kb+/MFqdlbUcuqovtx6Sg7D0uIa+4SFhhAb2brzxkeH8+OXvuTW/yzl/guOIUQlNCIiItJJOnvk/X7gmYP02QhMAEKBFKBpHXsfYN7BTmJmacA3ge8erK9zrtzMvgKyDtZXgs+MnDQmD0vh/vdWcfaxA0iIiWjc5vU6/rt0C394p4DNpVUcNySZO04bzjGDEg7rnBeNS6e0spa7384nISacX35j1CHV3IuIiIi0Vacm7865YvYthdmHmS0E6vCVvjzrbxsIjAA+bsWpLsNXz/5cK84VBQwHZrfiuBJkzIyfnjmC0x+YxwPvr+IXZ43COUdefhG/f3slK7fvYWS/3jx1xZFMzUpptyT7+mlD2VVZxyNz15IYE8EtJ2W3y3FFREREDiQoa96dc2Vm9hhwt5kVAjvxlbR8AbzX0M/MVgJ/cc79pUmbAVcBzzvnypsf28zuBV7DN8KfBvwMiAWe6rhPJB1peN/eXDA2nX98soFjBiXwz/kb+WxdCelJMTxw4TGcdVT/di9tMTN+fNpwSitqeeD9VSTEhO+3bEdERESkvQRl8u53M1AP/AuIBt4HZjrnPE365OArrWlqOr4SmO/s57gD8Y3IN5TkfApMcM5taK/ApfP94KRsXlu6le8/v4SUuAh+9c1RXDg2vUNvKDUzfnvOkZRV1fHL15aTGBPBt44d0GHnExERETHndA9ma+Xm5roFCxYEOgzZj/eW72BtcTmXjM8gNrLzvpdW13m4/InP+Wx9CY/OHMPxw1s/baWIiIhIS8xsoXMut3m75rmTbuPEkX24ZurQTk3cAaLCQ3lk5hhG9uvN9c8s4rN1JZ16fhEREek5lLyLtINeUeE8eflYBiRGc+VTn7N86+5AhyQiIiLdkJJ3kXaSHBfJP64cT1xkGDMf/4z1xRWBDklERES6GSXvIu1oQEI0/7hyHB6vl+88Np8du6sDHZKIiIh0I0reRdrZsLRePHn5OEorapn52GfsqqwNdEgiIiLSTSh5F+kARw9K4JGZuawrrmDW45+xcrtq4EVEROTwKXkX6SCThqXwl4uPZW1xBac9MI/vPbeYtUX7rBsmIiIi0mrBvEiTSJd38qi+zBucxKPz1vLER+t544utnDt6IDedkMWgpJhAhyciIiJdjBZpagMt0iSHo7i8hr/mreEfn27AOceFY9O58fhh9OkddcjH3LKrio9WF1NV6+G0I/qSdhjHEhERkeCxv0WalLy3gZJ3aQ/by6r58wer+NfnmwgNMWYel8F104aSHBd50H1LK2r5ZO1OPlpdzEeri1m/s7JxW4jBlKxUzh0zkJNH9iEqPLQjP4aIiIh0ICXv7UDJu7SnTSWV3P/eKl5evJmo8FCumDSYq6cOIT46vLFPZW09n68v5ePVxXy4upjl23bjHMRGhDJ+SDKThqUwaVgyYSEhvLx4My8v2sLWsmp6RYZxxlH9OGf0QMZmJmJmAfyk0t2VVtTy27dWUFJRS4gZoSFGSIgR2vDajNAQmrz2PQ9MjOabxwwgtdfBv7iKiPQ0St7bgZJ36QirC8u5/70CXv9iG72jwrhy8hDM4KPVxSzaWEqdxxEeahybnsikoSlMzkrmqIEJhIfue7+51+v4dO1OXly0hbeWbaOy1sOgpGjOOXYg54weQEZybAA+oXRnO3ZXc+lj81lfXMmwtDi8zuHxOjzO4W18Zp82j9exp7qe0BBjRk4a5+UO5PjhaS3+XIuI9ERK3tuBknfpSMu37ua+dwt4b8UOzGBkv95MGpbCxKHJjBucRExE2+4vr6ip539fbefFRZv5eM1OnIOxmYmcM3ogZxzVj95R4Qc/iMgBbNxZySWPfcrO8lr+PjOXicNS2rT/6sJy/rNwEy8t2kLRnhpS4iL41jEDOH/sILL79OqgqEVEugYl7+1Aybt0hvXFFfSODicpNqLdjrl1VxUvL97Ci4s2s7aogojQEDJTYkhPiiE9KZb0pGjSk33vBybGqF5eDip/+x4ufWw+tR4vT14+jmMGJRzyseo9XuYUFPGfBZt5b8UO6r2OowclcN6YgZx1dP+9SskOpqyqjo07K9lQUsHGkkoGJcZw6hF9NaIvIl2Okvd2oORdujrnHEs3l/HWl9tYW1zBppJKNpZUUlnr2atfn96RpCfFMCipIcGPITMllqEpccTHtN+Ivdfr2Fxaxcrtu1ldVE5uRhLjBie12/GlYyzeWMplT3xOZFgI/7hyPDl922+UfGd5Da8s2cp/Fmxi5fY9RIaFcOoRfTk/dxDHDUkGYMeeajbsrGxM0jfsrGRTSSUbSirZVVm3zzH79o5i5sQMLhqbTmI7fikWEelISt7bgZJ36Y6cc+ysqGVjiT8B2ulL6Bveb99dTdN/JlLiIhiSGsfQ1FiGpsYxxP88MDGG0JD93xhbXF5D/vY9rNy+h4Lte1i5Yw+rduzZ54vDzOMyuOPU4cRGahmKYPTR6mKufnoBKXGRPHPleNKTO2a9Auccy7bs5t8LNvHfJVvYXV1PUmwEFTX11NR7G/uFhhgDEqLJ8P/myPccS0ZyDAMTo/l8fQmPf7ieD1cXExUewjmjB3L5xEyyVJYjIkFOyXs7UPIuPVF1nYfNpVVs2FnBmqJy1hRWsLa4nDVFFZRU1Db2ayjFGZoax9DUONJ6R7KuuIL87Xso2LGH4vKv+ybFRpDTpxc5fb9+pCfF8Ne8NTz+0ToGJkZzz7ePZoJ/pFWCw/++2s73nl3M4JRY/nHluE5bV6C6zsM7y3eQl19IcmwE6cmxZPgT9f4J0a0qiVm5fTdPfrSelxZvobbey9TsVK6YlMnUrFRCDvClU0QkUJS8twMl7yJ7K62o9SXyhRWs8T+vLSpnQ0klHq8jOjyU7D5x5PTtRXafXgzv25ucvr1IiYvY7/SVn60r4bYXlrJhZyWXTczk9lNz2nyzrrS/Fxdu5vYXv+DIAfE8eflYEmK6ZvnJzvIanvtsI09/soHCPTUMTY3lskmDOXf0AP2ciUhQUfLeDpS8i7RObb2Xkopa0npFHtKoZmVtPXe/nc+TH68nMzmGe847mrGZwVMLX+fxsnVXFZtKqthU+nWJ0abSKrbtqsIBYSG++cy/fg7xPYd+3R4WEkJYqNE/Pppxg331/gMTo4NuXv4nP1rHXa8tZ9KwZB65NLdblDTV1nt588ttPPbhOr7cUkbvqDAuGp/Od8ZntOs1KKuq46utZXy1ZTfLtpbx5ZYyPF7HxePSuXBceptuxhWRnkXJeztQ8i7SuT5Zs5PbX1zK5tIqrpw0mFtPyem0mXCcc6wuLGfZ1jJfkl5SyabSSjaVVLGtrApvk386w0KMAYnRpCfF0D8+mpAQw+P1Uu/1zWde73HUe72+103aPF5HndfL2qIKyqp8N1r2j49i/JDkxmR+SEpswJJ55xx//mA1971bwMkj+/Cni47tdjMROedYuKGUxz9ax9vLtuN1EBMRysDEaAYmxvifoxmUGNP4PiEmvMVrsrO8hq+27ubLLWV8tbWMZVt2s7Hk61WQByREM6p/b3ZX1/Hp2hJiI0I5f+wgrpg0mEFJHXPvQGtV13m63bUV6eqUvLcDJe8ina+ipp7fvbWSf3y6gSEpsdxz3tGMyUjskHNt3VXFR6uL+XjNTj5aXUzhnprGbWm9IhmUFMMgf5I+MCmGQYkxpCfH0Ld31AFv1j0Yr9dRULiHz9aVMH9tCfPXlVBc7jt3Slwk4/2J/PghSWSn9eqUGm3nHL9+YwWPfbiOc0cP5PfnHklYN59ucXNpJe8u38Gmkio2l/p+k7K5pJI9NfV79YuNCG1M5PslRLFjdw1fbSlja1l1Y5+M5BiO6B/PqAG9fc/9e5Mc9/VKsl9tLeOxeet4delWvM5x6hF9uXLykA772W5JTb2Ht5dt55/zN/LZuhL6xUdxzKCExseRA+PbrZSorKqO6joPfTrpPgnpGeo83m49DayS93ag5F0kcD5eXcxtL3zBtrIqrp4yhFtOyj7skcJdlbV8smYnH60p5uPVO1lbXAFAcmwExw1NZtKwFMZkJJKe1Llz3zvnWFdc4Uvm15Uwf+3OxsQwPjqcowbGExMRSmiIERoSQqjhew75+jksJIQQ85XphJgRERZCVHgIkWGhRIWHEBUWSlR4KJFhIUSF+9uavP/T+6v4z8LNXD4pk5+dMbJH39RZVlXH5tJKNpdW+R++15tKKtm6q4qUXpEcOSC+MVkf1S++1VOqbi+r5qlP1vPPTzewu7qeY9MTuHrKEE4e2afDvixtKqnkn/M38p8Fm9hZUUt6UgxnHNWPzaVVLNlUyqaSKsA3k092n14cMyjen9AnMiwtrsUvqs45SivrWL+zgg07K1hfXOl73ul7LvVP4Tl+cBKXTMjglFF9iAzTSH93U1PvYcH6UvLyC1m8cRcZybEcm57AsekJ5PTpdVg/08451hRVsGB9CZ+tL2HB+lI2llSSEhdJZrJvICUz2TfTVEZyLJnJMV323pwGSt7bgZJ3kcAqr6nnN2+u4Nn5GxmaGsvPzxpFWq9IwkKMkBAj1Hz15CH+mvIQ//tQM0JDDa9zLNm4qzFZX7a1DOcvkxg/OIlJw1KYNCyFnD6dM7rdFptLK5m/toTP1pXw1bYy6uodHucrvdnr0VKb11Hr8R78JM3cfGIW3z8hK+hq8Lujipp6Xly0mcc+XMeGnZUMTIzmikmDOX/sIOLa4R6Deo+XD1YW8s/5G5m7qggDThzRh+9MyGDysJS9ft6Ly2tYumkXS5o89lT7fvsQFxnGkQPiOSY9gVAzf7JeyfqdFY19AMx8ZUJfJ1Mx1Hkc//p8ExtLKkmKjeC83IFcPC6djOTYw/ps5TX1fLS6mNkrC9m+u5qstLjGG+SHpcURHaEvCR1pU0kleQVFzMkv5OM1O6ms9RARGsLI/r3ZVFLJTv+sZNHhoRw5MJ5jB/mS+WMGJdI3fv+/ianzePlq625fsr6uhAUbShtnOEuOjSA3M5GcPr3Ysbumcb2HbU1++wXQOyqMzJRY31olybGkJ8eQEhdBXGQ4sZGhxEWGERcZRmxkGJFhIUH3b12XS97N7BrgIuBYIB4Y7Jxb34r9zgX+DxgKrAHudM693GS7Ab8ArgESgfnAd51zXx3s2EreRYLD3IIifvTiF3uVKbRFeKhx7KBEf7KezNGDErr1r17BV5pT6/FSU+elut5DdZ2Hmnov1XUequu8zd576BcfzeSslECH3eN4vI73VuzgsXnr+Gx9Cb0iw7hw3CDGZCTSNz6a/vFRpMS1/kbwHburef6zTTz/+Ua2lVXTp3ckF45N58Jxg+gXH92qY3i9jnU7K1iy8etkfsW23ThgUGJ04yhnRnIsmSm+54GJ0S2OrHu9jg9XF/Ps/I28u2IHHq9jSlYKl4xP54QRfVr993BtUTmz84uYvbKQ+et2Uudx9IoMY0BiNGuLK6j1rwVgBpnJsb5Zr/r0Iqdvb3L6xpGZHNvhZWBlVXVsKqmkcE81ZkZ4SAjhoUZYaAgRob6b1cNDfW3h/ve+dn9bSEjQDSKA7/6Iz9aVkJdfxJyCQtYU+X5jOTAxmuk5qUzPTuO4ocnERobhnGNTSRWLN5Wy2P/zs3zr7sbBhH7xUf5E3pfM13m8fLauhM/Xl7B44y6q6nzrgGQkx/gX8UskN3P/9wJV13nYVFLZ+Bufhi+WG3ZWsmVXFR7v/nPesBAjLiqM2AhfQh8X5Uvq4yJD+ctFowNyLbpi8n4zEA1UAX+kFcm7mR0HzMOXnL8EnAP8EpjknJvv73MH8FPgMiAf+DkwGchxzu050PGVvIsEjz3VdcxfW0Kdx7vPaLPXOTxefO0eLx7nSxq8zpHTtxfjBidpWkAJeks27eKxD9fx5pfb9ko6wkKMPr2j6J8QRb94X919v95R9EuIpn98NH3jo8jfvodnPt3QLEHO4MQRae2StNbUewgxO6wvvTt2V/Ovzzfx/Gcb2VpWTWqvSC4cO4gLxg5iYOLeN/DW1HuYv7aE2fmFzF5ZyPqdvhuBs9LimDE8jRk5aeRmJhIeGoLH61i/s4KC7XvI37GHfP/z+uKKxhvNI0JDGJoWR3afONJ6RZIYG0FiTASJMeG+Z//7hJjw/X7GhlmnGha121hSyeaSr9833IR+OMJCvk7wI8JC/K+/Tvi/bjMiwnxlbxFhIUQ2PkIb30eEhhAZ7nuOCAslLMQw85VHhZjvdYiZ/wEhIU1em7GxpJI5BUV8smYnVXUeIsJCGD84iek5aUzPSW31zfU19R6Wb93dmMwvblKqBRBiMKJfb8ZmJjE2M4nczMR2uVeizuNlS2kVpZW1VNR4KK+pp7ymnopmzw2vK2o87Kmpp7bey1vfn3LY5z8UXS55b2BmucDntC55/xeQ5Jw7qUnbe0CRc+4i/6j7VuAvzrn/598eDRQCtzrn/nag4yt5FxGRztZQc79tVzXbyqrYWlbN9rJqtu6qYpv/dUtlUYkx4ZyfO4iLxqWTmXJ4pSkdyeN15OX7Snpm5xcCMCMnjfNzB1FSUcvs/EI+Wl1MZa2HyLAQJg5NbkzY2zJLT3Wdh9WF5RQ0SehX7ShnZ0UN1XX7LyvrFRX2dWIfG0FNnZeNJZX7zDoVHmoMTIxhUFIM6Um+G9vTk2IaE886j6Pe46XW46Xe46jzeKnzOurqvdR7vdT6t9d5vNQ1bPe/rq337v3e46Wufu/tNR4vNXWext+w1Xq8vvZ632/V2iPdy0iOYXp2KtNz0pgwJLndSpIaSrXCQkMYnZ5AryhNoQo9J3nfCPzZOXdPk7bbgBudcxlmNgRfKc0459znTfq8ARQ752Yd6PhHJSe7104/Y6+2XqedStLFF+OtqmLTNdfus0/82WeTcM7Z1JeWsuWm7++zPfGiC+l9+unUbdvG1tvv2Gd70uWX0+v4GdSsXcf2X/xin+0p119H7MSJVK9YwY7f/Haf7am33ELM6GOpXLSYoj/+cZ/tfX7yY6JGjKDi448p/uvD+2zv+8tfEjlkMHs+mE3JE0/ss73/3b8nvF8/dr/5JqXPPb/P9gF/eoCwxER2vfQyZS+/vM/2QY/8jZDoaEqefZY9b729z/aMfzwNwM7HHqc8L2+vbRYVRfqjjwBQ9NBDVH7y6V7bQxMSGPjnPwFQ+If7qFqyZK/tYX37MuCeuwHY/pvfULNi5V7bIzIz6fd/vwJg289+Tu369XttjxwxnL4/+QkAW267nfrt2/faHn3MMaT98AcAbP7eTXh27dpre8xxE0i94QYANl59Da567xKQuOnTSb7yCgA2XDqT5vSzp5890M+efvY+xeGraa+t91IVHceX1/yYpLhIct95lrovvthr/2D/2fOMn8jL2TN4/vNN3Pqm79pFhoWSEBNOQkw4/b55Fn1mXtLuP3se55u+te7cCyk9egLlq9eQ+PAfGqd5rfc46ryO/x17GuszRzG6ppAZ7/+z8QbwhhHutCD92XP+R8qfH6QuPJLdzz9P7Xvv4PxbG1LBsD89jNeB57l/4D750Le1YVtMNDlPPgbo373O+ncv85l/tJi8d7ffG/cFdjRr2+Fvp8lzS30GtHRAf+39NQDDe/VqnyhFRETaiUFjOUXv3lGMmTQYgML3Qjj8wo3OFR8dwQ9PzuGmE7JYsfgxwsNCiA4PpaEYIzysY+qOQ80IDTP6JcUyKiuFmtA9bG+hVGPKt474+ovjosDOzd8W5n/ERoYREh0B0WHsCdu3HCgjzZfn7OwVRXmz0kLTOgBBo1NH3s3s18CdB+k2wzmX12Sftoy81wJXOeeebtI2E3jUORdpZhOBj4AM59zGJn0eBwY450450PFVNiMiIiIinWF/ZTOdPfJ+P/DMQfpsPMj2A9kO9GnW1sffTpPnPs3O07SPiIiIiEhQ6tTk3TlXDBR34Ck+AU4C7mnSdhLwsf/1OnxJ+kn4RvMxsyhgCnBbB8YlIiIiInLYgrbm3cz64qtRz/Y3jTSzBGCjc67E3+d94DPn3I/9fR4A5prZj4BXgLOBGfimgsQ558zsfuAnZrYSKMA3bWQ58GwnfCwRERERkUMWzKuSXAcsBv7pf/+G//03mvQZCvRreOOc+xi4EN8c7l8AM4ELGuZ497sb37zxDwIL/PuffLA53kVEREREAi3op4oMJrphVUREREQ6w/5uWA3mkXcREREREWlCybuIiIiISBehspk2MLMiYEOATp9Cx87UI8FF17tn0fXuWXS9exZd756nva55hnMutXmjkvcuwswWtFT3JN2TrnfPouvds+h69yy63j1PR19zlc2IiIiIiHQRSt5FRERERLoIJe9dxyOBDkA6la53z6Lr3bPoevcsut49T4dec9W8i4iIiIh0ERp5FxERERHpIpS8i4iIiIh0EUreRURERES6CCXvIiIiIiJdhJJ3EREREZEuQsm7iIiIiEgXoeRdRERERKSLUPIuIiIiItJFKHkXEREREekilLyLiIiIiHQRSt5FRERERLoIJe8iIiIiIl2EkncRERERkS5CybuIiIiISBeh5F1EREREpItQ8i4iIiIi0kUoeRcRERER6SKUvIuIiIiIdBFK3kVEREREuggl7yIiIiIiXYSSdxERERGRLkLJu4iIiIhIF6HkXURERESki1DyLiIiIiLSRSh5FxERERHpIpS8i4iIiIh0EUreRURERES6CCXvIiIiIiJdhJJ3EREREZEuQsm7iIiIiEgXoeRdRERERKSLUPIuIiIiItJFKHkXEREREekiwgIdQFeSkpLiMjMzA3LuiooKYmNjA3Ju6Xy63j2LrnfPouvds+h69zztdc0XLlxY7JxLbd6u5L0NMjMzWbBgQUDOnZeXx/Tp0wNybul8ut49i653z6Lr3bPoevc87XXNzWxDS+0qmxERERER6SKUvIuIiIiIdBFK3kVEREREuggl7yIiIiIiXYSSdxERERGRLkLJu4iIiIhIF6GpIkVERERE/JxzrCosZ/bKQvJ37OG+848JdEh7UfIuIiIiIj1aZW09H6/eyez8QvLyi9iyqwqA4X17sae6jl5R4QGO8GsBTd7N7E+HsNtdzrmSdg9GRERERHqM9cUVzM4vZHZ+EZ+u3UltvZeYiFAmD0vhxuOHMT0nlX7x0YEOcx+BHnm/EfgEqG1l/8nA/YCSdxERERFpteo6D5+tK2kcXV9XXAHA0NRYZk7IYMbwNHIzE4kMCw1wpAcW6OQd4GznXGFrOprZno4ORkRERES6j12VtTz58Xqe/Hg9uyrriAwLYeLQZC6flMn07DTSk2MCHWKbBDp5vxwoa0P/a4EdHRSLiIiIiHQTRXtq+PuHa3nmkw1U1Ho4cUQfLh4/iOOGpBAdEdyj6wcS0OTdOfdUG/s/21GxiIiIiEjXt3VXFY/MXctzn22k1uPlzKP6c8P0oYzo1zvQobWLQI+8t8jMEmg2B71uUhURERGR/VlfXMHDc9bw4qLNOAdnHzuA66cPZUhqXKBDa1dBk7ybWQbwMDAdiGi6CXBA1/39hoiIiIh0iIIde3hw9mpeW7qVsNAQLhqXzjVThzAwsWvVsrdW0CTvwBNAAnAlsBVfwi4iIiIiso8vN5fxl9mr+N9XO4iJCOWqKUO4avJg0npHBTq0DhVMyfs4YIJzblmgAxERERGR4FJd52H+uhLm5BeRV1DI2qIKekeFcdMJWVw+MZPE2IiDH6QbCKbkfR0QGeggRERERCQ4rCuuYE5+IXkFvoWUquu8RISFMGFIMjMnZHDumIFBtfppZwim5P37wG/N7Abn3OpAByMiIiIinauytp5P1+4kL7+IOQVFbNhZCcDglFguHJvOtJxUJgxO7tJTPR6uYEre/4tv5D3fzGqA+qYbnXPdY34fEREREWlUUlHLa0u38t6KHcxfV0JtvZfo8FAmDk3mysmDmZadSkZybKDDDBrBlLzfGOgARERERKTj1dR7+GBFIS8u2kJefiH1XsewtDhmTshgek4auZmJRIX33NH1Awma5L2tCzaJiIiISNfhnGPRxl28tGgzr3+xjbKqOtJ6RXLl5MGcPXoAw/uqyKI1Apq8m1lSw+JLZpZ0oL5apElERESk69lUUslLi7bw8uLNrN9ZSXR4KKeM6sM5owcyaVgKoSEW6BC7lECPvBeZWT/nXCFQTMtzu2uRJhEREZEuZHd1HW9+sY2XFm3hs/UlmMFxQ5K58fgsTj2iL3GRgU5Bu65A/8kdDzSMqM8IZCAiIiIicmicc6wpqiAvv5A5BUXMX1tCrcfL0NRYbjslh28dO4ABCdGBDrNbCGjy7pyb09JrEREREQlulbX1fLx6J3kFheTlF7G5tArAd+PpcRmcdXR/jhoYj5nKYtpToEfe9+GvfU8DQpq2O+eWByYiEREREfGNrpeTl19EXn4Rn63zja7HRIQycWgK100byrTsVAYlxQQ61G4taJJ3MzsWeAI4sqEJX617q2vezezHwDlADlADfAr82Dm3rEkfA34BXAMkAvOB7zrnvmq3DyMiIiLSDZTX1PPJmp3MaTa6npUWx6yJX0/rGBmmWxM7S9Ak78DjwBZ8K63uoOWbVw9mOvAQ8Dm+pP9XwHtmNrLJbDW3Az8ELgPygZ8D75pZjnNuz+F8ABEREZGuzDnHyu17mFNQxJz8IhZsKKHO44iJCGXSsBSun+4bXR+YqNH1QAmm5D0LOM85t/pQD+CcO6XpezO7FCgDJgGv+UfdbwZ+55x70d9nFlAIXAz87VDPLSIiItIV7aqs5cPVxczJL2JOQRGFe2oAGN63F1f4VzjNzUgiIizkIEeSzhBMyfuHwAjgkJP3FvTCVztf6n8/GOgLvNPQwTlXZWZzgYkoeRcREZFuzuN1fLF5l290vaCIpZt24XUQHx3O5KwUpmWnMi07lT69owIdqrTAnDuU6pT2Z2YDgL8DbwPLgLqm251zcw/hmP/GN6Kf65zzmNlE4CMgwzm3sUm/x4EBzUfu/duuwVcfT58+fcY8//zzbQ2jXZSXlxMXFxeQc0vn0/XuWXS9exZd754lWK53WY1jWXE9XxR5WLbTQ0Wdr754cHwIR6aEcmRKKIPjQ7RgUjtor2s+Y8aMhc653ObtwTTyngUcC+yTQHMIizSZ2X3AZGCyc85zqEE55x4BHgHIzc1106dPP9RDHZa8vDwCdW7pfLrePYuud8+i692zBOp613u8LN60izn5ReQVFLJsy24AUuIiOOXIvkzPSWPysBSSYiM6PbburqOveTAl738D3gN+y6HfsAqAmf0RuBCY4Zxb22TTdv9zH2Bjk/Y+TbaJiIiIdDnby6qZW+BL1uetKmZPdT2hIcbo9ARuOyWHadmpjOzXmxCNrndpwZS8DwROd86tOZyDmNkDwAX4EveVzTavw5ekn4RvRhrMLAqYAtx2OOcVERER6UzOOZZuLuPtZdvJyy9k5XbfpHl9ekdy+hH9mJaTyqRhKcRHhwc4UmlPwZS8vwuMAQ45eTezB4FLgW8BpWbW17+p3DlX7pxzZnY/8BMzWwkUAD8FyoFnDyN2ERERkU6xurCcV5ds4b9Lt7JhZyXhoUZuRhI/Om0403NSyenTS6uadmPBlLy/DfzBzI4CvmTfG1ZfasUxbvA/v9+s/ZfAXf7XdwPRwIN8vUjTyZrjXURERILV9rJqXlu6lf8u3cKyLbsJMZg4NIXvzhjGKaP6anS9Bwmm5P0h//NPWtjWqhtWnXMH/ZrpfNPr3MXXybyIiIhI0CmrrOOtZdv475KtfLpuJ87B0QPj+dmZIznrqH6kaSrHHiloknfnnGb+FxERkR6tus7D+ysK+e+SLeTlF1Hr8TIkJZbvn5DFN48ZwOCU2ECHKAEWNMm7iIiISE9UuKea2SsLeW9FIR+uKqaqzkNar0hmHpfBN48ZwBEDequGXRoFNHk3s/OBV5xzta3sfzbwP+dcZcdGJiIiItIxnHN8tXU3H6ws5P0VO1i6uQyA/vFRfHvMQE49oi8ThiRrwSRpUaBH3p8D+gJFrez/FHAMsPYg/URERESCRnWdh4/XFPP+ikI+WFnItrJqzODogQncenI2J4zow/C+miVGDi7QybsBz5hZTSv7684MERER6RK2l1UzZ1Mdzzy1gI9W+8phYiJCmZKVwi0nZTMjJ43UXpGBDlO6mEAn70+1sf8/gd0dEYiIiIjI4aj3eFm0cRez8wvJyy9ixTZfyjIgYTfn5Q7k+OFpTBiSTFT4QSfQE9mvgCbvzrnLA3l+ERERkcNRuLuavIIi5uQXMXdVEXuq6wkNMXIzErnj1OHE7dnAd86coXIYaTeBHnkXERER6TI8XseSTaXMXlnE7PxCvtrqG11P6xXJaUf0ZUZOGpOyUugd5Vs0KS9vkxJ3aVdK3kVEREQOYMfuauYUFDG3oIh5q4opq6ojNMQYnZ7AbafkMD0nlZH9NJ2jdA4l7yIiIiJN1NR7+HxdKXNX+RL2ldv3AJDaK5ITR/RhxvBUpgxLJT4mPMCRSk+k5F1ERER6NOcc64orGkfXP11bQlWdh/BQY2xmEj86bThTs1IZ0U9TOUrgKXkXERGRHmdPdR0fr9nZmLBvLq0CIDM5hvNzBzI1O5UJQ5KJjVSqJMElqH4izew04LvAEOAU59wmM7sKWOecez+w0YmIiEhX5Zxj+bbdzPHPDLNwQyn1XkdsRCjHDU3h2mlDmZaVSnpyTKBDFTmgoEnezewS4GHg78AJQEMhWShwO6DkXURERFqttKKWeauLG6dxLNrjWxNyZL/eXD11CFOzUhmTkUhEWEiAIxVpvaBJ3vEl6Fc75573j7Y3+BT4VYBiEhERkS7C43Us3byLOflFzCkoYunmXTgHCTHhTMlKZVp2KlOzUkjrrQXbpesKpuQ9C/ikhfZyoHcnxyIiIiJdwNZdVcxb5ZvC8cPVxeyqrMMMjh6YwE3HZzE9J5WjBiYQGqIbTaV7CKbkfSuQDWxo1j4VWNP54YiIiEiwqaipZ/66ncwtKGbeqiLWFFUAvkWSThjeh2k5qUwZlkJibESAIxXpGMGUvD8C/KlJycwgM5sC3A3cFbCoREREJGC8XseyrWXMW+VL1hduKKXO44gKD2H84GQuGpfO1OxUstLiNI2j9AhBk7w75+42s3jgXSAKmA3UAPc65x4MaHAiIiLSaXbsrm68yfSj1cWUVtYBMKp/b66cPIQpWSmMyUgkKjw0wJGKdL6gSd4BnHN3mtn/A0YCIcBy51x5gMMSERGRDlTv8bJ40y7y8guZvbKI5dt2A9CndyTHD+/D1OwUJg1LISUuMsCRigReUCXvAM65SmBBoOMQERGRjlO0p4Y5BUXMzi9kXkERu6vrCQ0xcjMSuePU4UzPSWV4X61oKtJc0CTvZhYJ3ADMANLwjbw3cs6NC0RcIiIicvg8XscS/+h6Xn4RX24pA3w3mp56RF9m5KQxKSuF3lHhBzmSSM8WNMk78ChwJvBfYDngAhuOiIiIHI6iPTXMLSgir6CIeauK2FVZR4jB6PREbjslh+k5qYzs11uj6yJtEEzJ+zeAbzrn5gQ6EBEREWm7prXrcwqKWLbFV7ueEhfJ8cPTmJGTxpSsFBJiNI2jyKEKpuS9ECgOdBAiIiLSetvLqv2j64XMW1XMHn/t+uj0BG47JYdp2b7R9RAtkiTSLoIpef8J8Bszu8w5VxroYERERGRfdR4vC9aXkldQyJz8IlZu3wP4ZoY57Yi+TM9JY9KwFOKjVbsu0hGCKXl/B7gWKDSz7UBd043OuSEBiUpERKSH21ZWRV5+EXn5hXy0eiflNfWEhRi5mYn86LThTMvWzDAinSWYkven8c3vfj+wA92wKiIiEhC19V4WbChhTn4ReflF5O/wja73j4/irKP7My07lUnDkumlmWFEOl0wJe8nAcc75+YfzkHMbCpwKzAG6A9c7px7ssn2J4FZzXab75ybcDjnFRER6cq27mo6ul5MRa2H8FBjbGYSPxkznOk5aWSlxWl0XSTAgil53wjUtMNx4oBl+Ebyn95Pn/eAS5u8r22H84qIiHQZNfUeFqwvZU6BL2Ev2OFb0HxAQjTfPHYA07NTmTgshbjIYEoVRCSY/kbeAtxtZjc451Yf6kGcc28Cb0LjKHtLapxz2w/1HCIiIl3RppLKxmkcP16zk8paDxGhIYwdnMh5YwYxPSeVYRpdFwlqwZS8/weIBPLNrAaob7rROde7Hc812cwKgV3AHOBO51xhOx5fREQk4KrrPHy6didzCoqYk1/E2uIKAAYlRXPu6IFMz0llwpBkYjW6LtJlmHPBcV+omTWvQ9+Lc+6pQzhmOXBjs5r3C4FKYB2QCfwaCAXGOOf2Kdsxs2uAawD69Okz5vnnn29rGO2ivLycuLi4gJxbOp+ud8+i692zdOT1ds6xo9LxZZGHL4o9rCzxUOeF8BAYnhTKUSmhHJkaSp8Y0+h6J9Hf756nva75jBkzFjrncpu3B03y3hFaSt5b6NMf2ABc4Jx76UDHy83NdQsWLGjfIFspLy+P6dOnB+Tc0vl0vXsWXe+epb2vd1llHR+vKWbuqiLmFhSzZVcVAENSYpmWk8q0bN/oelR4aLudU1pPf797nva65mbWYvIe0N+TmVmSc66k4fWB+jb0a2/Oua1mthnI6ojji4iItKd6j5elm3cxp6CYeauKWLppF14HvSLDOG5oMtdPH8rUrFTSk2MCHaqIdIBAF7kVmVk/f715MS3P7W7+9g4ZMjCzFGAAsK0jji8iInK4NpVUMqegiHmrivh49U721NQTYnDUwARunDGMqdmpHD0ogfDQkECHKiIdLNDJ+/FASZPXh13DY2ZxwDD/2xAg3cyO8Z+nBLgLeBFfsp4J/BYoBF4+3HOLiIi0h4qaej5Zs9NfClPE+p2VgG+RpDOO6seULN8iSQkxEQGOVEQ6W0CTd+fcnCav89rpsLnA7Cbvf+l/PAVcDxwJzAQS8CXws4HznXN72un8IiIibeKcY8W2Pcxd5ZsVZsGGEuo8jujwUCYMSWLWxEymZKUyNDVWN5qK9HCBHnlvZGYeoF/zKRvNLBkodM61qmzG/yXgQP+ynXLIQYqIiLSTkopa5vlvMp27qoiiPb4Jz4b37cXlkwYzNSuV3MxE3WgqInsJmuSd/SfckWgFVBER6eI8XseSTaW8uKqWPy77kC+2lOEcxEeHMyUrhanZqUzNSqVvfFSgQxWRIBbw5N3MfuB/6YDr/NM7NggFpgArOz0wERGRw1RcXsOc/CLyCny162VVdRhwbLpx8wnZTM1O4aiBCYSGqBRGRFon4Mk78D3/swFXAZ4m22qB9cB1nRyTiIhIm3m8jqWbd5GXX0RefiFfbC4DICUukhNH9GHG8FRsRz5nnDQpwJGKSFcV8OTdOTcYwMxmA+c450oDHJKIiEirlVTUMrfAl6zPKSiitLKOEINjBiXww5OymZ6Txqj+vQnxj67n5RUEOGIR6coCnrw3cM7NCHQMIiIiB9MwM8wHK3fw/spClmzahXOQFBvBjJw0puX4atcTYzWNo4i0v6BJ3gHM7ALgBCAN3xztjZxz3whIUCIi0uNV13n4eE0x768o5IOVhWwrqwbgqIHxfP+ELGbkpHHkgPjG0XURkY4SNMm7md0D3Ixv3vWttMOCTSIiIodqe1k1H6ws5IOVO/hwdTHVdV5iIkKZkpXCLSdmM314Kmm9NDOMiHSuoEne8S2cdJFz7oVAByIiIj2P1+v4cksZ7/sT9mVbdgMwMDGaC3IHccKIPowfkkRkmOZdF5HACabkPQRYEuggRESk5yitqG1c1XTuqiKKy2sJMRiTkcgdpw7nhBFpZKXFaVVTEQkawZS8PwJ8B7grwHGIiEg31TC6npdfRF5BIUs37cLrIDEmnKnZqUzPSWV6dppuNhWRoBVMyXsCcLGZnQR8AdQ13eicuykQQYmISNfWMJXjHP9CSTsrajGDowcm8L3js5iek6qFkkSkywim5H0kX5fNDG+2TTeviohIq3i8ji8272JOQRF5+UUs3fz1VI7T/KPrU7JSSdLouoh0QUGTvGuedxEROVRFe2oaR9fnrfItlGT+hZJuPiGb6TmpmspRRLqFoEneRUREWqve42Xxpl2Nq5o2zAyTEhfBjOFpTM9JY8qwFNWui0i3EzTJu5m9eqDtWqRJRKRn215WzZyCQvLyi/hwdTF7qusJDTFGpydw2yk5TMtOZWS/3hpdF5FuLWiSd2Bns/fhwNHAIOClzg9HREQCbWd5DW9+uY3/LtnKgg2lAPTpHcnpR/RjWk4qk4alEB8dHuAoRUQ6T9Ak7865y1tqN7M/ALs7ORwREQmQipp63l2+g/8u2cK8VcXUex1ZaXHcenI2J47sQ06fXpp3XUR6rKBJ3g/gb8CHwC8DHYiIiHSMOo+XuQVF/HfJVt5dvoOqOg/946O4cspgvnn0AEb0U8IuIgJdI3nPCXQAIiLS/rxex8KNpbyyeAtvfrmN0so6EmLCOXv0AL51zAByMxJVvy4i0kzQJO9m9qfmTUA/4DTg8c6PSERE2lt1nYf560qYW1DE28u2s2VXFVHhIZw0si/fOqY/U7JSiQgLCXSYIiJBK2iSd+DIZu+9QBFwC0reRUS6JOccqwvLfaubripm/tqd1NR7iQgLYdLQZG47JYeTRvYhNjKY/jsSEQleQfOv5YEWaTKzWKC+E8MREZFDVFZVx8eri30Je0ERW8uqARiaGssl4zOYmp3C+MHJREeEBjhSEZGuJ2iS95aYWRRwI3A7kBbgcEREpAVer+PLLWXM8a9wumTTLjxeR6/IMCYNS+HG41OZmp3CwMSYQIcqItLlBTx5N7MI4BfAyUAdcLdz7hUzmwn8DnDAHwMYooiINFNeU8+Hq4r5YOUOPlhZRHF5DWZw1IB4bpg+lKnZqRwzKIHwUNWvi4i0p4An78BdwHeBd4FJwH/M7FHgBODHwLPOubrAhSciIgCbSip5f8UO3l9ZyPy1JdR6vPSKCmN6ThrHD09lWnYaSbERgQ5TRKRbC4bk/XzgMufcy2Z2NLAYSARGOedU5y4iEiAer2PRxlLeX1HIByt3ULCjHIAhqbHMmpjBCSP6MCYjUaPrIiKdKBiS90HA5wDOuaVmVgv8Xom7iEjn21lew4eri8nLL2J2fiG7KusICzHGDU7igrHpHD88jcEpsYEOU0SkxwqG5D0cqGnyvg4oC1AsIiI9Sp3Hy6INpcxdVcTcgmKWbS3DOUiMCef44WmcMLwPU7JT6B0VHuhQRUSE4EjeAX5rZpX+1xHAXWa2VwLvnLupNQcys6nArcAYoD9wuXPuySbbDd8NstfgK8+ZD3zXOffV4X4IEZGuYMPOCuYWFDGnoJhP1hRTUeshNMQYnZ7AD07MZmp2KkcMiCdUq5uKiASdYEje5wJDm7z/GEhv1se14XhxwDLgaf+juduBHwKXAfnAz4F3zSzHObenDecREekSymvq+WTNTuYWFDF3VREbdvrGSgYmRvPNYwcwNSuVicOSNbouItIFBDx5d85Nb+fjvQm8CWBmTzbd5h91vxn4nXPuRX/bLKAQuBj4W3vGIiISCF6vY/m23Y2LJC3aWEqdxxEdHspxQ5O5fGImU7NTGZwSi++fRRER6SoCnrx3ssFAX+CdhgbnXJWZzQUmouRdRLqooj01zFvlS9Y/XF1McXktACP69eaKyYOZlpXKmMxEIsO0qqmISFdmzrWlIqVrMbNy4MaGmnczmwh8BGQ45zY26fc4MMA5d0oLx7gGX308ffr0GfP88893Ruj7KC8vJy4uLiDnls6n692zHMr1rvc6VpV6+bLYw7JiDxv3eAHoFQ6jUkI5MiWUUSmhJERqGsdgo7/fPYuud8/TXtd8xowZC51zuc3be9rIe5s55x4BHgHIzc1106dPD0gceXl5BOrc0vl0vXuW1l7v9cUVzF1VxJz8Ij5Zu5PKWg9hIcbojEQumJjK1KxURvXvTYhuNA1q+vvds+h69zwdfc17WvK+3f/cB9jYpL1Pk20iIkGh6Y2mcwqK2Fjiu9E0PSmGc0b7bjQ9bmgyvXSjqYhIj9HTkvd1+JL0k/AvDGVmUcAU4LYAxiUignN732i6cIPvRtOYiFAmDk3mqimDmZqVSqYWSRIR6bGCJnk3Mw/QzzlX2Kw9GSh0zrXqLisziwOG+d+GAOlmdgxQ4pzbaGb3Az8xs5VAAfBToBx4tl0+iIhIG+wsr+GTrfW8+u8lzC0oprjct2bdiH69uXLyEKZmpzAmQzeaioiIT9Ak78D+ijQjgdo2HCcXmN3k/S/9j6fwze1+NxANPMjXizSdrDneRaQz1Hm8LN64q3HO9S+3+FY0TYotYvKwFKZlpzIlO4W0XlGBDlVERIJQwJN3M/uB/6UDrvPPENMgFF9Jy8rWHs85l8f+vwjgfNPr3OV/iIh0uE0llcz1T+P48eqd7Kmp32tF09jyjVx21vG60VRERA4q4Mk78D3/swFXAZ4m22qB9cB1nRyTiMghq6r18Onanb7a9VVFrC2qAGBAQjRnHt2fadkpTByW0riiaV7eFiXuIiLSKgFP3p1zgwHMbDZwjnOuNMAhiYi0iXOOVYXlzMn3zQrz2foSauu9RIaFMGFIMt8Zn8HU7FSGpmpFUxEROTwBT94bOOdmBDoGEZHWKquq4+PVxczxT+O4rawagKy0OGZO8CXr4wYnERWuG01FRKT9BDR5N7M/AT92zlX4X++Xc+6mTgpLRGQfXq9j2dayxtH1xZt24fE6ekWGMTkrhe+fkMrU7FT6J0QHOlQREenGAj3yfiQQ3uS1iEjQKC6vYZ5/RdO5q4opqfBNfHXkgHiunzaUaTmpHDMogfDQkABHKiIiPUVAk/empTIqmxGRQKvzeFm4obRxGsdlW3YDkBwbwbTsVKZlpzI5K4WUuMgARyoiIj1VoEfeG5nZOOfcZ/vZ9h3n3DOdHZOIdH+bSiob69Y/WbOTcv80jmPSE7n15GymZqdyRP94zQYjIiJBIWiSd+B1M5vqnNtrTnczuxR4GFDyLiKHrbK2nk/X7mRuQTFzC4pYW/z1NI7fOKY/U7NSmTgsuXEaRxERkWASTMn7H4B3zGyic24zgJnNBP4KXBDQyESky3LOsXL7nsZSmM/XlVLr8RIV7p/GcUIG03JSGZKiaRxFRCT4BU3y7pz7vZmlAu+Z2WTgDHyJ+3nOuTcCG52IdCU7y2v40D+N47xVxRTtqQEgu08csyb6pnEcm6lpHEVEpOsJmuQdwDl3q5klA/OBvsC3nXNvBjgsEQlydR4vizaUMndVEXMLilm2tQznICEmnMnDUpiancrUrFT6xkcFOlQREZHDEuh53s9poflN4ATgOSCqoY9z7qXOjE1EgtumkkryCoqY2+xG02MHJXDLib4bTY8cEE+objQVEZFuJNAj7y8cYNsV/geAA/T7bZEerKbew4L1pcxeWcjs/ELWFH19o+lZR/dnWnYKxw1NIT5aN5qKiEj3Feh53rWyiYjs15ZdVeTlF5KXX8RHq4uprPUQERrC+CFJXDw+g+m60VRERHqYQI+8H5CZhTvn6gIdh4h0jjqPlwXrSxsT9vwdewDf6Po5owcwPTuNicOSiYkI6n+6REREOkzQ/A9oZjcBW5xzL/rfPw7MNLM1wDecc/kBDVBEOsTm0srGOdc/Wl3Mnpp6wkONsZlJ3DlmBDOGpzI0NU6j6yIiIgRR8g7chL/G3cymAucBFwPn4psD/szAhSYi7aW6zsOna3cyx3+zaUPtev/4KM48uh/Tc9KYNCyFuMhg+udJREQkOATT/44DgHX+12cB/3HO/dvMvgTmBS4sETkczjlWFZYzt6CIOQVFzF9XQm29l8iwEMYPSeaicelMz9HouoiISGsEU/K+G0gDNgEnAff42+sATc4s0oWUVdXx0epi5uT7VjXdVlYNwLC0OL4z3rei6fjBWiRJRESkrYIpeX8HeNTMFgHDgLf87aP4ekReRIJQw+j67JWFfLCykAUbSvF4Hb0iw5g0LIWbTkhlanYqAxKiAx2qiIhIlxZMyft3gf8HpONbWbXE3z4a34JNIhJEqmo9fLK2mA9WFjJ7ZRFbdlUBMLxvL66ZOoQZOWkcm55AeKhmhBUREWkvQZO8O+d2A99rof0XAQhHRFqwqaSS2fm+0fVP1uykpt5LdHgok4al8N0Zw5iek0p/ja6LiIh0mIAm72aW1DDCbmZJB+rbZCReRDpJdZ2Hz9aVMG9VEbPzi1hdWA5AZnIMF49PZ0ZOGuOHJBEZptp1ERGRzhDokfciM+vnnCsEigHXQh/ztys7EOlgzjnyd+xhXkExc1d9PTNMRGgI4wYncdG4dI4fnsbglNhAhyoiItIjBTp5Px5oGFGfEchARHqq4vIaPlpdzNyCYuatKqJwTw0AWWlxXDohgylZKYwfnEx0hL4/i4iIBFpAk3fn3JyWXotIx/F4HZ+vL2FOQRHzVhWxbMtuABJiwpk8LIWp2alMyUqhX7xq10VERIJNoEfeG5lZf3yrqeYAtUA+8G/nXGlAAxPpJvK37+GlxZv57+KtbN9dTViIMTojkVtPzmZqdiqj+scTGqJFkkRERIJZUCTvZnYtcD8QiW+xJoDewH1mdqVz7nl/v2Occ0sCEqRIF1S0p4b/LtnCy4u38NXW3YSFGNOyU/npmSOYlp1Kr6jwQIcoIiIibRDw5N3MTgUeBP4M3OOc2+pv7w/cAfzDzDYCNwAFwJLDPN9dQPPpJ3c45/oeznFFgkV1nYd3lu/gpUWbmbeqGI/XcdTAeH5x1kjOOro/KXGRgQ5RREREDlHAk3fgNuBe59yPmjb6k/jvm1kV8B6wE7irnc6ZD0xv8t7TTscVCQiv1zF/XQkvL97Mm19up7ymnv7xUVw7dQjnjB7AsLRegQ5RRERE2kEwJO+5wI0H2P4UcDswwzm3up3OWe+c295OxxIJiNp6LwvWl/DBykLeWradLbuqiI0I5bQj+3HO6AFMGJxMiGrYRUREupVgSN7DgaoDbK8CqtsxcQcYYmZbgRpgPvAT59zadjy+SIco2lNDXn4hs/MLmVtQTHlNPRGhIUwclsztp+Zw8si+mtJRRESkGzPnWloXqRMDMFsM/NU598h+tl8LXOecO7adznca0AtYCaQBPwWGA6Occztb6H8NcA1Anz59xjz//PPtEUablZeXExcXF5BzS+druN5e59iw28vSIg9LizysK/MCkBBpHJUayjGpoYxMDiUqTCPsXZn+fvcsut49i653z9Ne13zGjBkLnXO5zduDIXn/HvBLYJZz7rVm274BPAn83Dn3lw46fxywFvidc+6+A/XNzc11CxYs6IgwDiovL4/p06cH5NzSucpr6nn45Tx2hKaSV1BE0Z4azOCYQQkcn5PGjOFpjOrfGzMl7N2F/n73LLrePYuud8/TXtfczFpM3oOhbOYvwCTgv2ZWAKzwt48AsvDN9d4hiTuAc67czL7yn0skIGrqPcxeWcR/l2zh/ZWF1NZ76R21nanZqRw/PI1p2akka5YYERGRHi/gybvzDf1faGYvARfjW6QJfGUtP3PO/acjz29mUfjKZmZ35HlEmvN6HZ+tL+G/S7bwxhfb2F1dT0pcBBePS6df/Xau/OYMwkJDAh2miIiIBJGAJ+8NnHP/Bv7d0ecxs3uB14CN+GrefwbE4pvVRqTDrdy+m1cWb+XVJVvYWlZNTEQop4zqyzeP6c/kYSmEhYaQl1ekxF1ERET2ETTJeycaCDwHpABFwKfABOfchoBGJd3a1l1VvLp0K68s3sLK7XsIDTGmZqVwx2nDOWlkH2IieuJfRREREWmrHpcxOOcuDHQM0v0551hXXEFefhHvLN/O/HUlOAfHpifwy2+M4oyj+mmlUxEREWmzHpe8i3SUqloPn6wtJi+/iLz8IjaWVAIwNDWWm0/I5lvH9icjOTbAUYqIiEhXpuRd5BA551jrH13Pyy9k/roSauu9RIWHMGloCldPGcy07DTSk2MCHaqIiIh0E0reRdqg6ej67PxCNpX4FgcekhrLd8ZnMD0nlXGDk4gK1yqnIiIi0v4Cmryb2Z9a29c5d1NHxiKyP+uKK5i9spDZTUbXo8NDmTg0mWumDGF6ThqDkjS6LiIiIh0v0CPvR7ayX2CXgZUepbrOwydrd5K3spC8giI27PTVrg9JjeXSCb7R9bGZGl0XERGRzhfQ5N05NyOQ5xdpsGGnb3Q9r6CIT9bspMZfuz5xaApXTh7MdNWui4iISBAI9Mi7SEBU13n4dO1O5hT4ZoZZV1wBwOCUWC4al86M4WmMV+26iIiIBJmgSt7NLBv4NpAORDTd5py7IiBBSbfgnGNNUQVzCoqYU1DE/LW+0fWIsBCOG5LMrOMymJ6TRmaKpnIUERGR4BU0ybuZnQG8CCwGxgCfA0OBSGBeAEOTLmpPdR0fr/GNrs/JL2LLrq9nhrl4fDrTslMZPziZ6AiNrouIiEjXEDTJO/Ar4JfOud+a2R7gUmAr8A/gk4BGJl2Cc44V2/aQV1DInPwiFm4opd7riI0IZdKwFK6fPpRp2amaGUZERES6rGBK3nOAf/lf1wExzrlqM/sV8AZwX8Aik6C1q7KWeauKG8thivbUADCyX2+unjqEadmpjE5PJCIsJMCRioiIiBy+YEre9wBR/tfbgGHAMnwxJgYqKAkuXq/jyy1l/htNC1myaRdeBwkx4UzJSmVqVgrTslNJ6x118IOJiIiIdDHBlLzPByYDy/GNtP/BzI4GzkZlMz3azvIa5q7y1a3PXVVMSUUtZnDUwARuPD6L6TmpHD0wgdAQC3SoIiIiIh0qmJL3HwBx/td3Ab2Ac4EC/zbpIeo9XpZu3sWc/CLyCor4cksZzkFybATTslOZnpPK5GEpJMdFBjpUERERkU4VNMm7c25tk9eVwPUBDEc62bayKub669Y/XFXM7up6QgyOTU/kBydmMy0nlSP6xxOi0XURERHpwYImeTeztcBY59zOZu0JwCLn3JCABCYdoqbew4L1pY3TOObv2ANAn96RnHpEX6ZlpzF5WArxMeEBjlREREQkeARN8g5kAi1NuB0JDOjcUKQjrC/2LZI0t6CIj9fspKrOQ3ioMW5wEueOGc607DSy+8RhptF1ERERkZYEPHk3s3OavD3DzMqavA8FTgDWd2pQ0i4qa/9/e3cfZFV933H8/VmeRCCI7LLiA/K4oFWLig4qDysNCY2Z1jQPxjREnRGT0FibxNSY0ZR0Jp02tUZTdYyZZKRpLGnTGEtTq9GAIBAVrFVUnoQAirDL47qALLt8+8c5i7dXiKi799x77uc1c+fe+/udPb8vfLnLd3/7O+fXzrLOTZLWNLNxxz4Ahp14PJ+ccCpTG+qYOHIw/fpk/s/QzMzMrCKUQ9X0s/Q5gB8W9R0kKdy/WsqA7L2JCNZsa+WJNU08saaZZzbsoq3jEH179eCiUYO55uLhNI4dwvDaflmHamZmZlaRMi/eI6IGQNIGkjXv2zMOyd6FPfsPsmTddp5Yncyub215E4Cx9QO4+pLhTBlTx4Thgziu15FWRJmZmZnZu5F58d4pIkZkHYO9s4jgxS0tLFiVzK7/z+bddBwKBhzXk8npBklTGuoYOrBv1qGamZmZ5U7ZFO8Aki4DbgLOJFlG8xLwdxHxX5kGVuX2tbWzZN0Ofr1qGwtWvTW7fs6pA5ndOIqpDXWMP+0EevaoyThSMzMzs3wrm+Jd0rXAPcBPgLlp82TgQUlfjIgfZRZcFdq8cx8LVjfx+MtNLFu/g7b2Q/Tvk8yuTxs3hMaxQ6gb4E2SzMzMzEqpbIp3khn3r0TEXQVtP5S0Avg64OK9G7V3HOLZTbt5fNU2FqxqYs22VgBG1PZj5sTTmTZuCBcMP5HePT27bmZmZpaVcirehwH/fYT2h4HbShxLVWhqefPwbRwXr93Onv0H6VmT3Hf9UxNOY9q4IYys6591mGZmZmaWKqfifRMwHVhX1P4hYGPpw8mftvZDrNi463DB/vLrLQDUDejDB8+o5w/OGMKkMbV84DjvampmZmZWjjIv3iX9CLiBZHb9HyWdByxNuy8BZgLXZxRexdu8c9/hYn3puu3sbeugZ42YMHwQN80Yx9SGOs4YOsC7mpqZmZlVgMyLd+Aq4OsR8X1JTSQbMnXuuvoy8KmIeCiz6CrM/rYOntrw1q6m65v3AnDqoL5cfu4pTG2o4+LRtfT3rqZmZmZmFaccKrjDU74R8SDwYEkGlWYDXwOGAi8CfxERi0sxdleKCFZve4NFa5pZtGY7T/92J23th+jTs4aLRg1m5sTTmdJQx8jafp5dNzMzM6tw5VC8Q3JP95KRdAVwJzAbeDJ9fljSmRGxqZSxvBc797axeG1SrC9e20zTGwcAaKjvz+fSYv3CESd6V1MzMzOznCmX4n3rO80KR0RXVqJfAe6PiB+k76+XNAP4InBzF47TJQ52HGL1zg6WP7KaRWubeeG1PUTACcf3YtLoWqY01DF5TK13NTUzMzPLuXIp3q8DdpdiIEm9gfN5++0nHwUuLkUM78ayV3Yw65+W03qgnR41r3DuaSfw5Q82MKWhjrNPGUiPGi+FMTMzM6sWiijpipW3ByAdAk6KiKYSjXcy8BowNSIWFbR/E/jTiBhbdPx1JD9cUF9ff/68efNKEeZhLQeCn69tY3T/g5x7Sj/69XKxXg1aW1vp39/32K8Wznd1cb6ri/Ndfboq55deeumKiJhQ3F4OM+/Z/vTwDiLiPuA+gAkTJkRjY2PJY/ijD8PChQvJYmzLhvNdXZzv6uJ8Vxfnu/p0d87LYa/7Uk8lbwc6gPqi9npga4ljMTMzMzM7ZpkX7xFRU6olM+l4bcAKkt1cC03nrc2hzMzMzMzKTjksm8nC7cCPJT0NLAG+AJwM3JtpVGZmZmZmv0NVFu8R8VNJg4FbSDZpWgl8JCI2ZhuZmZmZmdnRVWXxDhAR9wD3ZB2HmZmZmdmxynzNu5mZmZmZHRsX72ZmZmZmFSLzTZoqiaRmIKt18bUkt7m06uB8Vxfnu7o439XF+a4+XZXz0yOirrjRxXuFkLT8SLtsWT4539XF+a4uznd1cb6rT3fn3MtmzMzMzMwqhIt3MzMzM7MK4eK9ctyXdQBWUs53dXG+q4vzXV2c7+rTrTn3mnczMzMzswrhmXczMzMzswrh4t3MzMzMrEK4eC9zkmZL2iDpTUkrJE3OOiZ7/yRNkfQfkl6TFJKuLuqXpDmStkjaL2mhpN/LKFx7nyTdLOkZSS2SmiXNl3RW0THOeU5I+jNJz6f5bpG0TNJlBf3OdY6ln/eQdFdBm3OeI2kuo+ixtaC/W/Pt4r2MSboCuBP4G+BcYCnwsKRhmQZmXaE/sBK4Adh/hP6/BL4KXA9cADQBv5I0oGQRWldqBO4BLgamAe3AY5JOLDjGOc+PV4GbgPOACcCvgV9IOiftd65zStJE4Drg+aIu5zx/VgNDCx5nF/R1a759wWoZk/QU8HxEzCpoWwv8LCJuzi4y60qSWoEvRcT96XsBW4C7IuLbaVtfkg//jRHx/axita4hqT+wB7g8IuY75/knaSdwM8ldKJzrHJI0EHgWuBb4K2BlRHzJn+/8kTQH+EREnHWEvm7Pt2fey5Sk3sD5wKNFXY+SzN5Zfo0ATqIg9xGxH1iEc58XA0i+/+5K3zvnOSWph6RPk/y2bSnOdZ7dRzK5tqCo3TnPp5HpspgNkuZJGpm2d3u+XbyXr1qgB7CtqH0byT8Ky6/O/Dr3+XUn8BywLH3vnOeMpLPT36odAO4FPhYRL+Bc55KkWcBo4JYjdDvn+fMUcDUwA5hFkselkgZTgnz37IqTmJnZsZF0OzAJmBQRHVnHY91mNTAeGAh8ApgrqTHDeKybSBpLcm3apIg4mHU81v0i4uHC95J+A6wHrgJ+093je+a9fG0HOoD6ovZ6YOvbD7cc6cyvc58zkr4LXAlMi4j1BV3Oec5ERFtErIuIFek1Ss8BX8a5zqOLSH5b/qKkdkntwFRgdvp6R3qcc55TEdEKvAiMoQSfcRfvZSoi2oAVwPSirukk6yYtvzaQfMAP517SccBknPuKJelO3ircVxV1O+f5VwP0wbnOo1+Q3GlkfMFjOTAvfb0G5zzX0nyOA16nBJ9xL5spb7cDP5b0NLAE+AJwMsn6Satg6d1GRqdva4BhksYDOyNik6Q7gG9IWkXyjf8WoBV4IINw7X2SdDcwE7gc2CWpc91ja0S0RkQ45/kh6W+BXwKbSS5O/gzJ7UIvc67zJyJ2A7sL2yTtJfl+vjJ9fwfOeW5Iug2YD2wChgC3Av2AuaX4jLt4L2MR8dP04odbSO4huhL4SERszDYy6wITgMI7EnwrfcwluQjmO0Bf4G5gEMnFMR+KiDdKG6Z1kdnp8+NF7d8C5qSvnfP8OAn45/R5D8k9v/8wIh5J+53r6uOc58upwL+QLJdqJlnnPrGgPuvWfPs+72ZmZmZmFcJr3s3MzMzMKoSLdzMzMzOzCuHi3czMzMysQrh4NzMzMzOrEC7ezczMzMwqhIt3MzMzM7MK4eLdzMwyJ2mOpJVZx2FmVu58n3czsyoj6X6gNiI+Wvi6RGMPJ9k+/IKIWF7Q3h/oExE7ShGHmVml8g6rZmb2vknqCXTEe5wRiohWku3Dzczsd/CyGTOzKiVpDnAVcJmkSB+Nad8pkuZJ2pU+filpTOHXSlop6WpJrwAHgH6SZkhanH7NTkmPSDqjYNgN6fMz6XgLC89XcP4aSbdK2izpgKQXJP1xQf/w9Os/LulXkvZJeknS9O752zIzKw8u3s3MqtdtwL8CjwFD08dSSccDC4A3ganARcDrwGNpX6cRwGeATwK/nx7fD7gDuBBoBPYA8yX1Tr/mwvR5RjrenxwlthuArwE3AWcDDwI/lzS+6LhvA99Lx38GmJcuwTEzyyUvmzEzq1IR0SppP3AgIrZ2tkv6LCDgms5lMJI+DzQBHyUp+AF6AzMjYlvBaf+9cAxJ1wAtJEX7k0Bz2rWjcMwjuBG4LSIeSN9/U9KUtP2zBcd9NyLmp2N9A/gcMD4dy8wsdzzzbmZmxc4nmVV/Q1KrpFaSGfRBwKiC414tKtyRNErSA5JekdQCbCP5v2bYsQ4u6QPAycCSoq4ngTOL2p4veL0lfR5yrGOZmVUaz7ybmVmxGuA54NNH6NtZ8HrvEfr/E3gV+DzwGtAOvEQyS98Vii+IPXi4IyIkgSemzCzHXLybmVW3NqBHUduzwJXA9ojYfawnkjQYGAfMjogFadt5/P//a9rS5+IxD4uIFklbgEuAxwu6JpH8IGBmVrU8O2FmVt1+C5wlaaykWkm9gJ+QLHd5SNJUSSMkTZH0D4V3nDmCXcB2YJak0ZKmAveSzL53agL2Ax+WVC9p4FHO9ffAjZKulNQg6a+BySQX2ZqZVS0X72Zm1e0HwMvAcpKLSS+JiH3AFGA98G/AKmAuyZr3XUc7UUQcAq4AzgFWAncDt5LcRrLzmHbgz4FrSdaoP3SU032PpID/TnqujwEfj4j/fY9/TjOzXPAOq2ZmZmZmFcIz72ZmZmZmFcLFu5mZmZlZhXDxbmZmZmZWIVy8m5mZmZlVCBfvZmZmZmYVwsW7mZmZmVmFcPFuZmZmZlYhXLybmZmZmVUIF+9mZmZmZhXi/wAjOyLjAr/uxQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt \n", + "\n", + "plt.rcParams['font.size'] = 14\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(12,8))\n", + "\n", + "# plot loss and reference value\n", + "ax1.plot(loss, label='Qiskit Runtime VQE')\n", + "ax1.axhline(y=target_energy, color='tab:red', ls='--', label='Target energy')\n", + "\n", + "# plot time taken\n", + "ax2.plot(np.cumsum(runtimes_in_min))\n", + "\n", + "# settings\n", + "ax1.set_title('Qiskit Runtime VQE')\n", + "ax1.set_ylabel('Eigenvalue')\n", + "ax1.legend(loc='best')\n", + "ax2.set_ylabel('Total Qiskit Runtime [min]')\n", + "ax2.set_xlabel('Iteration')\n", + "ax2.grid();\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Qiskit Runtime VQE: Direct call\n", + "\n", + "Instead of interacting with the Qiskit Runtime via the `VQEProgram` we can also directly call the the Qiskit Program in the cloud -- like running the Quantum Kernel Alignment (QKA) or Circuit Runner. Under the hood, `VQEProgram` is nothing but a proxy that takes care of creating the dictionaries interaction with the Qiskit Program running in the Qiskit Runtime on the cloud." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To call the VQE code on the Qiskit Runtime on the cloud, we interact with the `run` method of the provider\n", + "```\n", + "provider.run(progam_id, inputs, options, callback)\n", + "```\n", + "where \n", + "* `program_id` determines the Qiskit Runtime program run, i.e. `'vqe'` for the VQE\n", + "* `inputs` contains the input for the VQE algorithm\n", + "* `options` specifies the backend\n", + "* `callback` is the callback used inside the VQE" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's gather all the inputs to the VQE in a dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "vqe_inputs = {\n", + " 'ansatz': ansatz,\n", + " 'operator': hamiltonian,\n", + " 'optimizer': {'name': 'SPSA', 'maxiter': 5}, # let's only do a few iterations!\n", + " 'initial_point': initial_point,\n", + " 'measurement_error_mitigation': True,\n", + " 'shots': 1024\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The backend options only need to contain the name of the backend." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "backend_options = {\n", + " 'backend_name': backend.name()\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The callback acts a little different than before. Instead of only being passed the VQE callback arguments, it is passed a tuple with the job ID as first argument, followed by the VQE arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "intermediate_info = {\n", + " 'nfev': [],\n", + " 'parameters': [],\n", + " 'energy': [],\n", + " 'stddev': []\n", + "}\n", + "\n", + "def raw_callback(*args):\n", + " job_id, (nfev, parameters, energy, stddev) = args\n", + " intermediate_info['nfev'].append(nfev)\n", + " intermediate_info['parameters'].append(parameters)\n", + " intermediate_info['energy'].append(energy)\n", + " intermediate_info['stddev'].append(stddev)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can call the Qiskit Runtime VQE directly, without going through the convenience of the `VQEProgram`. \n", + "Note, that the result will not be of the same type as `VQE` or `VQEProgram`, but a plain dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: c2d4ut8likuqc7s4nqog\n" + ] + } + ], + "source": [ + "job = provider.runtime.run(\n", + " program_id='vqe',\n", + " inputs=vqe_inputs,\n", + " options=backend_options,\n", + " callback=raw_callback\n", + ")\n", + "print('Job ID:', job.job_id())" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "result = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reached -0.0805234521211203 after 16 evaluations.\n" + ] + } + ], + "source": [ + "print(f'Reached {result[\"optimal_value\"]} after {result[\"optimizer_evals\"]} evaluations.')" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available keys: ['optimizer_evals', 'optimizer_time', 'optimal_value', 'optimal_point', 'optimal_parameters', 'cost_function_evals', 'eigenstate', 'eigenvalue', 'aux_operator_eigenvalues', 'optimizer_history']\n" + ] + } + ], + "source": [ + "print('Available keys:', list(result.keys()))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}