diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000..3af52d88e
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,9 @@
+if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
+ source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
+fi
+
+watch_file flake.nix
+watch_file flake.lock
+if ! use flake . --impure; then
+ echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
+fi
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..f3d5c415e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..11fc491ef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/general-issue.md b/.github/ISSUE_TEMPLATE/general-issue.md
new file mode 100644
index 000000000..f61b1d25e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/general-issue.md
@@ -0,0 +1,8 @@
+---
+name: General issue
+about: Default GitHub template issue
+title: ''
+labels: ''
+assignees: ''
+
+---
diff --git a/.github/ISSUE_TEMPLATE/general-purprose-issue.md b/.github/ISSUE_TEMPLATE/general-purprose-issue.md
new file mode 100644
index 000000000..4c678ccff
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/general-purprose-issue.md
@@ -0,0 +1,8 @@
+---
+name: General purprose issue
+about: ''
+title: ''
+labels: ''
+assignees: ''
+
+---
diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 000000000..5571132e2
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,8 @@
+coverage:
+ status:
+ project:
+ default:
+ threshold: 0.5%
+
+github_checks:
+ annotations: false
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..ad2863c0c
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,6 @@
+
+Checklist:
+- [ ] Reviewers confirm new code works as expected.
+- [ ] Tests are passing.
+- [ ] Coverage does not decrease.
+- [ ] Documentation is updated.
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 000000000..23566a04f
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,3 @@
+template: |
+ ## What's Changed
+ $CHANGES
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 000000000..aa20172f8
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,24 @@
+# A single CI script with github workflow
+name: Build wheels and deploy
+
+on:
+ workflow_dispatch:
+ push:
+ merge_group:
+ release:
+ types:
+ - published
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ python-version: [ 3.9, '3.10', '3.11', '3.12']
+ uses: qiboteam/workflows/.github/workflows/deploy-pip-poetry.yml@v1
+ with:
+ os: ${{ matrix.os }}
+ python-version: ${{ matrix.python-version }}
+ publish: ${{ github.event_name == 'release' && github.event.action == 'published' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' }}
+ poetry-extras: "--with tests,docs"
+ secrets: inherit
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 000000000..ab3f1beb0
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,38 @@
+name: docs
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [master]
+ tags:
+ - "*"
+
+jobs:
+ evaluate-label:
+ runs-on: ubuntu-latest
+ outputs:
+ label: ${{ steps.label_step.outputs.version}}
+ steps:
+ - name: checks for the label
+ id: label_step
+ run: |
+ if [[ "${{ github.ref }}" == "refs/heads/master" ]]; then
+ echo "version=latest" >> $GITHUB_OUTPUT
+ fi
+ if [[ "${{ github.ref_type }}" == "branch" ]] && [[ "${{ github.ref }}" != "refs/heads/master" ]]; then
+ exit 1
+ fi
+ if [[ "${{ github.ref_type }}" == "tag" ]]; then
+ echo "version=stable" >> $GITHUB_OUTPUT
+ fi
+
+ deploy-docs:
+ needs: [evaluate-label]
+ uses: qiboteam/workflows/.github/workflows/deploy-ghpages-latest-stable.yml@v1
+ with:
+ python-version: "3.10"
+ package-manager: "poetry"
+ dependency-path: "**/poetry.lock"
+ trigger-label: "${{needs.evaluate-label.outputs.label}}"
+ project: qibo
+ poetry-extras: --with docs
diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml
new file mode 100644
index 000000000..1308a9180
--- /dev/null
+++ b/.github/workflows/rules.yml
@@ -0,0 +1,24 @@
+# A single CI script with github workflow
+name: Tests
+
+on:
+ workflow_dispatch:
+ push:
+ merge_group:
+ pull_request:
+ types: [labeled, opened] # opened is required to allow external contributors
+
+jobs:
+ build:
+ if: contains(github.event.pull_request.labels.*.name, 'run-workflow') || github.event_name == 'push'
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ python-version: [3.9, "3.10", "3.11", "3.12"]
+ uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@v1
+ with:
+ os: ${{ matrix.os }}
+ python-version: ${{ matrix.python-version }}
+ doctests: ${{ matrix.os == 'ubuntu-latest'}}
+ poetry-extras: "--with tests,docs"
+ secrets: inherit
diff --git a/.github/workflows/selfhosted.yml b/.github/workflows/selfhosted.yml
new file mode 100644
index 000000000..d46b55142
--- /dev/null
+++ b/.github/workflows/selfhosted.yml
@@ -0,0 +1,23 @@
+# CI workflow that runs on selfhosted GPU
+name: Tests with gpu
+
+on:
+ pull_request:
+ types: [labeled]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ if: contains(join(github.event.pull_request.labels.*.name), 'run-on')
+ uses: qiboteam/workflows/.github/workflows/selfhosted.yml@v1
+ with:
+ used-labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}
+ python-version: "3.10"
+ artifact-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ poetry-extras: "--with cuda11,tests"
+
+ secrets:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 82f927558..b5b3aa091 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,13 @@
# Byte-compiled / optimized / DLL files
+.vscode
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
+*.o
+*.cudao
# Distribution / packaging
.Python
@@ -20,6 +23,7 @@ parts/
sdist/
var/
wheels/
+pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
@@ -72,7 +76,6 @@ instance/
docs/_build/
# PyBuilder
-.pybuilder/
target/
# Jupyter Notebook
@@ -83,9 +86,7 @@ profile_default/
ipython_config.py
# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
+.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@@ -94,24 +95,7 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
@@ -124,6 +108,7 @@ celerybeat.pid
# Environments
.env
.venv
+.idea
env/
venv/
ENV/
@@ -148,15 +133,10 @@ dmypy.json
# Pyre type checker
.pyre/
-# pytype static type analyzer
-.pytype/
+# tmp files
+tmp/
+tmp.npy
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+# Mac
+.DS_Store
+.devenv
diff --git a/.history/.gitignore_20241129083618 b/.history/.gitignore_20241129083618
new file mode 100644
index 000000000..477767ec8
--- /dev/null
+++ b/.history/.gitignore_20241129083618
@@ -0,0 +1,203 @@
+# Byte-compiled / optimized / DLL files
+<<<<<<< HEAD
+=======
+.vscode
+>>>>>>> origin/qdp
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+<<<<<<< HEAD
+=======
+*.o
+*.cudao
+>>>>>>> origin/qdp
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+<<<<<<< HEAD
+=======
+pip-wheel-metadata/
+>>>>>>> origin/qdp
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+<<<<<<< HEAD
+cover/
+=======
+>>>>>>> origin/qdp
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+<<<<<<< HEAD
+.pybuilder/
+=======
+>>>>>>> origin/qdp
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+<<<<<<< HEAD
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+=======
+.python-version
+>>>>>>> origin/qdp
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+<<<<<<< HEAD
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+=======
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+>>>>>>> origin/qdp
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+<<<<<<< HEAD
+=======
+.idea
+>>>>>>> origin/qdp
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+<<<<<<< HEAD
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+=======
+# tmp files
+tmp/
+tmp.npy
+
+# Mac
+.DS_Store
+.devenv
+>>>>>>> origin/qdp
diff --git a/.history/.gitignore_20241129083847 b/.history/.gitignore_20241129083847
new file mode 100644
index 000000000..b5b3aa091
--- /dev/null
+++ b/.history/.gitignore_20241129083847
@@ -0,0 +1,142 @@
+# Byte-compiled / optimized / DLL files
+.vscode
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+*.o
+*.cudao
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+.idea
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# tmp files
+tmp/
+tmp.npy
+
+# Mac
+.DS_Store
+.devenv
diff --git a/.history/LICENSE_20241129083618 b/.history/LICENSE_20241129083618
new file mode 100644
index 000000000..f301b39fc
--- /dev/null
+++ b/.history/LICENSE_20241129083618
@@ -0,0 +1,209 @@
+<<<<<<< HEAD
+ Apache License
+=======
+ Apache License
+>>>>>>> origin/qdp
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+<<<<<<< HEAD
+ Copyright [yyyy] [name of copyright owner]
+=======
+ Copyright 2020 The QIBO team
+>>>>>>> origin/qdp
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/.history/LICENSE_20241129083850 b/.history/LICENSE_20241129083850
new file mode 100644
index 000000000..78ed7827f
--- /dev/null
+++ b/.history/LICENSE_20241129083850
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2020 The QIBO team
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/.history/README_20241129083618.md b/.history/README_20241129083618.md
new file mode 100644
index 000000000..02ed4633e
--- /dev/null
+++ b/.history/README_20241129083618.md
@@ -0,0 +1,84 @@
+<<<<<<< HEAD
+# qibo-qdp
+=======
+![Logo](https://github.com/qiboteam/qibo/blob/master/doc/source/_static/qibo_logo_dark.svg)
+
+[![codecov](https://codecov.io/gh/qiboteam/qibo/branch/master/graph/badge.svg?token=1EKZKVEVX0)](https://codecov.io/gh/qiboteam/qibo)
+![PyPI - Version](https://img.shields.io/pypi/v/qibo)
+![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qibo)
+
+Qibo is an open-source full stack API for quantum simulation and quantum hardware control.
+
+Some of the key features of Qibo are:
+- Definition of a standard language for the construction and execution of quantum circuits with device agnostic approach to simulation and quantum hardware control based on plug and play backend drivers.
+- A continuously growing code-base of quantum algorithms applications presented with examples and tutorials.
+- Efficient simulation backends with GPU, multi-GPU and CPU with multi-threading support.
+- Simple mechanism for the implementation of new simulation and hardware backend drivers.
+
+## Documentation
+
+[![docs](https://github.com/qiboteam/qibo/actions/workflows/publish.yml/badge.svg)](https://qibo.science/qibo/stable/)
+
+Qibo documentation is available [here](https://qibo.science).
+
+## Minimum Working Examples
+
+A simple [Quantum Fourier Transform (QFT)](https://en.wikipedia.org/wiki/Quantum_Fourier_transform) example to test your installation:
+```python
+from qibo.models import QFT
+
+# Create a QFT circuit with 15 qubits
+circuit = QFT(15)
+
+# Simulate final state wavefunction default initial state is |00>
+final_state = circuit()
+```
+
+Here another example with more gates and shots simulation:
+
+```python
+import numpy as np
+from qibo import Circuit, gates
+
+c = Circuit(2)
+c.add(gates.X(0))
+
+# Add a measurement register on both qubits
+c.add(gates.M(0, 1))
+
+# Execute the circuit with the default initial state |00>.
+result = c(nshots=100)
+```
+
+In both cases, the simulation will run in a single device CPU or GPU in double precision `complex128`.
+
+## Citation policy
+[![arXiv](https://img.shields.io/badge/arXiv-2009.01845-b31b1b.svg)](https://arxiv.org/abs/2009.01845)
+[![DOI](https://zenodo.org/badge/241307936.svg)](https://zenodo.org/badge/latestdoi/241307936)
+
+If you use the package please refer to [the documentation](https://qibo.science/qibo/stable/appendix/citing-qibo.html#publications) for citation instructions.
+
+## Contacts
+
+To get in touch with the community and the developers, consider joining the Qibo workspace on Matrix:
+
+[![Matrix](https://img.shields.io/matrix/qibo%3Amatrix.org?logo=matrix)](https://matrix.to/#/#qibo:matrix.org)
+
+If you have a question about the project, please contact us with [📫](mailto:qiboteam@qibo.science).
+
+## Supporters and collaborators
+
+- Quantum Research Center, Technology Innovation Institute (TII), United Arab Emirates
+- Università degli Studi di Milano (UNIMI), Italy.
+- Istituto Nazionale di Fisica Nucleare (INFN), Italy.
+- Università degli Studi di Milano-Bicocca (UNIMIB), Italy.
+- European Organization for Nuclear research (CERN), Switzerland.
+- Universitat de Barcelona (UB), Spain.
+- Barcelona Supercomputing Center (BSC), Spain.
+- Qilimanjaro Quantum Tech, Spain.
+- Centre for Quantum Technologies (CQT), Singapore.
+- Institute of High Performance Computing (IHPC), Singapore.
+- National Supercomputing Centre (NSCC), Singapore.
+- RIKEN Center for Computational Science (R-CCS), Japan.
+- NVIDIA (cuQuantum), USA.
+>>>>>>> origin/qdp
diff --git a/.history/README_20241129083659.md b/.history/README_20241129083659.md
new file mode 100644
index 000000000..a824cf72f
--- /dev/null
+++ b/.history/README_20241129083659.md
@@ -0,0 +1 @@
+# qibo-qdp
\ No newline at end of file
diff --git a/.history/README_20241129083845.md b/.history/README_20241129083845.md
new file mode 100644
index 000000000..7f5385f2f
--- /dev/null
+++ b/.history/README_20241129083845.md
@@ -0,0 +1,80 @@
+![Logo](https://github.com/qiboteam/qibo/blob/master/doc/source/_static/qibo_logo_dark.svg)
+
+[![codecov](https://codecov.io/gh/qiboteam/qibo/branch/master/graph/badge.svg?token=1EKZKVEVX0)](https://codecov.io/gh/qiboteam/qibo)
+![PyPI - Version](https://img.shields.io/pypi/v/qibo)
+![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qibo)
+
+Qibo is an open-source full stack API for quantum simulation and quantum hardware control.
+
+Some of the key features of Qibo are:
+- Definition of a standard language for the construction and execution of quantum circuits with device agnostic approach to simulation and quantum hardware control based on plug and play backend drivers.
+- A continuously growing code-base of quantum algorithms applications presented with examples and tutorials.
+- Efficient simulation backends with GPU, multi-GPU and CPU with multi-threading support.
+- Simple mechanism for the implementation of new simulation and hardware backend drivers.
+
+## Documentation
+
+[![docs](https://github.com/qiboteam/qibo/actions/workflows/publish.yml/badge.svg)](https://qibo.science/qibo/stable/)
+
+Qibo documentation is available [here](https://qibo.science).
+
+## Minimum Working Examples
+
+A simple [Quantum Fourier Transform (QFT)](https://en.wikipedia.org/wiki/Quantum_Fourier_transform) example to test your installation:
+```python
+from qibo.models import QFT
+
+# Create a QFT circuit with 15 qubits
+circuit = QFT(15)
+
+# Simulate final state wavefunction default initial state is |00>
+final_state = circuit()
+```
+
+Here another example with more gates and shots simulation:
+
+```python
+import numpy as np
+from qibo import Circuit, gates
+
+c = Circuit(2)
+c.add(gates.X(0))
+
+# Add a measurement register on both qubits
+c.add(gates.M(0, 1))
+
+# Execute the circuit with the default initial state |00>.
+result = c(nshots=100)
+```
+
+In both cases, the simulation will run in a single device CPU or GPU in double precision `complex128`.
+
+## Citation policy
+[![arXiv](https://img.shields.io/badge/arXiv-2009.01845-b31b1b.svg)](https://arxiv.org/abs/2009.01845)
+[![DOI](https://zenodo.org/badge/241307936.svg)](https://zenodo.org/badge/latestdoi/241307936)
+
+If you use the package please refer to [the documentation](https://qibo.science/qibo/stable/appendix/citing-qibo.html#publications) for citation instructions.
+
+## Contacts
+
+To get in touch with the community and the developers, consider joining the Qibo workspace on Matrix:
+
+[![Matrix](https://img.shields.io/matrix/qibo%3Amatrix.org?logo=matrix)](https://matrix.to/#/#qibo:matrix.org)
+
+If you have a question about the project, please contact us with [📫](mailto:qiboteam@qibo.science).
+
+## Supporters and collaborators
+
+- Quantum Research Center, Technology Innovation Institute (TII), United Arab Emirates
+- Università degli Studi di Milano (UNIMI), Italy.
+- Istituto Nazionale di Fisica Nucleare (INFN), Italy.
+- Università degli Studi di Milano-Bicocca (UNIMIB), Italy.
+- European Organization for Nuclear research (CERN), Switzerland.
+- Universitat de Barcelona (UB), Spain.
+- Barcelona Supercomputing Center (BSC), Spain.
+- Qilimanjaro Quantum Tech, Spain.
+- Centre for Quantum Technologies (CQT), Singapore.
+- Institute of High Performance Computing (IHPC), Singapore.
+- National Supercomputing Centre (NSCC), Singapore.
+- RIKEN Center for Computational Science (R-CCS), Japan.
+- NVIDIA (cuQuantum), USA.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..c88e90504
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,28 @@
+ci:
+ autofix_prs: true
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: debug-statements
+ - repo: https://github.com/psf/black
+ rev: 24.4.2
+ hooks:
+ - id: black
+ - repo: https://github.com/pycqa/isort
+ rev: 5.13.2
+ hooks:
+ - id: isort
+ args: ["--profile", "black"]
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.17.0
+ hooks:
+ - id: pyupgrade
+ - repo: https://github.com/hadialqattan/pycln
+ rev: v2.4.0
+ hooks:
+ - id: pycln
+ args: [--config=pyproject.toml]
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..335bbf749
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,118 @@
+# Qibo Code of Conduct
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and our
+community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language.
+* Being respectful of differing viewpoints and experiences.
+* Gracefully accepting constructive criticism.
+* Focusing on what is best for the community.
+* Showing empathy towards other community members.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances.
+* Trolling, insulting/derogatory comments, and personal or political attacks.
+* Public or private harassment.
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission.
+* Conduct which could reasonably be considered inappropriate for the forum in
+ which it occurs.
+
+All Qibo forums and spaces are meant for professional interactions, and any
+behavior which could reasonably be considered inappropriate in a professional
+setting is unacceptable.
+
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+
+## Scope
+
+This Code of Conduct applies to all content on https://qibo.science, Qibo's
+GitHub organization, or any other official Qibo web presence allowing for
+community interactions, as well as at all official Qibo events, whether offline
+or online.
+
+The Code of Conduct also applies within project spaces and in public spaces
+whenever an individual is representing Qibo or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+or de facto representative at an online or offline event.
+
+
+## Conflict Resolution
+
+Conflicts in an open source project can take many forms, from someone having a
+bad day and using harsh and hurtful language in the issue queue, to more serious
+instances such as sexist/racist statements or threats of violence, and
+everything in between.
+
+If the behavior is threatening or harassing, or for other reasons requires
+immediate escalation, please see below.
+
+However, for the vast majority of issues, we aim to empower individuals to first
+resolve conflicts themselves, asking for help when needed, and only after that
+fails to escalate further. This approach gives people more control over the
+outcome of their dispute.
+
+If you are experiencing or witnessing conflict, we ask you to use the following
+escalation strategy to address the conflict:
+
+1. Address the perceived conflict directly with those involved, preferably in a
+ real-time medium.
+2. If this fails, get a third party (e.g. a mutual friend, and/or someone with
+ background on the issue, but not involved in the conflict) to intercede.
+3. If you are still unable to resolve the conflict, and you believe it rises to
+ harassment or another code of conduct violation, report it.
+
+## Reporting Violations
+
+Violations of the Code of Conduct can be reported to Qibo's Project Coordinator,
+Stefano Carrazza (stefano.carrazza@unimi.it). The Project Coordinator will
+determine whether the Code of Conduct was violated, and will issue an
+appropriate sanction, possibly including a written warning or expulsion from the
+project, project sponsored spaces, or project forums. We ask that you make a
+good-faith effort to resolve your conflict via the conflict resolution policy
+before submitting a report.
+
+Violations of the Code of Conduct can occur in any setting, even those unrelated
+to the project. We will only consider complaints about conduct that has occurred
+within one year of the report.
+
+
+## Enforcement
+
+If the Project Coordinator receive a report alleging a violation of the Code of
+Conduct, the Project Coordinator will notify the accused of the report, and
+provide them an opportunity to discuss the report before a sanction is issued.
+The Project Coordinator will do his utmost to keep the reporter anonymous. If
+the act is ongoing (such as someone engaging in harassment), or involves a
+threat to anyone's safety (e.g. threats of violence), the Project Coordinator
+may issue sanctions without notice.
+
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at https://contributor-covenant.org/version/1/4.
diff --git a/LICENSE b/LICENSE
index 261eeb9e9..78ed7827f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
- Apache License
+ Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright 2020 The QIBO team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index a824cf72f..7f5385f2f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,80 @@
-# qibo-qdp
\ No newline at end of file
+![Logo](https://github.com/qiboteam/qibo/blob/master/doc/source/_static/qibo_logo_dark.svg)
+
+[![codecov](https://codecov.io/gh/qiboteam/qibo/branch/master/graph/badge.svg?token=1EKZKVEVX0)](https://codecov.io/gh/qiboteam/qibo)
+![PyPI - Version](https://img.shields.io/pypi/v/qibo)
+![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qibo)
+
+Qibo is an open-source full stack API for quantum simulation and quantum hardware control.
+
+Some of the key features of Qibo are:
+- Definition of a standard language for the construction and execution of quantum circuits with device agnostic approach to simulation and quantum hardware control based on plug and play backend drivers.
+- A continuously growing code-base of quantum algorithms applications presented with examples and tutorials.
+- Efficient simulation backends with GPU, multi-GPU and CPU with multi-threading support.
+- Simple mechanism for the implementation of new simulation and hardware backend drivers.
+
+## Documentation
+
+[![docs](https://github.com/qiboteam/qibo/actions/workflows/publish.yml/badge.svg)](https://qibo.science/qibo/stable/)
+
+Qibo documentation is available [here](https://qibo.science).
+
+## Minimum Working Examples
+
+A simple [Quantum Fourier Transform (QFT)](https://en.wikipedia.org/wiki/Quantum_Fourier_transform) example to test your installation:
+```python
+from qibo.models import QFT
+
+# Create a QFT circuit with 15 qubits
+circuit = QFT(15)
+
+# Simulate final state wavefunction default initial state is |00>
+final_state = circuit()
+```
+
+Here another example with more gates and shots simulation:
+
+```python
+import numpy as np
+from qibo import Circuit, gates
+
+c = Circuit(2)
+c.add(gates.X(0))
+
+# Add a measurement register on both qubits
+c.add(gates.M(0, 1))
+
+# Execute the circuit with the default initial state |00>.
+result = c(nshots=100)
+```
+
+In both cases, the simulation will run in a single device CPU or GPU in double precision `complex128`.
+
+## Citation policy
+[![arXiv](https://img.shields.io/badge/arXiv-2009.01845-b31b1b.svg)](https://arxiv.org/abs/2009.01845)
+[![DOI](https://zenodo.org/badge/241307936.svg)](https://zenodo.org/badge/latestdoi/241307936)
+
+If you use the package please refer to [the documentation](https://qibo.science/qibo/stable/appendix/citing-qibo.html#publications) for citation instructions.
+
+## Contacts
+
+To get in touch with the community and the developers, consider joining the Qibo workspace on Matrix:
+
+[![Matrix](https://img.shields.io/matrix/qibo%3Amatrix.org?logo=matrix)](https://matrix.to/#/#qibo:matrix.org)
+
+If you have a question about the project, please contact us with [📫](mailto:qiboteam@qibo.science).
+
+## Supporters and collaborators
+
+- Quantum Research Center, Technology Innovation Institute (TII), United Arab Emirates
+- Università degli Studi di Milano (UNIMI), Italy.
+- Istituto Nazionale di Fisica Nucleare (INFN), Italy.
+- Università degli Studi di Milano-Bicocca (UNIMIB), Italy.
+- European Organization for Nuclear research (CERN), Switzerland.
+- Universitat de Barcelona (UB), Spain.
+- Barcelona Supercomputing Center (BSC), Spain.
+- Qilimanjaro Quantum Tech, Spain.
+- Centre for Quantum Technologies (CQT), Singapore.
+- Institute of High Performance Computing (IHPC), Singapore.
+- National Supercomputing Centre (NSCC), Singapore.
+- RIKEN Center for Computational Science (R-CCS), Japan.
+- NVIDIA (cuQuantum), USA.
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 000000000..a0f406671
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,23 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+view: html
+ $(BROWSER) build/html/index.html
diff --git a/doc/source/_static/comp_basis_encoder.png b/doc/source/_static/comp_basis_encoder.png
new file mode 100644
index 000000000..eb0213953
Binary files /dev/null and b/doc/source/_static/comp_basis_encoder.png differ
diff --git a/doc/source/_static/css/style.css b/doc/source/_static/css/style.css
new file mode 100644
index 000000000..46c934cf5
--- /dev/null
+++ b/doc/source/_static/css/style.css
@@ -0,0 +1,7 @@
+.wy-side-nav-search {
+ background-color: #6400FF;
+}
+
+.wy-nav-top {
+ background-color: #6400FF;
+}
diff --git a/doc/source/_static/entangling_layer.png b/doc/source/_static/entangling_layer.png
new file mode 100644
index 000000000..413050188
Binary files /dev/null and b/doc/source/_static/entangling_layer.png differ
diff --git a/doc/source/_static/phase_encoder.png b/doc/source/_static/phase_encoder.png
new file mode 100644
index 000000000..df6796e1d
Binary files /dev/null and b/doc/source/_static/phase_encoder.png differ
diff --git a/doc/source/_static/qibo_logo_dark.svg b/doc/source/_static/qibo_logo_dark.svg
new file mode 100644
index 000000000..3b7dc406d
--- /dev/null
+++ b/doc/source/_static/qibo_logo_dark.svg
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/source/_static/qibo_logo_light.svg b/doc/source/_static/qibo_logo_light.svg
new file mode 100644
index 000000000..bb907e722
--- /dev/null
+++ b/doc/source/_static/qibo_logo_light.svg
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/source/_static/symplectic_matrix.png b/doc/source/_static/symplectic_matrix.png
new file mode 100644
index 000000000..63ca21305
Binary files /dev/null and b/doc/source/_static/symplectic_matrix.png differ
diff --git a/doc/source/_static/unary_encoder_ladder.png b/doc/source/_static/unary_encoder_ladder.png
new file mode 100644
index 000000000..fd7672b18
Binary files /dev/null and b/doc/source/_static/unary_encoder_ladder.png differ
diff --git a/doc/source/_static/unary_encoder_tree.png b/doc/source/_static/unary_encoder_tree.png
new file mode 100644
index 000000000..dd17b20c8
Binary files /dev/null and b/doc/source/_static/unary_encoder_tree.png differ
diff --git a/doc/source/api-reference/hep.rst b/doc/source/api-reference/hep.rst
new file mode 100644
index 000000000..c22dd2cd4
--- /dev/null
+++ b/doc/source/api-reference/hep.rst
@@ -0,0 +1,26 @@
+Models for High Energy Physics
+==============================
+
+The Qibo package comes with the following extra models:
+
+* qPDFs_
+
+_______________________
+
+.. _qPDFs:
+
+Quantum PDFs
+------------
+
+Qibo provides a variational circuit model for parton distribution functions,
+named qPDF. This model is based on :class:`qibo.models.Circuit` and provides a
+simple API to evaluate PDF flavours at specific values of the momentum fraction
+x. Further details and references about this model are presented in the
+``examples/qPDF`` tutorial.
+
+qPDF circuit model
+^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.models.hep.qPDF
+ :members:
+ :member-order: bysource
diff --git a/doc/source/api-reference/index.rst b/doc/source/api-reference/index.rst
new file mode 100644
index 000000000..89b61b542
--- /dev/null
+++ b/doc/source/api-reference/index.rst
@@ -0,0 +1,13 @@
+.. _Components:
+
+API reference
+=============
+
+In this section we present the main Qibo components and primitives included in
+the public API.
+
+.. toctree::
+ :maxdepth: 3
+
+ qibo
+ hep
diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst
new file mode 100644
index 000000000..64d385a6a
--- /dev/null
+++ b/doc/source/api-reference/qibo.rst
@@ -0,0 +1,2627 @@
+.. _Models:
+
+Models
+------
+
+Qibo provides models for both the circuit based and the adiabatic quantum
+computation paradigms. Circuit based models include :ref:`generalpurpose` which
+allow defining arbitrary circuits and :ref:`applicationspecific` such as the
+Quantum Fourier Transform (:class:`qibo.models.QFT`) and the
+Variational Quantum Eigensolver (:class:`qibo.models.VQE`).
+Adiabatic quantum computation is simulated using the :ref:`timeevolution`
+of state vectors.
+
+In order to perform calculations and apply gates to a state vector a backend
+has to be used. The backends are defined in ``qibo/backends``.
+Circuit and gate objects are backend independent and can be executed with
+any of the available backends.
+
+Qibo uses big-endian byte order, which means that the most significant qubit
+is the one with index 0, while the least significant qubit is the one with
+the highest index.
+
+.. _generalpurpose:
+
+Circuit models
+^^^^^^^^^^^^^^
+
+Circuit
+"""""""
+
+.. autoclass:: qibo.models.circuit.Circuit
+ :members:
+ :member-order: bysource
+
+
+Circuit addition
+""""""""""""""""
+
+:class:`qibo.models.circuit.Circuit` objects support addition. For example
+
+.. testsetup::
+
+ import qibo
+ from qibo import models
+ from qibo import gates
+
+.. testcode::
+
+ c1 = models.QFT(4)
+
+ c2 = models.Circuit(4)
+ c2.add(gates.RZ(0, 0.1234))
+ c2.add(gates.RZ(1, 0.1234))
+ c2.add(gates.RZ(2, 0.1234))
+ c2.add(gates.RZ(3, 0.1234))
+
+ c = c1 + c2
+
+will create a circuit that performs the Quantum Fourier Transform on four qubits
+followed by Rotation-Z gates.
+
+
+.. _circuit-fusion:
+
+Circuit fusion
+""""""""""""""
+
+The gates contained in a circuit can be fused up to two-qubits using the
+:meth:`qibo.models.circuit.Circuit.fuse` method. This returns a new circuit
+for which the total number of gates is less than the gates in the original
+circuit as groups of gates have been fused to a single
+:class:`qibo.gates.special.FusedGate` gate. Simulating the new circuit
+is equivalent to simulating the original one but in most cases more efficient
+since less gates need to be applied to the state vector.
+
+The fusion algorithm works as follows: First all gates in the circuit are
+transformed to unmarked :class:`qibo.gates.special.FusedGate`. The gates
+are then processed in the order they were added in the circuit. For each gate
+we identify the neighbors forth and back in time and attempt to fuse them to
+the gate. Two gates can be fused if their total number of target qubits is
+smaller than the fusion maximum qubits (specified by the user) and there are
+no other gates between acting on the same target qubits. Gates that are fused
+to others are marked. The new circuit queue contains the gates that remain
+unmarked after the above operations finish.
+
+Gates are processed in the original order given by user. There are no
+additional simplifications performed such as commuting gates acting on the same
+qubit or canceling gates even when such simplifications are mathematically possible.
+The user can specify the maximum number of qubits in a fused gate using
+the ``max_qubits`` flag in :meth:`qibo.models.circuit.Circuit.fuse`.
+
+For example the following:
+
+.. testcode::
+
+ from qibo import models, gates
+
+ c = models.Circuit(2)
+ c.add([gates.H(0), gates.H(1)])
+ c.add(gates.CZ(0, 1))
+ c.add([gates.X(0), gates.Y(1)])
+ fused_c = c.fuse()
+
+will create a new circuit with a single :class:`qibo.gates.special.FusedGate`
+acting on ``(0, 1)``, while the following:
+
+.. testcode::
+
+ from qibo import models, gates
+
+ c = models.Circuit(3)
+ c.add([gates.H(0), gates.H(1), gates.H(2)])
+ c.add(gates.CZ(0, 1))
+ c.add([gates.X(0), gates.Y(1), gates.Z(2)])
+ c.add(gates.CNOT(1, 2))
+ c.add([gates.H(0), gates.H(1), gates.H(2)])
+ fused_c = c.fuse()
+
+will give a circuit with two fused gates, the first of which will act on
+``(0, 1)`` corresponding to
+
+.. code-block:: python
+
+ [H(0), H(1), CZ(0, 1), X(0), H(0)]
+
+and the second will act to ``(1, 2)`` corresponding to
+
+.. code-block:: python
+
+ [Y(1), Z(2), CNOT(1, 2), H(1), H(2)]
+
+.. _applicationspecific:
+
+Quantum Fourier Transform (QFT)
+"""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.qft.QFT
+ :members:
+ :member-order: bysource
+
+Variational Quantum Eigensolver (VQE)
+"""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.variational.VQE
+ :members:
+ :member-order: bysource
+
+Adiabatically Assisted Variational Quantum Eigensolver (AAVQE)
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.variational.AAVQE
+ :members:
+ :member-order: bysource
+
+Quantum Approximate Optimization Algorithm (QAOA)
+"""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.variational.QAOA
+ :members:
+ :member-order: bysource
+
+Feedback-based Algorithm for Quantum Optimization (FALQON)
+""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.variational.FALQON
+ :members:
+ :member-order: bysource
+
+
+Grover's Algorithm
+""""""""""""""""""
+
+.. autoclass:: qibo.models.grover.Grover
+ :members:
+ :member-order: bysource
+
+
+Travelling Salesman Problem
+"""""""""""""""""""""""""""
+
+.. automodule:: qibo.models.tsp
+ :members:
+ :member-order: bysource
+
+
+Iterative Quantum Amplitude Estimation (IQAE)
+"""""""""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.models.iqae.IQAE
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.models.iqae.IterativeAmplitudeEstimationResult
+ :members:
+ :member-order: bysource
+
+
+Double Bracket Iteration algorithm for Diagonalization
+""""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+The Double Bracket Flow (DBF) has been presented `here `_
+as a novel strategy for preparing eigenstates of a quantum system. We implement in
+Qibo a discretized version of the algorithm, which executes sequential Double
+Bracket Iterations.
+
+.. autoclass:: qibo.models.dbi.double_bracket.DoubleBracketGeneratorType
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.models.dbi.double_bracket.DoubleBracketIteration
+ :members:
+ :member-order: bysource
+
+
+.. _timeevolution:
+
+Time evolution
+^^^^^^^^^^^^^^
+
+State evolution
+"""""""""""""""
+
+.. autoclass:: qibo.models.evolution.StateEvolution
+ :members:
+ :member-order: bysource
+
+Adiabatic evolution
+"""""""""""""""""""
+
+.. autoclass:: qibo.models.evolution.AdiabaticEvolution
+ :members:
+ :member-order: bysource
+
+
+.. _data-encoders:
+
+Data Encoders
+^^^^^^^^^^^^^
+
+We provide a family of algorithms that encode classical data into quantum circuits.
+
+Computational Basis Encoder
+"""""""""""""""""""""""""""
+
+Given a bitstring :math:`b` of length :math:`n`, this encoder generates a layer of Pauli-:math:`X`
+gates that creates the quantum state :math:`|\,b\,\rangle`.
+
+For instance, the following two circuit generations are equivalent:
+
+.. testsetup::
+
+ from qibo import Circuit, gates
+ from qibo.models.encodings import comp_basis_encoder
+
+.. testcode::
+
+ b = "101"
+ circuit_1 = comp_basis_encoder(b)
+
+ circuit_2 = Circuit(3)
+ circuit_2.add(gates.X(0))
+ circuit_2.add(gates.X(2))
+
+
+.. image:: ../_static/comp_basis_encoder.png
+ :width: 400
+ :height: 250
+ :align: center
+
+
+.. autofunction:: qibo.models.encodings.comp_basis_encoder
+
+
+Phase Encoder
+"""""""""""""
+
+Encodes data of length :math:`n` into the phases of :math:`n` qubits.
+
+
+For instance, the following two circuit generations are equivalent:
+
+.. testsetup::
+
+ import numpy as np
+
+ from qibo import Circuit, gates
+ from qibo.models.encodings import phase_encoder
+
+.. testcode::
+
+ nqubits = 3
+ phases = np.random.rand(nqubits)
+
+ circuit_1 = phase_encoder(phases, rotation="RX")
+
+ circuit_2 = Circuit(3)
+ circuit_2.add(gates.RX(qubit, phases[qubit]) for qubit in range(nqubits))
+
+
+.. image:: ../_static/phase_encoder.png
+ :width: 300
+ :height: 300
+ :align: center
+
+
+.. autofunction:: qibo.models.encodings.phase_encoder
+
+
+Unary Encoder
+"""""""""""""
+
+Given a classical ``data`` array :math:`\mathbf{x} \in \mathbb{R}^{d}` such that
+
+.. math::
+ \mathbf{x} = (x_{1}, x_{2}, \dots, x_{d}) \, ,
+
+this function generate the circuit that prepares the following quantum state
+:math:`\ket{\psi} \in \mathcal{H}`:
+
+.. math::
+ \ket{\psi} = \frac{1}{\|\mathbf{x}\|_{\textup{HS}}} \,
+ \sum_{k=1}^{d} \, x_{k} \, \ket{k} \, ,
+
+with :math:`\mathcal{H} \cong \mathbb{C}^{d}` being a :math:`d`-qubit Hilbert space,
+and :math:`\|\cdot\|_{\textup{HS}}` being the Hilbert-Schmidt norm.
+
+Here, :math:`\ket{k}` is a unary representation of the number :math:`k`.
+For instance, for :math:`d = 3`, the final state would be
+
+.. math::
+ \ket{\psi} = \frac{1}{\|\mathbf{x}\|_{\textup{HS}}} \,
+ \left( x_{1} \ket{001} + x_{2} \ket{010} + x_{3} \ket{100} \right) \, .
+
+There are multiple circuit architechtures that lead to unary encoding of classical data.
+For example, to encode a :math:`8`-dimensional data, one could use the so-called
+*tree* architechture below:
+
+.. image:: ../_static/unary_encoder_tree.png
+ :width: 400
+ :height: 500
+ :align: center
+
+where the first gate is the :class:`qibo.gates.X`
+and the parametrized gates are the :class:`qibo.gates.RBS`.
+To know how the angles :math:`\{\theta_{k}\}_{[k]}` are calculated for this architecture,
+please refer to S. Johri *et al.*, *Nearest Centroid Classification on a Trapped Ion Quantum Computer*,
+`arXiv:2012.04145v2 [quant-ph] `_.
+
+On the other hand, the same encoding could be performed using the so-called
+*diagonal* (also known as *ladder*) architecture below:
+
+.. image:: ../_static/unary_encoder_ladder.png
+ :width: 700
+ :height: 550
+ :align: center
+
+This architecture leads to a choice of angles based on
+`spherical coordinates in a d-dimensional hypersphere
+`_.
+
+
+.. autofunction:: qibo.models.encodings.unary_encoder
+
+
+Unary Encoder for Random Gaussian States
+""""""""""""""""""""""""""""""""""""""""
+
+Performs the same unary encoder as :class:`qibo.models.encodings.unary_encoder`
+using the *tree* architecture , with the difference being that now each entry
+of the :math:`d`-dimensional array is sampled from a Gaussian distribution
+:math:`\mathcal{N}(0, 1)`.
+
+
+.. autofunction:: qibo.models.encodings.unary_encoder_random_gaussian
+
+
+Entangling layer
+""""""""""""""""
+
+Generates a layer of nearest-neighbour two-qubit gates, assuming 1-dimensional connectivity.
+With the exception of :class:`qibo.gates.gates.GeneralizedfSim`,
+any of the two-qubit gates implemented in ``qibo`` can be selected to customize the entangling layer.
+If the chosen gate is parametrized, all phases are set to :math:`0.0`.
+Note that these phases can be updated a posterior by using
+:meth:`qibo.models.Circuit.set_parameters`.
+There are four possible choices of layer ``architecture``:
+``diagonal``, ``shifted``, ``even-layer``, and ``odd-layer``.
+For instance, we show below an example of each architecture for ``nqubits = 6``.
+
+
+.. image:: ../_static/entangling_layer.png
+ :width: 800
+ :height: 450
+ :align: center
+
+
+If ``closed_boundary`` is set to ``True``, then an extra gate is added connecting the last and the first qubit,
+with the last qubit as the control qubit and the first qubit as a target qubit.
+
+
+.. autofunction:: qibo.models.encodings.entangling_layer
+
+
+.. _error-mitigation:
+
+Error Mitigation
+^^^^^^^^^^^^^^^^
+
+Qibo allows for mitigating noise in circuits via error mitigation methods.
+Unlike error correction, error mitigation does not aim to correct qubit errors,
+but rather it provides the means to estimate the noise-free expected value of
+an observable measured at the end of a noisy circuit.
+
+Readout Mitigation
+""""""""""""""""""
+
+A common kind of error happening in quantum circuits is readout error, i.e. the
+error in the measurement of the qubits at the end of the computation.
+In Qibo there are currently two methods implemented for mitigating readout errors,
+and both can be used as standalone functions or in combination with the other
+general mitigation methods by setting the paramter `readout`.
+
+
+Response Matrix
+""""""""""""""""""
+Given :math:`n` qubits, all the possible :math:`2^n` states are constructed via the
+application of the corresponding sequence of :math:`X` gates
+:math:`X_0\otimes I_1\otimes\cdot\cdot\cdot\otimes X_{n-1}`.
+In the presence of readout errors, we will measure for each state :math:`i` some noisy
+frequencies :math:`F_i^{noisy}` different from the ideal ones
+:math:`F_i^{ideal}=\delta_{i,j}`.
+
+The effect of the error is modeled by the response matrix composed of the noisy frequencies as
+columns :math:`M=\big(F_0^{noisy},...,F_{n-1}^{noisy}\big)`. We have indeed that:
+
+.. math::
+ F_i^{noisy} = M \cdot F_i^{ideal}
+
+and, therefore, the calibration matrix obtained as :math:`M_{\text{cal}}=M^{-1}`
+can be used to recover the noise-free frequencies.
+
+The calibration matrix :math:`M_{\text{cal}}` lacks stochasticity, resulting in a 'negative probability' issue.
+The distributions that arise after applying :math:`M_{\text{cal}}` are quasiprobabilities;
+the individual elements can be negative surpass 1, provided they sum to 1.
+It is posible to use Iterative Bayesian Unfolding (IBU) to preserve non-negativity.
+See `Nachman et al `_ for more details.
+
+
+
+.. autofunction:: qibo.models.error_mitigation.get_response_matrix
+
+
+.. autofunction:: qibo.models.error_mitigation.iterative_bayesian_unfolding
+
+
+.. autofunction:: qibo.models.error_mitigation.apply_resp_mat_readout_mitigation
+
+
+.. autofunction:: qibo.models.error_mitigation.apply_randomized_readout_mitigation
+
+
+.. autofunction:: qibo.models.error_mitigation.get_expectation_val_with_readout_mitigation
+
+
+Randomized readout mitigation
+""""""""""""""""""""""""""""""
+This approach converts the effect of any noise map :math:`A` into a single multiplication
+factor for each Pauli observable, that is, diagonalizes the measurement channel.
+The multiplication factor :math:`\lambda` can be directly measured even without
+the quantum circuit. Dividing the measured value :math:`\langle O\rangle_{noisy}` by these
+factor results in the mitigated Pauli expectation value :math:`\langle O\rangle_{ideal}`,
+
+.. math::
+ \langle O\rangle_{ideal} = \frac{\langle O\rangle_{noisy}}{\lambda}
+
+.. autofunction:: qibo.models.error_mitigation.apply_randomized_readout_mitigation
+
+
+Zero Noise Extrapolation (ZNE)
+""""""""""""""""""""""""""""""
+
+Given a noisy circuit :math:`C` and an observable :math:`A`, Zero Noise Extrapolation (ZNE)
+consists in running :math:`n+1` versions of the circuit with different noise levels
+:math:`\{c_j\}_{j=0..n}` and, for each of them, measuring the expected value of the observable
+:math:`E_j=\langle A\rangle_j`.
+
+Then, an estimate for the expected value of the observable in the noise-free condition
+is obtained as:
+
+.. math::
+ \hat{E} = \sum_{j=0}^n \gamma_jE_j
+
+with :math:`\gamma_j` satisfying:
+
+.. math::
+ \sum_{j=0}^n \gamma_j = 1 \qquad \sum_{j=0}^n \gamma_j c_j^k = 0 \quad \text{for}\,\, k=1,..,n
+
+This implementation of ZNE relies on the insertion of gate pairs (that resolve to the
+identity in the noise-free case) to realize the different noise levels :math:`\{c_j\}`,
+see `He et al `_
+for more details. Hence, the canonical levels are mapped to the number of inserted pairs
+as :math:`c_j\rightarrow 2 c_j + 1`.
+
+.. autofunction:: qibo.models.error_mitigation.ZNE
+
+
+.. autofunction:: qibo.models.error_mitigation.get_gammas
+
+
+.. autofunction:: qibo.models.error_mitigation.get_noisy_circuit
+
+
+Clifford Data Regression (CDR)
+""""""""""""""""""""""""""""""
+
+In the Clifford Data Regression (CDR) method, a set of :math:`n` circuits
+:math:`S_n=\{C_i\}_{i=1,..,n}` is generated starting from the original circuit
+:math:`C_0` by replacing some of the non-Clifford gates with Clifford ones.
+Given an observable :math:`A`, all the circuits of :math:`S_n` are both simulated
+to obtain the correspondent expected values of :math:`A` in noise-free condition
+:math:`\{a_i^{exact}\}_{i=1,..,n}`, and run in noisy conditions to obtain the noisy
+expected values :math:`\{a_i^{noisy}\}_{i=1,..,n}`.
+
+Finally a model :math:`f` is trained to minimize the mean squared error:
+
+.. math::
+ E = \sum_{i=1}^n \bigg(a_i^{exact}-f(a_i^{noisy})\bigg)^2
+
+and learn the mapping :math:`a^{noisy}\rightarrow a^{exact}`.
+The mitigated expected value of :math:`A` at the end of :math:`C_0` is then
+obtained simply with :math:`f(a_0^{noisy})`.
+
+In this implementation the initial circuit is expected to be decomposed in the three
+Clifford gates :math:`RX(\frac{\pi}{2})`, :math:`CNOT`, :math:`X` and in :math:`RZ(\theta)`
+(which is Clifford only for :math:`\theta=\frac{n\pi}{2}`).
+By default the set of Clifford gates used for substitution is
+:math:`\{RZ(0),RZ(\frac{\pi}{2}),RZ(\pi),RZ(\frac{3}{2}\pi)\}`.
+See `Sopena et al `_ for more details.
+
+.. autofunction:: qibo.models.error_mitigation.CDR
+
+
+.. autofunction:: qibo.models.error_mitigation.sample_training_circuit_cdr
+
+
+Variable Noise CDR (vnCDR)
+""""""""""""""""""""""""""
+
+Variable Noise CDR (vnCDR) is an extension of the CDR method described above that factors
+in different noise levels as in ZNE. In detail, the set of circuits
+:math:`S_n=\{\mathbf{C}_i\}_{i=1,..,n}` is still generated as in CDR, but for each
+:math:`\mathbf{C}_i` we have :math:`k` different versions of it with increased noise
+:math:`\mathbf{C}_i=C_i^0,C_i^1,...,C_i^{k-1}`.
+
+Therefore, in this case we have a :math:`k`-dimensional predictor variable
+:math:`\mathbf{a}_i^{noisy}=\big(a_i^0, a_i^1,..,a_i^{k-1}\big)^{noisy}` for the same
+noise-free targets :math:`a_i^{exact}`, and we want to learn the mapping:
+
+.. math::
+ f:\mathbf{a}_i^{noisy}\rightarrow a_i^{exact}
+
+via minimizing the same mean squared error:
+
+.. math::
+ E = \sum_{i=1}^n \bigg(a_i^{exact}-f(\mathbf{a}_i^{noisy})\bigg)^2
+
+In particular, the default choice is to take :math:`f(\mathbf{x}):=\Gamma\cdot \mathbf{x}\;`,
+with :math:`\Gamma=\text{diag}(\gamma_0,\gamma_1,...,\gamma_{k-1})\;`, that corresponds to the
+ZNE calculation for the estimate of the expected value.
+
+Here, as in the implementation of the CDR above, the circuit is supposed to be decomposed in
+the set of primitive gates :math:`{RX(\frac{\pi}{2}),CNOT,X,RZ(\theta)}`.
+See `Sopena et al `_ for all the details.
+
+.. autofunction:: qibo.models.error_mitigation.vnCDR
+
+
+Importance Clifford Sampling (ICS)
+""""""""""""""""""""""""""""""""""
+
+In the Importance Clifford Sampling (ICS) method, a set of :math:`n` circuits
+:math:`S_n=\{C_i\}_{i=1,..,n}` that stabilizes a given Pauli observable is generated starting from the original circuit
+:math:`C_0` by replacing all the non-Clifford gates with Clifford ones.
+Given an observable :math:`A`, all the circuits of :math:`S_n` are both simulated
+to obtain the correspondent expected values of :math:`A` in noise-free condition
+:math:`\{a_i^{exact}\}_{i=1,..,n}`, and run in noisy conditions to obtain the noisy
+expected values :math:`\{a_i^{noisy}\}_{i=1,..,n}`.
+
+Finally, a theoretically inspired model :math:`f` is learned using the training data.
+
+The mitigated expected value of :math:`A` at the end of :math:`C_0` is then
+obtained simply with :math:`f(a_0^{noisy})`.
+
+In this implementation the initial circuit is expected to be decomposed in the three
+Clifford gates :math:`RX(\frac{\pi}{2})`, :math:`CNOT`, :math:`X` and in :math:`RZ(\theta)`
+(which is Clifford only for :math:`\theta=\frac{n\pi}{2}`).
+By default the set of Clifford gates used for substitution is
+:math:`\{RZ(0),RZ(\frac{\pi}{2}),RZ(\pi),RZ(\frac{3}{2}\pi)\}`.
+See `Sopena et al `_ for more details.
+
+.. autofunction:: qibo.models.error_mitigation.ICS
+
+
+.. autofunction:: qibo.models.error_mitigation.sample_clifford_training_circuit
+
+_______________________
+
+.. _Gates:
+
+Gates
+-----
+
+All supported gates can be accessed from the ``qibo.gates`` module.
+Read below for a complete list of supported gates.
+
+All gates support the ``controlled_by`` method that allows to control
+the gate on an arbitrary number of qubits. For example
+
+* ``gates.X(0).controlled_by(1, 2)`` is equivalent to ``gates.TOFFOLI(1, 2, 0)``,
+* ``gates.RY(0, np.pi).controlled_by(1, 2, 3)`` applies the Y-rotation to qubit 0 when qubits 1, 2 and 3 are in the ``|111>`` state.
+* ``gates.SWAP(0, 1).controlled_by(3, 4)`` swaps qubits 0 and 1 when qubits 3 and 4 are in the ``|11>`` state.
+
+Abstract gate
+^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.abstract.Gate
+ :members:
+ :member-order: bysource
+
+Single qubit gates
+^^^^^^^^^^^^^^^^^^
+
+Hadamard (H)
+""""""""""""
+
+.. autoclass:: qibo.gates.H
+ :members:
+ :member-order: bysource
+
+Pauli X (X)
+"""""""""""
+
+.. autoclass:: qibo.gates.X
+ :members:
+ :member-order: bysource
+
+Pauli Y (Y)
+"""""""""""
+
+.. autoclass:: qibo.gates.Y
+ :members:
+ :member-order: bysource
+
+Pauli Z (Z)
+"""""""""""
+
+.. autoclass:: qibo.gates.Z
+ :members:
+ :member-order: bysource
+
+Square-root of Pauli X (SX)
+"""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.SX
+ :members:
+ :member-order: bysource
+
+S gate (S)
+"""""""""""
+
+.. autoclass:: qibo.gates.S
+ :members:
+ :member-order: bysource
+
+T gate (T)
+"""""""""""
+
+.. autoclass:: qibo.gates.T
+ :members:
+ :member-order: bysource
+
+Identity (I)
+""""""""""""
+
+.. autoclass:: qibo.gates.I
+ :members:
+ :member-order: bysource
+
+Align (A)
+"""""""""
+
+.. autoclass:: qibo.gates.Align
+ :members:
+ :member-order: bysource
+
+Measurement (M)
+"""""""""""""""
+
+.. autoclass:: qibo.gates.M
+ :members:
+ :member-order: bysource
+
+Rotation X-axis (RX)
+""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RX
+ :members:
+ :member-order: bysource
+
+Rotation Y-axis (RY)
+""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RY
+ :members:
+ :member-order: bysource
+
+Rotation Z-axis (RZ)
+""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RZ
+ :members:
+ :member-order: bysource
+
+First general unitary (U1)
+""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.U1
+ :members:
+ :member-order: bysource
+
+Second general unitary (U2)
+"""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.U2
+ :members:
+ :member-order: bysource
+
+Third general unitary (U3)
+""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.U3
+ :members:
+ :member-order: bysource
+
+Two qubit gates
+^^^^^^^^^^^^^^^
+
+Controlled-NOT (CNOT)
+"""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CNOT
+ :members:
+ :member-order: bysource
+
+Controlled-Y (CY)
+"""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CY
+ :members:
+ :member-order: bysource
+
+Controlled-phase (CZ)
+"""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CZ
+ :members:
+ :member-order: bysource
+
+Controlled-Square Root of X (CSX)
+"""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CSX
+ :members:
+ :member-order: bysource
+
+Controlled-rotation X-axis (CRX)
+""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CRX
+ :members:
+ :member-order: bysource
+
+Controlled-rotation Y-axis (CRY)
+""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CRY
+ :members:
+ :member-order: bysource
+
+Controlled-rotation Z-axis (CRZ)
+""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CRZ
+ :members:
+ :member-order: bysource
+
+Controlled first general unitary (CU1)
+""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CU1
+ :members:
+ :member-order: bysource
+
+Controlled second general unitary (CU2)
+"""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CU2
+ :members:
+ :member-order: bysource
+
+Controlled third general unitary (CU3)
+""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CU3
+ :members:
+ :member-order: bysource
+
+Swap (SWAP)
+"""""""""""
+
+.. autoclass:: qibo.gates.SWAP
+ :members:
+ :member-order: bysource
+
+iSwap (iSWAP)
+"""""""""""""
+
+.. autoclass:: qibo.gates.iSWAP
+ :members:
+ :member-order: bysource
+
+Square root of iSwap (SiSWAP)
+"""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.SiSWAP
+ :members:
+ :member-order: bysource
+
+f-Swap (FSWAP)
+""""""""""""""
+
+.. autoclass:: qibo.gates.FSWAP
+ :members:
+ :member-order: bysource
+
+fSim
+""""
+
+.. autoclass:: qibo.gates.fSim
+ :members:
+ :member-order: bysource
+
+Sycamore gate
+"""""""""""""
+
+.. autoclass:: qibo.gates.SYC
+ :members:
+ :member-order: bysource
+
+fSim with general rotation
+""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.GeneralizedfSim
+ :members:
+ :member-order: bysource
+
+Parametric XX interaction (RXX)
+"""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RXX
+ :members:
+ :member-order: bysource
+
+Parametric YY interaction (RYY)
+"""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RYY
+ :members:
+ :member-order: bysource
+
+Parametric ZZ interaction (RZZ)
+"""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RZZ
+ :members:
+ :member-order: bysource
+
+Parametric ZX interaction (RZX)
+"""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RZX
+ :members:
+ :member-order: bysource
+
+Parametric XX-YY interaction (RXXYY)
+""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RXXYY
+ :members:
+ :member-order: bysource
+
+Givens gate
+"""""""""""
+
+.. autoclass:: qibo.gates.GIVENS
+ :members:
+ :member-order: bysource
+
+Reconfigurable Beam Splitter gate (RBS)
+"""""""""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.RBS
+ :members:
+ :member-order: bysource
+
+Echo Cross-Resonance gate (ECR)
+""""""""""""""""""""""""""""""""
+
+.. autoclass:: qibo.gates.ECR
+ :members:
+ :member-order: bysource
+
+Special gates
+^^^^^^^^^^^^^
+
+Toffoli
+"""""""
+
+.. autoclass:: qibo.gates.TOFFOLI
+ :members:
+ :member-order: bysource
+
+CCZ
+"""
+
+.. autoclass:: qibo.gates.CCZ
+ :members:
+ :member-order: bysource
+
+Deutsch
+"""""""
+
+.. autoclass:: qibo.gates.DEUTSCH
+ :members:
+ :member-order: bysource
+
+Arbitrary unitary
+"""""""""""""""""
+
+.. autoclass:: qibo.gates.Unitary
+ :members:
+ :member-order: bysource
+
+Callback gate
+"""""""""""""
+
+.. autoclass:: qibo.gates.CallbackGate
+ :members:
+ :member-order: bysource
+
+Fusion gate
+"""""""""""
+
+.. autoclass:: qibo.gates.FusedGate
+ :members:
+ :member-order: bysource
+
+IONQ Native gates
+^^^^^^^^^^^^^^^^^
+
+GPI
+"""
+
+.. autoclass:: qibo.gates.GPI
+ :members:
+ :member-order: bysource
+
+GPI2
+""""
+
+.. autoclass:: qibo.gates.GPI2
+ :members:
+ :member-order: bysource
+
+Mølmer–Sørensen (MS)
+""""""""""""""""""""
+
+.. autoclass:: qibo.gates.MS
+ :members:
+ :member-order: bysource
+
+Quantinuum native gates
+^^^^^^^^^^^^^^^^^^^^^^^
+
+U1q
+"""
+
+.. autoclass:: qibo.gates.U1q
+ :members:
+ :member-order: bysource
+
+.. note::
+ The other Quantinuum single-qubit and two-qubit native gates are
+ implemented in Qibo as:
+
+ - Pauli-:math:`Z` rotation: :class:`qibo.gates.RZ`
+ - Arbitrary :math:`ZZ` rotation: :class:`qibo.gates.RZZ`
+ - Fully-entangling :math:`ZZ`-interaction: :math:`R_{ZZ}(\pi/2)`
+
+
+IQM native gates
+^^^^^^^^^^^^^^^^
+
+Phase-:math:`RX`
+""""""""""""""""
+
+.. autoclass:: qibo.gates.PRX
+ :members:
+ :member-order: bysource
+
+.. note::
+ The other IQM two-qubit native gate is implemented in Qibo as:
+
+ - Controlled-:math:`Z` rotation: :class:`qibo.gates.CZ`
+
+
+_______________________
+
+
+.. _Channels:
+
+Channels
+--------
+
+Channels are implemented in Qibo as additional gates and can be accessed from
+the ``qibo.gates`` module. Channels can be used on density matrices to perform
+noisy simulations. Channels that inherit :class:`qibo.gates.UnitaryChannel`
+can also be applied to state vectors using sampling and repeated execution.
+For more information on the use of channels to simulate noise we refer to
+:ref:`How to perform noisy simulation? `
+The following channels are currently implemented:
+
+Kraus channel
+^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.KrausChannel
+ :members:
+ :member-order: bysource
+
+Unitary channel
+^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.UnitaryChannel
+ :members:
+ :member-order: bysource
+
+
+Pauli noise channel
+^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.PauliNoiseChannel
+ :members:
+ :member-order: bysource
+
+Depolarizing channel
+^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.DepolarizingChannel
+ :members:
+ :member-order: bysource
+
+Thermal relaxation channel
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.ThermalRelaxationChannel
+ :members:
+ :member-order: bysource
+
+Amplitude damping channel
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.AmplitudeDampingChannel
+ :members:
+ :member-order: bysource
+
+Phase damping channel
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.PhaseDampingChannel
+ :members:
+ :member-order: bysource
+
+Readout error channel
+^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.ReadoutErrorChannel
+ :members:
+ :member-order: bysource
+
+Reset channel
+^^^^^^^^^^^^^
+
+.. autoclass:: qibo.gates.ResetChannel
+ :members:
+ :member-order: bysource
+
+_______________________
+
+Noise
+-----
+
+In Qibo it is possible to create a custom noise model using the
+class :class:`qibo.noise.NoiseModel`. This enables the user to create
+circuits where the noise is gate and qubit dependent.
+
+For more information on the use of :class:`qibo.noise.NoiseModel` see
+:ref:`How to perform noisy simulation? `
+
+.. autoclass:: qibo.noise.NoiseModel
+ :members:
+ :member-order: bysource
+
+Quantum errors
+^^^^^^^^^^^^^^
+
+The quantum errors available to build a noise model are the following:
+
+.. autoclass:: qibo.noise.KrausError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.UnitaryError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.PauliError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.DepolarizingError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.ThermalRelaxationError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.AmplitudeDampingError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.PhaseDampingError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.ReadoutError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.ResetError
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.noise.CustomError
+ :members:
+ :member-order: bysource
+
+
+IBMQ noise model
+^^^^^^^^^^^^^^^^
+
+In Qibo, it is possible to build noisy circuits based on IBMQ's reported noise model of
+for its quantum computer by using the :class:`qibo.noise.IBMQNoiseModel` class.
+The noise model is built using a combination of the
+:class:`qibo.gates.ThermalRelaxationChannel` and :class:`qibo.gates.DepolarizingChannel`
+channels. . At the end of the circuit, if the qubit is measured,
+bitflips errors are set. Moreover, the model handles idle qubits by applying a thermal
+relaxation channel for the duration of the idle-time.
+
+For more information on the :class:`qibo.noise.IBMQNoiseModel` class, see the
+example on :ref:`Simulating quantum hardware `.
+
+
+.. autoclass:: qibo.noise.IBMQNoiseModel
+ :members:
+ :member-order: bysource
+
+
+_______________________
+
+.. _Hamiltonians:
+
+Hamiltonians
+------------
+
+The main abstract Hamiltonian object of Qibo is:
+
+.. autoclass:: qibo.hamiltonians.abstract.AbstractHamiltonian
+ :members:
+ :member-order: bysource
+
+
+Matrix Hamiltonian
+^^^^^^^^^^^^^^^^^^
+
+The first implementation of Hamiltonians uses the full matrix representation
+of the Hamiltonian operator in the computational basis. This matrix has size
+``(2 ** nqubits, 2 ** nqubits)`` and therefore its construction is feasible
+only when number of qubits is small.
+
+Alternatively, the user can construct this Hamiltonian using a sparse matrices.
+Sparse matrices from the
+`scipy.sparse `_
+module are supported by the ``numpy`` and ``qibojit`` backends while the
+`tensorflow.sparse `_ can be
+used for ``tensorflow``. Scipy sparse matrices support algebraic
+operations (addition, subtraction, scalar multiplication), linear algebra
+operations (eigenvalues, eigenvectors, matrix exponentiation) and
+multiplication to dense or other sparse matrices. All these properties are
+inherited by :class:`qibo.hamiltonians.Hamiltonian` objects created
+using sparse matrices. Tensorflow sparse matrices support only multiplication
+to dense matrices. Both backends support calculating Hamiltonian expectation
+values using a sparse Hamiltonian matrix.
+
+.. autoclass:: qibo.hamiltonians.Hamiltonian
+ :members:
+ :member-order: bysource
+
+
+Symbolic Hamiltonian
+^^^^^^^^^^^^^^^^^^^^
+
+Qibo allows the user to define Hamiltonians using ``sympy`` symbols. In this
+case the full Hamiltonian matrix is not constructed unless this is required.
+This makes the implementation more efficient for larger qubit numbers.
+For more information on constructing Hamiltonians using symbols we refer to the
+:ref:`How to define custom Hamiltonians using symbols? ` example.
+
+.. autoclass:: qibo.hamiltonians.SymbolicHamiltonian
+ :members:
+ :member-order: bysource
+
+
+When a :class:`qibo.hamiltonians.SymbolicHamiltonian` is used for time
+evolution then Qibo will automatically perform this evolution using the Trotter
+of the evolution operator. This is done by automatically splitting the Hamiltonian
+to sums of commuting terms, following the description of Sec. 4.1 of
+`arXiv:1901.05824 `_.
+For more information on time evolution we refer to the
+:ref:`How to simulate time evolution? ` example.
+
+In addition to the abstract Hamiltonian models, Qibo provides the following
+pre-coded Hamiltonians:
+
+
+Heisenberg XXZ
+^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.XXZ
+ :members:
+ :member-order: bysource
+
+Non-interacting Pauli-X
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.X
+ :members:
+ :member-order: bysource
+
+Non-interacting Pauli-Y
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.Y
+ :members:
+ :member-order: bysource
+
+Non-interacting Pauli-Z
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.Z
+ :members:
+ :member-order: bysource
+
+Transverse field Ising model
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.TFIM
+ :members:
+ :member-order: bysource
+
+Max Cut
+^^^^^^^
+
+.. autoclass:: qibo.hamiltonians.MaxCut
+ :members:
+ :member-order: bysource
+
+
+.. note::
+ All pre-coded Hamiltonians can be created as
+ :class:`qibo.hamiltonians.Hamiltonian` using ``dense=True``
+ or :class:`qibo.hamiltonians.SymbolicHamiltonian`
+ using the ``dense=False``. In the first case the Hamiltonian is created
+ using its full matrix representation of size ``(2 ** n, 2 ** n)``
+ where ``n`` is the number of qubits that the Hamiltonian acts on. This
+ matrix is used to calculate expectation values by direct matrix multiplication
+ to the state and for time evolution by exact exponentiation.
+ In contrast, when ``dense=False`` the Hamiltonian contains a more compact
+ representation as a sum of local terms. This compact representation can be
+ used to calculate expectation values via a sum of the local term expectations
+ and time evolution via the Trotter decomposition of the evolution operator.
+ This is useful for systems that contain many qubits for which constructing
+ the full matrix is intractable.
+
+_______________________
+
+
+.. _Symbols:
+
+Symbols
+-------
+
+Qibo provides a basic set of symbols which inherit the ``sympy.Symbol`` object
+and can be used to construct :class:`qibo.hamiltonians.SymbolicHamiltonian`
+objects as described in the previous section.
+
+.. autoclass:: qibo.symbols.Symbol
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.symbols.I
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.symbols.X
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.symbols.Y
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.symbols.Z
+ :members:
+ :member-order: bysource
+
+_______________________
+
+
+.. _States:
+
+Execution Outcomes
+------------------
+
+Qibo circuits return different objects when executed depending on what the
+circuit contains and on the settings of the simulation. The following table
+summarizes which outcomes to expect depending on whether:
+
+* the circuit contains noise channels
+* the qubits are measured at the end of the execution
+* some collapse measurement is present in the circuit
+* ``density_matrix`` is set to ``True`` in simulation
+
+.. table::
+
+ +----------+--------------+----------+----------------+------------------------------------------+
+ | Noise | Measurements | Collapse | Density Matrix | Outcome |
+ +==========+==============+==========+================+==========================================+
+ | ❌ | ❌ | ❌ | ❌ / ✅ | :class:`qibo.result.QuantumState` |
+ +----------+--------------+----------+----------------+------------------------------------------+
+ | ❌ | ✅ | ❌ | ❌ / ✅ | :class:`qibo.result.CircuitResult` |
+ +----------+--------------+----------+----------------+------------------------------------------+
+ | ❌ / ✅ | ❌ | ❌ / ✅ | ✅ | :class:`qibo.result.QuantumState` |
+ +----------+--------------+----------+----------------+------------------------------------------+
+ | ❌ / ✅ | ✅ | ❌ / ✅ | ❌ | :class:`qibo.result.MeasurementOutcomes` |
+ +----------+--------------+----------+----------------+------------------------------------------+
+ | ❌ / ✅ | ✅ | ❌ / ✅ | ✅ | :class:`qibo.result.CircuitResult` |
+ +----------+--------------+----------+----------------+------------------------------------------+
+
+Therefore, one of the three objects :class:`qibo.result.QuantumState`,
+:class:`qibo.result.MeasurementOutcomes` or :class:`qibo.result.CircuitResult`
+is going to be returned by the circuit execution. The first gives acces to the final
+state and probabilities via the :meth:`qibo.result.QuantumState.state` and
+:meth:`qibo.result.QuantumState.probabilities` methods, whereas the second
+allows to retrieve the final samples, the frequencies and the probabilities (calculated
+as ``frequencies/nshots``) with the :meth:`qibo.result.MeasurementOutcomes.samples`,
+:meth:`qibo.result.MeasurementOutcomes.frequencies` and
+:meth:`qibo.result.MeasurementOutcomes.probabilities` methods respectively. The
+:class:`qibo.result.CircuitResult` object includes all the above instead.
+
+Every time some measurement is performed at the end of the execution, the result
+will be a ``CircuitResult`` unless the final state could not be represented with the
+current simulation settings, i.e. if some stochasticity is present in the ciruit
+(via noise channels or collapse measurements) and ``density_matrix=False``. In that
+case a simple ``MeasurementOutcomes`` object is returned.
+
+If no measurement is appended at the end of the circuit, the final ``QuantumState``
+is going to be provided as output. However, if the circuit is stochastic,
+``density_matrix`` should be set to ``True`` in order to recover the final state,
+otherwise an error is raised.
+
+The final result of the circuit execution can also be saved to disk and loaded back:
+
+.. testsetup::
+
+ from qibo import gates, Circuit
+
+.. testcode::
+
+ c = Circuit(2)
+ c.add(gates.M(0,1))
+ # this will be a CircuitResult object
+ result = c()
+ # save it to final_result.npy
+ result.dump('final_result.npy')
+ # can be loaded back
+ from qibo.result import load_result
+
+ loaded_result = load_result('final_result.npy')
+
+.. autoclass:: qibo.result.QuantumState
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.result.MeasurementOutcomes
+ :members:
+ :member-order: bysource
+
+.. autoclass:: qibo.result.CircuitResult
+ :members:
+ :member-order: bysource
+
+
+
+.. _Callbacks:
+
+Callbacks
+---------
+
+Callbacks provide a way to calculate quantities on the state vector as it
+propagates through the circuit. Example of such quantity is the entanglement
+entropy, which is currently the only callback implemented in
+:class:`qibo.callbacks.EntanglementEntropy`.
+The user can create custom callbacks by inheriting the
+:class:`qibo.callbacks.Callback` class. The point each callback is
+calculated inside the circuit is defined by adding a :class:`qibo.gates.CallbackGate`.
+This can be added similarly to a standard gate and does not affect the state vector.
+
+.. autoclass:: qibo.callbacks.Callback
+ :members:
+ :member-order: bysource
+
+Entanglement entropy
+^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: qibo.callbacks.EntanglementEntropy
+ :members:
+ :member-order: bysource
+
+Norm
+^^^^
+
+.. autoclass:: qibo.callbacks.Norm
+ :members:
+ :member-order: bysource
+
+Overlap
+^^^^^^^
+
+.. autoclass:: qibo.callbacks.Overlap
+ :members:
+ :member-order: bysource
+
+Energy
+^^^^^^
+
+.. autoclass:: qibo.callbacks.Energy
+ :members:
+ :member-order: bysource
+
+Gap
+^^^
+
+.. autoclass:: qibo.callbacks.Gap
+ :members:
+ :member-order: bysource
+
+
+.. _Solvers:
+
+Solvers
+-------
+
+Solvers are used to numerically calculate the time evolution of state vectors.
+They perform steps in time by integrating the time-dependent Schrodinger
+equation.
+
+.. automodule:: qibo.solvers
+ :members:
+ :member-order: bysource
+
+
+.. _Optimizers:
+
+Optimizers
+----------
+
+Optimizers are used automatically by the ``minimize`` methods of
+:class:`qibo.models.VQE` and :class:`qibo.evolution.AdiabaticEvolution` models.
+The user does not have to use any of the optimizer methods included in the
+current section, however the required options of each optimization method
+can be passed when calling the ``minimize`` method of the respective Qibo
+variational model.
+
+.. automodule:: qibo.optimizers
+ :members:
+ :member-order: bysource
+ :exclude-members: ParallelBFGS
+
+
+.. _Parameter:
+
+Parameter
+---------
+
+It can be useful to define custom parameters in an optimization context. For
+example, the rotational angles which encodes information in a Quantum Neural Network
+are usually built as a combination of features and trainable parameters. For
+doing this, the :class:`qibo.parameter.Parameter` class can be used. It allows
+to define custom parameters which can be inserted into a :class:`qibo.models.circuit.Circuit`.
+Moreover, it automatically precomputes the analytical derivative of the parameter
+function, which can be used to calculate the derivatives of a variational model
+with respect to its parameters.
+
+.. automodule:: qibo.parameter
+ :members:
+ :member-order: bysource
+
+.. _Gradients:
+
+Gradients
+---------
+
+In the context of optimization, particularly when dealing with Quantum Machine
+Learning problems, it is often necessary to calculate the gradients of functions
+that are to be minimized (or maximized). Hybrid methods, which are based on the
+use of classical techniques for the optimization of quantum computation procedures,
+have been presented in the previous section. This approach is very useful in
+simulation, but some classical methods cannot be used when using real circuits:
+for example, in the context of neural networks, the Back-Propagation algorithm
+is used, where it is necessary to know the value of a target function during the
+propagation of information within the network. Using a real circuit, we would not
+be able to access this information without taking a measurement, causing the state
+of the system to collapse and losing the information accumulated up to that moment.
+For this reason, in `qibo` we have also implemented methods for calculating the
+gradients which can be performed directly on the hardware, such as the
+`Parameter Shift Rule`_.
+
+.. automodule:: qibo.derivative
+ :members:
+ :member-order: bysource
+
+.. _`Parameter Shift Rule`: https://arxiv.org/abs/1811.11184
+
+.. _Quantum Information:
+
+Quantum Information
+-------------------
+
+This module provides tools for generation and analysis of quantum (and classical) information.
+
+Basis
+^^^^^
+
+Set of functions related to basis and basis transformations.
+
+
+Pauli basis
+"""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_basis
+
+
+Computational basis to Pauli basis
+""""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.comp_basis_to_pauli
+
+
+Pauli basis to computational basis
+""""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_comp_basis
+
+
+Phase-space Representation of Stabilizer States
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A *stabilizer state* :math:`\ketbra{\psi}{\psi}` can be uniquely defined by
+the set of its *stabilizers*, i.e. those unitary operators :math:`U` that have
+:math:`\psi` as an eigenstate with eigenvalue :math:`1`.
+In general, :math:`n`-qubit stabilizer states are stabilized by :math:`d = 2^n`
+Pauli operators on said :math:`n` qubits.
+However, it is known that the set of :math:`d` Paulis can be generated by only
+:math:`n` unique members of the set.
+In that case, indeed, the number of operators needed to represent a
+stabilizer state reduces to :math:`n`.
+Each one of these :math:`n` Pauli *generators* takes :math:`2n + 1` bits to specify,
+yielding a :math:`n(2n+1)` total number of bits needed.
+In particular, `Aaronson and Gottesman (2004) `_ demonstrated that the application
+of Clifford gates on stabilizer states can be efficiently simulated in this representation
+at the cost of storing the generators of the *destabilizers*, in addition to the stabilizers.
+
+A :math:`n`-qubit stabilizer state is uniquely defined by a symplectic matrix of the form
+
+.. image:: ../_static/symplectic_matrix.png
+ :width: 2329px
+ :height: 1213px
+ :scale: 30 %
+ :align: center
+
+where :math:`(x_{kl},z_{kl})` are the bits encoding the :math:`n`-qubits Pauli generator as
+
+.. math::
+
+ P_{k} = \bigotimes_{l=1}^{n} \, i^{x_{kl} \, \oplus \, z_{kl}} \, X_{l}^{x_{kl}} \, Z_{l}^{z_{kl}}.
+
+The :class:`qibo.quantum_info.clifford.Clifford` object is in charge of storing the
+phase-space representation of a stabilizer state.
+This object is automatically created after the execution of a Clifford circuit through the
+:class:`qibo.backends.clifford.CliffordBackend`, but it can also be created by directly
+passing a symplectic matrix to the constructor.
+
+.. testsetup::
+
+ from qibo.quantum_info import Clifford
+ from qibo.backends import CliffordBackend
+
+ # construct the |00...0> state
+ backend = CliffordBackend("numpy")
+ symplectic_matrix = backend.zero_state(nqubits=3)
+ clifford = Clifford(symplectic_matrix, engine="numpy")
+
+The generators of the stabilizers can be extracted with the
+:meth:`qibo.quantum_info.clifford.Clifford.generators` method,
+or the complete set of :math:`d = 2^{n}` stabilizers operators can be extracted through the
+:meth:`qibo.quantum_info.clifford.Clifford.stabilizers` method.
+
+.. testcode::
+
+ generators, phases = clifford.generators()
+ stabilizers = clifford.stabilizers()
+
+The destabilizers can be extracted analogously with :meth:`qibo.quantum_info.clifford.Clifford.destabilizers`.
+
+We provide integration with the `stim `_ package.
+It is possible to run Clifford circuits using `stim` as an engine:
+
+.. code-block:: python
+
+ from qibo.backends import CliffordBackend
+ from qibo.quantum_info import Clifford, random_clifford
+
+ clifford_backend = CliffordBackend(engine="stim")
+
+ circuit = random_clifford(nqubits)
+ result = clifford_backend.execute_circuit(circuit)
+
+ ## Note that the execution above is equivalent to the one below
+
+ result = Clifford.from_circuit(circuit, engine="stim")
+
+
+.. autoclass:: qibo.quantum_info.clifford.Clifford
+ :members:
+ :member-order: bysource
+
+
+Entanglement measures
+^^^^^^^^^^^^^^^^^^^^^
+
+Set of functions to calculate entanglement measures.
+
+
+Concurrence
+"""""""""""
+
+.. autofunction:: qibo.quantum_info.concurrence
+
+
+Entanglement of formation
+"""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.entanglement_of_formation
+
+
+Entanglement fidelity
+"""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.entanglement_fidelity
+
+
+Meyer-Wallach entanglement
+""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.meyer_wallach_entanglement
+
+
+Entanglement capability
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.entangling_capability
+
+
+Entropy measures
+^^^^^^^^^^^^^^^^
+
+Set of functions to calculate entropy measures.
+
+
+Shannon entropy
+"""""""""""""""
+
+.. autofunction:: qibo.quantum_info.shannon_entropy
+
+
+Classical relative entropy
+""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.classical_relative_entropy
+
+
+Classical Rényi entropy
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.classical_renyi_entropy
+
+
+Classical Rényi relative entropy
+""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.classical_relative_renyi_entropy
+
+
+Classical Tsallis entropy
+"""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.classical_tsallis_entropy
+
+
+von Neumann entropy
+"""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.von_neumann_entropy
+
+.. note::
+ ``check_hermitian`` flag allows the user to choose if the function will check if input
+ ``state`` is Hermitian or not. Default option is ``check_hermitian=False``, i.e. the
+ assumption of Hermiticity. This is faster and, more importantly,
+ this function are intended to be used on Hermitian inputs. When ``check_hermitian=True``
+ and ``state`` is non-Hermitian, an error will be raised when using `cupy` backend.
+
+
+Relative von Neumann entropy
+""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.relative_von_neumann_entropy
+
+.. note::
+ ``check_hermitian`` flag allows the user to choose if the function will check if input
+ ``state`` is Hermitian or not. Default option is ``check_hermitian=False``, i.e. the
+ assumption of Hermiticity. This is faster and, more importantly,
+ this function are intended to be used on Hermitian inputs. When ``check_hermitian=True``
+ and either ``state`` or ``target`` is non-Hermitian,
+ an error will be raised when using `cupy` backend.
+
+
+Rényi entropy
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.renyi_entropy
+
+
+Relative Rényi entropy
+""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.relative_renyi_entropy
+
+
+Tsallis entropy
+"""""""""""""""
+
+.. autofunction:: qibo.quantum_info.tsallis_entropy
+
+
+Entanglement entropy
+""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.entanglement_entropy
+
+.. note::
+ ``check_hermitian`` flag allows the user to choose if the function will check if
+ the reduced density matrix resulting from tracing out ``bipartition`` from input
+ ``state`` is Hermitian or not. Default option is ``check_hermitian=False``, i.e. the
+ assumption of Hermiticity. This is faster and, more importantly,
+ this function are intended to be used on Hermitian inputs. When ``check_hermitian=True``
+ and the reduced density matrix is non-Hermitian, an error will be raised
+ when using `cupy` backend.
+
+
+Metrics
+^^^^^^^
+
+Set of functions that are used to calculate metrics of states, (pseudo-)distance measures
+between states, and distance measures between quantum channels.
+
+Purity
+""""""
+
+.. autofunction:: qibo.quantum_info.purity
+
+
+Impurity
+""""""""
+
+.. autofunction:: qibo.quantum_info.impurity
+
+
+Trace distance
+""""""""""""""
+
+.. autofunction:: qibo.quantum_info.trace_distance
+
+.. note::
+ ``check_hermitian`` flag allows the user to choose if the function will check if difference
+ between inputs, ``state - target``, is Hermitian or not. Default option is
+ ``check_hermitian=False``, i.e. the assumption of Hermiticity, because it is faster and,
+ more importantly, the functions are intended to be used on Hermitian inputs.
+ When ``check_hermitian=True`` and ``state - target`` is non-Hermitian, an error will be
+ raised when using `cupy` backend.
+
+
+Hilbert-Schmidt distance
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hilbert_schmidt_distance
+
+
+Fidelity
+""""""""
+
+.. autofunction:: qibo.quantum_info.fidelity
+
+
+Infidelity
+""""""""""
+
+.. autofunction:: qibo.quantum_info.infidelity
+
+
+Bures angle
+"""""""""""
+
+.. autofunction:: qibo.quantum_info.bures_angle
+
+
+Bures distance
+""""""""""""""
+
+.. autofunction:: qibo.quantum_info.bures_distance
+
+
+Process fidelity
+""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.process_fidelity
+
+
+Process infidelity
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.process_infidelity
+
+
+Average gate fidelity
+"""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.average_gate_fidelity
+
+
+Gate error
+""""""""""
+
+.. autofunction:: qibo.quantum_info.gate_error
+
+
+Diamond Norm
+""""""""""""
+
+.. autofunction:: qibo.quantum_info.diamond_norm
+
+
+Expressibility of parameterized quantum circuits
+""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.expressibility
+
+
+Frame Potential
+"""""""""""""""
+
+.. autofunction:: qibo.quantum_info.frame_potential
+
+
+Linear Algebra Operations
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Collection of linear algebra operations that are commonly used in quantum information theory.
+
+
+Commutator
+""""""""""
+
+.. autofunction:: qibo.quantum_info.commutator
+
+
+Anticommutator
+""""""""""""""
+
+.. autofunction:: qibo.quantum_info.anticommutator
+
+
+Partial trace
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.partial_trace
+
+
+Matrix exponentiation
+"""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.matrix_exponentiation
+
+
+Quantum Networks
+^^^^^^^^^^^^^^^^
+
+Quantum network is an object that unifies the representation of quantum states, channels,
+observables, and higher-order quantum operators.
+
+For more details, see G. Chiribella *et al.*, *Theoretical framework for quantum networks*,
+`Physical Review A 80.2 (2009): 022339
+`_.
+
+
+.. autoclass:: qibo.quantum_info.quantum_networks.QuantumNetwork
+ :members:
+ :member-order: bysource
+
+
+.. autoclass:: qibo.quantum_info.quantum_networks.QuantumComb
+ :members:
+ :member-order: bysource
+
+
+.. autoclass:: qibo.quantum_info.quantum_networks.QuantumChannel
+ :members:
+ :member-order: bysource
+
+
+Random Ensembles
+^^^^^^^^^^^^^^^^
+
+Functions that can generate random quantum objects.
+
+
+Haar-random :math:`U_{3}`
+"""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.uniform_sampling_U3
+
+
+Random Gaussian matrix
+""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_gaussian_matrix
+
+
+Random Hermitian matrix
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_hermitian
+
+
+Random unitary matrix
+"""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_unitary
+
+
+Random quantum channel
+""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_quantum_channel
+
+
+Random statevector
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_statevector
+
+
+Random density matrix
+"""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_density_matrix
+
+
+Random Clifford
+"""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_clifford
+
+
+Random Pauli
+""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_pauli
+
+
+Random Pauli Hamiltonian
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_pauli_hamiltonian
+
+
+Random stochastic matrix
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.random_stochastic_matrix
+
+
+Superoperator Transformations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Functions used to convert superoperators among their possible representations.
+For more in-depth theoretical description of the representations and transformations,
+we direct the reader to
+`Wood, Biamonte, and Cory, Quant. Inf. Comp. 15, 0579-0811 (2015) `_.
+
+
+Vectorization
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.vectorization
+
+.. note::
+ Due to ``numpy`` limitations on handling transposition of tensors,
+ this function will not work when the number of qubits :math:`n`
+ is such that :math:`n > 16`.
+
+
+Unvectorization
+"""""""""""""""
+
+.. autofunction:: qibo.quantum_info.unvectorization
+
+.. note::
+ Due to ``numpy`` limitations on handling transposition of tensors,
+ this function will not work when the number of qubits :math:`n`
+ is such that :math:`n > 16`.
+
+
+To Choi
+"""""""
+
+.. autofunction:: qibo.quantum_info.to_choi
+
+
+To Liouville
+""""""""""""
+
+.. autofunction:: qibo.quantum_info.to_liouville
+
+
+To Pauli-Liouville
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.to_pauli_liouville
+
+
+To Chi
+"""""""
+
+.. autofunction:: qibo.quantum_info.to_chi
+
+
+Choi to Liouville
+"""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.choi_to_liouville
+
+
+Choi to Pauli-Liouville
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.choi_to_pauli
+
+
+Choi to Kraus
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.superoperator_transformations.choi_to_kraus
+
+.. note::
+ Due to the spectral decomposition subroutine in this function, the resulting Kraus
+ operators :math:`\{K_{\alpha}\}_{\alpha}` might contain global phases. That
+ implies these operators are not exactly equal to the "true" Kraus operators
+ :math:`\{K_{\alpha}^{(\text{ideal})}\}_{\alpha}`. However, since these are
+ global phases, the operators' actions are the same, i.e.
+
+ .. math::
+ K_{\alpha} \, \rho \, K_{\alpha}^{\dagger} = K_{\alpha}^{\text{(ideal)}} \, \rho \,\,
+ (K_{\alpha}^{\text{(ideal)}})^{\dagger} \,\,\,\,\, , \,\, \forall \, \alpha
+
+.. note::
+ User can set ``validate_cp=False`` in order to speed up execution by not checking if
+ input map ``choi_super_op`` is completely positive (CP) and Hermitian. However, that may
+ lead to erroneous outputs if ``choi_super_op`` is not guaranteed to be CP. We advise users
+ to either set this flag carefully or leave it in its default setting (``validate_cp=True``).
+
+
+Choi to Chi-matrix
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.choi_to_chi
+
+
+Choi to Stinespring
+"""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.choi_to_stinespring
+
+
+Kraus to Choi
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_choi
+
+
+Kraus to Liouville
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_liouville
+
+
+Kraus to Pauli-Liouville
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_pauli
+
+
+Kraus to Chi-matrix
+"""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_chi
+
+
+Kraus to Stinespring
+""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_stinespring
+
+
+Liouville to Choi
+"""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.liouville_to_choi
+
+
+Liouville to Pauli-Liouville
+""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.liouville_to_pauli
+
+
+Liouville to Kraus
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.liouville_to_kraus
+
+.. note::
+ Due to the spectral decomposition subroutine in this function, the resulting Kraus
+ operators :math:`\{K_{\alpha}\}_{\alpha}` might contain global phases. That
+ implies these operators are not exactly equal to the "true" Kraus operators
+ :math:`\{K_{\alpha}^{(\text{ideal})}\}_{\alpha}`. However, since these are
+ global phases, the operators' actions are the same, i.e.
+
+ .. math::
+ K_{\alpha} \, \rho \, K_{\alpha}^{\dagger} = K_{\alpha}^{\text{(ideal)}} \, \rho \,\,
+ (K_{\alpha}^{\text{(ideal)}})^{\dagger} \,\,\,\,\, , \,\, \forall \, \alpha
+
+
+Liouville to Chi-matrix
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.liouville_to_chi
+
+
+Liouville to Stinespring
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.liouville_to_stinespring
+
+
+Pauli-Liouville to Liouville
+""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_liouville
+
+
+Pauli-Liouville to Choi
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_choi
+
+
+
+Pauli-Liouville to Kraus
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_kraus
+
+
+Pauli-Liouville to Chi-matrix
+"""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_chi
+
+
+Pauli-Liouville to Stinespring
+""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pauli_to_stinespring
+
+
+Chi-matrix to Choi
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.chi_to_choi
+
+
+Chi-matrix to Liouville
+"""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.chi_to_liouville
+
+
+Chi-matrix to Pauli-Liouville
+"""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.chi_to_pauli
+
+
+Chi-matrix to Kraus
+"""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.chi_to_kraus
+
+.. note::
+ Due to the spectral decomposition subroutine in this function, the resulting Kraus
+ operators :math:`\{K_{\alpha}\}_{\alpha}` might contain global phases. That
+ implies these operators are not exactly equal to the "true" Kraus operators
+ :math:`\{K_{\alpha}^{(\text{ideal})}\}_{\alpha}`. However, since these are
+ global phases, the operators' actions are the same, i.e.
+
+ .. math::
+ K_{\alpha} \, \rho \, K_{\alpha}^{\dagger} = K_{\alpha}^{\text{(ideal)}} \, \rho \,\,
+ (K_{\alpha}^{\text{(ideal)}})^{\dagger} \,\,\,\,\, , \,\, \forall \, \alpha
+
+.. note::
+ User can set ``validate_cp=False`` in order to speed up execution by not checking if
+ the Choi representation obtained from the input ``chi_matrix`` is completely positive
+ (CP) and Hermitian. However, that may lead to erroneous outputs if ``choi_super_op``
+ is not guaranteed to be CP. We advise users to either set this flag carefully or leave
+ it in its default setting (``validate_cp=True``).
+
+
+Chi-matrix to Stinespring
+"""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.chi_to_stinespring
+
+
+Stinespring to Choi
+"""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.stinespring_to_choi
+
+
+Stinespring to Liouville
+""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.stinespring_to_liouville
+
+
+Stinespring to Pauli-Liouville
+""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.stinespring_to_pauli
+
+
+Stinespring to Kraus
+""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.stinespring_to_kraus
+
+
+Stinespring to Chi-matrix
+"""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.stinespring_to_chi
+
+
+Kraus operators as probabilistic sum of unitaries
+"""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.kraus_to_unitaries
+
+.. note::
+ It is not guaranteed that a good approximation will be found or that any
+ approximation will be found at all. This functions will find good solutions
+ for a limited set of operators. We leave to the user to decide how to
+ best use this function.
+
+
+Utility Functions
+^^^^^^^^^^^^^^^^^
+
+Functions that can be used to calculate metrics and distance measures
+on classical probability arrays.
+
+Hamming weight
+""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hamming_weight
+
+
+Hamming distance
+""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hamming_distance
+
+
+Hadamard Transform
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hadamard_transform
+
+
+Hellinger distance
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hellinger_distance
+
+
+Hellinger fidelity
+""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hellinger_fidelity
+
+
+Hellinger shot error
+""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.hellinger_fidelity
+
+
+Haar integral
+"""""""""""""
+
+.. autofunction:: qibo.quantum_info.haar_integral
+
+
+Parameterized quantum circuit integral
+""""""""""""""""""""""""""""""""""""""
+
+.. autofunction:: qibo.quantum_info.pqc_integral
+
+
+.. _GST:
+
+
+
+Tomography
+----------
+
+Functions used to classically simulate tomography protocols.
+
+
+Gate Set Tomography
+^^^^^^^^^^^^^^^^^^^
+
+Gate Set Tomography (GST) is a powerful technique employed in quantum information processing
+to characterize the behavior of quantum gates on quantum hardware [1, 2, 3].
+The primary objective of GST is to provide a robust framework for obtaining a representation
+of quantum gates within a predefined gate set when subjected to noise inherent to the
+quantum hardware.
+
+By characterizing the impact of noise on quantum gates, GST enables the identification and
+quantification of errors, laying the groundwork for subsequent error mitigation strategies.
+The insights gained from GST are instrumental, for instance, in setting up the necessary
+parameters for Probabilistic Error Cancellation (PEC).
+
+In practice, given a set of operators (or gates), :math:`\mathcal{O}=\{O_0, O_1, \dots, O_n\}`,
+a set of initial states :math:`\{\rho_k\}`, and a set of measurement bases :math:`\{M_j\}`,
+one performs GST on the :math:`l`-th operator by choosing an initial state :math:`\rho_k`,
+applying the gate :math:`O_l \in \mathcal{O}`, measuring in the :math:`M_j` basis in order to
+obtain the following matrix:
+
+.. math::
+ \{\tilde{O}_l\}_{jk} = \text{tr}(M_j\,O_l\,\rho_k) \, ,
+
+which provides an estimated representation of the operator :math:`O_l` in the specific system.
+
+This implementation makes use, in particular, of
+:math:`\rho_k \in \{ \ketbra{0}{0}, \ketbra{1}{1}, \ketbra{+}{+}, \ketbra{y+}{y+} \}^{\otimes n}` and
+:math:`M_j \in \{ I, X, Y, Z\}^{\otimes n}` [4], with :math:`n\in\{1,2\}`
+being the number of qubits. However, :math:`\{\tilde{O}_l\}_{jk}` is not yet given in
+the Pauli-Liouville representation (also known as *Pauli Transfer Matrix*).
+To obtain the Pauli-Liouville representation, one needs the two matrices, described below.
+The matrix :math:`\tilde{g}` has its elements :math:`\tilde{g}_{jk}` defined as
+
+.. math::
+ \tilde{g}_{jk} = \text{tr}(M_j\,\rho_k) \, ,
+
+which is obtained by measuring the initial states :math:`\{\rho_k\}` in each basis element :math:`\{M_j\}`
+without any gates' application.
+The *gauge matrix* :math:`T` is given by
+
+.. math::
+ T = \begin{pmatrix}
+ 1 & 1 & 1 & 1 \\
+ 0 & 0 & 1 & 0 \\
+ 0 & 0 & 0 & 1 \\
+ 1 & -1 & 0 & 0 \\
+ \end{pmatrix} \, .
+
+This is the matrix, in a common gauge, implementing a change of basis.
+Therefore, the Pauli-Liouville representation can be recovered as
+
+.. math::
+ O_l^{PL} = T\,g^{-1}\,\tilde{O_l}\,T^{-1} \, .
+
+References:
+ 1. R. Blume-Kohout *et al*.
+ *Robust, self-consistent, closed-form tomography of quantum logic gates on a trapped ion qubit*
+ (2013), `arXiv:1310.4492 `_.
+
+ 2. D. Greenbaum, *Introduction to quantum gate set tomography* (2015),
+ `arXiv:1509.02921 `_.
+
+ 3. E. Nielsen *et al.*, *Gate set tomography* (2021),
+ `Quantum 5, 557 `_.
+
+ 4. S. Endo, S. C. Benjamin, and Y. Li,
+ *Practical quantum error mitigation for near-future applications* (2018),
+ `Physical Review X 8.3: 031027 `_.
+
+
+.. autofunction:: qibo.tomography.gate_set_tomography.GST
+
+
+
+.. _Parallel:
+
+Parallelism
+-----------
+
+We provide CPU multi-processing methods for circuit evaluation for multiple
+input states and multiple parameters for fixed input state.
+
+When using the methods below the ``processes`` option controls the number of
+processes used by the parallel algorithms through the ``multiprocessing``
+library. By default ``processes=None``, in this case the total number of logical
+cores are used. Make sure to select the appropriate number of processes for your
+computer specification, taking in consideration memory and physical cores. In
+order to obtain optimal results you can control the number of threads used by
+each process with the ``qibo.set_threads`` method. For example, for small-medium
+size circuits you may benefit from single thread per process, thus set
+``qibo.set_threads(1)`` before running the optimization.
+
+.. automodule:: qibo.parallel
+ :members:
+ :member-order: bysource
+ :exclude-members: ParallelResources
+
+.. _Backends:
+
+Backends
+--------
+
+The main calculation engine is defined in the abstract backend object
+:class:`qibo.backends.abstract.Backend`. This object defines the methods
+required by all Qibo models to perform simulation.
+
+Qibo currently provides two different calculation backends, one based on
+numpy and one based on Tensorflow. It is possible to define new backends by
+inheriting :class:`qibo.backends.abstract.Backend` and implementing
+its abstract methods.
+
+An additional backend is shipped as the separate library qibojit.
+This backend is supplemented by custom operators defined under which can be
+used to efficiently apply gates to state vectors or density matrices.
+
+We refer to :ref:`Packages ` section for a complete list of the
+available computation backends and instructions on how to install each of
+these libraries on top of qibo.
+
+Custom operators are much faster than implementations based on numpy or Tensorflow
+primitives, such as ``einsum``, but do not support some features, such as
+automatic differentiation for backpropagation of variational circuits which is
+only supported by the native ``tensorflow`` backend.
+
+The user can switch backends using
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("qibojit")
+ qibo.set_backend("numpy")
+
+before creating any circuits or gates. The default backend is the first available
+from ``qibojit``, ``pytorch``, ``tensorflow``, ``numpy``.
+
+Some backends support different platforms. For example, the qibojit backend
+provides two platforms (``cupy`` and ``cuquantum``) when used on GPU.
+The active platform can be switched using
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("qibojit", platform="cuquantum")
+ qibo.set_backend("qibojit", platform="cupy")
+
+The default backend order is qibojit (if available), tensorflow (if available),
+numpy. The default backend can be changed using the ``QIBO_BACKEND`` environment
+variable.
+
+Qibo optionally provides an interface to `qulacs `_ through the :class:`qibo.backends.qulacs.QulacsBackend`. To use ``qulacs`` for simulating a quantum circuit you can globally set the backend as in the other cases
+
+.. testcode:: python
+
+ import qibo
+ qibo.set_backend("qulacs")
+
+.. note::
+ GPU simulation through ``qulacs`` is not supported yet.
+
+.. autoclass:: qibo.backends.abstract.Backend
+ :members:
+ :member-order: bysource
+
+.. _Clifford:
+
+Clifford Simulation
+^^^^^^^^^^^^^^^^^^^
+
+A special backend in qibo supports the simulation of Clifford circuits.
+This :class:`qibo.backends.clifford.CliffordBackend` backend implements the phase-space formalism
+introduced in `https://arxiv.org/abs/quant-ph/0406196 `_ to efficiently simulate gate
+application and measurements sampling in the stabilizers state representation.
+The execution of a circuit through this backend creates a
+:class:`qibo.quantum_info.clifford.Clifford` object that gives access to the final measured
+samples through the :meth:`qibo.quantum_info.clifford.Clifford.samples` method,
+similarly to :class:`qibo.result.CircuitResult`.
+The probabilities and frequencies are computed starting from the samples by the
+:meth:`qibo.quantum_info.clifford.Clifford.frequencies` and
+:meth:`qibo.quantum_info.clifford.Clifford.probabilities` methods.
+
+.. _aaronson: https://arxiv.org/abs/quant-ph/0406196
+
+It is also possible to recover the standard state representation with the
+:meth:`qibo.quantum_info.clifford.Clifford.state` method.
+Note, however, that this process is inefficient as it involves the construction of all the
+stabilizers starting from the generators encoded inside the symplectic matrix.
+
+As for the other backends, the Clifford backend can be set with
+
+.. testcode:: python
+
+ import qibo
+ qibo.set_backend("clifford", platform="numpy")
+
+by specifying the engine used for calculation, if not provided the current :class:`qibo.backends.GlobalBackend` is used
+
+.. testcode:: python
+
+ import qibo
+
+ # setting numpy as the global backend
+ qibo.set_backend("numpy")
+ # the clifford backend will use the numpy backend as engine
+ backend = qibo.backends.CliffordBackend()
+
+Alternatively, a Clifford circuit can also be executed starting from the :class:`qibo.quantum_info.clifford.Clifford` object
+
+.. code-block:: python
+
+ from qibo.quantum_info import Clifford, random_clifford
+
+ nqubits = 2
+ circuit = random_clifford(nqubits)
+ result = Clifford.from_circuit(circuit)
+
+.. autoclass:: qibo.backends.clifford.CliffordBackend
+ :members:
+ :member-order: bysource
+
+Cloud Backends
+^^^^^^^^^^^^^^
+
+Additional backends that support the remote execution of quantum circuits through
+cloud service providers, such as IBM and QRC-TII, are provided by the optional qibo plugin
+`qibo-cloud-backends `_.
+For more information please refer to the
+`official documentation `_.
diff --git a/doc/source/appendix/citing-qibo.rst b/doc/source/appendix/citing-qibo.rst
new file mode 100644
index 000000000..7d08dc2e2
--- /dev/null
+++ b/doc/source/appendix/citing-qibo.rst
@@ -0,0 +1,201 @@
+Publications
+============
+
+If Qibo has been significant in your research, and you would like to acknowledge
+the project in your academic publication, we suggest citing the following documents:
+
+Peer-Reviewed Articles
+----------------------
+
+* S. Efthymiou, S. Ramos-Calderer, C. Bravo-Prieto, A. Pérez-Salinas, D.
+ Garcı́a-Martı́n, A. Garcia-Saez, J. I. Latorre, S. Carrazza, *Qibo: a
+ framework for quantum simulation with hardware acceleration*, Quantum Science
+ and Technology 7 (1) (2021) 015018. `doi:10.1088/2058-9565/ac39f5`_,
+ (`arXiv:2009.01845`_).
+
+ In *BibTeX* format:
+
+ .. code-block:: text
+
+ @article{qibo_paper,
+ doi = {10.1088/2058-9565/ac39f5},
+ url = {https://doi.org/10.1088/2058-9565/ac39f5},
+ year = 2021,
+ month = {dec},
+ publisher = {{IOP} Publishing},
+ volume = {7},
+ number = {1},
+ pages = {015018},
+ author = {Stavros Efthymiou and
+ Sergi Ramos-Calderer and
+ Carlos Bravo-Prieto and
+ Adri{\'{a}}n P{\'{e}}rez-Salinas and
+ Diego Garc{\'{\i}}a-Mart{\'{\i}}n and
+ Artur Garcia-Saez and
+ Jos{\'{e}} Ignacio Latorre and
+ Stefano Carrazza},
+ title = {Qibo: a framework for quantum simulation with hardware acceleration},
+ journal = {Quantum Science and Technology},
+ }
+
+.. _`doi:10.1088/2058-9565/ac39f5`: https://doi.org/10.1088/2058-9565/ac39f5
+.. _`arXiv:2009.01845`: https://arxiv.org/abs/2009.01845
+
+* S. Efthymiou, M. Lazzarin, A. Pasquale, S. Carrazza, *Quantum simulation with
+ just-in-time compilation*, Quantum (6) (2022).
+ `doi:10.22331/q-2022-09-22-814`_, (`arXiv:2203.08826`_).
+
+ In *BibTeX* format:
+
+ .. code-block:: text
+
+ @article{qibojit_paper,
+ doi = {10.22331/q-2022-09-22-814},
+ url = {https://doi.org/10.22331/q-2022-09-22-814},
+ title = {Quantum simulation with just-in-time compilation},
+ author = {Efthymiou, Stavros and Lazzarin, Marco and Pasquale, Andrea and Carrazza, Stefano},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {6},
+ pages = {814},
+ month = sep,
+ year = {2022}
+ }
+
+.. _`doi:10.22331/q-2022-09-22-814`: https://doi.org/10.22331/q-2022-09-22-814
+.. _`arXiv:2203.08826`: https://arxiv.org/abs/2203.08826
+
+* S. Efthymiou, A. Orgaz-Fuertes, R. Carobene, J. Cereijo, A. Pasquale, S.
+ Ramos-Calderer, S. Bordoni, D. Fuentes-Ruiz, A. Candido, E. Pedicillo, M.
+ Robbiati, Y.P. Tan, J. Wilkens, I. Roth, J.I. Latorre, S. Carrazza, *Qibolab:
+ an open-source hybrid quantum operating system* (2023),
+ `doi:10.22331/q-2024-02-12-1247`_, (`arXiv:2308.06313`_).
+
+ In *BibTeX* format:
+
+ .. code-block:: text
+
+ @article{qibolab_paper,
+ doi = {10.22331/q-2024-02-12-1247},
+ url = {https://doi.org/10.22331/q-2024-02-12-1247},
+ title = {Qibolab: an open-source hybrid quantum operating system},
+ author = {Efthymiou, Stavros and Orgaz-Fuertes, Alvaro and Carobene, Rodolfo and Cereijo, Juan and Pasquale, Andrea and Ramos-Calderer, Sergi and Bordoni, Simone and Fuentes-Ruiz, David and Candido, Alessandro and Pedicillo, Edoardo and Robbiati, Matteo and Tan, Yuanzheng Paul and Wilkens, Jadwiga and Roth, Ingo and Latorre, Jos{\'{e}} Ignacio and Carrazza, Stefano},
+ journal = {{Quantum}},
+ issn = {2521-327X},
+ publisher = {{Verein zur F{\"{o}}rderung des Open Access Publizierens in den Quantenwissenschaften}},
+ volume = {8},
+ pages = {1247},
+ month = feb,
+ year = {2024}
+ }
+
+.. _`doi:10.22331/q-2024-02-12-1247`: https://doi.org/10.22331/q-2024-02-12-1247
+.. _`arXiv:2308.06313`: https://arxiv.org/abs/2308.06313
+
+* R. Carobene, A. Candido, J. Serrano, A.O-Fuertes, A. Giachero, S. Carrazza,
+ *Qibosoq: an open-source framework for quantum circuit RFSoC programming*
+ (2023), (`arXiv:2310.05851`_)
+
+.. _`arXiv:2310.05851`: https://arxiv.org/abs/2310.05851
+
+Software References in Zenodo
+-----------------------------
+
+* S. Efthymiou, S. Ramos-Calderer, C. Bravo-Prieto, A.
+ Pérez-Salinas, D. García-Martín, A. Garcia-Saez, J. I. Latorre, S. Carrazza.
+ (2020). qiboteam/qibo: Qibo. Zenodo. `https://doi.org/10.5281/zenodo.3997194`_.
+
+.. _`https://doi.org/10.5281/zenodo.3997194`: https://doi.org/10.5281/zenodo.3997194
+
+* S. Efthymiou, M. Lazzarin, A. Pasquale and S. Carrazza. (2021). qiboteam/qibojit: Qibojit. Zenodo.
+ `https://doi.org/10.5281/zenodo.5248470`_.
+
+.. _`https://doi.org/10.5281/zenodo.5248470`: https://doi.org/10.5281/zenodo.5248470
+
+
+* S. Efthymiou, A. Orgaz, S. Carrazza, A. Pasquale, D.
+ Fuentes Ruiz, M. Lazzarin, S. Bordoni, E. Pedicillo, P.
+ Tan and M. Hantute. (2023). qiboteam/qibolab: Qibolab. Zenodo.
+ `https://doi.org/10.5281/zenodo.7748527`_.
+
+.. _`https://doi.org/10.5281/zenodo.7748527`: https://doi.org/10.5281/zenodo.7748527
+
+* A. Pasquale, S. Efthymiou, D. Fuentes Ruiz, E. Pedicillo, S.
+ Carrazza, A. Orgaz, A. Sopena, A. Candido, M. Robbiati and M.
+ Hantute (2023). qiboteam/qibocal: Qibocal. Zenodo.
+ `https://doi.org/10.5281/zenodo.7662185`_.
+
+.. _`https://doi.org/10.5281/zenodo.7662185`: https://doi.org/10.5281/zenodo.7662185
+
+* R. Carobene, A. Candido, J. Serrano, S. Carrazza, E. Pedicillo. (2023).
+ qiboteam/qibosoq: Qibosoq. Zenodo. `https://doi.org/10.5281/zenodo.8083285`_.
+
+.. _`https://doi.org/10.5281/zenodo.8083285`: https://doi.org/10.5281/zenodo.8083285
+
+
+
+Conference Proceedings
+----------------------
+
+* S. Carrazza, S. Efthymiou, M. Lazzarin, A. Pasquale. An open-source modular
+ framework for quantum computing. (2022) `ACAT2021`_, (`arXiv:2202.07017`_).
+
+.. _`ACAT2021`: https://indico.cern.ch/event/855454/
+.. _`arXiv:2202.07017`: https://arxiv.org/abs/2202.07017
+
+* M. Robbiati, S. Efthymiou, A. Pasquale, S. Carrazza.
+ A quantum analytical Adam descent through parameter shift rule using Qibo. (2022) `ICHEP2022`_, (`arXiv:2210.10787`_).
+
+.. _`ICHEP2022`: https://www.ichep2022.it/
+.. _`arXiv:2210.10787`: https://arxiv.org/abs/2210.10787
+
+* A. Pasquale, S. Efthymiou, S. Ramos-Calderer, J. Wilkens, I, Roth, S. Carrazza.
+ Towards an open-source framework to perform quantum calibration and characterization. (2023) `ACAT22`_, (`arXiv:2303.10397`_).
+
+.. _`ACAT22`: https://indico.cern.ch/event/1106990/
+.. _`arXiv:2303.10397`: https://arxiv.org/pdf/2303.10397
+
+
+
+Based on qibo
+-------------
+
+* A. Pérez-Salinas, J. M. Cruz-Martinez, Abdulla A. Alhajri, S. Carrazza.
+ Determining the proton content with a quantum computer. `Phys.Rev.D 103 (2021) 3, 034027`_,
+ (`arXiv:2011.13934`_).
+
+.. _`Phys.Rev.D 103 (2021) 3, 034027`: https://journals.aps.org/prd/abstract/10.1103/PhysRevD.103.034027
+.. _`arXiv:2011.13934`: https://arxiv.org/abs/2011.13934
+
+* C. Bravo-Prieto, J. Baglio, M. Cè, A. Francis, Dorota M. Grabowska, S. Carrazza.
+ Style-based quantum generative adversarial networks for Monte Carlo events.
+ `Quantum 6 (2022) 777`_, (`arXiv:2110.06933`_).
+
+.. _`Quantum 6 (2022) 777`: https://quantum-journal.org/papers/q-2022-08-17-777/
+.. _`arXiv:2110.06933`: https://arxiv.org/abs/2110.06933
+
+
+* M. Robbiati, J. M. Cruz-Martinez, S. Carrazza. Determining probability density
+ functions with adiabatic quantum computing. (2023), (`arXiv:2303.11346`_).
+
+.. _`arXiv:2303.11346`: https://arxiv.org/abs/2303.11346
+
+* J. M. Cruz-Martinez, M. Robbiati, S. Carrazza, Multi-variable integration with
+ a variational quantum circuit. (2023), (`arXiv:2308.05657`_).
+
+.. _`arXiv:2308.05657`: https://arxiv.org/abs/2308.05657
+
+
+
+Authorship Guideline
+--------------------
+
+In order to appear as an author of a Qibo publication (paper, proceedings, etc)
+each author must fullfil the following requirements:
+
+* Participate to the official meetings.
+
+* Contribute to the code with documented commits.
+
+* Contribute to the manuscript elaboration.
diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst
new file mode 100644
index 000000000..50ebdef53
--- /dev/null
+++ b/doc/source/code-examples/advancedexamples.rst
@@ -0,0 +1,2175 @@
+Advanced examples
+=================
+
+Here are a few short advanced `how to` examples.
+
+.. _gpu-examples:
+
+How to select hardware devices?
+-------------------------------
+
+Qibo supports execution on different hardware configurations including CPU with
+multi-threading, single GPU and multiple GPUs. Here we provide some useful
+information on how to control the devices that Qibo uses for circuit execution
+in order to maximize performance for the available hardware configuration.
+
+Switching between CPU and GPU
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If a GPU with CUDA support is available in the system and tensorflow or qibojit (cupy)
+are installed then circuits will be executed on the GPU automatically unless the user
+specifies otherwise. One can change the default simulation device using ``qibo.set_device``:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_device("/CPU:0")
+ final_state = c() # circuit will now be executed on CPU
+
+The syntax of device names follows the pattern ``'/{device type}:{device number}'``
+where device type can be CPU or GPU and the device number is an integer that
+distinguishes multiple devices of the same type starting from 0. For more details
+we refer to `Tensorflow's tutorial `_
+on manual device placement.
+Alternatively, running the command ``CUDA_VISIBLE_DEVICES=""`` in a terminal
+hides CUDA GPUs from this terminal session.
+
+In most cases the GPU accelerates execution compared to CPU, however the
+following limitations should be noted:
+
+* For small circuits (less than 10 qubits) the overhead from casting tensors on
+ GPU may be larger than executing the circuit on CPU, making CPU execution
+ preferable. In such cases disabling CPU multi-threading may also increase
+ performance (see next subsection).
+* A standard GPU has 12-16GB of memory and thus can simulate up to 30 qubits on
+ single-precision or 29 qubits with double-precision when Qibo's default gates
+ are used. For larger circuits one should either use the CPU (assuming it has
+ more memory) or a distributed circuit configuration. The latter allows splitting
+ the state vector on multiple devices and is useful both when multiple GPUs are
+ available in the system or even for re-using a single GPU (see relevant
+ subsection bellow).
+
+Note that if the used device runs out of memory during a circuit execution an error will be
+raised prompting the user to switch the default device using ``qibo.set_device``.
+
+Setting the number of CPU threads
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Qibo by default uses the ``qibojit`` backend which is based on
+custom operators. This backend uses OpenMP instructions for parallelization.
+In most cases, utilizing all available CPU threads provides better performance.
+However, for small circuits the parallelization overhead may decrease
+performance making single thread execution preferrable.
+
+You can restrict the number of threads by exporting the ``OMP_NUM_THREADS``
+environment variable with the requested number of threads before launching Qibo,
+or programmatically, during runtime, as follows:
+
+.. testsetup::
+
+ import qibo
+ qibo.set_backend("qibojit")
+
+.. testcode::
+
+ import qibo
+ # set the number of threads to 1
+ qibo.set_threads(1)
+ # retrieve the current number of threads
+ current_threads = qibo.get_threads()
+
+On the other hand, when using the ``tensorflow`` backend Qibo inherits
+Tensorflow's defaults for CPU thread configuration.
+Tensorflow allows restricting the number of threads as follows:
+
+.. code-block:: python
+
+ import tensorflow as tf
+ tf.config.threading.set_inter_op_parallelism_threads(1)
+ tf.config.threading.set_intra_op_parallelism_threads(1)
+ import qibo
+
+Note that this should be run during Tensorflow initialization in the beginning
+of the script and before creating the qibo backend.
+
+Using multiple GPUs
+^^^^^^^^^^^^^^^^^^^
+
+Qibo supports distributed circuit execution on multiple GPUs. This feature can
+be used as follows:
+
+.. code-block:: python
+
+ from qibo import Circuit, gates
+
+ # Define GPU configuration
+ accelerators = {"/GPU:0": 3, "/GPU:1": 1}
+ # this will use the first GPU three times and the second one time
+ # leading to four total logical devices
+ # construct the distributed circuit for 32 qubits
+ c = Circuit(32, accelerators)
+
+Gates can then be added normally using ``c.add`` and the circuit can be executed
+using ``c()``. Note that a ``memory_device`` is passed in the distributed circuit
+(if this is not passed the CPU will be used by default). This device does not perform
+any gate calculations but is used to store the full state. Therefore the
+distributed simulation is limited by the amount of CPU memory.
+
+Also, note that it is possible to reuse a single GPU multiple times increasing the number of
+"logical" devices in the distributed calculation. This allows users to execute
+circuits with more than 30 qubits on a single GPU by reusing several times using
+``accelerators = {"/GPU:0": ndevices}``. Such a simulation will be limited
+by CPU memory only.
+
+For systems without GPUs, the distributed implementation can be used with any
+type of device. For example if multiple CPUs, the user can pass these CPUs in the
+accelerator dictionary.
+
+Distributed circuits are generally slower than using a single GPU due to communication
+bottleneck. However for more than 30 qubits (which do not fit in single GPU) and
+specific applications (such as the QFT) the multi-GPU scheme can be faster than
+using only CPU.
+
+Note that simulating a circuit using multiple GPUs partitions the state in
+multiple pieces which are distributed to the different devices.
+Creating the full state as a single tensor would require merging
+these pieces and using twice as much memory. This is disabled by default,
+however the user may create the full state as follows:
+
+.. code-block:: python
+
+ # Create distributed circuits for two GPUs
+ c = Circuit(32, {"/GPU:0": 1, "/GPU:1": 1})
+ # Add gates
+ c.add(...)
+ # Execute (``result`` will be a ``DistributedState``)
+ result = c()
+
+ # ``DistributedState`` supports indexing and slicing
+ print(result[40])
+ # will print the 40th component of the final state vector
+ print(result[20:25])
+ # will print the components from 20 to 24 (inclusive)
+
+ # Access the full state (will double memory usage)
+ final_state = result.state()
+ # ``final_state`` is a ``tf.Tensor``
+
+
+How to use callbacks?
+---------------------
+
+Callbacks allow the user to apply additional functions on the state vector
+during circuit execution. An example use case of this is the calculation of
+entanglement entropy as the state propagates through a circuit. This can be
+implemented easily using :class:`qibo.callbacks.EntanglementEntropy`
+and the :class:`qibo.gates.CallbackGate` gate. For example:
+
+.. testcode::
+
+ from qibo import models, gates, callbacks
+
+ # create entropy callback where qubit 0 is the first subsystem
+ entropy = callbacks.EntanglementEntropy([0])
+
+ # initialize circuit with 2 qubits and add gates
+ c = models.Circuit(2) # state is |00> (entropy = 0)
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation in the initial state
+ c.add(gates.H(0)) # state is |+0> (entropy = 0)
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation after H
+ c.add(gates.CNOT(0, 1)) # state is |00> + |11> (entropy = 1))
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation after CNOT
+
+ # execute the circuit using the callback
+ final_state = c()
+
+The results can be accessed using indexing on the callback objects. In this
+example ``entropy[:]`` will return ``[0, 0, 1]`` which are the
+values of entropy after every gate in the circuit.
+
+The same callback object can be used in a second execution of this or a different
+circuit. For example
+
+.. testsetup::
+
+ from qibo import models, gates, callbacks
+
+ # create entropy callback where qubit 0 is the first subsystem
+ entropy = callbacks.EntanglementEntropy([0])
+
+ # initialize circuit with 2 qubits and add gates
+ c = models.Circuit(2) # state is |00> (entropy = 0)
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation in the initial state
+ c.add(gates.H(0)) # state is |+0> (entropy = 0)
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation after H
+ c.add(gates.CNOT(0, 1)) # state is |00> + |11> (entropy = 1))
+ c.add(gates.CallbackGate(entropy)) # performs entropy calculation after CNOT
+
+ # execute the circuit using the callback
+ final_state = c()
+
+.. testcode::
+
+ # c is the same circuit as above
+ # execute the circuit
+ final_state = c()
+ # execute the circuit a second time
+ final_state = c()
+
+ # print result
+ print(entropy[:]) # [0, 0, 1, 0, 0, 1]
+.. testoutput::
+ :hide:
+
+ ...
+
+The callback for entanglement entropy can also be used on state vectors directly.
+For example
+
+
+.. _params-examples:
+
+How to use parametrized gates?
+------------------------------
+
+Some Qibo gates such as rotations accept values for their free parameter. Once
+such gates are added in a circuit their parameters can be updated using the
+:meth:`qibo.models.circuit.Circuit.set_parameters` method. For example:
+
+.. testcode::
+
+ from qibo import Circuit, gates
+ # create a circuit with all parameters set to 0.
+ c = Circuit(3)
+ c.add(gates.RX(0, theta=0))
+ c.add(gates.RY(1, theta=0))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.fSim(0, 2, theta=0, phi=0))
+ c.add(gates.H(2))
+
+ # set new values to the circuit's parameters
+ params = [0.123, 0.456, (0.789, 0.321)]
+ c.set_parameters(params)
+
+initializes a circuit with all gate parameters set to 0 and then updates the
+values of these parameters according to the ``params`` list. Alternatively the
+user can use ``circuit.set_parameters()`` with a dictionary or a flat list.
+The keys of the dictionary should be references to the gate objects of
+the circuit. For example:
+
+.. testsetup::
+
+ from qibo import Circuit, gates
+
+.. testcode::
+
+ c = Circuit(3)
+ g0 = gates.RX(0, theta=0)
+ g1 = gates.RY(1, theta=0)
+ g2 = gates.fSim(0, 2, theta=0, phi=0)
+ c.add([g0, g1, gates.CZ(1, 2), g2, gates.H(2)])
+
+ # set new values to the circuit's parameters using a dictionary
+ params = {g0: 0.123, g1: 0.456, g2: (0.789, 0.321)}
+ c.set_parameters(params)
+ # equivalently the parameter's can be update with a list as
+ params = [0.123, 0.456, (0.789, 0.321)]
+ c.set_parameters(params)
+ # or with a flat list as
+ params = [0.123, 0.456, 0.789, 0.321]
+ c.set_parameters(params)
+
+If a list is given then its length and elements should be compatible with the
+parametrized gates contained in the circuit. If a dictionary is given then its
+keys should be all the parametrized gates in the circuit.
+
+The following gates support parameter setting:
+
+* ``RX``, ``RY``, ``RZ``, ``U1``, ``CU1``: Accept a single ``theta`` parameter.
+* :class:`qibo.gates.fSim`: Accepts a tuple of two parameters ``(theta, phi)``.
+* :class:`qibo.gates.GeneralizedfSim`: Accepts a tuple of two parameters
+ ``(unitary, phi)``. Here ``unitary`` should be a unitary matrix given as an
+ array or ``tf.Tensor`` of shape ``(2, 2)``.
+* :class:`qibo.gates.Unitary`: Accepts a single ``unitary`` parameter. This
+ should be an array or ``tf.Tensor`` of shape ``(2, 2)``.
+
+Note that a ``np.ndarray`` or a ``tf.Tensor`` may also be used in the place of
+a flat list. Using :meth:`qibo.models.circuit.Circuit.set_parameters` is more
+efficient than recreating a new circuit with new parameter values. The inverse
+method :meth:`qibo.models.circuit.Circuit.get_parameters` is also available
+and returns a list, dictionary or flat list with the current parameter values
+of all parametrized gates in the circuit.
+
+It is possible to hide a parametrized gate from the action of
+:meth:`qibo.models.circuit.Circuit.get_parameters` and
+:meth:`qibo.models.circuit.Circuit.set_parameters` by setting
+the ``trainable=False`` during gate creation. For example:
+
+.. testsetup::
+
+ from qibo import Circuit, gates
+
+.. testcode::
+
+ c = Circuit(3)
+ c.add(gates.RX(0, theta=0.123))
+ c.add(gates.RY(1, theta=0.456, trainable=False))
+ c.add(gates.fSim(0, 2, theta=0.789, phi=0.567))
+
+ print(c.get_parameters())
+ # prints [(0.123,), (0.789, 0.567)] ignoring the parameters of the RY gate
+
+.. testoutput::
+
+ [(0.123,), (0.789, 0.567)]
+
+
+This is useful when the user wants to freeze the parameters of specific
+gates during optimization.
+Note that ``trainable`` defaults to ``True`` for all parametrized gates.
+
+
+.. _collapse-examples:
+
+How to collapse state during measurements?
+------------------------------------------
+
+As mentioned in the :ref:`How to perform measurements? `
+measurement can by default be used only in the end of the circuit and they do
+not have any effect on the state. In this section we describe how to collapse
+the state during measurements and re-use measured qubits in the circuit.
+Collapsing the state means projecting to the ``|0>`` or ``|1>`` subspace according to
+the sampled result for each measured qubit.
+
+The state is collapsed when the ``collapse=True`` is used during instantiation
+of the :class:`qibo.gates.M` gate. For example
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(1, density_matrix=True)
+ c.add(gates.H(0))
+ output = c.add(gates.M(0, collapse=True))
+ c.add(gates.H(0))
+ result = c(nshots=1)
+ print(result)
+ # prints |+><+| if 0 is measured
+ # or |-><-| if 1 is measured
+.. testoutput::
+ :hide:
+
+ ...
+
+In this example the single qubit is measured while in the state (``|0> + |1>``) and
+is collapsed to either ``|0>`` or ``|1>``. The qubit can then be re-used by adding more
+gates that act to this. The outcomes of ``collapse=True`` measurements is not
+contained in the final result object but is accessible from the `output` object
+returned when adding the gate to the circuit. ``output`` supports the
+``output.samples()`` and ``output.frequencies()`` functionality as described
+in :ref:`How to perform measurements? `.
+
+Collapse gates are single-shot by default because the state collapse is not
+well-defined for more than one shots. If the user passes the ``nshots`` arguments
+during the circuit execution (eg. ``result = c(nshots=100)`` in the above
+example), then the circuit execution will be repeated ``nshots`` times using
+a loop:
+
+.. testsetup::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(1, density_matrix=True)
+ c.add(gates.H(0))
+ output = c.add(gates.M(0, collapse=True))
+ c.add(gates.H(0))
+ nshots = 100
+
+.. testcode::
+
+ for _ in range(nshots):
+ result = c()
+
+Note that this will be more time-consuming compared to multi-shot simulation
+of standard (non-collapse) measurements where the circuit is simulated once and
+the final state vector is sampled ``nshots`` times. For multi-shot simulation
+the outcomes are still accessible using ``output.samples()`` and
+``output.frequencies()``.
+
+Using normal measurements and collapse measurements in the same circuit is
+also possible:
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ output = c.add(gates.M(0, collapse=True))
+ c.add(gates.H(0))
+ c.add(gates.M(0, 1))
+ result = c(nshots=100)
+
+In this case ``output`` will contain the results of the first ``collapse=True``
+measurement while ``result`` will contain the results of the standard measurement.
+
+Conditioning gates on measurement outcomes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The output of ``collapse=True`` measurements can be used as a parameter in
+any parametrized gate as follows:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import Circuit, gates
+
+ c = Circuit(2, density_matrix=True)
+ c.add(gates.H(0))
+ output = c.add(gates.M(0, collapse=True))
+ c.add(gates.RX(1, theta=np.pi * output.symbols[0] / 4))
+ result = c()
+
+In this case the first qubit will be measured and if 1 is found a pi/4 X-rotation
+will be applied to the second qubit, otherwise no rotation. Qibo allows to
+use ``output`` as a parameter during circuit creation through the use of
+``sympy.Symbol`` objects. These symbols can be accessed through the ``output.symbols``
+list and they acquire a numerical value during execution when the measurement
+is performed. As explained above, if ``nshots > 1`` is given during circuit
+execution the execution is repeated using a loop.
+
+If more than one qubits are used in a ``collapse=True`` measurement gate the
+``output.symbols`` list can be indexed accordingly:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import Circuit, gates
+
+ c = Circuit(3, density_matrix=True)
+ c.add(gates.H(0))
+ output = c.add(gates.M(0, 1, collapse=True))
+ c.add(gates.RX(1, theta=np.pi * output.symbols[0] / 4))
+ c.add(gates.RY(2, theta=np.pi * (output.symbols[0] + output.symbols[1]) / 5))
+ result = c()
+
+
+How to invert a circuit?
+------------------------
+
+Many quantum algorithms require using a specific subroutine and its inverse
+in the same circuit. Qibo simplifies this implementation via the
+:meth:`qibo.models.circuit.Circuit.invert` method. This method produces
+the inverse of a circuit by taking the dagger of all gates in reverse order. It
+can be used with circuit addition to simplify the construction of algorithms,
+for example:
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ # Create a subroutine
+ subroutine = Circuit(6)
+ subroutine.add([gates.RX(i, theta=0.1) for i in range(5)])
+ subroutine.add([gates.CZ(i, i + 1) for i in range(0, 5, 2)])
+
+ # Create the middle part of the circuit
+ middle = Circuit(6)
+ middle.add([gates.CU2(i, i + 1, phi=0.1, lam=0.2) for i in range(0, 5, 2)])
+
+ # Create the total circuit as subroutine + middle + subroutine^{-1}
+ circuit = subroutine + middle + subroutine.invert()
+
+
+Note that circuit addition works only between circuits that act on the same number
+of qubits. It is often useful to add subroutines only on a subset of qubits of the
+large circuit. This is possible using the :meth:`qibo.models.circuit.Circuit.on_qubits`
+method. For example:
+
+.. testcode::
+
+ from qibo import models, gates
+
+ # Create a small circuit of 4 qubits
+ smallc = models.Circuit(4)
+ smallc.add((gates.RX(i, theta=0.1) for i in range(4)))
+ smallc.add((gates.CNOT(0, 1), gates.CNOT(2, 3)))
+
+ # Create a large circuit on 8 qubits
+ largec = models.Circuit(8)
+ # Add the small circuit on even qubits
+ largec.add(smallc.on_qubits(*range(0, 8, 2)))
+ # Add a QFT on odd qubits
+ largec.add(models.QFT(4).on_qubits(*range(1, 8, 2)))
+ # Add an inverse QFT on first 6 qubits
+ largec.add(models.QFT(6).invert().on_qubits(*range(6)))
+
+
+.. _vqe-example:
+
+How to write a VQE?
+-------------------
+
+The VQE requires an ansatz function and a ``Hamiltonian`` object.
+There are examples of VQE optimization in ``examples/benchmarks``:
+
+ - ``vqe.py``: a simple example with the XXZ model.
+
+Here is a simple example using the Heisenberg XXZ model Hamiltonian:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates, hamiltonians
+
+ nqubits = 6
+ nlayers = 4
+
+ # Create variational circuit
+ circuit = models.Circuit(nqubits)
+ for l in range(nlayers):
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(0, nqubits-1, 2)))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(1, nqubits-2, 2)))
+ circuit.add(gates.CZ(0, nqubits-1))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+
+ # Create XXZ Hamiltonian
+ hamiltonian = hamiltonians.XXZ(nqubits=nqubits)
+ # Create VQE model
+ vqe = models.VQE(circuit, hamiltonian)
+
+ # Optimize starting from a random guess for the variational parameters
+ initial_parameters = np.random.uniform(0, 2*np.pi,
+ 2*nqubits*nlayers + nqubits)
+ best, params, extra = vqe.minimize(initial_parameters, method='BFGS', compile=False)
+
+
+
+For more information on the available options of the ``vqe.minimize`` call we
+refer to the :ref:`Optimizers ` section of the documentation.
+Note that if the Stochastic Gradient Descent optimizer is used then the user
+has to use a backend based on tensorflow primitives and not the default custom
+backend, as custom operators currently do not support automatic differentiation.
+To switch the backend one can do ``qibo.set_backend("tensorflow")``.
+Check the :ref:`How to use automatic differentiation? `
+section for more details.
+
+When using a VQE with more than 12 qubits, it may be useful to fuse the circit implementing
+the ansatz using :meth:`qibo.models.Circuit.fuse`.
+This optimizes performance by fusing the layer of one-qubit parametrized gates with
+the layer of two-qubit entangling gates and applying both as a single layer of
+general two-qubit gates (as 4x4 matrices).
+
+.. testsetup::
+
+ import numpy as np
+ from qibo import models, gates, hamiltonians
+
+.. testcode::
+
+ circuit = models.Circuit(nqubits)
+ for l in range(nlayers):
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(0, nqubits-1, 2)))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(1, nqubits-2, 2)))
+ circuit.add(gates.CZ(0, nqubits-1))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit = circuit.fuse()
+
+.. _vqc-example:
+
+How to write a custom variational circuit optimization?
+-------------------------------------------------------
+
+Similarly to the VQE, a custom implementation of a Variational Quantum Circuit
+(VQC) model can be achieved by defining a custom loss function and calling the
+:meth:`qibo.optimizers.optimize` method.
+
+Here is a simple example using a custom loss function:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+ from qibo.optimizers import optimize
+
+ # custom loss function, computes fidelity
+ def myloss(parameters, circuit, target):
+ circuit.set_parameters(parameters)
+ final_state = circuit().state()
+ return 1 - np.abs(np.conj(target).dot(final_state))
+
+ nqubits = 6
+ nlayers = 2
+
+ # Create variational circuit
+ c = models.Circuit(nqubits)
+ for l in range(nlayers):
+ c.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ c.add((gates.CZ(q, q+1) for q in range(0, nqubits-1, 2)))
+ c.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ c.add((gates.CZ(q, q+1) for q in range(1, nqubits-2, 2)))
+ c.add(gates.CZ(0, nqubits-1))
+ c.add((gates.RY(q, theta=0) for q in range(nqubits)))
+
+ # Optimize starting from a random guess for the variational parameters
+ x0 = np.random.uniform(0, 2*np.pi, 2*nqubits*nlayers + nqubits)
+ data = np.random.normal(0, 1, size=2**nqubits)
+
+ # perform optimization
+ best, params, extra = optimize(myloss, x0, args=(c, data), method='BFGS')
+
+ # set final solution to circuit instance
+ c.set_parameters(params)
+
+
+.. _qaoa-example:
+
+How to use the QAOA?
+--------------------
+
+The quantum approximate optimization algorithm (QAOA) was introduced in
+`arXiv:1411.4028 `_ and is a prominent
+algorithm for solving hard optimization problems using the circuit-based model
+of quantum computation. Qibo provides an implementation of the QAOA as a model
+that can be defined using a :class:`qibo.hamiltonians.Hamiltonian`. When
+properly optimized, the QAOA ansatz will approximate the ground state of this
+Hamiltonian. Here is a simple example using the Heisenberg XXZ Hamiltonian:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, hamiltonians
+
+ # Create XXZ Hamiltonian for six qubits
+ hamiltonian = hamiltonians.XXZ(6)
+ # Create QAOA model
+ qaoa = models.QAOA(hamiltonian)
+
+ # Optimize starting from a random guess for the variational parameters
+ initial_parameters = 0.01 * np.random.uniform(0,1,4)
+ best_energy, final_parameters, extra = qaoa.minimize(initial_parameters, method="BFGS")
+
+In the above example the initial guess for parameters has length four and
+therefore the QAOA ansatz consists of four operators, two using the
+``hamiltonian`` and two using the mixer Hamiltonian. The user may specify the
+mixer Hamiltonian when defining the QAOA model, otherwise
+:class:`qibo.hamiltonians.X` will be used by default.
+Note that the user may set the values of the variational parameters explicitly
+using :meth:`qibo.models.QAOA.set_parameters`.
+Similarly to the VQE, we refer to :ref:`Optimizers ` for more
+information on the available options of the ``qaoa.minimize``.
+
+QAOA uses the ``|++...+>`` as the default initial state on which the variational
+operators are applied. The user may specify a different initial state when
+executing or optimizing by passing the ``initial_state`` argument.
+
+The QAOA model uses :ref:`Solvers ` to apply the exponential operators
+to the state vector. For more information on how solvers work we refer to the
+:ref:`How to simulate time evolution? ` section.
+When a :class:`qibo.hamiltonians.Hamiltonian` is used then solvers will
+exponentiate it using its full matrix. Alternatively, if a
+:class:`qibo.hamiltonians.SymbolicHamiltonian` is used then solvers
+will fall back to traditional Qibo circuits that perform Trotter steps. For
+more information on how the Trotter decomposition is implemented in Qibo we
+refer to the :ref:`Using Trotter decomposition ` example.
+
+When Trotter decomposition is used, it is possible to execute the QAOA circuit
+on multiple devices, by passing an ``accelerators`` dictionary when defining
+the model. For example the previous example would have to be modified as:
+
+.. code-block:: python
+
+ from qibo import models, hamiltonians
+
+ hamiltonian = hamiltonians.XXZ(6, dense=False)
+ qaoa = models.QAOA(hamiltonian, accelerators={"/GPU:0": 1, "/GPU:1": 1})
+
+
+.. _autodiff-example:
+
+How to use automatic differentiation?
+-------------------------------------
+
+As a deep learning framework, Tensorflow supports
+`automatic differentiation `_.
+This can be used to optimize the parameters of variational circuits. For example
+the following script optimizes the parameters of two rotations so that the circuit
+output matches a target state using the fidelity as the corresponding loss
+function.
+
+Note that, as in the following example, the rotation angles have to assume real values
+to ensure the rotational gates are representing unitary operators.
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("tensorflow")
+ import tensorflow as tf
+ from qibo import gates, models
+
+ # Optimization parameters
+ nepochs = 1000
+ optimizer = tf.keras.optimizers.Adam()
+ target_state = tf.ones(4, dtype=tf.complex128) / 2.0
+
+ # Define circuit ansatz
+ params = tf.Variable(
+ tf.random.uniform((2,), dtype=tf.float64)
+ )
+ c = models.Circuit(2)
+ c.add(gates.RX(0, params[0]))
+ c.add(gates.RY(1, params[1]))
+
+ for _ in range(nepochs):
+ with tf.GradientTape() as tape:
+ c.set_parameters(params)
+ final_state = c().state()
+ fidelity = tf.math.abs(tf.reduce_sum(tf.math.conj(target_state) * final_state))
+ loss = 1 - fidelity
+ grads = tape.gradient(loss, params)
+ optimizer.apply_gradients(zip([grads], [params]))
+
+
+Note that the ``"tensorflow"`` backend has to be used here because other custom
+backends do not support automatic differentiation.
+
+The optimization procedure may also be compiled, however in this case it is not
+possible to use :meth:`qibo.circuit.Circuit.set_parameters` as the
+circuit needs to be defined inside the compiled ``tf.GradientTape()``.
+For example:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("tensorflow")
+ import tensorflow as tf
+ from qibo import gates, models
+
+ nepochs = 1000
+ optimizer = tf.keras.optimizers.Adam()
+ target_state = tf.ones(4, dtype=tf.complex128) / 2.0
+ params = tf.Variable(tf.random.uniform((2,), dtype=tf.float64))
+
+ @tf.function
+ def optimize(params):
+ with tf.GradientTape() as tape:
+ c = models.Circuit(2)
+ c.add(gates.RX(0, theta=params[0]))
+ c.add(gates.RY(1, theta=params[1]))
+ final_state = c().state()
+ fidelity = tf.math.abs(tf.reduce_sum(tf.math.conj(target_state) * final_state))
+ loss = 1 - fidelity
+ grads = tape.gradient(loss, params)
+ optimizer.apply_gradients(zip([grads], [params]))
+
+ for _ in range(nepochs):
+ optimize(params)
+
+
+The user may also use ``tf.Variable`` and parametrized gates in any other way
+that is supported by Tensorflow, such as defining
+`custom Keras layers `_
+and using the `Sequential model API `_
+to train them.
+
+
+.. _noisy-example:
+
+How to perform noisy simulation?
+--------------------------------
+
+Qibo can perform noisy simulation with two different methods: by repeating the
+circuit execution multiple times and applying noise gates probabilistically
+or by using density matrices and applying noise channels. The two methods
+are analyzed in the following sections.
+
+Moreover, Qibo provides functionality to add bit-flip errors to measurements
+after the simulation is completed. This is analyzed in
+:ref:`Measurement errors `.
+
+
+
+.. _densitymatrix-example:
+
+Using density matrices
+^^^^^^^^^^^^^^^^^^^^^^
+
+Qibo circuits can evolve density matrices if they are initialized using the
+``density_matrix=True`` flag, for example:
+
+.. testcode::
+
+ import qibo
+ qibo.set_backend("qibojit")
+
+ from qibo import models, gates
+
+ # Define circuit
+ c = models.Circuit(2, density_matrix=True)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ # execute using the default initial state |00><00|
+ result = c() # will be |++><++|
+
+will perform the transformation
+
+.. math::
+ |00 \rangle \langle 00| \rightarrow (H_1 \otimes H_2)|00 \rangle \langle 00|(H_1 \otimes H_2)^\dagger = |++ \rangle \langle ++|
+
+Similarly to state vector circuit simulation, the user may specify a custom
+initial density matrix by passing the corresponding array when executing the
+circuit. If a state vector is passed as an initial state in a density matrix
+circuit, it will be transformed automatically to the equivalent density matrix.
+
+Additionally, Qibo provides several gates that represent channels which can
+be used during a density matrix simulation. We refer to the
+:ref:`Channels ` section of the documentation for a complete list of
+the available channels. Noise can be simulated using these channels,
+for example:
+
+.. testcode::
+
+ from qibo import models, gates
+
+ c = models.Circuit(2, density_matrix=True) # starts with state |00><00|
+ c.add(gates.X(1))
+ # transforms |00><00| -> |01><01|
+ c.add(gates.PauliNoiseChannel(0, [("X", 0.3)]))
+ # transforms |01><01| -> (1 - px)|01><01| + px |11><11|
+ result = c()
+ # result.state() will be tf.Tensor(diag([0, 0.7, 0, 0.3]))
+
+will perform the transformation
+
+.. math::
+ |00\rangle \langle 00|& \rightarrow (I \otimes X)|00\rangle \langle 00|(I \otimes X)
+ = |01\rangle \langle 01|
+ \\& \rightarrow 0.7|01\rangle \langle 01| + 0.3(X\otimes I)|01\rangle \langle 01|(X\otimes I)^\dagger
+ \\& = 0.7|01\rangle \langle 01| + 0.3|11\rangle \langle 11|
+
+Measurements and callbacks can be used with density matrices exactly as in the
+case of state vector simulation.
+
+
+.. _repeatedexec-example:
+
+Using repeated execution
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Simulating noise with density matrices is memory intensive as it effectively
+doubles the number of qubits. Qibo provides an alternative way of simulating
+the effect of channels without using density matrices, which relies on state
+vectors and repeated circuit execution with sampling. Noise can be simulated
+by creating a normal (non-density matrix) circuit and repeating its execution
+as follows:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+
+ # Define circuit
+ c = models.Circuit(5)
+ thetas = np.random.random(5)
+ c.add((gates.RX(i, theta=t) for i, t in enumerate(thetas)))
+ # Add noise channels to all qubits
+ c.add((gates.PauliNoiseChannel(i, [("X", 0.2), ("Y", 0.0), ("Z", 0.3)])
+ for i in range(5)))
+ # Add measurement of all qubits
+ c.add(gates.M(*range(5)))
+
+ # Repeat execution 1000 times
+ result = c(nshots=1000)
+
+In this example the simulation is repeated 1000 times and the action of the
+:class:`qibo.gates.PauliNoiseChannel` gate differs each time, because
+the error ``X``, ``Y`` and ``Z`` gates are sampled according to the given
+probabilities. Note that when a channel is used, the command ``c(nshots=1000)``
+has a different behavior than what is described in
+:ref:`How to perform measurements? `.
+Normally ``c(nshots=1000)`` would execute the circuit once and would then
+sample 1000 bit-strings from the final state. When channels are used, the full
+is executed 1000 times because the behavior of channels is probabilistic and
+different in each execution. Note that now the simulation time required will
+increase linearly with the number of repetitions (``nshots``).
+
+Note that executing a circuit with channels only once is possible, however,
+since the channel acts probabilistically, the results of a single execution
+are random and usually not useful on their own.
+It is possible also to use repeated execution with noise channels even without
+the presence of measurements. If ``c(nshots=1000)`` is called for a circuit
+that contains channels but no measurements measurements then the circuit will
+be executed 1000 times and the final 1000 state vectors will be returned as
+a tensor of shape ``(nshots, 2 ^ nqubits)``.
+Note that this tensor is usually large and may lead to memory errors,
+therefore this usage is not advised.
+
+Unlike the density matrix approach, it is not possible to use every channel
+with sampling and repeated execution. Specifically,
+:class:`qibo.gates.UnitaryChannel` and
+:class:`qibo.gates.PauliNoiseChannel` can be used with sampling, while
+:class:`qibo.gates.KrausChannel` requires density matrices.
+
+
+Adding noise after every gate
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In practical applications noise typically occurs after every gate.
+Qibo provides the :meth:`qibo.models.circuit.Circuit.with_pauli_noise` method
+which automatically creates a new circuit that contains a
+:class:`qibo.gates.PauliNoiseChannel` after every gate.
+The user can control the probabilities of the noise channel using a noise map,
+which is a dictionary that maps qubits to the corresponding probability
+triplets. For example, the following script
+
+.. testcode::
+
+ from qibo import models, gates
+
+ c = models.Circuit(2)
+ c.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+
+ # Define a noise map that maps qubit IDs to noise probabilities
+ noise_map = {0: list(zip(["X", "Z"], [0.1, 0.2])), 1: list(zip(["Y", "Z"], [0.2, 0.1]))}
+ noisy_c = c.with_pauli_noise(noise_map)
+
+will create a new circuit ``noisy_c`` that is equivalent to:
+
+.. testcode::
+
+ noisy_c2 = models.Circuit(2)
+ noisy_c2.add(gates.H(0))
+ noisy_c2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Y", 0.0), ("Z", 0.2)]))
+ noisy_c2.add(gates.H(1))
+ noisy_c2.add(gates.PauliNoiseChannel(1, [("X", 0.0), ("Y", 0.2), ("Z", 0.1)]))
+ noisy_c2.add(gates.CNOT(0, 1))
+ noisy_c2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Y", 0.0), ("Z", 0.2)]))
+ noisy_c2.add(gates.PauliNoiseChannel(1, [("X", 0.0), ("Y", 0.2), ("Z", 0.1)]))
+
+Note that ``noisy_c`` uses the gate objects of the original circuit ``c``
+(it is not a deep copy), while in ``noisy_c2`` each gate was created as
+a new object.
+
+The user may use a single tuple instead of a dictionary as the noise map
+In this case the same probabilities will be applied to all qubits.
+That is ``noise_map = list(zip(["X", "Z"], [0.1, 0.1]))`` is equivalent to
+``noise_map = {0: list(zip(["X", "Z"], [0.1, 0.1])), 1: list(zip(["X", "Z"], [0.1, 0.1])), ...}``.
+
+As described in the previous sections, if
+:meth:`qibo.models.circuit.Circuit.with_pauli_noise` is used in a circuit
+that uses state vectors then noise will be simulated with repeated execution.
+If the user wishes to use density matrices instead, this is possible by
+passing the ``density_matrix=True`` flag during the circuit initialization and call
+``.with_pauli_noise`` on the new circuit.
+
+.. _noisemodel-example:
+
+Using a noise model
+^^^^^^^^^^^^^^^^^^^
+
+In a real quantum circuit some gates can be highly faulty and introduce errors.
+In order to simulate this behavior Qibo provides the :class:`qibo.noise.NoiseModel`
+class which can store errors that are gate-dependent using the
+:meth:`qibo.noise.NoiseModel.add` method and generate the corresponding noisy circuit
+with :meth:`qibo.noise.NoiseModel.apply`. The corresponding noise is applied after
+every instance of the gate in the circuit. It is also possible to specify on which qubits
+the noise will be added.
+
+The current quantum errors available to build a custom noise model are:
+:class:`qibo.noise.PauliError`, :class:`qibo.noise.ThermalRelaxationError` and
+:class:`qibo.noise.ResetError`.
+
+Here is an example on how to use a noise model:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+ from qibo.noise import NoiseModel, PauliError
+
+ # Build specific noise model with 3 quantum errors:
+ # - Pauli error on H only for qubit 1.
+ # - Pauli error on CNOT for all the qubits.
+ # - Pauli error on RX(pi/2) for qubit 0.
+ noise = NoiseModel()
+ noise.add(PauliError([("X", 0.5)]), gates.H, 1)
+ noise.add(PauliError([("Y", 0.5)]), gates.CNOT)
+ is_sqrt_x = (lambda g: np.pi/2 in g.parameters)
+ noise.add(PauliError([("X", 0.5)]), gates.RX, qubits=0, conditions=is_sqrt_x)
+
+ # Generate noiseless circuit.
+ c = models.Circuit(2)
+ c.add([gates.H(0), gates.H(1), gates.CNOT(0, 1), gates.RX(0, np.pi/2), gates.RX(0, 3*np.pi/2), gates.RX(1, np.pi/2)])
+
+ # Apply noise to the circuit according to the noise model.
+ noisy_c = noise.apply(c)
+
+The noisy circuit defined above will be equivalent to the following circuit:
+
+.. testcode::
+
+ noisy_c2 = models.Circuit(2)
+ noisy_c2.add(gates.H(0))
+ noisy_c2.add(gates.H(1))
+ noisy_c2.add(gates.PauliNoiseChannel(1, [("X", 0.5)]))
+ noisy_c2.add(gates.CNOT(0, 1))
+ noisy_c2.add(gates.PauliNoiseChannel(0, [("Y", 0.5)]))
+ noisy_c2.add(gates.PauliNoiseChannel(1, [("Y", 0.5)]))
+ noisy_c2.add(gates.RX(0, np.pi/2))
+ noisy_c2.add(gates.PauliNoiseChannel(0, [("X", 0.5)]))
+ noisy_c2.add(gates.RX(0, 3*np.pi/2))
+ noisy_c2.add(gates.RX(1, np.pi/2))
+
+
+The :class:`qibo.noise.NoiseModel` class supports also density matrices,
+it is sufficient to pass a circuit which was initialized with ``density_matrix=True``.
+
+
+.. _measurementbitflips-example:
+
+Measurement errors
+^^^^^^^^^^^^^^^^^^
+
+:class:`qibo.measurements.CircuitResult` provides :meth:`qibo.measurements.CircuitResult.apply_bitflips`
+which allows adding bit-flip errors to the sampled bit-strings without having to
+re-execute the simulation. For example:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+
+ thetas = np.random.random(4)
+ c = models.Circuit(4)
+ c.add((gates.RX(i, theta=t) for i, t in enumerate(thetas)))
+ c.add([gates.M(0, 1), gates.M(2, 3)])
+ result = c(nshots=100)
+ # add bit-flip errors with probability 0.2 for all qubits
+ result.apply_bitflips(0.2)
+ # add bit-flip errors with different probabilities for each qubit
+ error_map = {0: 0.2, 1: 0.1, 2: 0.3, 3: 0.1}
+ result.apply_bitflips(error_map)
+
+The corresponding noisy samples and frequencies can then be obtained as described
+in the :ref:`How to perform measurements? ` example.
+
+Note that :meth:`qibo.measurements.CircuitResult.apply_bitflips` modifies
+the measurement samples contained in the corresponding state and therefore the
+original noiseless measurement samples are no longer accessible. It is possible
+to keep the original samples by creating a copy of the states before applying
+the bitflips:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+
+ thetas = np.random.random(4)
+ c = models.Circuit(4)
+ c.add((gates.RX(i, theta=t) for i, t in enumerate(thetas)))
+ c.add([gates.M(0, 1), gates.M(2, 3)])
+ result = c(nshots=100)
+ # add bit-flip errors with probability 0.2 for all qubits
+ result.apply_bitflips(0.2)
+ # add bit-flip errors with different probabilities for each qubit
+ error_map = {0: 0.2, 1: 0.1, 2: 0.3, 3: 0.1}
+ result.apply_bitflips(error_map)
+
+
+Alternatively, the user may specify a bit-flip error map when defining
+measurement gates:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, gates
+
+ thetas = np.random.random(6)
+ c = models.Circuit(6)
+ c.add((gates.RX(i, theta=t) for i, t in enumerate(thetas)))
+ c.add(gates.M(0, 1, p0=0.2))
+ c.add(gates.M(2, 3, p0={2: 0.1, 3: 0.0}))
+ c.add(gates.M(4, 5, p0=[0.4, 0.3]))
+ result = c(nshots=100)
+
+In this case ``result`` will contain noisy samples according to the given
+bit-flip probabilities. The probabilities can be given as a
+dictionary (must contain all measured qubits as keys),
+a list (must have the sample as the measured qubits) or
+a single float number (to be used on all measured qubits).
+Note that, unlike the previous code example, when bit-flip errors are
+incorporated as part of measurement gates it is not possible to access the
+noiseless samples.
+
+Moreover, it is possible to simulate asymmetric bit-flips using the ``p1``
+argument as ``result.apply_bitflips(p0=0.2, p1=0.1)``. In this case a
+probability of 0.2 will be used for 0->1 errors but 0.1 for 1->0 errors.
+Similarly to ``p0``, ``p1`` can be a single float number or a dictionary and
+can be used both in :meth:`qibo.measurements.CircuitResult.apply_bitflips`
+and the measurement gate. If ``p1`` is not specified the value of ``p0`` will
+be used for both errors.
+
+.. _noise-hardware-example:
+
+Simulating IBMQ's quantum hardware
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Qibo can perform a simulation of a real quantum computer using the
+:class:`qibo.noise.IBMQNoiseModel` class.
+It is possible by passing the circuit instance that we want to simulate
+and the noise channels' parameters as a dictionary.
+In this model, the user must set the relaxation times ``t1`` and ``t2`` for each qubit,
+an approximated `gate times`, and depolarizing errors for each one-qubit (`depolarizing_one_qubit`)
+and two-qubit (`depolarizing_two_qubit`) gates.
+Additionally, one can also pass single-qubit readout error probabilities (`readout_one_qubit`).
+
+.. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.noise import IBMQNoiseModel
+
+ nqubits = 2
+ circuit = Circuit(2, density_matrix=True)
+ circuit.add(
+ [
+ gates.H(0),
+ gates.X(1),
+ gates.Z(0),
+ gates.X(0),
+ gates.CNOT(0,1),
+ gates.CNOT(1, 0),
+ gates.X(1),
+ gates.Z(1),
+ gates.M(0),
+ gates.M(1),
+ ]
+ )
+
+ print("raw circuit:")
+ print(circuit.draw())
+
+ parameters = {
+ "t1": {"0": 250*1e-06, "1": 240*1e-06},
+ "t2": {"0": 150*1e-06, "1": 160*1e-06},
+ "gate_times" : (200*1e-9, 400*1e-9),
+ "excited_population" : 0,
+ "depolarizing_one_qubit" : 4.000e-4,
+ "depolarizing_two_qubit": 1.500e-4,
+ "readout_one_qubit" : {"0": (0.022, 0.034), "1": (0.015, 0.041)},
+ }
+
+ noise_model = IBMQNoiseModel()
+ noise_model.from_dict(parameters)
+ noisy_circuit = noise_model.apply(circuit)
+
+ print("noisy circuit:")
+ print(noisy_circuit.draw())
+
+.. testoutput::
+ :hide:
+
+ ...
+
+``noisy_circuit`` is the new circuit containing the error gate channels.
+
+.. #TODO: rewrite this optimization example after the fit function is moded to `qibo.optimizers`
+.. It is possible to learn the parameters of the noise model that best describe a frequency distribution obtained by running a circuit on quantum hardware. To do this,
+.. assuming we have a ``result`` object after running a circuit with a certain number of shots,
+
+.. .. testcode::
+
+.. noise = NoiseModel()
+.. params = {"idle_qubits" : True}
+.. noise.composite(params)
+
+.. result = noisy_circ(nshots=1000)
+
+.. noise.noise_model.fit(c, result)
+
+.. print(noise.noise_model.params)
+.. print(noise.noise_model.hellinger)
+
+.. .. testoutput::
+.. :hide:
+
+.. ...
+
+.. where ``noise.params`` is a dictionary with the parameters obatined after the optimization and ``noise.hellinger`` is the corresponding Hellinger fidelity.
+
+
+How to perform error mitigation?
+--------------------------------
+
+Noise and errors in circuits are one of the biggest obstacles to face in quantum computing.
+Say that you have a circuit :math:`C` and you want to measure an observable :math:`A` at the end of it,
+in general you are going to obtain an expected value :math:`\langle A \rangle_{noisy}` that
+can lie quiet far from the true one :math:`\langle A \rangle_{exact}`.
+In Qibo, different methods are implemented for mitigating errors in circuits and obtaining
+a better estimate of the noise-free expected value :math:`\langle A \rangle_{exact}`.
+
+
+Let's see how to use them. For starters, let's define a dummy circuit with some RZ, RX and CNOT gates:
+
+.. testcode::
+
+ import numpy as np
+
+ from qibo import Circuit, gates
+
+ # Define the circuit
+ nqubits = 3
+ hz = 0.5
+ hx = 0.5
+ dt = 0.25
+ circ = Circuit(nqubits, density_matrix=True)
+ circ.add(gates.RZ(q, theta=-2 * hz * dt - np.pi / 2) for q in range(nqubits))
+ circ.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ circ.add(gates.RZ(q, theta=-2 * hx * dt + np.pi) for q in range(nqubits))
+ circ.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ circ.add(gates.RZ(q, theta=-np.pi / 2) for q in range(nqubits))
+ circ.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ circ.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(0, nqubits - 1, 2))
+ circ.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ circ.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ circ.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2))
+ circ.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ # Include the measurements
+ circ.add(gates.M(*range(nqubits)))
+
+ # visualize the circuit
+ print(circ.draw())
+
+ # q0: ─RZ─RX─RZ─RX─RZ─o────o────────M─
+ # q1: ─RZ─RX─RZ─RX─RZ─X─RZ─X─o────o─M─
+ # q2: ─RZ─RX─RZ─RX─RZ────────X─RZ─X─M─
+
+.. testoutput::
+ :hide:
+
+ ...
+
+remember to initialize the circuit with ``density_matrix=True`` and to include the measuerement gates at the end for expectation value calculation.
+
+As observable we can simply take :math:`Z_0 Z_1 Z_2` :
+
+.. testcode::
+
+ from qibo.symbols import Z
+ from qibo.hamiltonians import SymbolicHamiltonian
+ from qibo.backends import GlobalBackend
+
+ backend = GlobalBackend()
+
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nqubits)])
+ obs = SymbolicHamiltonian(obs, backend=backend)
+
+We can obtain the exact expected value by running the circuit on any simulation ``backend``. To mimic the execution on
+the real quantum hardware, instead, we can use a noise model:
+
+.. testcode::
+
+ # Noise-free expected value
+ exact = obs.expectation(backend.execute_circuit(circ).state())
+ print(exact)
+ # 0.9096065335014379
+
+ from qibo.noise import DepolarizingError, ReadoutError, NoiseModel
+ from qibo.quantum_info import random_stochastic_matrix
+
+ # Define the noise model
+ noise = NoiseModel()
+ # depolarizing error after each CNOT
+ noise.add(DepolarizingError(0.1), gates.CNOT)
+ # readout error
+ # randomly initialize the bitflip probabilities
+ prob = random_stochastic_matrix(
+ 2**nqubits, diagonally_dominant=True, seed=2, backend=backend
+ )
+ noise.add(ReadoutError(probabilities=prob), gate=gates.M)
+ # Noisy expected value without mitigation
+ noisy = obs.expectation(backend.execute_circuit(noise.apply(circ)).state())
+ print(noisy)
+ # 0.5647937721701448
+
+.. testoutput::
+ :hide:
+
+ ...
+
+Note that when running on the quantum hardware, you won't need to use a noise model
+anymore, you will just have to change the backend to the appropriate one.
+
+Now let's check that error mitigation produces better estimates of the exact expected value.
+
+Readout Mitigation
+^^^^^^^^^^^^^^^^^^
+Firstly, let's try to mitigate the readout errors. To do this, we can either compute the
+response matrix and use it modify the final state after the circuit execution:
+
+.. testcode::
+
+ from qibo.models.error_mitigation import get_expectation_val_with_readout_mitigation, get_response_matrix
+
+ nshots = 10000
+ # compute the response matrix
+ response_matrix = get_response_matrix(
+ nqubits, backend=backend, noise_model=noise, nshots=nshots
+ )
+ # define mitigation options
+ readout = {"response_matrix": response_matrix}
+ # mitigate the readout errors
+ mit_val = get_expectation_val_with_readout_mitigation(circ, obs, noise, readout=readout)
+ print(mit_val)
+ # 0.5945794816381054
+
+.. testoutput::
+ :hide:
+
+ ...
+
+Or use the randomized readout mitigation:
+
+.. testcode::
+
+ from qibo.models.error_mitigation import apply_randomized_readout_mitigation
+
+ # define mitigation options
+ readout = {"ncircuits": 10}
+ # mitigate the readout errors
+ mit_val = get_expectation_val_with_readout_mitigation(circ, obs, noise, readout=readout)
+ print(mit_val)
+ # 0.5860884499785314
+
+.. testoutput::
+ :hide:
+
+ ...
+
+Alright, the expected value is improving, but we are still far from the ideal one.
+Readout mitigation alone is not enough, let's try to use some more advanced methods
+to get rid of the depolarizing error we introduced in the CNOT gates.
+
+Zero Noise Extrapolation (ZNE)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To run ZNE, we just need to define the noise levels to use. Each level corresponds to the
+number of CNOT or RX pairs (depending on the value of ``insertion_gate``) inserted in the
+circuit in correspondence to the original ones. Since we decided to simulate noisy CNOTs::
+
+ Level 1
+ q0: ─X─ --> q0: ─X───X──X─
+ q1: ─o─ --> q1: ─o───o──o─
+
+ Level 2
+ q0: ─X─ --> q0: ─X───X──X───X──X─
+ q1: ─o─ --> q1: ─o───o──o───o──o─
+
+ .
+ .
+ .
+
+For example if we use the five levels ``[0,1,2,3,4]`` :
+
+.. testcode::
+
+ from qibo.models.error_mitigation import ZNE
+
+ # Mitigated expected value
+ estimate = ZNE(
+ circuit=circ,
+ observable=obs,
+ noise_levels=np.arange(5),
+ noise_model=noise,
+ nshots=10000,
+ insertion_gate='CNOT',
+ backend=backend,
+ )
+ print(estimate)
+ # 0.8332843749999996
+
+.. testoutput::
+ :hide:
+
+ ...
+
+we get an expected value closer to the exact one. We can further improve by using ZNE
+combined with the readout mitigation:
+
+.. testcode::
+
+ # we can either use
+ # the response matrix computed earlier
+ readout = {'response_matrix': response_matrix}
+ # or the randomized readout
+ readout = {'ncircuits': 10}
+
+ # Mitigated expected value
+ estimate = ZNE(
+ circuit=circ,
+ observable=obs,
+ backend=backend,
+ noise_levels=np.arange(5),
+ noise_model=noise,
+ nshots=10000,
+ insertion_gate='CNOT',
+ readout=readout,
+ )
+ print(estimate)
+ # 0.8979124892467807
+
+.. testoutput::
+ :hide:
+
+ ...
+
+
+Clifford Data Regression (CDR)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For CDR instead, you don't need to define anything additional. However, keep in mind that the input
+circuit is expected to be decomposed in the set of primitive gates :math:`RX(\frac{\pi}{2}), CNOT, X` and :math:`RZ(\theta)`.
+
+.. testcode::
+
+ from qibo.models.error_mitigation import CDR
+
+ # Mitigated expected value
+ estimate = CDR(
+ circuit=circ,
+ observable=obs,
+ n_training_samples=10,
+ backend=backend,
+ noise_model=noise,
+ nshots=10000,
+ readout=readout,
+ )
+ print(estimate)
+ # 0.8983676333969615
+
+.. testoutput::
+ :hide:
+
+ ...
+
+Again, the mitigated expected value improves over the noisy one and is also slightly better compared to ZNE.
+
+
+Variable Noise CDR (vnCDR)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Being a combination of ZNE and CDR, vnCDR requires you to define the noise levels as done in ZNE, and the same
+caveat about the input circuit for CDR is valid here as well.
+
+.. testcode::
+
+ from qibo.models.error_mitigation import vnCDR
+
+ # Mitigated expected value
+ estimate = vnCDR(
+ circuit=circ,
+ observable=obs,
+ n_training_samples=10,
+ backend=backend,
+ noise_levels=np.arange(3),
+ noise_model=noise,
+ nshots=10000,
+ insertion_gate='CNOT',
+ readout=readout,
+ )
+ print(estimate)
+ # 0.8998376314644383
+
+.. testoutput::
+ :hide:
+
+ ...
+
+The result is similar to the one obtained by CDR. Usually, one would expect slightly better results for vnCDR,
+however, this can substantially vary depending on the circuit and the observable considered and, therefore, it is hard to tell
+a priori.
+
+
+Importance Clifford Sampling (ICS)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The use of iCS is straightforward, analogous to CDR and vnCDR.
+
+.. testcode::
+
+ from qibo.models.error_mitigation import ICS
+
+ # Mitigated expected value
+ estimate = ICS(
+ circuit=circ,
+ observable=obs,
+ n_training_samples=10,
+ backend=backend,
+ noise_model=noise,
+ nshots=10000,
+ readout=readout,
+ )
+ print(estimate)
+ # 0.9183495097398502
+
+.. testoutput::
+ :hide:
+
+ ...
+
+Again, the mitigated expected value improves over the noisy one and is also slightly better compared to ZNE.
+This was just a basic example usage of the three methods, for all the details about them you should check the API-reference page :ref:`Error Mitigation `.
+
+.. _timeevol-example:
+
+How to simulate time evolution?
+-------------------------------
+
+Simulating the unitary time evolution of quantum states is useful in many
+physics applications including the simulation of adiabatic quantum computation.
+Qibo provides the :class:`qibo.models.StateEvolution` model that simulates
+unitary evolution using the full state vector. For example:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models
+
+ # Define evolution model under the non-interacting sum(Z) Hamiltonian
+ # with time step dt=1e-1
+ nqubits = 4
+ evolve = models.StateEvolution(hamiltonians.Z(nqubits), dt=1e-1)
+ # Define initial state as |++++>
+ initial_state = np.ones(2 ** nqubits) / np.sqrt(2 ** nqubits)
+ # Get the final state after time t=2
+ final_state = evolve(final_time=2, initial_state=initial_state)
+
+
+When studying dynamics people are usually interested not only in the final state
+vector but also in observing how physical quantities change during the time
+evolution. This is possible using callbacks. For example, in the above case we
+can track how changes as follows:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models, callbacks
+
+ nqubits = 4
+ # Define a callback that calculates the energy (expectation value) of the X Hamiltonian
+ observable = callbacks.Energy(hamiltonians.X(nqubits))
+ # Create evolution object using the above callback and a time step of dt=1e-3
+ evolve = models.StateEvolution(hamiltonians.Z(nqubits), dt=1e-3,
+ callbacks=[observable])
+ # Evolve for total time t=1
+ initial_state = np.ones(2 ** nqubits) / np.sqrt(2 ** nqubits)
+ final_state = evolve(final_time=1, initial_state=initial_state)
+
+ print(observable[:])
+ # will print an array of shape ``(1001,)`` that holds (t) values
+.. testoutput::
+ :hide:
+
+ ...
+
+
+Note that the time step ``dt=1e-3`` defines how often we calculate during
+the evolution.
+
+In the above cases the exact time evolution operator (exponential of the Hamiltonian)
+was used to evolve the state vector. Because the evolution Hamiltonian is
+time-independent, the matrix exponentiation happens only once. It is possible to
+simulate time-dependent Hamiltonians by passing a function of time instead of
+a :class:`qibo.hamiltonians.Hamiltonian` in the
+:class:`qibo.models.StateEvolution` model. For example:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models
+
+ # Defina a time dependent Hamiltonian
+ nqubits = 4
+ ham = lambda t: np.cos(t) * hamiltonians.Z(nqubits)
+ # and pass it to the evolution model
+ evolve = models.StateEvolution(ham, dt=1e-3)
+ initial_state = np.ones(2 ** nqubits) / np.sqrt(2 ** nqubits)
+ final_state = evolve(final_time=1, initial_state=initial_state)
+
+
+The above script will still use the exact time evolution operator with the
+exponentiation repeated for each time step. The integration method can
+be changed using the ``solver`` argument when executing. The solvers that are
+currently implemented are the default exponential solver (``"exp"``) and two
+Runge-Kutta solvers: fourth-order (``"rk4"``) and fifth-order (``"rk45"``).
+For more information we refer to the :ref:`Solvers ` section.
+
+
+.. _trotterdecomp-example:
+
+Using Trotter decomposition
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Trotter decomposition provides a way to represent the unitary evolution of
+quantum states as a sequence of local unitaries. This allows to represent
+the physical process of time evolution as a quantum circuit. Qibo provides
+functionality to perform this transformation automatically, if the underlying
+Hamiltonian object is defined as a sum of commuting parts that consist of terms
+that can be exponentiated efficiently.
+Such Hamiltonian can be implemented in Qibo using
+:class:`qibo.hamiltonians.SymbolicHamiltonian`.
+The implementation of Trotter decomposition is based on Sec.
+4.1 of `arXiv:1901.05824 `_.
+Below is an example of how to use this object in practice:
+
+.. testcode::
+
+ from qibo import hamiltonians
+
+ # Define TFIM model as a non-dense ``SymbolicHamiltonian``
+ ham = hamiltonians.TFIM(nqubits=5, dense=False)
+ # This object can be used to create the circuit that
+ # implements a single Trotter time step ``dt``
+ circuit = ham.circuit(dt=1e-2)
+
+
+This is a standard :class:`qibo.core.circuit.Circuit` that
+contains :class:`qibo.gates.Unitary` gates corresponding to the
+exponentials of the Trotter decomposition and can be executed on any state.
+
+Note that in the transverse field Ising model (TFIM) that was used in this
+example is among the pre-coded Hamiltonians in Qibo and could be created as
+a :class:`qibo.hamiltonians.SymbolicHamiltonian` simply using the
+``dense=False`` flag. For more information on the difference between dense
+and non-dense Hamiltonians we refer to the :ref:`Hamiltonians `
+section. Note that only non-dense Hamiltonians created using ``dense=False``
+or through the :class:`qibo.hamiltonians.SymbolicHamiltonian` object
+can be used for evolution using Trotter decomposition. If a dense Hamiltonian
+is used then evolution will be done by exponentiating the full Hamiltonian
+matrix.
+
+Defining custom Hamiltonians from terms can be more complicated,
+however Qibo simplifies this process by providing the option
+to define Hamiltonians symbolically through the use of ``sympy``.
+For more information on this we refer to the
+:ref:`How to define custom Hamiltonians using symbols? `
+example.
+
+A :class:`qibo.hamiltonians.SymbolicHamiltonian` can also be used to
+simulate time evolution. This can be done by passing the Hamiltonian to a
+:class:`qibo.models.StateEvolution` model and using the exponential solver.
+For example:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import models, hamiltonians
+
+ nqubits = 5
+ # Create a critical TFIM Hamiltonian as ``SymbolicHamiltonian``
+ ham = hamiltonians.TFIM(nqubits=nqubits, h=1.0, dense=False)
+ # Define the |+++++> initial state
+ initial_state = np.ones(2 ** nqubits) / np.sqrt(2 ** nqubits)
+ # Define the evolution model
+ evolve = models.StateEvolution(ham, dt=1e-3)
+ # Evolve for total time T=1
+ final_state = evolve(final_time=1, initial_state=initial_state)
+
+This script creates the Trotter circuit for ``dt=1e-3`` and applies it
+repeatedly to the given initial state T / dt = 1000 times to obtain the
+final state of the evolution.
+
+Since Trotter evolution is based on Qibo circuits, it also supports distributed
+execution on multiple devices (GPUs). This can be enabled by passing an
+``accelerators`` dictionary when defining the
+:class:`qibo.models.StateEvolution` model. We refer to the
+:ref:`How to select hardware devices? ` example for more details
+on how the ``accelerators`` dictionary can be used.
+
+
+How to simulate adiabatic time evolution?
+-----------------------------------------
+
+Qibo provides the :class:`qibo.models.AdiabaticEvolution` model to simulate
+adiabatic time evolution. This is a special case of the
+:class:`qibo.models.StateEvolution` model analyzed in the previous example
+where the evolution Hamiltonian is interpolated between an initial "easy"
+Hamiltonian and a "hard" Hamiltonian that usually solves an optimization problem.
+Here is an example of adiabatic evolution simulation:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models
+
+ nqubits = 4
+ T = 1 # total evolution time
+ # Define the easy and hard Hamiltonians
+ h0 = hamiltonians.X(nqubits)
+ h1 = hamiltonians.TFIM(nqubits, h=0)
+ # Define the interpolation scheduling
+ s = lambda t: t
+ # Define evolution model
+ evolve = models.AdiabaticEvolution(h0, h1, s, dt=1e-2)
+ # Get the final state of the evolution
+ final_state = evolve(final_time=T)
+
+
+According to the adiabatic theorem, for proper scheduling and total evolution
+time the ``final_state`` should approximate the ground state of the "hard"
+Hamiltonian.
+
+If the initial state is not specified, the ground state of the easy Hamiltonian
+will be used, which is common for adiabatic evolution. A distributed execution
+can be used by passing an ``accelerators`` dictionary during the initialization
+of the ``AdiabaticEvolution`` model. In this case the default initial state is
+``|++...+>`` (full superposition in the computational basis).
+
+Callbacks may also be used as in the previous example. An additional callback
+(:class:`qibo.callbacks.Gap`) is available for calculating the
+energies and the gap of the adiabatic evolution Hamiltonian. Its usage is
+similar to other callbacks:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models, callbacks
+
+ nqubits = 4
+ h0 = hamiltonians.X(nqubits)
+ h1 = hamiltonians.TFIM(nqubits, h=0)
+
+ ground = callbacks.Gap(mode=0)
+ # define a callback for calculating the gap
+ gap = callbacks.Gap()
+ # define and execute the ``AdiabaticEvolution`` model
+ evolution = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-1,
+ callbacks=[gap, ground])
+
+ final_state = evolution(final_time=1.0)
+ # print the values of the gap at each evolution time step
+ print(gap[:])
+.. testoutput::
+ :hide:
+
+ ...
+
+
+The scheduling function ``s`` should be a callable that accepts one (s(t)) or
+two (s(t, p)) arguments. The first argument accepts values in [0, 1] and
+corresponds to the ratio ``t / final_time`` during evolution. The second
+optional argument is a vector of free parameters that can be optimized. The
+function should, by definition, satisfy the properties s(0, p) = 0 and
+s(1, p) = 1 for any p, otherwise errors will be raised.
+
+All state evolution functionality described in the previous example can also be
+used for simulating adiabatic evolution. The solver can be specified during the
+initialization of the :class:`qibo.models.AdiabaticEvolution` model and a
+Trotter decomposition may be used with the exponential solver. The Trotter
+decomposition will be used automatically if ``h0`` and ``h1`` are defined
+using as :class:`qibo.hamiltonians.SymbolicHamiltonian` objects. For
+pre-coded Hamiltonians this can be done simply as:
+
+.. testcode::
+
+ from qibo import hamiltonians, models
+
+ nqubits = 4
+ # Define ``SymolicHamiltonian``s
+ h0 = hamiltonians.X(nqubits, dense=False)
+ h1 = hamiltonians.TFIM(nqubits, h=0, dense=False)
+ # Perform adiabatic evolution using the Trotter decomposition
+ evolution = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-1)
+ final_state = evolution(final_time=1.0)
+
+
+When Trotter evolution is used, it is also possible to execute on multiple
+devices by passing an ``accelerators`` dictionary in the creation of the
+:class:`qibo.models.AdiabaticEvolution` model.
+
+Note that ``h0`` and ``h1`` should have the same type, either both
+:class:`qibo.hamiltonians.Hamiltonian` or both
+:class:`qibo.hamiltonians.SymbolicHamiltonian`.
+
+
+Optimizing the scheduling function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The free parameters ``p`` of the scheduling function can be optimized using
+the :meth:`qibo.models.AdiabaticEvolution.minimize` method. The parameters
+are optimized so that the final state of the adiabatic evolution approximates
+the ground state of the "hard" Hamiltonian. Optimization is similar to what is
+described in the :ref:`How to write a VQE? ` example and can be
+done as follows:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, models
+
+ # Define Hamiltonians
+ h0 = hamiltonians.X(3)
+ h1 = hamiltonians.TFIM(3)
+ # Define scheduling function with a free variational parameter ``p``
+ sp = lambda t, p: (1 - p) * np.sqrt(t) + p * t
+ # Define an evolution model with dt=1e-2
+ evolution = models.AdiabaticEvolution(h0, h1, sp, dt=1e-2)
+ # Find the optimal value for ``p`` starting from ``p = 0.5`` and ``T=1``.
+ initial_guess = [0.5, 1]
+ # best, params, extra = evolution.minimize(initial_guess, method="BFGS", options={'disp': True})
+ print(best) # prints the best energy found from the final state
+ print(params) # prints the optimal values for the parameters.
+.. testoutput::
+ :hide:
+
+ ...
+
+Note that the ``minimize`` method optimizes both the free parameters ``p`` of
+the scheduling function as well as the total evolution time. The initial guess
+for the total evolution time is the last value of the given ``initial_guess``
+array. For a list of the available optimizers we refer to
+:ref:`Optimizers `.
+
+
+.. _symbolicham-example:
+
+How to define custom Hamiltonians using symbols?
+------------------------------------------------
+
+In order to use the VQE, QAOA and time evolution models in Qibo the user has to
+define Hamiltonians based on :class:`qibo.hamiltonians.Hamiltonian` which
+uses the full matrix representation of the corresponding operator or
+:class:`qibo.hamiltonians.SymbolicHamiltonian` which uses a more efficient
+term representation. Qibo provides pre-coded Hamiltonians for some common models,
+such as the transverse field Ising model (TFIM) and the Heisenberg model
+(see :ref:`Hamiltonians ` for a complete list of the pre-coded models).
+In order to explore other problems the user needs to define the Hamiltonian
+objects from scratch.
+
+A standard way to define Hamiltonians is through their full matrix
+representation. For example the following code generates the TFIM Hamiltonian
+with periodic boundary conditions for four qubits by constructing the
+corresponding 16x16 matrix:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians, matrices
+
+ # ZZ terms
+ matrix = np.kron(np.kron(matrices.Z, matrices.Z), np.kron(matrices.I, matrices.I))
+ matrix += np.kron(np.kron(matrices.I, matrices.Z), np.kron(matrices.Z, matrices.I))
+ matrix += np.kron(np.kron(matrices.I, matrices.I), np.kron(matrices.Z, matrices.Z))
+ matrix += np.kron(np.kron(matrices.Z, matrices.I), np.kron(matrices.I, matrices.Z))
+ # X terms
+ matrix += np.kron(np.kron(matrices.X, matrices.I), np.kron(matrices.I, matrices.I))
+ matrix += np.kron(np.kron(matrices.I, matrices.X), np.kron(matrices.I, matrices.I))
+ matrix += np.kron(np.kron(matrices.I, matrices.I), np.kron(matrices.X, matrices.I))
+ matrix += np.kron(np.kron(matrices.I, matrices.I), np.kron(matrices.I, matrices.X))
+ # Create Hamiltonian object
+ ham = hamiltonians.Hamiltonian(4, matrix)
+
+
+Although it is possible to generalize the above construction to arbitrary number
+of qubits this procedure may be more complex for other Hamiltonians. Moreover
+constructing the full matrix does not scale well with increasing the number of
+qubits. This makes the use of :class:`qibo.hamiltonians.SymbolicHamiltonian`
+preferrable as the qubit number increases, as this Hamiltonians is not based
+in the full matrix representation.
+
+To simplify the construction of Hamiltonians, Qibo provides the
+:class:`qibo.hamiltonians.SymbolicHamiltonian` object which
+allows the user to construct Hamiltonian objects by writing their symbolic
+form using ``sympy`` symbols. Moreover Qibo provides quantum-computation specific
+symbols (:class:`qibo.symbols.Symbol`) such as the Pauli operators.
+For example, the TFIM on four qubits could be constructed as:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import hamiltonians
+ from qibo.symbols import X, Z
+
+ # Define Hamiltonian using Qibo symbols
+ # ZZ terms
+ symbolic_ham = sum(Z(i) * Z(i + 1) for i in range(3))
+ # periodic boundary condition term
+ symbolic_ham += Z(0) * Z(3)
+ # X terms
+ symbolic_ham += sum(X(i) for i in range(4))
+
+ # Define a Hamiltonian using the above form
+ ham = hamiltonians.SymbolicHamiltonian(symbolic_ham)
+ # This Hamiltonian is memory efficient as it does not construct the full matrix
+
+ # The corresponding dense Hamiltonian which contains the full matrix can
+ # be constructed easily as
+ dense_ham = ham.dense
+ # and the matrix is accessed as ``dense_ham.matrix`` or ``ham.matrix``.
+
+
+Defining Hamiltonians from symbols is usually a simple process as the symbolic
+form is very close to the form of the Hamiltonian on paper. Note that when a
+:class:`qibo.hamiltonians.SymbolicHamiltonian` is used for time evolution,
+Qibo handles automatically automatically the Trotter decomposition by splitting
+to the appropriate terms.
+
+Qibo symbols support an additional ``commutative`` argument which is set to
+``False`` by default since quantum operators are non-commuting objects.
+When the user knows that the Hamiltonian consists of commuting terms only, such
+as products of Z operators, switching ``commutative`` to ``True`` may speed-up
+some symbolic calculations, such as the ``sympy.expand`` used when calculating
+the Trotter decomposition for the Hamiltonian. This option can be used when
+constructing each symbol:
+
+
+.. testcode::
+
+ from qibo import hamiltonians
+ from qibo.symbols import Z
+
+ form = Z(0, commutative=True) * Z(1, commutative=True) + Z(1, commutative=True) * Z(2, commutative=True)
+ ham = hamiltonians.SymbolicHamiltonian(form)
+
+
+.. _hamexpectation-example:
+
+How to calculate expectation values using samples?
+--------------------------------------------------
+
+It is possible to calculate the expectation value of a :class:`qibo.hamiltonians.Hamiltonian`
+on a given state using the :meth:`qibo.hamiltonians.Hamiltonian.expectation` method,
+which can be called on a state or density matrix. For example
+
+
+.. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.hamiltonians import XXZ
+
+ circuit = Circuit(4)
+ circuit.add(gates.H(i) for i in range(4))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(2, 3))
+
+ hamiltonian = XXZ(4)
+
+ result = circuit()
+ expectation_value = hamiltonian.expectation(result.state())
+
+In this example, the circuit will be simulated to obtain the final state vector
+and the corresponding expectation value will be calculated through exact matrix
+multiplication with the Hamiltonian matrix.
+If a :class:`qibo.hamiltonians.SymbolicHamiltonian` is used instead, the expectation
+value will be calculated as a sum of expectation values of local terms, allowing
+calculations of more qubits with lower memory consumption. The calculation of each
+local term still requires the state vector.
+
+When executing a circuit on real hardware, usually only measurements of the state are
+available, not the state vector. Qibo provides :meth:`qibo.hamiltonians.Hamiltonian.expectation_from_samples`
+to allow calculation of expectation values directly from such samples:
+
+
+.. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.hamiltonians import Z
+
+ circuit = Circuit(4)
+ circuit.add(gates.H(i) for i in range(4))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(2, 3))
+ circuit.add(gates.M(*range(4)))
+
+ hamiltonian = Z(4)
+
+ result = circuit(nshots=1024)
+ expectation_value = hamiltonian.expectation_from_samples(result.frequencies())
+
+
+This example simulates the circuit similarly to the previous one but calculates
+the expectation value using the frequencies of shots, instead of the exact state vector.
+This can also be invoked directly from the ``result`` object:
+
+.. testcode::
+
+ expectation_value = result.expectation_from_samples(hamiltonian)
+
+
+The expectation from samples currently works only for Hamiltonians that are diagonal in
+the computational basis.
+
+
+.. _tutorials_transpiler:
+
+How to modify the transpiler?
+-----------------------------
+
+Logical quantum circuits for quantum algorithms are hardware agnostic. Usually an all-to-all qubit connectivity
+is assumed while most current hardware only allows the execution of two-qubit gates on a restricted subset of qubit
+pairs. Moreover, quantum devices are restricted to executing a subset of gates, referred to as native.
+This means that, in order to execute circuits on a real quantum chip, they must be transformed into an equivalent,
+hardware specific, circuit. The transformation of the circuit is carried out by the transpiler through the resolution
+of two key steps: connectivity matching and native gates decomposition.
+In order to execute a gate between two qubits that are not directly connected SWAP gates are required. This procedure is called routing.
+As on NISQ devices two-qubit gates are a large source of noise, this procedure generates an overall noisier circuit.
+Therefore, the goal of an efficient routing algorithm is to minimize the number of SWAP gates introduced.
+An important step to ease the connectivity problem, is finding anoptimal initial mapping between logical and physical qubits.
+This step is called placement.
+The native gates decomposition in the transpiling procedure is performed by the unroller. An optimal decomposition uses the least amount
+of two-qubit native gates. It is also possible to reduce the number of gates of the resulting circuit by exploiting
+commutation relations, KAK decomposition or machine learning techniques.
+Qibo implements a built-in transpiler with customizable options for each step. The main algorithms that can
+be used at each transpiler step are reported below with a short description.
+
+The initial placement can be found with one of the following procedures:
+- Trivial: logical-physical qubit mapping is an identity.
+- Custom: custom logical-physical qubit mapping.
+- Random greedy: the best mapping is found within a set of random layouts based on a greedy policy.
+- Subgraph isomorphism: the initial mapping is the one that guarantees the execution of most gates at
+the beginning of the circuit without introducing any SWAP.
+- Reverse traversal: this technique uses one or more reverse routing passes to find an optimal mapping by
+starting from a trivial layout.
+
+The routing problem can be solved with the following algorithms:
+- Shortest paths: when unconnected logical qubits have to interact, they are moved on the chip on
+the shortest path connecting them. When multiple shortest paths are present, the one that also matches
+the largest number of the following two-qubit gates is chosen.
+- Sabre: this heuristic routing technique uses a customizable cost function to add SWAP gates
+that reduce the distance between unconnected qubits involved in two-qubit gates.
+
+Qibolab unroller applies recursively a set of hard-coded gates decompositions in order to translate any gate into
+single and two-qubit native gates. Single qubit gates are translated into U3, RX, RZ, X and Z gates. It is possible to
+fuse multiple single qubit gates acting on the same qubit into a single U3 gate. For the two-qubit native gates it
+is possible to use CZ and/or iSWAP. When both CZ and iSWAP gates are available the chosen decomposition is the
+one that minimizes the use of two-qubit gates.
+
+Multiple transpilation steps can be implemented using the :class:`qibo.transpiler.pipeline.Pipeline`:
+
+.. testcode:: python
+
+ import networkx as nx
+
+ from qibo import gates
+ from qibo.models import Circuit
+ from qibo.transpiler.pipeline import Passes, assert_transpiling
+ from qibo.transpiler.optimizer import Preprocessing
+ from qibo.transpiler.router import ShortestPaths
+ from qibo.transpiler.unroller import Unroller, NativeGates
+ from qibo.transpiler.placer import Random
+
+ # Define connectivity as nx.Graph
+ def star_connectivity():
+ Q = [i for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2]
+ chip.add_edges_from(graph_list)
+ return chip
+
+ # Define the circuit
+ circuit = Circuit(2)
+ circuit.add(gates.H(0))
+ circuit.add(gates.CZ(0, 1))
+
+ # Define custom passes as a list
+ custom_passes = []
+ # Preprocessing adds qubits in the original circuit to match the number of qubits in the chip
+ custom_passes.append(Preprocessing(connectivity=star_connectivity()))
+ # Placement step
+ custom_passes.append(Random(connectivity=star_connectivity()))
+ # Routing step
+ custom_passes.append(ShortestPaths(connectivity=star_connectivity()))
+ # Gate decomposition step
+ custom_passes.append(Unroller(native_gates=NativeGates.default()))
+
+ # Define the general pipeline
+ custom_pipeline = Passes(custom_passes, connectivity=star_connectivity(), native_gates=NativeGates.default())
+
+ # Call the transpiler pipeline on the circuit
+ transpiled_circ, final_layout = custom_pipeline(circuit)
+
+ # Optinally call assert_transpiling to check that the final circuit can be executed on hardware
+ # For this test it is necessary to get the initial layout
+ initial_layout = custom_pipeline.get_initial_layout()
+ assert_transpiling(
+ original_circuit=circuit,
+ transpiled_circuit=transpiled_circ,
+ connectivity=star_connectivity(),
+ initial_layout=initial_layout,
+ final_layout=final_layout,
+ native_gates=NativeGates.default()
+ )
+
+In this case circuits will first be transpiled to respect the 5-qubit star connectivity, with qubit 2 as the middle qubit. This will potentially add some SWAP gates.
+Then all gates will be converted to native. The :class:`qibo.transpiler.unroller.Unroller` transpiler used in this example assumes Z, RZ, GPI2 or U3 as
+the single-qubit native gates, and supports CZ and iSWAP as two-qubit natives. In this case we restricted the two-qubit gate set to CZ only.
+The final_layout contains the final logical-physical qubit mapping.
+
+.. _gst_example:
+
+How to perform Gate Set Tomography?
+-----------------------------------
+
+In order to obtain an estimated representation of a set of quantum gates in a particular noisy environment, qibo provides a GST routine in its tomography module.
+
+Let's first define the set of gates we want to estimate:
+
+.. testcode::
+
+ from qibo import gates
+
+ gate_set = {gates.X, gates.H, gates.CZ}
+
+For simulation purposes we can define a noise model. Naturally this is not needed when running on real quantum hardware, which is intrinsically noisy. For example, we can suppose that the three gates we want to estimate are going to be noisy:
+
+.. testcode::
+
+ from qibo.noise import NoiseModel, DepolarizingError
+
+ noise_model = NoiseModel()
+ noise_model.add(DepolarizingError(1e-3), gates.X)
+ noise_model.add(DepolarizingError(1e-2), gates.H)
+ noise_model.add(DepolarizingError(3e-2), gates.CZ)
+
+Then the estimated representation of the gates in this noisy environment can be extracted by running the GST:
+
+.. testcode::
+
+ from qibo.tomography import GST
+
+ estimated_gates = GST(
+ gate_set = gate_set,
+ nshots = 10000,
+ noise_model = noise_model
+ )
+
+In some cases the empty circuit matrix :math:`E` can also be useful, and can be returned by setting the ``include_empty`` argument to ``True``:
+
+.. testcode::
+
+ empty_1q, empty_2q, *estimated_gates = GST(
+ gate_set = gate_set,
+ nshots = 10000,
+ noise_model = noise_model,
+ include_empty = True,
+ )
+
+where ``empty_1q`` and ``empty_2q`` correspond to the single and two qubits empty matrices respectively.
+Similarly, the Pauli-Liouville representation of the gates can be directly returned as well:
+
+.. testcode::
+
+ estimated_gates = GST(
+ gate_set = gate_set,
+ nshots = 10000,
+ noise_model = noise_model,
+ pauli_liouville = True,
+ )
diff --git a/doc/source/code-examples/applications-by-algorithm.rst b/doc/source/code-examples/applications-by-algorithm.rst
new file mode 100644
index 000000000..5726a779d
--- /dev/null
+++ b/doc/source/code-examples/applications-by-algorithm.rst
@@ -0,0 +1,81 @@
+Applications by algorithm
+-------------------------
+
+Variational Quantum Circuits
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/aavqe/README.md
+ tutorials/variational_classifier/README.md
+ tutorials/reuploading_classifier/README.md
+ tutorials/vqregressor/README.md
+ tutorials/autoencoder/README.md
+ tutorials/qsvd/README.md
+ tutorials/3_tangle/README.md
+ tutorials/EF_QAE/README.md
+ tutorials/unary/README.md
+ tutorials/bell-variational/README.md
+ tutorials/qPDF/qPDF.ipynb
+ tutorials/anomaly_detection/README.md
+ tutorials/qcnn_classifier/qcnn_demo.ipynb
+ tutorials/adiabatic_qml/adiabatic-qml.ipynb
+
+Grover's Algorithm
+^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/grover3sat/README.md
+ tutorials/hash-grover/README.md
+ tutorials/grover/README.md
+ tutorials/qfiae/qfiae_demo.ipynb
+
+
+Shor's Factorization Algorithms
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/shor/README.md
+
+
+Adiabatic Evolution
+^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/adiabatic/README.md
+ tutorials/adiabatic3sat/README.md
+
+
+FALQON
+^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/falqon/README.md
+
+Clustering
+^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qclustering/README.md
+
+
+Diagonalization Algorithms
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/dbi/README.md
+
+ tutorials/dbi/dbi_tutorial_basic_intro.ipynb
diff --git a/doc/source/code-examples/applications-by-topic.rst b/doc/source/code-examples/applications-by-topic.rst
new file mode 100644
index 000000000..500300e9b
--- /dev/null
+++ b/doc/source/code-examples/applications-by-topic.rst
@@ -0,0 +1,88 @@
+Applications by topic
+---------------------
+
+Complexity theory
+^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/grover3sat/README.md
+ tutorials/adiabatic3sat/README.md
+
+
+Condensed Matter Physics
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/aavqe/README.md
+
+
+Cryptography
+^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/hash-grover/README.md
+
+
+Finance
+^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/unary/README.md
+
+
+High-Energy Physics
+^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qPDF/qPDF.ipynb
+ tutorials/anomaly_detection/README.md
+
+
+Quantum Physics
+^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qsvd/README.md
+ tutorials/3_tangle/README.md
+ tutorials/adiabatic/README.md
+ tutorials/shor/README.md
+ tutorials/bell-variational/README.md
+ tutorials/falqon/README.md
+ tutorials/grover/README.md
+
+Quantum Machine Learning
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/variational_classifier/README.md
+ tutorials/reuploading_classifier/README.md
+ tutorials/vqregressor/README.md
+ tutorials/autoencoder/README.md
+ tutorials/EF_QAE/README.md
+ tutorials/qfiae/qfiae_demo.ipynb
+ tutorials/qcnn_classifier/qcnn_demo.ipynb
+ tutorials/qclustering/README.md
+ tutorials/adiabatic_qml/adiabatic-qml.ipynb
+
+Combinatorics
+^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qap/README.md
+ tutorials/mvc/README.md
diff --git a/doc/source/code-examples/applications.rst b/doc/source/code-examples/applications.rst
new file mode 100644
index 000000000..6b1337677
--- /dev/null
+++ b/doc/source/code-examples/applications.rst
@@ -0,0 +1,177 @@
+.. _applications:
+
+Applications
+============
+
+In this section we present some examples of quantum circuits applied to specific
+problems.
+
+Applications by topic
+---------------------
+
+Complexity theory
+^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/grover3sat/README.md
+ tutorials/adiabatic3sat/README.md
+
+
+Condensed Matter Physics
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/aavqe/README.md
+
+
+Cryptography
+^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/hash-grover/README.md
+
+
+Finance
+^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/unary/README.md
+
+
+High-Energy Physics
+^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qPDF/qPDF.ipynb
+ tutorials/anomaly_detection/README.md
+
+
+Quantum Physics
+^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qsvd/README.md
+ tutorials/3_tangle/README.md
+ tutorials/adiabatic/README.md
+ tutorials/shor/README.md
+ tutorials/bell-variational/README.md
+ tutorials/falqon/README.md
+ tutorials/grover/README.md
+
+Quantum Machine Learning
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/variational_classifier/README.md
+ tutorials/reuploading_classifier/README.md
+ tutorials/vqregressor/README.md
+ tutorials/autoencoder/README.md
+ tutorials/EF_QAE/README.md
+ tutorials/qfiae/qfiae_demo.ipynb
+ tutorials/qcnn_classifier/qcnn_demo.ipynb
+ tutorials/qclustering/README.md
+ tutorials/adiabatic_qml/adiabatic-qml.ipynb
+
+Combinatorics
+^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/qap/README.md
+ tutorials/mvc/README.md
+
+
+Applications by algorithm
+-------------------------
+
+Variational Quantum Circuits
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/aavqe/README.md
+ tutorials/variational_classifier/README.md
+ tutorials/reuploading_classifier/README.md
+ tutorials/vqregressor/README.md
+ tutorials/autoencoder/README.md
+ tutorials/qsvd/README.md
+ tutorials/3_tangle/README.md
+ tutorials/EF_QAE/README.md
+ tutorials/unary/README.md
+ tutorials/bell-variational/README.md
+ tutorials/qPDF/qPDF.ipynb
+ tutorials/anomaly_detection/README.md
+ tutorials/qcnn_classifier/qcnn_demo.ipynb
+ tutorials/adiabatic_qml/adiabatic-qml.ipynb
+
+Grover's Algorithm
+^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/grover3sat/README.md
+ tutorials/hash-grover/README.md
+ tutorials/grover/README.md
+ tutorials/qfiae/qfiae_demo.ipynb
+
+
+Shor's Factorization Algorithms
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/shor/README.md
+
+
+Adiabatic Evolution
+^^^^^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/adiabatic/README.md
+ tutorials/adiabatic3sat/README.md
+
+
+Diagonalization
+^^^^^^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/dbi/dbi_tutorial_basic_intro.ipynb
+
+FALQON
+^^^^^^
+
+.. toctree::
+ :maxdepth: 1
+
+ tutorials/falqon/README.md
+
+Clustering
+^^^^^^^^^^
+
+.. toctree::
+ :maxdepth: 2
+
+ applications-by-topic
+ applications-by-algorithm
diff --git a/doc/source/code-examples/examples.rst b/doc/source/code-examples/examples.rst
new file mode 100644
index 000000000..1d3d43b52
--- /dev/null
+++ b/doc/source/code-examples/examples.rst
@@ -0,0 +1,329 @@
+Basic examples
+==============
+
+Here are a few short basic `how to` examples.
+
+How to write and execute a circuit?
+-----------------------------------
+
+Here is an example of a circuit with 2 qubits:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import Circuit, gates
+
+ # Construct the circuit
+ c = Circuit(2)
+ # Add some gates
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ # Define an initial state (optional - default initial state is |00>)
+ initial_state = np.ones(4) / 2.0
+ # Execute the circuit and obtain the final state
+ result = c(initial_state) # c.execute(initial_state) also works
+ print(result.state())
+ # should print `tf.Tensor([1, 0, 0, 0])`
+ print(result.state())
+ # should print `np.array([1, 0, 0, 0])`
+
+.. testoutput::
+ :hide:
+
+ ...
+
+If you are planning to freeze the circuit and just query for different initial
+states then you can use the ``Circuit.compile()`` method which will improve
+evaluation performance, e.g.:
+
+.. code-block:: python
+
+ import numpy as np
+ # switch backend to "tensorflow"
+ import qibo
+ qibo.set_backend("tensorflow")
+ from qibo import Circuit, gates
+
+ c = Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+ c.add(gates.CU1(0, 1, 0.1234))
+ c.compile()
+
+ for i in range(100):
+ init_state = np.ones(4) / 2.0 + i
+ c(init_state)
+
+Note that compiling is only supported when the native ``tensorflow`` backend is
+used. This backend is much slower than ``qibojit`` which uses custom operators
+to apply gates.
+
+
+How to print a circuit summary?
+-------------------------------
+
+It is possible to print a summary of the circuit using ``circuit.summary()``.
+This will print basic information about the circuit, including its depth, the
+total number of qubits and all gates in order of the number of times they appear.
+The QASM name is used as identifier of gates.
+For example
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.H(2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ print(c.summary())
+ # Prints
+ '''
+ Circuit depth = 5
+ Total number of gates = 6
+ Number of qubits = 3
+ Most common gates:
+ h: 3
+ cx: 2
+ ccx: 1
+ '''
+.. testoutput::
+ :hide:
+
+ Circuit depth = 5
+ Total number of gates = 6
+ Number of qubits = 3
+ Most common gates:
+ h: 3
+ cx: 2
+ ccx: 1
+
+
+The circuit property ``circuit.gate_types`` (or ``circuit.gate_names``) will return a ``collections.Counter``
+that contains the gate types (or names) and the corresponding numbers of appearance. The
+method ``circuit.gates_of_type()`` can be used to access gate objects of specific type or name.
+For example for the circuit of the previous example:
+
+.. testsetup::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.H(2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+
+.. testcode::
+
+ common_gates = c.gate_names.most_common()
+ # returns the list [("h", 3), ("cx", 2), ("ccx", 1)]
+
+ most_common_gate = common_gates[0][0]
+ # returns "h"
+
+ all_h_gates = c.gates_of_type(gates.H)
+ # returns the list [(0, ref to H(0)), (1, ref to H(1)), (4, ref to H(2))]
+
+A circuit may contain multi-controlled or other gates that are not supported by
+OpenQASM. The ``circuit.decompose(*free)`` method decomposes such gates to
+others that are supported by OpenQASM. For this decomposition to work the user
+has to specify which qubits can be used as free/work. For more information on
+this decomposition we refer to the related publication on
+`arXiv:9503016 `_. Currently only the
+decomposition of multi-controlled ``X`` gates is implemented.
+
+
+.. _measurement-examples:
+
+How to perform measurements?
+----------------------------
+
+In order to obtain measurement results from a circuit one has to add measurement
+gates (:class:`qibo.abstractions.gates.M`) and provide a number of shots (``nshots``)
+when executing the circuit. In this case the returned
+:class:`qibo.abstractions.states.AbstractState` will contain all the
+information about the measured samples. For example
+
+.. testsetup::
+ import qibo
+ qibo.set_backend("numpy")
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(2)
+ c.add(gates.X(0))
+ # Add a measurement register on both qubits
+ c.add(gates.M(0, 1))
+ # Execute the circuit with the default initial state |00>.
+ result = c(nshots=100)
+
+Measurements are now accessible using the ``samples`` and ``frequencies`` methods
+on the ``result`` object. In particular
+
+* ``result.samples(binary=True)`` will return the array ``[[1, 0], [1, 0], ..., [1, 0]]`` with shape ``(100, 2)``,
+* ``result.samples(binary=False)`` will return the array ``[2, 2, ..., 2]``,
+* ``result.frequencies(binary=True)`` will return ``collections.Counter({"10": 100})``,
+* ``result.frequencies(binary=False)`` will return ``collections.Counter({2: 100})``.
+
+In addition to the functionality described above, it is possible to collect
+measurement results grouped according to registers. The registers are defined
+during the addition of measurement gates in the circuit. For example
+
+.. testsetup::
+ import qibo
+ qibo.set_backend("numpy")
+
+.. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(5)
+ c.add(gates.X(0))
+ c.add(gates.X(4))
+ c.add(gates.M(0, 1, register_name="A"))
+ c.add(gates.M(3, 4, register_name="B"))
+ result = c(nshots=100)
+
+creates a circuit with five qubits that has two registers: ``A`` consisting of
+qubits ``0`` and ``1`` and ``B`` consisting of qubits ``3`` and ``4``. Here
+qubit ``2`` remains unmeasured. Measured results can now be accessed as
+
+* ``result.samples(binary=False, registers=True)`` will return a dictionary with the measured sample tensors for each register: ``{"A": [2, 2, ...], "B": [1, 1, ...]}``,
+* ``result.frequencies(binary=True, registers=True)`` will return a dictionary with the frequencies for each register: ``{"A": collections.Counter({"10": 100}), "B": collections.Counter({"01": 100})}``.
+
+Setting ``registers=False`` (default option) will ignore the registers and return the
+results similarly to the previous example. For example ``result.frequencies(binary=True)``
+will return ``collections.Counter({"1001": 100})``.
+
+It is possible to define registers of multiple qubits by either passing
+the qubit ids seperately, such as ``gates.M(0, 1, 2, 4)``, or using the ``*``
+operator: ``gates.M(*[0, 1, 2, 4])``. The ``*`` operator is useful if qubit
+ids are saved in an iterable. For example ``gates.M(*range(5))`` is equivalent
+to ``gates.M(0, 1, 2, 3, 4)``.
+
+Unmeasured qubits are ignored by the measurement objects. Also, the
+order that qubits appear in the results is defined by the order the user added
+the measurements and not the qubit ids.
+
+The final state vector is still accessible via :meth:`qibo.measurements.CircuitResult.state`.
+Note that the state vector accessed this way corresponds to the state as if no
+measurements occurred, that is the state is not collapsed during the measurement.
+This is because measurement gates are only used to sample bitstrings and do not
+have any effect on the state vector. There are two reasons for this choice.
+First, when more than one measurement shots are used the final collapsed state
+is not uniquely defined as it would be different for each measurement result.
+Second the user may wish to re-sample the final state vector in order to
+obtain more measurement shots without having to re-execute the full simulation.
+For applications that require the state vector to be collapsed during measurements
+we refer to the :ref:`How to collapse state during measurements? `
+
+The measured shots are obtained using pseudo-random number generators of the
+underlying backend (numpy or Tensorflow). If the user has installed a custom
+backend (eg. qibojit) and asks for frequencies with more than 100000 shots,
+a custom Metropolis algorithm will be used to obtain the corresponding samples,
+for increase performance. The user can change the threshold for which this
+algorithm is used using the ``qibo.set_metropolis_threshold()`` method,
+for example:
+
+.. testcode::
+
+ import qibo
+
+ print(qibo.get_metropolis_threshold()) # prints 100000
+ qibo.set_metropolis_threshold(int(1e8))
+ print(qibo.get_metropolis_threshold()) # prints 10^8
+.. testoutput::
+ :hide:
+
+ 100000
+ 100000000
+
+
+If the Metropolis algorithm is not used and the user asks for frequencies with
+a high number of shots then the corresponding samples are generated in batches.
+The batch size can be controlled using the ``qibo.get_batch_size()`` and
+``qibo.set_batch_size()`` functions similarly to the above example.
+The default batch size is 2^18.
+
+
+How to write a Quantum Fourier Transform?
+-----------------------------------------
+
+A simple Quantum Fourier Transform (QFT) example to test your installation:
+
+.. testcode::
+
+ from qibo.models import QFT
+
+ # Create a QFT circuit with 15 qubits
+ circuit = QFT(15)
+
+ # Simulate final state wavefunction default initial state is |00>
+ final_state = circuit()
+
+
+Please note that the ``QFT()`` function is simply a shorthand for the circuit
+construction. For number of qubits higher than 30, the QFT can be distributed to
+multiple GPUs using ``QFT(31, accelerators)``. Further details are presented in
+the section :ref:`How to select hardware devices? `.
+
+
+.. _precision-example:
+
+How to modify the simulation precision?
+---------------------------------------
+
+By default the simulation is performed in ``double`` precision (``complex128``).
+We provide the ``qibo.set_precision`` function to modify the default behaviour.
+Note that `qibo.set_precision` must be called before allocating circuits:
+
+.. testcode::
+
+ import qibo
+ qibo.set_precision("single") # enables complex64
+ # or
+ qibo.set_precision("double") # re-enables complex128
+
+ # ... continue with circuit creation and execution
+
+
+.. _visualize-example:
+
+How to visualize a circuit?
+---------------------------
+
+It is possible to print a schematic diagram of the circuit using ``circuit.draw()``.
+This will print an unicode text based representation of the circuit, including gates,
+and qubits lines.
+For example
+
+.. testcode::
+
+ from qibo.models import QFT
+
+ c = QFT(5)
+ print(c.draw())
+ # Prints
+ '''
+ q0: ─H─U1─U1─U1─U1───────────────────────────x───
+ q1: ───o──|──|──|──H─U1─U1─U1────────────────|─x─
+ q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─
+ q3: ─────────o──|───────o──|────o──|──H─U1───|─x─
+ q4: ────────────o──────────o───────o────o──H─x───
+ '''
+.. testoutput::
+ :hide:
+
+ q0: ─H─U1─U1─U1─U1───────────────────────────x───
+ q1: ───o──|──|──|──H─U1─U1─U1────────────────|─x─
+ q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─
+ q3: ─────────o──|───────o──|────o──|──H─U1───|─x─
+ q4: ────────────o──────────o───────o────o──H─x───
diff --git a/doc/source/code-examples/index.rst b/doc/source/code-examples/index.rst
new file mode 100644
index 000000000..408de1dc5
--- /dev/null
+++ b/doc/source/code-examples/index.rst
@@ -0,0 +1,15 @@
+.. _examples:
+
+Code examples
+=============
+
+In this section we present code examples from basic to advanced features
+implemented in Qibo. Furthermore we include a list of quantum applications for
+different topics and quantum algorithms:
+
+.. toctree::
+ :maxdepth: 2
+
+ examples
+ advancedexamples
+ applications
diff --git a/doc/source/code-examples/tutorials/3_tangle/README.md b/doc/source/code-examples/tutorials/3_tangle/README.md
new file mode 120000
index 000000000..f008ff00c
--- /dev/null
+++ b/doc/source/code-examples/tutorials/3_tangle/README.md
@@ -0,0 +1 @@
+../../../../../examples/3_tangle/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/3_tangle/images b/doc/source/code-examples/tutorials/3_tangle/images
new file mode 120000
index 000000000..e9ad26f35
--- /dev/null
+++ b/doc/source/code-examples/tutorials/3_tangle/images
@@ -0,0 +1 @@
+../../../../../examples/3_tangle/images
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/EF_QAE/README.md b/doc/source/code-examples/tutorials/EF_QAE/README.md
new file mode 120000
index 000000000..ee97ab024
--- /dev/null
+++ b/doc/source/code-examples/tutorials/EF_QAE/README.md
@@ -0,0 +1 @@
+../../../../../examples/EF_QAE/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/EF_QAE/images b/doc/source/code-examples/tutorials/EF_QAE/images
new file mode 120000
index 000000000..ac0e0cd3a
--- /dev/null
+++ b/doc/source/code-examples/tutorials/EF_QAE/images
@@ -0,0 +1 @@
+../../../../../examples/EF_QAE/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/aavqe/README.md b/doc/source/code-examples/tutorials/aavqe/README.md
new file mode 120000
index 000000000..e4f69db74
--- /dev/null
+++ b/doc/source/code-examples/tutorials/aavqe/README.md
@@ -0,0 +1 @@
+../../../../../examples/aavqe/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/aavqe/images b/doc/source/code-examples/tutorials/aavqe/images
new file mode 120000
index 000000000..217ce013d
--- /dev/null
+++ b/doc/source/code-examples/tutorials/aavqe/images
@@ -0,0 +1 @@
+../../../../../examples/aavqe/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic/README.md b/doc/source/code-examples/tutorials/adiabatic/README.md
new file mode 120000
index 000000000..6e57b3afa
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic/README.md
@@ -0,0 +1 @@
+../../../../../examples/adiabatic/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic/images b/doc/source/code-examples/tutorials/adiabatic/images
new file mode 120000
index 000000000..570e89017
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic/images
@@ -0,0 +1 @@
+../../../../../examples/adiabatic/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic3sat/README.md b/doc/source/code-examples/tutorials/adiabatic3sat/README.md
new file mode 120000
index 000000000..4f6d15905
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic3sat/README.md
@@ -0,0 +1 @@
+../../../../../examples/adiabatic3sat/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic3sat/images b/doc/source/code-examples/tutorials/adiabatic3sat/images
new file mode 120000
index 000000000..efd89b073
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic3sat/images
@@ -0,0 +1 @@
+../../../../../examples/adiabatic3sat/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic_qml/adiabatic-qml.ipynb b/doc/source/code-examples/tutorials/adiabatic_qml/adiabatic-qml.ipynb
new file mode 120000
index 000000000..32041dfb0
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic_qml/adiabatic-qml.ipynb
@@ -0,0 +1 @@
+../../../../../examples/adiabatic_qml/adiabatic-qml.ipynb
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/adiabatic_qml/images b/doc/source/code-examples/tutorials/adiabatic_qml/images
new file mode 120000
index 000000000..f5e96ef28
--- /dev/null
+++ b/doc/source/code-examples/tutorials/adiabatic_qml/images
@@ -0,0 +1 @@
+../../../../../examples/adiabatic_qml/images
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/anomaly_detection/README.md b/doc/source/code-examples/tutorials/anomaly_detection/README.md
new file mode 120000
index 000000000..631704e07
--- /dev/null
+++ b/doc/source/code-examples/tutorials/anomaly_detection/README.md
@@ -0,0 +1 @@
+../../../../../examples/anomaly_detection/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/anomaly_detection/images b/doc/source/code-examples/tutorials/anomaly_detection/images
new file mode 120000
index 000000000..9bee242a0
--- /dev/null
+++ b/doc/source/code-examples/tutorials/anomaly_detection/images
@@ -0,0 +1 @@
+../../../../../examples/anomaly_detection/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/autoencoder/README.md b/doc/source/code-examples/tutorials/autoencoder/README.md
new file mode 120000
index 000000000..2525ed147
--- /dev/null
+++ b/doc/source/code-examples/tutorials/autoencoder/README.md
@@ -0,0 +1 @@
+../../../../../examples/autoencoder/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/autoencoder/images b/doc/source/code-examples/tutorials/autoencoder/images
new file mode 120000
index 000000000..537542031
--- /dev/null
+++ b/doc/source/code-examples/tutorials/autoencoder/images
@@ -0,0 +1 @@
+../../../../../examples/autoencoder/images
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/bell-variational/README.md b/doc/source/code-examples/tutorials/bell-variational/README.md
new file mode 120000
index 000000000..8b7692fb0
--- /dev/null
+++ b/doc/source/code-examples/tutorials/bell-variational/README.md
@@ -0,0 +1 @@
+../../../../../examples/bell-variational/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/bell-variational/images b/doc/source/code-examples/tutorials/bell-variational/images
new file mode 120000
index 000000000..7cd50ce4f
--- /dev/null
+++ b/doc/source/code-examples/tutorials/bell-variational/images
@@ -0,0 +1 @@
+../../../../../examples/bell-variational/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/dbi/README.md b/doc/source/code-examples/tutorials/dbi/README.md
new file mode 120000
index 000000000..50b9e9eae
--- /dev/null
+++ b/doc/source/code-examples/tutorials/dbi/README.md
@@ -0,0 +1 @@
+../../../../../examples/dbi/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/dbi/dbi_tutorial_basic_intro.ipynb b/doc/source/code-examples/tutorials/dbi/dbi_tutorial_basic_intro.ipynb
new file mode 120000
index 000000000..79ea4d0ea
--- /dev/null
+++ b/doc/source/code-examples/tutorials/dbi/dbi_tutorial_basic_intro.ipynb
@@ -0,0 +1 @@
+../../../../../examples/dbi/dbi_tutorial_basic_intro.ipynb
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/falqon/README.md b/doc/source/code-examples/tutorials/falqon/README.md
new file mode 120000
index 000000000..8e2ad3360
--- /dev/null
+++ b/doc/source/code-examples/tutorials/falqon/README.md
@@ -0,0 +1 @@
+../../../../../examples/falqon/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/falqon/images b/doc/source/code-examples/tutorials/falqon/images
new file mode 120000
index 000000000..196328214
--- /dev/null
+++ b/doc/source/code-examples/tutorials/falqon/images
@@ -0,0 +1 @@
+../../../../../examples/falqon/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/grover/README.md b/doc/source/code-examples/tutorials/grover/README.md
new file mode 120000
index 000000000..2d8afa998
--- /dev/null
+++ b/doc/source/code-examples/tutorials/grover/README.md
@@ -0,0 +1 @@
+../../../../../examples/grover/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/grover3sat/README.md b/doc/source/code-examples/tutorials/grover3sat/README.md
new file mode 120000
index 000000000..4dd053dd1
--- /dev/null
+++ b/doc/source/code-examples/tutorials/grover3sat/README.md
@@ -0,0 +1 @@
+../../../../../examples/grover3sat/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/grover3sat/images b/doc/source/code-examples/tutorials/grover3sat/images
new file mode 120000
index 000000000..b9881a98a
--- /dev/null
+++ b/doc/source/code-examples/tutorials/grover3sat/images
@@ -0,0 +1 @@
+../../../../../examples/grover3sat/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/hash-grover/README.md b/doc/source/code-examples/tutorials/hash-grover/README.md
new file mode 120000
index 000000000..551d2cbc8
--- /dev/null
+++ b/doc/source/code-examples/tutorials/hash-grover/README.md
@@ -0,0 +1 @@
+../../../../../examples/hash-grover/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/hash-grover/images b/doc/source/code-examples/tutorials/hash-grover/images
new file mode 120000
index 000000000..e3402622e
--- /dev/null
+++ b/doc/source/code-examples/tutorials/hash-grover/images
@@ -0,0 +1 @@
+../../../../../examples/hash-grover/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/mvc/README.md b/doc/source/code-examples/tutorials/mvc/README.md
new file mode 120000
index 000000000..5e9397841
--- /dev/null
+++ b/doc/source/code-examples/tutorials/mvc/README.md
@@ -0,0 +1 @@
+../../../../../examples/mvc/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qPDF/qPDF.ipynb b/doc/source/code-examples/tutorials/qPDF/qPDF.ipynb
new file mode 120000
index 000000000..fa2a78e8a
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qPDF/qPDF.ipynb
@@ -0,0 +1 @@
+../../../../../examples/qPDF/qPDF.ipynb
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qap/README.md b/doc/source/code-examples/tutorials/qap/README.md
new file mode 120000
index 000000000..047aaaa93
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qap/README.md
@@ -0,0 +1 @@
+../../../../../examples/qap/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qclustering/README.md b/doc/source/code-examples/tutorials/qclustering/README.md
new file mode 120000
index 000000000..3f4087fbb
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qclustering/README.md
@@ -0,0 +1 @@
+../../../../../examples/qclustering/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qclustering/figures b/doc/source/code-examples/tutorials/qclustering/figures
new file mode 120000
index 000000000..e2ddeeb1c
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qclustering/figures
@@ -0,0 +1 @@
+../../../../../examples/qclustering/figures
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qcnn_classifier/images b/doc/source/code-examples/tutorials/qcnn_classifier/images
new file mode 120000
index 000000000..47c2f1309
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qcnn_classifier/images
@@ -0,0 +1 @@
+../../../../../examples/qcnn_classifier/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qcnn_classifier/qcnn_demo.ipynb b/doc/source/code-examples/tutorials/qcnn_classifier/qcnn_demo.ipynb
new file mode 120000
index 000000000..8e11aae35
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qcnn_classifier/qcnn_demo.ipynb
@@ -0,0 +1 @@
+../../../../../examples/qcnn_classifier/qcnn_demo.ipynb
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qfiae/images b/doc/source/code-examples/tutorials/qfiae/images
new file mode 120000
index 000000000..ab88b09df
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qfiae/images
@@ -0,0 +1 @@
+../../../../../examples/qfiae/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qfiae/qfiae_demo.ipynb b/doc/source/code-examples/tutorials/qfiae/qfiae_demo.ipynb
new file mode 120000
index 000000000..220fd5740
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qfiae/qfiae_demo.ipynb
@@ -0,0 +1 @@
+../../../../../examples/qfiae/qfiae_demo.ipynb
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qsvd/README.md b/doc/source/code-examples/tutorials/qsvd/README.md
new file mode 120000
index 000000000..1d6120205
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qsvd/README.md
@@ -0,0 +1 @@
+../../../../../examples/qsvd/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/qsvd/images b/doc/source/code-examples/tutorials/qsvd/images
new file mode 120000
index 000000000..6f9e86fde
--- /dev/null
+++ b/doc/source/code-examples/tutorials/qsvd/images
@@ -0,0 +1 @@
+../../../../../examples/qsvd/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/reuploading_classifier/README.md b/doc/source/code-examples/tutorials/reuploading_classifier/README.md
new file mode 120000
index 000000000..ac484a5fc
--- /dev/null
+++ b/doc/source/code-examples/tutorials/reuploading_classifier/README.md
@@ -0,0 +1 @@
+../../../../../examples/reuploading_classifier/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/reuploading_classifier/images b/doc/source/code-examples/tutorials/reuploading_classifier/images
new file mode 120000
index 000000000..b30dfa931
--- /dev/null
+++ b/doc/source/code-examples/tutorials/reuploading_classifier/images
@@ -0,0 +1 @@
+../../../../../examples/reuploading_classifier/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/shor/README.md b/doc/source/code-examples/tutorials/shor/README.md
new file mode 120000
index 000000000..d4f07f9e5
--- /dev/null
+++ b/doc/source/code-examples/tutorials/shor/README.md
@@ -0,0 +1 @@
+../../../../../examples/shor/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/shor/images b/doc/source/code-examples/tutorials/shor/images
new file mode 120000
index 000000000..60ad930b4
--- /dev/null
+++ b/doc/source/code-examples/tutorials/shor/images
@@ -0,0 +1 @@
+../../../../../examples/shor/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/unary/README.md b/doc/source/code-examples/tutorials/unary/README.md
new file mode 120000
index 000000000..2573c3e8c
--- /dev/null
+++ b/doc/source/code-examples/tutorials/unary/README.md
@@ -0,0 +1 @@
+../../../../../examples/unary/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/unary/images b/doc/source/code-examples/tutorials/unary/images
new file mode 120000
index 000000000..7f0f7f8fd
--- /dev/null
+++ b/doc/source/code-examples/tutorials/unary/images
@@ -0,0 +1 @@
+../../../../../examples/unary/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/variational_classifier/README.md b/doc/source/code-examples/tutorials/variational_classifier/README.md
new file mode 120000
index 000000000..fdee1c3bf
--- /dev/null
+++ b/doc/source/code-examples/tutorials/variational_classifier/README.md
@@ -0,0 +1 @@
+../../../../../examples/variational_classifier/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/variational_classifier/images b/doc/source/code-examples/tutorials/variational_classifier/images
new file mode 120000
index 000000000..de38c2c0a
--- /dev/null
+++ b/doc/source/code-examples/tutorials/variational_classifier/images
@@ -0,0 +1 @@
+../../../../../examples/variational_classifier/images/
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/vqregressor/README.md b/doc/source/code-examples/tutorials/vqregressor/README.md
new file mode 120000
index 000000000..52af3f97a
--- /dev/null
+++ b/doc/source/code-examples/tutorials/vqregressor/README.md
@@ -0,0 +1 @@
+../../../../../examples/vqregressor/README.md
\ No newline at end of file
diff --git a/doc/source/code-examples/tutorials/vqregressor/images b/doc/source/code-examples/tutorials/vqregressor/images
new file mode 120000
index 000000000..f7b916aa2
--- /dev/null
+++ b/doc/source/code-examples/tutorials/vqregressor/images
@@ -0,0 +1 @@
+../../../../../examples/vqregressor/images
\ No newline at end of file
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 000000000..4937bd588
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,138 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+from recommonmark.transform import AutoStructify
+
+sys.path.insert(0, os.path.abspath(".."))
+import qibo
+
+# -- Project information -----------------------------------------------------
+
+project = "qibo"
+copyright = "The Qibo team"
+author = "The Qibo team"
+
+# The full version, including alpha/beta/rc tags
+release = qibo.__version__
+
+
+# -- General configuration ---------------------------------------------------
+#
+# https://stackoverflow.com/questions/56336234/build-fail-sphinx-error-contents-rst-not-found
+master_doc = "index"
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.coverage",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.intersphinx",
+ "sphinx_copybutton",
+ "sphinx.ext.viewcode",
+ "recommonmark",
+ "nbsphinx",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# Markdown configuration
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+source_suffix = {".rst": "restructuredtext", ".txt": "markdown", ".md": "markdown"}
+
+autosectionlabel_prefix_document = True
+# Allow to embed rst syntax in markdown files.
+enable_eval_rst = True
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = "furo"
+html_favicon = "favicon.ico"
+
+# custom title
+html_title = "Qibo · v" + release
+
+html_theme_options = {
+ "top_of_page_button": "edit",
+ "source_repository": "https://github.com/qiboteam/qibo/",
+ "source_branch": "master",
+ "source_directory": "doc/source/",
+ "light_logo": "qibo_logo_dark.svg",
+ "dark_logo": "qibo_logo_light.svg",
+ "light_css_variables": {
+ "color-brand-primary": "#6400FF",
+ "color-brand-secondary": "#6400FF",
+ "color-brand-content": "#6400FF",
+ },
+ "footer_icons": [
+ {
+ "name": "GitHub",
+ "url": "https://github.com/qiboteam/qibo",
+ "html": """
+
+
+
+ """,
+ "class": "",
+ },
+ ],
+}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+
+# -- Intersphinx -------------------------------------------------------------
+
+intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
+
+
+# -- Doctest ------------------------------------------------------------------
+#
+
+doctest_path = [os.path.abspath("../examples")]
+
+# -- Autodoc ------------------------------------------------------------------
+#
+autodoc_member_order = "bysource"
+
+
+# Adapted this from
+# https://github.com/readthedocs/recommonmark/blob/ddd56e7717e9745f11300059e4268e204138a6b1/docs/conf.py
+# app setup hook
+def setup(app):
+ app.add_config_value("recommonmark_config", {"enable_eval_rst": True}, True)
+ app.add_transform(AutoStructify)
+ app.add_css_file("css/style.css")
+
+
+html_show_sourcelink = False
diff --git a/doc/source/developer-guides/contributing.rst b/doc/source/developer-guides/contributing.rst
new file mode 100644
index 000000000..16cc88a75
--- /dev/null
+++ b/doc/source/developer-guides/contributing.rst
@@ -0,0 +1,82 @@
+How to contribute?
+==================
+
+In the following paragraphs we describe the guidelines for contributing to Qibo.
+
+Code review process
+-------------------
+
+All code submissions require a review and continous integration tests
+beforing accepting the code and merging to the git master branch.
+
+We use the GitHub pull request mechanism which can be summarized as follows:
+
+1. Fork the Qibo repository.
+
+2. Checkout master and create a new branch from it
+
+ .. code-block::
+
+ git checkout master -b new_branch
+
+ where ``new_branch`` is the name of your new branch.
+
+3. Implement your new feature on ``new_branch``.
+
+4. When you are done, push your branch with:
+
+ .. code-block::
+
+ git push origin new_branch
+
+5. At this point you can create a pull request by visiting the Qibo GitHub page.
+
+6. The review process will start and changes in your code may be requested.
+
+Tests
+-----
+
+When commits are pushed to the branches in the GitHub repository,
+we perform integrity checks to ensure that the new features do
+not break Qibo functionalities and meets our coding standards.
+
+The current code standards that are applied to any new changes:
+
+- **Tests**: We use pytest to run our tests that must continue to pass when new changes are integrated in the code. Regression tests, which are run by the continuous integration workflow are stored in ``qibo/tests``. These tests contain several examples about how to use Qibo.
+- **Coverage**: Test coverage should be maintained / be at least at the same level when new features are implemented.
+- **Pylint**: Test code for anomalies, such as bad coding practices, missing documentation, unused variables.
+- **Pre commit**: We use pre-commit to enforce automation and to format the code. The `pre-commit ci `_ will automatically run pre-commit whenever a commit is performed inside a pull request.
+
+Besides the linter, further custom rules are applied e.g. checks for ``print`` statements that bypass the logging system
+(such check can be excluded line by line with the ``# CodeText:skip`` flag).
+
+Documentation
+-------------
+
+The Qibo documentation is automatically generated with `sphinx
+ `_, thus all functions should be documented using
+docstrings. The ``doc`` folder contains the project setup for the documentation
+web page.
+
+The documentation requirements can be installed with:
+
+.. code-block::
+
+ pip install qibo[docs]
+
+Alternatively, install the packages listed in the ``extras_require`` option in
+``setup.py``.
+
+In order to build the documentation web page locally please perform the following steps:
+
+.. code-block::
+
+ cd doc
+ make html
+
+This last command generates a web page in ``doc/build/html/``. You can browse
+the local compiled documentation by opening ``doc/build/html/index.html``.
+
+The sections in the documentation are controlled by the ``*.rst`` files located
+in ``doc/source/``. The application tutorials are rendered from markdown by
+linking the respective files from ``examples/`` in ``doc/source/tutorials/``.
diff --git a/doc/source/developer-guides/general.rst b/doc/source/developer-guides/general.rst
new file mode 100644
index 000000000..0b9640c9b
--- /dev/null
+++ b/doc/source/developer-guides/general.rst
@@ -0,0 +1,36 @@
+Code overview
+=============
+
+The Qibo framework in this repository implements a common system to deal with
+classical hardware and future quantum hardware.
+
+Features
+--------
+
+The main Qibo objects are circuits, defined in ``qibo/models/circuit.py`` and
+gates, defined in ``qibo/gates``. These allow the user to simulate circuits
+that follow the gate-based approach of quantum computation or to execute
+them on different hardware. These objects are backend agnostic, meaning that
+the same circuit can be executed using different backends.
+Backends are defined in ``qibo/backends`` and are used to simulate the abstract
+circuits or execute them on hardware.
+
+Qibo provides additional features that are useful for quantum applications, such
+as Hamiltonians (``qibo/hamiltonians``), time evolution simulation (``qibo/models/evolution.py``)
+and variational models (``qibo/models/variational.py``).
+
+Including a new backend
+-----------------------
+
+New backends can be implemented by inheriting
+:class:`qibo.backends.abstract.Backend` and implementing its abstract
+methods. If the backend is for classical simulation one may prefer to
+inherit :class:`qibo.backends.abstract.Simulator` instead.
+
+
+Examples and tutorials
+----------------------
+
+The ``examples`` folder contains benchmark code for applications/tutorials
+described in :ref:`Applications ` while ``examples/benchmarks``
+contains some code for benchmarking only.
diff --git a/doc/source/developer-guides/index.rst b/doc/source/developer-guides/index.rst
new file mode 100644
index 000000000..1c89debbe
--- /dev/null
+++ b/doc/source/developer-guides/index.rst
@@ -0,0 +1,11 @@
+Developer guides
+================
+
+In this section we provide a code overview and contribution guidelines for
+developers.
+
+.. toctree::
+ :maxdepth: 2
+
+ general
+ contributing
diff --git a/doc/source/favicon.ico b/doc/source/favicon.ico
new file mode 100644
index 000000000..62c493a64
Binary files /dev/null and b/doc/source/favicon.ico differ
diff --git a/doc/source/getting-started/backends.png b/doc/source/getting-started/backends.png
new file mode 100644
index 000000000..3ddd96d01
Binary files /dev/null and b/doc/source/getting-started/backends.png differ
diff --git a/doc/source/getting-started/backends.rst b/doc/source/getting-started/backends.rst
new file mode 100644
index 000000000..59e21aaec
--- /dev/null
+++ b/doc/source/getting-started/backends.rst
@@ -0,0 +1,87 @@
+
+.. _backend-drivers:
+
+Backend drivers
+===============
+
+Qibo provides backends for quantum simulation on classical
+hardware and quantum hardware management and control. In the image below we
+present a schematic view of the currently supported backends.
+
+.. image:: backends.png
+
+Quantum simulation is proposed through dedicated backends for single node
+multi-GPU and multi-threading CPU setups. Quantum hardware control is supported
+for chips based on superconducting qubits.
+
+.. _packages:
+
+Following the overview description above, in this section we present the python
+packages for the modules and backends presented.
+
+_______________________
+
+Base package
+^^^^^^^^^^^^
+
+* :ref:`installing-qibo` is the base package for coding and using the API. This package contains all primitives and algorithms for start coding quantum circuits, adiabatic evolution and more (see :ref:`Components`). This package comes with a lightweight quantum simulator based on :ref:`installing-numpy` which works on multiple CPU architectures such as x86 and arm64.
+
+.. _simulation-backends:
+
+Simulation backends
+^^^^^^^^^^^^^^^^^^^
+
+We provide multiple simulation backends for Qibo, which are automatically loaded
+if the corresponding packages are installed, following the hierarchy below:
+
+* :ref:`installing-numpy`: a lightweight quantum simulator shipped with the :ref:`installing-qibo` base package. Use this simulator if your CPU architecture is not supported by the other backends. Please note that the simulation performance is quite poor in comparison to other backends.
+* :ref:`installing-qibojit`: an efficient simulation backend for CPU, GPU and multi-GPU based on just-in-time (JIT) compiled custom operators. Install this package if you need to simulate quantum circuits with large number of qubits or complex quantum algorithms which may benefit from computing parallelism.
+* :ref:`installing-tensorflow`: a pure TensorFlow implementation for quantum simulation which provides access to gradient descent optimization and the possibility to implement classical and quantum architectures together. This backend is not optimized for memory and speed, use :ref:`installing-qibojit` instead.
+* :ref:`installing-pytorch`: a pure PyTorch implementation for quantum simulation which provides access to gradient descent optimization and the possibility to implement classical and quantum architectures together. This backend is not optimized for memory and speed, use :ref:`installing-qibojit` instead.
+* :ref:`clifford `: a specialized backend for the simulation of quantum circuits with Clifford gates. This backend uses :ref:`installing-qibojit` and/or :ref:`installing-numpy`.
+* `qibotn `_: an interface to Tensor Networks simulation algorithms designed for GPUs and multi-node CPUs. This backend makes possible scaling quantum circuit simulation to a larger number of qubits.
+* `qulacs `_: an interface to the `qulacs` library for quantum simulation. GPU support is not available yet.
+
+The default backend that is used is the first available from the above list.
+The user can switch to a different using the ``qibo.set_backend`` method
+(see :ref:`Backends ` section for more details).
+
+The active default backend will be printed as an info message the first time
+Qibo is imported in the code. If qibojit is not installed, an additional warning
+will appear prompting the user to install qibojit for increased
+performance and multi-threading and/or GPU capabilities. The logging level can
+be controlled using the ``QIBO_LOG_LEVEL`` environment variable. This can be set
+to 3 to hide info messages or 4 to hide both info and warning messages. The
+default value is 1 allowing all messages to appear.
+
+.. note::
+ The `qibojit` backend implements in-place updates. This
+ implies that the initial state is modified at runtime without performing any copies to save memory.
+ For algorithms that require access to the initial state after its modification, such as quantum machine learning applications,
+ it is suggested to perform a copy of the state explicitly if really needed by the algorithm.
+
+
+.. _hardware-backends:
+
+Hardware backends & tools
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We provide the following hardware control backends for Qibo:
+
+* `qibolab `_: a dedicated Qibo backend
+ for the automatic deployment of quantum circuits on self-hosted quantum
+ hardware. It includes: an API for arbitrary pulse sequence generation, drivers
+ for multiple commercial and open-hardware control devices, and circuit to
+ pulse transpilation software.
+
+* `qibocal `_: is a python library that
+ implements Quantum Characterization Validation and Verification using Qibo and
+ Qibolab.
+
+* `qibosoq `_: is the server that
+ integrates `Qick `_ in the
+ Qibolab ecosystem for executing arbitrary circuits and pulse sequences.
+
+* `qibo-cloud-backends `_:
+ provides access to cloud quantum hardware. This module is compatible with labs
+ using Qibo for control and calibration and external vendors.
diff --git a/doc/source/getting-started/index.rst b/doc/source/getting-started/index.rst
new file mode 100644
index 000000000..f1b6a1e8b
--- /dev/null
+++ b/doc/source/getting-started/index.rst
@@ -0,0 +1,17 @@
+Getting started
+===============
+
+In this section we present the basic aspects of Qibo design and provide
+installation instructions. Please visit the following section in order to learn
+the project idea and how to prepare your system for quantum simulation and
+hardware control.
+
+.. toctree::
+ :maxdepth: 1
+
+ overview
+ quickstart
+ backends
+ installation
+
+Already installed all packages? Please visit :ref:`examples ` and learn how to use Qibo in practice.
diff --git a/doc/source/getting-started/installation.rst b/doc/source/getting-started/installation.rst
new file mode 100644
index 000000000..6d596cba2
--- /dev/null
+++ b/doc/source/getting-started/installation.rst
@@ -0,0 +1,263 @@
+Installation instructions
+=========================
+
+Operating systems support
+-------------------------
+
+In the table below we summarize the status of *pre-compiled binaries
+distributed with pypi* for the packages listed above.
+
++------------------+------+---------+------------+
+| Operating System | qibo | qibojit | tensorflow |
++==================+======+=========+============+
+| Linux x86 | Yes | Yes | Yes |
++------------------+------+---------+------------+
+| MacOS >= 10.15 | Yes | Yes | Yes |
++------------------+------+---------+------------+
+| Windows | Yes | Yes | Yes |
++------------------+------+---------+------------+
+
+.. note::
+ All packages are supported for Python >= 3.9.
+
+
+Backend installation
+--------------------
+
+.. _installing-qibo:
+
+qibo
+^^^^
+
+The ``qibo`` is the base required package which includes the language API and a
+lightweight cross-platform simulator based on ``numpy``. In order to accelerate
+simulation please consider specialized backends listed in
+:ref:`simulation-backends`.
+
+Installing with pip
+"""""""""""""""""""
+
+The installation using ``pip`` is the recommended approach to install Qibo.
+Make sure you have Python 3.9 or greater, then use ``pip`` to install ``qibo`` with:
+
+.. code-block:: bash
+
+ pip install qibo
+
+The ``pip`` program will download and install all the required
+dependencies for Qibo.
+
+Installing with conda
+"""""""""""""""""""""
+
+We provide conda packages for ``qibo`` through the `conda-forge
+`_ channel.
+
+To install the package with conda run:
+
+.. code-block:: bash
+
+ conda install -c conda-forge qibo
+
+
+Installing from source
+""""""""""""""""""""""
+
+The installation procedure presented in this section is useful when you have to
+develop the code from source.
+
+In order to install Qibo from source, you can simply clone the GitHub repository
+with
+
+.. code-block::
+
+ git clone https://github.com/qiboteam/qibo.git
+ cd qibo
+ pip install .
+
+_______________________
+
+.. _installing-qibojit:
+
+qibojit
+^^^^^^^
+
+The ``qibojit`` package contains a simulator implementation based on
+just-in-time (JIT) custom kernels using `numba `_
+and `cupy `_. We also provide another implementation based
+on `cuQuantum `_ primitives available
+when running Qibo on GPU.
+
+This backend is used by default, however, if needed, in order to switch to the
+``qibojit`` backend please do:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("qibojit")
+
+Custom cupy kernels will be used by default if a GPU is available and
+custom numba kernels if a GPU is not available.
+If a GPU is available it is possible to switch to the cuQuantum implementation
+using the ``platform`` argument, for example:
+
+.. code-block:: python
+
+ import qibo
+ # switch to the cuquantum implementation
+ qibo.set_backend("qibojit", platform="cuquantum")
+ # switch to custom numba kernels (even if a GPU is available)
+ qibo.set_backend("qibojit", platform="numba")
+
+
+Installing with pip
+"""""""""""""""""""
+
+The installation using ``pip`` is the recommended approach to install
+``qibojit``.
+
+In order to install the package use the following command:
+
+.. code-block:: bash
+
+ pip install qibojit
+
+.. note::
+ The ``pip`` program will download and install all the required
+ dependencies except `cupy `_ and/or
+ `cuQuantum `_
+ which are required for GPU acceleration.
+ The cuQuantum dependency is optional, as it is required only for
+ the ``cuquantum`` platform. Please install `cupy `_ by following the
+ instructions from the `official website
+ `_ for your GPU hardware.
+ The installation instructions for `cuQuantum `_
+ are available in the `official documentation `__.
+ ``qibojit`` is compatible with
+ `cuQuantum SDK v22.03 `__
+ and
+ `cuQuantum SDK v22.05 `__.
+
+
+Installing with conda
+"""""""""""""""""""""
+
+We provide conda packages for ``qibo`` and ``qibojit`` through the `conda-forge
+`_ channel.
+
+To install both packages with conda run:
+
+.. code-block:: bash
+
+ conda install -c conda-forge qibojit
+
+.. note::
+ The ``conda`` program will download and install all the required
+ dependencies except `cupy `_ and/or
+ `cuQuantum `_
+ which are required for GPU acceleration.
+ The cuQuantum dependency is optional, as it is required only for
+ the ``cuquantum`` platform. Please install `cupy `_ by following the
+ instructions from the `official website
+ `_ for your GPU hardware.
+ The installation instructions for `cuQuantum `_
+ are available in the `official documentation `__.
+ ``qibojit`` is compatible with
+ `cuQuantum SDK v22.03 `__
+ and
+ `cuQuantum SDK v22.05 `__.
+
+
+Installing from source
+""""""""""""""""""""""
+
+The installation procedure presented in this section is useful if you have to
+develop the code from source.
+
+In order to install the package perform the following steps:
+
+.. code-block::
+
+ git clone https://github.com/qiboteam/qibojit.git
+ cd qibojit
+
+Then proceed with the ``qibojit`` installation using ``pip``
+
+.. code-block::
+
+ pip install .
+
+_______________________
+
+.. _installing-tensorflow:
+
+tensorflow
+^^^^^^^^^^
+
+If the `TensorFlow `_ package is installed Qibo
+will detect and provide to the user the possibility to use ``tensorflow``
+backend.
+
+This backend is used by default if ``qibojit`` is not installed, however, if
+needed, in order to switch to the ``tensorflow`` backend please do:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("tensorflow")
+
+In order to install the package, we recommend the installation using:
+
+.. code-block:: bash
+
+ pip install qibo tensorflow
+
+.. note::
+ TensorFlow can be installed following its `documentation
+ `_.
+
+_______________________
+
+.. _installing-numpy:
+
+numpy
+^^^^^
+
+The ``qibo`` base package is distributed with a lightweight quantum simulator
+shipped with the qibo base package. No extra packages are required.
+
+This backend is used by default if ``qibojit`` or ``tensorflow`` are not
+installed, however, if needed, in order to switch to the ``numpy`` backend
+please do:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("numpy")
+
+_______________________
+
+
+.. _installing-pytorch:
+
+pytorch
+^^^^^^^
+
+If the `PyTorch `_ package is installed Qibo
+will detect and provide to the user the possibility to use ``pytorch``
+backend.
+
+In order to switch to the ``pytorch`` backend please do:
+
+.. code-block:: python
+
+ import qibo
+ qibo.set_backend("pytorch")
+
+In order to install the package, we recommend the installation using:
+
+.. code-block:: bash
+
+ pip install qibo torch
+
+_______________________
diff --git a/doc/source/getting-started/overview.rst b/doc/source/getting-started/overview.rst
new file mode 100644
index 000000000..f79380301
--- /dev/null
+++ b/doc/source/getting-started/overview.rst
@@ -0,0 +1,31 @@
+Why Qibo?
+=========
+
+The Qibo project targets the development of an open-source full stack API for
+quantum simulation and quantum hardware control.
+
+Quantum technologies, such as NISQ devices, are developed by research
+institutions and require a high level of knowledge of the physics and electronic
+devices used to prepare, execute and retrieve measurements from the experimental
+apparatus.
+
+In this context, Qibo proposes an agnostic approach to quantum simulation and
+hardware control, providing the required components and standards to quickly
+connect the classical hardware and experimental setup into a software stack
+which automates all aspects of a quantum computation.
+
+In the picture below, we summarize the major components of the Qibo "ecosystem".
+
+.. image:: overview.svg
+
+The first component is the language API, based on Python 3, which defines the
+interface for the development of quantum applications, models and new
+algorithms. We also provide a large code-base of models and algorithms,
+presented with code examples and step-by-step tutorials. Finally, we provide
+several tools for the laboratory management and quantum hardware control.
+
+Qibo provides a plug and play mechanism of :ref:`backend drivers ` which
+specializes the code for quantum simulation on different classical hardware
+configurations, such as multi-threading CPU, single GPU and multi-GPU, and
+similarly for quantum hardware control, from superconducting to ion trap
+technologies including FPGA and AWG devices.
diff --git a/doc/source/getting-started/overview.svg b/doc/source/getting-started/overview.svg
new file mode 100644
index 000000000..3142961cd
--- /dev/null
+++ b/doc/source/getting-started/overview.svg
@@ -0,0 +1,2573 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/doc/source/getting-started/quickstart.rst b/doc/source/getting-started/quickstart.rst
new file mode 100644
index 000000000..d5e65db23
--- /dev/null
+++ b/doc/source/getting-started/quickstart.rst
@@ -0,0 +1,50 @@
+Quick start
+-----------
+
+To quickly install Qibo and a lightweight simulator for CPU, open a
+terminal with ``python >= 3.8`` and type:
+
+.. code-block:: bash
+
+ pip install qibo
+
+This will install the basic primitives to start coding quantum applications.
+
+Instead, if you use `conda `_ type:
+
+.. code-block:: bash
+
+ conda install -c conda-forge qibo
+
+.. note::
+ The ``qibo`` package alone includes a lightweight ``numpy`` simulator for
+ single-thread CPU. Please visit the `backends `_
+ documentation for more details about simulation backends.
+
+Here an example of Quantum Fourier Transform (QFT) to test your installation:
+
+.. testcode::
+
+ from qibo.models import QFT
+
+ # Create a QFT circuit with 15 qubits
+ circuit = QFT(15)
+
+ # Simulate final state wavefunction default initial state is |00>
+ final_state = circuit()
+
+Here an example of adding gates and measurements:
+
+.. testcode::
+
+ import numpy as np
+ from qibo import Circuit, gates
+
+ c = Circuit(2)
+ c.add(gates.X(0))
+
+ # Add a measurement register on both qubits
+ c.add(gates.M(0, 1))
+
+ # Execute the circuit with the default initial state |00>.
+ result = c(nshots=100)
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 000000000..f9183a36f
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,117 @@
+.. title::
+ Qibo
+
+
+What is Qibo?
+=============
+
+.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3997195.svg
+ :target: https://doi.org/10.5281/zenodo.3997195
+
+Qibo is an open-source full stack API for quantum simulation and quantum hardware control.
+Qibo aims to contribute as a community driven quantum middleware software with
+
+1. *Simplicity:* agnostic design to quantum primitives.
+2. *Flexibility:* transparent mechanism to execute code on classical and quantum hardware.
+3. *Community:* a common place where find solutions to accelerate quantum development.
+4. *Documentation:* describe all steps required to support new quantum devices or simulators.
+5. *Applications:* maintain a large ecosystem of applications, quantum models and algorithms.
+
+Components
+----------
+
+The main components of ``Qibo`` are presented in :doc:`getting-started/index`
+
+.. image:: qibo_ecosystem.png
+
+Key features
+------------
+
+* Definition of a standard language for the construction and execution of quantum circuits with device agnostic approach to simulation and quantum hardware control based on plug and play backend drivers.
+* A continuously growing code-base of quantum algorithms applications presented with examples and tutorials.
+* Efficient simulation backends with GPU, multi-GPU and CPU with multi-threading support.
+* Simple mechanism for the implementation of new simulation and hardware backend drivers.
+
+How to Use the Documentation
+============================
+
+Welcome to the comprehensive documentation for ``Qibo``! This guide will help
+you navigate through the various sections and make the most of the resources
+available.
+
+1. **Installation and Setup**: Begin by referring to the
+ :doc:`/getting-started/index` and :doc:`/getting-started/backends` guide to
+ set up the ``Qibo`` library in your environment.
+
+2. **Tutorials**: Explore the :doc:`/code-examples/index` section for a range of
+ tutorials that cater to different levels of expertise. These tutorials cover
+ basic examples, advanced examples and tutorials with algorithm for specific
+ applications.
+
+3. **API Documentation**: Dive into the :doc:`/api-reference/index` section,
+ which offers a detailed overview of the main components that constitute the
+ ``Qibo`` API. This section provides a comprehensive understanding of the key
+ elements, helping you build a holistic view of the API's capabilities.
+
+4. **Backends**: The documentation present in this document is limited to Qibo's
+ high-level API, please visit the external links listed in the left navigation
+ bar to learn more about external backends such as ``Qibolab``.
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Introduction
+
+ getting-started/index
+ code-examples/index
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Main documentation
+
+ api-reference/index
+ developer-guides/index
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Appendix
+
+ appendix/citing-qibo
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Documentation links
+
+ Qibo docs
+ Qibolab docs
+ Qibocal docs
+ Qibosoq docs
+ Qibochem docs
+ Qibotn docs
+ Qibo-cloud-backends docs
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
+
+
+Supporters and collaborators
+============================
+
+* Quantum Research Center, Technology Innovation Institute (TII), United Arab Emirates
+* Università degli Studi di Milano (UNIMI), Italy.
+* Istituto Nazionale di Fisica Nucleare (INFN), Italy.
+* Università degli Studi di Milano-Bicocca (UNIMIB), Italy.
+* European Organization for Nuclear research (CERN), Switzerland.
+* Universitat de Barcelona (UB), Spain.
+* Barcelona Supercomputing Center (BSC), Spain.
+* Qilimanjaro Quantum Tech, Spain.
+* Centre for Quantum Technologies (CQT), Singapore.
+* Institute of High Performance Computing (IHPC), Singapore.
+* National Supercomputing Centre (NSCC), Singapore.
+* RIKEN Center for Computational Science (R-CCS), Japan.
+* NVIDIA (cuQuantum), USA.
diff --git a/doc/source/qibo_ecosystem.png b/doc/source/qibo_ecosystem.png
new file mode 100644
index 000000000..96f21ef96
Binary files /dev/null and b/doc/source/qibo_ecosystem.png differ
diff --git a/examples/3_tangle/README.md b/examples/3_tangle/README.md
new file mode 100644
index 000000000..0f438d345
--- /dev/null
+++ b/examples/3_tangle/README.md
@@ -0,0 +1,54 @@
+# Measuring the tangle of three-qubit states
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/3_tangle](https://github.com/qiboteam/qibo/tree/master/examples/3_tangle).
+
+Based in the paper [Entropy 2020, 22(4), 436](http://dx.doi.org/10.3390/e22040436). In this `README.md` file you can see a short
+set of instructions for the usage of the example and brief explanations.
+
+#### What this example does?
+
+This example provides a variational strategy to compute the tangle of an unknown three-qubit state, given many copies of it.
+It is based on the result that local unitary operations cannot affect the entanglement of any quantum state. In particular,
+any three-qubit state can be transformed with local operations acting on every qubit into
+a canonical form. From this canonical form, it is immediate to perform measurements that return an estimate of the tangle.
+See [Phys. Rev. Lett. 85, 1560](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.85.1560) for more details.
+
+The strategy is simple. An unknown three-qubit state is received, and then one unitary gate is applied in every qubit. The
+exact gates are obtained following a minimization procedure. The set of gates chosen is such that minimizes the number
+of outcomes of the states |001>, |010> and |011>. It is guaranteed that this quantity can be exactly zero for any state
+for noiseless devices. The circuit is just
+
+![circuit](images/circuit.png)
+
+with
+
+![canonical](images/canonical.png)
+
+The tangle is computed through
+
+![tangle](images/tangle.png)
+
+#### Usage
+In this example there are only two files
+- `canonizator.py` contains all functions that are needed to run the experiment.
+- `main.py` is the file calling all other functions. The action of every line of code is commented in the source code.
+
+The parameters to be fixed for a run of the experiment are
+- `N`: Number of random states considered
+- `p`: probability of occurring an error
+- `shots`: shots for measuring the optimized tangle
+- `post_selection`: whether we discarded bad outcomes or not, useful for noisy devices
+
+
+#### Results
+
+This example looks for the difference between the exact and the measured tangles. Plots pop up at the end of the `main.py`
+file. For completeness, we present here some results as obtained in the main reference. The black line in the figure below
+represents the optimal performance of the algorithm, that is, the measured tangle is equal to the exact one. At the top,
+no errors are considered, at the bottom there is noise in the device. Red plots consider post-selection, while green ones
+do not. As expected, less noise and post-selection lead to more accurate estimates of the tangle.
+
+![results](images/results.png)
+
+_Note_: The number of random state for this example is 100. This may lead to a long run in terms of execution time.
+Consider decreasing the number of points.
diff --git a/examples/3_tangle/canonizator.py b/examples/3_tangle/canonizator.py
new file mode 100644
index 000000000..264ef8ccb
--- /dev/null
+++ b/examples/3_tangle/canonizator.py
@@ -0,0 +1,152 @@
+import numpy as np
+from scipy.optimize import minimize
+
+from qibo import Circuit, gates
+
+
+def ansatz(p=0):
+ """Ansatz for driving a random state into its up-tp-phases canonical form.
+
+ Args:
+ p (float): probability of occuring a single-qubit depolarizing error
+
+ Returns:
+ Qibo circuit implementing the variational ansatz.
+ """
+ C = Circuit(3, density_matrix=p > 0)
+ for i in range(3):
+ C.add(gates.RZ(i, theta=0))
+ C.add(gates.RY(i, theta=0))
+ C.add(gates.RZ(i, theta=0))
+ if p > 0:
+ C.add(
+ gates.PauliNoiseChannel(i, [("X", p / 3), ("Y", p / 3), ("Z", p / 3)])
+ )
+ for i in range(3):
+ if p > 0:
+ C.add(gates.PauliNoiseChannel(i, [("X", 10 * p)]))
+ C.add(gates.M(i))
+ return C
+
+
+def cost_function(theta, state, circuit, shots=1000):
+ """Cost function encoding the difference between a state and its up-to-phases canonical form
+ Args:
+ theta (array): parameters of the unitary rotations.
+ state (cplx array): three-qubit random state.
+ circuit (models.Circuit): Qibo variational circuit.
+ shots (int): Shots used for measuring every circuit.
+ Returns:
+ float, cost function
+ """
+ circuit.set_parameters(theta)
+ measurements = circuit(state, nshots=shots).frequencies(binary=False)
+ return (measurements[1] + measurements[2] + measurements[3]) / shots
+
+
+def canonize(state, circuit, shots=1000):
+ """Function to transform a given state into its up-to-phases canonical form
+ Args:
+ state (cplx array): three-qubit random state.
+ circuit (models.Circuit): Qibo variational circuit.
+ shots (int): Shots used for measuring every circuit.
+ Returns:
+ Value cost function, parameters to canonize the given state
+ """
+ theta = np.zeros(9)
+ result = minimize(
+ cost_function, theta, args=(state, circuit, shots), method="powell"
+ )
+ return result.fun, result.x
+
+
+def canonical_tangle(state, theta, circuit, shots=1000, post_selection=True):
+ """Tangle of a canonized quantum state
+ Args:
+ state (cplx array): three-qubit random state.
+ theta (array): parameters of the unitary rotations.
+ circuit (models.Circuit): Qibo variational circuit.
+ shots (int): Shots used for measuring every circuit.
+ post_selection (bool): whether post selection is applied or not
+ Returns:
+ tangle
+ """
+ circuit.set_parameters(theta)
+ result = circuit(state, nshots=shots).frequencies(binary=False)
+ measures = np.zeros(8)
+ for i, r in result.items():
+ measures[i] = result[i] / shots
+ if post_selection:
+ measures[1] = 0
+ measures[2] = 0
+ measures[3] = 0
+ measures = measures / np.sum(measures)
+ return 4 * opt_hyperdeterminant(measures)
+
+
+def hyperdeterminant(state):
+ """Hyperdeterminant of any quantum state
+ Args:
+ state (cplx array): three-qubit random state
+ Returns:
+ Hyperdeterminant
+ """
+ indices = [
+ (1, [(0, 0, 7, 7), (1, 1, 6, 6), (2, 2, 5, 5), (3, 3, 4, 4)]),
+ (
+ -2,
+ [
+ (0, 7, 3, 4),
+ (0, 7, 5, 2),
+ (0, 7, 6, 1),
+ (3, 4, 5, 2),
+ (3, 4, 6, 1),
+ (5, 2, 6, 1),
+ ],
+ ),
+ (4, [(0, 6, 5, 3), (7, 1, 2, 4)]),
+ ]
+ hyp = sum(
+ coeff * sum(state[i] * state[j] * state[k] * state[l] for i, j, k, l in ids)
+ for coeff, ids in indices
+ )
+
+ return hyp
+
+
+def opt_hyperdeterminant(measures):
+ """Hyperdeterminant of a canonized quantum state from its outcomes
+ Args:
+ measures (array): outcomes of the canonized state
+ Returns:
+ Hyperdeterminant
+ """
+ hyp = measures[0] * measures[7]
+ return hyp
+
+
+def create_random_state(seed, density_matrix=False):
+ """Function to create a random quantum state from sees
+ Args:
+ seed (int): random seed
+ Returns:
+ Random quantum state
+ """
+ np.random.seed(seed)
+ state = (np.random.rand(8) - 0.5) + 1j * (np.random.rand(8) - 0.5)
+ state = state / np.linalg.norm(state)
+ if density_matrix:
+ return np.tensordot(np.conj(state), state, axes=0)
+ else:
+ return state
+
+
+def compute_random_tangle(seed):
+ """Function to compute the tangle of a randomly created random quantum state from seed
+ Args:
+ seed (int): random seed
+ Returns:
+ Tangle
+ """
+ state = create_random_state(seed)
+ return 4 * np.abs(hyperdeterminant(state))
diff --git a/examples/3_tangle/images/canonical.png b/examples/3_tangle/images/canonical.png
new file mode 100644
index 000000000..fb76abc05
Binary files /dev/null and b/examples/3_tangle/images/canonical.png differ
diff --git a/examples/3_tangle/images/circuit.png b/examples/3_tangle/images/circuit.png
new file mode 100644
index 000000000..4680f69e3
Binary files /dev/null and b/examples/3_tangle/images/circuit.png differ
diff --git a/examples/3_tangle/images/results.png b/examples/3_tangle/images/results.png
new file mode 100644
index 000000000..326044cfd
Binary files /dev/null and b/examples/3_tangle/images/results.png differ
diff --git a/examples/3_tangle/images/tangle.png b/examples/3_tangle/images/tangle.png
new file mode 100644
index 000000000..c152252d7
Binary files /dev/null and b/examples/3_tangle/images/tangle.png differ
diff --git a/examples/3_tangle/main.py b/examples/3_tangle/main.py
new file mode 100644
index 000000000..0c7918734
--- /dev/null
+++ b/examples/3_tangle/main.py
@@ -0,0 +1,65 @@
+import argparse
+
+import matplotlib.pyplot as plt
+import numpy as np
+from canonizator import *
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--N", default=100, help="Number of random states.", type=int)
+parser.add_argument(
+ "--p", default=0.001, help="Probability of occurring an error.", type=float
+)
+parser.add_argument(
+ "--shots", default=1000, help="Shots used for measuring every circuit.", type=float
+)
+parser.add_argument(
+ "--post_selection", default=True, help="Post selection technique", type=bool
+)
+parser.add_argument("--no_plot", action="store_true", help="Avoid plots")
+
+
+def main(N, p, shots, post_selection, no_plot):
+ # Initialize exact and measured tangles
+ tangles = np.empty(N)
+ opt_tangles = np.empty(N)
+ circuit = ansatz(p)
+ for i in range(N):
+ """
+ For every seed from 0 to N, the steps to follow are
+ 1) A random state with three qubits is created from the seed
+ 2) The tangle of the recently created random state is computed exactly
+ 3) Transformation to the up-to-phases canonical form is performed by applying local operations
+ that drive some coefficients to zero
+ 4) The tangle of the up-to-phases canonical form of the created state is measured from the outcomes of the state.
+ Post-selection can be applied at this stage
+
+ Results are stored into variables to paint the results
+ """
+ if i % 10 == 0:
+ print("Initialized state with seed %s" % i + "/ %s" % N)
+ state = create_random_state(i, p > 0)
+ tangles[i] = compute_random_tangle(i)
+ fun, params = canonize(state, circuit, shots=np.int32(shots))
+ opt_tangles[i] = canonical_tangle(
+ state, params, circuit, post_selection=post_selection
+ )
+
+ if not no_plot:
+ print("Painting results")
+ fig, ax = plt.subplots() # Plotting
+ if post_selection:
+ color = "red"
+ else:
+ color = "green"
+ ax.scatter(tangles, opt_tangles, s=20, c=color)
+ ax.plot([0.0, 1.0], [0.0, 1.0], color="black")
+ ax.set(
+ xlabel="Exact tangle", ylabel="Measured tangle", xlim=[0, 1], ylim=[0, 1]
+ )
+ plt.grid("on")
+ plt.show()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/EF_QAE/README.md b/examples/EF_QAE/README.md
new file mode 100644
index 000000000..7799b8623
--- /dev/null
+++ b/examples/EF_QAE/README.md
@@ -0,0 +1,76 @@
+# Quantum autoencoders with enhanced data encoding
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/EF_QAE](https://github.com/qiboteam/qibo/tree/master/examples/EF_QAE).
+
+## Problem overview
+
+Noisy intermediate-scale quantum computers are limited by coherence times and gate errors. Therefore, any algorithm that reduces quantum computational resources is especially valuable in the near future. Here, we present the enhanced feature quantum autoencoder (EF-QAE). As we will see, its key ingredient is to define a parameterized quantum circuit that depends upon adjustable parameters and a feature vector that characterizes the model we aim to compress.
+
+Remarkably, EF-QAE achieves better compression than the standard [quantum autoencoder (QAE)](https://iopscience.iop.org/article/10.1088/2058-9565/aa8072) using the same amount of limited quantum resources. In contrast, it needs additional classical optimization. In this sense, EF-QAE is a step toward what could be done on NISQ computers, shortening the distance between current quantum devices and practical applications.
+
+## Implementing the solution
+
+The code herein aims to implement the EF-QAE, based on the manuscript ["Quantum autoencoders with enhanced data encoding"](https://arxiv.org/abs/2010.06599). A tutorial based on the standard QAE can be found at [https://github.com/qiboteam/qibo/tree/master/examples/autoencoder](https://github.com/qiboteam/qibo/tree/master/examples/autoencoder).
+
+A graphical depiction of a quantum encoder can be seen in the following figure. In a quantum encoder the information contained in some of the input qubits must be discarded after the initial encoding. Then, fresh qubits (here initialized to the |0> state, but one may consider any other easy-to-construct reference state) are prepared and used to implement the final decoding, which is finally compared to the initial state.
+
+![autoencoder](images/autoencoder.png)
+
+In the following, however, we present the EF-QAE. A schematic diagram of the EF-QAE can be seen in the next figure. The algorithm can be initialized with a set of initial states, a feature vector **x**, and a shallow sequence of quantum gates U. In this scheme, we define a unitary U(**θ**, **x**) acting on the initial state, where **x** is a feature vector that characterizes the set of input states. For instance, as we will see, **x** may be the transverse field λ of the 1D Ising spin chain. Once the trial state is prepared, measurements are performed to evaluate the cost function C(**θ**). This result is then fed into the classical optimizer, where the parameters **θ** are adjusted in a quantum-classical loop until the cost function converges to a value close to 0. When the loop terminates, U(**θ**opt , **x**) prepares compressed states of a particular model.
+
+![diagram](images/diagram.png)
+
+A figure of merit for the wrong answer when training is simply the total amount of non-zero measurement outcomes on the discarded qubits, which shall be minimized. In order to design the cost function to be local, different outcomes may be penalized by their Hamming distance to the |0> state, which is just the number of symbols that are different in the binary representation. Notice that this cost function has a value of zero if and only if the compression is successfully completed.
+
+To implement the quantum autoencoder model on a quantum computer we must define the form of the parametrized unitary, decomposing it into a quantum circuit suitable for optimization. This can be done using a similar structure to that in the following figure.
+
+![ansatz](images/ansatz.png)
+
+We now encode the feature vector **x** into each of the single Ry qubit rotations by using a linear function as
+
+![Ry](images/Ry.png)
+
+where *i*,*j* simply indicates a component of the vector, and **θ** are the parameters adjusted in the optimization loop.
+
+## How to run an example?
+
+To run a particular instance of the problem we have to set up the initial
+arguments:
+- `layers` (int): number of ansatz layers.
+- `autoencoder` (int): 0 to run the EF-QAE or 1 to run the QAE.
+- `example` (int): 0 to run Ising model example or 1 to run the Handwritten digits example.
+
+As an example, in order to use 3 layers in our variational quantum ansatz on the EF-QAE for the Ising model example,
+you should execute the following command:
+
+```python
+python main.py --layers 3 --autoencoder 0 --example 0
+```
+
+## Interpreting results
+
+### Ising model
+
+The EF-QAE can be verified on simulations. In the following, we benchmark both the EF-QAE and the standard QAE in the case of a paradigmatic quantum spin chain with 6 qubits, the transverse field Ising model. The EF-QAE and QAE are optimized over a training set of ground states of the Ising model. Specifically, we have considered N=20 equispaced ground states in between (a) λ=0.5 and (b) λ=1.0, with initial random parameters. We have considered the above variational quantum circuit with 3 layers, and therefore, the resulting compressed state contains 4 qubits. Note that the feature vector **x** for the EF-QAE is simply a scalar that takes the value of the transverse field λ.
+
+In the following figure, we show the cost function value as a function of the number of optimization steps. The EF-QAE* is simply the EF-QAE initialized with the optimal parameters of QAE. This way, the EF-QAE* will always improve the QAE performance. As can be seen, the EF-QAE achieves twice the compression of the QAE using the same quantum resources. Notice, however, that the EF-QAE contains twice as many variational parameters, and therefore, the increase in performance is at the expense of additional classical optimization.
+
+![cost_ising](images/cost_ising.png)
+
+Now, we can play around with the code and assess both EF-QAE and QAE with the optimal parameters against two test ground states of the Ising model, specifically, with λ=0.60 and λ=0.75. This can be done by applying U† (**θ**opt , **x**) to the compressed test state, initializing two qubits to |00>. The results are shown in the following figure. Here, we show a density matrix visualization of the input, trash, and output state. The EF-QAE achieves better compression to the |00> trash state, and therefore, higher fidelity on the output state. As we change the values of the transverse field, we note that the compression differs. Both cases considered, however, the performance of the EF-QAE is preferable.
+
+![space_ising](images/space_ising.png)
+
+### Handwritten digits
+
+Here, we benchmark EF-QAE and QAE models in the case of 8x8 handwritten digit compression with 6 qubits using 4 layers. The data comprising each digit consists of a matrix with values from 0 to 16 corresponding to a gray map. Each value of this matrix is encoded in the amplitude of a 6-qubit state, further restricted to normalization.
+
+The EF-QAE and QAE are optimized over a training set of handwritten digits obtained from the Python package ["Scikit Learn"](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_digits.html). Specifically, we have considered N=20 handwritten digits, 10 of each corresponding to **0** or **1**. The simulation details are equivalent to those in the previous section. Here, the feature vector for the EF-QAE corresponds to **x**=(1,2). That is, we simply input a value of x=1 (x=2) if the handwritten digit corresponds to **0** (**1**).
+
+In the following figure, we show the cost function value as a function of the number of optimization steps. Recall that EF-QAE* is simply the EF-QAE initialized with the optimal parameters of QAE. The behavior is similar to the one observed in the case of the Ising model, where the EF-QAE achieves three times the compression of the QAE using the same quantum resources.
+
+![handwritten_cost](images/handwritten_cost.png)
+
+Once again, to gain insight into the compression process, we assess both EF-QAE and QAE with the optimal parameters against two handwritten test digits corresponding to **0** and **1**. The results are shown in the following figure. Here, we plot the output digit of the EF-QAE and QAE. Once more, since EF-QAE achieves better compression to the |00> trash state, we obtain higher fidelity on the output state. Remarkably, in both cases, the performance of the EF-QAE is improved with respect to the QAE.
+
+![handwritten_output](images/handwritten_output.png)
diff --git a/examples/EF_QAE/images/Ry.png b/examples/EF_QAE/images/Ry.png
new file mode 100644
index 000000000..224036961
Binary files /dev/null and b/examples/EF_QAE/images/Ry.png differ
diff --git a/examples/EF_QAE/images/ansatz.png b/examples/EF_QAE/images/ansatz.png
new file mode 100644
index 000000000..d8e8dca65
Binary files /dev/null and b/examples/EF_QAE/images/ansatz.png differ
diff --git a/examples/EF_QAE/images/autoencoder.png b/examples/EF_QAE/images/autoencoder.png
new file mode 100644
index 000000000..d44987e6f
Binary files /dev/null and b/examples/EF_QAE/images/autoencoder.png differ
diff --git a/examples/EF_QAE/images/cost_ising.png b/examples/EF_QAE/images/cost_ising.png
new file mode 100644
index 000000000..d8fc54371
Binary files /dev/null and b/examples/EF_QAE/images/cost_ising.png differ
diff --git a/examples/EF_QAE/images/diagram.png b/examples/EF_QAE/images/diagram.png
new file mode 100644
index 000000000..5c1163821
Binary files /dev/null and b/examples/EF_QAE/images/diagram.png differ
diff --git a/examples/EF_QAE/images/handwritten_cost.png b/examples/EF_QAE/images/handwritten_cost.png
new file mode 100644
index 000000000..e4b10cfbd
Binary files /dev/null and b/examples/EF_QAE/images/handwritten_cost.png differ
diff --git a/examples/EF_QAE/images/handwritten_output.png b/examples/EF_QAE/images/handwritten_output.png
new file mode 100644
index 000000000..6f6f706b4
Binary files /dev/null and b/examples/EF_QAE/images/handwritten_output.png differ
diff --git a/examples/EF_QAE/images/space_ising.png b/examples/EF_QAE/images/space_ising.png
new file mode 100644
index 000000000..140b870e1
Binary files /dev/null and b/examples/EF_QAE/images/space_ising.png differ
diff --git a/examples/EF_QAE/main.py b/examples/EF_QAE/main.py
new file mode 100644
index 000000000..44774fc4e
--- /dev/null
+++ b/examples/EF_QAE/main.py
@@ -0,0 +1,351 @@
+#!/usr/bin/env python3
+import argparse
+
+import numpy as np
+from scipy.optimize import minimize
+from sklearn.datasets import load_digits
+
+from qibo import gates, hamiltonians, models
+
+
+def main(layers, autoencoder, example, maxiter):
+ def encoder_hamiltonian_simple(nqubits, ncompress):
+ """Creates the encoding Hamiltonian.
+ Args:
+ nqubits (int): total number of qubits.
+ ncompress (int): number of discarded/trash qubits.
+
+ Returns:
+ Encoding Hamiltonian.
+ """
+ m0 = hamiltonians.Z(ncompress).matrix
+ m1 = np.eye(2 ** (nqubits - ncompress), dtype=m0.dtype)
+ ham = hamiltonians.Hamiltonian(nqubits, np.kron(m1, m0))
+ return 0.5 * (ham + ncompress)
+
+ def rotate(theta, x):
+ new_theta = []
+ index = 0
+ for l in range(layers):
+ for q in range(nqubits):
+ new_theta.append(theta[index] * x + theta[index + 1])
+ index += 2
+ for q in range(nqubits):
+ new_theta.append(theta[index] * x + theta[index + 1])
+ index += 2
+ for q in range(nqubits - compress, nqubits, 1):
+ new_theta.append(theta[index] * x + theta[index + 1])
+ index += 2
+ return new_theta
+
+ cost_function_steps = []
+ nqubits = 6
+ compress = 2
+ encoder = encoder_hamiltonian_simple(nqubits, compress)
+ count = [0]
+
+ if example == 0:
+ ising_groundstates = []
+ lambdas = np.linspace(0.5, 1.0, 20)
+ for lamb in lambdas:
+ ising_ham = -1 * hamiltonians.TFIM(nqubits, h=lamb)
+ ising_groundstates.append(ising_ham.eigenvectors()[0])
+
+ if autoencoder == 1:
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 3))
+ circuit.add(gates.CZ(5, 1))
+ circuit.add(gates.CZ(4, 2))
+ circuit.add(gates.CZ(4, 0))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 2))
+ circuit.add(gates.CZ(4, 3))
+ circuit.add(gates.CZ(5, 0))
+ circuit.add(gates.CZ(4, 1))
+ for q in range(nqubits - compress, nqubits, 1):
+ circuit.add(gates.RY(q, theta=0))
+
+ def cost_function_QAE_Ising(params, count):
+ """Evaluates the cost function to be minimized for the QAE and Ising model.
+
+ Args:
+ params (array or list): values of the parameters.
+
+ Returns:
+ Value of the cost function.
+ """
+ cost = 0
+ circuit.set_parameters(
+ params
+ ) # this will change all thetas to the appropriate values
+ for i in range(len(ising_groundstates)):
+ final_state = circuit.execute(
+ np.copy(ising_groundstates[i])
+ ).state()
+ cost += np.real(encoder.expectation(final_state))
+
+ cost_function_steps.append(
+ cost / len(ising_groundstates)
+ ) # save cost function value after each step
+
+ if count[0] % 50 == 0:
+ print(count[0], cost / len(ising_groundstates))
+ count[0] += 1
+
+ return cost / len(ising_groundstates)
+
+ nparams = 2 * nqubits * layers + compress
+ initial_params = np.random.uniform(0, 2 * np.pi, nparams)
+
+ result = minimize(
+ cost_function_QAE_Ising,
+ initial_params,
+ args=(count),
+ method="BFGS",
+ options={"maxiter": maxiter},
+ )
+
+ elif autoencoder == 0:
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 3))
+ circuit.add(gates.CZ(5, 1))
+ circuit.add(gates.CZ(4, 2))
+ circuit.add(gates.CZ(4, 0))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 2))
+ circuit.add(gates.CZ(4, 3))
+ circuit.add(gates.CZ(5, 0))
+ circuit.add(gates.CZ(4, 1))
+ for q in range(nqubits - compress, nqubits, 1):
+ circuit.add(gates.RY(q, theta=0))
+
+ def cost_function_EF_QAE_Ising(params, count):
+ """Evaluates the cost function to be minimized for the EF-QAE and Ising model.
+
+ Args:
+ params (array or list): values of the parameters.
+
+ Returns:
+ Value of the cost function.
+ """
+ cost = 0
+ for i in range(len(ising_groundstates)):
+ newparams = rotate(params, lambdas[i])
+ circuit.set_parameters(newparams)
+ final_state = circuit.execute(
+ np.copy(ising_groundstates[i])
+ ).state()
+ cost += np.real(encoder.expectation(final_state))
+
+ cost_function_steps.append(
+ cost / len(ising_groundstates)
+ ) # save cost function value after each step
+
+ if count[0] % 50 == 0:
+ print(count[0], cost / len(ising_groundstates))
+ count[0] += 1
+
+ return cost / len(ising_groundstates)
+
+ nparams = 4 * nqubits * layers + 2 * compress
+ initial_params = np.random.uniform(0, 2 * np.pi, nparams)
+
+ result = minimize(
+ cost_function_EF_QAE_Ising,
+ initial_params,
+ args=(count),
+ method="BFGS",
+ options={"maxiter": maxiter},
+ )
+
+ else:
+ raise ValueError(
+ "You have to introduce a value of 0 or 1 in the autoencoder argument."
+ )
+
+ elif example == 1:
+ digits = load_digits()
+ vector_0 = []
+ vector_1 = []
+ for value in [0, 10, 20, 30, 36, 48, 49, 55, 72, 78]:
+ vector_0.append(
+ np.array(digits.data[value])
+ / np.linalg.norm(np.array(digits.data[value]))
+ )
+ for value in [1, 11, 21, 42, 47, 56, 70, 85, 90, 93]:
+ vector_1.append(
+ np.array(digits.data[value])
+ / np.linalg.norm(np.array(digits.data[value]))
+ )
+
+ if autoencoder == 1:
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 3))
+ circuit.add(gates.CZ(5, 1))
+ circuit.add(gates.CZ(4, 2))
+ circuit.add(gates.CZ(4, 0))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 2))
+ circuit.add(gates.CZ(4, 3))
+ circuit.add(gates.CZ(5, 0))
+ circuit.add(gates.CZ(4, 1))
+ for q in range(nqubits - compress, nqubits, 1):
+ circuit.add(gates.RY(q, theta=0))
+
+ def cost_function_QAE_Digits(params, count):
+ """Evaluates the cost function to be minimized for the QAE and Handwritten digits.
+
+ Args:
+ params (array or list): values of the parameters.
+
+ Returns:
+ Value of the cost function.
+ """
+ cost = 0
+ circuit.set_parameters(
+ params
+ ) # this will change all thetas to the appropriate values
+ for i in range(len(vector_0)):
+ final_state = circuit.execute(np.copy(vector_0[i])).state()
+ cost += np.real(encoder.expectation(final_state))
+ for i in range(len(vector_1)):
+ final_state = circuit.execute(np.copy(vector_1[i])).state()
+ cost += np.real(encoder.expectation(final_state))
+
+ cost_function_steps.append(
+ cost / (len(vector_0) + len(vector_1))
+ ) # save cost function value after each step
+
+ if count[0] % 50 == 0:
+ print(count[0], cost / (len(vector_0) + len(vector_1)))
+ count[0] += 1
+
+ return cost / (len(vector_0) + len(vector_1))
+
+ nparams = 2 * nqubits * layers + compress
+ initial_params = np.random.uniform(0, 2 * np.pi, nparams)
+
+ result = minimize(
+ cost_function_QAE_Digits,
+ initial_params,
+ args=(count),
+ method="BFGS",
+ options={"maxiter": maxiter},
+ )
+
+ elif autoencoder == 0:
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 3))
+ circuit.add(gates.CZ(5, 1))
+ circuit.add(gates.CZ(4, 2))
+ circuit.add(gates.CZ(4, 0))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.CZ(5, 4))
+ circuit.add(gates.CZ(5, 2))
+ circuit.add(gates.CZ(4, 3))
+ circuit.add(gates.CZ(5, 0))
+ circuit.add(gates.CZ(4, 1))
+ for q in range(nqubits - compress, nqubits, 1):
+ circuit.add(gates.RY(q, theta=0))
+
+ def cost_function_EF_QAE_Digits(params, count):
+ """Evaluates the cost function to be minimized for the EF-QAE and Handwritten digits.
+
+ Args:
+ params (array or list): values of the parameters.
+
+ Returns:
+ Value of the cost function.
+ """
+ cost = 0
+ newparams = rotate(params, 1)
+ circuit.set_parameters(newparams)
+ for i in range(len(vector_0)):
+ final_state = circuit.execute(np.copy(vector_0[i])).state()
+ cost += np.real(encoder.expectation(final_state))
+ newparams = rotate(params, 2)
+ circuit.set_parameters(newparams)
+ for i in range(len(vector_1)):
+ final_state = circuit.execute(np.copy(vector_1[i])).state()
+ cost += np.real(encoder.expectation(final_state))
+
+ cost_function_steps.append(
+ cost / (len(vector_0) + len(vector_1))
+ ) # save cost function value after each step
+
+ if count[0] % 50 == 0:
+ print(count[0], cost / (len(vector_0) + len(vector_1)))
+ count[0] += 1
+
+ return cost / (len(vector_0) + len(vector_1))
+
+ nparams = 4 * nqubits * layers + 2 * compress
+ initial_params = np.random.uniform(0, 2 * np.pi, nparams)
+
+ result = minimize(
+ cost_function_EF_QAE_Digits,
+ initial_params,
+ args=(count),
+ method="BFGS",
+ options={"maxiter": maxiter},
+ )
+
+ else:
+ raise ValueError(
+ "You have to introduce a value of 0 or 1 in the autoencoder argument."
+ )
+
+ else:
+ raise ValueError(
+ "You have to introduce a value of 0 or 1 in the example argument."
+ )
+
+ print("Final parameters: ", result.x)
+ print("Final cost function: ", result.fun)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--layers", default=3, type=int, help="(int): number of ansatz layers"
+ )
+ parser.add_argument(
+ "--autoencoder",
+ default=0,
+ type=int,
+ help="(int): 0 to run the EF-QAE or 1 to run the QAE",
+ )
+ parser.add_argument(
+ "--example",
+ default=0,
+ type=int,
+ help="(int): 0 to run Ising model example or 1 to run the Handwritten digits example",
+ )
+ parser.add_argument(
+ "--maxiter", default=50000, type=int, help="(int): maximum number of iterations"
+ )
+ args = parser.parse_args()
+ main(**vars(args))
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 000000000..7b68e4ed2
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,36 @@
+# Tutorials
+
+In this folder we present some examples of quantum circuits applied to specific
+physics problems.
+
+- [Scaling of variational quantum circuit depth for condensed matter systems](aavqe/README.md)
+- [Grover’s Algorithm for solving Satisfiability Problems](grover3sat/README.md)
+- [Grover's Algorithm for solving a Toy Sponge Hash function](hash-grover/README.md)
+- [Variational Quantum Classifier](variational_classifier/README.md)
+- [Quantum Convolutional Neural Network classifier](qcnn_classifier/README.md)
+- [Data reuploading for a universal quantum classifier](reuploading_classifier/README.md)
+- [Parameter Shift Rule for an hardware-compatible Variational Quantum Regressor](vqregressor/README.md)
+- [Quantum autoenconder for data compression](autoencoder/README.md)
+- [Quantum Singular Value Decomposer](qsvd/README.md)
+- [Measuring the tangle of three-qubit states](3_tangle/README.md)
+- [Quantum unary approach to option pricing](unary/README.md)
+- [Simple Adiabatic Evolution Examples](adiabatic/README.md)
+- [Adiabatic evolution for solving an Exact Cover problem](adiabatic-3SAT/README.md)
+- [Quantum autoencoders with enhanced data encoding](EF_QAE/README.md)
+- [Shor's factorization algorithm](shor/README.md)
+- [Determining the proton content with proton with a quantum computer](qPDF/qPDF.ipynb)
+- [Quantum Fourier Iterative Amplitude Estimation](qfiae/qfiae_demo.ipynb)
+- [Maximal violation of Bell inequalities variationally](bell-variational/README.md)
+- [Feedback-based ALgorithm for Quantum OptimizatioN - FALQON](falqon/README.md)
+- [A general Grover model](grover/README.md)
+- [Quantum anomaly detection](anomaly_detection/README.md)
+- [Quantum k-medians clustering](qclustering/README.md)
+- [Determining probability density functions with adiabatic quantum computing](adiabatic_qml/adiabatic-qml.ipynb)
+- [Quadratic assignment problem (QAP)](qap/README.md)
+- [Minimum Vertex Cover (MVC)](mvc/README.md)
+
+In the `benchmarks` folder we have included examples concerning:
+- A generic benchmark script for multiple circuits (`benchmarks/main.py`)
+- Variational Quantum Eigensolver (`benchmarks/vqe.py`)
+- Adiabatic evolution for the Ising Hamiltonian using linear scaling (`benchmarks/evolution.py`)
+- Quantum approximate optimization algorithm model (`benchmarks/qaoa.py`)
diff --git a/examples/aavqe/README.md b/examples/aavqe/README.md
new file mode 100644
index 000000000..a855056fb
--- /dev/null
+++ b/examples/aavqe/README.md
@@ -0,0 +1,131 @@
+# Scaling of variational quantum circuit depth for condensed matter systems
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/aavqe](https://github.com/qiboteam/qibo/tree/master/examples/aavqe).
+
+## Problem overview
+Strongly-correlated many-body systems can give rise to exceptional quantum
+phenomena.
+
+In particular, the Ising model or the XXZ model have fundamental significance in
+condensed matter physics, and thus the realization of these systems may attract
+tremendous interest.
+
+Indeed, it is then an ambitious goal to prepare the ground state of those
+systems, and gain some insight into the physics of the problem.
+
+## Implementing the solution
+
+The code herein aims to reproduce the results of the manuscript ["Scaling of
+variational quantum circuit depth for condensed matter
+systems"](https://quantum-journal.org/papers/q-2020-05-28-272/).
+
+The idea is to benchmark the accuracy of the [Variational Quantum Eigensolver
+(VQE)](https://www.nature.com/articles/ncomms5213) based on a finite-depth
+variational quantum circuit encoding ground states of local Hamiltonians,
+namely, the Ising and XXZ models.
+
+We focus on a fixed geometry of the network represented in the following
+figure. It can either be interpreted as the Floquet evolution of a local
+Hamiltonian or as a Trotter approximation of continuous evolution by a local
+Hamiltonian defined on a line.
+
+![ansatz1](images/ansatz-1.png)
+
+Notice that any potential advantage of the VQE could be lost without practical
+approaches to perform the parameter optimization due to the high-dimensional
+parameter landscape and the problem of [barren
+plateaus](https://www.nature.com/articles/s41467-018-07090-4).
+
+A particular proposal to try to solve this optimization problem is the
+[Adiabatically Assisted Variational Quantum Eigensolver
+(AAVQE)](https://arxiv.org/abs/1806.02287).
+
+The AAVQE is a strategy to circumvent the convergence issue, inspired by the
+adiabatic theorem. The AAVQE method consists of parametrizing a Hamiltonian as H
+= (1-s)H0 + sHP where H0 is a Hamiltonian for
+which ground state can be easily prepared, HP is the problem
+Hamiltonian, and s = [0,1] is the interpolation parameter.
+
+The interpolation parameter is used to adjust the Hamiltonian from one VQE run
+to the next, and the state preparation parameters at each step are initialized
+by the optimized parameters of the previous step.
+
+## How to run an example?
+
+To run a particular instance of the problem we have to set up the initial
+arguments:
+- `nqubits` (int): number of quantum bits.
+- `layers` (int): number of ansatz layers.
+- `maxsteps` (int): number of maximum iterations on each adiabatic step.
+- `T_max` (int): number of maximum adiabatic steps.
+- `initial_parameters` (array or list): values of the initial parameters.
+- `easy_hamiltonian` (`qibo.hamiltonians.Hamiltonian`): initial Hamiltonian
+ object, defined as `sz_hamiltonian`.
+- `problem_hamiltonian` (`qibo.hamiltonians.Hamiltonian`): problem Hamiltonian
+ object, namely, the Ising or XXZ hamiltonians.
+
+As an example, in order to simulate a circuit with 4 qubits with 2 layers with 4
+maximum adiabatic steps and a maximum of 5000 iterations on each adiabatic step,
+you should execute the following command:
+
+```python
+python main.py --nqubits 4 --layers 2 --maxsteps 5000 --T_max 4
+```
+
+## Interpreting results
+
+A legitimate question is how accurate a variational circuit can be, and how
+close can the state we extract by running a AAVQE on our set of quantum circuits
+get to exact ground state of the system. Since we are dealing with finite
+systems, the Hamiltonians we are considering always have a gap (at least
+proportional to 1/n). We can bound the distance from our trial wave-function to
+the exact ground state with the difference on the energies ε =
+(EAAVQE - E0 ). We will use this error in the ground state
+energy as a measure of the quality of the circuit.
+
+We may begin by considering the results for λ=10 in the Ising model. Notice that
+we can obtain the ground state in this regime in perturbation theory. Using the
+perturbation theory in the form of a continuous unitary transformation, we
+expect that the precision of our ansatz will scale exponentially with the number
+of layers. Our expectations are confirmed in the numerical results:
+
+![lambda10](images/Lambda10_Ising.png)
+
+If you do not get those results, do not despair. Sometimes, fine-tuning of the
+arguments, i.e., `maxsteps`, `T_max` or the optimizer itself, is required,
+especially for a large number of qubits. Now we can repeat the same analysis for
+12 qubits as we decrease λ towards the phase transition, i.e. at λ=1.
+
+![lambda](images/Lambdas_Ising.png)
+
+For λ=2, we still appreciate an exponential scaling of the accuracy, but as
+expected, the slope of the semi-logarithmic plot is lower. For λ=1, we
+appreciate that the behaviour of the AAVQE ground state energy accuracy as a
+function of the number of layers changes drastically from the behaviour observed
+at larger λ. Recall that λ=1 in the thermodynamic limit is the location of the
+phase transition between the two gapped phases.
+
+We can now perform numerical simulations of quantum circuits of several layers
+that are optimized to encode the ground state of systems with different sizes
+from 6 to 16 qubits with periodic boundary conditions. We can use the two
+Hamiltonians (Ising and XXZ Hamiltonians). In both cases, the Hamiltonians are
+tuned to a critical point, choosing λ=1 for the Ising model (left) and Δ=1/2 for
+the XXZ model (right).
+
+![lambda10](images/ising.png)
+
+The accuracy clearly shows two different regimes. Initially, the accuracy varies
+very little as we increase the number of layers, and hence the number of
+variational parameters and the entangling power of the circuit. The error indeed
+stays constant from one to several layers for the Ising and XXZ model. This
+behaviour seems to be completely independent of the system size since curves
+obtained by optimizing the energy almost coincide. We thus seem to observe a
+finite-depth regime, where the precision of the variational scheme depends very
+little on the number of layers and improves very slowly. This regime changes
+drastically at a critical number of layers that strongly depends on the size of
+the system. At that critical number of layers, the precision improves several
+orders of magnitude abruptly. This improvement is particularly abrupt in the
+case of the Ising model, whereby just adding one layer, the accuracy can improve
+several orders of magnitude. For the XXZ model, we see similar features though
+the overall accuracy is lower as a consequence of the higher amount of
+entanglement in the ground state.
diff --git a/examples/aavqe/images/Lambda10_Ising.png b/examples/aavqe/images/Lambda10_Ising.png
new file mode 100644
index 000000000..e3d60aa16
Binary files /dev/null and b/examples/aavqe/images/Lambda10_Ising.png differ
diff --git a/examples/aavqe/images/Lambdas_Ising.png b/examples/aavqe/images/Lambdas_Ising.png
new file mode 100644
index 000000000..9f84ff696
Binary files /dev/null and b/examples/aavqe/images/Lambdas_Ising.png differ
diff --git a/examples/aavqe/images/ansatz-1.png b/examples/aavqe/images/ansatz-1.png
new file mode 100644
index 000000000..02d80ac0e
Binary files /dev/null and b/examples/aavqe/images/ansatz-1.png differ
diff --git a/examples/aavqe/images/ising.png b/examples/aavqe/images/ising.png
new file mode 100644
index 000000000..8bb5aba07
Binary files /dev/null and b/examples/aavqe/images/ising.png differ
diff --git a/examples/aavqe/main.py b/examples/aavqe/main.py
new file mode 100644
index 000000000..083b76dbb
--- /dev/null
+++ b/examples/aavqe/main.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+import argparse
+
+import numpy as np
+
+from qibo import gates, hamiltonians, models
+
+
+def main(nqubits, layers, maxsteps, T_max):
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ circuit.add(gates.CZ(q, q + 1) for q in range(0, nqubits - 1, 2))
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ circuit.add(gates.CZ(q, q + 1) for q in range(1, nqubits - 2, 2))
+ circuit.add(gates.CZ(0, nqubits - 1))
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ problem_hamiltonian = hamiltonians.XXZ(nqubits)
+ easy_hamiltonian = hamiltonians.X(nqubits)
+ s = lambda t: t
+ aavqe = models.variational.AAVQE(
+ circuit, easy_hamiltonian, problem_hamiltonian, s, nsteps=maxsteps, t_max=T_max
+ )
+
+ initial_parameters = np.random.uniform(
+ 0, 2 * np.pi * 0.1, 2 * nqubits * layers + nqubits
+ )
+ best, params = aavqe.minimize(initial_parameters)
+
+ print("Final parameters: ", params)
+ print("Final energy: ", best)
+
+ # We compute the difference from the exact value to check performance
+ eigenvalue = problem_hamiltonian.eigenvalues()
+ print(eigenvalue)
+ print("Difference from exact value: ", best - np.real(eigenvalue[0]))
+ print("Log difference: ", -np.log10(best - np.real(eigenvalue[0])))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=6, type=int)
+ parser.add_argument("--layers", default=5, type=int)
+ parser.add_argument("--maxsteps", default=10, type=int)
+ parser.add_argument("--T_max", default=5, type=int)
+ args = parser.parse_args()
+ main(**vars(args))
diff --git a/examples/adiabatic/README.md b/examples/adiabatic/README.md
new file mode 100644
index 000000000..4f36d7599
--- /dev/null
+++ b/examples/adiabatic/README.md
@@ -0,0 +1,98 @@
+# Simple Adiabatic Evolution Examples
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/adiabatic](https://github.com/qiboteam/qibo/tree/master/examples/adiabatic)
+
+## Introduction
+
+Qibo provides models for simulating the unitary evolution of state vectors as
+well adiabatic evolution. The goal of adiabatic evolution is to find the ground
+state of a "hard" Hamiltonian H1 . To do so, an "easy" Hamiltonian
+H0 is selected and the system is prepared in its ground state. The
+system is then left to evolve under the slowly changing Hamiltonian
+H(t) = (1 - s(t)) H0 + s(t) H1 for a total time T.
+If the scheduling function s(t) and the total time T are chosen appropriately
+the state at the end of this evolution will be the ground state of H1 .
+
+This example contains two scripts. The first performs the adiabatic evolution
+for a given choice of s(t) and T and plots the dynamics of the H1
+energy as well as the overlap between the evolved state and the actual ground
+state of H1 , which can be calculated via exact diagonalization for
+small systems. The second script optimizes the total time T and a parametrized
+family of s(t). Both scripts a sum of X operators H0 and the transverse field Ising model as H1 .
+
+## Adiabatic evolution dynamics
+
+A simple adiabatic evolution example can be run using the `linear.py` script.
+This supports the following options:
+
+- `nqubits` (int): Number of qubits in the system.
+- `hfield` (float): Transverse field Ising model (`qibo.hamiltonians.TFIM`) h-field h value.
+- `T` (int): Total time of the adiabatic evolution.
+- `dt` (float): Time step used for integration.
+- `solver` (str): Solver used for integration.
+
+The following plots correspond to the adiabatic evolution of N=4 qubits for
+total time T=4 using linear scaling s(t)=t/T. At the left we show how the
+expectation value of H1 changes during evolution and at the right we
+plot the overlap between the evolved state and the ground state of H1 .
+We see that at the end of evolution the evolved state reaches the ground state.
+
+![dynamics](images/dynamics_n4T4.0.png)
+
+To gain more insight on the importance of the total evolution time T, we repeat
+the evolution for various choices of T and check how it affects the final
+energy and overlap. This is shown in the following plot
+
+![Tplots](images/linears_T15plots_n4.png)
+
+We see that the best overlap for this example is obtained for T=4.
+
+## Optimizing scheduling
+
+An example of scheduling function optimization can be run using the `optimize.py`
+script. This optimizes a polynomial ansatz for s(t) using the H1
+energy as the loss function. The total evolution time T is also optimized as an
+additional free parameter. The following options are supported:
+
+- `nqubits` (int): Number of qubits in the system.
+- `hfield` (float): Transverse field Ising model (`qibo.hamiltonians.TFIM`) h-field h value.
+- `params` (str): Initial guess for free parameters. The numbers should be
+ seperated using `,`. The last parameter is the initial guess for the total
+ time T. The rest parameters are used to define the polynomial for s(t).
+- `dt` (float): Time step used for integration.
+- `solver` (str): Solver used for integration.
+- `method` (str): Which scipy optimizer to use.
+- `maxiter` (int): Maximum iterations for scipy optimizer.
+- `save` (str): Name to use for saving optimization history.
+ If ``None`` history will not be saved.
+
+The following plots correspond to the optimization of a 3rd order polynomial
+for s(t). The first plot shows the final form of s(t) after optimization.
+The second plot shows how the loss function changes during optimization.
+The second line of plots shows the dynamics of the H1 energy and
+the overlap with the actual ground state when using the optimized schedule s(t)
+and total time T.
+
+![optdynamics](images/poly3_powell_n4.png)
+
+Note that the optimized 3rd order polynomial s(t) is capable of reaching a
+good approximation of the ground state energy after total time T=2 which is
+less than the T=4 required for the linear s(t) analyzed in the previous section.
+
+## Error of Trotter decomposition
+
+The `trotter_error.py` script can be used to analyze the errors of time
+evolution that uses the Trotter decomposition. The overlap between the final
+state obtained using Trotter operators and the exact final state is used to
+quantify this error. The script uses the transverse field Ising model as the
+evolution Hamiltonian and supports the following options:
+
+- `nqubits` (int): Number of qubits in the system.
+- `hfield` (float): Transverse field Ising model h-field h value.
+- `T` (float): Total time of the adiabatic evolution.
+- `save` (bool): Whether to save the plots.
+
+In the following plot we see that for N=6 qubits, the error as quantified by
+1 - overlap scales as dt^4.
+
+![trottererror](images/trotter_error_n6T1.png)
diff --git a/examples/adiabatic/images/dynamics_n4T4.0.png b/examples/adiabatic/images/dynamics_n4T4.0.png
new file mode 100644
index 000000000..80a44c493
Binary files /dev/null and b/examples/adiabatic/images/dynamics_n4T4.0.png differ
diff --git a/examples/adiabatic/images/linears_T15plots_n4.png b/examples/adiabatic/images/linears_T15plots_n4.png
new file mode 100644
index 000000000..4d8143314
Binary files /dev/null and b/examples/adiabatic/images/linears_T15plots_n4.png differ
diff --git a/examples/adiabatic/images/poly3_powell_n4.png b/examples/adiabatic/images/poly3_powell_n4.png
new file mode 100644
index 000000000..bb3654c9c
Binary files /dev/null and b/examples/adiabatic/images/poly3_powell_n4.png differ
diff --git a/examples/adiabatic/images/trotter_error_n6T1.png b/examples/adiabatic/images/trotter_error_n6T1.png
new file mode 100644
index 000000000..30a638714
Binary files /dev/null and b/examples/adiabatic/images/trotter_error_n6T1.png differ
diff --git a/examples/adiabatic/linear.py b/examples/adiabatic/linear.py
new file mode 100644
index 000000000..8609a31fc
--- /dev/null
+++ b/examples/adiabatic/linear.py
@@ -0,0 +1,85 @@
+"""Adiabatic evolution for the Ising Hamiltonian using linear scaling."""
+
+import argparse
+from pathlib import Path
+
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+
+from qibo import callbacks, hamiltonians, models
+
+matplotlib.rcParams["mathtext.fontset"] = "cm"
+matplotlib.rcParams["font.family"] = "STIXGeneral"
+matplotlib.rcParams["font.size"] = 14
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=4, type=int)
+parser.add_argument("--hfield", default=1, type=float)
+parser.add_argument("--T", default=1, type=float)
+parser.add_argument("--dt", default=1e-2, type=float)
+parser.add_argument("--solver", default="exp", type=str)
+parser.add_argument("--save", action="store_true")
+
+
+def main(nqubits, hfield, T, dt, solver, save):
+ """Performs adiabatic evolution with critical TFIM as the "hard" Hamiltonian.
+
+ Plots how the energy and the overlap with the actual ground state
+ changes during the evolution.
+ Linear scheduling is used.
+
+ Args:
+ nqubits (int): Number of qubits in the system.
+ hfield (float): Transverse field Ising model h-field h value.
+ T (float): Total time of the adiabatic evolution.
+ dt (float): Time step used for integration.
+ solver (str): Solver used for integration.
+ save (bool): Whether to save the plots.
+ """
+ h0 = hamiltonians.X(nqubits)
+ h1 = hamiltonians.TFIM(nqubits, h=hfield)
+ bac = h1.backend
+
+ # Calculate target values (H1 ground state)
+ target_state = h1.ground_state()
+ target_energy = bac.to_numpy(h1.eigenvalues()[0]).real
+
+ # Check ground state
+ state_energy = bac.to_numpy(h1.expectation(target_state)).real
+ np.testing.assert_allclose(state_energy.real, target_energy)
+
+ energy = callbacks.Energy(h1)
+ overlap = callbacks.Overlap(target_state)
+ evolution = models.AdiabaticEvolution(
+ h0, h1, lambda t: t, dt=dt, solver=solver, callbacks=[energy, overlap]
+ )
+ final_psi = evolution(final_time=T)
+
+ # Plots
+ tt = np.linspace(0, T, int(T / dt) + 1)
+ plt.figure(figsize=(12, 4))
+ plt.subplot(121)
+ plt.plot(tt, energy[:], linewidth=2.0, label="Evolved state")
+ plt.axhline(y=target_energy, color="red", linewidth=2.0, label="Ground state")
+ plt.xlabel("$t$")
+ plt.ylabel("$H_1$")
+ plt.legend()
+
+ plt.subplot(122)
+ plt.plot(tt, overlap[:], linewidth=2.0)
+ plt.xlabel("$t$")
+ plt.ylabel("Overlap")
+
+ if save:
+ out_fol = Path("images")
+ out_fol.mkdir(exist_ok=True)
+ plt.savefig(out_fol / f"dynamics_n{nqubits}T{T}.png", bbox_inches="tight")
+ else:
+ plt.show()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/adiabatic/optimize.py b/examples/adiabatic/optimize.py
new file mode 100644
index 000000000..f81bf51f9
--- /dev/null
+++ b/examples/adiabatic/optimize.py
@@ -0,0 +1,84 @@
+"""Adiabatic evolution scheduling optimization for the Ising Hamiltonian."""
+
+import argparse
+from pathlib import Path
+
+import numpy as np
+
+from qibo import callbacks, hamiltonians, models
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=4, type=int)
+parser.add_argument("--hfield", default=1, type=float)
+parser.add_argument("--params", default="1", type=str)
+parser.add_argument("--dt", default=1e-2, type=float)
+parser.add_argument("--solver", default="exp", type=str)
+parser.add_argument("--method", default="Powell", type=str)
+parser.add_argument("--maxiter", default=None, type=int)
+parser.add_argument("--save", default=None, type=str)
+
+
+def spolynomial(t, params):
+ """General polynomial scheduling satisfying s(0)=0 and s(1)=1"""
+ f = sum(p * t ** (i + 2) for i, p in enumerate(params))
+ f += (1 - np.sum(params)) * t
+ return f
+
+
+def main(nqubits, hfield, params, dt, solver, method, maxiter, save):
+ """Optimizes the scheduling of the adiabatic evolution.
+
+ The ansatz for s(t) is a polynomial whose order is defined by the length of
+ ``params`` given.
+
+ Args:
+ nqubits (int): Number of qubits in the system.
+ hfield (float): Transverse field Ising model h-field h value.
+ params (str): Initial guess for the free parameters.
+ dt (float): Time step used for integration.
+ solver (str): Solver used for integration.
+ method (str): Which scipy optimizer to use.
+ maxiter (int): Maximum iterations for scipy optimizer.
+ save (str): Name to use for saving optimization history.
+ If ``None`` history will not be saved.
+ """
+ h0 = hamiltonians.X(nqubits)
+ h1 = hamiltonians.TFIM(nqubits, h=hfield)
+ bac = h1.backend
+
+ # Calculate target values (H1 ground state)
+ target_state = h1.ground_state()
+ target_energy = bac.to_numpy(h1.eigenvalues()[0]).real
+
+ # Check ground state
+ state_energy = bac.to_numpy(h1.expectation(target_state)).real
+ np.testing.assert_allclose(state_energy.real, target_energy)
+
+ evolution = models.AdiabaticEvolution(h0, h1, spolynomial, dt=dt, solver=solver)
+ options = {"maxiter": maxiter, "disp": True}
+ energy, parameters, _ = evolution.minimize(
+ params, method=method, options=options, messages=True
+ )
+
+ print("\nBest energy found:", energy)
+ print("Final parameters:", parameters)
+
+ final_state = evolution(parameters[-1])
+ overlap = bac.to_numpy(callbacks.Overlap(target_state).apply(bac, final_state)).real
+ print("Target energy:", target_energy)
+ print("Overlap:", overlap)
+
+ if save:
+ out_fol = Path("optparams")
+ out_fol.mkdir(exist_ok=True)
+ evolution.opt_history["loss"].append(target_energy)
+ np.save(out_fol / f"{save}_n{nqubits}_loss.npy", evolution.opt_history["loss"])
+ np.save(
+ out_fol / f"{save}_n{nqubits}_params.npy", evolution.opt_history["params"]
+ )
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ args["params"] = [float(x) for x in args["params"].split(",")]
+ main(**args)
diff --git a/examples/adiabatic/optparams/poly3_powell_n4_loss.npy b/examples/adiabatic/optparams/poly3_powell_n4_loss.npy
new file mode 100644
index 000000000..eb80288ee
Binary files /dev/null and b/examples/adiabatic/optparams/poly3_powell_n4_loss.npy differ
diff --git a/examples/adiabatic/optparams/poly3_powell_n4_params.npy b/examples/adiabatic/optparams/poly3_powell_n4_params.npy
new file mode 100644
index 000000000..8955a60ea
Binary files /dev/null and b/examples/adiabatic/optparams/poly3_powell_n4_params.npy differ
diff --git a/examples/adiabatic/trotter_error.py b/examples/adiabatic/trotter_error.py
new file mode 100644
index 000000000..7caadec8e
--- /dev/null
+++ b/examples/adiabatic/trotter_error.py
@@ -0,0 +1,89 @@
+"""Error of evolution using Trotter decomposition."""
+
+import argparse
+
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+from scipy.stats import linregress
+
+from qibo import callbacks, hamiltonians, models
+
+matplotlib.rcParams["mathtext.fontset"] = "cm"
+matplotlib.rcParams["font.family"] = "STIXGeneral"
+matplotlib.rcParams["font.size"] = 14
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=4, type=int)
+parser.add_argument("--hfield", default=1, type=float)
+parser.add_argument("--T", default=1, type=float)
+parser.add_argument("--save", action="store_true")
+
+
+def main(nqubits, hfield, T, save):
+ """Compares exact with Trotterized state evolution.
+
+ Plots the overlap between the exact final state and the state from the
+ Trotterized evolution as a function of the number of time steps used for
+ the discretization of time.
+ The transverse field Ising model (TFIM) is used as a toy model for this
+ experiment.
+
+ Args:
+ nqubits (int): Number of qubits in the system.
+ hfield (float): Transverse field Ising model h-field h value.
+ T (float): Total time of the adiabatic evolution.
+ save (bool): Whether to save the plots.
+ """
+ dense_h = hamiltonians.TFIM(nqubits, h=hfield)
+ trotter_h = hamiltonians.TFIM(nqubits, h=hfield, dense=False)
+ initial_state = np.ones(2**nqubits) / np.sqrt(2**nqubits)
+
+ nsteps_list = np.arange(50, 550, 50)
+ overlaps = []
+ for nsteps in nsteps_list:
+ exact_ev = models.StateEvolution(dense_h, dt=T / nsteps)
+ trotter_ev = models.StateEvolution(trotter_h, dt=T / nsteps)
+ exact_state = exact_ev(final_time=T, initial_state=np.copy(initial_state))
+ trotter_state = trotter_ev(final_time=T, initial_state=np.copy(initial_state))
+ ovlp = callbacks.Overlap(exact_state).apply(dense_h.backend, trotter_state)
+ overlaps.append(dense_h.backend.to_numpy(ovlp))
+
+ dt_list = T / nsteps_list
+ overlaps = 1 - np.array(overlaps)
+
+ exponent = int(linregress(np.log(dt_list), np.log(overlaps))[0])
+ err = [
+ overlaps[0] * (dt_list / dt_list[0]) ** (exponent - 1),
+ overlaps[0] * (dt_list / dt_list[0]) ** exponent,
+ overlaps[0] * (dt_list / dt_list[0]) ** (exponent + 1),
+ ]
+ alphas = [1.0, 0.7, 0.4]
+ labels = [
+ f"$\\delta t ^{exponent - 1}$",
+ f"$\\delta t ^{exponent}$",
+ f"$\\delta t ^{exponent - 1}$",
+ ]
+
+ plt.figure(figsize=(7, 4))
+ plt.semilogy(
+ nsteps_list, overlaps, marker="o", markersize=8, linewidth=2.0, label="Error"
+ )
+ for e, a, l in zip(err, alphas, labels):
+ plt.semilogy(
+ nsteps_list, e, color="red", alpha=a, linestyle="--", linewidth=2.0, label=l
+ )
+ plt.xlabel("Number of steps")
+ plt.ylabel("$1 -$ Overlap")
+ plt.legend()
+
+ if save:
+ plt.savefig(f"images/trotter_error_n{nqubits}T{T}.png", bbox_inches="tight")
+ else:
+ plt.show()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/adiabatic3sat/README.md b/examples/adiabatic3sat/README.md
new file mode 100644
index 000000000..c8d083d75
--- /dev/null
+++ b/examples/adiabatic3sat/README.md
@@ -0,0 +1,132 @@
+# Adiabatic evolution for solving an Exact Cover problem
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/adiabatic3sat](https://github.com/qiboteam/qibo/tree/master/examples/adiabatic3sat)
+
+## Introduction
+
+Adiabatic quantum computation aims to reach the ground state of a problem Hamiltonian encoding the solution of a hard problem, by adiabatically evolving a system from the ground state of a known, easy to prepare, Hamiltonian to the problem Hamiltonian.
+
+An Exact Cover instance of a 3SAT problem is characterized by a set of clauses containing 3 bits that are considered satisfied if one of them is in position 1, while the other remain at 0. The solution of this instance is bitstring that fulfils all the clauses at the same time.
+
+## Adiabatic evolution
+
+Adiabatic computation deals with finding the ground state of a Hamiltonian that encodes the solution of a computational problem. This has to be mapped to the time dependent Hamiltonian in the form
+
+![hamiltonian](images/H.png)
+
+where H_0 and H_p are the initial and problem Hamiltonian respectively, and s(t) defines the schedule that the adiabatic evolution follows. How fast this evolution can be performed depends on the minimum gap energy of the system during the evolution, that is the difference of energy between the ground state and the first excited state along the schedule. The smaller the gap energy the slower the evolution has to be.
+
+The initial Hamiltonian used in this example is the following,
+
+![initial-hamiltonian](images/h0.png)
+
+which has a ground state of the equal superposition of all possible computational states.
+
+As for the problem Hamiltonian that encodes the solution of an Exact cover problem, first we define the operator
+
+![z-operator](images/z-matrix.png)
+
+that can be used to create a Hamiltonian with a ground state when the Exact Cover clause is satisfied. This clause Hamiltonian reads
+
+![clause-hamiltonian](images/hc.png)
+
+where the indices i, j, k represent the three different qubits that the particular clause addresses. After this is defined, the problem Hamiltonian only needs to add up all clause Hamiltonians, so that the only state that remains at 0 energy will be the one that fulfils at the same time
+
+![problem-hamiltonain](images/hp.png)
+
+
+## How to run the example?
+
+Run the file `main.py` from the console to perform an adiabatic evolution for an instance of 8 qubits.
+
+The program supports the following basic arguments:
+
+- `--nqubits` (int) allows for instances with different number of qubits (default=8).
+- `--instance` (int) choose instance to use (default=1).
+- `--T` (float) set the total time of the adiabatic evolution (default=10).
+- `--dt` (float) set the interval of the calculations over the adiabatic evolution (default=1e-2).
+- `--solver` (str) set the type of solver for the evolution (default='exp').
+- `--plot` add this modifier to output energy and gap plots during the evolution. Capped under 14 qubits due to memory.
+- `--trotter` add this modifier to perform the trotterization of the Hamiltonian.
+
+The program returns:
+
+- The most common solution found after the evolution.
+- The probability of the most common solution.
+- `{}_qubits_energy.png` plots detailing the evolution of the lowest two eigenvalues for {} qubits. **(if enabled)**
+- `{}_qubits_gap.png` plots detailing the evolution of the gap energy for {} qubits. **(if enabled)**
+
+Initially supported number of qubits are [4, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30], with 10 different instances for qubits 8 and up.
+
+The `main.py` script uses linear scheduling for the adiabatic evolution by
+default. The user may switch the scheduling function to a polynomial of
+arbitrary order by passing the following argument:
+
+- `--params` (str) list of float values separated with commas (`,`) that
+ define the coefficients of the polynomial scheduling. The polynomial is
+ constructed so that it satisfies s(0)=0 and s(T)=T by definition.
+
+It is also possible to optimize the polynomial coefficients using the following
+arguments:
+
+- `--method` (str) optimization method to use. See the
+[Qibo optimizer documentation](https://qibo.science/qibo/stable/api-reference/qibo.html#optimizers)
+for more details on the available optimization methods.
+- `--maxiter` (int) maximum number of optimization iterations
+
+When an optimization method is given then the given `--params` are used as the
+initial guess for the variational parameters. If no `--params` are given linear
+scheduling is used and only the final time `T` is optimized.
+
+The functions used in this example, including problem hamiltonian creation are
+included in `functions.py`.
+
+## Create your own instances
+
+An example of an instance for 4 qubits reads:
+
+```text
+ 4 3 1
+0 1 0 0
+ 1 2 3
+ 2 3 4
+ 1 2 4
+```
+
+The first line includes:
+- number of qubits
+- number of clauses
+- number of 1's in the solution
+
+The second line is the solution of the instance.
+
+The following lines correspond to the three qubits present in each clause.
+
+Should the solution not be known, leave an empty line in place of the solution as well as remove the number of 1's in the solution.
+
+## Optimization example
+
+For demonstration we use `main.py` to optimize a 3SAT instance with N=10 qubits.
+We use a fourth order polynomial as the ansatz for the scheduling and after
+optimizing the coefficients and the total evolution time T using
+[`scipy`s BFGS method](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy.optimize.minimize)
+we find that the optimal values are:
+`-3.96837267,0.6582704,1.29674208` and `T=46.85073995`.
+Executing the script for these parameters we find the correct bitstring
+solution `0110000101` to the 3SAT problem with probability 99%.
+
+In the plot that follows we show the ground and excited state energies
+for the adiabatic evolution Hamiltonian, as well as the difference between
+them (gap) and the final form of the optimized scheduling function.
+We observe that in agreement with what we expect from theory the scheduling is
+"slower" at the area where the gap is minimum.
+
+![gap-plot](images/optn10_plot.png)
+
+Below we plot the probability to measure the correct solution from the
+final state of the evolution as a function of the total evolution time T. We
+find that when using the optimized scheduling (fourth order polynomial) the
+probability reaches 99% within T=50, while for the default linear scheduling
+T=400 is required to reach the same probability.
+
+![prob-plot](images/prob_plot.png)
diff --git a/examples/adiabatic3sat/functions.py b/examples/adiabatic3sat/functions.py
new file mode 100644
index 000000000..c6fa86582
--- /dev/null
+++ b/examples/adiabatic3sat/functions.py
@@ -0,0 +1,111 @@
+import matplotlib.pyplot as plt
+import numpy as np
+import sympy
+
+from qibo import hamiltonians, matrices, symbols
+
+
+def read_file(file_name, instance):
+ """Collect data from .txt file that characterizes the problem instance.
+ Args:
+ file_name (str): name of the file that contains the instance information.
+ instance (str): number of intance to use.
+
+ Returns:
+ control (list): important parameters of the instance.
+ [number of qubits, number of clauses, number of ones in the solution]
+ solution (list): list of the correct outputs of the instance for testing.
+ clauses (list): list of all clauses, with the qubits each clause acts upon.
+ """
+ file = open(f"../data3sat/{file_name}bit/n{file_name}i{instance}.txt")
+ control = list(map(int, file.readline().split()))
+ solution = list(map(str, file.readline().split()))
+ clauses = [list(map(int, file.readline().split())) for _ in range(control[1])]
+ return control, solution, clauses
+
+
+def times(qubits, clauses):
+ """Count the times each qubit appears in a clause to normalize H0.
+ Args:
+ qubits (int): # of total qubits in the instance.
+ clauses (list): clauses of the Exact Cover instance.
+
+ Returns:
+ times (list): number of times a qubit apears in all clauses.
+ """
+ times = np.zeros(qubits)
+ for clause in clauses:
+ for num in clause:
+ times[num - 1] += 1
+ return times
+
+
+def h_problem(qubits, clauses):
+ """Hamiltonian that satisfies all Exact Cover clauses.
+ Args:
+ qubits (int): # of total qubits in the instance.
+ clauses (list): clauses for an Exact Cover instance.
+
+ Returns:
+ sham (sympy.Expr): Symbolic form of the problem Hamiltonian.
+ smap (dict): Dictionary that maps the symbols that appear in the
+ Hamiltonian to the corresponding matrices and target qubits.
+ """
+ z_matrix = (matrices.I - matrices.Z) / 2.0
+ z = [symbols.Symbol(i, z_matrix) for i in range(qubits)]
+ return sum((sum(z[i - 1] for i in clause) - 1) ** 2 for clause in clauses)
+
+
+def h_initial(qubits, times):
+ """Initial hamiltonian for adiabatic evolution.
+ Args:
+ qubits (int): # of total qubits in the instance.
+ times (list): number of times a qubit apears in all clauses.
+
+ Returns:
+ sham (sympy.Expr): Symbolic form of the easy Hamiltonian.
+ smap (dict): Dictionary that maps the symbols that appear in the
+ Hamiltonian to the corresponding matrices and target qubits.
+ """
+ return sum(0.5 * times[i] * (1 - symbols.X(i)) for i in range(qubits))
+
+
+def spolynomial(t, params):
+ """General polynomial scheduling satisfying s(0)=0 and s(1)=1."""
+ f = sum(p * t ** (i + 2) for i, p in enumerate(params))
+ f += (1 - np.sum(params)) * t
+ return f
+
+
+def plot(qubits, ground, first, gap, dt, T):
+ """Get the first two eigenvalues and the gap energy
+ Args:
+ qubits (int): # of total qubits in the instance.
+ ground (list): ground state energy during the evolution.
+ first (list): first excited state during the evolution.
+ gap (list): gap energy during the evolution.
+ T (float): Final time for the schedue.
+ dt (float): time interval for the evolution.
+
+ Returns:
+ {}_qubits_energy.png: energy evolution of the ground and first excited state.
+ {}_qubits_gap_energy.png: gap evolution during the adiabatic process.
+ """
+ fig, ax = plt.subplots()
+ times = np.arange(0, T + dt, dt)
+ ax.plot(times, ground, label="ground state", color="C0")
+ ax.plot(times, first, label="first excited state", color="C1")
+ plt.ylabel("energy")
+ plt.xlabel("schedule")
+ plt.title("Energy during adiabatic evolution")
+ ax.legend()
+ fig.tight_layout()
+ fig.savefig(f"{qubits}_qubits_energy.png", dpi=300, bbox_inches="tight")
+ fig, ax = plt.subplots()
+ ax.plot(times, gap, label="gap energy", color="C0")
+ plt.ylabel("energy")
+ plt.xlabel("schedule")
+ plt.title("Energy during adiabatic evolution")
+ ax.legend()
+ fig.tight_layout()
+ fig.savefig(f"{qubits}_qubits_gap.png", dpi=300, bbox_inches="tight")
diff --git a/examples/adiabatic3sat/images/H.png b/examples/adiabatic3sat/images/H.png
new file mode 100644
index 000000000..77b40ea26
Binary files /dev/null and b/examples/adiabatic3sat/images/H.png differ
diff --git a/examples/adiabatic3sat/images/h0.png b/examples/adiabatic3sat/images/h0.png
new file mode 100644
index 000000000..f6b4862bb
Binary files /dev/null and b/examples/adiabatic3sat/images/h0.png differ
diff --git a/examples/adiabatic3sat/images/hc.png b/examples/adiabatic3sat/images/hc.png
new file mode 100644
index 000000000..c2e9fe267
Binary files /dev/null and b/examples/adiabatic3sat/images/hc.png differ
diff --git a/examples/adiabatic3sat/images/hp.png b/examples/adiabatic3sat/images/hp.png
new file mode 100644
index 000000000..3a9dfffd5
Binary files /dev/null and b/examples/adiabatic3sat/images/hp.png differ
diff --git a/examples/adiabatic3sat/images/optn10_plot.png b/examples/adiabatic3sat/images/optn10_plot.png
new file mode 100644
index 000000000..33330287e
Binary files /dev/null and b/examples/adiabatic3sat/images/optn10_plot.png differ
diff --git a/examples/adiabatic3sat/images/prob_plot.png b/examples/adiabatic3sat/images/prob_plot.png
new file mode 100644
index 000000000..a47677a34
Binary files /dev/null and b/examples/adiabatic3sat/images/prob_plot.png differ
diff --git a/examples/adiabatic3sat/images/z-matrix.png b/examples/adiabatic3sat/images/z-matrix.png
new file mode 100644
index 000000000..149197ab5
Binary files /dev/null and b/examples/adiabatic3sat/images/z-matrix.png differ
diff --git a/examples/adiabatic3sat/main.py b/examples/adiabatic3sat/main.py
new file mode 100644
index 000000000..06b0d5cce
--- /dev/null
+++ b/examples/adiabatic3sat/main.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+import argparse
+
+import functions
+import numpy as np
+
+from qibo import callbacks, hamiltonians, models
+
+
+def main(nqubits, instance, T, dt, solver, plot, dense, params, method, maxiter):
+ """Adiabatic evoluition to find the solution of an exact cover instance.
+
+ Args:
+ nqubits (int): number of qubits for the file that contains the
+ information of an Exact Cover instance.
+ instance (int): intance used for the desired number of qubits.
+ T (float): maximum schedule time. The larger T, better final results.
+ dt (float): time interval for the evolution.
+ solver (str): solver used for the adiabatic evolution.
+ plot (bool): decides if plots of the energy and gap will be returned.
+ dense (bool): decides if the full Hamiltonian matrix will be used.
+ params (list): list of polynomial coefficients for scheduling function.
+ Default is linear scheduling.
+ method (str): Method to use for scheduling optimization (optional).
+ maxiter (bool): Maximum iterations for scheduling optimization (optional).
+
+ Returns:
+ Result of the most probable outcome after the adiabatic evolution.
+ Plots of the ground and excited state energies and the underlying gap
+ during the adiabatic evolution. The plots are created only if the
+ ``--plot`` option is enabled.
+ """
+ # Read 3SAT clauses from file
+ control, solution, clauses = functions.read_file(nqubits, instance)
+ nqubits = int(control[0])
+ # Define "easy" and "problem" Hamiltonians
+ times = functions.times(nqubits, clauses)
+ sh0 = functions.h_initial(nqubits, times)
+ sh1 = functions.h_problem(nqubits, clauses)
+ H0 = hamiltonians.SymbolicHamiltonian(sh0)
+ H1 = hamiltonians.SymbolicHamiltonian(sh1)
+ if dense:
+ print("Using the full Hamiltonian evolution\n")
+ H0, H1 = H0.dense, H1.dense
+ else:
+ print("Using Trotter decomposition for the Hamiltonian\n")
+
+ print("-" * 20 + "\n")
+ if plot and nqubits >= 14:
+ print(
+ f"Currently not possible to calculate gap energy for {nqubits} qubits."
+ + "\n Proceeding to adiabatic evolution without plotting data.\n"
+ )
+ plot = False
+ if plot and method is not None:
+ print("Not possible to calculate gap energy during optimization.")
+ plot = False
+
+ # Define scheduling according to given params
+ if params is None:
+ # default is linear scheduling
+ s = lambda t: t
+ else:
+ if method is None:
+ s = lambda t: functions.spolynomial(t, params)
+ else:
+ s = functions.spolynomial
+
+ # Define evolution model and (optionally) callbacks
+ if plot:
+ ground = callbacks.Gap(0)
+ excited = callbacks.Gap(1)
+ gap = callbacks.Gap()
+ evolve = models.AdiabaticEvolution(
+ H0, H1, s, dt, solver=solver, callbacks=[gap, ground, excited]
+ )
+ else:
+ evolve = models.AdiabaticEvolution(H0, H1, s, dt, solver=solver)
+
+ if method is not None:
+ print(f"Optimizing scheduling using {method}.\n")
+ if params is None:
+ params = [T]
+ else:
+ params.append(T)
+ if method == "sgd":
+ options = {"nepochs": maxiter}
+ else:
+ options = {"maxiter": maxiter, "disp": True}
+ energy, params, _ = evolve.minimize(params, method=method, options=options)
+ T = params[-1]
+
+ # Perform evolution
+ initial_state = np.ones(2**nqubits) / np.sqrt(2**nqubits)
+ final_state = evolve(final_time=T, initial_state=initial_state)
+ output_dec = (np.abs(final_state) ** 2).argmax()
+ max_output = "{0:0{bits}b}".format(output_dec, bits=nqubits)
+ max_prob = (np.abs(final_state) ** 2).max()
+ print(f"Exact cover instance with {nqubits} qubits.\n")
+ if solution:
+ print(f"Known solution: {''.join(solution)}\n")
+ print("-" * 20 + "\n")
+ print(
+ f"Adiabatic evolution with total time {T}, evolution step {dt} and "
+ f"solver {solver}.\n"
+ )
+ print(f"Most common solution after adiabatic evolution: {max_output}.\n")
+ print(f"Found with probability: {max_prob}.\n")
+ if plot:
+ print("-" * 20 + "\n")
+ functions.plot(nqubits, ground[:], excited[:], gap[:], dt, T)
+ print("Plots finished.\n")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=8, type=int)
+ parser.add_argument("--instance", default=1, type=int)
+ parser.add_argument("--T", default=10, type=float)
+ parser.add_argument("--dt", default=1e-2, type=float)
+ parser.add_argument("--solver", default="exp", type=str)
+ parser.add_argument("--plot", action="store_true")
+ parser.add_argument("--dense", action="store_true")
+ parser.add_argument("--params", default=None, type=str)
+ parser.add_argument("--method", default=None, type=str)
+ parser.add_argument("--maxiter", default=None, type=int)
+ args = vars(parser.parse_args())
+ if args["params"] is not None:
+ args["params"] = [float(x) for x in args["params"].split(",")]
+ main(**args)
diff --git a/examples/adiabatic_qml/adiabatic-qml.ipynb b/examples/adiabatic_qml/adiabatic-qml.ipynb
new file mode 100644
index 000000000..0ee08d3d3
--- /dev/null
+++ b/examples/adiabatic_qml/adiabatic-qml.ipynb
@@ -0,0 +1,700 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.1.14|INFO|2023-07-31 13:42:55]: Using numpy backend on /CPU:0\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "from qibo import hamiltonians, set_backend\n",
+ "from qibo.derivative import parameter_shift\n",
+ "\n",
+ "from qaml_scripts.evolution import generate_adiabatic\n",
+ "from qaml_scripts.training import train_adiabatic_evolution\n",
+ "from qaml_scripts.rotational_circuit import rotational_circuit\n",
+ "\n",
+ "set_backend(\"numpy\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Determining probability density functions with adiabatic quantum computing \n",
+ "\n",
+ "Our goal is to fit Probability Density Functions (PDFs). In a few words, we want a model which, given data $x$ sampled \n",
+ "from a distribution $\\rho$,\n",
+ "returns an estimation of the PDF: $\\hat{\\rho}(x)$.\n",
+ "For doing this, we start by fitting the Cumulative Density Function (CDF) $F$ of the sample and then we compute the PDF as derivative of the CDF (as it is defined).\n",
+ "\n",
+ "We start by considering an adiabatic evolution as machine learning model to fit the CDF, and then we translate the evolution into a corresponding quantum circuit. This procedure allows us to calculate the derivative of the predictor using the Parameter Shift Rule [1].\n",
+ "\n",
+ "The whole strategy can be summarized with the following scheme:\n",
+ " \n",
+ "\n",
+ "> In this example we focus on one-dimensional PDFs, and we rely on the results of Ref. [2]."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2. Adiabatic Evolution in a nutshell\n",
+ "\n",
+ "Considering a quantum system set to be in an initial configuration described by $H_0$, \n",
+ "we call adiabatic evolution (AE) of the system from $H_0$ to $H_1$ an adiabatic\n",
+ "process governed by the following hamiltonian:\n",
+ "$$ H_{\\rm ad}(\\tau; \\vec{\\theta}) = \\bigl[1 - s(\\tau; \\vec{\\theta})\\bigr] H_0 + \n",
+ "s(\\tau; \\vec{\\theta}) H_1,$$\n",
+ "where we define $s(\\tau; \\vec{\\theta})$ scheduling function of the evolution. According\n",
+ "to the _adiabatic theorem_, if the process is slow enough, the system remains in\n",
+ "the groundstate of $H_{\\rm ad}$ during the evolution.\n",
+ "\n",
+ "A first simpler example of adiabatic evolution with `Qibo` can be found [here](https://github.com/qiboteam/qibo/tree/master/examples/adiabatic).\n",
+ "\n",
+ "#### We select a polynomial scheduling\n",
+ "\n",
+ "As scheduling function we are going to use a polynomial function of the form:\n",
+ "$$ s(\\tau; \\vec{\\theta}) = \\frac{1}{\\Theta} \\sum_{k=0}^{N_{\\rm params}} \\theta_k x^k, \n",
+ "\\qquad \\text{with} \\qquad \\Theta = \\sum_{k=1}^{N_{\\rm params}} \\theta_k,$$\n",
+ "in order to satisfy the computational condition $s(0)=0$ and $s(1)=1$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 3. Adiabatic Evolution as tool for encoding CDFs\n",
+ "\n",
+ "We encode the CDF of the sample into the energy of a target observable during an adiabatic evolution. This kind of approach can be useful because, when fitting a CDF, we need to \n",
+ "satisfy some conditions:\n",
+ "\n",
+ "1. the CDF is strictly monotonic;\n",
+ "2. $M(x_a) = 0$ and $M(x_b)=1$, with $x_a$ and $x_b$ limits of the random variable \n",
+ " $x$ domain.\n",
+ "\n",
+ "#### Inducing monotonoy\n",
+ "\n",
+ "Regarding the first point, as heuristic consideration we can think that an adiabatic \n",
+ "process is naturally led to follow a \"monotonic\" behaviour. In particular, if the \n",
+ "coefficients of the scheduling function are **positive**, we induce the process to \n",
+ "be monotonic. \n",
+ "\n",
+ "#### Exploiting the boundaries \n",
+ "\n",
+ "Secondly, by selecting two hamiltonians $H_0$ and $H_1$ whose energies on the ground state \n",
+ "satisfy our boundary conditions, we can easily contrain the problem to our desired\n",
+ "requirements.\n",
+ "\n",
+ "At this point, we can track the expected value of some target observable $E$ during the evolution. \n",
+ "\n",
+ "> __The goal:__ we perform the mapping: $(x_j, F_j) \\to (\\tau_j, E_j)$ and we train the\n",
+ "> evolution to let energies pass through our training points ${F_j}$. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Adiabatic Evolution settings in this problem\n",
+ "\n",
+ "We need to define an adiabatic evolution in which encoding a Cumulative Density\n",
+ "Function. We need to set the energy boundaries to $E(0)=0$ and $E(1)=1$.\n",
+ "\n",
+ "For doing this, we set $H_0=\\hat{X}$ and $H_1=\\hat{Z}$. Then we set a Pauli $\\hat{Z}$ to be \n",
+ "the target observable whose energy is tracked during the evolution:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Expectation of Z over the ground state of a Z: 0.0\n",
+ "Expectation of Z over the ground state of a Z: -1.0\n"
+ ]
+ }
+ ],
+ "source": [
+ "hx = hamiltonians.X(nqubits=1)\n",
+ "hz = hamiltonians.Z(nqubits=1)\n",
+ "\n",
+ "print(f\"Expectation of Z over the ground state of a Z: {hz.expectation(hx.ground_state())}\")\n",
+ "print(f\"Expectation of Z over the ground state of a Z: {hz.expectation(hz.ground_state())}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 4. Set the Adiabatic Evolution \n",
+ "\n",
+ "The next step is to define some parameters of the evolution, such as the timestep, the final time, etc."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Energy at 0: 0.0\n",
+ "Energy at 1: -1.0\n",
+ "\n",
+ "First ten evolution times:\n",
+ "[0. 0.002 0.004 0.006 0.008 0.01 0.012 0.014 0.016 0.018]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Definition of the Adiabatic evolution\n",
+ "\n",
+ "nqubits = 1\n",
+ "finalT = 50\n",
+ "dt = 1e-1\n",
+ "\n",
+ "# rank of the polynomial scheduling\n",
+ "nparams = 15\n",
+ "\n",
+ "# set hamiltonianas\n",
+ "h0 = hamiltonians.X(nqubits, dense=True)\n",
+ "h1 = hamiltonians.Z(nqubits, dense=True)\n",
+ "# we choose a target observable\n",
+ "obs_target = h1\n",
+ "\n",
+ "# ground states of initial and final hamiltonians\n",
+ "gs_h0 = h0.ground_state()\n",
+ "gs_h1 = h1.ground_state()\n",
+ "\n",
+ "# energies at the ground states\n",
+ "e0 = obs_target.expectation(gs_h0)\n",
+ "e1 = obs_target.expectation(gs_h1)\n",
+ "\n",
+ "print(f\"Energy at 0: {e0}\")\n",
+ "print(f\"Energy at 1: {e1}\")\n",
+ "\n",
+ "# initial guess for parameters\n",
+ "# picking up from U(0,1) helps to get the monotony\n",
+ "init_params = np.random.uniform(0, 1, nparams)\n",
+ "\n",
+ "# Number of steps of the adiabatic evolution\n",
+ "nsteps = int(finalT/dt)\n",
+ "\n",
+ "# array of x values, we want it bounded in [0,1]\n",
+ "xarr = np.linspace(0, 1, num=nsteps+1, endpoint=True)\n",
+ "print(\"\\nFirst ten evolution times:\")\n",
+ "print(xarr[0:10])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the `qaml_scripts/evolution.py` script, a function is defined to generate an `AdiabaticEvolution` object in which we set the polynomial scheduling parameters to be equal to some `init_params` set."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# generate an adiabatic evolution object and an energy callbacks container\n",
+ "evolution, energy = generate_adiabatic(h0=h0, h1=h1, obs_target=obs_target, dt=dt, params=init_params)\n",
+ "# evolve until final time\n",
+ "_ = evolution(final_time=finalT)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Some useful plotting functions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_energy(times, energies, title, cdf=None):\n",
+ " \"\"\"\n",
+ " Plot -E of the target Z observable moving from h0 to h1.\n",
+ " If cdf is not None, we also compare -E with the target CDF values.\n",
+ " \"\"\"\n",
+ " plt.figure(figsize=(8,5))\n",
+ " plt.plot(times, -np.array(energies), color=\"purple\", lw=2, alpha=0.8, label=\"Energy callbacks\")\n",
+ " if cdf is not None:\n",
+ " plt.plot(times, -np.array(cdf), color=\"orange\", lw=2, alpha=0.8, label=\"Empirical CDF\")\n",
+ " plt.title(title)\n",
+ " plt.xlabel(r'$\\tau$')\n",
+ " plt.ylabel(\"E\")\n",
+ " plt.grid(True)\n",
+ " plt.legend()\n",
+ " plt.show()\n",
+ "\n",
+ "def show_sample(times, sample, cdf, title):\n",
+ " \"\"\"Plot data sample and its empirical CDF.\"\"\"\n",
+ " plt.figure(figsize=(8,5))\n",
+ " plt.hist(sample, bins=50, color=\"black\", alpha=0.3, cumulative=True, density=True, label=\"Sample\")\n",
+ " plt.plot(times, -cdf, color=\"orange\", lw=2, alpha=0.8, label=\"Target CDF\")\n",
+ " plt.title(title)\n",
+ " plt.xlabel(r'$\\tau$')\n",
+ " plt.ylabel(\"CDF\")\n",
+ " plt.grid(True)\n",
+ " plt.legend()\n",
+ " plt.show()\n",
+ "\n",
+ "def plot_final_results(times, sample, e, de, title):\n",
+ " \"\"\"Plot final results.\"\"\"\n",
+ " plt.figure(figsize=(12,4))\n",
+ "\n",
+ " plt.subplot(1,2,1)\n",
+ " plt.title(\"PDF histogram\")\n",
+ " plt.hist(sample, bins=40, color=\"orange\", histtype=\"stepfilled\", edgecolor=\"orange\", hatch=\"//\", alpha=0.3, density=True)\n",
+ " plt.hist(sample, bins=40, color=\"orange\", alpha=1, lw=1.5, histtype=\"step\", density=True)\n",
+ " plt.plot(times, de, color=\"purple\", lw=2, label=r\"Estimated $\\rho$\")\n",
+ " plt.xlabel('x')\n",
+ " plt.ylabel(r\"$\\rho$\")\n",
+ " plt.legend()\n",
+ "\n",
+ " plt.subplot(1,2,2)\n",
+ " plt.title(\"CDF histogram\")\n",
+ " plt.hist(sample, bins=40, color=\"orange\", histtype=\"stepfilled\", edgecolor=\"orange\", hatch=\"//\", alpha=0.3, density=True, cumulative=True)\n",
+ " plt.hist(sample, bins=40, color=\"orange\", alpha=1, lw=1.5, histtype=\"step\", density=True, cumulative=True)\n",
+ " plt.plot(times, -np.array(e), color=\"purple\", lw=2, label=r\"Estimated $F$\")\n",
+ " plt.xlabel('x')\n",
+ " plt.ylabel(r\"$F$\")\n",
+ " plt.legend()\n",
+ "\n",
+ " plt.tight_layout()\n",
+ " plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can have a look to the expected value of the target $\\hat{Z}$ Hamiltonian during the random initialized adiabatic evolution."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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9pgkKTQ0VBERERoyZIhmzZql2267zVvLrFmz9Ic//EGNGzeusNYxY8bonXfe0UsvvaSLL75Ye/fu1caNG72fMTo6WjNmzFCjRo20du1a3X333YqOjtajjz4qqeSstMPh8G5/8veo9LPcfvvt+vHHH/Xzzz/rnnvuUcuWLTVy5EjvNhMnTlSTJk2Ul5enRx99VPfff79mz54tSdq9e7euvvpq9e3bV998843CwsK0ZMkSBQYGKiwsrMwxFy5cqOuvv16TJ0/WXXfdJUm699571bVrV/3tb3+Tw+HQ6tWrFRERUWEfjx07pqCgIF1yySXMM1tFLpdL8+fPV//+/b3/swNroYfWRv+s4eDmg1rz7hql/y9dhseQQw4FK1gKK/lLY3Z2tlontdYFAy+o8VpKTwJVhqXCbHx8vLKyssqsy8rKUlhYWIVnZaWSQBMQEFBuvdPpLPcL5Xa7ZbPZZLfby8zpWXCoQHn78866brfbfXZ/FrZVfm5Rm82m2bNnlwtBTzzxhJ544gnv8vDhw3XLLbdIKjlr/dprr+nnn3/W7373O7355ps677zzNGnSJO/27777rhITE7V161a1bt1aktSqVSu98MIL3m3GjBmjNm3a6MUXX5RUMpZ5/fr1evbZZ73fy5EjR6p3797KyspSQkKC9u3bp6+++krffPNNhZ8xJydHr776ql5//XWNGDHCe9xLLrnEu83TTz/tfZ6cnKwtW7Zo5syZevzxx73fE+n499Bms3n7WyoxMVEvv/yybDab2rVrp19//VWvvPKK7r77bknSnXfeKY/Ho+zsbIWFhenVV1/VBRdcoPz8fIWEhOitt95SeHi498yuJLVt27ZMX2w2m/79739r6NCheueddzRkyBDv6xkZGXr00UfVvn17SVKbNm3KN/c3drtdNputwp9dVA7fO+ujh9ZG/2qnvP15WvrCUqUtTPOuK/1vqF+gn6JaRKlBfAOl7U1TXMc4n/SwKsewVJjt1auX5syZU2bd/Pnz1atXrxo9bnDDc7gbmCG5il1y+jlVxROzVT7uZZddprfeeqvMuqioqDLLnTt39j5v0KCBwsLCtG/fPknSmjVr9O233yokJKTce2/bts0bZrt161bmtU2bNumCC8r+X1qPHj3KLXfo0EHvvfeexowZow8++EDNmjUrE05PtGHDBhUWFuqKK6445eedNWuW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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_energy(xarr, energy.results, \"Not trained evolution\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 5. Sample from a distribution\n",
+ "\n",
+ "We sample a target dataset from a Gamma distribution."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def cdf_fun(xarr):\n",
+ " \"\"\"\n",
+ " Generate a sample of data following a Gamma distribution with shape=10 and scale=0.5.\n",
+ " The sample is then used to build an histogram whose nbins is equal to the nsteps of the\n",
+ " evolution. We need to do this in order to map the x into the evolutionary times t.\n",
+ " \n",
+ " Args:\n",
+ " xarr (np.array): times;\n",
+ " \n",
+ " Returns:\n",
+ " cdf: cdf values associated to times;\n",
+ " sample: data sample not normalised into the range [0,1];\n",
+ " normed_sample: data sample normalised into the range [0,1].\n",
+ " \"\"\"\n",
+ " \n",
+ " nvals = 10000\n",
+ " shape = 10\n",
+ " scale = 0.5\n",
+ " \n",
+ " sample = np.random.gamma(shape, scale, nvals)\n",
+ " normed_sample = (sample - np.min(sample)) / (np.max(sample) - np.min(sample)) \n",
+ "\n",
+ " h, b = np.histogram(normed_sample, bins=nsteps, range=[0,1], density=False)\n",
+ " # Sanity check\n",
+ " np.testing.assert_allclose(b, xarr)\n",
+ "\n",
+ " cdf_raw = np.insert(np.cumsum(h)/len(h), 0, 0)\n",
+ "\n",
+ " # Translate the CDF such that it goes from 0 to 1\n",
+ " cdf_norm = (cdf_raw - np.min(cdf_raw)) / (np.max(cdf_raw) - np.min(cdf_raw))\n",
+ " # And now make it go from the E_initial to E_final (E0 to E1)\n",
+ " cdf = e0 + cdf_norm*(e1 - e0)\n",
+ "\n",
+ " return cdf, sample, normed_sample"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cdf, sample, normed_sample = cdf_fun(xarr)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### A look to the CDF"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAArMAAAHWCAYAAABkNgFvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB+80lEQVR4nO3dd3wUdf7H8dfuZndTSIOQhEAILfQqCIIFVIqiqOd5KirNs53ys3BnwQbYK+rZsIGVw+6pIIIopyI2ivQWAgEhARJIT3aTnd8fSzaEBEggyewm7yePfWS+s7M7n803S96Z/c53LIZhGIiIiIiIBCCr2QWIiIiIiBwvhVkRERERCVgKsyIiIiISsBRmRURERCRgKcyKiIiISMBSmBURERGRgKUwKyIiIiIBS2FWRERERAKWwqyIiIiIBCyFWRFpNIYMGcKQIUN87W3btmGxWHjzzTd968aPH0+TJk3qvbY2bdpw/vnn1/t+RUQCncKsiPitlJQUrr/+etq1a0dwcDARERGceuqpPPfccxQWFppdXkD45JNPsFgsvP7660fcZuHChVgsFv7973/71n3xxRcMHjyY2NhYQkNDadeuHZdeeinz58+v9RoXL17MxRdfTHx8PA6Hg9jYWEaNGsUnn3zi26bsD4+ym91uJyYmhkGDBnH33XeTlpZW5fMe+phDb5dffnmtvw4RMUeQ2QWIiFRl7ty5/O1vf8PpdDJ27Fi6d++Oy+Xixx9/5Pbbb2ft2rW8+uqrZpfp98477zwiIyOZPXs211xzTZXbzJ49G5vN5gt4Tz31FLfffjuDBw9m8uTJhIaGsmXLFr755hvmzJnDOeecU2v1TZkyhQceeIDk5GSuv/56kpKSyMzMZN68efz1r3/lvffe44orrvBtP3r0aEaOHInH42H//v389ttvPPvsszz33HO88cYbVYbUm2++mZNPPrnCujZt2tTaaxARcynMiojfSU1N5fLLLycpKYlvv/2WFi1a+O676aab2LJlC3PnzjWxwsDhdDq55JJLmDVrFrt27SIhIaHC/UVFRXz66acMGzaM2NhYSkpKePDBBxk2bBgLFiyo9Hx79uyptdo++ugjHnjgAS655BJmz56N3W733Xf77bfz9ddf43a7KzzmpJNO4qqrrqqwbvv27QwfPpxx48bRpUsXevXqVeH+008/nUsuuaTW6hYR/6JhBiLid5544gny8vJ44403KgTZMh06dOCWW27xtWfNmsVZZ51FbGwsTqeTrl278vLLL59QDVu3bmXEiBGEhYWRkJDAAw88gGEYFbZ56qmnGDRoEM2aNSMkJIS+ffvy0UcfVfl87777Lv379yc0NJTo6GjOOOOMKsPiod566y2CgoK4/fbbfevmzJlD3759CQ8PJyIigh49evDcc88d9XmuuuoqPB4Pc+bMqXTf3Llzyc7O5sorrwRg37595OTkcOqpp1b5XLGxsUfdV5l3332Xvn37EhISQtOmTbn88svZsWNHhW3uu+8+mjZtysyZMysE2TIjRoyo1jjipKQk3nzzTVwuF0888US16hORhkNhVkT8zhdffEG7du0YNGhQtbZ/+eWXSUpK4u677+bpp58mMTGRG2+8kRdffPG49l9aWso555xDXFwcTzzxBH379mXKlClMmTKlwnbPPfccffr04YEHHuCRRx4hKCiIv/3tb5WOGk+bNo0xY8Zgt9t54IEHmDZtGomJiXz77bdHrOHVV19lwoQJ3HXXXTz55JOAd2zr6NGjiY6O5vHHH+exxx5jyJAhLFmy5Kiv54wzzqBVq1bMnj270n2zZ88mNDSUiy66CPCG1ZCQEL744guysrKq8+2q5OGHH2bs2LEkJyczffp0br31VhYtWsQZZ5zBgQMHANi8eTMbNmzgoosuIjw8/Lj2c6iBAwfSvn17Fi5cWOm+3Nxc9u3bV+Hm8XhOeJ8i4icMERE/kp2dbQDGhRdeWO3HFBQUVFo3YsQIo127dhXWDR482Bg8eLCvnZqaagDGrFmzfOvGjRtnAMb//d//+dZ5PB7jvPPOMxwOh7F3794j7tflchndu3c3zjrrLN+6zZs3G1ar1fjLX/5ilJaWVtje4/H4lpOSkozzzjvPMAzDeO655wyLxWI8+OCDFba/5ZZbjIiICKOkpORI34ojuv322w3A2Lhxo29ddna2ERwcbIwePbrCtvfff78BGGFhYca5555rPPzww8ayZcuqtZ9t27YZNpvNePjhhyusX716tREUFORb/9///tcAjGeeeaZaz1vWV08++eQRt7nwwgsNwMjOzjYMwzC+++47A6jylpqaWq39ioj/05FZEfErOTk5ADU6WhcSEuJbzs7OZt++fQwePJitW7eSnZ19XHVMnDjRt2yxWJg4cSIul4tvvvmmyv3u37+f7OxsTj/9dJYvX+5b/9lnn+HxeLj//vuxWiv+l2uxWCrt94knnuCWW27h8ccf5957761wX1RUFPn5+VUefTyWsnGmhx6d/fjjjykqKvINMSgzbdo0Zs+eTZ8+ffj666+555576Nu3LyeddBLr168/6n4++eQTPB4Pl156aYUjofHx8SQnJ/Pdd98Bx9fPx1I2pVpubm6F9ffffz8LFy6scIuPj6+1/YqIuXQCmIj4lYiICKByIDmaJUuWMGXKFJYuXUpBQUGF+7Kzs4mMjKxRDVarlXbt2lVY17FjR8A7RVSZL7/8koceeoiVK1dSXFzsW39oSE1JScFqtdK1a9dj7vd///sfc+fO5c4776wwTrbMjTfeyAcffMC5555Ly5YtGT58OJdeemm1Zhfo2bMn3bt35z//+Q9Tp04FvME2JiaGESNGVNp+9OjRjB49mpycHH755RfefPNNZs+ezahRo1izZg3BwcFV7mfz5s0YhkFycnKV95eNjT2efj6WvLw8oHJA7tGjB0OHDq21/YiIf1GYFRG/EhERQUJCAmvWrKnW9ikpKZx99tl07tyZ6dOnk5iYiMPhYN68eTzzzDN1Njbyhx9+4IILLuCMM87gpZdeokWLFtjtdmbNmlXl2NTq6NatGwcOHOCdd97h+uuvp23bthXuj42NZeXKlXz99dd89dVXfPXVV8yaNYuxY8fy1ltvHfP5r7rqKu666y5+//13WrVqxXfffcf1119PUNCRfxVEREQwbNgwhg0bht1u56233uKXX35h8ODBVW7v8XiwWCx89dVX2Gy2SveXHT3t3LkzAKtXrz5m3dW1Zs0aYmNjfUFZRBoHDTMQEb9z/vnnk5KSwtKlS4+57RdffEFxcTGff/45119/PSNHjmTo0KEVhgDUlMfjYevWrRXWbdq0CSifn/Tjjz8mODiYr7/+mquvvppzzz23yqN/7du3x+PxsG7dumPuNyYmhm+++Qa73c7ZZ5/Nrl27Km3jcDgYNWoUL730ku+iEm+//TZbtmw55vOPHj0ai8XC7Nmzef/99yktLa00xOBo+vXrB8Du3buPuE379u0xDIO2bdsydOjQSrdTTjkF8B7p7tSpE//97399R1RPxNKlS0lJSWH48OEn/FwiElgUZkXE79xxxx2EhYVxzTXXkJGRUen+lJQU33RUZUf/jEOmzcrOzmbWrFknVMMLL7zgWzYMgxdeeMEXMsv2a7FYKC0t9W23bds2PvvsswrPc9FFF2G1WnnggQcqHSU2DpvqC6BVq1Z88803FBYWMmzYMDIzM333HboM3uEQPXv2BKgwzOFIWrduzemnn87777/Pu+++S9u2bSvNGFFQUHDEPyK++uorADp16nTEfVx88cXYbDamTZtW6fUZhlHhNUybNo3MzEyuueYaSkpKKj3XggUL+PLLL4/5urZv38748eNxOBxVDs8QkYZNwwxExO+0b9+e2bNnc9lll9GlS5cKVwD76aef+PDDDxk/fjwAw4cP9x2tvP7668nLy+O1114jNjb2qEcQjyY4OJj58+czbtw4BgwYwFdffcXcuXO5++67ad68OeC9stb06dM555xzuOKKK9izZw8vvvgiHTp0YNWqVb7n6tChA/fccw8PPvggp59+OhdffDFOp5PffvuNhIQEHn300Ur779ChAwsWLGDIkCGMGDGCb7/9loiICK655hqysrI466yzaNWqFdu3b+f555+nd+/edOnSpVqv7aqrruK6665j165d3HPPPZXuLygoYNCgQZxyyimcc845JCYmcuDAAT777DN++OEHLrroIvr06XPE52/fvj0PPfQQkydPZtu2bb6pt1JTU/n000+57rrr+Ne//gXAZZddxurVq3n44YdZsWIFo0eP9l0BbP78+SxatKjSkI3ly5fz7rvv4vF4OHDgAL/99hsff/wxFouFd955xxfuRaQRMXEmBRGRo9q0aZNx7bXXGm3atDEcDocRHh5unHrqqcbzzz9vFBUV+bb7/PPPjZ49exrBwcFGmzZtjMcff9yYOXNmpSmYqjs1V1hYmJGSkmIMHz7cCA0NNeLi4owpU6ZUmlrrjTfeMJKTkw2n02l07tzZmDVrljFlyhSjqv9aZ86cafTp08dwOp1GdHS0MXjwYGPhwoW++w+dmqvML7/8YoSHhxtnnHGGUVBQYHz00UfG8OHDjdjYWMPhcBitW7c2rr/+emP37t3V/p5mZWUZTqfTAIx169ZVut/tdhuvvfaacdFFFxlJSUmG0+k0QkNDjT59+hhPPvmkUVxcXK39fPzxx8Zpp51mhIWFGWFhYUbnzp2Nm266qcLUYGUWLVpkXHjhhUZsbKwRFBRkNG/e3Bg1apTx3//+17dNWV+V3YKCgoymTZsa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0nz3t23blrlz53Lbbbfx3HPP0apVK15//XVNy3VQcXEx6enpFdYFBQX5ZiI4lptuuonXXnuN0aNHc8cdd9C0aVO2bNnCnDlzeP3114847vf6669n+vTp3Hnnnfz9739n5cqVvotZHHrkNTo6mosvvpjbb7+d4cOHVxrfe6jg4GDuvPNO7rjjDhwOB6eeeip79+5l7dq1/P3vfyc5OZm0tDTmzJnDySefzNy5c/n000+r9ToPlZaWxqRJk7j++utZvnw5zz//vO/obuvWrXE4HLzwwgtceeWVbNu2jQcffLDC4ydOnMjzzz/P5ZdfzuTJk4mMjOTnn3+mf//+dOrUybddbGws3377LWeeeSajR49mzpw5uN1ubr/9di655BLatm3Lzp07+e233/jrX/9a49chIsfBU+oNqIW7Kh5FLdrjvRXvAdf+E9uHLRiC48pvzubgjAFnM3A09YZUW+jBQBrqPeopUsfy0vOYf8t8slKyALBYLfQa14u+1/bF5gi82XJMDbNDhgzxTZ9Ulaqu7jVkyBBWrFhRh1UFrvnz51cYDgDeo6YbNmyo1uMTEhJYsmQJd955J8OHD6e4uJikpCTOOeecow6RaNu2LR999BH//Oc/ee655xg4cCD33HMP//jHPyqNJf373//O7NmzfSehHc19991HUFAQ999/P7t27aJFixbccMMNAFxwwQXcdtttTJw4keLiYs477zzuu+8+pk6dWq3XWmbs2LEUFhbSv39/bDYbt9xyC9dddx0AzZs358033+Tuu+/m+eef56STTuKpp56qMJVZs2bN+Pbbb7n99tsZPHgwNpuN3r17c+qpp1baV3x8PN9++y1Dhgzhyiuv5O233yYzM5OxY8eSkZFBTEwMF198cZWzb4jICXLneUNrQRocWAvZayB7HXiKj/3YI7EEQUgCOKK8R1EdUeCIhiZtoUk779HVoHCNyRS/kpeRxxfXfUHurlwAnBFOhj0xjIR+CSZXdvwsxtHSZAOUk5NDZGQk2dnZVc5mkJqaStu2bWvtbPH6HDPrTx5++GFmzJjBjh07Kqx/5513uO2229i1a1fAXGzBX/qwLn4+Gwu32828efMYOXJk4J08JEA1+9DwQP42KNoHB1ZB3hYo+BMK/wR39c+MBrwnDDmbQ3DswaOq8RAS7/0aHOs9khoc7z1xSY5J70H/ULCvgC+u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DyciIqLO95+2Yx4Ze3fTtFkcWIIwLHaw2DDwfsUSdHB9EFiDsNmdWBxObMEObM5gbMGhWEOisTaJxhYWRVCoE3uTCJo7gzQlUz1xu90sXLiQYcOGacxsgFIfBj71YWBT/wW++u7Dsk/Sq8PUMBsTE4PNZiMjI6PC+oyMDOLj46t8THx8fI22dzqdOJ2VP1a32+310hmtr3+BNfPm0VsnDwW8+vqZkbqjPgx86sPApv4LfPXVhzXZh6lnyjgcDvr27cuiRYt86zweD4sWLWLgwIFVPmbgwIEVtgfvIe8jbS8iIiIiDZfpwwwmTZrEuHHj6NevH/379+fZZ58lPz/fN7vB2LFjadmyJY8++igAt9xyC4MHD+bpp5/mvPPOY86cOfz++++8+uqrZr4MERERETGB6WH2sssuY+/evdx///2kp6fTu3dv5s+f7zvJKy0tDau1/ADyoEGDmD17Nvfeey933303ycnJfPbZZ41vjlkRERERMT/MAkycOJGJEydWed/ixYsrrfvb3/7G3/72tzquSkRERET8nWaXFxEREZGApTArIiIiIgFLYVZEREREApbCrIiIiIgELIVZEREREQlYCrMiIiIiErAUZkVEREQkYCnMioiIiEjAUpgVERERkYClMCsiIiIiAcsvLmdbnwzDACAnJ6de9ud2uykoKCAnJwe73V4v+5TapT4MfOrDwKc+DGzqv8BX331YltPKctvRNLowm5ubC0BiYqLJlYiIiIjI0eTm5hIZGXnUbSxGdSJvA+LxeNi1axfh4eFYLJY6319OTg6JiYns2LGDiIiIOt+f1D71YeBTHwY+9WFgU/8FvvruQ8MwyM3NJSEhAav16KNiG92RWavVSqtWrep9vxEREXoDBzj1YeBTHwY+9WFgU/8Fvvrsw2MdkS2jE8BEREREJGApzIqIiIhIwFKYrWNOp5MpU6bgdDrNLkWOk/ow8KkPA5/6MLCp/wKfP/dhozsBTEREREQaDh2ZFREREZGApTArIiIiIgFLYVZEREREApbCrIiIiIgELIXZWvDiiy/Spk0bgoODGTBgAL/++utRt//www/p3LkzwcHB9OjRg3nz5tVTpXIkNenD1157jdNPP53o6Giio6MZOnToMftc6l5N34dl5syZg8Vi4aKLLqrbAuWYatqHBw4c4KabbqJFixY4nU46duyo/09NVNP+e/bZZ+nUqRMhISEkJiZy2223UVRUVE/VyuG+//57Ro0aRUJCAhaLhc8+++yYj1m8eDEnnXQSTqeTDh068Oabb9Z5nVUy5ITMmTPHcDgcxsyZM421a9ca1157rREVFWVkZGRUuf2SJUsMm81mPPHEE8a6deuMe++917Db7cbq1avruXIpU9M+vOKKK4wXX3zRWLFihbF+/Xpj/PjxRmRkpLFz5856rlzK1LQPy6SmphotW7Y0Tj/9dOPCCy+sn2KlSjXtw+LiYqNfv37GyJEjjR9//NFITU01Fi9ebKxcubKeKxfDqHn/vffee4bT6TTee+89IzU11fj666+NFi1aGLfddls9Vy5l5s2bZ9xzzz3GJ598YgDGp59+etTtt27daoSGhhqTJk0y1q1bZzz//POGzWYz5s+fXz8FH0Jh9gT179/fuOmmm3zt0tJSIyEhwXj00Uer3P7SSy81zjvvvArrBgwYYFx//fV1WqccWU378HAlJSVGeHi48dZbb9VViXIMx9OHJSUlxqBBg4zXX3/dGDdunMKsyWrahy+//LLRrl07w+Vy1VeJchQ17b+bbrrJOOussyqsmzRpknHqqafWaZ1SPdUJs3fccYfRrVu3Cusuu+wyY8SIEXVYWdU0zOAEuFwuli1bxtChQ33rrFYrQ4cOZenSpVU+ZunSpRW2BxgxYsQRt5e6dTx9eLiCggLcbjdNmzatqzLlKI63Dx944AFiY2P5+9//Xh9lylEcTx9+/vnnDBw4kJtuuom4uDi6d+/OI488QmlpaX2VLQcdT/8NGjSIZcuW+YYibN26lXnz5jFy5Mh6qVlOnD/lmaB632MDsm/fPkpLS4mLi6uwPi4ujg0bNlT5mPT09Cq3T09Pr7M65ciOpw8Pd+edd5KQkFDpTS3143j68Mcff+SNN95g5cqV9VChHMvx9OHWrVv59ttvufLKK5k3bx5btmzhxhtvxO12M2XKlPooWw46nv674oor2LdvH6eddhqGYVBSUsINN9zA3XffXR8lSy04Up7JycmhsLCQkJCQeqtFR2ZFTsBjjz3GnDlz+PTTTwkODja7HKmG3NxcxowZw2uvvUZMTIzZ5chx8ng8xMbG8uqrr9K3b18uu+wy7rnnHmbMmGF2aVINixcv5pFHHuGll15i+fLlfPLJJ8ydO5cHH3zQ7NIkAOnI7AmIiYnBZrORkZFRYX1GRgbx8fFVPiY+Pr5G20vdOp4+LPPUU0/x2GOP8c0339CzZ8+6LFOOoqZ9mJKSwrZt2xg1apRvncfjASAoKIiNGzfSvn37ui1aKjie92GLFi2w2+3YbDbfui5dupCeno7L5cLhcNRpzVLuePrvvvvuY8yYMVxzzTUA9OjRg/z8fK677jruuecerFYda/N3R8ozERER9XpUFnRk9oQ4HA769u3LokWLfOs8Hg+LFi1i4MCBVT5m4MCBFbYHWLhw4RG3l7p1PH0I8MQTT/Dggw8yf/58+vXrVx+lyhHUtA87d+7M6tWrWblype92wQUXcOaZZ7Jy5UoSExPrs3zh+N6Hp556Klu2bPH9IQKwadMmWrRooSBbz46n/woKCioF1rI/TAzDqLtipdb4VZ6p91POGpg5c+YYTqfTePPNN41169YZ1113nREVFWWkp6cbhmEYY8aMMe666y7f9kuWLDGCgoKMp556yli/fr0xZcoUTc1lspr24WOPPWY4HA7jo48+Mnbv3u275ebmmvUSGr2a9uHhNJuB+Wrah2lpaUZ4eLgxceJEY+PGjcaXX35pxMbGGg899JBZL6FRq2n/TZkyxQgPDzf+85//GFu3bjUWLFhgtG/f3rj00kvNegmNXm5urrFixQpjxYoVBmBMnz7dWLFihbF9+3bDMAzjrrvuMsaMGePbvmxqrttvv91Yv3698eKLL2pqrkD2/PPPG61btzYcDofRv39/4+eff/bdN3jwYGPcuHEVtv/ggw+Mjh07Gg6Hw+jWrZsxd+7ceq5YDleTPkxKSjKASrcpU6bUf+HiU9P34aEUZv1DTfvwp59+MgYMGGA4nU6jXbt2xsMPP2yUlJTUc9VSpib953a7jalTpxrt27c3goODjcTEROPGG2809u/fX/+Fi2EYhvHdd99V+butrN/GjRtnDB48uNJjevfubTgcDqNdu3bGrFmz6r1uwzAMi2HoeL6IiIiIBCaNmRURERGRgKUwKyIiIiIBS2FWRERERAKWwqyIiIiIBCyFWREREREJWAqzIiIiIhKwFGZFREREJGApzIqIiIhIwFKYFREREZGApTArIiIiIgFLYVZEREREApbCrIhIgLrwwguxWCxV3j7//HOzyxMRqRcWwzAMs4sQEZGay8zMxO12k5eXR3JyMvPmzaNPnz4AxMTEEBQUZHKFIiJ1T2FWRCTALV26lFNPPZWcnByaNGlidjkiIvVKwwxERALcqlWraNOmjYKsiDRKCrMiIgFu1apV9OzZ0+wyRERMoTArIhLgtm3bRqdOncwuQ0TEFAqzIiIBzuPxsH37dv788090GoSINDYKsyIiAe7mm29myZIldOrUSWFWRBodzWYgIiIiIgFLR2ZFREREJGApzIqIiIhIwFKYFREREZGApTArIiIiIgFLYVZEREREApbCrIiIiIgELIVZEREREQlYCrMiIiIiErAUZkVEREQkYCnMioiIiEjAUpgVERERkYD1/9LsktrE6zFoAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# we plot sample and empirical CDF\n",
+ "show_sample(times=xarr, sample=normed_sample, cdf=cdf, title=\"Target Cumulative Density Function\")\n",
+ "# we plot CDF and (-energy) of the target observable during the non-trained evolution\n",
+ "plot_energy(times=xarr, energies=energy.results, title=\"Callbacks VS eCDF\", cdf=cdf)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 6. Train the evolution to follow the CDF\n",
+ "\n",
+ "The training procedure is the following:\n",
+ "\n",
+ "1. we fill the scheduling with a set of parameters;\n",
+ "2. we perform the evolution with the defined set of parameters and we collect all the energies $\\{E_k\\}_{k=1}^{N_{\\rm data}}$, where $E_k$ is the expected value of Z over the evolved state at $\\tau_k \\equiv x_k$.\n",
+ "3. we calculate a loss function:\n",
+ " $$ J_{\\rm mse} = \\frac{1}{N_{\\rm data}} \\sum_{k=1}^{N_{\\rm data}} \\bigl[E_k - F(x_k)\\bigr]^2. $$\n",
+ "4. we use the CMA-ES optimizer to find the best set of parameters of the scheduling which lead the energy of Z to pass through\n",
+ " the CDF values.\n",
+ " \n",
+ "> The following cell takes some minutes to be computed. \n",
+ "> All the training procedure can be found in the `qaml_script/training.py` script."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB99UlEQVR4nO3dd3hTZfvA8W92995QaNlLQEAQREEsFlEUXLyKMl634gBRQVHAhZMXFRDlFUEUAbc/4QUZorI3oiyBljI66W7aJE3O749IoLSFFtqeJr0/15WL5jnPObl7Wpo7z9QoiqIghBBCCOEhtGoHIIQQQghRkyS5EUIIIYRHkeRGCCGEEB5FkhshhBBCeBRJboQQQgjhUSS5EUIIIYRHkeRGCCGEEB5FkhshhBBCeBRJboQQQgjhUSS5EULUmsmTJ6PRaFR57bVr16LRaFi7dq0qr38hycnJaDQa5s2bV6PXVfOeC1FfSHIjhArmzZuHRqPBy8uLEydOlDvet29fOnTocFHXnjVrVpXfMM1mM5MnT663CYComPzchDg/SW6EUJHFYuGNN96o0WtWN7mZMmVKrb1JTpw4keLi4lq5dkN2vp+b3HMhJLkRQlWdO3dmzpw5nDx5Uu1QqqSoqKha9fV6PV5eXrUUjaiI3HMhJLkRQlXPP/88dru9Sq03paWlvPLKKzRv3hyTyURcXBzPP/88FovFVScuLo6//vqLX3/9FY1Gg0ajoW/fvhVeLzk5mfDwcACmTJniqj958mQARo4ciZ+fH4cPH2bgwIH4+/szbNgwAH7//XfuuOMOmjRpgslkIjY2ljFjxpRrMaho/IdGo2H06NF8//33dOjQAZPJRPv27Vm+fHm5GE+cOMG///1vIiMjXfXmzp1brt7x48cZPHgwvr6+REREMGbMmDL35UIu9Drp6eno9XqmTJlS7twDBw6g0WiYMWOGq+zIkSPccccdhISE4OPjw5VXXsnSpUsvGEffvn0r/HmNHDmSuLg44MI/t4rueVV+d8D5+3PTTTexbt06unfvjpeXF82aNeOzzz67YOxC1Cd6tQMQoiGLj49n+PDhzJkzh/HjxxMTE1Np3fvvv5/58+dz++238/TTT7N582amTp3Kvn37+O677wCYPn06jz/+OH5+frzwwgsAREZGVni98PBwPvzwQx555BGGDBnCrbfeCkDHjh1ddUpLS0lMTKR379688847+Pj4APDVV19hNpt55JFHCA0NZcuWLXzwwQccP36cr7766oLf97p16/j222959NFH8ff35/333+e2224jJSWF0NBQwJlQXHnlla5kKDw8nP/973/cd9995Ofn89RTTwFQXFzMddddR0pKCk888QQxMTEsWLCANWvWXDCOqr5OZGQkffr0YcmSJUyaNKnM+YsXL0an03HHHXe4rterVy/MZjNPPPEEoaGhzJ8/n5tvvpmvv/6aIUOGVCmuylTl53auqvzunHbo0CFuv/127rvvPkaMGMHcuXMZOXIkXbt2pX379pcUuxB1RhFC1LlPP/1UAZStW7cqhw8fVvR6vfLEE0+4jvfp00dp37696/muXbsUQLn//vvLXGfcuHEKoKxZs8ZV1r59e6VPnz5ViiMzM1MBlEmTJpU7NmLECAVQxo8fX+6Y2WwuVzZ16lRFo9EoR48edZVNmjRJOffPDKAYjUbl0KFDrrLdu3crgPLBBx+4yu677z4lOjpaycrKKnP+v/71LyUwMNAVw/Tp0xVAWbJkiatOUVGR0qJFCwVQfvnll/Peg6q+zkcffaQAyp49e8rUa9eundKvXz/X86eeekoBlN9//91VVlBQoMTHxytxcXGK3W5XFEVRkpKSFED59NNPXfX69OlT4c9uxIgRStOmTV3Pz/dzO/eeV+d3p2nTpgqg/Pbbb66yjIwMxWQyKU8//XS51xKivpJuKSFU1qxZM+69914+/vhjUlNTK6yzbNkyAMaOHVum/OmnnwaoUpfHxXrkkUfKlXl7e7u+LioqIisri169eqEoCjt37rzgNRMSEmjevLnreceOHQkICODIkSMAKIrCN998w6BBg1AUhaysLNcjMTGRvLw8duzYATjvTXR0NLfffrvrej4+Pjz44IMXjKM6r3Prrbei1+tZvHix6/w///yTvXv3MnToUFfZsmXL6N69O71793aV+fn58eCDD5KcnMzevXsvGFdNqu7vTrt27bj66qtdz8PDw2ndurXrZyOEO5DkRoh6YOLEiZSWllY69ubo0aNotVpatGhRpjwqKoqgoCCOHj1aK3Hp9XoaN25crjwlJYWRI0cSEhKCn58f4eHh9OnTB4C8vLwLXrdJkyblyoKDg8nJyQEgMzOT3NxcPv74Y8LDw8s8Ro0aBUBGRgbgvDctWrQoN86k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BIgeQIHZyttvv83EiRO58sorufLKK9WOVoh6LemXJEpLnCt1N+vfDJ3R8wfbS3JTk9LWwKYRUFoEhiDQnrU4ksMG5hTn8RpeJCw1NdX19eLFi3nppZc4cOCAq8zPz8/1taIo2O32Ko1LCA8Pr1YcRqORqKioap2jJneKtUE5zwcEmyGK5+bb+WJ1Kv/6179o166denEK4Sb+XnZmllRD6JIC6ZaqOXaL8w9yaREYw8omNuB8bgxzHt8xtka7qKKiolyPwMBANBqN6/n+/fvx9/fnf//7H127dsVkMrFu3ToOHz7MLbfcQmRkJH5+flxxxRWsWrWqzHXP7ZbSaDT897//ZciQIfj4+NCyZUt+/PFH1/Fzu6XmzZtHUFAQK1asoG3btvj5+TFgwIAyyVhpaSlPPPEEQUFBhIaG8txzzzFixAgGDx5c6fd7+rrff/89LVu2xMvLi8TERI4dO1am3ocffkjz5s0xGo20bt2aBQsWlDl+drdUcnIyGo2Gb7/9lmuvvRYfHx86derExo0bXd/bqFGjyMvLQ6PRoNFoXK1hs2bNcsURGRnJ7bffXpUfm6jI6Q8I5hQwBIJXhOthUbwpzNzHxIRDbFv6liQ2QlRBcXYxJ7eeBMA/xp/IjpEqR1Q3JLmpKanLnZ80DUFQWTeRRuM8bj7mXB21Do0fP5433niDffv20bFjRwoLCxk4cCCrV69m586dDBgwgEGDBpGSknLe60yZMoU777yTP/74g4EDBzJs2DCys7MrrW82m3nnnXdYsGABv/32GykpKYwbN851/M033+SLL77g008/Zf369eTn51dpHIzZbOa1117js88+Y/369eTm5vKvf/3Ldfy7777jySef5Omnn+bPP//koYceYtSoUfzyyy/nve4LL7zAuHHj2LVrF61ateKuu+6itLSUXr16MX36dAICAkhNTSU1NZVx48axbds2nnjiCV5++WUOHDjA8uXLueaaay4Yv6jAeT4gWG02jh47SX6JgcgwP1rmf+h2Y9iEUEPSmiQUhwJA8+ubX3AYg6eQbqmakrbK2YR+bovNubQGZ720lXW6OurLL79M//79Xc9DQkLo1KmT6/krr7zCd999x48//sjo0aMrvc7IkSO56667AHj99dd5//332bJlCwMGDKiwvs1mY/bs2TRv7lxTYfTo0bz88suu4x988AETJkxgyJAhAMyYMYNly5Zd8Pux2WzMmDGDHj16ADB//nzatm3Lli1b6N69O++88w4jR47k0UcfBWDs2LFs2rSJd955h2uvvbbS644bN44bb7wRcCZy7du359ChQ7Rp06ZMq9hpKSkp+Pr6ctNNN+Hv70/Tpk25/PLLLxi/qMB5PiAYDAYiIiIICAhAq5Se+YAgKwwLcV6Hfz7s+trT17Y5m7Tc1BRLTvXqW6tZ/xJ169atzPPCwkLGjRtH27ZtCQoKws/Pj3379l2w5ebsqba+vr4EBASQkZFRaX0fHx9XYgMQHR3tqp+Xl0d6ejrdu3d3HdfpdHTt2vWC349er+eKK65wPW/Tpg1BQUHs27cPgH379nHVVVeVOeeqq65yHa/K9xcdHQ1w3u+vf//+NG3alGbNmnHvvffyxRdfYDabLxi/qMA5HxAUIOvUKfILCtAAQYGBaDWash8QhBCVKsooIm1nGgBBTYMIaRmickR1R5KbmmKq+qwbwLlRXx06d9bTuHHj+O6773j99df5/fff2bVrF5dddhlWq/W81zEYyrZMaTQaHA5HJbUrrq8oSjWjrztnx3u6+fZ835+/vz87duzgyy+/JDo6mpdeeolOnTrJjKqLcdYHhNLSUlJSUjiVdQqbzVZx/Tr+gCCEuzmy6ojr723zxIbTJQWS3NScqATQaJ2zos7HYXPWi+p//nq1bP369YwcOZIhQ4Zw2WWXERUVRXJycp3GEBgYSGRkJFu3bnWV2e12duzYccFzS0tL2bZtm+v5gQMHyM3NpW1b5y63bdu2Zf369WXOWb9+/SUNQjUajdjt9nLler2ehIQE3nrrLf744w+Sk5NZs2bNRb9Og/XPB4SSkhKOpqRQWlpKkyaxhIZU8mmzjj8gCOFuzu6Sata/mYqR1D0Zc1NTogc417ExpzgHQ1aUISsK2HLBpwlEJ9Z5iGdr2bIl3377LYMGDUKj0fDiiy+et4Witjz++ONMnTqVFi1a0KZNGz744ANycnIu+AnDYDDw+OOP8/7776PX6xk9ejRXXnmlq4vrmWee4c477+Tyyy8nISGB//u//+Pbb78tNyOsOuLi4igsLGT16tV06tQJHx8f1qxZw5EjR7jmmmsIDg5m2bJlOBwOWrdufdGv02BFJaAkL+RUVjp6vZ6YmBgMFS1ZUE8+IAhRnxWcLCDjT2eXemjLUILjG9aHAWm5qSk6k3OBPr0vWLPKt+A4bM5yva+zns6kTpz/mDZtGsHBwfTq1YtBgwaRmJhIly5d6jyO5557jrvuuovhw4fTs2dP/Pz8SExMxMvL67zn+fj48Nxzz3H33Xdz1VVX4efnx+LFi13HBw8ezHvvvcc777xD+/bt+eijj/j000/p27fvRcfaq1cvHn74YYYOHUp4eDhvvfUWQUFBfPvtt/Tr14+2bdsye/ZsvvzyS9q3b3/Rr9NQOSKvR+MTS3SIF7GNG1ec2Lg+IMSq/gFBiPqsTKvN9Q2r1QZAo9TnARC1ID8/n8DAQPLy8ggICChzrKSkhKSkJOLj4y/45lqpyhYg02hrbYViT+JwOGjbti133nknr7zySoV13HWl4Br5/fJQW7Zs4cEHH+TnBeOJOPxM5Qth2nKdHxBqeCFMITzNN3d/w6mDpwD41w//IqBRwAXOqP/O9/59LumWqmlR/SBxs3OaatpK56BHY7CzCT06UfUWm/rm6NGj/Pzzz/Tp0weLxcKMGTNISkri7rvvVjs0UUf++usvEhMTadu2LbqY/hA+/zwfEJrIBwQhLiD3aK4rsQlvF+4RiU11SXJTG3Qm5/obsgbHBWm1WubNm8e4ceNQFIUOHTqwatUq18Bg4dmys7MZMmQIsbGx/O9///tnXzL5gCDEpUhaneT6uiGtbXM2SW6EqmJjY8vNarqQkSNHMnLkyNoJSNQZi8VC7969yc7OZuPGjWU3XJUPCEJctORfkl1fx/eLVy8QFUlyI4RQhclk4pNPPiEyMpJmzRregEchakNBagGZ+zIBCGsdhn+Mv8oRqUNmS1WggY2xFnVEfq/O+PbbbyktLaVnz56S2AhRg85utYnrF6daHGqT5OYsp1enleXzRW04/Xt17qrNDc0nn3zCbbfdxtKlS9UORQiPk/TLmfE28dc2zC4pkG6pMnQ6HUFBQa69hHx8fBrUctWidiiKgtlsJiMjg6CgIHQ6ndohqebgwYOMHj2aBx98kFtuuUXtcITwKMXZxaTvSgece0kFxQepG5CKJLk5x+kdn8+3WaIQFyMoKKjMjuINjcPh4P7776dRo0b85z//UTscITxO8tpkV/d3XL+4Bv3hXJKbc2g0GqKjo4mIiKh8wz4hqslgMDToFhuApUuX8vvvv/PLL7/g4+OjdjhCeJzktcmurxtylxRIclMpnU7X4N+MhKhJN910E1u2bOGKK65QOxQhPI6lwMKJLScA8Iv0I6xtmMoRqUsGFAshatXBgwf58ssv0Wg0ktgIUUtS1qXgKHWu6N3Qu6RAWm6EELUoOzubm266Cb1ez6233orJJKsLC1Ebyizc18C7pECSGyFELbFardx2221kZ2ezefNmSWyEqCWlJaUcW38MAO9gb6I6N9yJC6ep3i01c+ZM4uLi8PLyokePHmzZsuW89adPn07r1q3x9vYmNjaWMWPGUFJSUkfRCiGqaurUqaxbt47vvvuO5s0b5v42QtSFYxuPUWopBaBpn6ZotA27SwpUTm4WL17M2LFjmTRpEjt27KBTp04kJiZWOg174cKFjB8/nkmTJrFv3z4++eQTFi9ezPPPP1/HkQshzkdRFH755RfGjx/P1VdfrXY4Qng02UuqPI2i4prwPXr04IorrmDGjBmAcx2M2NhYHn/8ccaPH1+u/ujRo9m3bx+rV692lT399NNs3ryZdevWVek18/PzCQwMJC8vj4CAhrcNvBC1rbS0FL1eT35+PkajES8vL7VDEsJj2W12FvRfgLXQitHXyL0r70Vn9MyZvtV5/1at5cZqtbJ9+3YSEhLOBKPVkpCQwMaNGys8p1evXmzfvt3VdXXkyBGWLVvGwIEDK30di8VCfn5+mYcQonYcO3aMDh06cPToUQICAiSxEaKWpW5PxVpoBaDJ1U08NrGpLtUGFGdlZWG324mMjCxTHhkZyf79+ys85+677yYrK4vevXujKAqlpaU8/PDD5+2Wmjp1KlOmTKnR2IUQFXv66afJy8sjODhY7VCEaBCS1py1l5R0SbmoPqC4OtauXcvrr7/OrFmz2LFjB99++y1Lly7llVdeqfScCRMmkJeX53ocO3asDiMWwkPZLXD8B9j2OKy/B7Y9zu6lL/PDd1/x9ttvS5evEHVAcSiuVYl1Rh2NezZWN6B6RLWWm7CwMHQ6Henp6WXK09PTK91/58UXX+Tee+/l/vvvB+Cyyy6jqKiIBx98kBdeeAGttnyuZjKZZAqqEDUpbQ3sGAvmY6A4Fw1zKApN8gv4810fWlwXrXKAQjQM6X+kU5xdDEBsr1gM3gaVI6o/VGu5MRqNdO3atczgYIfDwerVq+nZs2eF55jN5nIJzOktElQcFy1Ew5G2BjaNAHMKGALBKwK8IrBpgyiy6mkWaUCzeaSznhCiVkmXVOVUXcRv7NixjBgxgm7dutG9e3emT59OUVERo0aNAmD48OE0atSIqVOnAjBo0CCmTZvG5ZdfTo8ePTh06BAvvvgigwYNkn2ghKhtdouzxaa0CIxhcNby7iaTiUaxcWgUBaxZznqJm0EnraZC1AZFUVxTwLU6LU16N1E3oHpG1eRm6NChZGZm8tJLL5GWlkbnzp1Zvny5a5BxSkpKmZaaiRMnotFomDhxIidOnCA8PJxBgwbx2muvqfUtCNFwpC53dkUZglyJjd3hID09nbCwMIwGg7PcEOSsl7oCGt+sashCeKpTB05RkFoAQHS3aEwB8kHibKquc6MGWedGiIu07XFI+tzZFfWP9IwM8vLyaNasGfqzW09LMiD+Huj2gQqBCuH5tn64lZ2f7ASg94TetLutncoR1T63WOdGCOFmLDllnubm5ZGbk0tEeHjZxOY0a075MiFEjTjdJaXRaIjrE6dqLPWRJDdCiKoxnVm7pshsJj09naDgIAKDgiqub5S1boSoDbnJueQccX54iOgYgU+Yj8oR1T+S3AghqiYqATRacNiw2+34+/kRERFBuS36HDZnvaj+akQphMdL+kVmSV2IqgOKhRBuJHoAik9jNOZjBPiFEeDvX76OooAtF3yaQHRinYcoRENQZqPMayW5qYi03AghqkZnYtHB7pzKs6BYspwtNGdz2JzTwPW+0GWaTAMXohYUphWSuTcTgLDWYfjHVPAhQ0hyI4SomrS0NB56aTFfJiei8W0CtjznrKjTD1ues8XmyvkQ1U/tcIXwSGd3ScX1i1MvkHpOuqWEEFUyceJEDAYDd4/5LwT6OtexSVvpnBVlDHaOsYlOlBYbIWqRdElVjSQ3QogLWrt2LZ988gkzZswgJCTEWdj4ZlmkT4g6VJxdTNquNAACmwQSFB+kbkD1mHRLCSEuaPfu3Vx77bU88sgjaociRIN19LejKA7nurvx/eLRaMrNVRT/kORGCHFBTz75JCtXriy3ca0Qou7IRplVJ3+phBCVmjt3Lq+++iqKosjmtEKoyFpo5cSWEwD4RvgS1jZM5YjqN0luhBAVyszM5Omnn+bw4cPS/C2EylLWpeAodQDSJVUVktwIISo0btw4AN566y2VIxFCSJdU9chsKSFEOf/3f//HZ599xvz58wkPD1c7HCEatFJLKcc2HAPAO9ibqM5RKkdU/0nLjRCinBUrVnD99ddz7733qh2KEA3e8U3HKS0pBaBpn6ZotNIldSHSciOEKGfGjBmYzWbp1xeiHji7Syru2jj1AnEj0nIjhHDJzs7mhx9+QFEUfHx81A5HiAbPbrOT8lsKAAYfA42uaKRyRO5BkhshhMurr77KsGHDOHXqlNqhCCGA1O2pWAosADS5ugk6oyzJUBWS3AghADh06BAzZsxg/PjxhIXJGhpC1Adnd0k1u66ZipG4F0luhBAAjB8/noiICMaOHat2KEIIQHEoJK9NBkBv0tO4Z2N1A3IjMqBYCMHPP//MN998w4IFC2SsjRD1RNquNIqziwFo3KsxBm+DyhG5D2m5EULQt29fPvvsM4YNG6Z2KEKIf8jCfRdPkhshGriMjAyMRiP33nuvTP0Wop5QFMWV3Gj1Wpr0bqJyRO5FkhshGrCtW7fSpEkT1q9fr3YoQoizZO7NpCijCIBG3Rth8jepHJF7keRGiAZKURTGjBlD69at6dGjh9rhCCHOkrRauqQuhQwoFqKBWrVqFevXr2fZsmXo9fKnQIj6QlEUkn9JBkCj1dC0T1N1A3JD0nIjRAPkcDh4/vnn6d69OwMGDFA7HCHEWbIPZZN3LA+A6C7ReAd7qxyR+5GPa0I0QEVFRbRs2ZJHH31UBhELUc+cbrUB6ZK6WJLcCNEA+fv7s3DhQrXDEEJU4OzxNnF949QLxI1Jt5QQDcz777/Pl19+qXYYQogK5KXkkX04G4DIyyLxjfBVOSL3JMmNEA1ITk4OL774Itu2bVM7FCFEBc5euC+uX5x6gbg56ZYSwlPZLZC6HNJWgSUHTMF8/X9JaLHxzDPPqB2dEKICsipxzZDkRghPlLYGdowF8zFQHACU2ku5tVEht7wfTQR7gSh1YxRClFGQWkDm3kwAQluFEtAoQOWI3Jd0SwnhadLWwKYRYE4BQyB4RYBXBJn5GopLjYT7lDiPp61RO1IhxFnKzJK6TlptLoUkN0J4ErvF2WJTWgTGMNCe2UU4KjqaqOjGaExhzuM7xjrrCyHqBemSqjmS3AjhSVKXO7uiDEHwz/o1VpuNkpISNOBciVijcR43H4PUFWpGK4T4h/mUmfTd6QAExQURHB+sckTuTZIbITxJ2irnGJuzWmwy0tM5mZqKcnY9rcFZL21lnYcohCgvaU0SiuL8XyqtNpdOkhshPIklp8xTc3ExRUVmwsPDqXAdYmtORaVCiDp2ZOUR19fNEpqpGIlnkORGCE9iKtuUferUKUwmI35+fhXXN0rTtxBqM2eZSduZBkBgk0BCWoaoHJH7k+RGCE8SlQAaLThsFJeUYC4yExIaWr7VxmFz1ovqr0aUQoiznN0l1ax/M9nvrQZIciOEJ4keAD6xYMvFoNcTFh6Gv79/2TqKArZcZ73oRFXCFEKccXjlYdfXzfs3VzESzyHJjRCeRGeCLtNA74venkNokH/ZVhuHDaxZoPd11tOZ1IpUCAEUZRSRvuusWVLNpau4JkhyI4SnierH9M2Xc6rEF2x5UJJx5mHLA58mcOV8iOqndqRCNHjSJVU7ZPsFITzM3r17GfvmTwR8PJN/923knO5tzXEOHo7q7+yKkhYbIeoF6ZKqHZLcCOFhpk6dSqNGjbhn+H1gNELjm9UOSQhRgaKMItfCfcHNggluJl1SNUWSGyE8yOHDh1m4cCHvvfceRqNR7XCEEOdxZJWsbVNbZMyNEB5k4cKFhIeHc99996kdihDiAiS5qT2S3AjhQSZOnMjWrVvx9vZWOxQhxHkUphWS/oezSyqkeYh0SdUwSW6E8BC7d+9Go9EQGxurdihCiAs4svqsVpv+0mpT0yS5EcIDrF69ms6dO7NmzRq1QxFCVIHsJVW7JLkRws3ZbDZGjx5N7969ufbaa9UORwhxAQUnC8j4MwOA0JahBMUFqRuQB5LZUkK4uc8//5z9+/ezc+dOWQBMCDcgXVK1T1puhHBjdrudqVOnMmTIEDp37qx2OEKIKjjys3RJ1TZpuRHCjVmtVu6++24GDRqkdihCiCrIPZpL5r5MAMLahBHYJFDliDyTJDdCuDFvb28mT56sdhhCiCo6vOLMdgstBrRQMRLPJt1SQripbdu28cILL2A2m9UORQhRBYqicGj5IQA0Gg3Nr5e9pGqL6snNzJkziYuLw8vLix49erBly5bz1s/NzeWxxx4jOjoak8lEq1atWLZsWR1FK0T98eGHH7Jw4UK8vLzUDkUIUQVZ+7LIS8kDIKpLFL4RvipH5LlUTW4WL17M2LFjmTRpEjt27KBTp04kJiaSkZFRYX2r1Ur//v1JTk7m66+/5sCBA8yZM4dGjRrVceRCqOvkyZMsXLiQ+++/H61W9c8oQogqON1qA9DyhpYqRuL5VB1zM23aNB544AFGjRoFwOzZs1m6dClz585l/Pjx5erPnTuX7OxsNmzYgMFgACAuLq4uQxaiXnjzzTfx8vJi9OjRaocihKgCxaFw+GfneButXkt8v3iVI/Jsqn3ks1qtbN++nYSEhDPBaLUkJCSwcePGCs/58ccf6dmzJ4899hiRkZF06NCB119/HbvdXunrWCwW8vPzyzyEcGe5ubnMmTOHp556isBAmWkhhDs4uf0k5izn+LjYq2IxBZhUjsizqdZyk5WVhd1uJzIyskx5ZGQk+/fvr/CcI0eOsGbNGoYNG8ayZcs4dOgQjz76KDabjUmTJlV4ztSpU5kyZUqNxy+EWoKCgvjll19o0UJmWgjhLs7ukpJZUrXPrTrrHQ4HERERfPzxx3Tt2pWhQ4fywgsvMHv27ErPmTBhAnl5ea7HsWPH6jBiIWqW3W5HURR69OhBaGio2uEIIarAbrWTtDoJAIOPgaZXN1U5Is+nWnITFhaGTqcjPT29THl6ejpRUVEVnhMdHU2rVq3Q6XSusrZt25KWlobVaq3wHJPJREBAQJmHEO5qzpw59OjRo9LfdyFE/XNswzGshc7/s3F949B7yRJztU215MZoNNK1a1dWr17tKnM4HKxevZqePXtWeM5VV13FoUOHcDgcrrKDBw8SHR2N0Wis9ZiFUFN+fj6TJ0+mZcuW8vsuhBuRLqm6p2q31NixY5kzZw7z589n3759PPLIIxQVFblmTw0fPpwJEya46j/yyCNkZ2fz5JNPcvDgQZYuXcrrr7/OY489pta3IESdeffdd8nPz+fNN99UOxQhRBVZi6wc/e0oAN7B3jTqLkuX1AVV28aGDh1KZmYmL730EmlpaXTu3Jnly5e7BhmnpKSUWcMjNjaWFStWMGbMGDp27EijRo148sknee6559T6FoSoE2azmZkzZ/LAAw/QuHFjtcMRQlRR0uok7FbnjN5m/Zuh1bvVUFe3pVEURVE7iLqUn59PYGAgeXl5Mv5GuI0dO3Zwyy238NtvvxEfL+tjCOEu/u/B/yN1RyoAg+cPJqJ9hMoRua/qvH/LqCYh3ECXLl1ITk4uM5heCFG/FZwscCU2QXFBhLcLVzmihkPax4So57Zs2cLx48clsRHCzRz86aDr61Y3tUKj0agYTcMiyY0Q9ZjNZmPYsGE8/vjjaocihKgGxaG4khuNRkPLgbKXVF2Sbikh6rFvvvmGQ4cO8dVXX6kdihCiGtJ2pVFwsgCAmO4xsgN4HZOWGyHqsZkzZ9K3b186d+6sdihCiGo4t0tK1C1puRGintq3bx/r1q1jyZIlaocihKiG0pJSjqw6Aji3W4i/VmY41jVpuRGinjIYDDzyyCPcfPPNaocihKiGpF+SsJltgHNtG9luoe7JHReinmrRogWzZs1SOwwhRDVJl5T6pOVGiHrol19+Ye7cuTSwNTaFcHtFGUWc3HISgIBGAUR1rngjaFG7JLkRoh56+eWX+eSTT2RdDCHczN/L/nZ9KGl5Y0v5P6wS6ZYSop7ZvXs3a9euZfHixWqHIoSoBkVROPDjAdfzljfK2jZqkZYbIeqZ9957j8aNGzNkyBC1QxFCVEPqjlTyUvIAiLkihoBGsn+hWiS5EaIeycjIYOHChYwePRqDwaB2OEKIatj/3X7X122HtFUxEiHdUkLUI0FBQcydO5cBAwaoHYoQohos+RaS1iQB4BXoRVzfOHUDauAkuRGiHjEajdx9991qhyGEqKaDSw9it9oB51gbnVE2ulWTJDdCqMFugdTlkLYKLDlgCmbVnzB/ZTrzPvtSdgAXwo0oilKmS6rNkDYqRiNAkhsh6l7aGtgxFszHQHEAoADdSvNpfpUfusxfIaqfujEKIaosY08GOUdyAIjsFElwfLDKEQkZUCxEXUpbA5tGgDkFDIHgFQFeERSV+nCqwEHjEMV5PG2N2pEKIapo//cykLi+qXJyc+utt5Kfnw/AZ599hsViqbWghPBIdouzxaa0CIxhoHXOhlKAzKwsDCZvDD5RzuM7xjrrCyHqNWuRlcM/HwbA6GekWUIzlSMSUI3k5qeffqKoqAiAUaNGkZeXV2tBCeGRUpc7u6IMQXDWqqXFxcXYbDYiIiKc5YYgZ73UFaqFKoSomr+X/k1pSSkALW5oIZtk1hNV/im0adOGCRMmcO2116IoCkuWLCEgoOIFioYPH15jAQrhMdJWOcfYaMuuX+Pj7U3z5s3Raf/5rKE1OOulrYTGsiO4EPWVoijs/Wqv63nbW6VLqr6ocnIze/Zsxo4dy9KlS9FoNEycOLHCPTM0Go0kN0JUxJJTrshqs6HX688kNmUOlq8vhKg/Tm47SU6S8/9p1OVRhLYMVTkicVqVk5tevXqxadMmALRaLQcPHnQ2owshqsZUfgZFamoqer2eRjEx5esbZcaFEPXZX0v+cn3d/s72KkYiznVRs6WSkpIIDw+v6ViE8GxRCaDRgsMGQInFQklxSfnuXYfNWS+qvwpBCiGqojC9kKO/HgXAJ8yH+GvjVY5InK3KLTd//PFHmed79uyptG7Hjh0vPiIhPFX0APCJdU4DN4aRm5OD3qDHz8/vTB1FAVsu+DSB6ETVQhVCnN++b/ahOBTAOdZGq5eVVeqTKic3nTt3RqPRoChKhWNtzma32y85MCE8js4EXabBphE4SjIxm/MJCgrD9b/JYXMmNnpfZz2dScVghRCVsVvtrrVttDqtDCSuh6qcaiYlJXHkyBGSkpL45ptviI+PZ9asWezcuZOdO3cya9YsmjdvzjfffFOb8Qrh3qL6wZXzseojCfXXE+xthZIM58OW52yxuXK+rFAsRD12ZPURirOLAYjrF4dPmI/KEYlzVbnlpmnTpq6v77jjDt5//30GDhzoKuvYsSOxsbG8+OKLDB48uEaDFMKjRPXD65bdeKWucE73tuY4Bw9H9Xd2RUmLjRD12t4lZ6Z/y0Di+umiVhvas2cP8fHlB0/Fx8ezd+/eCs4QQpy2fft2iouLueqqQWhkHRsh3Er6H+mk70kHILRlKFGdo1SOSFTkokZAtW3blqlTp2K1Wl1lVquVqVOn0rat9D0KcT4TJ07kqaeeUjsMIcRF+OPzM5NrOtzd4YJjUIU6LqrlZvbs2QwaNIjGjRu7Zkadnk31008/1Vx0QniYbdu2sXz5chYsWCB/FIVwM/kn8klemwyAT6gPLRJbqBuQqNRFJTfdu3fnyJEjfPHFF+zf7xwxPnToUO6++258fX1rNEAhPMnkyZNp06YNd911l9qhCCGq6c8v/3RN/24/tD06o07liERlLiq5mTp1KpGRkTz44INlyufOnUtmZibPPfdcjQQnhCdJSkpi6dKlzJ07F51O/igK4U4s+RYO/HAAAL1JT9vbZAhGfXZRY24++ugj2rRpU668ffv2zJ49+5KDEsIT+fv78+qrrzJ06FC1QxFCVNO+7/ZhK3auLt7q5lZ4BXqpHJE4n4tquUlLSyM6OrpceXh4OKmpqZcclBCeKCwsjBdeeEHtMIQQ1WS32flrkXMfKY1Gw2V3X6ZyROJCLqrlJjY2lvXr15crX79+PTEVbQAoRAO3fPlyXnjhBUpLS9UORQhRTYd/PkxRZhEATfs0JTA2UOWIxIVcVMvNAw88wFNPPYXNZqNfP+dKqqtXr+bZZ5/l6aefrtEAhfAEb7/9Nmazmddee03tUIQQ1aA4FHbP2+16ftkwabVxBxeV3DzzzDOcOnWKRx991LXWjZeXF8899xwTJkyo0QCFcHd79uxhzZo1fPnll2qHIoSopuS1yeQk5QAQ1TmK6MvLD8kQ9Y9GURTlYk8uLCxk3759eHt707JlS0ym+r9sfH5+PoGBgeTl5REQEKB2OKIBuP/++1m+fDlJSUkYDAa1wxFCVJGiKHx3z3dkHcgC4Ib3byC2V6zKUTVc1Xn/vqiWm9P8/Py44oorLuUSQni0nJwcvvjiC1566SVJbIRwM8c3HnclNmFtwmjcs7HKEYmquqTkRghxfsHBwfzyyy+0bNlS7VCEENWgKAo7Ptnhen75vy+XVcXdiCQ3QtQSh8OBRqPhyiuvVDsUIUQ1pe1MI323c4PM4Phg4vrGqRuQqJaLmgouhLiwBQsW0KNHD8xms9qhCCGq6exWm86jOqPRSquNO5HkRohaUFpayiuvvEJMTAw+Pj5qhyOEqIb0P9I5sfkEAP4x/jRPbK5yRKK6pFtKiFqwYMECDh8+zNdff612KEKIato2e5vr686jOqPVSTuAu5GfmBA1zGaz8corr3DbbbfRuXNntcMRQlTDye0nObHF2WoT0CiA1oNaqxyRuBiS3AhRww4dOoTZbOb5559XOxQhRDUoisK2D8+02nR9qCtavbxNuiPplhKihrVt25bjx4+j0+nUDkUIUQ0nNp8gbVcaAEFxQbQY0ELliMTFkpRUiBqUl5fH8ePH0ev1siaGEG5EURS2ztrqet71oa4yQ8qNSXIjRA368MMPadeuHUVFRWqHIoSohpTfU8jcmwlAaMtQml3XTOWIxKWQ5EaIGmK1Wvnggw8YOnQovr6+aocjhKgixaGwdeZZrTYPS6uNu5PkRogasmjRIk6ePMnYsWPVDkUIUQ0HfzpI9uFsACLaR9D0mqYqRyQulSQ3QtQARVF49913GThwIG3btlU7HCFEFZWWlJZZ16bHkz1kvJwHkNlSQtSAwsJC2rVrx4MPPqh2KEKIatjz5R6KMpxj5Jpe05ToLtEqRyRqgiQ3QtQAf39/vvzyS7XDEEJUQ0luCbvn7QZAo9XQfXR3lSMSNUW6pYS4RPv372fBggWUlpaqHYoQohp2fLIDa5EVgNa3tCa4WbDKEYmaUi+Sm5kzZxIXF4eXlxc9evRgy5YtVTpv0aJFaDQaBg8eXLsBCnEeb7/9NhMmTEBRFLVDEUJUUf7xfPZ+tRcAvZeerg92VTkiUZNUT24WL17M2LFjmTRpEjt27KBTp04kJiaSkZFx3vOSk5MZN24cV199dR1FKkR5aWlpfP755zzxxBMYDAa1wxFCVNGm6ZtwlDoAuGzYZfiGy/INnkT15GbatGk88MADjBo1inbt2jF79mx8fHyYO3dupefY7XaGDRvGlClTaNZMFloS6pk1axYGg0EGEgvhRk5sOUHy2mQAfMJ86Dyis6rxiJqnanJjtVrZvn07CQkJrjKtVktCQgIbN26s9LyXX36ZiIgI7rvvvroIU4gKmc1mZs2axX333UdQUJDa4QghqsBhd7DhnQ2u590f747BR1pdPY2qs6WysrKw2+1ERkaWKY+MjGT//v0VnrNu3To++eQTdu3aVaXXsFgsWCwW1/P8/PyLjleIs+n1et5991369OmjdihCiCra980+co7kAM4F+1re0FLliERtUL1bqjoKCgq49957mTNnDmFhYVU6Z+rUqQQGBroesbGxtRylaCiMRiMjRowgLi5O7VCEEFVQkldSZsG+nuN6yjYLHkrV5CYsLAydTkd6enqZ8vT0dKKiosrVP3z4MMnJyQwaNAi9Xo9er+ezzz7jxx9/RK/Xc/jw4XLnTJgwgby8PNfj2LFjtfb9iIYjMzOTESNGcPToUbVDEUJU0faPtmPJd7bktxzYksjLIi9whnBXqiY3RqORrl27snr1aleZw+Fg9erV9OzZs1z9Nm3asGfPHnbt2uV63HzzzVx77bXs2rWrwlYZk8lEQEBAmYcQl2r+/Pl8+eWX+Pj4qB2KEKIKsg9ls/dr59Rvg7dBFuzzcKqvUDx27FhGjBhBt27d6N69O9OnT6eoqIhRo0YBMHz4cBo1asTUqVPx8vKiQ4cOZc4/PZDz3HIhaovVauU///kP99xzD+Hh4WqHI4S4AEVR2PDOBhSHcy2qzqM64xshU789merJzdChQ8nMzOSll14iLS2Nzp07s3z5ctcg45SUFLRatxoaJDzcjBkzSEtL45lnnlE7FCFEFRxecZiT204C4B/jT8d7OqockahtGqWBLauan59PYGAgeXl50kUlqq24uJgmTZowdOhQZsyYoXY4QogLsBRYWHLbEoqziwFI/E8iTa9uqnJU4mJU5/1b9ZYbIdyJt7c3f/zxB97e3mqHIoSogm0fbnMlNk37NJXEpoGQ5EaIKiotLcVutxMdHa12KEKIKsjcl+kaRKz30nPVM1epHJGoKzKYRYjK2C1w/AfY9jisv4fkrwdx/4BwsjJOqB2ZEOICFIfCuqnrXIOIuz7YFb8oP5WjEnVFWm6EqEjaGtgxFszHQHFurhdZXMiMeyFw243QZRpE9VM5SCFEZfZ9u4/MvZkABDcLpsNdMqO2IZGWGyHOlbYGNo0AcwoYAsErAqsumJOnStGYgpzlm0Y46wkh6p3i7GK2ztzqet57fG90Bp2KEYm6JsmNEGezW5wtNqVFYAwDrXNDvby8PLQ6LX7+wc7y0iJnPbvlAhcUQtS19W+vx1Lg/L/Z6qZWRHeRcXINjSQ3QpwtdbmzK8oQBJoze85YLBYCAgLQajTOckOQs17qCtVCFUKUl7w2mSMrjwDgFehFjyd7qByRUIMkN0KcLW2Vc4zNPy02pzVu1IiIs1cj1hqc9dJW1nGAQojKWAosrHtjnet5z3E98Q6WZRsaIkluhDibJad8kdWKAmg0FewebC1fXwihjs3vbcacZQYg9qpYWgxooXJEQi2S3AhxNlNwmadWm43kpGQKCwsrrm8MrrhcCFGnTmw9wf7v9wNg8DFw9fNXV/yBRDQIktwIcbaoBNBowWEDIC83F61Oi6/vOZvsOWzOelH9VQhSCHG20pJSfn/1d9fzHk/0wC9S1rRpyCS5EeJs0QPAJxZsuSgOB3n5+QSeHkh8mqKALddZLzpRtVCFEE5bP9xK/ol8AKIuj6LtrW1VjkioTZIbIc6mMzkX6NP7Yi1KRaPYCQwKOnPcYQNrFuh9nfV0JtVCFUJA6o5U/lz4JwA6o44+L/ZBo5XuqIZOkhshzhXVD66cT5EjhIggIyZHLpRkOB+2PPBpAlfOlxWKhVCZtcjK2slrURTnFgvdHulGYJNAlaMS9YFsvyBERaL6EXLX3851bNJWOmdFGYOdY2yiE6XFRoh6YOO0jRScLAAguks0HYd1VDkiUV9IciNEBVatWkXbtm1p1PhmaHyz2uEIIc5x9LejHPjhAOCcHdV3cl/pjhIu0i0lxDksFgt33XUX77zzjtqhCCEqUJxTzG+v/uZ63vPpnvjH+KsYkahvJLkR4hz/93//R1ZWFg8++KDaoQghzqE4FNZOXktxdjEATa9pSuubW6sclahvJLkR4hyLFi2ia9eutG0r00mFqG/2fLmHY+uPAeAd4s3VL8hifaI8SW6EOEtBQQFLly5l6NChaocihDhH5t5MtnywxfW875S++IT6qBeQqLckuRHiLGazmZtvvpk777xT7VCEEGexFlpZPWE1jlIHAJ1GdCK2Z6zKUYn6SmZLCXGWyMhIFi9erHYYQoizKIrC76//7lqFOPKySK545AqVoxL1mbTcCPGP3377jfnz57sWBBNC1A9/fvknh38+DIDRz0i/1/qh1cvbl6ic/HYI8Y933nmHmTNnyuBEIeqRE1tPsGn6JtfzPpP6yLRvcUGS3AgBpKamsmzZMkaOHKl2KEKIfxSkFrB6wmoUh7M19fJ/X078tfEqRyXcgSQ3QgAff/wxXl5eDBs2TO1QhBBAqaWUlc+spCS3BIDYXrF0e7ibylEJdyHJjWjwbDYbH330Effeey+BgbLpnhBqUxwKv075laz9WQAENAqg36v9ZHsFUWUyW0oI4I033qB79+5qhyGEALbN3uYaQKz30nP9u9djCpDNakXVSXIjGjyDwcDw4cPVDkMIARz48QA75+4EQKPR0O+1foS0CFE5KuFupFtKNGg///wz//rXvygqKlI7FCEavOObj/P7a7+7nvd8uidxfeLUC0i4LWm5EQ2WoihMnDgRg8GAj48s4S6EmrIOZLHq2VU47M4ViDv8qwMd/tVB5aiEu5LkRjRYq1evZuvWraxYsULWthFCRblHc/nf6P9hLbICzp2+e47tqXJUwp1Jt5RosKZOnUqXLl3o37+/2qEI0WAVphWy7NFlFOcUAxDZMZJ+r8nMKHFppOVGNEjJycn8/vvvfPnll9JqI4RKirOLWfroUgrTCwEIbRXKgPcGYPA2qByZcHeS3IgGKS4ujiNHjhAdHa12KEI0SCW5JSwbvYy8lDwAApsEMnDGQEz+MuVbXDrplhINTnp6OiUlJTRu3BidTqd2OEI0OCW5JSx9dCmnDp4CwDfCl4EzB+Id4q1yZMJTSHIjGpzHHntMxtkIoZJzExufMB9umn0T/tGyGaaoOZLciAblr7/+4ptvvmHEiBFqhyJEg1NRYjPo40EENpFtT0TNkjE3okF57bXXaNKkiaxILEQdK84pZtljyySxEXVCkhvRYBw8eJDFixfzwQcfYDQa1Q5HiAajKLOIZY8uIycpB5DERtQ+SW5Eg5GTk0P//v3597//rXYoQjQYBScLWPrIUvJP5APgG+7LTR/dJImNqFUaRVEUtYOoS/n5+QQGBpKXl0dAQIDa4QghhMfKScph2aPLKMp07t3mH+PvHDwcI4OHRfVV5/1bBhSLBuGjjz5i586daochRIORdSCL/3vg/1yJTXB8MDf/92ZJbESdkORGeLyTJ0/y+OOP8/PPP6sdihANQvof6fz00E+U5JYAENYmjEFzBuEb4atyZKKhkDE3wuPNmDEDLy8vHn74YbVDEcLjndhygp+f/hlbsQ2AyE6R3PDeDRj9ZBC/qDuS3AiPVlRUxEcffcT9999PYKAMYBSiNh397SirnluF3WYHoFH3Rlz/7vWyV5Soc5LcCI/22WefkZubyxNPPKF2KEJ4tL+X/c3ayWtRHM45Kk37NCVhagI6o2xxIuqeJDfCo91yyy0EBgYSFxendihCeKw/vviDTf/Z5HreYkAL+k7ui1YvwzqFOiS5ER5LURRiYmK4++671Q5FCI+kKApbPtjC7s92u8ra3d6Oq569Co1Wo2JkoqGTtFp4JIvFQteuXfm///s/tUMRwiM5Sh38OuXXMolN14e6ctVzktgI9UnLjfBIH330Ebt376Zly5ZqhyKEx7EV21g1fhXH1h8DQKPV0Ht8b9re2lblyIRwkuRGeJxTp04xZcoURowYQZs2bdQORwiPUpxdzIqxK8j4MwMAnUFHv9f6Ed8vXuXIhDhDkhvhcV566SVsNhuvv/662qEI4VGyD2ez4qkVFKQWAGD0NXL9tOuJ6RqjcmRClCXJjfAopaWl/Pnnn0yePJmoqCi1wxHCYxzbcIxV41dhMzsX5/ON8GXA9AGEtgpVOTIhypPkRrg3uwVSl0PaKrDkoDcFs/bzMTgir1c7MiE8xl9f/cWGtze41rAJbxdO4rREfMJ8VI5MiIpJciPcV9oa2DEWzMdAcWArLQXAkLwQnU8sdJkGUf1UDlII92W32ln35joO/HDAVRbfL55rX74WvZe8fYj6q15MBZ85cyZxcXF4eXnRo0cPtmzZUmndOXPmcPXVVxMcHExwcDAJCQnnrS88VNoa2DQCzClgCASvCFKzS0nNsTufm1Ocx9PWqB2pEG6p4GQBP/z7hzKJTacRnUh4I0ESG1HvqZ7cLF68mLFjxzJp0iR27NhBp06dSExMJCMjo8L6a9eu5a677uKXX35h48aNxMbGcv3113PixIk6jlyoxm5xttiUFoExDLQGiktKKC4uITg4GLQGZ3lpkbOe3aJ2xEK4lZT1KXx7z7dk7c8CQG/Sc+3L19Lj8R6yho1wCxpFURQ1A+jRowdXXHEFM2bMAMDhcBAbG8vjjz/O+PHjL3i+3W4nODiYGTNmMHz48AvWz8/PJzAwkLy8PAICAi45fqGC4z/Apn87W2i0zg35jh8/jq20lLi4OFx/eh02sOXBlZ9C45tVC1cId1FqKWXze5v5a8lfrrLA2EAS3kogtKUMHBbqqs77t6otN1arle3bt5OQkOAq02q1JCQksHHjxipdw2w2Y7PZCAkJqa0wRX2TtgoUhyuxMRcXU1RkJiw0lDKfKbUGZ720laqEKYQ7OXXwFN/d812ZxKZpn6YMWTBEEhvhdlTtOM3KysJutxMZGVmmPDIykv3791fpGs899xwxMTFlEqSzWSwWLJYz3RL5+fkXH7CoHyw5ZZ6aTCYiIiPw8/evuL41p+JyIQSlllJ2z9/Nzrk7cZQ6ANAZdVz51JW0u6MdGo10Qwn349ajwt544w0WLVrE2rVr8fLyqrDO1KlTmTJlSh1HJmqVKdj1pQLotFqCg4Iqr28MrvyYEA1YyvoUNry1gfwTZz70hbYKpd+r/QhuJv9vhPtStVsqLCwMnU5Henp6mfL09PQLLsD2zjvv8MYbb/Dzzz/TsWPHSutNmDCBvLw81+PYsWM1ErtQUVQCaLQoDhvHjh0jNy+v4noOG2i0ENW/buMTop4rTCvk52d+ZvmTy12JjVanpfOozgyeP1gSG+H2VG25MRqNdO3aldWrVzN48GDAOaB49erVjB49utLz3nrrLV577TVWrFhBt27dzvsaJpMJk8lUk2ELtUUPAJ9YbLmHKDYXExpawXgARQFbLvg0gejEOg9RiPrIbrOzZ+EedszZQWlJqas8uks0vcf3lqRGeAzVu6XGjh3LiBEj6NatG927d2f69OkUFRUxatQoAIYPH06jRo2YOnUqAG+++SYvvfQSCxcuJC4ujrS0NAD8/Pzw8/NT7fsQdUhnwt75bbJ/HEh0iB5fL0PZ4w6bM7HR+zoX8tNJcivEyW0nWf/menKSzoxB8w7x5soxV9JiQAsZWyM8iurJzdChQ8nMzOSll14iLS2Nzp07s3z5ctcg45SUFLTaM71nH374IVarldtvv73MdSZNmsTkyZPrMnShoiW/ZjHng1L+b0oLsGU7Z0WdptE6W2xkhWIhMGeZ2TR9E4eWH3KVabQa2t3Rjm4Pd8PkL8m/8Dyqr3NT12SdG8/w6KOPkpKSwk8/fAOpK5zTva05zsHDUf2dXVHSYiMaMIfdwd6v9rLtw21Yi6yu8ogOEfSe0Juw1mEqRidE9VXn/VuSG+G2iouL8fb2VjsMIeqd9D3prJu6jlMHT7nKTAEmuj/enTa3tJFVhoVbqs77t+rdUkJUh8ViYdWqVQwcOFASGyHOUZJbwuYPNpfZDwqgzeA2dB/dHa+gipfMEMLTSHIj3MpHH33EmDFjOHjwIM2bN1c7HCHqBcWhcODHA2x+fzOW/DOLloa2CqX3+N5Edow8z9lCeB5JboTbyMzMZNKkSYwaNUoSGyH+kXUgi/VvrCd9z5n1woy+Rro90o12d7RDq1N9f2Qh6pwkN8JtTJw4EcC1LIAQDZm10MrWD7ey96u9KI4zQydb3NCCK5+8Ep8wHxWjE0JdktwIt7Bnzx7++9//Mm3aNMLDw9UORwjVKA6Fg0sPsuWDLRRnF7vKg+ODueq5q4jpFqNidELUD5LcCLfQqlUrZsyYwf333692KEKoJuPPDDa8vYGMvzJcZXovPV0e6MJld1+GzqBTMToh6g9JbkS9Z7VaMZlMPPLII2qHIoQqzFlmNn+wmb+X/l2mPL5fPD3H9sQvSlZnF+JsktyIes1isXD55Zfz9NNPc99996kdjhB1yma28eeiP9k1bxc2s81VHtwsmF7jetGoeyMVoxOi/pLkRtRrM2bM4ODBg/Ts2VPtUISoM3arnX3f7WPnJzvLjKsx+Zvo+nBX2t0us6CEOB9JbkS9lZGRwcsvv8wjjzxCu3bt1A5HiFpXWlLKgR8PsPuz3RSmFbrKNVoNbYa04YpHrpCF+ISoAkluRL310ksvodVqZUNU4fEs+Rb++uov/lr0F8U5xWWONUtoRrdHuhHUNEid4IRwQ5LciHrJ4XBQWFjI5MmTCQ0NVTscIWqcoihk7s1k79d7ObziMHarvczx2F6xXPHoFYS1kQ0uhaguSW5EvaTVavn8889pYPu6igbAZrZxaMUh9n29j6wDWWWOabQamvVvRucRnQltJUm9EBdLkhtR7yxcuBCAu+++G41Gdi8WniEnKYe9X+/l75/+xlpkLXPM6Gek1U2t6HBXBwIanX+3YyHEhUlyI+qVQ4cO8fDDD3PHHXdw9913qx2OEJfEUeog+ddk9n61l5PbTpY7Ht4unHa3t6NZ/2YYvA0qRCiEZ5LkRtQbFouFO++8k8jISP7zn/+oHY4QF60oo4j93+9n37f7MGeZyxzTm/Q0H9Ccdre1I7ydbCUiRG2Q5EbUG+PGjeOvv/5i06ZNBARI07xwL4qicHLbSfZ+tZfktcllNrMECGwSSLs72tHqxlaYAkwqRSlEwyDJjagXzGYz69ev5z//+Q+XX3652uEIUWWWAgt/L/2bvV/vJTc5t8wxjVZD0z5NaX9He2KuiJExZELUEUluRL3g4+PDpk2bMBhk3IFwD6cOnuKvr/7i0P8OUVpSWuaYT6gPbYa0oe2tbfGN8FUpQiEaLkluhOqmTZtG37596dKli9qhCHFedqudI6uOsPfrvaT/kV7ueHSXaNrd0Y64vnGyQ7cQKpLkRqjq119/5emnn+bdd9+V5EbUW3nH8tj//X4O/HCAktySMscMPgZa3dSKdre3I7hZsEoRCiHOplEa2Cpp+fn5BAYGkpeXJ4NWVVZYWEjHjh1p1KgRa9euRaeTT7qi/ii1lJK0Oon93+8ndUdqueMhzUNod2c7Wt7QEoOPdKcKUduq8/4tLTdCNc888wzp6emsXLlSEhtRLzhKHZzcdpIjq45wZNURrIVlF9vT6rXEXxdP+zvaE9kpUgYIC1FPSXIjVJGTk8N3333HW2+9RfPmzdUORzRglnwLJ7ae4Nj6YySvTcaSbylXJ7BJIG0Gt6HVTa3wDvFWIUohRHVIciNUERwczO7du4mIiFA7FNHAOEodpO9J5/im45zYdILMfZnl1qQB0HvpaZbQjDaD20grjRBuRpIbUXvsFkhdDmmrwJIDpmCUyOuYOm8n/37gEaKiotSOUDQQ+cfzObbxGMc3HufktpPYzLYK6xm8DTS5ugnNEprRuGdj2RJBCDclyY2oHWlrYMdYMB8DxeEqthz4lIH6Io5v8yXqpmdVDFB4MpvZxsltJ10JTf7x/ErrhjQPodGVjWh8ZWOiu0SjN8mfRSHcnfwvFjUvbQ1sGgGlRWAIAq3z029xSQmpWSm0iDHiV/oBpHWDqH7qxio8hqXAQsrvKRxZfYTjG49jt9orrOcd7E2jHs5kplGPRviGyyJ7QngaSW5EzbJbnC02pUVgDIN/xilYrVZOHD+O0eiFT1BjsJ1y1kvcDDrZZ0dcnJK8Eo7+epQjq45wYssJHKWOcnW0Oi2RnSJp3LMxsT1jCW0VikYr42eE8GSS3Iialbrc2RVlCHIlNuBstdEbDDRq1AitVus8bj4GqSug8c2qhSvcj/mUmeS1ySStTuLktpMVDgb2CfWhaZ+mNOndhJhuMbIOjRANjCQ3omalrXKOsfmnK0oBNEBgQAABAQG40h2twVkvbaUkN+KCCk4WkLw2mSOrj5DxRwYVrT3qG+FL/HXxNLuuGZEdI6V1RogGTJIbUbMsOWWeZp/Kxu6wEx4eToVvNdacikpFA2crtpG6I5UTm09wfNNxco5U/HviH+NPs4RmxPeLJ7xduCQ0QghAkhtR00xn9tbJy88nKyuLsLDQihMbAKPsxdPQ2cw2Tv19iqx9WWTtzyJrXxY5STkVdjcBBDcLJu7aOJpd14yQliGy/owQohxJbkTNikqA5IUUFuSSlpZBYFAgIaGh5es5bKDRQlT/uo9RqMZaZOXUgVNk7c8ic18mp/afIjc5t8JuptM0Wg3h7cJpek1T4vvFExQXVHcBCyHckiQ3omZFD6BICcFaeAh/fz8iIyPLt9ooCthywacJRCeqEKSoK+ZTZlK3p3Jy+0nSdqSRk3ThbkitTktw82AiOkTQ+MrGxHSLwRQgM+qEEFUnyY2oWToT+h7v4/j1LoK9tGgcNtfgYsDZYmPLBb0vdJkm08A9TFFmEak7Uknd7nzkHs09b32tXktoy1BC24QS1iaM8LbhhLQIQWeUjVSFEBdPkhtRY9LS0khPT6dTpxsw9f+2whWK0WidLTZdpskCfh6gKKOIk9tPuhKavJS8SutqdVpCW4cS1taZxIS1CSO4eTA6gyQyQoiaJcmNqBGKojB27FjWrFnD0aNHMUX1cy7Ql7rCOd3bmuMcPBzV39kVJS02bkdRFApOFpC+O92Z0GxPPe+2BlqdlvD24cR0iyG6SzSRHSNlvRkhRJ2Q5EbUiP/85z98+eWXfP7555hM/yQuOpNzDRtZx8btKA6FwrRC8lLyyNqfRfof6WT8mUFxdnGl52j1WiI6RBDdJZqYbjFEXBYhG08KIVQhyY24ZIsWLWLcuHGMHz+eYcOGqR2OqKKzE5i8Y3nkH8t3/puST/6J/Aq3MjibzqAj4jJnMhPdNZrIyyLRe8mfFCGE+jTK+eZgeqD8/HwCAwPJy8sjICBA7XDcnsVioW3btvTq1YvPPvvMubWCqFdsZhu5ybnkJOWQm5RLzpEc8o7mVSmBOZvJ30TEZRHOhObyaCIui5AdtIUQdaY679/yl0lcNKvVislkYsOGDc4p37KYmuocdgc5h3PI+DPD9chNOv86MufSGXUExgYS0CSAwNhAguKCiLgsgqCmQbICsBDCLUhyI6pNURTefvttFi9ezK+//kpUVJTaITVIiqJQlF5Exl9nEpmsfVmUlpRe8Fy9SU9AbAABsc4EJiA2gMAmgQTGBuIT5iNJjBDCrUlyI6rl+PHjPPzwwyxdupSJEyfi6+urdkgNgt1mJ/9YPjlJOeQcyXGu8PtnJuZT5vOep9VpCWkZQmirUIKbBRMUH0RwfDB+UX6SwAghPJYkN6LKFi9ezEMPPYSvry/ff/89t9xyi9ohuT1FUSjJLcGcaaYoswhzlhlzphlzlvN5cVaxq7yyvZbO5h/tT3iHcCI6RBDRIYKwNmEyLkYI0eDIX72Gzm6B1OWQtsq5o7cp2Lk/VPSAcmvRaLVabrnlFqZPn05wsGx4WVWKomDONHPq71NkH8om53AOBakFFKUXYc40Y7fZL+q6Rj8j4e3DiWgf4UpmvEO8azh6IYRwPzJbqiFLW3OeVYRjUS5/l3WHjCxdupSpU6fKgOEqsJltZB/KLvv4OxtLgeWirqfRaPAK9sI33JfApoGurqWQFiEExgZK15IQosGozvu3JDcNVdoa2DQCSovAEFRu/yeHJYesvGL+Nd1Clu4yfv31V2mt+YeiKFgLrOQm55Z5ZB/KpuBkQZWvYwow4Rvh63r4hPvgE+aDb7gvPmHOr71DvdHqZHq9EELIVHBxfnaLs8WmtAiMYXBOi0xRsZW09AJCfB1892Ic/rdtRmvw3O4Ou9WO+ZSZ4lPFFOcUY8mzUJJXQkluifPr3HO+ziup1vowvhG+hLQIKfMIbBIoC94JIUQtkb+uDVHqcmdXlCGoTGKjABqcC/MZjSb8g0PRK/mQvtKtt1A4PWU6LyWP3KO5zhV5j+ZRmFqI+ZQZS/7FdRmdy+BtILh5MCEtnQlMaMtQQlqEYAqQfbSEEKIuSXLTEKWtco6x0RpQgKLCQgoKC9HrdISHhxMcEkJwSAgagJIC58aXbpDcOOwOCtMKyTlyZiXenCM55CbnYjPbLvn6OqMOryAv1yMgNoCguCDXwy9SplcLIUR9IMlNQ2TJcSY1RUVkZWVhKbFgNBoI+mdMTbm3Z2tOnYWmKAqlxaVYC61Y8i1Yi6zYimzYzM6Htcjq/LrI5jpWlFFEYWohhemFVZoufZrepHeOawnzxifUOcbl7OTFK8gLU6DJ+XWgl3QjCSGEm5C/1vVdNaZqV5kpGIfDwYkTJ/D29ia2SSw+3ucZU2OsuYHEDruDovQi8o/nk3/8n40aj+dTcLyAoswirAVWHPaqj2e5EI1Gg38jf4Li/2lhaRrkXIm3aSDeId4yA0wIITyQJDf1WWVTtZMXgk8sdJkGUf0ueJmioiI2bNjA2rVr2bVrFz/Oug9d8kLimjTC6OVbvqXmNIfNOS08qn+VQ1YcCsU5xc6WlLRCClILKEwtJO9YHgXHCyg4WVCjyctppgAT/tH++MX4ERz/z0q8zYIJiguSReyEEKKBkb/69dUFpmpjTnEev3J+pQlOaWkpr7zyCtOmTaOwsJDw8HCuu+46sr26E+4Ti8mcAoqPa1Cx4lBwlDpw2BUUuwOtko3VEcmeb4Kwmn+ntKS07KO4tHxZSWm1uoZO0+q0+Eb6YgowYfQ3YvI3YfQzYvQzYvA1YPAxYPSt+GufUB8MPoYLv4gQQogGQda5qY/sFljRw5nAVDBVGwBFAWsW+DSBxM2gM6EoClu2bOF///sfkyZNQqPRcNVVV9GjRw8eeOABmsU2oyjdOT7FcXwV0bYJaBUzFosPdpvWlZRotXaMJjOlpSbWLh/JyWNtauTbMngbnJs1Ni7/8I30lfVchBBCVMrtFvGbOXMmb7/9NmlpaXTq1IkPPviA7t27V1r/q6++4sUXXyQ5OZmWLVvy5ptvMnDgwCq9llskN8d/gE3/RjEEoig6FIeC4nC2rCiKAg4F50/Nik4p4qvfb2DdthCOJR8j91QuQX5B3DfqPrwMXthLnGu4FKYVYi20lnmZmNj99OzzFb7+OWg0DpxDiRUURUtRQTAbf73jvImNRqNB76Uv89CZdHiHeju7iKL88Iv2wz/an4DGAXgFe8kYFyGEEBfFrZKbxYsXM3z4cGbPnk2PHj2YPn06X331FQcOHCAiIqJc/Q0bNnDNNdcwdepUbrrpJhYuXMibb77Jjh076NChwwVfrz4lN5YCC4WpZ8alnN5vqFnQf2gUuZbiIv9/aio4/klsFMWBw+FMRIxGI94++Wxe34ofllyHl5cXwcHB1fq+tDobsfF7iW9zCJ9ACxpTCMWm3lh8+2IM8D/TNeRjQO+tx+BtcCUyWoNWkhUhhBB1wq2Smx49enDFFVcwY8YMABwOB7GxsTz++OOMHz++XP2hQ4dSVFTETz/95Cq78sor6dy5M7Nnz77g69VWcnNiywky92Wi2E+PW3E4v/7nX1uxzbXCrTnH2ZJiKbCg1WrRarVYSiwUFhVitVq5+c4ltG53GHORH3q9HkVRsFicC81ptBq0Gi1arQadTo+3TwEpRzrwy/L7Ko1NZ9DhG+mLX5Sf69+ARgH4RTv/9Y3wRauXLiEhhBD1l9tsv2C1Wtm+fTsTJkxwlWm1WhISEti4cWOF52zcuJGxY8eWKUtMTOT777+vsL7FYnElBuC8ObUheW0yOxbs4MiRIyiKgsPhQFEUNBqNq0Vp7969FBcXlzmvefPmBAUFkZObw4kTJ9Dr9RQWGEEDWoMWg7cBtKD31aM36NBqtaDVOIfhaDToHMVEX9mOQbcOQmfSoTPq0JucrSp6kx6dUYfBxyCLywkhhGgwVE1usrKysNvtREZGlimPjIxk//79FZ6TlpZWYf20tLQK60+dOpUpU6bUTMDnodU7W2B8fHzQaDRotVrXv6dFR0djt9udSUugAVOYibjL4ohuFY0xxIhPpA/h8eH42laj23Y/Pga/srOkzuWwgU2PX+ch+DWOrvXvUQghhHAHHj8VfMKECWVaevLz84mNja3x12k1qBXRXaLR6DRodVq0eq3ra41Og96kxxRowhRgQu+lP/9YFftA2Bt74dlStlznbKnoxBr/foQQQgh3pWpyExYWhk6nIz09vUx5eno6UVFRFZ4TFRVVrfomkwmTqfY3LgxtGUpoy9CauZjO5Fygb9MI53Tvita5seWC3tdZ72JXKhZCCCE8kKqjSI1GI127dmX16tWuMofDwerVq+nZs2eF5/Ts2bNMfYCVK1dWWt9tRfVzLtDn0wRseVCSceZhy3OWn2cBPyGEEKKhUr1bauzYsYwYMYJu3brRvXt3pk+fTlFREaNGjQJg+PDhNGrUiKlTpwLw5JNP0qdPH959911uvPFGFi1axLZt2/j444/V/DZqR1Q/5wJ9qSucO3Nbc5z7PEX1d3ZFSYuNEEIIUY7qyc3QoUPJzMzkpZdeIi0tjc6dO7N8+XLXoOGUlJQyg3J79erFwoULmThxIs8//zwtW7bk+++/r9IaN25JZ4LGNzsfQgghhLgg1de5qWv1aRE/IYQQQlRNdd6/ZeU2IYQQQngUSW6EEEII4VEkuRFCCCGER5HkRgghhBAeRZIbIYQQQngUSW6EEEII4VEkuRFCCCGER5HkRgghhBAeRZIbIYQQQngU1bdfqGunF2TOz89XORIhhBBCVNXp9+2qbKzQ4JKbgoICAGJjY1WORAghhBDVVVBQQGBg4HnrNLi9pRwOBydPnsTf3x+NRlOj187Pzyc2NpZjx47JvlW1SO5z3ZD7XDfkPtcdudd1o7bus6IoFBQUEBMTU2ZD7Yo0uJYbrVZL48aNa/U1AgIC5D9OHZD7XDfkPtcNuc91R+513aiN+3yhFpvTZECxEEIIITyKJDdCCCGE8CiS3NQgk8nEpEmTMJlMaofi0eQ+1w25z3VD7nPdkXtdN+rDfW5wA4qFEEII4dmk5UYIIYQQHkWSGyGEEEJ4FEluhBBCCOFRJLkRQgghhEeR5KaaZs6cSVxcHF5eXvTo0YMtW7act/5XX31FmzZt8PLy4rLLLmPZsmV1FKl7q859njNnDldffTXBwcEEBweTkJBwwZ+LcKru7/NpixYtQqPRMHjw4NoN0ENU9z7n5uby2GOPER0djclkolWrVvK3owqqe5+nT59O69at8fb2JjY2ljFjxlBSUlJH0bqn3377jUGDBhETE4NGo+H777+/4Dlr166lS5cumEwmWrRowbx582o9ThRRZYsWLVKMRqMyd+5c5a+//lIeeOABJSgoSElPT6+w/vr16xWdTqe89dZbyt69e5WJEycqBoNB2bNnTx1H7l6qe5/vvvtuZebMmcrOnTuVffv2KSNHjlQCAwOV48eP13Hk7qW69/m0pKQkpVGjRsrVV1+t3HLLLXUTrBur7n22WCxKt27dlIEDByrr1q1TkpKSlLVr1yq7du2q48jdS3Xv8xdffKGYTCbliy++UJKSkpQVK1Yo0dHRypgxY+o4cveybNky5YUXXlC+/fZbBVC+++6789Y/cuSI4uPjo4wdO1bZu3ev8sEHHyg6nU5Zvnx5rcYpyU01dO/eXXnsscdcz+12uxITE6NMnTq1wvp33nmncuONN5Yp69Gjh/LQQw/Vapzurrr3+VylpaWKv7+/Mn/+/NoK0SNczH0uLS1VevXqpfz3v/9VRowYIclNFVT3Pn/44YdKs2bNFKvVWlcheoTq3ufHHntM6devX5mysWPHKldddVWtxulJqpLcPPvss0r79u3LlA0dOlRJTEysxcgURbqlqshqtbJ9+3YSEhJcZVqtloSEBDZu3FjhORs3bixTHyAxMbHS+uLi7vO5zGYzNpuNkJCQ2grT7V3sfX755ZeJiIjgvvvuq4sw3d7F3Ocff/yRnj178thjjxEZGUmHDh14/fXXsdvtdRW227mY+9yrVy+2b9/u6ro6cuQIy5YtY+DAgXUSc0Oh1vtgg9s482JlZWVht9uJjIwsUx4ZGcn+/fsrPCctLa3C+mlpabUWp7u7mPt8rueee46YmJhy/6HEGRdzn9etW8cnn3zCrl276iBCz3Ax9/nIkSOsWbOGYcOGsWzZMg4dOsSjjz6KzWZj0qRJdRG227mY+3z33XeTlZVF7969URSF0tJSHn74YZ5//vm6CLnBqOx9MD8/n+LiYry9vWvldaXlRniUN954g0WLFvHdd9/h5eWldjgeo6CggHvvvZc5c+YQFhamdjgezeFwEBERwccff0zXrl0ZOnQoL7zwArNnz1Y7NI+ydu1aXn/9dWbNmsWOHTv49ttvWbp0Ka+88oraoYkaIC03VRQWFoZOpyM9Pb1MeXp6OlFRURWeExUVVa364uLu82nvvPMOb7zxBqtWraJjx461Gabbq+59Pnz4MMnJyQwaNMhV5nA4ANDr9Rw4cIDmzZvXbtBu6GJ+n6OjozEYDOh0OldZ27ZtSUtLw2q1YjQaazVmd3Qx9/nFF1/k3nvv5f777wfgsssuo6ioiAcffJAXXngBrVY++9eEyt4HAwICaq3VBqTlpsqMRiNdu3Zl9erVrjKHw8Hq1avp2bNnhef07NmzTH2AlStXVlpfXNx9Bnjrrbd45ZVXWL58Od26dauLUN1ade9zmzZt2LNnD7t27XI9br75Zq699lp27dpFbGxsXYbvNi7m9/mqq67i0KFDruQR4ODBg0RHR0tiU4mLuc9ms7lcAnM6oVRky8Uao9r7YK0OV/YwixYtUkwmkzJv3jxl7969yoMPPqgEBQUpaWlpiqIoyr333quMHz/eVX/9+vWKXq9X3nnnHWXfvn3KpEmTZCp4FVT3Pr/xxhuK0WhUvv76ayU1NdX1KCgoUOtbcAvVvc/nktlSVVPd+5ySkqL4+/sro0ePVg4cOKD89NNPSkREhPLqq6+q9S24here50mTJin+/v7Kl19+qRw5ckT5+eeflebNmyt33nmnWt+CWygoKFB27typ7Ny5UwGUadOmKTt37lSOHj2qKIqijB8/Xrn33ntd9U9PBX/mmWeUffv2KTNnzpSp4PXRBx98oDRp0kQxGo1K9+7dlU2bNrmO9enTRxkxYkSZ+kuWLFFatWqlGI1GpX379srSpUvrOGL3VJ373LRpUwUo95g0aVLdB+5mqvv7fDZJbqquuvd5w4YNSo8ePRSTyaQ0a9ZMee2115TS0tI6jtr9VOc+22w2ZfLkyUrz5s0VLy8vJTY2Vnn00UeVnJycug/cjfzyyy8V/r09fW9HjBih9OnTp9w5nTt3VoxGo9KsWTPl008/rfU4NYoi7W9CCCGE8Bwy5kYIIYQQHkWSGyGEEEJ4FEluhBBCCOFRJLkRQgghhEeR5EYIIYQQHkWSGyGEEEJ4FEluhBBCCOFRJLkRQgghhEeR5EYIIYQQHkWSGyGEEEJ4FEluhBBuLzMzk6ioKF5//XVX2YYNGzAajeV2JBZCeD7ZW0oI4RGWLVvG4MGD2bBhA61bt6Zz587ccsstTJs2Te3QhBB1TJIbIYTHeOyxx1i1ahXdunVjz549bN26FZPJpHZYQog6JsmNEMJjFBcX06FDB44dO8b27du57LLL1A5JCKECGXMjhPAYhw8f5uTJkzgcDpKTk9UORwihEmm5EUJ4BKvVSvfu3encuTOtW7dm+vTp7Nmzh4iICLVDE0LUMUluhBAe4ZlnnuHrr79m9+7d+Pn50adPHwIDA/npp5/UDk0IUcekW0oI4fbWrl3L9OnTWbBgAQEBAWi1WhYsWMDvv//Ohx9+qHZ4Qog6Ji03QgghhPAo0nIjhBBCCI8iyY0QQgghPIokN0IIIYTwKJLcCCGEEMKjSHIjhBBCCI8iyY0QQgghPIokN0IIIYTwKJLcCCGEEMKjSHIjhBBCCI8iyY0QQgghPIokN0IIIYTwKJLcCCGEEMKj/D+dCyiP8djjjAAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Training on 17 points of the total of 500\n",
+ "Reusing previous best parameters\n",
+ "Iterat #Fevals function value axis ratio sigma min&max std t[m:s]\n",
+ " 1 12 6.609329424041481e-02 1.0e+00 1.61e+00 2e+00 2e+00 0:00.8\n",
+ " 2 24 7.491483294146274e-02 1.1e+00 1.55e+00 2e+00 2e+00 0:01.6\n",
+ " 3 36 8.512073615805898e-02 1.2e+00 1.44e+00 1e+00 2e+00 0:02.5\n",
+ " 7 84 1.054656149048312e-01 1.5e+00 1.65e+00 2e+00 2e+00 0:05.7\n",
+ " 12 144 5.713344493033991e-02 1.9e+00 2.16e+00 2e+00 2e+00 0:09.8\n",
+ " 19 228 4.798973921880101e-02 2.4e+00 2.80e+00 2e+00 3e+00 0:15.5\n",
+ " 27 324 4.494600871473839e-02 3.3e+00 2.89e+00 2e+00 3e+00 0:22.0\n",
+ " 36 432 3.828896215012124e-02 4.3e+00 4.32e+00 4e+00 5e+00 0:29.3\n",
+ " 46 552 1.717985656290756e-02 5.3e+00 5.89e+00 5e+00 8e+00 0:37.4\n",
+ " 58 696 1.184467478922581e-02 8.0e+00 6.14e+00 5e+00 9e+00 0:47.2\n",
+ " 71 852 6.486786453612226e-03 1.1e+01 4.87e+00 4e+00 8e+00 0:57.7\n",
+ " 85 1020 5.494133269030308e-03 1.6e+01 4.34e+00 4e+00 7e+00 1:09.1\n",
+ " 100 1200 2.372823660186176e-03 2.2e+01 2.82e+00 2e+00 5e+00 1:21.3\n",
+ " 117 1404 2.138673148245246e-03 2.9e+01 1.70e+00 9e-01 3e+00 1:35.1\n",
+ " 135 1620 2.054091897997754e-03 3.7e+01 1.31e+00 5e-01 2e+00 1:49.6\n",
+ " 154 1848 1.998711170428923e-03 5.3e+01 1.18e+00 3e-01 2e+00 2:05.1\n",
+ " 174 2088 1.983613455803880e-03 8.8e+01 6.98e-01 1e-01 1e+00 2:21.3\n",
+ " 195 2340 1.954724356488905e-03 1.2e+02 4.58e-01 7e-02 8e-01 2:38.3\n",
+ " 200 2400 1.950942683480392e-03 1.3e+02 5.15e-01 7e-02 9e-01 2:42.4\n",
+ " 224 2688 1.886923259537389e-03 2.2e+02 9.73e-01 1e-01 2e+00 3:01.9\n",
+ " 249 2988 1.684770340182751e-03 3.9e+02 4.11e+00 4e-01 1e+01 3:22.3\n",
+ " 275 3300 1.520911356964065e-03 4.6e+02 3.85e+00 3e-01 1e+01 3:43.4\n",
+ " 300 3600 1.459147737578972e-03 3.3e+02 4.41e+00 3e-01 9e+00 4:03.5\n",
+ " 329 3948 1.323676097422344e-03 4.2e+02 3.92e+00 3e-01 9e+00 4:27.0\n",
+ " 359 4308 1.259655411501023e-03 3.5e+02 2.49e+00 1e-01 5e+00 4:51.3\n",
+ " 387 4644 9.592523868747612e-04 6.5e+02 4.72e+00 2e-01 1e+01 5:13.9\n",
+ "termination on ftarget=0.001 (Mon Jul 31 16:27:51 2023)\n",
+ "final/bestever f-value = 9.713643e-04 9.592524e-04 after 4645/4641 evaluations\n",
+ "incumbent solution: [ -0.6469428 51.53964644 28.03206207 -84.03823824 -75.84633116\n",
+ " 3.49948159 59.0232561 20.84592617 ...]\n",
+ "std deviations: [ 0.21441993 10.42975528 8.70043442 2.65252423 6.18333283 7.51347648\n",
+ " 7.3330395 5.12020917 ...]\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABz4ElEQVR4nO3dd3hUZdrH8e/09AZpQKjSq4AgiIuwIFhQLCtrA7u7dpFVsYAV7IsuKOqrgq4FV7EjICAqHWkWmtQESAIE0pOp5/1jZCAk9CSTTH6f65orzDPPOeeeQ2DuearJMAwDERERkRBhDnYAIiIiIpVJyY2IiIiEFCU3IiIiElKU3IiIiEhIUXIjIiIiIUXJjYiIiIQUJTciIiISUpTciIiISEhRciMiIiIhRcmNiBzRddddR9OmTYNy7SlTpmAymdi2bVtQrn8s8+fPx2QyMX/+/Eo9bzDvuUioUHIjUguZTKbjelT2B69Ujl27dvHYY4+xevXqYIciEpKswQ5ARE7ce++9V+b5u+++y3fffVeuvG3btqd0nTfffBOfz3dK55Dydu3axeOPP07Tpk3p0qVLmdd0z0VOnZIbkVrommuuKfN8yZIlfPfdd+XKD1dcXExERMRxX8dms51UfHLydM9FTp26pURC1DnnnEOHDh1YsWIFf/nLX4iIiOChhx4C4IsvvuCCCy6gQYMGOBwOWrRowZNPPonX6y1zjsPHf2zbtg2TycQLL7zAG2+8QYsWLXA4HJxxxhksX768XAzr16/n8ssvJyEhgbCwMLp3786XX35Zrt7vv/9O//79CQ8Pp1GjRjz11FMn1HpxrOv8/PPPmEwmpk6dWu7YWbNmYTKZ+PrrrwNlq1at4rzzziMmJoaoqCj++te/smTJkmPG0bRpU6677rpy5eeccw7nnHMO4B+rc8YZZwBw/fXXB7oQp0yZAlQ85qaoqIj77ruPtLQ0HA4HrVu35oUXXsAwjDL1TCYTd9xxB59//jkdOnTA4XDQvn17Zs6ceczYRUKJWm5EQlhOTg7nnXcef//737nmmmtITk4G/IN1o6KiGDlyJFFRUcybN48xY8aQn5/P888/f8zzfvDBBxQUFHDrrbdiMpl47rnnuPTSS9myZUug5eH333/nrLPOomHDhjz44INERkby8ccfM3ToUD799FMuueQSALKysujXrx8ejydQ74033iA8PPy43uPxXKd79+40b96cjz/+mBEjRpQ5ftq0acTHxzNo0KDA+c4++2xiYmK4//77sdlsvP7665xzzjn88MMP9OzZ87jvf0Xatm3LE088wZgxY7jllls4++yzAejdu3eF9Q3D4KKLLuL777/nxhtvpEuXLsyaNYt//etf7Ny5k3//+99l6i9YsIDp06dz2223ER0dzSuvvMJll11Geno69erVO6XYRWoNQ0Rqvdtvv904/J9z3759DcCYPHlyufrFxcXlym699VYjIiLCKC0tDZSNGDHCaNKkSeD51q1bDcCoV6+esW/fvkD5F198YQDGV199FSj761//anTs2LHM+Xw+n9G7d2+jZcuWgbJ77rnHAIylS5cGynbv3m3ExsYagLF169ajvvfjvc7o0aMNm81WJm6n02nExcUZN9xwQ6Bs6NChht1uNzZv3hwo27VrlxEdHW385S9/CZR9//33BmB8//33gbImTZoYI0aMKBdj3759jb59+waeL1++3ACMd955p1zdw+/5559/bgDGU089Vabe5ZdfbphMJmPTpk2BMsCw2+1lytasWWMAxn/+859y1xIJVeqWEglhDoeD66+/vlz5oa0iBQUF7N27l7PPPpvi4mLWr19/zPMOGzaM+Pj4wPMDrQ9btmwBYN++fcybN48rrrgicP69e/eSk5PDoEGD+OOPP9i5cycAM2bM4Mwzz6RHjx6B8yUmJnL11VcfM44Tuc6wYcNwu91Mnz49cPzs2bPJzc1l2LBhAHi9XmbPns3QoUNp3rx5oF5qaipXXXUVCxYsID8//5hxVaYZM2ZgsVi46667ypTfd999GIbBt99+W6Z8wIABtGjRIvC8U6dOxMTEBP5uROoCJTciIaxhw4bY7fZy5b///juXXHIJsbGxxMTEkJiYGBiMnJeXd8zzNm7cuMzzA4nO/v37Adi0aROGYfDoo4+SmJhY5jF27FgAdu/eDcD27dtp2bJluWu0bt36mHGcyHU6d+5MmzZtmDZtWuD4adOmUb9+ffr37w/Anj17KC4urvDabdu2xefzkZGRccy4KtP27dtp0KAB0dHR5eI58PqhDv+7Af/fz4G/G5G6QGNuREJYReNWcnNz6du3LzExMTzxxBO0aNGCsLAwVq5cyQMPPHBcA3ktFkuF5cafA1wPnGPUqFGBsSyHO+200473bRzRiV5n2LBhPP300+zdu5fo6Gi+/PJLrrzySqzWyvmv0GQyVVju9XqPeM8q27H+bkTqAiU3InXM/PnzycnJYfr06fzlL38JlG/durXSrnGgS8dmszFgwICj1m3SpAl//PFHufINGzZU6nXAn9w8/vjjfPrppyQnJ5Ofn8/f//73wOuJiYlERERUeO3169djNptJS0s74vnj4+PJzc0tV759+/Yy3VxHSoIq0qRJE+bMmUNBQUGZ1psD3YdNmjQ57nOJ1BXqlhKpYw58sz/0m7zL5eLVV1+ttGskJSVxzjnn8Prrr5OZmVnu9T179gT+fP7557NkyRKWLVtW5vX333+/Uq8D/q6cjh07Mm3aNKZNm0ZqamqZBM9isXDuuefyxRdflNn2ITs7mw8++IA+ffoQExNzxHhatGjBkiVLcLlcgbKvv/66XFdWZGQkQIWJ0OHOP/98vF4vEydOLFP+73//G5PJxHnnnXfMc4jUNWq5EaljevfuTXx8PCNGjOCuu+7CZDLx3nvvVXq3xaRJk+jTpw8dO3bk5ptvpnnz5mRnZ7N48WJ27NjBmjVrALj//vt57733GDx4MHfffXdgKniTJk345ZdfKu06BwwbNowxY8YQFhbGjTfeiNlc9jveU089xXfffUefPn247bbbsFqtvP766zidTp577rmjxnLTTTfxySefMHjwYK644go2b97Mf//73zIDfMGfBMXFxTF58mSio6OJjIykZ8+eNGvWrNw5hwwZQr9+/Xj44YfZtm0bnTt3Zvbs2XzxxRfcc8895c4tImgquEgoONJU8Pbt21dYf+HChcaZZ55phIeHGw0aNDDuv/9+Y9asWeWmNh9pKvjzzz9f7pyAMXbs2DJlmzdvNoYPH26kpKQYNpvNaNiwoXHhhRcan3zySZl6v/zyi9G3b18jLCzMaNiwofHkk08ab7311nFNBT+R6xiGYfzxxx8GYADGggULKjzfypUrjUGDBhlRUVFGRESE0a9fP2PRokVl6lQ0FdwwDOPFF180GjZsaDgcDuOss84yfv7553JTwQ3DP32+Xbt2htVqLTMt/PB7bhiGUVBQYNx7771GgwYNDJvNZrRs2dJ4/vnnDZ/PV6YeYNx+++3l3s+RpqiLhCqTYWiUmYiIiIQOjbkRERGRkKLkRkREREKKkhsREREJKUpuREREJKQouREREZGQouRGRERE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fvll4PXDu6WmTJlCXFwcs2bNom3btkRFRTF48OAyyZjH4+Guu+4iLi6OevXq8cADDzBixAiGDh16xPd74Lyff/45LVu2JCwsjEGDBpGRkVGm3muvvUaLFi2w2+20bt2a9957r8zrh3ZLbdu2DZPJxPTp0+nXrx8RERF07tyZxYsXB97b9ddfT15eHiaTCZPJFGgNe/XVVwNxJCcnc/nllx/PX5sAJftLmHHHDIpzigFo/5dszuw5GXNpBthiISwp8HD6wijas45HB2zm52+eU2IjIkek5KayZM70d0XZ4irekA/85bY4f73MWdUZHQ8++CDPPPMM69ato1OnThQWFnL++eczd+5cVq1axeDBgxkyZAjp6elHPc/jjz/OFVdcwS+//ML555/P1Vdfzb59+45Yv7i4mBdeeIH33nuPH3/8kfT0dEaNGhV4/dlnn+X999/nnXfeYeHCheTn5x/XOJji4mKefvpp3n33XRYuXEhubi5///vfA69/9tln3H333dx333389ttv3HrrrVx//fV8//33Rz3vww8/zKhRo1i9ejWtWrXiyiuvxOPx0Lt3byZMmEBMTAyZmZlkZmYyatQofv75Z+666y6eeOIJNmzYwMyZM/nLX/5yzPjFv/LwzLtnkpeeB0BC80h69fsfJm/5Lwhut5vtOzLJd9pJrh9Fy/zXat0YNhGpPuqWqixZc/xN6Ie32BzObPPXy/quWldHfeKJJxg4cGDgeUJCAp07dw48f/LJJ/nss8/48ssvueOOO454nuuuu44rr7wSgHHjxvHKK6+wbNkyBg8eXGF9t9vN5MmTadGiBQB33HEHTzzxROD1//znP4wePZpLLrkEgIkTJzJjxoxjvh+3283EiRPp2bMnAFOnTqVt27YsW7aMHj168MILL3Dddddx2223ATBy5EiWLFnCCy+8QL9+/Y543lGjRnHBBRcA/kSuffv2bNq0iTZt2pRpFTsgPT2dyMhILrzwQqKjo2nSpAmnn376MeOv6wyfwZzRc9iz1r/ycGRSJBc8asG8YWeFXxBsNhvJyclER0djNjwHvyBohWERqYBabiqLc/+J1XedYP1T1L179zLPCwsLGTVqFG3btiUuLo6oqCjWrVt3zJabTp06Bf4cGRlJTEwMu3fvPmL9iIiIQGIDkJqaGqifl5dHdnY2PXr0CLxusVjo1q3bMd+P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Xr0qPKZXr15l6gN89913R6wvJ3efAZ577jmefPJJZs6cSffu3asj1FrtRO9zmzZt+PXXX1m9enXgcdFFF9GvXz9Wr15NWlpadYZfa5zM7/NZZ53Fpk2bAskjwMaNG0lNTVVicwQnc5+Li4vLJTAHEkpDWy5WmqB9DlbpcOUQ89FHHxkOh8OYMmWKsXbtWuOWW24x4uLijKysLMMwDOPaa681HnzwwUD9hQsXGlar1XjhhReMdevWGWPHjtVU8ONwovf5mWeeMex2u/HJJ58YmZmZgUdBQUGw3kKtcKL3+XCaLXV8TvQ+p6enG9HR0cYdd9xhbNiwwfj666+NpKQk46mnngrWW6gVTvQ+jx071oiOjjY+/PBDY8uWLcbs2bONFi1aGFdccUWw3kKtUFBQYKxatcpYtWqVARgvvfSSsWrVKmP79u2GYRjGgw8+aFx77bWB+gemgv/rX/8y1q1bZ0yaNElTwWui//znP0bjxo0Nu91u9OjRw1iyZEngtb59+xojRowoU//jjz82WrVqZdjtdqN9+/bGN998U80R104ncp+bNGliAOUeY8eOrf7Aa5kT/X0+lJKb43ei93nRokVGz549DYfDYTRv3tx4+umnDY/HU81R1z4ncp/dbrfx2GOPGS1atDDCwsKMtLQ047bbbjP2799f/YHXIt9//32F/98euLcjRoww+vbtW+6YLl26GHa73WjevLnxzjvvVHmcJsNQ+5uIiIiEDo25ERERkZCi5EZERERCipIbERERCSlKbkRERCSkKLkRERGRkKLkRkREREKKkhsREREJKUpuREREJKQouREREZGQouRGREREQoqSGxGp9fbs2UNKSgrjxo0LlC1atAi73V5uR2IRCX3aW0pEQsKMGTMYOnQoixYtonXr1nTp0oWLL76Yl156KdihiUg1U3IjIiHj9ttvZ86cOXTv3p1ff/2V5cuX43A4gh2WiFQzJTciEjJKSkro0KEDGRkZrFixgo4dOwY7JBEJAo25EZGQsXnzZnbt2oXP52Pbtm3BDkdEgkQtNyISElwuFz169KBLly60bt2aCRMm8Ouvv5KUlBTs0ESkmim5EZGQ8K9//YtPPvmENWvWEBUVRd++fYmNjeXrr78OdmgiUs3ULSUitd78+fOZMGEC7733HjExMZjNZt577z1++uknXnvttWCHJyLVTC03IiIiElLUciMiIiIhRcmNiIiIhBQlNyIiIhJSlNyIiIhISFFyIyIiIiFFyY2IiIiEFCU3IiIiElKU3IiIiEhIUXIjIiIiIUXJjYiIiIQUJTciIiISUpTciIiISEj5f6pc0rKrHlsVAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "best_params = train_adiabatic_evolution(\n",
+ " nsteps=nsteps,\n",
+ " xarr=xarr,\n",
+ " cdf=cdf,\n",
+ " training_n=20,\n",
+ " init_params=init_params,\n",
+ " e0=e0,\n",
+ " e1=e1,\n",
+ " target_loss=1e-3,\n",
+ " finalT=finalT,\n",
+ " h0=h0,\n",
+ " h1=h1,\n",
+ " obs_target=obs_target\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 7. From Adiabatic Evolution to a quantum circuit\n",
+ "\n",
+ "We define an unitary operator which can be used to get the evolved state at any \n",
+ "evolution time $\\tau$ thanks to some calculations on the evolution operator\n",
+ "associated to $H_{\\rm ad}$. This operator is translated into a circuit composed \n",
+ "of some rotations in `qaml_scripts/rotational_circuit.py`.\n",
+ "\n",
+ "> If we are able to translate the problem to a VQC composed of rotations, we can\n",
+ "> use the Parameter Shift Rule to derivate it very easily."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "rotcirc = rotational_circuit(best_p=best_params, finalT=finalT)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Circuit diagram: q0: ─RZ─RX─RZ─RZ─RZ─RX─RZ─RX─RZ─M─\n",
+ "\n",
+ "Circ1 params: [(0.00045531799358689007,), (0.20000960479244062,), (-0.00044624108017310427,)]\n",
+ "\n",
+ "Circ2 params: [(-0.00018573270144739418,), (1.600129455712294,), (-5.447340081898844e-06,)]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# a look to the circuit\n",
+ "# this circuit must be filled with a time value to be well defined\n",
+ "\n",
+ "circ1 = rotcirc.rotations_circuit(t=0.1)\n",
+ "circ2 = rotcirc.rotations_circuit(t=0.8)\n",
+ "\n",
+ "print(f\"Circuit diagram: {circ1.draw()}\")\n",
+ "print(f\"\\nCirc1 params: {circ1.get_parameters()}\")\n",
+ "print(f\"\\nCirc2 params: {circ2.get_parameters()}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Calculate derivative of the circuit with respect to $\\tau$\n",
+ "\n",
+ "Here we use the parameter shift rule to compute the derivatives of the circuit with respect to the target variables. We need to combine the PSR (which calculates the derivative with respect to the rotational angles) with the analytical derivative of the angles with respect to the time $t$ on which they depend. The calculation is non trivial and explicitly shown in [2]; here we get this derivation calling the `rotcirc.derivative_rotation_angles` method implemented in `qaml_scripts/rotational_circuit.py`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 50,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def psr_energy(t, nshots=None):\n",
+ " \"\"\"Calculate derivative of the energy with respect to the real time t.\"\"\"\n",
+ "\n",
+ " c = rotcirc.rotations_circuit(t)\n",
+ " dd1, dd2, dd3 = rotcirc.derivative_rotation_angles(t)\n",
+ "\n",
+ " d1 = dd1 * parameter_shift(circuit=c, parameter_index=0, hamiltonian=obs_target, nshots=nshots)\n",
+ " # looking inside the circuit you will see the second angle is filled with a \"-\" before\n",
+ " d2 = - dd2 * parameter_shift(circuit=c, parameter_index=1, hamiltonian=obs_target, nshots=nshots)\n",
+ " d3 = dd3 * parameter_shift(circuit=c, parameter_index=2, hamiltonian=obs_target, nshots=nshots)\n",
+ "\n",
+ " return (d1 + d2 + d3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Calculate derivatives time by time\n",
+ "\n",
+ "As final step, we loop over the target times and we calculate both the energy of the observable (the CDF) and its derivative (the PDF)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 51,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "nshots = 1000\n",
+ "real_times = np.linspace(0, finalT, len(xarr))\n",
+ "\n",
+ "de = []\n",
+ "e = []\n",
+ "\n",
+ "# loop over times\n",
+ "for t in real_times:\n",
+ " c = rotcirc.rotations_circuit(t)\n",
+ " exp = obs_target.expectation(c.execute(nshots=nshots).state())\n",
+ " # to avoid numerical instabilities when close to zero\n",
+ " if exp > 0:\n",
+ " e.append(-exp)\n",
+ " else:\n",
+ " e.append(exp)\n",
+ " de.append(psr_energy(t))\n",
+ "\n",
+ "de = - np.asarray(de)*finalT"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 52,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAADAdElEQVR4nOzdd3hUVeLG8e9MyqQXAil0pEPoAtIEFEFFFNfuKuiubRUbrgV1da38dl0LropdsKBiwYaLIgiIIL1XQTpJIEB6n5nfH5fMTUidkGQmk/fzPPM49869d84bOXfOnDn3XIvT6XQiIiIiIiIiIiJSj6yeLoCIiIiIiIiIiDQ+6pQSEREREREREZF6p04pERERERERERGpd+qUEhERERERERGReqdOKRERERERERERqXfqlBIRERERERERkXqnTikREREREREREal36pQSEREREREREZF6p04pERERERERERGpd+qUEpEGacSIESQmJla53d69e7FYLMyYMaPuCyUiIiLixWbMmIHFYmH16tVVbjtixAhGjBhR94USkUZNnVIiUqnixkvxIygoiE6dOjFp0iRSUlJc2y1atKjUdjabjbi4OEaMGMGzzz7L0aNHqzx2ycdDDz1UnzHLeO2119SRJSIiIjW2e/dubr31Vs444wyCgoKIiIhgyJAhTJs2jdzcXNd2bdu2dbV/rFYrUVFR9OjRg1tuuYUVK1aUe+yK2k/x8fH1Fa9cy5Yt45///CdpaWkeLYeINBz+ni6AiDQMTz75JO3atSMvL4+lS5cyffp0vv/+ezZv3kxISIhru7vuuov+/ftjt9s5evQoy5Yt4/HHH+eFF15g9uzZnHPOORUeu6TqjIKqjjZt2pCbm0tAQIBb+7322ms0bdqUG264oVbKISIiIo3H3LlzueKKK7DZbEyYMIHExEQKCgpYunQp999/P1u2bOHNN990bd+7d2/uu+8+ADIzM9m2bRufffYZb731Fvfeey8vvPBCmfc477zzmDBhQql1wcHBtZbhxx9/dHufZcuW8cQTT3DDDTcQFRVVa2UREd+lTikRqZYLLriAM888E4CbbrqJmJgYXnjhBb7++muuueYa13bDhg3j8ssvL7Xvhg0bGD16NJdddhlbt24lISGhwmPXtuLRXQ1NTk5Oqc4+ERERaRj27NnD1VdfTZs2bVi4cGGpds8dd9zBrl27mDt3bql9WrRowXXXXVdq3b/+9S+uvfZaXnzxRTp27Mjf/va3Uq936tSpzD61KTAwsM6OXVeys7MJDQ31dDFExA26fE9EaqR4xNOePXuq3LZXr1689NJLpKWl8corr9RqObZu3crIkSMJCQmhRYsW/Pvf/y71enlzSiUnJ3PjjTfSsmVLbDYbCQkJXHLJJezduxcwhtFv2bKFxYsXu4bDl5xT4Y8//uCKK66gSZMmhISEcNZZZ5VpXALs27ePiy++mNDQUGJjY7n33nv54YcfsFgsLFq0yLVd8fxYa9as4eyzzyYkJISHH34YgK+//pqxY8fSvHlzbDYb7du356mnnsJut5d6r+JjbNy4keHDhxMSEkKHDh34/PPPAVi8eDEDBw4kODiYzp0789NPP53GX11EREQq8u9//5usrCzeeeedMj/EAXTo0IG77767yuMEBwfzwQcf0KRJE5555hmcTmetlTE/P5/JkyfTrFkzQkNDufTSS8tMtVDenFL//e9/6d69OyEhIURHR3PmmWcya9YsAP75z39y//33A9CuXTtXG6q4fVVUVMRTTz1F+/btsdlstG3blocffpj8/PxS7+FwOPjnP/9J8+bNCQkJYeTIkWzdupW2bduWGsFePA3E4sWLuf3224mNjaVly5aA0Qa7/fbb6dy5M8HBwcTExHDFFVe4ynLqMZYuXcpdd91Fs2bNiIqK4tZbb6WgoIC0tDQmTJhAdHQ00dHRPPDAA7X6/0FENFJKRGpo9+7dAMTExFRr+8svv5y//vWv/PjjjzzzzDOlXktPTyc1NbXUuqZNm1Z5zBMnTnD++efzpz/9iSuvvJLPP/+cBx98kB49enDBBRdUuN9ll13Gli1buPPOO2nbti1Hjhxh/vz57N+/n7Zt2/LSSy9x5513EhYWxiOPPAJAXFwcACkpKQwePJicnBzuuusuYmJimDlzJhdffDGff/45l156KWD8UnfOOeeQlJTE3XffTXx8PLNmzeLnn38ut0zHjh3jggsu4Oqrr+a6665zvd+MGTMICwtj8uTJhIWFsXDhQh577DEyMjJ47rnnyvw9LrroIq6++mquuOIKpk+fztVXX81HH33EPffcw2233ca1117Lc889x+WXX86BAwcIDw+v8u8sIiIi1fftt99yxhlnMHjw4NM+VlhYGJdeeinvvPMOW7dupXv37q7X8vLyyrSfwsPDsdlsVR73zjvvJDo6mscff5y9e/fy0ksvMWnSJD799NMK93nrrbe46667uPzyy7n77rvJy8tj48aNrFixgmuvvZY//elP7Ny5k48//pgXX3zR1ZZr1qwZYIy0nzlzJpdffjn33XcfK1asYOrUqWzbto05c+a43mfKlCn8+9//Zty4cYwZM4YNGzYwZswY8vLyyi3X7bffTrNmzXjsscfIzs4GYNWqVSxbtoyrr76ali1bsnfvXqZPn86IESPYunVrmdHod955J/Hx8TzxxBP89ttvvPnmm0RFRbFs2TJat27Ns88+y/fff89zzz1HYmJimcsmReQ0OEVEKvHee+85AedPP/3kPHr0qPPAgQPOTz75xBkTE+MMDg52Hjx40Ol0Op0///yzE3B+9tlnFR6rV69ezujo6DLHLu9RleHDhzsB5/vvv+9al5+f74yPj3dedtllrnV79uxxAs733nvP6XQ6nSdOnHACzueee67S43fv3t05fPjwMuvvueceJ+D85ZdfXOsyMzOd7dq1c7Zt29Zpt9udTqfT+fzzzzsB51dffeXaLjc319mlSxcn4Pz555/LZHn99dfLvF9OTk6ZdbfeeqszJCTEmZeXV+YYs2bNcq3bvn27E3BarVbnb7/95lr/ww8/lPqbiIiISO1IT093As5LLrmk2vu0adPGOXbs2Apff/HFF52A8+uvv3atq6j9VNVne3Hba9SoUU6Hw+Faf++99zr9/PycaWlprnXDhw8v1Ra65JJLnN27d6/0+M8995wTcO7Zs6fU+vXr1zsB50033VRq/d///ncn4Fy4cKHT6XQ6k5OTnf7+/s7x48eX2u6f//ynE3BOnDixTJahQ4c6i4qKSm1fXvtp+fLlZdqOxccYM2ZMqb/HoEGDnBaLxXnbbbe51hUVFTlbtmxZbvtQRGpOl++JSLWMGjWKZs2a0apVK66++mrCwsKYM2cOLVq0qPYxwsLCyMzMLLP+1VdfZf78+aUe1T1eybkUAgMDGTBgAH/88UeF+wQHBxMYGMiiRYs4ceJEtcte7Pvvv2fAgAEMHTq0VDluueUW9u7dy9atWwGYN28eLVq04OKLL3ZtFxQUxM0331zucW02GzfeeGO55S2WmZlJamoqw4YNIycnh+3bt5faNiwsjKuvvtq13LlzZ6KioujatSsDBw50rS9+XtnfSURERNyXkZEBUKsjkcPCwgDKtKEuueSSMu2nMWPGVOuYt9xyCxaLxbU8bNgw7HY7+/btq3CfqKgoDh48yKpVq9zO8P333wMwefLkUuuLJ3cvngZhwYIFFBUVcfvtt5fa7s4776zw2DfffDN+fn6l1pVsPxUWFnLs2DE6dOhAVFQUa9euLXOMv/71r6X+HgMHDsTpdPLXv/7Vtc7Pz48zzzxT7SeRWqbL90SkWl599VU6deqEv78/cXFxdO7cGavVvX7trKyschtpAwYMqNFE5y1btizVgACIjo5m48aNFe5js9n417/+xX333UdcXBxnnXUWF110ERMmTKjWbZT37dtXqoOnWNeuXV2vJyYmsm/fPtq3b1+mfB06dCj3uC1atCh3QtEtW7bw6KOPsnDhQldDt1h6enqp5fL+HpGRkbRq1arMOqBGnXIiIiJSsYiICKBsB9LpyMrKAsp2dLVs2ZJRo0bV6JitW7cutRwdHQ1U3jZ48MEH+emnnxgwYAAdOnRg9OjRXHvttQwZMqTK99u3bx9Wq7VMOyg+Pp6oqChXZ1jxf0/drkmTJq4ynurUOzgD5ObmMnXqVN577z0OHTpUah6oU9tPUPbvUdxWKq8NpfaTSO3SSCkRqZYBAwYwatQoRowYQdeuXd3ukCosLGTnzp0VdsrUxKm/ihVzVjEB5T333MPOnTuZOnUqQUFB/OMf/6Br166sW7eu1srmrvJu4ZyWlsbw4cPZsGEDTz75JN9++y3z58/nX//6F2BMBFpSRX+Pmv6dRERExD0RERE0b96czZs319oxi4/l6TZU165d2bFjB5988glDhw7liy++YOjQoTz++OPVft9TfzyrDeW1oe68806eeeYZrrzySmbPns2PP/7I/PnziYmJKdN+AvfaUGo/idQudUqJSL34/PPPyc3Nrfaw8rrWvn177rvvPn788Uc2b95MQUEBzz//vOv1ihpNbdq0YceOHWXWF19K16ZNG9d/d+/eXabhsmvXrmqXcdGiRRw7dowZM2Zw9913c9FFFzFq1KgKfykUERERz7vooovYvXs3y5cvP+1jZWVlMWfOHFq1auUale1JoaGhXHXVVbz33nvs37+fsWPH8swzz7gmIa+s/eRwOPj9999LrU9JSSEtLa1U+wnKtpeOHTvm1gilzz//nIkTJ/L8889z+eWXc9555zF06FDS0tKqfQwRqR/qlBKROrdhwwbuueceoqOjueOOOzxalpycnDJ3b2nfvj3h4eGlbkkcGhpabsPlwgsvZOXKlaUamtnZ2bz55pu0bduWbt26ATBmzBgOHTrEN99849ouLy+Pt956q9plLf51rmTHVkFBAa+99lq1jyEiIiL164EHHiA0NJSbbrqJlJSUMq/v3r2badOmVXmc3Nxcrr/+eo4fP84jjzxSJ6OM3HHs2LFSy4GBgXTr1g2n00lhYSFgtJ+AMm2oCy+8EICXXnqp1PoXXngBgLFjxwJw7rnn4u/vz/Tp00tt98orr7hVVj8/vzI/DP73v//Fbre7dRwRqXuaU0pEatUvv/xCXl4edrudY8eO8euvv/LNN98QGRnJnDlzqjVvU13auXMn5557LldeeSXdunXD39+fOXPmkJKSUmqS8H79+jF9+nSefvppOnToQGxsLOeccw4PPfQQH3/8MRdccAF33XUXTZo0YebMmezZs4cvvvjCdVnjrbfeyiuvvMI111zD3XffTUJCAh999BFBQUFA9YavDx48mOjoaCZOnMhdd92FxWLhgw8+0LBxERERL9a+fXtmzZrFVVddRdeuXZkwYQKJiYkUFBSwbNkyPvvsM2644YZS+xw6dIgPP/wQMEZHbd26lc8++4zk5GTuu+8+br31Vg8kKW306NHEx8czZMgQ4uLi2LZtG6+88gpjx451zXfVr18/AB555BGuvvpqAgICGDduHL169WLixIm8+eabrukJVq5cycyZMxk/fjwjR44EIC4ujrvvvpvnn3+eiy++mPPPP58NGzbwv//9j6ZNm1a7Y+6iiy7igw8+IDIykm7durF8+XJ++uknYmJi6uaPIyI1pk4pEalVL7/8MgABAQGuO7898cQT3HzzzTRr1szDpTMmrLzmmmtYsGABH3zwAf7+/nTp0oXZs2dz2WWXubZ77LHH2LdvH//+97/JzMxk+PDhnHPOOcTFxbFs2TIefPBB/vvf/5KXl0fPnj359ttvXb/ygXGnnIULF3LnnXcybdo0wsLCmDBhAoMHD+ayyy5zdU5VJiYmhu+++4777ruPRx99lOjoaK677jrOPfdcr7kMUkRERMq6+OKL2bhxI8899xxff/0106dPx2az0bNnT55//vkyd+Ndv349119/PRaLhfDwcFq1asW4ceO46aabGDBggIdSlHbrrbfy0Ucf8cILL5CVlUXLli256667ePTRR13b9O/fn6eeeorXX3+defPm4XA42LNnD6Ghobz99tucccYZzJgxw/VD5ZQpU8rMSfWvf/2LkJAQ3nrrLX766ScGDRrEjz/+yNChQ6vVfgKYNm0afn5+fPTRR+Tl5TFkyBB++ukntZ9EvJDFqZ/cRUTqzUsvvcS9997LwYMHadGihaeLIyIiIuL10tLSiI6O5umnn+aRRx7xdHFEpBZpTikRkTqSm5tbajkvL4833niDjh07qkNKREREpByntp/AnItqxIgR9VsYEalzunxPRKSO/OlPf6J169b07t2b9PR0PvzwQ7Zv385HH33k6aKJiIiIeKVPP/2UGTNmcOGFFxIWFsbSpUv5+OOPGT16NEOGDPF08USklqlTSkSkjowZM4a3336bjz76CLvdTrdu3fjkk0+46qqrPF00EREREa/Us2dP/P39+fe//01GRoZr8vOnn37a00UTkTqgOaVERERERERERKTeaU4pERERERERERGpd+qUEhERERERERGRetco55RyOBwcPnyY8PBwLBaLp4sjIiIiXszpdJKZmUnz5s2xWhvv73lqP4mIiEh1Vbf91Cg7pQ4fPkyrVq08XQwRERFpQA4cOEDLli09XQyPUftJRERE3FVV+6lRdkqFh4cDxh8nIiLCw6URERERb5aRkUGrVq1c7YfGSu0nERERqa7qtp8aZadU8ZDziIgINapERESkWhr7JWtqP4mIiIi7qmo/Nd6JEURERERERERExGPUKSUiIiIiIiIiIvVOnVIiIiIiIiIiIlLvGuWcUiIiIna7ncLCQk8XQ7xAQEAAfn5+ni6Gz1DdkmKqWyIiUhV1SomISKPidDpJTk4mLS3N00URLxIVFUV8fHyjn8z8dKhuSXlUt0REpDLqlBIRkUal+EtzbGwsISEh+qLUyDmdTnJycjhy5AgACQkJHi5Rw6W6JSWpbomISHWoU0pERBoNu93u+tIcExPj6eKIlwgODgbgyJEjxMbG6nKjGlDdkvKobomISFU00bmIiDQaxfPchISEeLgk4m2K/000lLmQlixZwrhx42jevDkWi4Wvvvqqyn0WLVpE3759sdlsdOjQgRkzZtRaeVS3pCINrW6JiEj9UqeUiIg0OrqsSE7V0P5NZGdn06tXL1599dVqbb9nzx7G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aPCaN2+e5wvuB9w99utio+rSuVv/W7duFSNGjBBGo1EkJyeLv/zlL8Jut3u41P7Bnbq32Wzi2WefFX369BGBgYEiKSlJPPTQQ6K0tNTzBfcD3377baO/xy/U+cyZM8W4ceMabJOWliYMBoNITk4W77//vsfLTQ2x/aQutp/Uw/aTuth+Ug/bT+rxlfaTRgiOgyMiIiIiIiIiIs/inFJERERERERERORx7JQiIiIiIiIiIiKPY6cUERERERERERF5HDuliIiIiIiIiIjI49gpRUREREREREREHsdOKSIiIiIiIiIi8jh2ShERERERERERkcexU4qIiIiIiIiIiDyOnVJERERERERERORx7JQiIiIiIiIiIiKPY6cUERERERERERF5HDuliKhTKy4uRlxcHF588UXXsq1bt8JgMCArK0vFkhERERF5J7afiKi9aIQQQu1CEBGpae3atZg8eTK2bt2Kfv36IS0tDbfccgv++te/ql00IiIiIq/E9hMRtQd2ShERAZg9ezY2bdqEjIwM7N+/Hz/99BOMRqPaxSIiIiLyWmw/EdGlYqcUERGAmpoaDBo0CCdPnsSuXbswePBgtYtERERE5NXYfiKiS8U5pYiIABw/fhynT5+G0+lEXl6e2sUhIiIi8npsPxHRpeJIKSLq9KxWK4YPH460tDT069cPixYtwv79+xETE6N20YiIiIi8EttPRNQe2ClFRJ3eY489hs8++wz79u1DaGgoxo0bh4iICKxZs0btohERERF5JbafiKg98PY9IurUNm/ejEWLFuGjjz5CeHg4tFotPvroI3z//fd466231C4eERERkddh+4mI2gtHShERERERERERkcdxpBQREREREREREXkcO6WIiIiIiIiIiMjj2ClFREREREREREQex04pIiIiIiIiIiLyOHZKERERERERERGRx7FTioiIiIiIiIiIPI6dUkRERERERERE5HHslCIiIiIiIiIiIo9jpxQREREREREREXkcO6WIiIiIiIiIiMjj2ClFREREREREREQex04pIiIiIiIiIiLyuP8PVjMNoYBDbLgAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_final_results(times=xarr, sample=normed_sample, e=e, de=de, title=\"Final estimations\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As you can see, even with a small training, the evolutionary model (right plot) starts to learn the shape of the empirical CDF, and its derivative approximates the PDF function. Some better results can be obtained by setting a smaller target value of the loss function when training the adiabatic evolution. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### References\n",
+ "\n",
+ "[1] [_Evaluating analytic gradients on quantum hardware_, 2018](https://arxiv.org/abs/1811.11184)\n",
+ "\n",
+ "[2] [_Determining probability density functions with adiabatic quantum computing_, 2023](https://arxiv.org/abs/2303.11346)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "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.10.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/examples/adiabatic_qml/images/pdf_est.png b/examples/adiabatic_qml/images/pdf_est.png
new file mode 100644
index 000000000..d07faf48c
Binary files /dev/null and b/examples/adiabatic_qml/images/pdf_est.png differ
diff --git a/examples/adiabatic_qml/qaml_data/best_p.npy b/examples/adiabatic_qml/qaml_data/best_p.npy
new file mode 100644
index 000000000..828d713a2
Binary files /dev/null and b/examples/adiabatic_qml/qaml_data/best_p.npy differ
diff --git a/examples/adiabatic_qml/qaml_data/cdf.npy b/examples/adiabatic_qml/qaml_data/cdf.npy
new file mode 100644
index 000000000..3502faf62
Binary files /dev/null and b/examples/adiabatic_qml/qaml_data/cdf.npy differ
diff --git a/examples/adiabatic_qml/qaml_data/not_normed_sample.npy b/examples/adiabatic_qml/qaml_data/not_normed_sample.npy
new file mode 100644
index 000000000..2455aa6db
Binary files /dev/null and b/examples/adiabatic_qml/qaml_data/not_normed_sample.npy differ
diff --git a/examples/adiabatic_qml/qaml_data/xarr.npy b/examples/adiabatic_qml/qaml_data/xarr.npy
new file mode 100644
index 000000000..4f98f31b5
Binary files /dev/null and b/examples/adiabatic_qml/qaml_data/xarr.npy differ
diff --git a/examples/adiabatic_qml/qaml_scripts/evolution.py b/examples/adiabatic_qml/qaml_scripts/evolution.py
new file mode 100644
index 000000000..73c411c6b
--- /dev/null
+++ b/examples/adiabatic_qml/qaml_scripts/evolution.py
@@ -0,0 +1,49 @@
+import numpy as np
+
+from qibo import callbacks, models
+
+
+def generate_schedule(params, schedule="poly"):
+ """Generate the scheduling used into the adiabatic evolution."""
+ nparams = len(params)
+
+ def poly(t):
+ """General polynomial scheduling satisfying s(0)=0 and s(1)=1"""
+ f = np.sum(t ** np.arange(1, 1 + nparams) * params)
+ f /= np.sum(params)
+ return f
+
+ def derpoly(t):
+ "Derivative of the polynomial above"
+ f = np.sum(np.arange(1, 1 + nparams) * (t ** np.arange(0, nparams) * params))
+ return f / np.sum(params)
+
+ return poly, derpoly
+
+
+def generate_adiabatic(params, h0, h1, obs_target, dt=1e-1, solver="exp"):
+ """Generate the adiabatic evolution object."""
+ energy = callbacks.Energy(obs_target)
+ # same scheduling function
+ schedule, _ = generate_schedule(params, schedule="poly")
+ return (
+ models.AdiabaticEvolution(
+ h0,
+ h1,
+ schedule,
+ dt=1e-1,
+ solver="exp",
+ callbacks=[energy],
+ ),
+ energy,
+ )
+
+
+# Perform adiabatic evolution (returns energy callback)
+def perform_adiabatic(params, finalT, h0, h1, obs_target, dt=1e-1, solver="exp"):
+ """Return energy callback as an array"""
+ evolution, energy = generate_adiabatic(
+ params, h0, h1, obs_target, dt=1e-1, solver="exp"
+ )
+ _ = evolution(final_time=finalT)
+ return np.array(energy.results)
diff --git a/examples/adiabatic_qml/qaml_scripts/rotational_circuit.py b/examples/adiabatic_qml/qaml_scripts/rotational_circuit.py
new file mode 100644
index 000000000..384bb1cdc
--- /dev/null
+++ b/examples/adiabatic_qml/qaml_scripts/rotational_circuit.py
@@ -0,0 +1,240 @@
+from functools import lru_cache
+
+import numpy as np
+from qaml_scripts.evolution import generate_schedule
+from scipy.integrate import quad
+
+import qibo
+from qibo.noise import DepolarizingError, NoiseModel
+
+
+class rotational_circuit:
+ def __init__(self, best_p, finalT, nqubits=1, q=0):
+ """
+ Class containing all functions used for generating the circuit composed
+ of three rotations.
+
+ Args:
+ best_p (float array): best parameters array.
+ finalT (float): final real time in the evolution.
+ nqubits (int): number of qubits of the quantum device (default 1).
+ q (int): target qubit used into the device (default 0).
+ """
+
+ self.poly, self.derpoly = generate_schedule(best_p)
+ self.finalT = finalT
+ self.nqubits = nqubits
+ self.q = q
+
+ @lru_cache
+ def sched(self, t):
+ """The schedule at a time t is poly(t/finalT)"""
+ return self.poly(t / self.finalT)
+
+ @lru_cache
+ def eigenval(self, t):
+ """Compute the eigenvalue of the Hamiltonian at a time t"""
+ s = self.sched(t)
+ return np.sqrt(s**2 + (1 - s) ** 2)
+
+ @lru_cache
+ def integral_eigen(self, t):
+ """Compute the integral of eigenval from 0 to t"""
+ res = quad(self.eigenval, 0, t)
+ return res[0]
+
+ @lru_cache
+ def u00(self, t, swap=1.0):
+ """Compute the value of u00 (real and imag) at a time T"""
+ if t == self.finalT:
+ t -= 1e-2
+ integral = self.integral_eigen()
+ l = self.eigenval(t)
+ s = self.sched(t)
+
+ # Normalization for the change of basis matrix P^{-1}HP = H_diagonal so that PP^-1 = I
+ normalize = 2.0 / (1 - s) * np.sqrt(l * (l - s))
+ fac = swap * (l - s) / (1 - s)
+
+ # (the multiplication by t not sure where does it come from)
+ ti = t / self.finalT
+ real_part = swap * np.cos(integral) * (1 + fac) / normalize
+ imag_part = np.sin(integral) * (1 - fac) / normalize
+
+ return real_part, imag_part
+
+ @lru_cache
+ def u10(self, t):
+ """Compute the value of u10 (real and imag), the offdiagonal term"""
+ pr, pi = self.u00(t, swap=-1.0)
+ return pr, pi
+
+ @lru_cache
+ def old_rotation_angles(self, t):
+ x, y = self.u00(t)
+ u, z = self.u10(t)
+
+ a = x + 1j * y
+ b = -u + 1j * z
+
+ arga = np.angle(a)
+ moda = np.absolute(a)
+ argb = np.angle(b)
+
+ theta = -2 * np.arccos(moda)
+ psi = -0.5 * np.pi - arga + argb
+ phi = -arga + np.pi * 0.5 - argb
+ return psi, theta, phi
+
+ @lru_cache
+ def sched_p(self, t):
+ """The schedule at a time t is poly(t/finalT)"""
+ return self.derpoly(t / self.finalT) / self.finalT
+
+ @lru_cache
+ def eigenvalp(self, l, s, sp):
+ return sp * (2 * s - 1) / l
+
+ @lru_cache
+ def n(self, l, s):
+ return (1 - s) / 2 / np.sqrt(l * (l - s))
+
+ @lru_cache
+ def nder(self, l, s, lp, sp):
+ roote = l * (l - s)
+ upder = (1 - s) * (2 * lp * l - lp * s - sp * l)
+ inter = -sp - upder / 2 / roote
+ return 1 / 2 / np.sqrt(roote) * inter
+
+ @lru_cache
+ def f(self, l, s):
+ return (l - s) / (1 - s)
+
+ @lru_cache
+ def fp(self, l, s, lp, sp):
+ return (lp - sp - lp * s + sp * l) / (1 - s) ** 2
+
+ @lru_cache
+ def rotation_angles(self, t):
+ """Calculates rotation angles."""
+
+ I = self.integral_eigen(t)
+ l = self.eigenval(t)
+ s = self.sched(t)
+
+ fac = self.f(l, s)
+
+ norma = self.n(l, s)
+ inside00 = gt = 1 + fac**2 + 2 * fac * np.cos(2 * I)
+
+ absu00 = norma * np.sqrt(inside00)
+
+ upf = 1 - l
+ dpf = 1 + l - 2 * s
+ sinIt = np.sin(I)
+ cosIt = np.cos(I)
+
+ arga = np.arctan2(sinIt * upf, cosIt * dpf)
+ argb = np.arctan2(sinIt * dpf, -cosIt * upf)
+
+ theta = -2 * np.arccos(absu00)
+ psi = -0.5 * np.pi - arga + argb
+ phi = -arga + np.pi * 0.5 - argb
+
+ return phi, theta, psi
+
+ @lru_cache
+ def derivative_rotation_angles(self, t):
+ """Calculates derivatives of the rotation angles."""
+ s = self.sched(t)
+ l = self.eigenval(t)
+ I = self.integral_eigen(t)
+
+ derI = l
+ sp = self.sched_p(t)
+ lp = self.eigenvalp(l, s, sp)
+
+ nt = self.n(l, s)
+ ntp = self.nder(l, s, lp, sp)
+
+ ft = self.f(l, s)
+ ftp = self.fp(l, s, lp, sp)
+
+ gt = 1 + ft**2 + 2 * ft * np.cos(2 * I)
+
+ # Terms of the final sum for the derivative of the theta
+ x1 = ntp * np.sqrt(gt)
+ y1 = 2 * ft * ftp
+
+ y2 = 2 * ftp * np.cos(2 * I)
+ y3 = -2 * ft * np.sin(2 * I) * (2 * derI)
+
+ dgt = y1 + y2 + y3
+
+ absu00 = nt * np.sqrt(gt)
+ dabsu = x1 + nt / np.sqrt(gt) * (dgt) / 2.0
+ darcos = 2.0 / np.sqrt(1 - absu00**2)
+ dtheta = darcos * dabsu
+
+ # Let's do the derivative of the phi,psi
+ upf = 1 - l
+ dpf = 1 + l - 2 * s
+ tanI = np.tan(I)
+
+ dtan = l / np.cos(I) ** 2
+ dfrac_01 = 2 * (lp - sp + sp * l - s * lp) / upf**2
+ dfrac_00 = -2 * (lp - sp + sp * l - s * lp) / dpf**2
+
+ inside_arga = tanI * (upf / dpf)
+ inside_argb = -tanI * (dpf / upf)
+
+ dinside_arga = dtan * (upf / dpf) + tanI * dfrac_00
+ darga = dinside_arga / (1 + inside_arga**2)
+
+ dinside_argb = -dtan * (dpf / upf) - tanI * dfrac_01
+ dargb = dinside_argb / (1 + inside_argb**2)
+
+ dpsi = -darga + dargb
+ dphi = -darga - dargb
+
+ return dphi, dtheta, dpsi
+
+ @lru_cache
+ def rotations_circuit(self, t):
+ psi, theta, phi = self.rotation_angles(t)
+
+ c = qibo.models.Circuit(self.nqubits, density_matrix=True)
+
+ # H gate
+ c.add(qibo.gates.RZ(q=self.q, theta=np.pi / 2, trainable=False))
+ c.add(qibo.gates.RX(q=self.q, theta=np.pi / 2, trainable=False))
+ c.add(qibo.gates.RZ(q=self.q, theta=np.pi / 2, trainable=False))
+
+ # RZ(psi)
+ c.add(qibo.gates.RZ(q=self.q, theta=psi))
+
+ # RX(theta)
+ c.add(qibo.gates.RZ(q=self.q, theta=np.pi / 2, trainable=False))
+ c.add(qibo.gates.RX(q=self.q, theta=-np.pi / 2, trainable=False))
+ c.add(qibo.gates.RZ(q=self.q, theta=-theta))
+ c.add(qibo.gates.RX(q=self.q, theta=np.pi / 2, trainable=False))
+
+ # RZ(phi)
+ c.add(qibo.gates.RZ(q=self.q, theta=phi))
+
+ c.add(qibo.gates.M(self.q))
+
+ return c
+
+ @lru_cache
+ def numeric_derivative(self, t, h=1e-7):
+ # Do the derivative with 4 points
+ a1, b1, c1 = self.rotation_angles(t + 2 * h)
+ a2, b2, c2 = self.rotation_angles(t + h)
+ a3, b3, c3 = self.rotation_angles(t - h)
+ a4, b4, c4 = self.rotation_angles(t - 2 * h)
+
+ dd1 = (-a1 + 8 * a2 - 8 * a3 + a4) / 12 / h
+ dd2 = (-b1 + 8 * b2 - 8 * b3 + b4) / 12 / h
+ dd3 = (-c1 + 8 * c2 - 8 * c3 + c4) / 12 / h
+ return dd1, dd2, dd3
diff --git a/examples/adiabatic_qml/qaml_scripts/training.py b/examples/adiabatic_qml/qaml_scripts/training.py
new file mode 100644
index 000000000..fd0a71beb
--- /dev/null
+++ b/examples/adiabatic_qml/qaml_scripts/training.py
@@ -0,0 +1,175 @@
+import matplotlib.pyplot as plt
+import numpy as np
+from qaml_scripts.evolution import perform_adiabatic
+
+import qibo
+
+
+def train_adiabatic_evolution(
+ nsteps,
+ xarr,
+ cdf,
+ training_n,
+ init_params,
+ e0,
+ e1,
+ target_loss,
+ finalT,
+ h0,
+ h1,
+ obs_target,
+):
+ """Train the adiabatic evolution to fit a target empirical CDF"""
+
+ # --------------------------- PLOTTING FUNCTION -----------------------------------------------
+
+ def plot_for_energies(parameters, label="", true_law=None, title=""):
+ """Plot energies, training points and CDF for a set of energies given a set of parameters"""
+ energies = perform_adiabatic(
+ params=parameters,
+ finalT=finalT,
+ h0=h0,
+ h1=h1,
+ obs_target=obs_target,
+ )
+
+ plt.title(title)
+ plt.plot(xarr, -np.array(cdf), label="eCDF", color="black", lw=1, ls="--")
+ plt.plot(
+ xarr, -np.array(energies), label=label, color="purple", lw=2, alpha=0.8
+ )
+ plt.plot(
+ xarr[idx_training],
+ -np.array(cdf_training),
+ "o",
+ label="Training points",
+ color="orange",
+ alpha=0.85,
+ markersize=8,
+ )
+ if true_law != None:
+ plt.plot(xarr, true_law, c="orange", lw=1, ls="--")
+ plt.xlabel("x")
+ plt.ylabel("cdf")
+ plt.legend()
+ plt.show()
+
+ # ----------------------------- LOSS FUNCTION ---------------------------------------------
+
+ def loss_evaluation(params, penalty=True):
+ """Evaluating loss function related to the cdf fit"""
+
+ # Retrieve the energy per time step for this set of parameters
+ energies = perform_adiabatic(
+ params=params,
+ finalT=finalT,
+ h0=h0,
+ h1=h1,
+ obs_target=obs_target,
+ )
+
+ # Select the points we are training on
+ e_train = energies[idx_training]
+
+ loss = np.mean((e_train - cdf_training) ** 2 / norm_cdf)
+
+ if penalty:
+ # Penalty term for negative derivative
+ delta_energy = good_direction * np.diff(energies)
+ # Remove non-monotonous values
+ delta_energy *= delta_energy < 0
+
+ pos_penalty = np.abs(np.sum(delta_energy))
+ val_loss = loss
+
+ loss = val_loss + pos_penalty
+
+ return loss
+
+ # ------------------------------ GENETIC ALGORITHM CALL --------------------------------------------------
+
+ def optimize(
+ force_positive=False,
+ target=5e-2,
+ max_iterations=50000,
+ max_evals=500000,
+ initial_p=None,
+ ):
+ """Use Qibo to optimize the parameters of the schedule function"""
+
+ options = {
+ "verbose": -1,
+ "tolfun": 1e-12,
+ "ftarget": target, # Target error
+ "maxiter": max_iterations, # Maximum number of iterations
+ "maxfeval": max_evals, # Maximum number of function evaluations
+ "maxstd": 20,
+ }
+
+ if force_positive:
+ options["bounds"] = [0, 1e5]
+
+ if initial_p is None:
+ initial_p = initial_p
+ else:
+ print("Reusing previous best parameters")
+
+ result = qibo.optimizers.optimize(
+ loss_evaluation, initial_p, method="cma", options=options
+ )
+
+ return result, result[1]
+
+ # ------------------------------ BUILD TRAINING SET AND OPTIMIZE! --------------------------------
+
+ # Definition of the loss function and optimization routine
+ good_direction = 1 if (e1 - e0) > 0 else -1
+
+ # But select those for which the difference between them is greater than some threshold
+ min_step = 1e-3
+ # but never go more than max_skip points without selecting one
+ max_skip = 0
+
+ if training_n > nsteps:
+ raise Exception("The notebook cannot run with nsteps < training_n")
+
+ # Select a subset of points for training, but skip first and include last
+ idx_training_raw = np.linspace(0, nsteps, num=training_n, endpoint=True, dtype=int)[
+ 1:
+ ]
+
+ # And, from this subset, remove those that do not add that much info
+ idx_training = []
+ cval = cdf[0]
+ nskip = 0
+ for p in idx_training_raw[:-2]:
+ diff = cval - cdf[p]
+ if diff > min_step or nskip > max_skip:
+ nskip = 0
+ idx_training.append(p)
+ cval = cdf[p]
+ else:
+ nskip += 1
+
+ idx_training.append(idx_training_raw[-1])
+
+ cdf_training = cdf[idx_training]
+ norm_cdf = np.abs(
+ cdf_training
+ ) # To normalize the points according to their absolute value
+
+ # Definition of the loss function and optimization routine
+ good_direction = 1 if (e1 - e0) > 0 else -1
+
+ # Fit before training
+ plot_for_energies(init_params, label="Initial state", title="Not trained evolution")
+ print(f"Training on {len(idx_training)} points of the total of {nsteps}")
+
+ _, best_params = optimize(
+ target=target_loss, force_positive=False, initial_p=init_params
+ )
+
+ # Fit after training
+ plot_for_energies(best_params, label="Initial state", title="Trained evolution")
+
+ return best_params
diff --git a/examples/anomaly_detection/README.md b/examples/anomaly_detection/README.md
new file mode 100644
index 000000000..6dd2cd8bd
--- /dev/null
+++ b/examples/anomaly_detection/README.md
@@ -0,0 +1,91 @@
+# Anomaly detection with variational quantum circuits
+
+This tutorial presents a simplified version of ['Long-Lived Particles Anomaly Detection with Parametrized Quantum Circuits'](https://doi.org/10.3390/particles6010016).
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/anomaly_detection](https://github.com/qiboteam/qibo/tree/master/examples/anomaly_detection).
+
+## Problem overview
+
+With the contemporary peak in interest regarding machine learning algorithms for their many applications in scientific research as well as industrial technology, we also have the simultaneous development of quantum computing. A combination of these two fields has lead to the development of quantum machine learning algorithms.
+With this example we want to study the quantum version of a classic machine learning algorithm known as [anomaly detection](https://arxiv.org/abs/2007.02500). This algorithm is implemented with an artificial neural network, in particular an [autoencoder](https://arxiv.org/abs/2003.05991). In quantum machine learning, the autoencoder is realised using a variational quantum circuit. The proposed algorithm is not meant to outperform the classical counterpart on classical data. This work aims to demonstrate that it is possible to use quantum variational algorithms for anomaly detection with possible future advantages in the analysis of quantum data.
+
+## Background
+
+In this section we want to explain the main elements to understand the proposed quantum anomaly detection algorithm.
+
+### Anomaly detection
+
+Anomaly detection is a classification algorithm that allows to identify anomalous data. The advantage in using this machine learning technique is that only a dataset with non anomalous (standard) data samples is required for the training.
+To achieve this it's necessary to train a particular artificial neural network (ANN) architecture called autoencoder. An autoencoder is composed of two main parts: encoder and decoder.
+
+![Autoencoder architecture](images/Fig1.png)
+
+The encoder compresses initial data down to a small dimension (called latent dimension). The decoder inverts the process to reconstruct the original data from the compressed one. The parameters of the neural network are trained in order to minimize the difference between the initial and reconstructed data. The loss function (also called reconstruction loss) is therefore a measure of how accurately the reconstructed data resembles the original.
+
+For anomaly detection, the autoencoder is trained only on data samples belonging to the standard class. When the trained model is applied to new samples we expect the loss function to have different values for standard and anomalous data.
+By choosing a threshold value for the loss function it is possible to classify an input based on whether its reconstruction loss lands above or below this threshold. The ROC curve (Receiver Operating Characteristic) indicates the true positive rate and false positive rate as a function of the threshold. This can help to set the threshold value in order to maximize true positive classifications and minimize false positives.
+
+### Variational quantum circuits
+
+A Variational Quantum Circuit (VQC), also known as parametrized quantum circuit, can be used as the quantum counterpart of classical ANNs. In this kind of circuits the input information is stored in the initial state of the qubits. It can be stored as the phase (phase encoding) or in the states amplitudes (amplitude encoding). The initial state is transformed using rotation gates and entangling gates, usually controlled-not (C-NOT) gates. These gates can be organised in layers, in this circuit architecture one layer is composed of rotation gates (R_x, R_y, R_z) acting on all qubits followed by a series of C-NOT gates coupling neighbouring qubits. The trainable weights are the angles of rotation gates and can be trained using standard backpropagation (implemented with Tensorflow).
+
+![variational quantum circuit (one layer)](images/Fig2.png)
+
+A quantum circuit implements a unitary, thus invertible, transformation on the initial state. This represents a great advantage for the autoencoder architecture, as the decoder can be taken as the inverse of the encoder quantum circuit. In order to compress information the encoder circuit has to disentangle and set to zero state a given number of qubits. The loss function is thus taken as the expected measurement values of these qubits. In this way, for the training of the circuit, it is necessary only the encoder.
+
+![Quantum autoencoder](images/Fig3.png)
+
+## Algorithm implementation
+
+This section refers to the optimal algorithm parameters (default).
+
+Anomaly detection on handwritten digits is carried out on the MNIST dataset using zeros as the standard data and ones as the anomalous data. We compressed the images down to 8X8 pixels, in this way it is possible to encode initial data in six qubits. It is necessary to normalise the initial data array so that it can be encoded as state amplitudes.
+The best configuration has been found with six layers and three compressed qubits. For entangling gates we have tested different C-NOT configurations, the one that gave better performance is reported in figure below.
+Moreover this configuration requires only nearest neighbour connectivity for six qubits placed in a ring topology. In order to improve the performance, rotation gates with trainable parameters were added at the end of the encoder circuit for the three compressed qubits. A summary of the employed circuit is reported in the next figure.
+
+![Circuit ansatz](images/Fig4.png)
+
+### Training
+
+For the training of the circuit a dataset of 5000 images of zero handwritten digits has been employed. The loss function is the sum of the probabilities of the ground state for the first three qubits, thus these qubits are forced to the |1> state.
+Training has been performed for 20 epochs using [Adam optimizer](https://arxiv.org/pdf/1412.6980.pdf), with a dynamic learning rate that spans from 0.4 in the first epochs to 0.001 in the last ones. This variable learning rate has helped reducing the problem of [barren plateaus](https://arxiv.org/pdf/1803.11173.pdf).
+
+### Performance evaluation
+
+To test the anomaly detection algorithm after the training phase, we have used 2000 standard images not used in the training and 2000 anomalous images. Figure below shows the loss distribution for the two test datasets.
+
+![Loss function distribution](images/Fig5.png)
+
+The ROC curve shows the rate of true positive with respect to the rate of false positive by moving the loss value threshold for the binary classification.
+
+![ROC curve](images/Fig6.png)
+
+## How to run an example?
+
+The code is divided into two parts, training of the circuit (`train.py`) and performance evaluation (`test.py`).
+
+It is possible to define the following hyper-parameters for the training of the circuit (default have good performance):
+- `n_layers` (int): number of ansatz circuit layers (default 6).
+- `batch_size` (int): number of samples in one training batch (default 20).
+- `nepochs` (int): number of training epochs (default 20).
+- `train_size` (int): number of samples used for training, the remainings are used for performance evaluation, total samples are 7000 (default 5000).
+- `filename` (str): location and file name where trained parameters are saved (default "parameters/trained_params.npy").
+- `lr_boundaries` (list): epochs when learning rate is reduced, 6 monotone growing values from 0 to nepochs (default [3,6,9,12,15,18]).
+
+It is possible to define the following hyper-parameters for the performance evaluation of the circuit, `n_layers` must be equal to the one used for training:
+- `n_layers` (int): number of ansatz circuit layers (default 6).
+- `train_size` (int): number of samples used for training, the remainings are used for performance evaluation, total samples are 7000 (default 5000).
+- `filename` (str): location and file name of trained parameters to be tested (default "parameters/trained_params.npy").
+- `plot` (bool): make plots of ROC and loss function distribution (default True).
+- `save_loss` (bool): save losses for standard and anomalous data (default False).
+
+As an example, in order to use 4 layers in the variational quantum ansatz, you should execute the following command for training:
+
+```bash
+python train.py --n_layers 4
+```
+And the following command for performance evaluation:
+
+```bash
+python test.py --n_layers 4
+```
diff --git a/examples/anomaly_detection/data/anomalous_data.npy b/examples/anomaly_detection/data/anomalous_data.npy
new file mode 100644
index 000000000..0d57f3d40
Binary files /dev/null and b/examples/anomaly_detection/data/anomalous_data.npy differ
diff --git a/examples/anomaly_detection/data/standard_data.npy b/examples/anomaly_detection/data/standard_data.npy
new file mode 100644
index 000000000..7bd92673e
Binary files /dev/null and b/examples/anomaly_detection/data/standard_data.npy differ
diff --git a/examples/anomaly_detection/images/Fig1.png b/examples/anomaly_detection/images/Fig1.png
new file mode 100644
index 000000000..7c072874d
Binary files /dev/null and b/examples/anomaly_detection/images/Fig1.png differ
diff --git a/examples/anomaly_detection/images/Fig2.png b/examples/anomaly_detection/images/Fig2.png
new file mode 100644
index 000000000..858a00658
Binary files /dev/null and b/examples/anomaly_detection/images/Fig2.png differ
diff --git a/examples/anomaly_detection/images/Fig3.png b/examples/anomaly_detection/images/Fig3.png
new file mode 100644
index 000000000..e7f2b8cdf
Binary files /dev/null and b/examples/anomaly_detection/images/Fig3.png differ
diff --git a/examples/anomaly_detection/images/Fig4.png b/examples/anomaly_detection/images/Fig4.png
new file mode 100644
index 000000000..7f2c7a35e
Binary files /dev/null and b/examples/anomaly_detection/images/Fig4.png differ
diff --git a/examples/anomaly_detection/images/Fig5.png b/examples/anomaly_detection/images/Fig5.png
new file mode 100644
index 000000000..b559fd425
Binary files /dev/null and b/examples/anomaly_detection/images/Fig5.png differ
diff --git a/examples/anomaly_detection/images/Fig6.png b/examples/anomaly_detection/images/Fig6.png
new file mode 100644
index 000000000..f79c15eee
Binary files /dev/null and b/examples/anomaly_detection/images/Fig6.png differ
diff --git a/examples/anomaly_detection/parameters/trained_params.npy b/examples/anomaly_detection/parameters/trained_params.npy
new file mode 100644
index 000000000..e14de6839
Binary files /dev/null and b/examples/anomaly_detection/parameters/trained_params.npy differ
diff --git a/examples/anomaly_detection/test.py b/examples/anomaly_detection/test.py
new file mode 100644
index 000000000..043db58a0
--- /dev/null
+++ b/examples/anomaly_detection/test.py
@@ -0,0 +1,209 @@
+import argparse
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+import numpy as np
+import tensorflow as tf
+
+import qibo
+from qibo import Circuit, gates
+
+LOCAL_FOLDER = Path(__file__).parent
+
+
+def main(n_layers, train_size, filename, plot, save_loss):
+ """Implements performance evaluation of a trained circuit, as described in https://doi.org/10.3390/particles6010016.
+
+ Args:
+ n_layers (int): number of ansatz circuit layers (default 6).
+ train_size (int): number of samples used for training, the remainings are used for performance evaluation, total samples are 7000 (default 5000).
+ filename (str): location and file name of trained parameters to be tested (default "parameters/trained_params.npy").
+ plot (bool): make plots of ROC and loss function distribution (default True).
+ save_loss (bool): save losses for standard and anomalous data (default False).
+ """
+
+ qibo.set_backend("tensorflow")
+
+ # Circuit ansatz
+ def make_encoder(n_qubits, n_layers, params, q_compression):
+ """Create encoder quantum circuit.
+
+ Args:
+ n_qubits (int): number of qubits in the circuit.
+ n_layers (int): number of ansatz circuit layers.
+ params (tf.Tensor): parameters of the circuit.
+ q_compression (int): number of compressed qubits.
+
+ Returns:
+ encoder (qibo.models.Circuit): variational quantum circuit.
+ """
+
+ index = 0
+ encoder = Circuit(n_qubits)
+ for i in range(n_layers):
+ for j in range(n_qubits):
+ encoder.add(gates.RX(j, params[index]))
+ encoder.add(gates.RY(j, params[index + 1]))
+ encoder.add(gates.RZ(j, params[index + 2]))
+ index += 3
+
+ for j in range(n_qubits):
+ encoder.add(gates.CNOT(j, (j + 1) % n_qubits))
+
+ for j in range(q_compression):
+ encoder.add(gates.RX(j, params[index]))
+ encoder.add(gates.RY(j, params[index + 1]))
+ encoder.add(gates.RZ(j, params[index + 2]))
+ index += 3
+ return encoder
+
+ # Evaluate loss function (3 qubit compression) for one sample
+ def compute_loss_test(encoder, vector):
+ """Evaluate loss function for one test sample.
+
+ Args:
+ encoder (qibo.models.Circuit): variational quantum circuit (trained).
+ vector (tf.Tensor): test sample, in the form of 1d vector.
+
+ Returns:
+ loss (tf.Variable): loss of the test sample.
+ """
+ reconstructed = encoder(vector)
+ # 3 qubits compression
+ loss = (
+ reconstructed.probabilities(qubits=[0])[0]
+ + reconstructed.probabilities(qubits=[1])[0]
+ + reconstructed.probabilities(qubits=[2])[0]
+ )
+ return loss
+
+ # Other hyperparameters
+ n_qubits = 6
+ q_compression = 3
+
+ # Load and pre-process data
+ file_dataset_standard = LOCAL_FOLDER / "data" / "standard_data.npy"
+ dataset_np_s = np.load(file_dataset_standard)
+ dataset_np_s = dataset_np_s[train_size:]
+ dataset_s = tf.convert_to_tensor(dataset_np_s)
+ file_dataset_anomalous = LOCAL_FOLDER / "data" / "anomalous_data.npy"
+ dataset_np_a = np.load(file_dataset_anomalous)
+ dataset_np_a = dataset_np_a[train_size:]
+ dataset_a = tf.convert_to_tensor(dataset_np_a)
+
+ # Load trained parameters
+ trained_params_np = np.load(filename)
+ trained_params = tf.convert_to_tensor(trained_params_np)
+
+ # Create and print encoder circuit
+ encoder_test = make_encoder(n_qubits, n_layers, trained_params, q_compression)
+ encoder_test.compile()
+ print("Circuit model summary")
+ print(encoder_test.draw())
+
+ print("Computing losses...")
+ # Compute loss for standard data
+ loss_s = []
+ for i in range(len(dataset_np_s)):
+ loss_s.append(compute_loss_test(encoder_test, dataset_s[i]).numpy())
+
+ # Compute loss for anomalous data
+ loss_a = []
+ for i in range(len(dataset_np_a)):
+ loss_a.append(compute_loss_test(encoder_test, dataset_a[i]).numpy())
+
+ if save_loss:
+ file_loss_standard = LOCAL_FOLDER / "results" / "losses_standard_data.npy"
+ file_loss_anomalous = LOCAL_FOLDER / "results" / "losses_anomalous_data.npy"
+ np.save(file_loss_standard, loss_s)
+ np.save(file_loss_anomalous, loss_a)
+
+ # Make graphs for performance analysis
+ if plot:
+ """Loss distribution graph"""
+ plt.hist(loss_a, bins=60, histtype="step", color="red", label="Anomalous data")
+ plt.hist(loss_s, bins=60, histtype="step", color="blue", label="Standard data")
+ plt.ylabel("Number of images")
+ plt.xlabel("Loss value")
+ plt.title("Loss function distribution (MNIST dataset)")
+ plt.legend()
+ file_plot = LOCAL_FOLDER / "results" / "loss_distribution.png"
+ plt.savefig(file_plot)
+ plt.close()
+
+ """Compute ROC curve"""
+ max1 = np.amax(loss_s)
+ max2 = np.amax(loss_a)
+ ma = max(max1, max2)
+ min1 = np.amin(loss_s)
+ min2 = np.amin(loss_a)
+ mi = min(min1, min2)
+
+ tot_neg = len(loss_s)
+ tot_pos = len(loss_a)
+
+ n_step = 100.0
+ n_step_int = 100
+ step = (ma - mi) / n_step
+ fpr = []
+ tpr = []
+ for i in range(n_step_int):
+ treshold = i * step + mi
+ c = 0
+ for j in range(tot_neg):
+ if loss_s[j] > treshold:
+ c += 1
+ false_positive = c / float(tot_neg)
+ fpr.append(false_positive)
+ c = 0
+ for j in range(tot_pos):
+ if loss_a[j] > treshold:
+ c += 1
+ true_positive = c / float(tot_pos)
+ tpr.append(true_positive)
+
+ """Roc curve graph """
+ plt.title("Receiver Operating Characteristic")
+ plt.plot(fpr, tpr)
+ plt.xlim([0, 1])
+ plt.ylim([0, 1])
+ plt.ylabel("True Positive Rate")
+ plt.xlabel("False Positive Rate")
+ file_roc = LOCAL_FOLDER / "results" / "ROC.png"
+ plt.savefig(file_roc)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--n_layers",
+ default=6,
+ type=int,
+ help="(int): number of ansatz circuit layers",
+ )
+ parser.add_argument(
+ "--train_size",
+ default=5000,
+ type=int,
+ help="(int): number of samples used for training, the remainings are used for performance evaluation (total samples 7000)",
+ )
+ parser.add_argument(
+ "--filename",
+ default=LOCAL_FOLDER / "parameters" / "trained_params.npy",
+ type=str,
+ help="(str): location and file name of trained parameters to be tested",
+ )
+ parser.add_argument(
+ "--plot",
+ default=True,
+ type=bool,
+ help="(bool): make plots of ROC and loss function distribution",
+ )
+ parser.add_argument(
+ "--save_loss",
+ default=False,
+ type=bool,
+ help="(bool): save losses for standard and anomalous data",
+ )
+ args = parser.parse_args()
+ main(**vars(args))
diff --git a/examples/anomaly_detection/train.py b/examples/anomaly_detection/train.py
new file mode 100644
index 000000000..0f662a01f
--- /dev/null
+++ b/examples/anomaly_detection/train.py
@@ -0,0 +1,205 @@
+import argparse
+import math
+from pathlib import Path
+
+import numpy as np
+import tensorflow as tf
+
+import qibo
+from qibo import Circuit, gates
+
+LOCAL_FOLDER = Path(__file__).parent
+
+
+def main(n_layers, batch_size, nepochs, train_size, filename, lr_boundaries):
+ """Implements training of variational quantum circuit, as described in https://doi.org/10.3390/particles6010016.
+
+ Args:
+ n_layers (int): number of ansatz circuit layers (default 6).
+ batch_size (int): number of samples in one training batch (default 20).
+ nepochs (int): number of training epochs (default 20).
+ train_size (int): number of samples used for training, the remainings are used for performance evaluation, total samples are 7000 (default 5000).
+ filename (str): location and file name where trained parameters are saved (default "parameters/trained_params.npy").
+ lr_boundaries (list): epochs when learning rate is reduced, 6 monotone growing values from 0 to nepochs (default [3,6,9,12,15,18]).
+ """
+
+ qibo.set_backend("tensorflow")
+
+ # Circuit ansatz
+ def make_encoder(n_qubits, n_layers, params, q_compression):
+ """Create encoder quantum circuit.
+
+ Args:
+ n_qubits (int): number of qubits in the circuit.
+ n_layers (int): number of ansatz circuit layers.
+ params (tf.Variable): parameters of the circuit.
+ q_compression (int): number of compressed qubits.
+
+ Returns:
+ encoder (qibo.models.Circuit): variational quantum circuit.
+ """
+
+ index = 0
+ encoder = Circuit(n_qubits)
+ for i in range(n_layers):
+ for j in range(n_qubits):
+ encoder.add(gates.RX(j, params[index]))
+ encoder.add(gates.RY(j, params[index + 1]))
+ encoder.add(gates.RZ(j, params[index + 2]))
+ index += 3
+
+ for j in range(n_qubits):
+ encoder.add(gates.CNOT(j, (j + 1) % n_qubits))
+
+ for j in range(q_compression):
+ encoder.add(gates.RX(j, params[index]))
+ encoder.add(gates.RY(j, params[index + 1]))
+ encoder.add(gates.RZ(j, params[index + 2]))
+ index += 3
+ return encoder
+
+ # Evaluate loss function (3 qubit compression) for one sample
+ @tf.function
+ def compute_loss(encoder, params, vector):
+ """Evaluate loss function for one train sample.
+
+ Args:
+ encoder (qibo.models.Circuit): variational quantum circuit.
+ params (tf.Variable): parameters of the circuit.
+ vector (tf.Tensor): train sample, in the form of 1d vector.
+
+ Returns:
+ loss (tf.Variable): loss of the training sample.
+ """
+
+ encoder.set_parameters(params)
+ reconstructed = encoder(vector)
+ # 3 qubits compression
+ loss = (
+ reconstructed.probabilities(qubits=[0])[0]
+ + reconstructed.probabilities(qubits=[1])[0]
+ + reconstructed.probabilities(qubits=[2])[0]
+ )
+ return loss
+
+ # One optimization step
+ @tf.function
+ def train_step(batch_size, encoder, params, dataset):
+ """Evaluate loss function on one train batch.
+
+ Args:
+ batch_size (int): number of samples in one training batch.
+ encoder (qibo.models.Circuit): variational quantum circuit.
+ params (tf.Variable): parameters of the circuit.
+ vector (tf.Tensor): train sample, in the form of 1d vector.
+
+ Returns:
+ loss (tf.Variable): average loss of the training batch.
+ """
+
+ loss = 0.0
+ with tf.GradientTape() as tape:
+ for sample in range(batch_size):
+ loss = loss + compute_loss(encoder, params, dataset[sample])
+ loss = loss / batch_size
+ grads = tape.gradient(loss, params)
+ optimizer.apply_gradients(zip([grads], [params]))
+ return loss
+
+ # Other hyperparameters
+ n_qubits = 6
+ q_compression = 3
+
+ # Load and pre-process data
+ file_dataset = LOCAL_FOLDER / "data" / "standard_data.npy"
+ dataset_np = np.load(file_dataset)
+ dataset = tf.convert_to_tensor(dataset_np)
+ train = dataset[0:train_size]
+
+ # Initialize random parameters
+ n_params = (n_layers * n_qubits + q_compression) * 3
+ params = tf.Variable(tf.random.normal((n_params,)))
+
+ # Create and print encoder circuit
+ encoder = make_encoder(n_qubits, n_layers, params, q_compression)
+ print("Circuit model summary")
+ print(encoder.draw())
+
+ # Define optimizer parameters
+ steps_for_epoch = math.ceil(train_size / batch_size)
+ boundaries = [
+ steps_for_epoch * lr_boundaries[0],
+ steps_for_epoch * lr_boundaries[1],
+ steps_for_epoch * lr_boundaries[2],
+ steps_for_epoch * lr_boundaries[3],
+ steps_for_epoch * lr_boundaries[4],
+ steps_for_epoch * lr_boundaries[5],
+ ]
+ values = [0.4, 0.2, 0.08, 0.04, 0.01, 0.005, 0.001]
+ learning_rate_fn = tf.keras.optimizers.schedules.PiecewiseConstantDecay(
+ boundaries, values
+ )
+ optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate_fn)
+
+ # This array contains the parameters at each epoch
+ trained_params = np.zeros((nepochs, n_params), dtype=float)
+
+ # Training
+ print("Trained parameters will be saved in: ", filename)
+ print("Start training")
+ for epoch in range(nepochs):
+ tf.random.shuffle(train)
+ for i in range(steps_for_epoch):
+ loss = train_step(
+ batch_size,
+ encoder,
+ params,
+ train[i * batch_size : (i + 1) * batch_size],
+ )
+ trained_params[epoch] = params.numpy()
+ print("Epoch: %d Loss: %f" % (epoch + 1, loss))
+
+ # Save parameters of last epoch
+ np.save(filename, trained_params[-1])
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--n_layers",
+ default=6,
+ type=int,
+ help="(int): number of ansatz circuit layers",
+ )
+ parser.add_argument(
+ "--batch_size",
+ default=20,
+ type=int,
+ help="(int): number of samples in one training batch",
+ )
+ parser.add_argument(
+ "--nepochs",
+ default=20,
+ type=int,
+ help="(int): number of training epochs",
+ )
+ parser.add_argument(
+ "--train_size",
+ default=5000,
+ type=int,
+ help="(int): number of samples used for training, the remainings are used for performance evaluation (total samples 7000)",
+ )
+ parser.add_argument(
+ "--filename",
+ default=LOCAL_FOLDER / "parameters" / "trained_params.npy",
+ type=str,
+ help="(str): location and file name where trained parameters are saved",
+ )
+ parser.add_argument(
+ "--lr_boundaries",
+ default=[3, 6, 9, 12, 15, 18],
+ type=list,
+ help="(list): epochs when learning rate is reduced (6 monotone growing values from 0 to nepochs)",
+ )
+ args = parser.parse_args()
+ main(**vars(args))
diff --git a/examples/autoencoder/README.md b/examples/autoencoder/README.md
new file mode 100644
index 000000000..2d920ae4f
--- /dev/null
+++ b/examples/autoencoder/README.md
@@ -0,0 +1,42 @@
+# Quantum autoencoder for data compression
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/autoencoder](https://github.com/qiboteam/qibo/tree/master/examples/autoencoder).
+
+## Problem overview
+
+The task of an autoencoder given an input *x*, is to map *x* to a lower dimensional point *y* such that *x* can likely be recovered from *y*. Specifically, once we obtain *y*, we have effectively compressed the input *x*.
+
+Given that quantum mechanics is able to generate different patterns compared to classical physics, a quantum autoencoder should be able to recognize patterns beyond classical capabilities.
+
+Recall that a limiting factor for near applications is the amount of quantum resources that can be realized in an experiment. Therefore, for experiments in the near future, a quantum autoencoder which can reduce the experimental overhead in terms of these resources is especially valuable.
+
+
+## Implementing the solution
+
+The code herein aims to implement a quantum autoencoder, partly based on the manuscript ["Quantum autoencoders for efficient compression of quantum data"](https://iopscience.iop.org/article/10.1088/2058-9565/aa8072).
+
+A graphical depiction of a quantum encoder can be seen in the following figure. In a quantum encoder the information contained in some of the input qubits must be discarded after the initial encoding. Then, fresh qubits (here initialized to the |0> state, but one may consider any other easy-to-construct reference state) are prepared and used to implement the final decoding, which is finally compared to the initial state.
+
+![autoencoder](images/autoencoder.png)
+
+The learning task for a quantum autoencoder is to find unitaries which preserve the quantum information of the input through the smaller intermediate latent space. Here, we propose a cost function designed from local operators, possibly avoiding [trainability concerns](https://www.nature.com/articles/s41467-018-07090-4). A figure of merit for the wrong answer when training is simply the total amount of non-zero measurement outcomes on the discarded qubits, which shall be minimized. In order to design the cost function to be local, different outcomes may be penalized by their Hamming distance to the |0> state, which is just the number of symbols that are different in the binary representation. Notice that this cost function has a value of zero if and only if the compression is successfully completed. Note as well that it is defined in terms of local observables and therefore, it does not suffer, for circuits of depth O(log n), from the problem of [exponentially vanishing gradients](https://arxiv.org/abs/2001.00550).
+
+To implement the quantum autoencoder model on a quantum computer we must define the form of the parametrized unitary, decomposing it into a quantum circuit suitable for optimization. In the following figure we depict the ansatz that we have considered. It consists of layers composed of CZ gates acting on alternating pairs of neighboring qubits which are preceded by Ry qubit rotations. After implementing the layered ansatz, a final layer of Ry qubit gates is applied. Finally, measurements on the desired discarded qubits have to be performed for the training. In this example, we encode the Ising model for different λ.
+
+![ansatz1](images/ansatz-1.png)
+
+## How to run an example?
+
+To run a particular instance of the problem we have to set up the initial
+arguments:
+- `nqubits` (int): number of quantum bits.
+- `layers` (int): number of ansatz layers.
+- `compress` (int): number of compressed/discarded qubits.
+- `lambdas` (list or array): different λ on the Ising model to consider for the training.
+
+As an example, in order to compress 2 qubits on an initial quantum state with 6 qubits, and using 3 layers,
+you should execute the following command:
+
+```python
+python main.py --nqubits 6 --layers 3 --compress 2 --lambdas [0.9, 0.95, 1.0, 1.05, 1.10]
+```
diff --git a/examples/autoencoder/images/ansatz-1.png b/examples/autoencoder/images/ansatz-1.png
new file mode 100644
index 000000000..02d80ac0e
Binary files /dev/null and b/examples/autoencoder/images/ansatz-1.png differ
diff --git a/examples/autoencoder/images/autoencoder.png b/examples/autoencoder/images/autoencoder.png
new file mode 100644
index 000000000..186e2d250
Binary files /dev/null and b/examples/autoencoder/images/autoencoder.png differ
diff --git a/examples/autoencoder/main.py b/examples/autoencoder/main.py
new file mode 100644
index 000000000..024d7e94b
--- /dev/null
+++ b/examples/autoencoder/main.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+import argparse
+
+import numpy as np
+from scipy.optimize import minimize
+
+from qibo import gates, hamiltonians, models
+
+
+def main(nqubits, layers, compress, lambdas, maxiter):
+ def encoder_hamiltonian_simple(nqubits, ncompress):
+ """Creates the encoding Hamiltonian.
+ Args:
+ nqubits (int): total number of qubits.
+ ncompress (int): number of discarded/trash qubits.
+
+ Returns:
+ Encoding Hamiltonian.
+ """
+ m0 = hamiltonians.Z(ncompress).matrix
+ m1 = np.eye(2 ** (nqubits - ncompress), dtype=m0.dtype)
+ ham = hamiltonians.Hamiltonian(nqubits, np.kron(m1, m0))
+ return 0.5 * (ham + ncompress)
+
+ def cost_function(params, count):
+ """Evaluates the cost function to be minimized.
+
+ Args:
+ params (array or list): values of the parameters.
+
+ Returns:
+ Value of the cost function.
+ """
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ for q in range(0, nqubits - 1, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+ for q in range(1, nqubits - 2, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ circuit.add(gates.CZ(0, nqubits - 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=0))
+
+ cost = 0
+ circuit.set_parameters(
+ params
+ ) # this will change all thetas to the appropriate values
+ for i in range(len(ising_groundstates)):
+ final_state = circuit(np.copy(ising_groundstates[i]))
+ cost += np.real(encoder.expectation(final_state.state()))
+
+ if count[0] % 50 == 0:
+ print(count[0], cost / len(ising_groundstates))
+ count[0] += 1
+
+ return cost / len(ising_groundstates)
+
+ nparams = 2 * nqubits * layers + nqubits
+ initial_params = np.random.uniform(0, 2 * np.pi, nparams)
+ encoder = encoder_hamiltonian_simple(nqubits, compress)
+
+ ising_groundstates = []
+ for lamb in lambdas:
+ ising_ham = -1 * hamiltonians.TFIM(nqubits, h=lamb)
+ ising_groundstates.append(ising_ham.eigenvectors()[0])
+
+ count = [0]
+ result = minimize(
+ lambda p: cost_function(p, count),
+ initial_params,
+ method="L-BFGS-B",
+ options={"maxiter": maxiter, "maxfun": 2.0e3},
+ )
+
+ print("Final parameters: ", result.x)
+ print("Final cost function: ", result.fun)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=4, type=int)
+ parser.add_argument("--layers", default=2, type=int)
+ parser.add_argument("--compress", default=2, type=int)
+ parser.add_argument("--lambdas", default=[0.9, 0.95, 1.0, 1.05, 1.10], type=list)
+ parser.add_argument("--maxiter", default=2000, type=int)
+ args = parser.parse_args()
+ main(**vars(args))
diff --git a/examples/bell-variational/README.md b/examples/bell-variational/README.md
new file mode 100644
index 000000000..f5c2aef2d
--- /dev/null
+++ b/examples/bell-variational/README.md
@@ -0,0 +1,49 @@
+# Maximal violation of Bell inequalities variationally
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/bell-variational](https://github.com/qiboteam/qibo/tree/master/examples/bell-variational)
+
+## Introduction
+
+During the advent of quantum mechanics, the properties of entanglement, or non-local correlations between quantum systems, were discussed and even doubted by prominent physicists. John Bell proposed an experiment where non-local interactions would be proven using correlations between measurements on two different qubits.
+
+This experiment has been performed and proven multiple times since. Now that quantum computers are available, the proof of quantum non-locality is not only an interesting exercise but also an easy way to quickly gauge how "quantum" a device is.
+
+## Bell's theorem
+
+In his original [paper](https://journals.aps.org/ppf/pdf/10.1103/PhysicsPhysiqueFizika.1.195), John Bell proposed a test for the nature of quantum mechanics in response to Einstein, Podolski and Rosen's famous [thought experiment](https://journals.aps.org/pr/abstract/10.1103/PhysRev.47.777). He argued that if quantum mechanics was a local hidden-variable theory, the correlations of measurements on two entangled bits need to satisfy certain constraints.
+
+These constraints are usually presented with inequalities, the most used of them being the [CHSH inequality](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.23.880) presented a bit later.
+
+What at the time were but proposals, when tested by real-life experiments it was later found that violation of Bell inequalities existed in nature, proving that quantum mechanics was indeed a non-local theory thaht could not be understood under a local hidden variable formalism.
+
+## CHSH inequality
+
+The CHSH inequality, named after its creators, is usually displayed as:
+
+![circuit](images/chshequation.png)
+
+where a, a', b and b' are the settings of the detectors of qubit A and B respectively, and E(a, b) are the quantum correlations between the measurements of both systems.
+
+If the theory of local hidden variables holds, the absolute value of S is upper-bounded by 2. However, in the case that quantum mechanics is a non-local theory, that is not the case and the absolute value of S goes as high as 2 times sqrt(2).
+
+This maximum violation only occurs on maximally entangled states and with a specific setting of the (a, a', b, b') parameters. These parameters are known, but the aim of this example is to variationally find the optimal parameters in order to fully violate this inequality.
+
+## Setup of the example
+
+This example consists of a very simple parametrized circuit with two variational angles. The first one sets up the state, and the second one sets up the direction of the measurements. Then the circuit is measured in all possible combinations of the Z and X basis.
+
+![circuit](images/circuit.png)
+
+Ideally, the circuit should prepare a maximally entangled state, by setting the first rotation parameter to, for example, 90 degrees, and choosing a measurement direction of 45 (225) in degrees.
+
+### Aim of the example
+
+This small example is designed to be an early test for small quantum devices, in order to test both the quantum nature of the chip and the device's handling of an easy variational circuit.
+
+## How to run the example?
+
+Run the `main.py` file from the console in order to perform the minimization.
+
+By modifying the flag `--nshots` one can control the number of samples to take for each quantum circuit. The more samples taken the more costly is the algorithm but the smoother the cost function is in order for the minimization to find the global minima.
+
+The example the returns the final cost function and the optimized angles, as well as the final value of the CHSH inequality as well as the target for maximal violation and relative distance.
diff --git a/examples/bell-variational/functions.py b/examples/bell-variational/functions.py
new file mode 100644
index 000000000..4dbce4c53
--- /dev/null
+++ b/examples/bell-variational/functions.py
@@ -0,0 +1,84 @@
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+def bell_circuit(basis):
+ """Create a Bell circuit with a distinct measurement basis and parametrizable gates.
+ Args:
+ basis (str): '00', '01, '10', '11' where a '1' marks a measurement in the X basis
+ and a '0' a measurement in the Z basis.
+
+ Returns:
+ :class:`qibo.core.circuit.Circuit`
+
+ """
+ c = Circuit(2)
+ c.add(gates.RY(0, theta=0))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.RY(0, theta=0))
+ for a in range(2):
+ if basis[a] == "1":
+ c.add(gates.H(a))
+ c.add(gates.M(*range(2)))
+ return c
+
+
+def set_parametrized_circuits():
+ """Create all Bell circuits needed to compute the CHSH inequelities.
+ Returns:
+ list(:class:`qibo.core.circuit.Circuit`)
+
+ """
+ chsh_circuits = []
+ basis = ["00", "01", "10", "11"]
+ for base in basis:
+ chsh_circuits.append(bell_circuit(base))
+ return chsh_circuits
+
+
+def compute_chsh(frequencies, nshots):
+ """Computes the CHSH inequelity value given the restults of an experiment.
+ Args:
+ frequencies (list): list of dictionaries with the result for the measurement
+ and the number of times it repeated, for each measurement
+ basis
+ nshots (int): number of samples taken in each run of the circuit.
+
+ Returns:
+ chsh (float): value of the CHSH inequality.
+
+ """
+ chsh = 0
+ aux = 0
+ for freq in frequencies:
+ for outcome in freq:
+ if aux == 1:
+ chsh -= (-1) ** (int(outcome[0]) + int(outcome[1])) * freq[outcome]
+ else:
+ chsh += (-1) ** (int(outcome[0]) + int(outcome[1])) * freq[outcome]
+ aux += 1
+ chsh /= nshots
+ return chsh
+
+
+def cost_function(parameters, circuits, nshots):
+ """Compute the cost function that attempts to maximize the violation of CHSH inequalities.
+ Args:
+ parameters (np.array): parameters for the variational algorithm. First determines the
+ initial state and second the measurement direction.
+ circuits (list(:class:`qibo.core.circuit.Circuit`)): Bell circuits in the 4 needed basis.
+ nshots (int): number of sampling for the experiment.
+
+ Returns:
+ cost (float): closer to 0 as the inequalities are maximally violated. Can go negative due
+ to sampling. (can be squared to avoid that)
+
+ """
+ frequencies = []
+ for circuit in circuits:
+ circuit.set_parameters(parameters)
+ frequencies.append(circuit(nshots=nshots).frequencies())
+ chsh = compute_chsh(frequencies, nshots)
+ cost = np.sqrt(2) * 2 - np.abs(chsh)
+ return cost
diff --git a/examples/bell-variational/images/chshequation.png b/examples/bell-variational/images/chshequation.png
new file mode 100644
index 000000000..cb19294f2
Binary files /dev/null and b/examples/bell-variational/images/chshequation.png differ
diff --git a/examples/bell-variational/images/circuit.png b/examples/bell-variational/images/circuit.png
new file mode 100644
index 000000000..8c6b2e64f
Binary files /dev/null and b/examples/bell-variational/images/circuit.png differ
diff --git a/examples/bell-variational/main.py b/examples/bell-variational/main.py
new file mode 100644
index 000000000..08afb689a
--- /dev/null
+++ b/examples/bell-variational/main.py
@@ -0,0 +1,49 @@
+import argparse
+
+import numpy as np
+from functions import compute_chsh, cost_function, set_parametrized_circuits
+
+from qibo import set_backend
+from qibo.optimizers import optimize
+
+
+def main(nshots, backend):
+ """Variationally find a maximally entangled state and the correct measurement angle
+ for violation of Bell inequalities.
+ Args:
+ nshots: number of shots to use for the minimization.
+ backend: choice of backend to run the example in.
+
+ """
+ set_backend(backend)
+ initial_parameters = np.random.uniform(0, 2 * np.pi, 2)
+ circuits = set_parametrized_circuits()
+ best, params, _ = optimize(
+ cost_function, initial_parameters, args=(circuits, nshots)
+ )
+ print(f"Cost: {best}\n")
+ print(f"Parameters: {params}\n")
+ print(f"Angles for the RY gates: {(params*180/np.pi)%360} in degrees.\n")
+ frequencies = []
+ for circuit in circuits:
+ circuit.set_parameters(params)
+ frequencies.append(circuit(nshots=nshots).frequencies())
+ chsh = compute_chsh(frequencies, nshots)
+ print(f"CHSH inequality value: {chsh}\n")
+ print(f"Target: {np.sqrt(2)*2}\n")
+ print(f"Relative distance: {100*np.abs(np.abs(chsh)-np.sqrt(2)*2)/np.sqrt(2)*2}%\n")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--nshots",
+ default=10000,
+ type=int,
+ help="Number of shots for each circuit base",
+ )
+ parser.add_argument(
+ "--backend", default="qibojit", type=str, help="Backend to use for the example"
+ )
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/benchmarks/circuits.py b/examples/benchmarks/circuits.py
new file mode 100644
index 000000000..cdcd1444a
--- /dev/null
+++ b/examples/benchmarks/circuits.py
@@ -0,0 +1,73 @@
+import numpy as np
+
+from qibo import gates, models
+
+
+def VariationalCircuit(nqubits, nlayers=1, theta_values=None):
+ if theta_values is None:
+ theta = iter(2 * np.pi * np.random.random(nlayers * 2 * nqubits))
+ else:
+ theta = iter(theta_values)
+
+ for l in range(nlayers):
+ for i in range(nqubits):
+ yield gates.RY(i, next(theta))
+ for i in range(0, nqubits - 1, 2):
+ yield gates.CZ(i, i + 1)
+ for i in range(nqubits):
+ yield gates.RY(i, next(theta))
+ for i in range(1, nqubits - 2, 2):
+ yield gates.CZ(i, i + 1)
+ yield gates.CZ(0, nqubits - 1)
+
+
+def OneQubitGate(nqubits, gate_type="H", params={}, nlayers=1):
+ gate = lambda q: getattr(gates, gate_type)(q, **params)
+ for _ in range(nlayers):
+ for i in range(nqubits):
+ yield gate(i)
+
+
+def TwoQubitGate(nqubits, gate_type="H", params={}, nlayers=1):
+ gate = lambda q: getattr(gates, gate_type)(q, q + 1, **params)
+ for _ in range(nlayers):
+ for i in range(0, nqubits - 1, 2):
+ yield gate(i)
+ for i in range(1, nqubits - 1, 2):
+ yield gate(i)
+
+
+def ToffoliGate(nqubits, nlayers=1):
+ for _ in range(nlayers):
+ for i in range(0, nqubits - 2, 3):
+ yield gates.TOFFOLI(i, i + 1, i + 2)
+ for i in range(1, nqubits - 2, 3):
+ yield gates.TOFFOLI(i, i + 1, i + 2)
+ for i in range(2, nqubits - 2, 3):
+ yield gates.TOFFOLI(i, i + 1, i + 2)
+
+
+def PrepareGHZ(nqubits):
+ yield gates.H(0)
+ for i in range(nqubits - 1):
+ yield gates.CNOT(i, i + 1)
+
+
+_CIRCUITS = {
+ "variational": VariationalCircuit,
+ "one-qubit-gate": OneQubitGate,
+ "two-qubit-gate": TwoQubitGate,
+ "toffoli-gate": ToffoliGate,
+ "ghz": PrepareGHZ,
+}
+
+
+def CircuitFactory(nqubits, circuit_name, accelerators=None, **kwargs):
+ if circuit_name == "qft":
+ circuit = models.QFT(nqubits, accelerators=accelerators)
+ else:
+ if circuit_name not in _CIRCUITS:
+ raise KeyError(f"Unknown benchmark circuit type {circuit_name}.")
+ circuit = models.Circuit(nqubits, accelerators=accelerators)
+ circuit.add(_CIRCUITS.get(circuit_name)(nqubits, **kwargs))
+ return circuit
diff --git a/examples/benchmarks/evolution.py b/examples/benchmarks/evolution.py
new file mode 100644
index 000000000..990bafd90
--- /dev/null
+++ b/examples/benchmarks/evolution.py
@@ -0,0 +1,71 @@
+"""Adiabatic evolution for the Ising Hamiltonian using linear scaling."""
+
+import argparse
+import time
+
+from utils import BenchmarkLogger, parse_accelerators
+
+import qibo
+from qibo import callbacks, hamiltonians, models
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=4, type=int)
+parser.add_argument("--dt", default=1e-2, type=float)
+parser.add_argument("--solver", default="exp", type=str)
+parser.add_argument("--dense", action="store_true")
+parser.add_argument("--accelerators", default=None, type=str)
+parser.add_argument("--backend", default="qibojit", type=str)
+parser.add_argument("--filename", default=None, type=str)
+
+
+def main(nqubits, dt, solver, backend, dense=False, accelerators=None, filename=None):
+ """Performs adiabatic evolution with critical TFIM as the "hard" Hamiltonian."""
+ qibo.set_backend(backend)
+ if accelerators is not None:
+ dense = False
+ solver = "exp"
+
+ logs = BenchmarkLogger(filename)
+ logs.append(
+ {
+ "nqubits": nqubits,
+ "dt": dt,
+ "solver": solver,
+ "dense": dense,
+ "backend": qibo.get_backend(),
+ "precision": qibo.get_precision(),
+ "device": qibo.get_device(),
+ "threads": qibo.get_threads(),
+ "accelerators": accelerators,
+ }
+ )
+ print(f"Using {solver} solver and dt = {dt}.")
+ print(f"Accelerators: {accelerators}")
+ print("Backend:", logs[-1]["backend"])
+
+ start_time = time.time()
+ h0 = hamiltonians.X(nqubits, dense=dense)
+ h1 = hamiltonians.TFIM(nqubits, h=1.0, dense=dense)
+ logs[-1]["hamiltonian_creation_time"] = time.time() - start_time
+ print(f"\nnqubits = {nqubits}, solver = {solver}")
+ print(f"dense = {dense}, accelerators = {accelerators}")
+ print("Hamiltonians created in:", logs[-1]["hamiltonian_creation_time"])
+
+ start_time = time.time()
+ evolution = models.AdiabaticEvolution(
+ h0, h1, lambda t: t, dt=dt, solver=solver, accelerators=accelerators
+ )
+ logs[-1]["creation_time"] = time.time() - start_time
+ print("Evolution model created in:", logs[-1]["creation_time"])
+
+ start_time = time.time()
+ final_psi = evolution(final_time=1.0)
+ logs[-1]["simulation_time"] = time.time() - start_time
+ print("Simulation time:", logs[-1]["simulation_time"])
+ logs.dump()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ args["accelerators"] = parse_accelerators(args.pop("accelerators"))
+ main(**args)
diff --git a/examples/benchmarks/example.sh b/examples/benchmarks/example.sh
new file mode 100755
index 000000000..b9f31a200
--- /dev/null
+++ b/examples/benchmarks/example.sh
@@ -0,0 +1,32 @@
+# example bash file for executing `main.py` benchmarks
+# device configurations and `nqubits` are based on DGX machine with
+# 4x V100 32GB GPUs
+FILENAME="dgx.dat" # log file name
+export CUDA_VISIBLE_DEVICES="0"
+for NQUBITS in 20 21 22 23 24 25 26 27 28 29 30
+do
+ python main.py --circuit qft --nqubits $NQUBITS --filename $FILENAME
+ python main.py --circuit qft --nqubits $NQUBITS --filename $FILENAME --precision "single"
+ python main.py --circuit variational --nqubits $NQUBITS --filename $FILENAME
+ python main.py --circuit variational --nqubits $NQUBITS --filename $FILENAME --precision "single"
+done
+python main.py --circuit qft --nqubits 31 --filename $FILENAME --precision "single"
+python main.py --circuit variational --nqubits 31 --filename $FILENAME --precision "single"
+export CUDA_VISIBLE_DEVICES=""
+for NQUBITS in 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
+do
+ python main.py --circuit qft --nqubits $NQUBITS --filename $FILENAME
+ python main.py --circuit variational --nqubits $NQUBITS --filename $FILENAME
+done
+
+export CUDA_VISIBLE_DEVICES="0,1"
+ACCEL="1/GPU:0,1/GPU:1"
+python main.py --circuit qft --nqubits 31 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL
+python main.py --circuit qft --nqubits 32 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL --precision "single"
+export CUDA_VISIBLE_DEVICES="0,1,2,3"
+ACCEL="1/GPU:0,1/GPU:1,1/GPU:2,1/GPU:3"
+python main.py --circuit qft --nqubits 32 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL
+python main.py --circuit qft --nqubits 33 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL --precision "single"
+ACCEL="2/GPU:0,2/GPU:1,2/GPU:2,2/GPU:3"
+python main.py --circuit qft --nqubits 33 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL
+python main.py --circuit qft --nqubits 34 --filename $FILENAME --device "/CPU:0" --accelerators $ACCEL --precision "single"
diff --git a/examples/benchmarks/images/adiabatic_evolution.png b/examples/benchmarks/images/adiabatic_evolution.png
new file mode 100644
index 000000000..8d0b844bf
Binary files /dev/null and b/examples/benchmarks/images/adiabatic_evolution.png differ
diff --git a/examples/benchmarks/images/adiabatic_evolution_dt.png b/examples/benchmarks/images/adiabatic_evolution_dt.png
new file mode 100644
index 000000000..fe8f48089
Binary files /dev/null and b/examples/benchmarks/images/adiabatic_evolution_dt.png differ
diff --git a/examples/benchmarks/images/qft_c128.png b/examples/benchmarks/images/qft_c128.png
new file mode 100644
index 000000000..b6bb27f53
Binary files /dev/null and b/examples/benchmarks/images/qft_c128.png differ
diff --git a/examples/benchmarks/images/qft_c64.png b/examples/benchmarks/images/qft_c64.png
new file mode 100644
index 000000000..b185f2b1a
Binary files /dev/null and b/examples/benchmarks/images/qft_c64.png differ
diff --git a/examples/benchmarks/images/qibo_c64_vs_c128.png b/examples/benchmarks/images/qibo_c64_vs_c128.png
new file mode 100644
index 000000000..6459fa5f3
Binary files /dev/null and b/examples/benchmarks/images/qibo_c64_vs_c128.png differ
diff --git a/examples/benchmarks/images/qibo_configurations.png b/examples/benchmarks/images/qibo_configurations.png
new file mode 100644
index 000000000..d18def4ad
Binary files /dev/null and b/examples/benchmarks/images/qibo_configurations.png differ
diff --git a/examples/benchmarks/images/qibo_configurations_small.png b/examples/benchmarks/images/qibo_configurations_small.png
new file mode 100644
index 000000000..4158f9f91
Binary files /dev/null and b/examples/benchmarks/images/qibo_configurations_small.png differ
diff --git a/examples/benchmarks/images/qibo_shots_cpu.png b/examples/benchmarks/images/qibo_shots_cpu.png
new file mode 100644
index 000000000..cb4dc08b9
Binary files /dev/null and b/examples/benchmarks/images/qibo_shots_cpu.png differ
diff --git a/examples/benchmarks/images/qibo_shots_gpu.png b/examples/benchmarks/images/qibo_shots_gpu.png
new file mode 100644
index 000000000..cc0187cbf
Binary files /dev/null and b/examples/benchmarks/images/qibo_shots_gpu.png differ
diff --git a/examples/benchmarks/images/var5layer_c128.png b/examples/benchmarks/images/var5layer_c128.png
new file mode 100644
index 000000000..7386eb4c7
Binary files /dev/null and b/examples/benchmarks/images/var5layer_c128.png differ
diff --git a/examples/benchmarks/images/var5layer_c64.png b/examples/benchmarks/images/var5layer_c64.png
new file mode 100644
index 000000000..9f4f8571d
Binary files /dev/null and b/examples/benchmarks/images/var5layer_c64.png differ
diff --git a/examples/benchmarks/images/varcircuit.png b/examples/benchmarks/images/varcircuit.png
new file mode 100644
index 000000000..155b58148
Binary files /dev/null and b/examples/benchmarks/images/varcircuit.png differ
diff --git a/examples/benchmarks/main.py b/examples/benchmarks/main.py
new file mode 100644
index 000000000..31b7eaec8
--- /dev/null
+++ b/examples/benchmarks/main.py
@@ -0,0 +1,223 @@
+"""
+Generic benchmark script that runs circuits defined in `benchmark_models.py`.
+
+The type of the circuit is selected using the ``--type`` flag.
+"""
+
+import argparse
+import os
+import time
+
+import numpy as np
+
+os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" # disable Tensorflow warnings
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=20, type=int)
+parser.add_argument("--circuit", default="qft", type=str)
+parser.add_argument("--backend", default="qibojit", type=str)
+parser.add_argument("--precision", default="double", type=str)
+parser.add_argument("--nreps", default=1, type=int)
+parser.add_argument("--nshots", default=None, type=int)
+parser.add_argument("--transfer", action="store_true")
+parser.add_argument("--fuse", action="store_true")
+
+parser.add_argument("--device", default=None, type=str)
+parser.add_argument("--accelerators", default=None, type=str)
+parser.add_argument("--memory", default=None, type=int)
+parser.add_argument("--threading", default=None, type=str)
+parser.add_argument("--compile", action="store_true")
+
+parser.add_argument("--nlayers", default=None, type=int)
+parser.add_argument("--gate-type", default=None, type=str)
+
+parser.add_argument("--filename", default=None, type=str)
+
+# params
+_PARAM_NAMES = {"theta", "phi"}
+parser.add_argument("--theta", default=None, type=float)
+parser.add_argument("--phi", default=None, type=float)
+args = vars(parser.parse_args())
+
+
+def limit_gpu_memory(memory_limit=None):
+ """Limits GPU memory that is available to Tensorflow.
+
+ Args:
+ memory_limit: Memory limit in MBs.
+ """
+ import tensorflow as tf
+
+ if memory_limit is None:
+ print("\nNo GPU memory limiter used.\n")
+ return
+
+ print(f"\nAttempting to limit GPU memory to {memory_limit}.\n")
+ gpus = tf.config.list_physical_devices("GPU")
+ for gpu in tf.config.list_physical_devices("GPU"):
+ config = tf.config.experimental.VirtualDeviceConfiguration(
+ memory_limit=memory_limit
+ )
+ tf.config.experimental.set_virtual_device_configuration(gpu, [config])
+ print(f"Limiting memory of {gpu.name} to {memory_limit}.")
+ print()
+
+
+def select_numba_threading(threading):
+ from numba import config
+
+ print(f"\nSwitching threading to {threading}.\n")
+ config.THREADING_LAYER = threading
+
+
+threading = args.pop("threading")
+if args.get("backend") == "qibojit" and threading is not None:
+ select_numba_threading(threading)
+
+memory = args.pop("memory")
+if args.get("backend") == "tensorflow":
+ limit_gpu_memory(memory)
+
+import circuits
+from utils import BenchmarkLogger, parse_accelerators
+
+import qibo
+
+
+def get_active_branch_name():
+ """Returns the name of the active git branch."""
+ from pathlib import Path
+
+ qibo_dir = Path(qibo.__file__).parent.parent.parent
+ head_dir = qibo_dir / ".git" / "HEAD"
+ with head_dir.open("r") as f:
+ content = f.read().splitlines()
+ for line in content:
+ if line[0:4] == "ref:":
+ return line.partition("refs/heads/")[2]
+
+
+def main(
+ nqubits,
+ circuit_name,
+ backend="custom",
+ precision="double",
+ nreps=1,
+ nshots=None,
+ transfer=False,
+ fuse=False,
+ device=None,
+ accelerators=None,
+ threadsafe=False,
+ compile=False,
+ get_branch=True,
+ nlayers=None,
+ gate_type=None,
+ params={},
+ filename=None,
+):
+ """Runs circuit simulation benchmarks for different circuits.
+
+ See benchmark documentation for a description of arguments.
+ """
+ qibo.set_backend(backend)
+ qibo.set_precision(precision)
+ if device is not None:
+ qibo.set_device(device)
+
+ logs = BenchmarkLogger(filename)
+ # Create log dict
+ logs.append(
+ {
+ "nqubits": nqubits,
+ "circuit_name": circuit_name,
+ "threading": "",
+ "backend": qibo.get_backend(),
+ "precision": qibo.get_precision(),
+ "device": qibo.get_device(),
+ "accelerators": accelerators,
+ "nshots": nshots,
+ "transfer": transfer,
+ "fuse": fuse,
+ "compile": compile,
+ }
+ )
+ if get_branch:
+ logs[-1]["branch"] = get_active_branch_name()
+
+ params = {k: v for k, v in params.items() if v is not None}
+ kwargs = {"nqubits": nqubits, "circuit_name": circuit_name}
+ if params:
+ kwargs["params"] = params
+ if nlayers is not None:
+ kwargs["nlayers"] = nlayers
+ if gate_type is not None:
+ kwargs["gate_type"] = gate_type
+ if accelerators is not None:
+ kwargs["accelerators"] = accelerators
+ logs[-1].update(kwargs)
+
+ start_time = time.time()
+ circuit = circuits.CircuitFactory(**kwargs)
+ if nshots is not None:
+ # add measurement gates
+ circuit.add(qibo.gates.M(*range(nqubits)))
+ if fuse:
+ circuit = circuit.fuse()
+ logs[-1]["creation_time"] = time.time() - start_time
+
+ start_time = time.time()
+ if compile:
+ circuit.compile()
+ # Try executing here so that compile time is not included
+ # in the simulation time
+ result = circuit(nshots=nshots)
+ del result
+ logs[-1]["compile_time"] = time.time() - start_time
+
+ start_time = time.time()
+ result = circuit(nshots=nshots)
+ logs[-1]["dry_run_time"] = time.time() - start_time
+ start_time = time.time()
+ if transfer:
+ result = result.numpy()
+ logs[-1]["dry_run_transfer_time"] = time.time() - start_time
+ del result
+
+ simulation_times, transfer_times = [], []
+ for _ in range(nreps):
+ start_time = time.time()
+ result = circuit(nshots=nshots)
+ simulation_times.append(time.time() - start_time)
+ start_time = time.time()
+ if transfer:
+ result = result.numpy()
+ transfer_times.append(time.time() - start_time)
+ logs[-1]["dtype"] = str(result.state().dtype)
+ if nshots is None:
+ del result
+
+ logs[-1]["simulation_times"] = simulation_times
+ logs[-1]["transfer_times"] = transfer_times
+ logs[-1]["simulation_times_mean"] = np.mean(simulation_times)
+ logs[-1]["simulation_times_std"] = np.std(simulation_times)
+ logs[-1]["transfer_times_mean"] = np.mean(transfer_times)
+ logs[-1]["transfer_times_std"] = np.std(transfer_times)
+
+ start_time = time.time()
+ if nshots is not None:
+ freqs = result.frequencies()
+ logs[-1]["measurement_time"] = time.time() - start_time
+
+ print()
+ print(logs)
+ print()
+ logs.dump()
+
+
+if __name__ == "__main__":
+ args["circuit_name"] = args.pop("circuit")
+ args["accelerators"] = parse_accelerators(args.pop("accelerators"))
+ args["params"] = {k: args.pop(k) for k in _PARAM_NAMES}
+ main(**args)
diff --git a/examples/benchmarks/qaoa.py b/examples/benchmarks/qaoa.py
new file mode 100644
index 000000000..2cbb704c1
--- /dev/null
+++ b/examples/benchmarks/qaoa.py
@@ -0,0 +1,89 @@
+"""
+Benchmark Quantum Approximate Optimization Algorithm model.
+"""
+
+import argparse
+import time
+
+import numpy as np
+from utils import BenchmarkLogger
+
+import qibo
+from qibo import hamiltonians, models
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=4, help="Number of qubits.", type=int)
+parser.add_argument("--nangles", default=4, help="Number of layers.", type=int)
+parser.add_argument(
+ "--dense", action="store_true", help="Use dense Hamiltonian or terms."
+)
+parser.add_argument("--solver", default="exp", help="Solver to apply gates.", type=str)
+parser.add_argument("--method", default="Powell", help="Optimization method.", type=str)
+parser.add_argument(
+ "--maxiter", default=None, help="Maximum optimization iterations.", type=int
+)
+parser.add_argument(
+ "--filename", default=None, help="Name of file to save logs.", type=str
+)
+
+
+def main(
+ nqubits,
+ nangles,
+ dense=True,
+ solver="exp",
+ method="Powell",
+ maxiter=None,
+ filename=None,
+):
+ """Performs a QAOA minimization test."""
+
+ print("Number of qubits:", nqubits)
+ print("Number of angles:", nangles)
+ logs = BenchmarkLogger(filename)
+ logs.append(
+ {
+ "nqubits": nqubits,
+ "nangles": nangles,
+ "dense": dense,
+ "solver": solver,
+ "backend": qibo.get_backend(),
+ "precision": qibo.get_precision(),
+ "device": qibo.get_device(),
+ "threads": qibo.get_threads(),
+ "method": method,
+ "maxiter": maxiter,
+ }
+ )
+
+ hamiltonian = hamiltonians.XXZ(nqubits=nqubits, dense=dense)
+ start_time = time.time()
+ qaoa = models.QAOA(hamiltonian, solver=solver)
+ logs[-1]["creation_time"] = time.time() - start_time
+
+ target = np.real(np.min(hamiltonian.eigenvalues()))
+ print("\nTarget state =", target)
+
+ np.random.seed(0)
+ initial_parameters = np.random.uniform(0, 0.1, nangles)
+
+ start_time = time.time()
+ options = {"disp": True, "maxiter": maxiter}
+ best, params, _ = qaoa.minimize(initial_parameters, method=method, options=options)
+ logs[-1]["minimization_time"] = time.time() - start_time
+
+ logs[-1]["best_energy"] = float(best)
+ logs[-1]["target_energy"] = float(target)
+ logs[-1]["epsilon"] = np.log10(1 / np.abs(best - target))
+ print("Found state =", best)
+ print("Final eps =", logs[-1]["epsilon"])
+
+ print("\nCreation time =", logs[-1]["creation_time"])
+ print("Minimization time =", logs[-1]["minimization_time"])
+ print("Total time =", logs[-1]["creation_time"] + logs[-1]["minimization_time"])
+ logs.dump()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/benchmarks/results/ADIABATIC.md b/examples/benchmarks/results/ADIABATIC.md
new file mode 100644
index 000000000..7dabb7737
--- /dev/null
+++ b/examples/benchmarks/results/ADIABATIC.md
@@ -0,0 +1,73 @@
+# Adiabatic evolution Benchmark
+
+We use Qibo to simulate adiabatic evolution under the transverse field Ising
+Hamiltonians using linear scaling `s(t) = t`. We simulate for a total time
+of T=1 using double precision (`complex128`) and different
+[solvers](https://qibo.science/qibo/stable/api-reference/qibo.html#solvers)
+on CPU and GPU. In the first section we provide the scaling of execution time
+as a function of the number of qubits in the system and in the second section
+as a function of the time step `dt` used to discretize and integrate in time.
+
+## Scaling vs number of qubits
+
+`nqubits` | Trotter (GPU) | Trotter (multi-GPU) | Trotter (CPU) | Exp (GPU) | Exp (CPU) | RK4 (GPU) | RK4 (CPU) | Trotter RK4 (GPU) | Trotter RK4 (CPU)
+-- | -- | -- | -- | -- | -- | -- | -- | -- | --
+5 | 0.17445 | | 0.10366 | 0.83614 | 0.78556 | 0.25242 | 0.156 | 0.89121 | 0.5226
+6 | 0.19523 | | 0.11258 | 0.85455 | 1.28687 | 0.25621 | 0.1549 | 1.03522 | 0.61248
+7 | 0.21635 | | 0.12165 | 0.95289 | 2.39186 | 0.2697 | 0.21857 | 1.18833 | 0.70617
+8 | 0.24125 | | 0.44973 | 1.05927 | 8.69052 | 0.33928 | 0.35587 | 1.34537 | 2.05456
+9 | 0.26052 | | 0.4862 | 1.60342 | 34.69458 | 0.59477 | 0.83639 | 1.47065 | 2.32306
+10 | 0.27457 | | 0.52353 | 6.2302 | 240.25221 | 1.71798 | 3.23398 | 1.55437 | 2.57444
+11 | 0.2933 | | 0.5912 | 30.81785 | 1885.62326 | 7.22978 | 15.72184 | 1.69087 | 2.74806
+12 | 0.31446 | | 0.63475 | 198.71525 | 15068.34078 | 31.00997 | 61.33608 | 1.89166 | 3.07578
+13 | 0.33565 | | 0.66254 | 1400.82069 | | 134.25498 | 242.00017 | 2.46156 | 3.5229
+14 | 0.34066 | | 0.71195 | | | | 1018.38455 | 2.80647 | 3.9003
+15 | 0.36345 | | 0.78478 | | | | 4151.16189 | 3.25374 | 4.07862
+16 | 0.38424 | | 0.82753 | | | | | 4.72084 | 5.06896
+17 | 0.40764 | | 0.86366 | | | | | 6.61444 | 6.82558
+18 | 0.42951 | | 1.02822 | | | | | 12.43154 | 10.31241
+19 | 0.48832 | | 1.3632 | | | | | 17.55895 | 19.5482
+20 | 0.76556 | | 2.12358 | | | | | 38.71244 | 45.04425
+21 | 0.91476 | | 3.98938 | | | | | 253.88249 | 107.72325
+22 | 1.59016 | | 8.75235 | | | | | 604.18377 | 428.68612
+23 | 2.74303 | | 23.00663 | | | | | 1243.40607 | 1352.20024
+24 | 4.94233 | | 47.59778 | | | | | 2577.88493 | 2769.86169
+25 | 9.48782 | 63.56629 | 98.07511 | | | | | 5402.67001 | 5607.38338
+26 | 18.90535 | 115.46439 | 202.28142 | | | | | 11297.93766 | 11523.4246
+27 | 38.76025 | 219.39538 | 419.13751 | | | | | 22595.86618 | 23790.17723
+28 | 79.88714 | 429.85937 | 869.88294 | | | | | |
+29 | 164.25789 | 856.78085 | 1800.57866 | | | | | |
+30 | 2783.62451 (2x) | 1690.57241 | 3702.52683 | | | | | |
+31 | 8146.88747 (4x) | 3390.6916 | 7608.61619 | | | | | |
+32 | 16593.04995 (8x) | 6052.76292 | 15745.90306 | | | | | |
+
+![adiabatic-nqubits](../images/adiabatic_evolution.png)
+
+## Scaling vs time step
+
+Results are for N=10 qubits.
+
+`dt (1e-3)` | Trotter (GPU) | Trotter (CPU) | Exp (GPU) | Exp (CPU)
+-- | -- | -- | -- | --
+100.0 | 0.03239 | 0.05764 | 1.83808 | 26.25529
+94.79 | 0.03262 | 0.05912 | 1.83009 | 26.31765
+89.58 | 0.03714 | 0.06667 | 1.89369 | 28.76947
+84.37 | 0.03547 | 0.0615 | 1.96616 | 28.75735
+79.16 | 0.03848 | 0.06849 | 1.95164 | 31.09359
+73.95 | 0.04137 | 0.07304 | 2.07511 | 33.57253
+68.74 | 0.04405 | 0.07813 | 2.06827 | 36.10914
+63.53 | 0.04697 | 0.08388 | 2.12316 | 38.70984
+58.32 | 0.05231 | 0.09371 | 2.2088 | 43.57428
+53.11 | 0.05482 | 0.09903 | 2.191 | 45.9787
+47.89 | 0.0605 | 0.10862 | 2.33777 | 50.97479
+42.68 | 0.06892 | 0.12416 | 2.47674 | 58.57135
+37.47 | 0.0764 | 0.14047 | 2.70509 | 66.2002
+32.26 | 0.08562 | 0.15811 | 2.81736 | 75.97506
+27.05 | 0.10267 | 0.19317 | 3.14004 | 90.25913
+21.84 | 0.12787 | 0.23906 | 3.62534 | 112.58938
+16.63 | 0.17028 | 0.3146 | 4.339 | 150.19589
+11.42 | 0.24472 | 0.45362 | 5.62884 | 217.54051
+6.21 | 0.44506 | 0.82831 | 9.2002 | 403.11168
+1.0 | 2.75154 | 5.08593 | 50.19016 | 2418.04262
+
+![adiabatic-dt](../images/adiabatic_evolution_dt.png)
diff --git a/examples/benchmarks/results/HARDWARE.md b/examples/benchmarks/results/HARDWARE.md
new file mode 100644
index 000000000..a62172934
--- /dev/null
+++ b/examples/benchmarks/results/HARDWARE.md
@@ -0,0 +1,80 @@
+# Hardware configurations
+
+A core point in Qibo is the support of different hardware configurations
+despite its simple installation procedure. The user can easily switch between
+CPU and GPU as described in
+[How to select hardware devices?](https://qibo.science/qibo/stable/code-examples/advancedexamples.html#how-to-select-hardware-devices).
+A question that arises is how to determine the optimal device configuration for
+the circuit one would like to simulate.
+While the answer to this question depends both on the circuit specifics
+(number of qubits, number of gates, number of re-executions) and the
+exact hardware specifications (CPU or GPU model and available memory), here
+we provide a basic comparison using the DGX station to simulate the QFT circuit
+in double precision (`complex128`).
+
+
+## Heuristic rules
+
+Based on the results presented below, in the following table we provide some
+heuristic rules for optimal device selection according to the number of qubits.
+More stars means a shorter execution time is expected.
+We stress that these general rules may not be valid on every case as the optimal
+configuration depends on various factors, such as the exact circuit structure
+and hardware specifications (CPU and GPU speed and memory).
+
+`nqubits` | 0-15 | 15-30 | >30
+-- | :--: | :--: | :--:
+CPU single thread | `***` | `*` | `*`
+CPU multi-threading | `*` | `**` | `**`
+single GPU | `*` | `***` | `**`
+multi-GPU | - | - | `***`
+
+
+## Large circuits
+
+Here we measure QFT execution time for large circuits (25 to 33 qubits) using
+different CPU thread configuration, single GPU and multi-GPU.
+
+`nqubits` | single-GPU | multi-GPU | 1-thread | 10-threads | 20-threads | 40-threads
+-- | -- | -- | -- | -- | -- | --
+25 | 0.25463 | 2.06511 | 31.42569 | 4.29928 | 2.83574 | 2.68645
+26 | 0.47597 | 2.7348 | 67.51781 | 8.95446 | 5.86079 | 5.60514
+27 | 0.93437 | 3.86462 | 144.42056 | 18.79864 | 12.2377 | 11.78381
+28 | 1.87903 | 6.34791 | 309.06432 | 40.01433 | 25.70007 | 24.84334
+29 | 3.8849 | 11.05362 | 663.96249 | 84.17542 | 53.86609 | 52.47754
+30 | 8.10977 | 21.21188 | 1426.46899 | 177.51885 | 113.43018 | 110.6095
+31 | 68.09852 | 41.26969 | 3064.75608 | 375.57098 | 238.23101 | 232.35723
+32 | 182.78114 | 66.40625 | 6584.73906 | 794.48593 | 501.6703 | 488.00321
+33 | 468.13457 | 161.33294 | 14147.73073 | 1667.25349 | 1046.75898 | 1021.06619
+
+![hardware-large](../images/qibo_configurations.png)
+
+
+## Small circuits
+
+Here we measure QFT execution time for small circuits (25 to 33 qubits) using
+different single-thread CPU, multi-thread CPU and GPU.
+
+`nqubits` | single-GPU | 1-thread | 40-threads
+-- | -- | -- | --
+5 | 0.00348 | 0.0021 | 0.00225
+6 | 0.0047 | 0.00284 | 0.00305
+7 | 0.00594 | 0.0036 | 0.00755
+8 | 0.00747 | 0.00455 | 0.01225
+9 | 0.00869 | 0.00557 | 0.01734
+10 | 0.01048 | 0.00683 | 0.02044
+11 | 0.0123 | 0.00827 | 0.02346
+12 | 0.01439 | 0.01043 | 0.02695
+13 | 0.01653 | 0.01299 | 0.02982
+14 | 0.01975 | 0.01885 | 0.03459
+15 | 0.02168 | 0.02576 | 0.03783
+16 | 0.02439 | 0.04158 | 0.04161
+17 | 0.02723 | 0.07478 | 0.04668
+18 | 0.03017 | 0.14607 | 0.05237
+19 | 0.03361 | 0.29966 | 0.06529
+20 | 0.03881 | 0.63581 | 0.09226
+21 | 0.04525 | 1.37577 | 0.1233
+22 | 0.0682 | 3.08223 | 0.23931
+23 | 0.09557 | 6.77251 | 0.56214
+
+![hardware-small](../images/qibo_configurations_small.png)
diff --git a/examples/benchmarks/results/PRECISION.md b/examples/benchmarks/results/PRECISION.md
new file mode 100644
index 000000000..93bf8eaed
--- /dev/null
+++ b/examples/benchmarks/results/PRECISION.md
@@ -0,0 +1,26 @@
+# Simulation precision
+
+Qibo allows the user to easily switch between single (``complex64``)
+and double (``complex128``) precision as described in
+[How to modify the simulation precision?](https://qibo.science/qibo/stable/code-examples/examples.html#how-to-modify-the-simulation-precision).
+In this section we compare simulation performance of both precisions.
+We find that as the number of qubits grows using single precision is ~2
+times faster on GPU and ~1.5 faster on CPU.
+
+
+`nqubits` | CPU c64 | CPU c128 | GPU c64 | GPU c128
+-- | -- | -- | -- | --
+23 | 0.41793 | 0.57448 | 0.07276 | 0.09249
+24 | 0.83167 | 1.28207 | 0.09982 | 0.14664
+25 | 1.75593 | 2.68645 | 0.15088 | 0.25463
+26 | 3.68007 | 5.60514 | 0.26269 | 0.47597
+27 | 7.71366 | 11.78381 | 0.48103 | 0.93437
+28 | 16.36245 | 24.84334 | 0.93884 | 1.87903
+29 | 34.66599 | 52.47754 | 1.88652 | 3.8849
+30 | 73.43444 | 110.6095 | 3.90816 | 8.10977
+31 | 155.01974 | 232.35723 | 8.08614 |
+32 | 329.56916 | 488.00321 | |
+33 | 694.67844 | 1021.06619 | |
+34 | 1465.81645 | | |
+
+![precision-benchmarks](../images/qibo_c64_vs_c128.png)
diff --git a/examples/benchmarks/results/QFT.md b/examples/benchmarks/results/QFT.md
new file mode 100644
index 000000000..1dbc7f9a8
--- /dev/null
+++ b/examples/benchmarks/results/QFT.md
@@ -0,0 +1,42 @@
+# Quantum Fourier Transform Benchmark
+
+The Quantum Fourier Transform is used as a subroutine in many quantum
+algorithms and thus constitutes an example with great practical importance.
+The gates used in this circuit are `H`, `CU1`, and `SWAP`,
+all of which are available in Qibo and other used libraries,
+except QCGPU where `SWAP` was implemented using three `CNOT` gates.
+
+
+## Single precision (`complex64`)
+
+`nqubits` | Qibo (GPU) | Qibo (multi-GPU) | QCGPU (GPU) | Qibo (CPU) | Qibo (CPU-1) | QCGPU (CPU) | Cirq (CPU) | TFQ (CPU)
+-- | -- | -- | -- | -- | -- | -- | -- | --
+25 | 0.15088 | 1.7333 | 0.33926 | 1.75593 | 28.983 | 2.81661 | 20.04615 | 27.31083
+26 | 0.26269 | 2.09426 | 0.70485 | 3.68007 | 62.33386 | 5.98906 | 42.59027 | 57.70737
+27 | 0.48103 | 2.79575 | 1.4615 | 7.71366 | 133.49707 | 12.57996 | 88.89556 | 123.62698
+28 | 0.93884 | 3.9641 | 3.04468 | 16.36245 | 285.99587 | 26.39612 | 185.83514 | 264.49645
+29 | 1.88652 | 6.53928 | 6.32165 | 34.66599 | 616.35557 | 54.92496 | 383.06087 | 558.00182
+30 | 3.90816 | 11.76768 | 13.16367 | 73.43444 | 1328.38914 | 116.36015 | 805.87551 | 1179.73661
+31 | 8.08614 | 21.9935 | 27.39081 | 155.01974 | 2863.07021 | 245.91649 | 1689.20272 | core dumped
+32 | 67.08172 (2x) | 42.51865 | OOM | 329.56916 | 6170.86231 | fails | 3550.67372 |
+33 | 182.26496 (4x) | 70.30869 | | 694.67844 | 13300.37152 | | fails |
+34 | 464.34191 (8x) | 160.39392 | | 1465.81645 | 28667.11473 | | |
+
+![qft-64](../images/qft_c64.png)
+
+
+## Double precision (`complex128`)
+
+`nqubits` | Qibo (GPU) | Qibo (multi-GPU) | Qulacs (GPU) | Qibo (CPU) | Qibo (CPU-1) | Qulacs (CPU) | IntelQS (CPU) | Qiskit (CPU) | PyQuil (CPU)
+-- | -- | -- | -- | -- | -- | -- | -- | -- | --
+25 | 0.25463 | 2.06511 | 0.87435 | 2.68645 | 31.42569 | 5.41102 | 17.2777 | 267.32462 | 116.83364
+26 | 0.47597 | 2.7348 | 1.78204 | 5.60514 | 67.51781 | 11.11646 | 37.10058 | 537.95522 | 244.95282
+27 | 0.93437 | 3.86462 | 3.64287 | 11.78381 | 144.42056 | 23.01487 | 78.81581 | 1075.48524 | 518.42093
+28 | 1.87903 | 6.34791 | 7.45497 | 24.84334 | 309.06432 | 47.82596 | 166.74768 | 2156.63058 | 1084.50795
+29 | 3.8849 | 11.05362 | 15.27099 | 52.47754 | 663.96249 | 99.40361 | 350.3245 | 4290.20236 | fails
+30 | 8.10977 | 21.21188 | 31.42752 | 110.6095 | 1426.46899 | 207.96072 | 744.3759 | 8564.4294 |
+31 | 68.09852 (2x) | 41.26969 | OOM | 232.35723 | 3064.75608 | 435.65923 | 1553.70923 | fails |
+32 | 182.78114 (4x) | 66.40625 | | 488.00321 | 6584.73906 | 912.57703 | 3279.90343 | |
+33 | 468.13457 (8x) | 161.33294 | | 1021.06619 | 14147.73073 | fails | 6867.85147 | |
+
+![qft-c128](../images/qft_c128.png)
diff --git a/examples/benchmarks/results/SHOTS.md b/examples/benchmarks/results/SHOTS.md
new file mode 100644
index 000000000..12a494330
--- /dev/null
+++ b/examples/benchmarks/results/SHOTS.md
@@ -0,0 +1,32 @@
+# Measurement Shots Benchmark
+
+Qibo simulates quantum measurements using its standard dense state vector
+simulator, followed by sampling from the distribution corresponding to the final
+state vector. Since the dense state vector is used instead of repeated circuit
+executions, measurement time does not have a strong dependence on the number of
+shots.
+The plots and numbers below contain only the time required for sampling
+as the state vector simulation time (time required to apply gates) has been
+subtracted from the total execution time. The circuit used in this benchmark
+consists of a layer of ``H`` gates applied to every qubit followed by a
+measurement of all qubits.
+
+
+`nshots` | CPU N=12 | GPU N=12 | CPU N=22 | GPU N=22
+-- | -- | -- | -- | --
+10 | 0.00222 | 0.001 | 0.06738 | 0.01154
+50 | 0.00216 | 0.00099 | 0.06737 | 0.01367
+100 | 0.00227 | 0.00099 | 0.06743 | 0.0216
+500 | 0.00227 | 0.00115 | 0.06756 | 0.08641
+1000 | 0.00236 | 0.00101 | 0.06785 | 0.11819
+5000 | 0.00342 | 0.00098 | 0.06934 | 0.11781
+10000 | 0.00452 | 0.00158 | 0.07157 | 0.12264
+50000 | 0.01242 | 0.0069 | 0.08722 | 0.13751
+100000 | 0.01939 | 0.01337 | 0.10656 | 0.15444
+250000 | 0.04117 | 0.03254 | 0.16497 | 0.21442
+500000 | 0.07463 | 0.06422 | 0.26315 | 0.31621
+750000 | 0.11058 | 0.09289 | 0.35994 | 0.40627
+1000000 | 0.13462 | 0.12177 | 0.46654 | 0.50615
+
+![shots-cpu](../images/qibo_shots_cpu.png)
+![shots-gpu](../images/qibo_shots_gpu.png)
diff --git a/examples/benchmarks/results/VAR5.md b/examples/benchmarks/results/VAR5.md
new file mode 100644
index 000000000..e97cdc806
--- /dev/null
+++ b/examples/benchmarks/results/VAR5.md
@@ -0,0 +1,48 @@
+# Variational Circuit Benchmark
+
+Variational circuits are typically used in quantum machine learning and similar
+applications and constitute a good candidate for applications of near-term
+quantum computers due to their short depth and.
+The circuit used in the benchmark consists of a layer of RY rotations followed
+by a layer of ``CZ`` gates that entangle neighbouring qubits, as shown in the
+figure below:
+
+![var5-circuit](../images/varcircuit.png)
+
+The configuration is repeated for five layers and the variational parameters
+are chosen randomly from 0 to 2pi in all benchmarks.
+
+
+## Single precision (`complex64`)
+
+`nqubits` | Qibo (V100) | QCGPU (V100) | Qibo (CPU) | QCGPU (CPU) | Cirq (CPU) | TFQ (CPU)
+-- | -- | -- | -- | -- | -- | --
+25 | 0.445 | 0.38624 | 1.3471 | 3.65103 | 105.25651 | 11.3111
+26 | 0.54765 | 0.79253 | 2.55375 | 7.61292 | 210.74509 | 25.81774
+27 | 0.70547 | 1.60987 | 4.99354 | 15.42897 | 427.60195 | 48.05409
+28 | 1.08272 | 3.30945 | 10.15224 | 31.70288 | 876.43086 | 107.95146
+29 | 1.72881 | 6.7313 | 20.49916 | 64.19206 | 1778.92683 | 204.5487
+30 | 3.21179 | 13.82931 | 42.34363 | 133.33737 | 3674.24012 | 454.36985
+31 | 5.8706 | 28.10267 | 86.44963 | 272.46449 | 7477.32876 | core dumped
+32 | OOM | OOM | 179.30808 | fails | 14950.43044 |
+33 | | | 366.64664 | | fails |
+34 | | | 759.29275 | | |
+
+![var5-c64](../images/var5layer_c64.png)
+
+
+## Double precision (`complex128`)
+
+`nqubits` | Qibo (V100) | Qulacs (V100) | Qibo (CPU) | Qulacs (CPU) | IntelQS (CPU) | Qiskit (CPU) | PyQuil (CPU)
+-- | -- | -- | -- | -- | -- | -- | --
+25 | 0.50875 | 0.97298 | 1.88217 | 6.6459 | 27.96521 | 265.69477 | 99.8847
+26 | 0.68834 | 1.97169 | 3.6478 | 13.33385 | 57.50249 | 533.14815 | 207.44335
+27 | 1.00492 | 3.98935 | 7.3389 | 26.44321 | 117.75236 | 1067.66561 | 421.39925
+28 | 1.6831 | 8.10374 | 14.84471 | 54.18735 | 242.76708 | 2146.48768 | 869.36062
+29 | 3.04162 | 16.42699 | 30.48203 | 110.05296 | 525.83829 | 4275.40255 | fails
+30 | 5.827 | 33.46775 | 62.2646 | 225.94718 | 1043.19987 | 9324.27692 |
+31 | OOM | OOM | 128.6027 | 460.50558 | 2100.28082 | fails |
+32 | | | 263.47171 | 947.05816 | 4365.85043 | |
+33 | | | 544.67465 | fails | 8946.80753 | |
+
+![var5-c128](../images/var5layer_c128.png)
diff --git a/examples/benchmarks/utils.py b/examples/benchmarks/utils.py
new file mode 100644
index 000000000..4bd3deeda
--- /dev/null
+++ b/examples/benchmarks/utils.py
@@ -0,0 +1,53 @@
+import json
+import os
+
+
+class BenchmarkLogger(list):
+ def __init__(self, filename=None):
+ self.filename = filename
+ if filename is not None and os.path.isfile(filename):
+ print(f"Extending existing logs from {filename}.")
+ with open(filename) as file:
+ super().__init__(json.load(file))
+ else:
+ if filename is not None:
+ print(f"Creating new logs in {filename}.")
+ super().__init__()
+
+ def dump(self):
+ if self.filename is not None:
+ with open(self.filename, "w") as file:
+ json.dump(list(self), file)
+
+ def __str__(self):
+ return "\n".join(f"{k}: {v}" for k, v in self[-1].items())
+
+
+def parse_accelerators(accelerators):
+ """Transforms string that specifies accelerators to dictionary.
+
+ The string that is parsed has the following format:
+ n1device1,n2device2,n3device3,...
+ and is transformed to the dictionary:
+ {'device1': n1, 'device2': n2, 'device3': n3, ...}
+
+ Example:
+ 2/GPU:0,2/GPU:1 --> {'/GPU:0': 2, '/GPU:1': 2}
+ """
+ if accelerators is None:
+ return None
+
+ def read_digit(x):
+ i = 0
+ while x[i].isdigit():
+ i += 1
+ return x[i:], int(x[:i])
+
+ acc_dict = {}
+ for entry in accelerators.split(","):
+ device, n = read_digit(entry)
+ if device in acc_dict:
+ acc_dict[device] += n
+ else:
+ acc_dict[device] = n
+ return acc_dict
diff --git a/examples/benchmarks/vqe.py b/examples/benchmarks/vqe.py
new file mode 100644
index 000000000..2b345b044
--- /dev/null
+++ b/examples/benchmarks/vqe.py
@@ -0,0 +1,102 @@
+"""
+Benchmark Variational Quantum Eigensolver.
+"""
+
+import argparse
+import time
+
+import numpy as np
+from utils import BenchmarkLogger
+
+import qibo
+from qibo import gates, hamiltonians, models
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=6, help="Number of qubits.", type=int)
+parser.add_argument("--nlayers", default=4, help="Number of layers.", type=int)
+parser.add_argument("--method", default="Powell", help="Optimization method.", type=str)
+parser.add_argument(
+ "--maxiter", default=None, help="Maximum optimization iterations.", type=int
+)
+parser.add_argument(
+ "--backend", default="qibojit", help="Qibo backend to use.", type=str
+)
+parser.add_argument("--fuse", action="store_true", help="Use gate fusion.")
+parser.add_argument(
+ "--filename", default=None, help="Name of file to save logs.", type=str
+)
+
+
+def create_circuit(nqubits, nlayers):
+ """Creates variational circuit."""
+ circuit = models.Circuit(nqubits)
+ for l in range(nlayers):
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ circuit.add(gates.CZ(q, q + 1) for q in range(0, nqubits - 1, 2))
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ circuit.add(gates.CZ(q, q + 1) for q in range(1, nqubits - 2, 2))
+ circuit.add(gates.CZ(0, nqubits - 1))
+ circuit.add(gates.RY(q, theta=0) for q in range(nqubits))
+ return circuit
+
+
+def main(
+ nqubits, nlayers, backend, fuse=False, method="Powell", maxiter=None, filename=None
+):
+ """Performs a VQE circuit minimization test."""
+ qibo.set_backend(backend)
+ logs = BenchmarkLogger(filename)
+ logs.append(
+ {
+ "nqubits": nqubits,
+ "nlayers": nlayers,
+ "fuse": fuse,
+ "backend": qibo.get_backend(),
+ "precision": qibo.get_precision(),
+ "device": qibo.get_device(),
+ "threads": qibo.get_threads(),
+ "method": method,
+ "maxiter": maxiter,
+ }
+ )
+ print("Number of qubits:", nqubits)
+ print("Number of layers:", nlayers)
+ print("Backend:", logs[-1]["backend"])
+
+ start_time = time.time()
+ circuit = create_circuit(nqubits, nlayers)
+ if fuse:
+ circuit = circuit.fuse()
+ hamiltonian = hamiltonians.XXZ(nqubits=nqubits)
+ vqe = models.VQE(circuit, hamiltonian)
+ logs[-1]["creation_time"] = time.time() - start_time
+
+ target = np.real(np.min(hamiltonian.eigenvalues()))
+ print("\nTarget state =", target)
+
+ np.random.seed(0)
+ nparams = 2 * nqubits * nlayers + nqubits
+ initial_parameters = np.random.uniform(0, 2 * np.pi, nparams)
+
+ start_time = time.time()
+ options = {"disp": False, "maxiter": maxiter}
+ best, params, _ = vqe.minimize(
+ initial_parameters, method=method, options=options, compile=False
+ )
+ logs[-1]["minimization_time"] = time.time() - start_time
+ epsilon = np.log10(1 / np.abs(best - target))
+ print("Found state =", best)
+ print("Final eps =", epsilon)
+
+ logs[-1]["best_energy"] = float(best)
+ logs[-1]["epsilon_energy"] = float(epsilon)
+
+ print("\nCreation time =", logs[-1]["creation_time"])
+ print("Minimization time =", logs[-1]["minimization_time"])
+ print("Total time =", logs[-1]["minimization_time"] + logs[-1]["creation_time"])
+ logs.dump()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/conftest.py b/examples/conftest.py
new file mode 100644
index 000000000..db7ba3083
--- /dev/null
+++ b/examples/conftest.py
@@ -0,0 +1,2 @@
+def pytest_addoption(parser):
+ parser.addoption("--examples-timeout", type=int, default=100000)
diff --git a/examples/data3sat/10bit/n10i1.txt b/examples/data3sat/10bit/n10i1.txt
new file mode 100644
index 000000000..98e162765
--- /dev/null
+++ b/examples/data3sat/10bit/n10i1.txt
@@ -0,0 +1,10 @@
+ 10 8 4
+0 1 1 0 0 0 0 1 0 1
+ 4 5 8
+ 5 6 10
+ 4 6 8
+ 3 4 7
+ 1 2 6
+ 1 6 8
+ 1 9 10
+ 2 5 7
diff --git a/examples/data3sat/10bit/n10i10.txt b/examples/data3sat/10bit/n10i10.txt
new file mode 100644
index 000000000..86d37501b
--- /dev/null
+++ b/examples/data3sat/10bit/n10i10.txt
@@ -0,0 +1,11 @@
+ 10 9 3
+1 0 0 0 0 1 1 0 0 0
+ 1 9 10
+ 1 8 10
+ 2 7 10
+ 4 6 9
+ 5 6 10
+ 2 3 7
+ 5 6 9
+ 6 8 10
+ 1 2 3
diff --git a/examples/data3sat/10bit/n10i2.txt b/examples/data3sat/10bit/n10i2.txt
new file mode 100644
index 000000000..910674f02
--- /dev/null
+++ b/examples/data3sat/10bit/n10i2.txt
@@ -0,0 +1,10 @@
+ 10 8 5
+0 0 1 0 1 0 0 1 1 1
+ 1 5 6
+ 1 4 8
+ 4 6 9
+ 1 2 5
+ 1 6 9
+ 6 7 8
+ 4 7 10
+ 3 6 7
diff --git a/examples/data3sat/10bit/n10i3.txt b/examples/data3sat/10bit/n10i3.txt
new file mode 100644
index 000000000..b4ce4f14f
--- /dev/null
+++ b/examples/data3sat/10bit/n10i3.txt
@@ -0,0 +1,10 @@
+ 10 8 3
+0 0 0 0 1 1 0 0 0 1
+ 5 7 9
+ 4 7 10
+ 2 5 8
+ 2 3 10
+ 2 9 10
+ 6 7 9
+ 4 6 9
+ 1 5 8
diff --git a/examples/data3sat/10bit/n10i4.txt b/examples/data3sat/10bit/n10i4.txt
new file mode 100644
index 000000000..56cefcaaf
--- /dev/null
+++ b/examples/data3sat/10bit/n10i4.txt
@@ -0,0 +1,11 @@
+ 10 9 5
+1 0 0 1 1 0 1 0 1 0
+ 2 7 8
+ 2 3 7
+ 1 8 10
+ 2 9 10
+ 5 8 10
+ 4 6 8
+ 6 7 8
+ 2 5 10
+ 2 5 8
diff --git a/examples/data3sat/10bit/n10i5.txt b/examples/data3sat/10bit/n10i5.txt
new file mode 100644
index 000000000..cc3cb19ba
--- /dev/null
+++ b/examples/data3sat/10bit/n10i5.txt
@@ -0,0 +1,10 @@
+ 10 8 4
+0 1 0 0 0 0 1 0 1 1
+ 1 2 4
+ 4 5 7
+ 4 7 8
+ 2 4 5
+ 3 6 7
+ 3 8 10
+ 1 2 5
+ 1 4 9
diff --git a/examples/data3sat/10bit/n10i6.txt b/examples/data3sat/10bit/n10i6.txt
new file mode 100644
index 000000000..bb3f9f660
--- /dev/null
+++ b/examples/data3sat/10bit/n10i6.txt
@@ -0,0 +1,11 @@
+ 10 9 3
+1 0 0 0 0 1 0 1 0 0
+ 1 2 7
+ 3 4 6
+ 3 6 7
+ 4 5 8
+ 1 7 9
+ 1 2 10
+ 5 6 9
+ 2 3 6
+ 1 3 10
diff --git a/examples/data3sat/10bit/n10i7.txt b/examples/data3sat/10bit/n10i7.txt
new file mode 100644
index 000000000..25d6d8276
--- /dev/null
+++ b/examples/data3sat/10bit/n10i7.txt
@@ -0,0 +1,10 @@
+ 10 8 3
+0 0 0 0 1 0 0 0 1 1
+ 1 5 8
+ 2 8 9
+ 1 7 9
+ 1 4 10
+ 5 6 8
+ 2 3 10
+ 4 5 8
+ 1 2 5
diff --git a/examples/data3sat/10bit/n10i8.txt b/examples/data3sat/10bit/n10i8.txt
new file mode 100644
index 000000000..4f52136cd
--- /dev/null
+++ b/examples/data3sat/10bit/n10i8.txt
@@ -0,0 +1,11 @@
+ 10 9 2
+0 0 0 0 1 1 0 0 0 0
+ 1 3 6
+ 3 5 8
+ 3 6 10
+ 2 5 9
+ 4 5 9
+ 2 6 7
+ 4 6 9
+ 1 2 6
+ 5 7 10
diff --git a/examples/data3sat/10bit/n10i9.txt b/examples/data3sat/10bit/n10i9.txt
new file mode 100644
index 000000000..ba0a051e7
--- /dev/null
+++ b/examples/data3sat/10bit/n10i9.txt
@@ -0,0 +1,11 @@
+ 10 9 3
+1 0 0 0 0 0 0 0 1 1
+ 2 3 10
+ 1 7 8
+ 2 7 9
+ 5 6 9
+ 1 3 4
+ 2 6 10
+ 5 6 10
+ 3 8 10
+ 1 2 8
diff --git a/examples/data3sat/12bit/n12i1.txt b/examples/data3sat/12bit/n12i1.txt
new file mode 100644
index 000000000..27dd8999b
--- /dev/null
+++ b/examples/data3sat/12bit/n12i1.txt
@@ -0,0 +1,12 @@
+ 12 10 5
+0 1 0 1 0 0 1 1 0 0 0 1
+ 4 6 9
+ 6 7 11
+ 5 7 9
+ 3 5 8
+ 1 3 7
+ 1 7 10
+ 8 9 11
+ 7 9 10
+ 1 11 12
+ 2 6 9
diff --git a/examples/data3sat/12bit/n12i10.txt b/examples/data3sat/12bit/n12i10.txt
new file mode 100644
index 000000000..54d78e9b6
--- /dev/null
+++ b/examples/data3sat/12bit/n12i10.txt
@@ -0,0 +1,12 @@
+ 12 10 5
+0 1 1 0 1 0 1 0 0 0 1 0
+ 1 2 9
+ 7 9 10
+ 2 8 12
+ 4 10 11
+ 2 6 10
+ 3 6 8
+ 1 3 10
+ 3 8 10
+ 4 5 12
+ 1 5 9
diff --git a/examples/data3sat/12bit/n12i2.txt b/examples/data3sat/12bit/n12i2.txt
new file mode 100644
index 000000000..e010ef8b7
--- /dev/null
+++ b/examples/data3sat/12bit/n12i2.txt
@@ -0,0 +1,13 @@
+ 12 11 5
+0 1 1 1 0 1 0 1 0 0 0 0
+ 1 3 7
+ 7 8 9
+ 4 10 11
+ 1 2 9
+ 4 5 12
+ 1 3 9
+ 8 9 11
+ 1 6 10
+ 5 8 9
+ 4 5 11
+ 5 6 7
diff --git a/examples/data3sat/12bit/n12i3.txt b/examples/data3sat/12bit/n12i3.txt
new file mode 100644
index 000000000..90cb39151
--- /dev/null
+++ b/examples/data3sat/12bit/n12i3.txt
@@ -0,0 +1,12 @@
+ 12 10 5
+0 1 0 0 0 0 1 0 1 1 0 1
+ 4 9 11
+ 3 7 8
+ 1 3 12
+ 2 6 11
+ 5 6 10
+ 1 2 3
+ 1 4 10
+ 1 3 9
+ 6 8 9
+ 2 3 6
diff --git a/examples/data3sat/12bit/n12i4.txt b/examples/data3sat/12bit/n12i4.txt
new file mode 100644
index 000000000..fd9bdf075
--- /dev/null
+++ b/examples/data3sat/12bit/n12i4.txt
@@ -0,0 +1,11 @@
+ 12 9 4
+1 1 0 0 1 0 0 0 0 1 0 0
+ 2 8 9
+ 2 3 8
+ 1 9 12
+ 2 11 12
+ 6 10 11
+ 4 7 10
+ 3 5 7
+ 7 8 10
+ 2 6 12
diff --git a/examples/data3sat/12bit/n12i5.txt b/examples/data3sat/12bit/n12i5.txt
new file mode 100644
index 000000000..3b949936e
--- /dev/null
+++ b/examples/data3sat/12bit/n12i5.txt
@@ -0,0 +1,13 @@
+ 12 11 3
+1 0 0 1 0 1 0 0 0 0 0 0
+ 3 4 11
+ 2 3 6
+ 2 6 9
+ 1 2 5
+ 5 6 8
+ 4 8 10
+ 3 5 6
+ 1 2 3
+ 4 10 11
+ 4 7 9
+ 4 10 12
diff --git a/examples/data3sat/12bit/n12i6.txt b/examples/data3sat/12bit/n12i6.txt
new file mode 100644
index 000000000..82f66ccd3
--- /dev/null
+++ b/examples/data3sat/12bit/n12i6.txt
@@ -0,0 +1,12 @@
+ 12 10 4
+1 0 1 0 1 0 0 0 0 1 0 0
+ 3 4 6
+ 1 2 12
+ 6 7 10
+ 2 3 7
+ 2 3 9
+ 1 4 12
+ 1 6 9
+ 2 10 11
+ 1 8 11
+ 2 5 12
diff --git a/examples/data3sat/12bit/n12i7.txt b/examples/data3sat/12bit/n12i7.txt
new file mode 100644
index 000000000..3d3847432
--- /dev/null
+++ b/examples/data3sat/12bit/n12i7.txt
@@ -0,0 +1,12 @@
+ 12 10 5
+1 0 0 0 0 0 0 0 1 1 1 1
+ 6 7 10
+ 4 6 9
+ 3 5 12
+ 4 7 12
+ 4 5 9
+ 1 7 8
+ 1 3 6
+ 3 8 11
+ 2 8 9
+ 4 7 9
diff --git a/examples/data3sat/12bit/n12i8.txt b/examples/data3sat/12bit/n12i8.txt
new file mode 100644
index 000000000..045caad24
--- /dev/null
+++ b/examples/data3sat/12bit/n12i8.txt
@@ -0,0 +1,11 @@
+ 12 9 5
+1 0 0 0 1 0 1 1 1 0 0 0
+ 2 4 5
+ 2 7 12
+ 6 7 12
+ 4 9 12
+ 2 3 9
+ 1 11 12
+ 1 10 11
+ 3 8 12
+ 4 7 11
diff --git a/examples/data3sat/12bit/n12i9.txt b/examples/data3sat/12bit/n12i9.txt
new file mode 100644
index 000000000..78ed647bf
--- /dev/null
+++ b/examples/data3sat/12bit/n12i9.txt
@@ -0,0 +1,13 @@
+ 12 11 5
+0 0 0 1 1 1 1 0 1 0 0 0
+ 1 5 12
+ 3 9 10
+ 2 5 10
+ 1 4 11
+ 5 10 12
+ 3 4 8
+ 1 2 6
+ 3 7 10
+ 3 4 11
+ 1 6 8
+ 2 6 8
diff --git a/examples/data3sat/14bit/n14i1.txt b/examples/data3sat/14bit/n14i1.txt
new file mode 100644
index 000000000..2ed87740f
--- /dev/null
+++ b/examples/data3sat/14bit/n14i1.txt
@@ -0,0 +1,13 @@
+ 14 11 5
+0 0 1 1 0 0 1 0 0 0 1 1 0 0
+ 5 7 10
+ 7 8 13
+ 5 9 11
+ 4 6 10
+ 1 3 8
+ 5 6 7
+ 2 8 11
+ 8 10 11
+ 1 12 14
+ 8 12 13
+ 2 7 10
diff --git a/examples/data3sat/14bit/n14i10.txt b/examples/data3sat/14bit/n14i10.txt
new file mode 100644
index 000000000..3f793a58b
--- /dev/null
+++ b/examples/data3sat/14bit/n14i10.txt
@@ -0,0 +1,13 @@
+ 14 11 3
+0 1 0 1 0 0 0 1 0 0 0 0 0 0
+ 7 8 9
+ 6 8 12
+ 4 5 13
+ 1 2 3
+ 3 4 10
+ 2 6 10
+ 2 3 12
+ 2 7 14
+ 7 8 11
+ 5 8 9
+ 2 10 12
diff --git a/examples/data3sat/14bit/n14i2.txt b/examples/data3sat/14bit/n14i2.txt
new file mode 100644
index 000000000..19010a69e
--- /dev/null
+++ b/examples/data3sat/14bit/n14i2.txt
@@ -0,0 +1,13 @@
+ 14 11 4
+0 0 0 0 1 0 1 0 0 1 0 1 0 0
+ 5 9 13
+ 5 6 11
+ 6 7 9
+ 4 9 12
+ 1 7 11
+ 1 2 7
+ 3 5 14
+ 5 8 11
+ 4 7 13
+ 3 9 10
+ 3 12 13
diff --git a/examples/data3sat/14bit/n14i3.txt b/examples/data3sat/14bit/n14i3.txt
new file mode 100644
index 000000000..845e8b15a
--- /dev/null
+++ b/examples/data3sat/14bit/n14i3.txt
@@ -0,0 +1,15 @@
+ 14 13 4
+0 0 0 1 1 0 0 0 0 0 1 0 0 1
+ 9 11 12
+ 4 7 13
+ 7 13 14
+ 1 11 12
+ 2 9 11
+ 3 4 9
+ 1 10 14
+ 5 6 13
+ 3 13 14
+ 9 10 11
+ 6 11 13
+ 5 8 12
+ 3 11 12
diff --git a/examples/data3sat/14bit/n14i4.txt b/examples/data3sat/14bit/n14i4.txt
new file mode 100644
index 000000000..f3764b842
--- /dev/null
+++ b/examples/data3sat/14bit/n14i4.txt
@@ -0,0 +1,14 @@
+ 14 12 5
+0 1 1 0 1 0 0 0 0 0 0 1 0 1
+ 5 6 7
+ 2 8 9
+ 1 3 7
+ 3 10 13
+ 2 9 10
+ 5 8 11
+ 8 9 14
+ 4 7 12
+ 2 7 9
+ 6 8 12
+ 1 5 13
+ 1 10 14
diff --git a/examples/data3sat/14bit/n14i5.txt b/examples/data3sat/14bit/n14i5.txt
new file mode 100644
index 000000000..070cee396
--- /dev/null
+++ b/examples/data3sat/14bit/n14i5.txt
@@ -0,0 +1,13 @@
+ 14 11 5
+0 0 0 1 0 0 0 0 1 1 0 1 0 1
+ 3 7 9
+ 2 4 5
+ 1 6 10
+ 3 4 13
+ 5 6 12
+ 3 5 14
+ 1 5 14
+ 1 2 9
+ 8 10 11
+ 2 4 8
+ 4 6 11
diff --git a/examples/data3sat/14bit/n14i6.txt b/examples/data3sat/14bit/n14i6.txt
new file mode 100644
index 000000000..68860e8e6
--- /dev/null
+++ b/examples/data3sat/14bit/n14i6.txt
@@ -0,0 +1,14 @@
+ 14 12 5
+0 0 0 1 1 1 1 0 0 0 1 0 0 0
+ 5 9 12
+ 7 8 14
+ 3 4 10
+ 6 8 12
+ 8 11 14
+ 1 3 4
+ 1 6 12
+ 6 10 13
+ 4 10 14
+ 3 4 9
+ 7 12 13
+ 2 6 12
diff --git a/examples/data3sat/14bit/n14i7.txt b/examples/data3sat/14bit/n14i7.txt
new file mode 100644
index 000000000..4ada032c2
--- /dev/null
+++ b/examples/data3sat/14bit/n14i7.txt
@@ -0,0 +1,12 @@
+ 14 10 5
+1 0 0 0 0 0 0 0 1 1 1 1 0 0
+ 7 8 10
+ 1 4 5
+ 8 11 14
+ 6 9 13
+ 2 7 10
+ 6 11 13
+ 2 8 11
+ 3 7 12
+ 1 8 14
+ 4 12 13
diff --git a/examples/data3sat/14bit/n14i8.txt b/examples/data3sat/14bit/n14i8.txt
new file mode 100644
index 000000000..859bbf3fe
--- /dev/null
+++ b/examples/data3sat/14bit/n14i8.txt
@@ -0,0 +1,13 @@
+ 14 11 5
+1 0 1 0 1 0 0 0 0 0 1 0 1 0
+ 12 13 14
+ 5 8 9
+ 3 4 8
+ 6 8 11
+ 1 4 12
+ 1 2 10
+ 2 10 13
+ 2 7 11
+ 3 7 10
+ 2 4 11
+ 3 9 12
diff --git a/examples/data3sat/14bit/n14i9.txt b/examples/data3sat/14bit/n14i9.txt
new file mode 100644
index 000000000..c7f36c191
--- /dev/null
+++ b/examples/data3sat/14bit/n14i9.txt
@@ -0,0 +1,13 @@
+ 14 11 6
+0 0 1 0 1 0 1 0 0 1 0 0 1 1
+ 2 5 8
+ 3 8 9
+ 3 6 11
+ 4 6 10
+ 4 7 12
+ 1 8 10
+ 2 3 6
+ 1 9 13
+ 2 8 14
+ 2 11 14
+ 7 8 9
diff --git a/examples/data3sat/16bit/n16i1.txt b/examples/data3sat/16bit/n16i1.txt
new file mode 100644
index 000000000..8a55b449b
--- /dev/null
+++ b/examples/data3sat/16bit/n16i1.txt
@@ -0,0 +1,16 @@
+ 16 14 6
+1 1 1 0 0 1 0 0 1 0 0 0 0 0 1 0
+ 1 12 14
+ 3 10 12
+ 3 4 10
+ 1 12 16
+ 6 7 14
+ 3 14 16
+ 7 13 15
+ 5 9 13
+ 4 6 10
+ 5 6 7
+ 9 11 13
+ 2 11 12
+ 2 8 16
+ 4 5 15
diff --git a/examples/data3sat/16bit/n16i10.txt b/examples/data3sat/16bit/n16i10.txt
new file mode 100644
index 000000000..adaea90c7
--- /dev/null
+++ b/examples/data3sat/16bit/n16i10.txt
@@ -0,0 +1,16 @@
+ 16 14 4
+0 1 0 1 0 1 1 0 0 0 0 0 0 0 0 0
+ 7 9 16
+ 2 12 14
+ 6 10 11
+ 5 6 9
+ 7 9 14
+ 7 13 16
+ 7 9 12
+ 3 6 14
+ 7 8 13
+ 6 11 15
+ 4 12 16
+ 1 7 8
+ 6 9 11
+ 2 9 11
diff --git a/examples/data3sat/16bit/n16i2.txt b/examples/data3sat/16bit/n16i2.txt
new file mode 100644
index 000000000..b461830d1
--- /dev/null
+++ b/examples/data3sat/16bit/n16i2.txt
@@ -0,0 +1,15 @@
+ 16 13 5
+1 0 0 1 0 1 1 0 0 0 0 1 0 0 0 0
+ 3 4 15
+ 3 6 16
+ 1 5 16
+ 1 2 11
+ 9 12 13
+ 2 4 9
+ 1 9 10
+ 5 7 13
+ 4 10 16
+ 1 15 16
+ 3 7 14
+ 6 8 15
+ 3 10 12
diff --git a/examples/data3sat/16bit/n16i3.txt b/examples/data3sat/16bit/n16i3.txt
new file mode 100644
index 000000000..350f3634d
--- /dev/null
+++ b/examples/data3sat/16bit/n16i3.txt
@@ -0,0 +1,14 @@
+ 16 12 7
+1 1 1 0 0 0 1 0 0 0 1 0 1 0 0 1
+ 4 9 13
+ 3 6 15
+ 1 8 10
+ 3 8 10
+ 3 5 15
+ 2 5 6
+ 3 4 14
+ 8 9 11
+ 11 12 15
+ 1 5 6
+ 9 12 16
+ 7 10 15
diff --git a/examples/data3sat/16bit/n16i4.txt b/examples/data3sat/16bit/n16i4.txt
new file mode 100644
index 000000000..62fbc98de
--- /dev/null
+++ b/examples/data3sat/16bit/n16i4.txt
@@ -0,0 +1,15 @@
+ 16 13 6
+1 0 0 0 0 0 1 0 1 0 1 1 1 0 0 0
+ 10 13 14
+ 3 4 12
+ 5 6 12
+ 4 6 12
+ 5 13 15
+ 1 4 16
+ 1 2 8
+ 3 11 16
+ 2 4 7
+ 4 9 15
+ 8 10 12
+ 12 14 15
+ 4 5 11
diff --git a/examples/data3sat/16bit/n16i5.txt b/examples/data3sat/16bit/n16i5.txt
new file mode 100644
index 000000000..25328fd2f
--- /dev/null
+++ b/examples/data3sat/16bit/n16i5.txt
@@ -0,0 +1,15 @@
+ 16 13 5
+0 0 1 1 0 0 0 0 1 0 0 1 1 0 0 0
+ 8 9 11
+ 5 9 14
+ 7 10 12
+ 3 11 15
+ 2 4 15
+ 6 9 11
+ 4 15 16
+ 8 9 15
+ 1 2 13
+ 4 8 10
+ 1 8 13
+ 1 9 15
+ 2 10 12
diff --git a/examples/data3sat/16bit/n16i6.txt b/examples/data3sat/16bit/n16i6.txt
new file mode 100644
index 000000000..38a560fbc
--- /dev/null
+++ b/examples/data3sat/16bit/n16i6.txt
@@ -0,0 +1,16 @@
+ 16 14 6
+0 1 0 0 1 0 1 0 0 0 1 1 0 0 0 1
+ 6 13 16
+ 5 13 14
+ 5 8 10
+ 7 8 10
+ 1 6 12
+ 1 2 6
+ 8 15 16
+ 5 9 10
+ 7 9 14
+ 4 5 14
+ 1 2 4
+ 3 4 11
+ 2 3 14
+ 2 8 15
diff --git a/examples/data3sat/16bit/n16i7.txt b/examples/data3sat/16bit/n16i7.txt
new file mode 100644
index 000000000..ed32fa76d
--- /dev/null
+++ b/examples/data3sat/16bit/n16i7.txt
@@ -0,0 +1,15 @@
+ 16 13 7
+1 1 0 1 0 0 0 1 0 1 0 0 0 1 0 1
+ 1 7 12
+ 3 15 16
+ 2 9 12
+ 5 6 8
+ 6 7 10
+ 8 12 13
+ 3 4 11
+ 10 11 13
+ 3 13 16
+ 8 9 11
+ 5 11 16
+ 7 8 13
+ 3 9 14
diff --git a/examples/data3sat/16bit/n16i8.txt b/examples/data3sat/16bit/n16i8.txt
new file mode 100644
index 000000000..37903df91
--- /dev/null
+++ b/examples/data3sat/16bit/n16i8.txt
@@ -0,0 +1,15 @@
+ 16 13 7
+1 0 0 0 0 0 1 0 1 1 1 0 1 1 0 0
+ 5 7 12
+ 9 12 15
+ 4 8 10
+ 3 11 12
+ 5 11 16
+ 2 7 12
+ 3 8 9
+ 4 5 13
+ 3 14 15
+ 4 5 10
+ 1 5 12
+ 4 5 11
+ 6 14 16
diff --git a/examples/data3sat/16bit/n16i9.txt b/examples/data3sat/16bit/n16i9.txt
new file mode 100644
index 000000000..ed29acad2
--- /dev/null
+++ b/examples/data3sat/16bit/n16i9.txt
@@ -0,0 +1,15 @@
+ 16 13 5
+1 0 0 1 1 1 0 0 1 0 0 0 0 0 0 0
+ 2 6 10
+ 6 12 13
+ 6 8 12
+ 1 7 8
+ 3 4 8
+ 4 10 12
+ 1 7 12
+ 9 10 14
+ 4 7 13
+ 4 15 16
+ 5 11 14
+ 6 8 11
+ 9 13 16
diff --git a/examples/data3sat/18bit/n18i1.txt b/examples/data3sat/18bit/n18i1.txt
new file mode 100644
index 000000000..a1e2f4ca9
--- /dev/null
+++ b/examples/data3sat/18bit/n18i1.txt
@@ -0,0 +1,16 @@
+ 18 14 6
+1 0 1 0 0 0 1 1 1 0 0 0 0 0 1 0 0 0
+ 3 12 13
+ 4 15 16
+ 5 9 16
+ 9 16 18
+ 1 14 16
+ 3 11 14
+ 3 4 12
+ 1 13 18
+ 3 16 17
+ 8 14 17
+ 6 10 15
+ 5 7 11
+ 10 12 15
+ 2 9 18
diff --git a/examples/data3sat/18bit/n18i10.txt b/examples/data3sat/18bit/n18i10.txt
new file mode 100644
index 000000000..362489d75
--- /dev/null
+++ b/examples/data3sat/18bit/n18i10.txt
@@ -0,0 +1,17 @@
+ 18 15 6
+0 0 0 0 0 1 1 0 1 1 0 1 0 0 1 0 0 0
+ 3 6 8
+ 7 14 18
+ 6 13 17
+ 3 5 9
+ 2 4 15
+ 5 6 17
+ 2 12 13
+ 9 17 18
+ 2 15 16
+ 1 7 18
+ 4 10 11
+ 1 12 17
+ 5 9 16
+ 7 17 18
+ 3 7 11
diff --git a/examples/data3sat/18bit/n18i2.txt b/examples/data3sat/18bit/n18i2.txt
new file mode 100644
index 000000000..dff6a1d24
--- /dev/null
+++ b/examples/data3sat/18bit/n18i2.txt
@@ -0,0 +1,18 @@
+ 18 16 5
+0 0 0 0 1 1 1 0 0 1 0 0 0 0 1 0 0 0
+ 6 11 16
+ 8 10 18
+ 4 5 12
+ 8 10 16
+ 10 14 18
+ 2 4 5
+ 6 13 14
+ 2 8 15
+ 7 13 17
+ 5 13 18
+ 8 13 15
+ 9 15 17
+ 1 7 18
+ 4 14 15
+ 3 8 15
+ 1 6 17
diff --git a/examples/data3sat/18bit/n18i3.txt b/examples/data3sat/18bit/n18i3.txt
new file mode 100644
index 000000000..588464c23
--- /dev/null
+++ b/examples/data3sat/18bit/n18i3.txt
@@ -0,0 +1,16 @@
+ 18 14 7
+1 0 0 0 1 1 0 0 0 1 1 1 0 0 0 1 0 0
+ 3 12 18
+ 2 5 8
+ 4 10 17
+ 9 12 14
+ 14 16 17
+ 4 5 13
+ 4 8 11
+ 1 13 15
+ 1 3 14
+ 5 8 9
+ 7 10 15
+ 3 12 17
+ 6 9 17
+ 1 2 9
diff --git a/examples/data3sat/18bit/n18i4.txt b/examples/data3sat/18bit/n18i4.txt
new file mode 100644
index 000000000..36a129cf5
--- /dev/null
+++ b/examples/data3sat/18bit/n18i4.txt
@@ -0,0 +1,17 @@
+ 18 15 6
+0 1 0 1 0 0 1 0 1 0 0 0 1 0 0 0 1 0
+ 8 15 17
+ 6 9 12
+ 6 10 17
+ 7 8 11
+ 3 8 17
+ 3 9 18
+ 2 6 15
+ 11 13 15
+ 4 11 14
+ 7 14 15
+ 2 5 10
+ 9 10 16
+ 4 6 8
+ 1 7 14
+ 6 12 17
diff --git a/examples/data3sat/18bit/n18i5.txt b/examples/data3sat/18bit/n18i5.txt
new file mode 100644
index 000000000..d271ad5b2
--- /dev/null
+++ b/examples/data3sat/18bit/n18i5.txt
@@ -0,0 +1,17 @@
+ 18 15 5
+1 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 0 0
+ 8 15 18
+ 8 10 13
+ 3 7 16
+ 8 9 15
+ 6 13 17
+ 4 13 18
+ 1 8 9
+ 7 10 12
+ 2 11 13
+ 3 6 7
+ 10 15 17
+ 7 10 17
+ 5 9 12
+ 8 15 17
+ 5 14 16
diff --git a/examples/data3sat/18bit/n18i6.txt b/examples/data3sat/18bit/n18i6.txt
new file mode 100644
index 000000000..dd62fdf26
--- /dev/null
+++ b/examples/data3sat/18bit/n18i6.txt
@@ -0,0 +1,17 @@
+ 18 15 7
+0 0 1 0 0 1 1 0 1 1 0 0 1 0 0 0 0 1
+ 4 6 12
+ 5 8 13
+ 6 8 16
+ 7 11 16
+ 5 13 17
+ 9 15 16
+ 9 11 12
+ 2 8 9
+ 3 8 12
+ 2 15 18
+ 4 13 15
+ 4 8 18
+ 4 5 10
+ 4 13 14
+ 1 13 16
diff --git a/examples/data3sat/18bit/n18i7.txt b/examples/data3sat/18bit/n18i7.txt
new file mode 100644
index 000000000..f918b5fe5
--- /dev/null
+++ b/examples/data3sat/18bit/n18i7.txt
@@ -0,0 +1,17 @@
+ 18 15 7
+0 0 1 0 0 0 0 1 1 1 0 0 0 0 1 1 1 0
+ 7 15 18
+ 4 10 12
+ 3 11 14
+ 2 12 16
+ 4 14 16
+ 3 4 6
+ 1 4 8
+ 5 6 9
+ 6 8 18
+ 1 3 18
+ 2 5 17
+ 3 5 7
+ 12 14 15
+ 5 9 18
+ 12 13 15
diff --git a/examples/data3sat/18bit/n18i8.txt b/examples/data3sat/18bit/n18i8.txt
new file mode 100644
index 000000000..93e3bf8b6
--- /dev/null
+++ b/examples/data3sat/18bit/n18i8.txt
@@ -0,0 +1,17 @@
+ 18 15 5
+0 0 1 0 1 1 1 0 0 0 0 1 0 0 0 0 0 0
+ 6 8 15
+ 4 5 17
+ 2 3 17
+ 1 3 16
+ 6 16 17
+ 6 10 13
+ 9 12 13
+ 1 5 11
+ 5 14 18
+ 4 7 11
+ 3 8 11
+ 2 5 10
+ 4 7 14
+ 11 12 15
+ 8 10 12
diff --git a/examples/data3sat/18bit/n18i9.txt b/examples/data3sat/18bit/n18i9.txt
new file mode 100644
index 000000000..4ee9d6e27
--- /dev/null
+++ b/examples/data3sat/18bit/n18i9.txt
@@ -0,0 +1,17 @@
+ 18 15 7
+0 1 0 1 1 0 0 0 1 1 0 0 0 0 0 1 0 1
+ 13 14 18
+ 1 7 16
+ 2 14 15
+ 3 10 13
+ 1 4 17
+ 5 7 15
+ 6 10 13
+ 5 8 13
+ 2 15 17
+ 5 12 15
+ 2 13 14
+ 3 9 12
+ 3 4 8
+ 10 11 13
+ 9 13 15
diff --git a/examples/data3sat/20bit/n20i1.txt b/examples/data3sat/20bit/n20i1.txt
new file mode 100644
index 000000000..3a72835dd
--- /dev/null
+++ b/examples/data3sat/20bit/n20i1.txt
@@ -0,0 +1,19 @@
+ 20 17 8
+1 0 0 0 0 1 0 0 1 0 0 1 1 1 1 1 0 0 0 0
+ 1 17 20
+ 1 2 8
+ 2 16 19
+ 4 13 19
+ 7 12 17
+ 9 11 20
+ 4 5 14
+ 9 11 18
+ 11 16 20
+ 2 4 6
+ 2 9 17
+ 8 14 19
+ 5 14 20
+ 8 15 17
+ 10 16 19
+ 3 9 17
+ 1 7 19
diff --git a/examples/data3sat/20bit/n20i10.txt b/examples/data3sat/20bit/n20i10.txt
new file mode 100644
index 000000000..a23ba31fa
--- /dev/null
+++ b/examples/data3sat/20bit/n20i10.txt
@@ -0,0 +1,19 @@
+ 20 17 7
+1 0 0 0 1 0 0 0 1 1 0 1 0 0 0 0 1 1 0 0
+ 9 13 15
+ 10 19 20
+ 9 19 20
+ 4 10 19
+ 4 13 18
+ 1 3 14
+ 1 6 19
+ 4 11 18
+ 1 7 8
+ 5 11 20
+ 13 14 17
+ 2 10 11
+ 2 8 12
+ 12 16 20
+ 8 10 11
+ 3 4 5
+ 1 6 14
diff --git a/examples/data3sat/20bit/n20i2.txt b/examples/data3sat/20bit/n20i2.txt
new file mode 100644
index 000000000..3182c2f0f
--- /dev/null
+++ b/examples/data3sat/20bit/n20i2.txt
@@ -0,0 +1,18 @@
+ 20 16 6
+0 1 0 0 0 0 1 1 0 0 0 1 1 0 0 0 1 0 0 0
+ 6 8 19
+ 2 3 9
+ 2 4 20
+ 1 7 14
+ 1 3 13
+ 5 13 14
+ 4 10 13
+ 1 4 8
+ 2 11 19
+ 4 10 17
+ 3 7 20
+ 2 15 18
+ 8 15 20
+ 5 12 20
+ 3 8 15
+ 13 16 18
diff --git a/examples/data3sat/20bit/n20i3.txt b/examples/data3sat/20bit/n20i3.txt
new file mode 100644
index 000000000..305c674be
--- /dev/null
+++ b/examples/data3sat/20bit/n20i3.txt
@@ -0,0 +1,18 @@
+ 20 16 8
+0 0 0 1 0 0 1 1 1 0 0 0 1 0 1 0 1 1 0 0
+ 3 9 14
+ 2 7 20
+ 6 9 12
+ 6 17 20
+ 1 7 19
+ 8 14 19
+ 5 7 16
+ 3 6 15
+ 2 4 14
+ 2 7 12
+ 5 14 18
+ 5 11 13
+ 7 16 20
+ 6 16 18
+ 6 10 13
+ 9 10 12
diff --git a/examples/data3sat/20bit/n20i4.txt b/examples/data3sat/20bit/n20i4.txt
new file mode 100644
index 000000000..a3c6d5a37
--- /dev/null
+++ b/examples/data3sat/20bit/n20i4.txt
@@ -0,0 +1,19 @@
+ 20 17 6
+0 0 0 0 0 1 1 0 1 1 0 1 0 0 0 0 0 1 0 0
+ 7 14 19
+ 2 9 15
+ 4 10 11
+ 5 6 16
+ 4 17 18
+ 5 6 13
+ 1 6 15
+ 5 6 14
+ 3 16 18
+ 7 17 20
+ 2 8 12
+ 7 15 17
+ 8 10 15
+ 1 8 10
+ 3 5 10
+ 4 12 15
+ 3 7 15
diff --git a/examples/data3sat/20bit/n20i5.txt b/examples/data3sat/20bit/n20i5.txt
new file mode 100644
index 000000000..e457e1d99
--- /dev/null
+++ b/examples/data3sat/20bit/n20i5.txt
@@ -0,0 +1,18 @@
+ 20 16 7
+0 1 0 0 1 1 1 1 0 1 0 0 0 1 0 0 0 0 0 0
+ 4 5 16
+ 6 9 15
+ 6 9 18
+ 8 12 18
+ 6 15 19
+ 10 16 18
+ 10 12 13
+ 3 9 10
+ 3 8 13
+ 2 16 20
+ 4 14 16
+ 5 9 20
+ 4 5 11
+ 7 12 17
+ 1 14 17
+ 2 4 20
diff --git a/examples/data3sat/20bit/n20i6.txt b/examples/data3sat/20bit/n20i6.txt
new file mode 100644
index 000000000..8a0914c1f
--- /dev/null
+++ b/examples/data3sat/20bit/n20i6.txt
@@ -0,0 +1,18 @@
+ 20 16 8
+0 0 0 0 0 0 1 0 1 0 0 1 0 1 1 1 1 0 0 1
+ 10 13 16
+ 5 6 14
+ 8 14 18
+ 9 13 18
+ 11 19 20
+ 2 11 12
+ 11 13 14
+ 1 5 12
+ 5 6 15
+ 8 12 18
+ 4 10 14
+ 7 11 19
+ 1 3 20
+ 10 11 17
+ 10 12 19
+ 7 11 18
diff --git a/examples/data3sat/20bit/n20i7.txt b/examples/data3sat/20bit/n20i7.txt
new file mode 100644
index 000000000..fa2fb9da4
--- /dev/null
+++ b/examples/data3sat/20bit/n20i7.txt
@@ -0,0 +1,19 @@
+ 20 17 8
+1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 1 0 1 1 1
+ 11 12 17
+ 3 17 18
+ 6 8 9
+ 15 17 20
+ 2 7 12
+ 3 9 20
+ 12 17 20
+ 2 14 19
+ 10 13 19
+ 2 14 16
+ 4 13 20
+ 1 2 17
+ 8 14 15
+ 6 15 20
+ 10 12 16
+ 3 10 18
+ 5 17 19
diff --git a/examples/data3sat/20bit/n20i8.txt b/examples/data3sat/20bit/n20i8.txt
new file mode 100644
index 000000000..278c6b4ac
--- /dev/null
+++ b/examples/data3sat/20bit/n20i8.txt
@@ -0,0 +1,19 @@
+ 20 17 7
+0 0 1 0 0 0 0 0 0 0 1 1 0 1 0 1 0 0 1 1
+ 9 17 20
+ 1 18 19
+ 5 14 15
+ 6 8 14
+ 13 15 19
+ 1 6 19
+ 9 13 14
+ 6 18 19
+ 8 9 11
+ 7 8 16
+ 2 13 14
+ 2 10 12
+ 1 7 14
+ 1 3 17
+ 4 12 13
+ 1 2 12
+ 8 13 20
diff --git a/examples/data3sat/20bit/n20i9.txt b/examples/data3sat/20bit/n20i9.txt
new file mode 100644
index 000000000..acd107ac3
--- /dev/null
+++ b/examples/data3sat/20bit/n20i9.txt
@@ -0,0 +1,19 @@
+ 20 17 8
+1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 1 1 1 0 0
+ 3 11 15
+ 1 5 19
+ 5 8 17
+ 6 11 14
+ 5 9 14
+ 2 17 19
+ 5 13 16
+ 2 14 16
+ 4 10 13
+ 3 5 9
+ 7 8 12
+ 11 12 15
+ 10 15 17
+ 8 14 16
+ 1 13 15
+ 15 18 20
+ 12 15 18
diff --git a/examples/data3sat/22bit/n22i1.txt b/examples/data3sat/22bit/n22i1.txt
new file mode 100644
index 000000000..067b1d31e
--- /dev/null
+++ b/examples/data3sat/22bit/n22i1.txt
@@ -0,0 +1,20 @@
+ 22 18 9
+0 1 1 0 1 1 0 1 0 0 1 1 0 0 1 0 0 1 0 0 0 0
+ 1 6 21
+ 1 11 17
+ 4 18 20
+ 1 15 20
+ 2 9 22
+ 10 13 18
+ 4 6 21
+ 8 10 17
+ 5 9 22
+ 7 12 21
+ 7 8 16
+ 1 4 11
+ 3 14 16
+ 7 12 17
+ 6 10 19
+ 3 10 13
+ 11 13 22
+ 9 12 19
diff --git a/examples/data3sat/22bit/n22i10.txt b/examples/data3sat/22bit/n22i10.txt
new file mode 100644
index 000000000..eefab8551
--- /dev/null
+++ b/examples/data3sat/22bit/n22i10.txt
@@ -0,0 +1,19 @@
+ 22 17 8
+0 1 0 0 0 0 0 1 0 1 0 0 1 1 0 0 1 0 1 1 0 0
+ 1 13 18
+ 7 13 16
+ 8 9 18
+ 5 18 19
+ 12 20 21
+ 5 20 21
+ 6 9 17
+ 9 13 15
+ 5 14 15
+ 4 5 20
+ 3 11 13
+ 11 12 19
+ 1 10 16
+ 2 11 18
+ 16 19 21
+ 6 10 22
+ 3 7 13
diff --git a/examples/data3sat/22bit/n22i2.txt b/examples/data3sat/22bit/n22i2.txt
new file mode 100644
index 000000000..ce2849f32
--- /dev/null
+++ b/examples/data3sat/22bit/n22i2.txt
@@ -0,0 +1,21 @@
+ 22 19 9
+0 1 0 0 0 1 1 0 0 0 0 0 0 1 1 0 1 1 1 0 0 1
+ 3 6 8
+ 4 5 19
+ 11 12 15
+ 15 16 21
+ 1 7 8
+ 12 17 21
+ 9 14 20
+ 4 21 22
+ 3 10 15
+ 9 18 20
+ 3 12 17
+ 4 11 18
+ 2 12 21
+ 5 19 21
+ 7 12 13
+ 4 6 13
+ 9 13 18
+ 1 5 18
+ 1 3 15
diff --git a/examples/data3sat/22bit/n22i3.txt b/examples/data3sat/22bit/n22i3.txt
new file mode 100644
index 000000000..82fc993b2
--- /dev/null
+++ b/examples/data3sat/22bit/n22i3.txt
@@ -0,0 +1,22 @@
+ 22 20 8
+0 0 1 0 1 1 0 0 1 0 1 0 1 0 0 0 0 1 0 1 0 0
+ 3 8 22
+ 2 17 20
+ 9 16 21
+ 5 14 21
+ 3 8 16
+ 13 16 17
+ 15 17 20
+ 2 3 12
+ 2 10 11
+ 1 18 21
+ 1 3 4
+ 8 13 15
+ 2 4 20
+ 3 12 19
+ 4 13 14
+ 7 13 22
+ 6 16 17
+ 4 10 20
+ 8 9 14
+ 14 18 19
diff --git a/examples/data3sat/22bit/n22i4.txt b/examples/data3sat/22bit/n22i4.txt
new file mode 100644
index 000000000..8f106a552
--- /dev/null
+++ b/examples/data3sat/22bit/n22i4.txt
@@ -0,0 +1,19 @@
+ 22 17 6
+0 0 1 0 0 1 0 0 0 1 0 1 0 1 1 0 0 0 0 0 0 0
+ 1 14 21
+ 2 12 22
+ 3 18 22
+ 11 13 14
+ 7 12 19
+ 9 14 16
+ 4 15 21
+ 3 5 20
+ 9 13 15
+ 6 21 22
+ 11 12 20
+ 2 3 18
+ 5 11 14
+ 1 10 17
+ 1 12 21
+ 9 14 17
+ 4 8 14
diff --git a/examples/data3sat/22bit/n22i5.txt b/examples/data3sat/22bit/n22i5.txt
new file mode 100644
index 000000000..aacc9fdb6
--- /dev/null
+++ b/examples/data3sat/22bit/n22i5.txt
@@ -0,0 +1,21 @@
+ 22 19 9
+1 0 0 1 0 0 1 0 1 0 0 0 1 1 1 0 0 0 0 1 0 1
+ 4 10 18
+ 6 14 18
+ 5 15 16
+ 5 8 14
+ 7 11 12
+ 7 18 21
+ 10 16 20
+ 12 14 19
+ 3 18 20
+ 11 16 22
+ 11 20 21
+ 5 15 18
+ 5 8 15
+ 3 13 18
+ 1 5 21
+ 9 17 21
+ 1 3 18
+ 2 5 9
+ 3 7 17
diff --git a/examples/data3sat/22bit/n22i6.txt b/examples/data3sat/22bit/n22i6.txt
new file mode 100644
index 000000000..2c38c97b5
--- /dev/null
+++ b/examples/data3sat/22bit/n22i6.txt
@@ -0,0 +1,21 @@
+ 22 19 7
+0 1 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 0 1 1 0 1
+ 10 18 22
+ 9 12 16
+ 4 8 19
+ 10 11 18
+ 7 15 21
+ 5 16 22
+ 2 3 14
+ 1 9 11
+ 9 12 15
+ 2 13 16
+ 4 7 9
+ 5 20 21
+ 12 18 21
+ 9 13 20
+ 6 11 15
+ 10 18 20
+ 6 17 20
+ 15 21 22
+ 4 13 19
diff --git a/examples/data3sat/22bit/n22i7.txt b/examples/data3sat/22bit/n22i7.txt
new file mode 100644
index 000000000..1899d9e67
--- /dev/null
+++ b/examples/data3sat/22bit/n22i7.txt
@@ -0,0 +1,20 @@
+ 22 18 7
+1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 1 0 0 1 0 0
+ 6 19 21
+ 2 13 17
+ 9 10 11
+ 1 4 10
+ 3 10 20
+ 8 17 21
+ 1 5 18
+ 1 19 22
+ 5 13 20
+ 4 6 22
+ 15 19 22
+ 4 7 11
+ 3 13 16
+ 19 20 22
+ 6 7 10
+ 6 8 10
+ 6 8 14
+ 12 14 17
diff --git a/examples/data3sat/22bit/n22i8.txt b/examples/data3sat/22bit/n22i8.txt
new file mode 100644
index 000000000..325507ccc
--- /dev/null
+++ b/examples/data3sat/22bit/n22i8.txt
@@ -0,0 +1,20 @@
+ 22 18 8
+1 0 1 0 1 1 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 1
+ 1 16 19
+ 19 20 21
+ 6 9 12
+ 13 16 17
+ 1 9 13
+ 14 15 19
+ 5 8 21
+ 3 8 13
+ 5 12 13
+ 6 16 21
+ 1 9 21
+ 2 7 22
+ 4 19 20
+ 8 14 21
+ 4 16 22
+ 5 7 9
+ 11 17 18
+ 5 10 13
diff --git a/examples/data3sat/22bit/n22i9.txt b/examples/data3sat/22bit/n22i9.txt
new file mode 100644
index 000000000..7405004ee
--- /dev/null
+++ b/examples/data3sat/22bit/n22i9.txt
@@ -0,0 +1,22 @@
+ 22 20 7
+0 0 1 0 0 1 1 0 0 0 0 1 1 0 0 0 0 0 1 0 1 0
+ 2 19 22
+ 2 18 19
+ 15 20 21
+ 5 16 19
+ 4 17 19
+ 1 3 20
+ 5 6 16
+ 1 6 17
+ 3 16 22
+ 10 11 12
+ 11 16 19
+ 3 10 14
+ 8 17 19
+ 7 11 22
+ 9 18 21
+ 5 12 15
+ 4 13 17
+ 9 19 22
+ 2 14 19
+ 4 16 19
diff --git a/examples/data3sat/24bit/n24i1.txt b/examples/data3sat/24bit/n24i1.txt
new file mode 100644
index 000000000..d20762618
--- /dev/null
+++ b/examples/data3sat/24bit/n24i1.txt
@@ -0,0 +1,24 @@
+ 24 22 10
+0 1 0 0 0 1 0 0 1 0 0 1 0 1 0 0 1 0 0 1 1 1 1 0
+ 5 18 20
+ 4 10 20
+ 1 8 22
+ 10 19 23
+ 6 7 16
+ 2 4 11
+ 6 13 19
+ 16 17 18
+ 5 8 22
+ 1 12 15
+ 4 12 15
+ 4 7 22
+ 3 7 9
+ 12 13 16
+ 16 18 23
+ 1 7 9
+ 13 18 23
+ 10 15 22
+ 5 23 24
+ 3 11 17
+ 10 19 21
+ 3 14 19
diff --git a/examples/data3sat/24bit/n24i10.txt b/examples/data3sat/24bit/n24i10.txt
new file mode 100644
index 000000000..1f5259f15
--- /dev/null
+++ b/examples/data3sat/24bit/n24i10.txt
@@ -0,0 +1,23 @@
+ 24 21 8
+0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 1 1 1 0 1 0
+ 7 9 19
+ 11 17 18
+ 4 5 17
+ 6 14 20
+ 1 12 23
+ 11 15 19
+ 8 21 22
+ 4 17 22
+ 9 16 24
+ 13 18 19
+ 6 14 19
+ 22 23 24
+ 3 9 23
+ 1 2 8
+ 9 13 17
+ 3 9 17
+ 1 20 22
+ 4 12 21
+ 6 21 22
+ 9 18 21
+ 6 10 12
diff --git a/examples/data3sat/24bit/n24i2.txt b/examples/data3sat/24bit/n24i2.txt
new file mode 100644
index 000000000..fe0816dd9
--- /dev/null
+++ b/examples/data3sat/24bit/n24i2.txt
@@ -0,0 +1,22 @@
+ 24 20 8
+1 1 1 0 0 0 1 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0 0 0
+ 4 12 20
+ 2 13 23
+ 6 21 23
+ 21 22 23
+ 8 13 14
+ 4 6 14
+ 10 14 19
+ 1 6 20
+ 1 4 17
+ 14 18 19
+ 3 16 23
+ 7 20 22
+ 4 12 19
+ 5 12 16
+ 2 6 19
+ 5 15 20
+ 7 9 23
+ 2 4 11
+ 2 5 24
+ 2 9 17
diff --git a/examples/data3sat/24bit/n24i3.txt b/examples/data3sat/24bit/n24i3.txt
new file mode 100644
index 000000000..98eb5218b
--- /dev/null
+++ b/examples/data3sat/24bit/n24i3.txt
@@ -0,0 +1,22 @@
+ 24 20 8
+1 0 0 1 1 0 0 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1
+ 2 11 15
+ 3 23 24
+ 12 13 20
+ 9 10 17
+ 15 21 24
+ 1 16 22
+ 1 14 21
+ 3 4 18
+ 17 22 24
+ 13 15 21
+ 1 22 23
+ 8 16 17
+ 9 10 16
+ 1 15 18
+ 5 7 21
+ 1 6 16
+ 3 5 15
+ 11 17 23
+ 10 21 23
+ 8 19 23
diff --git a/examples/data3sat/24bit/n24i4.txt b/examples/data3sat/24bit/n24i4.txt
new file mode 100644
index 000000000..1992681d1
--- /dev/null
+++ b/examples/data3sat/24bit/n24i4.txt
@@ -0,0 +1,22 @@
+ 24 20 9
+0 0 1 0 0 1 0 0 1 0 0 1 1 0 0 0 0 0 1 1 1 0 1 0
+ 4 14 20
+ 1 14 21
+ 11 17 20
+ 8 10 20
+ 5 6 22
+ 2 4 23
+ 1 3 22
+ 8 21 22
+ 8 13 17
+ 12 15 18
+ 1 6 14
+ 7 19 24
+ 5 9 14
+ 3 11 15
+ 2 6 14
+ 6 7 8
+ 5 9 18
+ 14 16 20
+ 10 13 16
+ 11 19 22
diff --git a/examples/data3sat/24bit/n24i5.txt b/examples/data3sat/24bit/n24i5.txt
new file mode 100644
index 000000000..64b377127
--- /dev/null
+++ b/examples/data3sat/24bit/n24i5.txt
@@ -0,0 +1,21 @@
+ 24 19 10
+0 0 0 1 0 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1 1 0 0 1
+ 16 19 23
+ 5 10 16
+ 13 15 18
+ 4 8 10
+ 9 19 24
+ 7 17 23
+ 3 6 12
+ 3 5 20
+ 6 8 23
+ 3 15 17
+ 12 23 24
+ 2 19 21
+ 2 9 24
+ 6 13 15
+ 1 16 23
+ 7 12 21
+ 9 22 24
+ 1 11 12
+ 3 9 14
diff --git a/examples/data3sat/24bit/n24i6.txt b/examples/data3sat/24bit/n24i6.txt
new file mode 100644
index 000000000..32705a107
--- /dev/null
+++ b/examples/data3sat/24bit/n24i6.txt
@@ -0,0 +1,24 @@
+ 24 22 9
+1 1 0 1 0 0 1 0 0 0 1 0 1 0 0 1 0 0 0 0 1 1 0 0
+ 2 5 14
+ 1 5 17
+ 3 7 9
+ 2 8 15
+ 1 12 18
+ 2 3 15
+ 7 8 20
+ 3 10 21
+ 8 9 13
+ 12 14 22
+ 1 12 14
+ 6 16 17
+ 15 19 21
+ 1 20 24
+ 3 8 11
+ 1 18 24
+ 3 13 19
+ 4 14 19
+ 8 13 17
+ 3 7 23
+ 8 17 22
+ 5 16 18
diff --git a/examples/data3sat/24bit/n24i7.txt b/examples/data3sat/24bit/n24i7.txt
new file mode 100644
index 000000000..02535bdcb
--- /dev/null
+++ b/examples/data3sat/24bit/n24i7.txt
@@ -0,0 +1,20 @@
+ 24 18 9
+0 0 0 0 1 0 0 1 0 0 0 0 1 1 1 1 0 0 1 0 1 0 1 0
+ 6 7 13
+ 8 18 22
+ 4 19 20
+ 4 11 21
+ 6 13 18
+ 2 3 16
+ 6 17 23
+ 3 5 22
+ 10 18 23
+ 11 12 14
+ 4 6 16
+ 9 10 19
+ 11 13 24
+ 1 6 15
+ 8 12 17
+ 7 15 18
+ 6 18 19
+ 12 13 18
diff --git a/examples/data3sat/24bit/n24i8.txt b/examples/data3sat/24bit/n24i8.txt
new file mode 100644
index 000000000..c2370e818
--- /dev/null
+++ b/examples/data3sat/24bit/n24i8.txt
@@ -0,0 +1,23 @@
+ 24 21 8
+0 1 0 0 1 0 0 1 1 0 0 0 1 0 1 0 0 1 0 0 0 0 1 0
+ 4 8 14
+ 1 2 17
+ 5 6 21
+ 14 18 24
+ 2 3 14
+ 7 12 23
+ 5 7 20
+ 15 17 19
+ 10 18 19
+ 12 21 23
+ 5 11 22
+ 5 6 22
+ 9 11 22
+ 11 17 23
+ 5 7 22
+ 4 9 20
+ 11 13 22
+ 6 10 18
+ 8 12 16
+ 2 4 7
+ 7 10 23
diff --git a/examples/data3sat/24bit/n24i9.txt b/examples/data3sat/24bit/n24i9.txt
new file mode 100644
index 000000000..c7cc4b481
--- /dev/null
+++ b/examples/data3sat/24bit/n24i9.txt
@@ -0,0 +1,21 @@
+ 24 19 7
+0 1 0 0 0 1 1 0 1 0 0 0 0 1 0 0 0 0 1 1 0 0 0 0
+ 3 5 7
+ 6 11 18
+ 10 11 14
+ 11 12 14
+ 4 7 18
+ 13 14 21
+ 1 5 7
+ 4 6 13
+ 7 16 24
+ 2 3 18
+ 2 4 22
+ 1 12 19
+ 1 14 18
+ 3 4 20
+ 6 17 23
+ 9 15 22
+ 1 9 23
+ 3 20 23
+ 8 9 13
diff --git a/examples/data3sat/26bit/n26i1.txt b/examples/data3sat/26bit/n26i1.txt
new file mode 100644
index 000000000..688c67c88
--- /dev/null
+++ b/examples/data3sat/26bit/n26i1.txt
@@ -0,0 +1,23 @@
+ 26 21 9
+1 0 1 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 1
+ 5 9 24
+ 1 13 16
+ 4 9 10
+ 4 13 17
+ 5 7 24
+ 4 8 9
+ 5 6 23
+ 13 14 17
+ 17 19 25
+ 1 8 9
+ 14 20 25
+ 10 16 24
+ 5 25 26
+ 3 12 18
+ 11 21 23
+ 3 15 20
+ 5 13 21
+ 2 14 25
+ 6 22 25
+ 23 24 25
+ 4 7 15
diff --git a/examples/data3sat/26bit/n26i10.txt b/examples/data3sat/26bit/n26i10.txt
new file mode 100644
index 000000000..09694d65f
--- /dev/null
+++ b/examples/data3sat/26bit/n26i10.txt
@@ -0,0 +1,24 @@
+ 26 22 9
+1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0
+ 1 15 22
+ 22 23 24
+ 10 19 26
+ 11 15 23
+ 7 21 26
+ 1 13 14
+ 7 16 18
+ 2 9 18
+ 20 23 25
+ 4 9 20
+ 3 9 21
+ 6 9 12
+ 15 21 25
+ 7 18 23
+ 3 6 17
+ 9 22 23
+ 7 8 10
+ 1 3 13
+ 3 6 7
+ 8 20 21
+ 20 23 24
+ 5 18 19
diff --git a/examples/data3sat/26bit/n26i2.txt b/examples/data3sat/26bit/n26i2.txt
new file mode 100644
index 000000000..328c9204b
--- /dev/null
+++ b/examples/data3sat/26bit/n26i2.txt
@@ -0,0 +1,24 @@
+ 26 22 10
+0 1 0 0 1 0 0 0 0 1 1 1 1 0 0 0 0 1 0 0 1 1 0 0 1 0
+ 2 6 15
+ 1 6 18
+ 3 8 10
+ 2 8 16
+ 1 13 20
+ 2 4 16
+ 7 8 22
+ 3 11 23
+ 9 10 14
+ 13 16 24
+ 1 13 15
+ 6 17 18
+ 16 21 23
+ 1 22 26
+ 3 9 12
+ 4 5 24
+ 2 19 26
+ 3 14 21
+ 4 16 21
+ 9 14 18
+ 3 8 25
+ 9 18 24
diff --git a/examples/data3sat/26bit/n26i3.txt b/examples/data3sat/26bit/n26i3.txt
new file mode 100644
index 000000000..82ce153c0
--- /dev/null
+++ b/examples/data3sat/26bit/n26i3.txt
@@ -0,0 +1,25 @@
+ 26 23 8
+1 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 1 0 1
+ 3 5 23
+ 10 20 25
+ 12 13 15
+ 4 6 18
+ 9 11 21
+ 12 14 26
+ 1 7 16
+ 9 12 18
+ 7 16 20
+ 7 20 21
+ 4 5 24
+ 13 14 19
+ 7 10 11
+ 14 19 26
+ 1 6 8
+ 1 3 17
+ 4 18 19
+ 6 22 23
+ 1 17 19
+ 2 16 23
+ 3 5 26
+ 6 10 23
+ 6 12 20
diff --git a/examples/data3sat/26bit/n26i4.txt b/examples/data3sat/26bit/n26i4.txt
new file mode 100644
index 000000000..33b7012ea
--- /dev/null
+++ b/examples/data3sat/26bit/n26i4.txt
@@ -0,0 +1,24 @@
+ 26 22 10
+0 1 0 0 0 0 1 1 0 0 0 0 0 1 1 0 1 1 0 0 1 1 0 0 0 1
+ 6 8 11
+ 2 9 16
+ 3 10 22
+ 9 15 23
+ 1 8 16
+ 12 13 15
+ 4 8 23
+ 3 15 23
+ 8 12 16
+ 8 10 25
+ 6 12 18
+ 9 20 21
+ 10 21 24
+ 1 14 19
+ 11 12 15
+ 2 19 20
+ 4 20 22
+ 5 14 24
+ 11 15 23
+ 7 10 23
+ 5 10 17
+ 6 25 26
diff --git a/examples/data3sat/26bit/n26i5.txt b/examples/data3sat/26bit/n26i5.txt
new file mode 100644
index 000000000..7659126d3
--- /dev/null
+++ b/examples/data3sat/26bit/n26i5.txt
@@ -0,0 +1,25 @@
+ 26 23 9
+0 0 0 0 1 1 0 0 1 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 1 1
+ 4 21 25
+ 1 3 24
+ 8 10 15
+ 15 23 25
+ 1 4 9
+ 10 14 18
+ 5 7 12
+ 2 4 24
+ 12 13 22
+ 4 11 22
+ 11 12 24
+ 1 19 21
+ 10 20 23
+ 16 21 26
+ 6 7 23
+ 11 22 23
+ 1 10 11
+ 8 20 24
+ 5 8 21
+ 7 17 26
+ 13 14 24
+ 9 12 17
+ 2 5 7
diff --git a/examples/data3sat/26bit/n26i6.txt b/examples/data3sat/26bit/n26i6.txt
new file mode 100644
index 000000000..fcc31038c
--- /dev/null
+++ b/examples/data3sat/26bit/n26i6.txt
@@ -0,0 +1,23 @@
+ 26 21 10
+0 0 0 0 1 0 0 1 1 1 0 1 1 0 0 1 0 0 0 0 0 0 0 1 1 1
+ 11 16 19
+ 5 14 21
+ 3 11 13
+ 2 3 24
+ 7 9 21
+ 12 17 23
+ 1 10 21
+ 4 13 21
+ 9 20 21
+ 2 13 23
+ 8 19 22
+ 5 17 20
+ 1 23 25
+ 10 20 22
+ 6 21 25
+ 5 15 17
+ 15 21 25
+ 2 4 8
+ 8 14 22
+ 1 10 18
+ 2 6 26
diff --git a/examples/data3sat/26bit/n26i7.txt b/examples/data3sat/26bit/n26i7.txt
new file mode 100644
index 000000000..486949b95
--- /dev/null
+++ b/examples/data3sat/26bit/n26i7.txt
@@ -0,0 +1,22 @@
+ 26 20 9
+0 0 0 1 1 0 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 1 0 0 0 1
+ 9 10 26
+ 8 21 24
+ 5 10 14
+ 18 19 20
+ 7 13 23
+ 2 15 18
+ 2 20 22
+ 1 15 22
+ 12 13 20
+ 3 11 20
+ 5 7 24
+ 4 8 9
+ 8 22 24
+ 2 11 16
+ 2 3 4
+ 8 21 25
+ 1 7 21
+ 11 14 24
+ 2 3 17
+ 5 6 12
diff --git a/examples/data3sat/26bit/n26i8.txt b/examples/data3sat/26bit/n26i8.txt
new file mode 100644
index 000000000..e3e29319a
--- /dev/null
+++ b/examples/data3sat/26bit/n26i8.txt
@@ -0,0 +1,23 @@
+ 26 21 8
+1 0 0 0 1 1 0 0 0 0 0 0 0 1 1 0 1 0 1 1 0 0 0 0 0 0
+ 9 19 24
+ 17 21 26
+ 6 13 22
+ 3 17 25
+ 1 21 22
+ 6 8 10
+ 3 5 22
+ 4 16 19
+ 3 17 23
+ 3 7 14
+ 1 12 24
+ 4 20 26
+ 15 25 26
+ 6 13 16
+ 6 16 26
+ 1 3 22
+ 7 17 25
+ 7 20 22
+ 11 19 25
+ 2 5 24
+ 5 9 18
diff --git a/examples/data3sat/26bit/n26i9.txt b/examples/data3sat/26bit/n26i9.txt
new file mode 100644
index 000000000..8096a1d9c
--- /dev/null
+++ b/examples/data3sat/26bit/n26i9.txt
@@ -0,0 +1,24 @@
+ 26 22 9
+0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 1 0 1 1 1 0 1 0 0 1
+ 8 22 23
+ 3 19 24
+ 4 12 16
+ 6 21 22
+ 1 8 26
+ 8 17 25
+ 8 12 16
+ 4 10 17
+ 2 3 23
+ 5 16 22
+ 3 9 23
+ 14 15 17
+ 2 15 23
+ 9 11 19
+ 9 11 17
+ 5 20 22
+ 4 20 24
+ 1 13 14
+ 7 11 12
+ 8 10 26
+ 2 6 23
+ 8 13 18
diff --git a/examples/data3sat/28bit/n28i1.txt b/examples/data3sat/28bit/n28i1.txt
new file mode 100644
index 000000000..37ceec485
--- /dev/null
+++ b/examples/data3sat/28bit/n28i1.txt
@@ -0,0 +1,23 @@
+ 28 21 8
+1 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 1 0 1 0 0 0
+ 7 10 23
+ 9 15 20
+ 7 12 20
+ 2 24 26
+ 7 18 23
+ 2 20 22
+ 5 13 18
+ 4 6 12
+ 10 11 16
+ 15 17 20
+ 14 21 23
+ 11 19 22
+ 1 17 21
+ 21 25 28
+ 17 21 25
+ 19 20 23
+ 8 22 25
+ 15 16 27
+ 5 9 27
+ 5 18 27
+ 3 13 15
diff --git a/examples/data3sat/28bit/n28i10.txt b/examples/data3sat/28bit/n28i10.txt
new file mode 100644
index 000000000..029568071
--- /dev/null
+++ b/examples/data3sat/28bit/n28i10.txt
@@ -0,0 +1,25 @@
+ 28 23 10
+1 0 0 0 1 0 1 0 0 1 0 1 1 0 0 1 1 0 0 0 0 1 0 1 0 0 0 0
+ 2 13 28
+ 13 21 28
+ 2 21 22
+ 5 18 23
+ 22 25 27
+ 9 14 16
+ 3 11 12
+ 1 9 27
+ 8 10 20
+ 9 24 26
+ 11 13 26
+ 10 11 25
+ 1 6 9
+ 8 12 19
+ 16 21 23
+ 3 17 21
+ 3 5 8
+ 10 18 25
+ 17 18 28
+ 4 17 28
+ 7 9 25
+ 1 4 19
+ 5 6 15
diff --git a/examples/data3sat/28bit/n28i2.txt b/examples/data3sat/28bit/n28i2.txt
new file mode 100644
index 000000000..30ecdca2e
--- /dev/null
+++ b/examples/data3sat/28bit/n28i2.txt
@@ -0,0 +1,26 @@
+ 28 24 9
+0 0 0 1 0 0 1 0 1 0 0 0 0 1 0 0 1 0 1 0 0 1 1 0 0 1 0 0
+ 2 4 8
+ 1 9 12
+ 3 4 20
+ 2 15 23
+ 9 13 25
+ 3 14 21
+ 11 19 27
+ 5 19 20
+ 6 23 28
+ 8 17 28
+ 4 8 28
+ 6 14 18
+ 5 19 25
+ 1 14 18
+ 2 7 12
+ 5 10 17
+ 21 25 26
+ 3 17 27
+ 11 12 14
+ 16 18 26
+ 9 15 24
+ 4 16 20
+ 12 19 24
+ 18 22 27
diff --git a/examples/data3sat/28bit/n28i3.txt b/examples/data3sat/28bit/n28i3.txt
new file mode 100644
index 000000000..0a6229b31
--- /dev/null
+++ b/examples/data3sat/28bit/n28i3.txt
@@ -0,0 +1,25 @@
+ 28 23 9
+0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 1 1 1 0
+ 6 12 18
+ 15 17 21
+ 16 27 28
+ 5 9 12
+ 11 22 28
+ 8 20 27
+ 4 7 13
+ 3 5 23
+ 7 9 27
+ 3 18 20
+ 14 26 28
+ 3 22 24
+ 2 11 28
+ 6 15 17
+ 1 19 27
+ 8 14 25
+ 10 26 28
+ 2 13 14
+ 4 11 16
+ 1 27 28
+ 1 22 24
+ 9 22 23
+ 3 17 23
diff --git a/examples/data3sat/28bit/n28i4.txt b/examples/data3sat/28bit/n28i4.txt
new file mode 100644
index 000000000..b161c902c
--- /dev/null
+++ b/examples/data3sat/28bit/n28i4.txt
@@ -0,0 +1,25 @@
+ 28 23 10
+0 0 0 0 0 1 1 0 0 1 0 0 1 0 0 0 1 1 1 0 0 1 0 0 0 1 1 0
+ 20 25 26
+ 14 19 23
+ 5 9 22
+ 4 15 18
+ 12 17 21
+ 14 27 28
+ 12 26 28
+ 5 6 21
+ 5 14 26
+ 23 25 26
+ 5 19 25
+ 2 4 19
+ 1 8 26
+ 5 15 26
+ 2 10 12
+ 7 15 28
+ 17 20 24
+ 3 13 16
+ 2 11 17
+ 16 22 28
+ 12 13 15
+ 4 5 7
+ 14 21 26
diff --git a/examples/data3sat/28bit/n28i5.txt b/examples/data3sat/28bit/n28i5.txt
new file mode 100644
index 000000000..6c008b8b6
--- /dev/null
+++ b/examples/data3sat/28bit/n28i5.txt
@@ -0,0 +1,24 @@
+ 28 22 9
+1 0 0 0 0 1 0 0 0 1 1 0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 1 1
+ 14 22 24
+ 9 11 15
+ 1 3 7
+ 1 5 22
+ 1 4 14
+ 13 15 26
+ 6 19 26
+ 12 21 28
+ 10 15 16
+ 19 25 28
+ 3 10 14
+ 8 13 14
+ 11 23 25
+ 5 12 24
+ 1 4 25
+ 4 26 27
+ 3 11 15
+ 10 17 26
+ 7 22 24
+ 2 20 27
+ 5 6 25
+ 12 16 18
diff --git a/examples/data3sat/28bit/n28i6.txt b/examples/data3sat/28bit/n28i6.txt
new file mode 100644
index 000000000..238e99b70
--- /dev/null
+++ b/examples/data3sat/28bit/n28i6.txt
@@ -0,0 +1,26 @@
+ 28 24 9
+0 1 0 0 1 0 0 0 0 0 1 0 1 1 0 0 0 1 0 0 0 1 0 1 0 0 1 0
+ 15 23 24
+ 16 20 27
+ 17 26 27
+ 18 23 25
+ 15 19 22
+ 3 24 28
+ 12 18 23
+ 12 20 27
+ 12 15 27
+ 10 25 27
+ 3 5 23
+ 10 11 21
+ 3 4 22
+ 6 18 26
+ 10 11 20
+ 8 22 26
+ 2 20 23
+ 9 11 26
+ 5 8 9
+ 3 13 28
+ 6 22 23
+ 10 14 15
+ 7 16 18
+ 1 4 5
diff --git a/examples/data3sat/28bit/n28i7.txt b/examples/data3sat/28bit/n28i7.txt
new file mode 100644
index 000000000..8614da861
--- /dev/null
+++ b/examples/data3sat/28bit/n28i7.txt
@@ -0,0 +1,24 @@
+ 28 22 11
+1 0 1 0 1 0 0 1 0 0 0 1 0 0 0 1 1 1 0 0 1 0 0 1 0 0 1 0
+ 8 11 22
+ 13 19 21
+ 5 6 20
+ 7 17 23
+ 1 14 26
+ 13 18 22
+ 9 24 26
+ 5 20 25
+ 11 19 27
+ 15 21 22
+ 7 16 22
+ 25 27 28
+ 4 10 27
+ 1 2 10
+ 11 16 20
+ 3 11 20
+ 1 23 26
+ 4 13 24
+ 7 24 26
+ 11 21 25
+ 6 12 14
+ 5 20 23
diff --git a/examples/data3sat/28bit/n28i8.txt b/examples/data3sat/28bit/n28i8.txt
new file mode 100644
index 000000000..f5739dbdc
--- /dev/null
+++ b/examples/data3sat/28bit/n28i8.txt
@@ -0,0 +1,25 @@
+ 28 23 11
+1 1 0 0 0 1 0 0 1 0 0 1 0 0 0 1 1 0 0 1 0 1 0 1 1 0 0 0
+ 12 13 14
+ 8 20 27
+ 3 14 17
+ 9 14 26
+ 5 8 16
+ 18 23 25
+ 18 20 28
+ 22 26 27
+ 1 23 28
+ 10 11 16
+ 4 9 10
+ 10 18 25
+ 6 14 28
+ 9 11 19
+ 4 10 12
+ 4 19 22
+ 1 7 15
+ 17 18 28
+ 2 3 27
+ 19 24 27
+ 1 14 27
+ 2 7 28
+ 17 21 26
diff --git a/examples/data3sat/28bit/n28i9.txt b/examples/data3sat/28bit/n28i9.txt
new file mode 100644
index 000000000..145a1481f
--- /dev/null
+++ b/examples/data3sat/28bit/n28i9.txt
@@ -0,0 +1,24 @@
+ 28 22 10
+0 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 1 0 0 0 0 0 0 0 0 1 1
+ 7 12 23
+ 6 10 13
+ 12 20 24
+ 24 26 27
+ 3 16 20
+ 6 22 28
+ 3 7 15
+ 4 7 13
+ 1 5 12
+ 6 15 26
+ 7 9 27
+ 13 21 23
+ 8 13 19
+ 7 14 17
+ 14 25 26
+ 16 22 24
+ 2 17 21
+ 3 4 16
+ 5 18 26
+ 7 10 11
+ 2 4 6
+ 16 20 25
diff --git a/examples/data3sat/30bit/n30i1.txt b/examples/data3sat/30bit/n30i1.txt
new file mode 100644
index 000000000..14249b3d7
--- /dev/null
+++ b/examples/data3sat/30bit/n30i1.txt
@@ -0,0 +1,25 @@
+ 30 23 11
+0 0 0 1 1 0 1 1 1 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 0
+ 8 25 29
+ 1 10 17
+ 2 17 22
+ 3 13 21
+ 1 14 26
+ 9 24 29
+ 3 26 30
+ 3 24 26
+ 21 28 29
+ 7 22 25
+ 5 24 25
+ 2 3 27
+ 6 8 21
+ 1 8 23
+ 4 22 30
+ 10 17 18
+ 14 15 17
+ 15 22 26
+ 4 14 19
+ 3 28 29
+ 11 23 26
+ 12 24 29
+ 7 16 20
diff --git a/examples/data3sat/30bit/n30i10.txt b/examples/data3sat/30bit/n30i10.txt
new file mode 100644
index 000000000..2da849807
--- /dev/null
+++ b/examples/data3sat/30bit/n30i10.txt
@@ -0,0 +1,26 @@
+ 30 24 10
+0 1 0 0 0 1 0 1 1 0 0 0 0 1 1 1 0 0 0 0 0 1 0 1 0 1 0 0 0 0
+ 21 22 27
+ 11 12 15
+ 12 24 29
+ 6 11 30
+ 11 14 28
+ 17 21 24
+ 4 8 18
+ 7 8 12
+ 8 12 29
+ 5 10 14
+ 6 19 23
+ 2 13 27
+ 11 15 20
+ 1 15 19
+ 1 7 14
+ 9 20 27
+ 16 17 27
+ 3 15 27
+ 14 18 28
+ 19 23 26
+ 8 11 18
+ 6 25 29
+ 14 28 30
+ 2 4 7
diff --git a/examples/data3sat/30bit/n30i2.txt b/examples/data3sat/30bit/n30i2.txt
new file mode 100644
index 000000000..a02dc4216
--- /dev/null
+++ b/examples/data3sat/30bit/n30i2.txt
@@ -0,0 +1,27 @@
+ 30 25 10
+0 0 1 0 1 0 1 0 1 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 1 1 0 0
+ 5 19 22
+ 11 17 27
+ 5 10 26
+ 2 24 30
+ 19 23 24
+ 9 13 17
+ 20 26 30
+ 5 10 17
+ 9 18 26
+ 6 13 20
+ 12 18 21
+ 5 23 30
+ 12 23 29
+ 14 20 26
+ 14 16 29
+ 21 28 30
+ 4 16 21
+ 2 5 8
+ 1 9 13
+ 2 16 25
+ 10 14 27
+ 3 15 22
+ 12 21 29
+ 7 25 30
+ 7 15 19
diff --git a/examples/data3sat/30bit/n30i3.txt b/examples/data3sat/30bit/n30i3.txt
new file mode 100644
index 000000000..aa402ea28
--- /dev/null
+++ b/examples/data3sat/30bit/n30i3.txt
@@ -0,0 +1,24 @@
+ 30 22 11
+0 0 1 1 0 0 0 0 1 1 0 0 1 0 0 1 0 1 0 1 1 1 0 1 0 0 0 0 0 0
+ 6 20 27
+ 2 5 20
+ 1 9 28
+ 5 16 27
+ 2 10 12
+ 7 16 30
+ 19 21 25
+ 3 14 17
+ 2 11 18
+ 18 23 29
+ 12 14 16
+ 4 6 8
+ 1 8 20
+ 15 22 28
+ 8 13 23
+ 9 14 29
+ 15 16 29
+ 7 24 29
+ 9 11 12
+ 3 8 26
+ 17 22 28
+ 2 14 21
diff --git a/examples/data3sat/30bit/n30i4.txt b/examples/data3sat/30bit/n30i4.txt
new file mode 100644
index 000000000..eacda15d6
--- /dev/null
+++ b/examples/data3sat/30bit/n30i4.txt
@@ -0,0 +1,26 @@
+ 30 24 9
+0 0 0 1 0 1 0 0 1 1 0 1 0 0 1 0 0 0 0 1 0 1 0 0 0 0 0 1 0 0
+ 17 20 24
+ 5 6 13
+ 17 20 23
+ 5 11 20
+ 16 21 28
+ 7 11 28
+ 15 16 25
+ 25 27 28
+ 8 15 18
+ 5 12 16
+ 7 8 9
+ 1 12 26
+ 14 22 23
+ 2 6 29
+ 1 6 13
+ 1 3 4
+ 3 10 27
+ 7 22 23
+ 19 24 28
+ 12 13 18
+ 9 21 30
+ 4 21 24
+ 9 19 26
+ 3 6 16
diff --git a/examples/data3sat/30bit/n30i5.txt b/examples/data3sat/30bit/n30i5.txt
new file mode 100644
index 000000000..a92b268fa
--- /dev/null
+++ b/examples/data3sat/30bit/n30i5.txt
@@ -0,0 +1,26 @@
+ 30 24 11
+1 0 0 1 0 0 0 0 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 0 1 0 0
+ 10 13 19
+ 6 15 19
+ 5 22 26
+ 12 23 30
+ 12 23 25
+ 10 14 29
+ 7 9 27
+ 4 5 20
+ 13 29 30
+ 10 20 26
+ 3 4 18
+ 8 16 20
+ 10 28 29
+ 1 2 24
+ 2 17 21
+ 6 9 30
+ 2 12 18
+ 5 9 25
+ 16 21 25
+ 3 20 28
+ 6 13 23
+ 5 8 15
+ 1 6 25
+ 1 8 11
diff --git a/examples/data3sat/30bit/n30i6.txt b/examples/data3sat/30bit/n30i6.txt
new file mode 100644
index 000000000..1315d1b09
--- /dev/null
+++ b/examples/data3sat/30bit/n30i6.txt
@@ -0,0 +1,28 @@
+ 30 26 14
+0 1 1 1 0 1 1 0 0 1 1 1 0 1 0 0 0 0 0 0 1 1 1 0 1 0 0 0 1 0
+ 7 8 30
+ 1 25 30
+ 1 8 14
+ 1 23 30
+ 5 9 22
+ 3 5 16
+ 8 11 16
+ 8 23 24
+ 4 26 27
+ 9 15 25
+ 18 28 29
+ 9 17 23
+ 3 17 30
+ 4 15 16
+ 6 18 24
+ 2 15 30
+ 10 16 26
+ 13 24 25
+ 6 16 24
+ 8 12 24
+ 10 17 30
+ 4 8 28
+ 3 5 9
+ 4 18 19
+ 11 19 20
+ 17 18 21
diff --git a/examples/data3sat/30bit/n30i7.txt b/examples/data3sat/30bit/n30i7.txt
new file mode 100644
index 000000000..ca973eebd
--- /dev/null
+++ b/examples/data3sat/30bit/n30i7.txt
@@ -0,0 +1,28 @@
+ 30 26 13
+1 1 0 0 1 1 1 0 0 0 0 1 1 1 0 0 0 1 1 0 0 0 0 0 0 1 1 0 1 0
+ 18 20 30
+ 2 16 28
+ 22 26 30
+ 10 19 30
+ 8 16 18
+ 2 17 22
+ 7 11 15
+ 9 12 28
+ 9 21 27
+ 2 15 22
+ 6 15 16
+ 8 18 21
+ 7 10 28
+ 6 10 20
+ 8 24 27
+ 3 28 29
+ 13 21 22
+ 7 8 15
+ 3 10 18
+ 6 20 24
+ 7 11 25
+ 10 14 15
+ 4 10 29
+ 15 18 23
+ 5 9 11
+ 1 11 25
diff --git a/examples/data3sat/30bit/n30i8.txt b/examples/data3sat/30bit/n30i8.txt
new file mode 100644
index 000000000..e213ccaeb
--- /dev/null
+++ b/examples/data3sat/30bit/n30i8.txt
@@ -0,0 +1,26 @@
+ 30 24 13
+1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 1 1
+ 11 12 27
+ 2 8 10
+ 19 22 27
+ 7 14 17
+ 20 24 25
+ 17 23 24
+ 10 20 25
+ 9 18 25
+ 4 11 13
+ 3 14 23
+ 19 20 30
+ 4 7 21
+ 7 14 25
+ 4 10 19
+ 2 12 28
+ 6 7 23
+ 6 26 27
+ 20 22 30
+ 1 14 26
+ 11 18 22
+ 5 16 21
+ 3 16 23
+ 22 26 29
+ 15 16 19
diff --git a/examples/data3sat/30bit/n30i9.txt b/examples/data3sat/30bit/n30i9.txt
new file mode 100644
index 000000000..92ba406b0
--- /dev/null
+++ b/examples/data3sat/30bit/n30i9.txt
@@ -0,0 +1,28 @@
+ 30 26 11
+1 0 0 0 1 0 1 0 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 1 1 0 1
+ 11 21 22
+ 10 19 23
+ 3 13 28
+ 5 12 26
+ 13 17 25
+ 14 21 30
+ 7 18 21
+ 14 24 29
+ 3 15 27
+ 5 23 25
+ 6 9 13
+ 7 19 25
+ 2 22 24
+ 2 11 19
+ 2 22 30
+ 21 23 27
+ 19 20 24
+ 4 7 20
+ 16 28 29
+ 7 21 25
+ 6 26 30
+ 15 23 27
+ 15 17 22
+ 8 18 30
+ 1 6 22
+ 2 12 30
diff --git a/examples/data3sat/4bit/n4i1.txt b/examples/data3sat/4bit/n4i1.txt
new file mode 100644
index 000000000..1bd3c1490
--- /dev/null
+++ b/examples/data3sat/4bit/n4i1.txt
@@ -0,0 +1,5 @@
+ 4 3 1
+0 1 0 0
+ 1 2 3
+ 2 3 4
+ 1 2 4
diff --git a/examples/data3sat/8bit/n8i1.txt b/examples/data3sat/8bit/n8i1.txt
new file mode 100644
index 000000000..63349ac0a
--- /dev/null
+++ b/examples/data3sat/8bit/n8i1.txt
@@ -0,0 +1,8 @@
+ 8 6 3
+0 0 0 0 1 0 1 1
+ 3 6 8
+ 2 5 6
+ 1 2 8
+ 1 4 7
+ 3 4 7
+ 1 3 7
diff --git a/examples/data3sat/8bit/n8i10.txt b/examples/data3sat/8bit/n8i10.txt
new file mode 100644
index 000000000..56a0a8126
--- /dev/null
+++ b/examples/data3sat/8bit/n8i10.txt
@@ -0,0 +1,9 @@
+ 8 7 3
+0 0 0 1 0 0 1 1
+ 1 5 7
+ 2 3 4
+ 2 5 7
+ 2 4 5
+ 1 4 6
+ 2 3 8
+ 5 6 7
diff --git a/examples/data3sat/8bit/n8i2.txt b/examples/data3sat/8bit/n8i2.txt
new file mode 100644
index 000000000..5c2b770c0
--- /dev/null
+++ b/examples/data3sat/8bit/n8i2.txt
@@ -0,0 +1,9 @@
+ 8 7 3
+1 0 0 0 1 0 0 1
+ 2 5 7
+ 1 4 6
+ 1 2 4
+ 2 3 8
+ 3 5 7
+ 2 4 8
+ 1 4 7
diff --git a/examples/data3sat/8bit/n8i3.txt b/examples/data3sat/8bit/n8i3.txt
new file mode 100644
index 000000000..9c8f7f654
--- /dev/null
+++ b/examples/data3sat/8bit/n8i3.txt
@@ -0,0 +1,8 @@
+ 8 6 3
+1 1 1 0 0 0 0 0
+ 2 5 6
+ 2 7 8
+ 2 4 7
+ 1 6 7
+ 1 6 8
+ 3 4 7
diff --git a/examples/data3sat/8bit/n8i4.txt b/examples/data3sat/8bit/n8i4.txt
new file mode 100644
index 000000000..e34f04fa5
--- /dev/null
+++ b/examples/data3sat/8bit/n8i4.txt
@@ -0,0 +1,9 @@
+ 8 7 2
+0 0 0 0 1 0 0 1
+ 2 7 8
+ 4 7 8
+ 3 5 7
+ 2 3 5
+ 5 6 7
+ 1 4 8
+ 2 3 8
diff --git a/examples/data3sat/8bit/n8i5.txt b/examples/data3sat/8bit/n8i5.txt
new file mode 100644
index 000000000..ce37d7a01
--- /dev/null
+++ b/examples/data3sat/8bit/n8i5.txt
@@ -0,0 +1,9 @@
+ 8 7 4
+1 1 0 0 1 0 1 0
+ 1 4 6
+ 3 4 5
+ 3 6 7
+ 2 3 4
+ 4 7 8
+ 3 5 6
+ 3 7 8
diff --git a/examples/data3sat/8bit/n8i6.txt b/examples/data3sat/8bit/n8i6.txt
new file mode 100644
index 000000000..7063285a7
--- /dev/null
+++ b/examples/data3sat/8bit/n8i6.txt
@@ -0,0 +1,9 @@
+ 8 7 2
+0 0 0 1 0 0 1 0
+ 1 2 4
+ 1 3 7
+ 4 6 8
+ 1 6 7
+ 3 7 8
+ 2 3 7
+ 5 7 8
diff --git a/examples/data3sat/8bit/n8i7.txt b/examples/data3sat/8bit/n8i7.txt
new file mode 100644
index 000000000..02224a5ae
--- /dev/null
+++ b/examples/data3sat/8bit/n8i7.txt
@@ -0,0 +1,9 @@
+ 8 7 3
+1 0 0 1 1 0 0 0
+ 5 6 8
+ 1 3 8
+ 1 2 6
+ 2 5 6
+ 3 4 7
+ 1 6 7
+ 1 2 8
diff --git a/examples/data3sat/8bit/n8i8.txt b/examples/data3sat/8bit/n8i8.txt
new file mode 100644
index 000000000..b3d78c7ed
--- /dev/null
+++ b/examples/data3sat/8bit/n8i8.txt
@@ -0,0 +1,8 @@
+ 8 6 2
+1 0 0 0 0 0 1 0
+ 4 5 7
+ 1 2 5
+ 1 3 8
+ 1 4 6
+ 2 7 8
+ 1 6 8
diff --git a/examples/data3sat/8bit/n8i9.txt b/examples/data3sat/8bit/n8i9.txt
new file mode 100644
index 000000000..81c6b94b3
--- /dev/null
+++ b/examples/data3sat/8bit/n8i9.txt
@@ -0,0 +1,8 @@
+ 8 6 2
+0 0 0 1 0 1 0 0
+ 2 6 8
+ 3 5 6
+ 3 4 7
+ 1 4 5
+ 4 5 8
+ 4 5 7
diff --git a/examples/dbi/README.md b/examples/dbi/README.md
new file mode 100644
index 000000000..76640ff99
--- /dev/null
+++ b/examples/dbi/README.md
@@ -0,0 +1,58 @@
+# Double-bracket quantum algorithms
+
+Qibo features a model implementing double-bracke quantum algorithms (DBQAs) which are helpful for approximating eigenstates based on the ability to run the evolution under the input Hamiltonian.
+
+More specifically, given a Hamiltonian $H_0$, how can we find a circuit which after applying to the reference state (usually $|0\rangle^{\otimes L}$ for $L$ qubits) will approximate an eigenstate?
+
+A standard way is to run variational quantum circuits. For example, Qibo already features the `VQE` model [2] which provides the implementation of the variational quantum eigensolver framework.
+DBQAs allow to go beyond VQE in that they take a different approach to compiling the quantum circuit approximating the eigenstate.
+
+## What is the unitary of DBQA?
+
+Given $H_0$ we begin by assuming that we were given a diagonal and hermitian operator $D_0$ and a time $s_0$.
+The `dbi` module provides numerical strategies for selecting them.
+For any such choice we define the bracket
+$$ W_0 = [D_0, H_0]$$
+and the double-bracket rotation (DBR) of the input Hamiltonian to time $s$
+$$H_0(s) = e^{sW} H e^{- s W}$$
+
+### Why are double-bracket rotations useful?
+We can show that the magnitude of the off-diagonal norms will decrease.
+For this let us set the notation that $\sigma(A)$ is the restriction to the off-diagonal of the matrix A.
+In `numpy` this can be implemented by `\sigma(A) = A-np.diag(A)`. In Qibo we implement this as
+https://github.com/qiboteam/qibo/blob/8c9c610f5f2190b243dc9120a518a7612709bdbc/src/qibo/models/dbi/double_bracket.py#L145-L147
+which is part of the basic `DoubleBracketIteration` class in the `dbi` module.
+
+With this notation we next use the Hilbert-Schmidt scalar product and norm to measure the progress of diagonalization
+ $$||\sigma(H_0(s))||^2- ||\sigma (H_0 )||^2= -2s \langle W, [H,\sigma(H)\rangle+O(s^2)$$
+This equation tells us that as long as the scalar product $\langle W, [H,\sigma(H)\rangle$ is positive then after the DBR the magnitude of the off-diagonal couplings in $H_0(s)$ is less than in $H_0$.
+
+For the implementation of the DBR unitary $U_0(s) = e^{-s W_0}$ see
+https://github.com/qiboteam/qibo/blob/363a6e5e689e5b907a7602bd1cc8d9811c60ee69/src/qibo/models/dbi/double_bracket.py#L68
+
+### How to choose $D$?
+
+For theoretical considerations the canonical bracket is useful.
+For this we need the notation of the dephasing channel $\Delta(H)$ which is equivalent to `np.diag(h)`.
+ $M = [\Delta(H),\sigma(H)]= [H,\sigma(H)]= [\Delta(H),H]$
+ The canonical bracket appears on its own in the monotonicity relation above and gives an unconditional reduction of the magnitude of the off-diagonal terms
+ $$||\sigma(H_0(s))||^2- ||\sigma (H_0 )||^2= -2s ||M||^2+O(s^2)$$
+- the multi qubit Pauli Z generator with $Z(\mu) = (Z_1)^{\mu_1}\ldots (Z_L)^{\mu_L}$ where we optimize over all binary strings $\mu\in \{0,1\}^L$
+- the magnetic field $D = \sum_i B_i Z_i$
+- the two qubit Ising model $D = \sum_i B_i Z_i + \sum_{i,j} J_{i,j} Z_i Z_j$, please follow the tutorial by Matteo and use the QIBO ising model for that with $h=0$
+
+
+### How to choose s?
+
+The theory above shows that in generic cases the DBR will have a linear diagonalization effect (as quantified by $||\sigma(H_0(s))||$).
+This can be further expanded with Taylor expansion and the Qibo implementation comes with methods for fitting the first local minimum.
+Additionally a grid search for the optimal step is provided for an exhaustive evaluation and hyperopt can be used for a more efficient 'unstructured' optimization; additionally simulated annealing is provided which sometimes outperforms hyperopt (and grid search), see example notebooks.
+The latter methods may output DBR durations $s_k$ which correspond to secondary local minima.
+
+
+
+
+
+[1] https://arxiv.org/abs/2206.11772
+
+[2] https://github.com/qiboteam/vqe-sun
diff --git a/examples/dbi/dbi_tutorial_basic_intro.ipynb b/examples/dbi/dbi_tutorial_basic_intro.ipynb
new file mode 100644
index 000000000..425684b91
--- /dev/null
+++ b/examples/dbi/dbi_tutorial_basic_intro.ipynb
@@ -0,0 +1,777 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "2a33581d",
+ "metadata": {},
+ "source": [
+ "## Double-Bracket Iteration diagonalization algorithm\n",
+ "\n",
+ "In this example we present the `Qibo`'s implementation of the Double-Bracket Iteration (DBI) algorithm, which can be used to prepare the eigenstates of a quantum system. \n",
+ "\n",
+ "#### The initial setup\n",
+ "\n",
+ "At first we import some useful packages."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "62d9723f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# uncomment this line if seaborn is not installed\n",
+ "# !python -m pip install seaborn"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "b80b4738",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from copy import deepcopy\n",
+ "\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "import seaborn as sns\n",
+ "\n",
+ "from hyperopt import hp, tpe\n",
+ "\n",
+ "from qibo import hamiltonians, set_backend\n",
+ "from qibo.models.dbi.double_bracket import DoubleBracketGeneratorType, DoubleBracketIteration, DoubleBracketScheduling"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a5e25f51",
+ "metadata": {},
+ "source": [
+ "Here we define a simple plotting function useful to keep track of the diagonalization process."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "933d9a00",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def visualize_matrix(matrix, title=\"\"):\n",
+ " \"\"\"Visualize hamiltonian in a heatmap form.\"\"\"\n",
+ " fig, ax = plt.subplots(figsize=(5,5))\n",
+ " ax.set_title(title)\n",
+ " try:\n",
+ " im = ax.imshow(np.absolute(matrix), cmap=\"inferno\")\n",
+ " except TypeError:\n",
+ " im = ax.imshow(np.absolute(matrix.get()), cmap=\"inferno\")\n",
+ " fig.colorbar(im, ax=ax)\n",
+ "\n",
+ "def visualize_drift(h0, h):\n",
+ " \"\"\"Visualize drift of the evolved hamiltonian w.r.t. h0.\"\"\"\n",
+ " fig, ax = plt.subplots(figsize=(5,5))\n",
+ " ax.set_title(r\"Drift: $|\\hat{H}_0 - \\hat{H}_{\\ell}|$\")\n",
+ " try:\n",
+ " im = ax.imshow(np.absolute(h0 - h), cmap=\"inferno\")\n",
+ " except TypeError:\n",
+ " im = ax.imshow(np.absolute((h0 - h).get()), cmap=\"inferno\")\n",
+ "\n",
+ " fig.colorbar(im, ax=ax)\n",
+ "\n",
+ "def plot_histories(histories, labels):\n",
+ " \"\"\"Plot off-diagonal norm histories over a sequential evolution.\"\"\"\n",
+ " colors = sns.color_palette(\"inferno\", n_colors=len(histories)).as_hex()\n",
+ " plt.figure(figsize=(5,5*6/8))\n",
+ " for i, (h, l) in enumerate(zip(histories, labels)):\n",
+ " plt.plot(h, lw=2, color=colors[i], label=l, marker='.')\n",
+ " plt.legend()\n",
+ " plt.xlabel(\"Iterations\")\n",
+ " plt.ylabel(r\"$\\| \\sigma(\\hat{H}) \\|^2$\")\n",
+ " plt.title(\"Loss function histories\")\n",
+ " plt.grid(True)\n",
+ " plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4efd4a97",
+ "metadata": {},
+ "source": [
+ "We need to define a target hamiltonian which we aim to diagonalize. As an example, we consider the Transverse Field Ising Model (TFIM):\n",
+ "$$ H_{\\rm TFIM} = - \\sum_{q=0}^{N}\\bigl( Z_i Z_{i+1} + h X_i \\bigr),$$\n",
+ "which is already implemented in `Qibo`. For this tutorial we set $N=6$ and $h=3$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "7125940f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.2.9|INFO|2024-07-03 11:29:11]: Using numpy backend on /CPU:0\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# set the qibo backend (we suggest qibojit if N >= 20)\n",
+ "set_backend(\"numpy\")\n",
+ "\n",
+ "# hamiltonian parameters\n",
+ "nqubits = 5\n",
+ "h = 3\n",
+ "\n",
+ "# define the hamiltonian\n",
+ "h = hamiltonians.TFIM(nqubits=nqubits, h=h)\n",
+ "\n",
+ "# vosualize the matrix\n",
+ "visualize_matrix(h.matrix, title=\"Target hamiltonian\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c2ca8392",
+ "metadata": {},
+ "source": [
+ "#### The generator of the evolution\n",
+ "\n",
+ "The model is implemented following the procedure presented in [1], and the first practical step is to define the generator of the iteration $\\hat{\\mathcal{U}}_{\\ell}$, which executes one diagonalization step $$\\hat{H}_{\\ell} = \\hat{\\mathcal{U}}_{\\ell}^{\\dagger} \\hat{H} \\hat{\\mathcal{U}}_{\\ell}.$$\n",
+ "In `Qibo`, we define the iteration type through a `DoubleBracketGeneratorType` object, which can be chosen between one of the following:\n",
+ "- `canonical`: the generator of the iteration at step $k+1$ is defined using the commutator between the off diagonal part $\\sigma(\\hat{H_k})$ and the diagonal part $\\Delta(\\hat{H}_k)$ of the target evolved hamiltonian:\n",
+ " $$\\hat{\\mathcal{U}}_{k+1}=\\exp\\bigl\\{s[\\Delta(\\hat{H}_k), \\sigma(\\hat{H}_k)]\\bigr\\}.$$ \n",
+ "- `single_commutator`: the evolution follows a similar procedure of the previous point in this list, but any additional matrix $D_k$ can be used to control the evolution at each step:\n",
+ " $$ \\hat{\\mathcal{U}}_{k+1}=\\exp\\bigl\\{s[D_k, \\hat{H}_k]\\bigr\\}. $$\n",
+ "- `group_commutator`: the following group commutator is used to compute the evolution:\n",
+ " $$ \\hat{\\mathcal{U}}_{k+1}= e^{is\\hat{H_k}} e^{isD_k} e^{-is\\hat{H_k}} e^{-isD_k}, $$\n",
+ "which approximates the canonical commutator for small $s$.\n",
+ "\n",
+ "In order to set one of this evolution generators one can do as follow:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "1adafc19",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "DoubleBracketGeneratorType.canonical\n",
+ "DoubleBracketGeneratorType.single_commutator\n",
+ "DoubleBracketGeneratorType.group_commutator\n",
+ "DoubleBracketGeneratorType.group_commutator_third_order\n"
+ ]
+ }
+ ],
+ "source": [
+ "# we have a look inside the DoubleBracketGeneratorType class\n",
+ "for generator in DoubleBracketGeneratorType:\n",
+ " print(generator)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "8a4d0e9d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# here we set the canonical generator\n",
+ "iterationtype = DoubleBracketGeneratorType.canonical"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a5527622",
+ "metadata": {},
+ "source": [
+ "#### The `DoubleBracketIteration` class\n",
+ "\n",
+ "A `DoubleBracketIteration` object can be initialize by calling the `qibo.models.double_braket.DoubleBracketIteration` model and passing the target hamiltonian and the generator type we want to use to perform the evolutionary steps."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "9521c464",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dbf = DoubleBracketIteration(hamiltonian=deepcopy(h), mode=iterationtype)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a262c69f",
+ "metadata": {},
+ "source": [
+ "#### `DoubleBracketIteration` features"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "290e5828",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Backend: numpy\n"
+ ]
+ }
+ ],
+ "source": [
+ "# on which qibo backend am I running the algorithm?\n",
+ "print(f\"Backend: {dbf.backend}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "3e2b9950",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Initial form of the target hamiltonian:\n",
+ "[[-5.-0.j -3.-0.j -3.-0.j ... -0.-0.j -0.-0.j -0.-0.j]\n",
+ " [-3.-0.j -1.-0.j -0.-0.j ... -0.-0.j -0.-0.j -0.-0.j]\n",
+ " [-3.-0.j -0.-0.j -1.-0.j ... -0.-0.j -0.-0.j -0.-0.j]\n",
+ " ...\n",
+ " [-0.-0.j -0.-0.j -0.-0.j ... -1.-0.j -0.-0.j -3.-0.j]\n",
+ " [-0.-0.j -0.-0.j -0.-0.j ... -0.-0.j -1.-0.j -3.-0.j]\n",
+ " [-0.-0.j -0.-0.j -0.-0.j ... -3.-0.j -3.-0.j -5.-0.j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# the initial target hamiltonian is a qibo hamiltonian\n",
+ "# thus the matrix can be accessed typing h.matrix\n",
+ "print(f\"Initial form of the target hamiltonian:\\n{dbf.h0.matrix}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "638ba4b5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# let's visualize it in a more graphical way\n",
+ "visualize_matrix(dbf.h0.matrix, r\"$H_0$\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "08f0c466",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# since we didn't perform yet any evolutionary step they are the same\n",
+ "visualize_drift(dbf.h0.matrix, dbf.h.matrix)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bb5f10da",
+ "metadata": {},
+ "source": [
+ "which shows $\\hat{H}$ is now identical to $\\hat{H}_0$ since no evolution step has been performed yet."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "90e6fdff",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# diagonal part of the H target\n",
+ "visualize_matrix(dbf.diagonal_h_matrix)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a0101ae0",
+ "metadata": {},
+ "source": [
+ "The Hilbert-Schmidt norm of a Hamiltonian is defined as:\n",
+ "\n",
+ "$\\langle A\\rangle_{HS}=\\sqrt{A^\\dagger A}$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "0d90c8b5",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "HS norm of the off diagonal part of H: 37.94733192202055\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Hilbert-Schmidt norm of the off-diagonal part\n",
+ "# which we want to bring to be close to zero\n",
+ "print(f\"HS norm of the off diagonal part of H: {dbf.off_diagonal_norm}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a1d1eb77",
+ "metadata": {},
+ "source": [
+ "Finally, the energy fluctuation of the system at step $k$ over a given state $\\mu$\n",
+ "\n",
+ "$$ \\Xi(\\mu) = \\sqrt{\\langle \\mu | \\hat{H}_k^2 | \\mu \\rangle - \\langle \\mu | \\hat{H}_k | \\mu \\rangle^2} $$\n",
+ "\n",
+ "can be computed:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "13710cc2",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "6.708203932499369"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# define a quantum state\n",
+ "# for example the ground state of a multi-qubit Z hamiltonian\n",
+ "Z = hamiltonians.Z(nqubits=nqubits)\n",
+ "state = Z.ground_state()\n",
+ "\n",
+ "# compute energy fluctuations using current H and given state\n",
+ "dbf.energy_fluctuation(state)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4d34e1e3",
+ "metadata": {},
+ "source": [
+ "#### Call the `DoubleBracketIteration` to perform a DBF iteration\n",
+ "\n",
+ "If the DBF object is called, a Double Bracket Iteration iteration is performed. This can be done customizing the iteration by setting the iteration step and the desired `DoubleBracketGeneratorType`. If no generator is provided, the one passed at the initialization time is used (default is `DoubleBracketGeneratorType.canonical`)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "a7749a96",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Initial value of the off-diagonal norm: 37.94733192202055\n",
+ "One step later off-diagonal norm: 34.179717587686405\n"
+ ]
+ }
+ ],
+ "source": [
+ "# perform one evolution step\n",
+ "\n",
+ "# initial value of the off-diagonal norm\n",
+ "print(f\"Initial value of the off-diagonal norm: {dbf.off_diagonal_norm}\")\n",
+ "\n",
+ "dbf(step=0.01, mode=iterationtype)\n",
+ "\n",
+ "# after one step\n",
+ "print(f\"One step later off-diagonal norm: {dbf.off_diagonal_norm}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dab441bb",
+ "metadata": {},
+ "source": [
+ "We can check now if something happened by plotting the drift:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "fc01baa4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize_drift(dbf.h0.matrix, dbf.h.matrix)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9223433b",
+ "metadata": {},
+ "source": [
+ "The set step can be good, but maybe not the best one. In order to do this choice in a wiser way, we can call the DBF hyperoptimization routine to search for a better initial step. The `dbf.hyperopt_step` method is built on top of the [`hyperopt`](https://hyperopt.github.io/hyperopt/) package. Any algorithm or sampling space provided by the official package can be used. We are going to use the default options (we sample new steps from a uniform space following a _Tree of Parzen estimators algorithm_)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "0d7b86d3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# restart\n",
+ "dbf.h = dbf.h0\n",
+ "\n",
+ "# optimization of the step, we allow to search in [1e-5, 1]\n",
+ "step = dbf.choose_step(\n",
+ " scheduling=DoubleBracketScheduling.hyperopt,\n",
+ " step_min = 1e-5,\n",
+ " step_max = 1,\n",
+ " space = hp.uniform,\n",
+ " optimizer = tpe,\n",
+ " max_evals = 1000,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "1b9b1431",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize_matrix(dbf.h.matrix)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "52fa3599",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize_drift(dbf.h0.matrix, dbf.h.matrix)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "084c3bcb",
+ "metadata": {},
+ "source": [
+ "#### Let's evolve the model for `NSTEPS`\n",
+ "\n",
+ "We know recover the initial hamiltonian, and we perform a sequence of DBF iteration steps, in order to show how this mechanism can lead to a proper diagonalization of the target hamiltonian.\n",
+ "\n",
+ "#### Method 1: fixed step"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "d1f197b1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# restart\n",
+ "dbf_1 = DoubleBracketIteration(hamiltonian=deepcopy(h), mode=iterationtype)\n",
+ "off_diagonal_norm_history = [dbf_1.off_diagonal_norm]\n",
+ "histories, labels = [], [\"Fixed step\"]\n",
+ "\n",
+ "# set the number of evolution steps\n",
+ "NSTEPS = 20\n",
+ "step = 0.005\n",
+ "\n",
+ "for s in range(NSTEPS):\n",
+ " dbf_1(step=step)\n",
+ " off_diagonal_norm_history.append(dbf_1.off_diagonal_norm)\n",
+ "\n",
+ "histories.append(off_diagonal_norm_history)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "c115c222",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdsAAAF2CAYAAAAm+DIEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABcg0lEQVR4nO3deVhUdfvH8few76AIAoKCiGviQmqkqYngUopttqtlqbmUT7s9mUtu2fLTeoysTC2jRUszSw0XMBVNUXONlHCLTVH2bWDO7w9icmSHGWaA+3VdczVz5nu+53OOp7k5u0pRFAUhhBBCGIyZsQMIIYQQTZ0UWyGEEMLApNgKIYQQBibFVgghhDAwKbZCCCGEgUmxFUIIIQxMiq0QQghhYFJshRBCCAOTYiuEEEIYmBRbIYxk27Zt9OzZExsbG1QqFRkZGcaOVCGVSsXcuXMbfLoTJkzAwcGhRm2NlbEy58+fR6VSsWbNGmNHESZCiq1oUtasWYNKpeLw4cPGjlKl9PR0xo4di62tLStWrOCLL77A3t7eaHl+/vlnkypWDSEyMpJly5YZO4ZoJiyMHUCI5ujQoUNkZ2fz5ptvMnToUGPH4eeff2bFihUVFtz8/HwsLEz7p6IuGSMjIzl58iQzZ87Ue5527dqRn5+PpaWl3vsWjZNp/x8kRBOVlpYGgIuLi3GD1ICNjY2xI1TLVDIWFxej0WiwsrIymUzCNMhuZNEsHT16lBEjRuDk5ISDgwMhISEcOHBAp41arWbevHkEBARgY2ODq6srAwYMICoqStsmJSWFJ554Am9vb6ytrfH09CQ8PJzz589XOu3Bgwczfvx4APr06YNKpWLChAkA+Pr6at/fPM7gwYO1n6Ojo1GpVHz77bcsXLgQb29vbGxsCAkJ4dy5c+XGP3jwICNHjqRFixbY29sTGBjI8uXLgdJjoytWrABKj32WvcpUdDy0JsuvbJf+vn37eP7553Fzc8Pe3p577rmHK1euVLp8bvb3338zZswYHBwccHNz48UXX6SkpESnzc0Zs7OzmTlzJr6+vlhbW+Pu7k5oaChHjhzRLs+ffvqJCxcuaOfX19dXO35aWhoTJ06kdevW2NjY0KNHD9auXaszzbLjsu+88w7Lli3D398fa2trTp8+Xekx2z/++IP777+fli1bYmNjw6233srmzZt12tRkvRONj2zZimbn1KlT3HHHHTg5OfHyyy9jaWnJypUrGTx4MDExMfTr1w+AuXPnsnjxYp566in69u1LVlYWhw8f5siRI4SGhgJw3333cerUKWbMmIGvry9paWlERUVx8eJFnR/vG/33v/+lU6dOfPzxx8yfPx8/Pz/8/f3rNC9LlizBzMyMF198kczMTJYuXcqjjz7KwYMHtW2ioqK4++678fT05LnnnsPDw4MzZ86wZcsWnnvuOSZPnkxSUhJRUVF88cUXelt+ZWbMmEGLFi2YM2cO58+fZ9myZUyfPp1vvvmm2mmVlJQwbNgw+vXrxzvvvMOOHTt499138ff355lnnql0vClTprBhwwamT59O165dSU9PZ+/evZw5c4bevXvz3//+l8zMTC5fvsz//d//AWhPxsrPz2fw4MGcO3eO6dOn4+fnx/r165kwYQIZGRk899xzOtNavXo1BQUFTJo0CWtra1q2bIlGo6lwufXv3582bdrw6quvYm9vz7fffsuYMWP47rvvuOeee4CarXeiEVKEaEJWr16tAMqhQ4cqbTNmzBjFyspKSUhI0A5LSkpSHB0dlYEDB2qH9ejRQ7nrrrsq7ef69esKoLz99tt6y9muXTtl/Pjx5doPGjRIGTRokPbz7t27FUDp0qWLUlhYqB2+fPlyBVBOnDihKIqiFBcXK35+fkq7du2U69ev6/Sp0Wi076dNm6ZU9nMAKHPmzNF+runyK5vHoUOH6kzrP//5j2Jubq5kZGRUOL0y48ePVwBl/vz5OsN79eqlBAUFVZnR2dlZmTZtWpX933XXXUq7du3KDV+2bJkCKOvWrdMOKyoqUoKDgxUHBwclKytLURRFSUxMVADFyclJSUtL0+mj7LvVq1drh4WEhCjdu3dXCgoKtMM0Go1y++23KwEBAdph1a13onGS3ciiWSkpKeGXX35hzJgxtG/fXjvc09OTRx55hL1795KVlQWUHk89deoUZ8+erbAvW1tbrKysiI6O5vr16w2S/2ZPPPEEVlZW2s933HEHAH/99RdQurs3MTGRmTNnljs+fOOu4pqqzfIrM2nSJJ1p3XHHHZSUlHDhwoUaTXPKlCk6n++44w7t/FXGxcWFgwcPkpSUVKNp3Ojnn3/Gw8ODhx9+WDvM0tKSZ599lpycHGJiYnTa33fffbi5uVXZ57Vr19i1axdjx44lOzubq1evcvXqVdLT0xk2bBhnz57l77//1mavar0TjZMUW9GsXLlyhby8PDp16lTuuy5duqDRaLh06RIA8+fPJyMjg44dO9K9e3deeukljh8/rm1vbW3NW2+9xdatW2ndujUDBw5k6dKlpKSkNNj8tG3bVudzixYtALTFPyEhAYBbbrlFL9OrzfKracaq2NjYlCtkLVq0qHbcpUuXcvLkSXx8fOjbty9z586ttkCXuXDhAgEBAZiZ6f48dunSRfv9jfz8/Krt89y5cyiKwuzZs3Fzc9N5zZkzB/j3pLnq1jvROEmxFaISAwcOJCEhgc8++4xbbrmFTz/9lN69e/Ppp59q28ycOZM///yTxYsXY2Njw+zZs+nSpQtHjx6t0zQr29q8+YSgMubm5hUOVxSlTtM3hPpkrGzc6owdO5a//vqLDz74AC8vL95++226devG1q1b69RfVWxtbattU3YM98UXXyQqKqrCV4cOHYCarXei8ZFiK5oVNzc37OzsiI+PL/fdH3/8gZmZGT4+PtphLVu25IknnuCrr77i0qVLBAYGljsz19/fnxdeeIFffvmFkydPUlRUxLvvvlunfC1atKjwTlI13eV6s7ITr06ePFllu5ruUq7t8jMmT09Ppk6dyqZNm0hMTMTV1ZWFCxdqv69sntu1a8fZs2fLneT0xx9/aL+vrbJd7paWlgwdOrTCl6Ojo7Z9TdY70bhIsRXNirm5OWFhYfzwww86l+ekpqYSGRnJgAEDcHJyAkrv8nQjBwcHOnToQGFhIQB5eXkUFBTotPH398fR0VHbprb8/f05cOAARUVF2mFbtmwpt2u2pnr37o2fnx/Lli0rV8Rv3LIsu3tVdbeMrM3yM5aSkhIyMzN1hrm7u+Pl5aXz72Jvb1+uHcDIkSNJSUnROVu6uLiYDz74AAcHBwYNGlTrTO7u7gwePJiVK1eSnJxc7vsbL4Wqbr0TjZNc+iOapM8++4xt27aVG/7cc8+xYMECoqKiGDBgAFOnTsXCwoKVK1dSWFjI0qVLtW27du3K4MGDCQoKomXLlhw+fFh7OQnAn3/+SUhICGPHjqVr165YWFiwceNGUlNTeeihh+qU+6mnnmLDhg0MHz6csWPHkpCQwLp16+p8aZCZmRkRERGMGjWKnj178sQTT+Dp6ckff/zBqVOn2L59OwBBQUEAPPvsswwbNgxzc/NK56Gmy89YsrOz8fb25v7776dHjx44ODiwY8cODh06pLPHISgoiG+++Ybnn3+ePn364ODgwKhRo5g0aRIrV65kwoQJxMXF4evry4YNG9i3bx/Lli3T2QKtjRUrVjBgwAC6d+/O008/Tfv27UlNTSU2NpbLly/z+++/A9Wvd6KRMu7J0ELoV9nlJpW9Ll26pCiKohw5ckQZNmyY4uDgoNjZ2Sl33nmnsn//fp2+FixYoPTt21dxcXFRbG1tlc6dOysLFy5UioqKFEVRlKtXryrTpk1TOnfurNjb2yvOzs5Kv379lG+//bbGOSu6ROndd99V2rRpo1hbWyv9+/dXDh8+XOmlP+vXr9cZt6JLThRFUfbu3auEhoYqjo6Oir29vRIYGKh88MEH2u+Li4uVGTNmKG5ubopKpdK5DIibLqup6fKrbB7Lsu/evbvKZTR+/HjF3t6+3PA5c+aUu0zpxoyFhYXKSy+9pPTo0UM7vz169FA+/PBDnXFycnKURx55RHFxcVEAncuAUlNTlSeeeEJp1aqVYmVlpXTv3r3cMi1b1hVd+lXZv0NCQoIybtw4xcPDQ7G0tFTatGmj3H333cqGDRu0bapb70TjpFIUEzqTQgghhGiC5JitEEIIYWBSbIUQQggDk2IrhBBCGJgUWyGEEMLApNgKIYQQBibFVgghhDAwualFHWg0GpKSknB0dKzTk1OEEEI0DYqikJ2djZeXV7mHV9xIim0dJCUlmcz9X4UQQhjfpUuX8Pb2rvR7KbZ1UHa7tkuXLtXrPrBqtZpffvmFsLAwLC0t9RXPYCSvYUlew5K8htVc82ZlZeHj41PtbTyl2NZB2a5jJyenehdbOzs7nJycGs3KKXkNR/IaluQ1rOaet7pDinKClBBCCGFgUmyFEEIIA5NiK4QQQhiYHLMVQgg9KSkpQa1W66UvtVqNhYUFBQUFlJSU6KVPQ2qqeS0tLTE3N6/39KTYCiFEPSmKQkpKChkZGXrt08PDg0uXLjWK6/mbcl4XFxc8PDzqNV9SbIUQop7KCq27uzt2dnZ6KTYajYacnBwcHByqvFmCqWiKeRVFIS8vj7S0NAA8PT3rPD0ptkIIUQ8lJSXaQuvq6qq3fjUaDUVFRdjY2DSa4tUU89ra2gKQlpaGu7t7nXcpm/4SacIK09Ixv5BCYVq6saMIIeqo7BitnZ2dkZMIQyn7t63P8Xgptkby9w87OHj/s9hG7uDgA8+StHmnsSMJIeqhMRynFHWjj39bKbZGkHspmT8WrwRFKR2gUTiz5GMKZAtXCCGaJCm2RlDhbmONhvxLKQ0fRgghKjB48GBmzpxp0GnMnTuXnj17GnQapkKKrRHY+XjCzbslVCpsfTyME0gI0SxNmDABlUpV7nXu3Dm+//573nzzTWNHrNT58+dRqVQcO3bM2FFqRIqtEdi4u9Jl1mSdgmtuZ4O5jbURUwkhmqPhw4eTnJys8/Lz86Nly5bVPslG1JxJF9uIiAgCAwO1T9cJDg5m69atwL9/1VT0Wr9+faV9VvSX3PDhwxtqlrS8RofQd/1yStq0AqAkN5+z73/e4DmEEKalIC2da3EnG+wcDmtrazw8PHRe5ubmOruR//jjD+zs7IiMjNSO9+2332Jra8vp06cByMjI4Nlnn6V169Y4OTkxZMgQfv/9d51pLVmyhNatW+Po6MjEiRMpKCioMtv169d59NFHcXNzw9bWloCAAFavXg2An58fAL169UKlUjF48GDteJ9++ildunTBxsaGzp078+GHH2q/K6sdX3/9NWFhYdjZ2XHLLbcQExNT52VYEyZdbL29vVmyZAlxcXEcPnyYIUOGEB4ezqlTp/Dx8Sn319i8efNwcHBgxIgRVfZ7819yX331VQPNkS6b1q0ouGcg5val13Elb9lN+sHfqxlLCNFUJW3eyb4xz3B02jxi753G1W2/GjsSAJ07d+add95h6tSpXLx4kcuXLzNlyhTeeustunbtCsDYsWO5cuUKP/30E3FxcfTu3ZuQkBCuXbsGlBbnuXPnsmjRIg4fPoynp6dOEazI7NmzOX36NFu3buXMmTNERETQqlXpBspvv/0GwI4dO0hOTub7778H4Msvv+SNN95g4cKFnDlzhkWLFjF79mzWrl2r0/crr7zC9OnTiYuLIzg4mFGjRpGebrg/cEz6phajRo3S+bxw4UIiIiI4cOAA3bp1w8ND9xjnxo0bGTt2LA4ODlX2W/aXnClQHO1oP/VRzr79KQB/LF5Jv8h3sbCzNXIyIUR9/DbhFYrSM2rcXinRUHTthvYahQvLPif5882ozGu+XWTl6kLfNW/VuP2WLVt0fjNHjBhR4d7BqVOn8vPPP/PYY49hZWVFnz59mDFjBgB79+7l0KFD/Pnnn7i5uWFmZsY777zDpk2b2LBhA5MmTWLZsmVMnDiRiRMnArBgwQJ27NhR5dbtxYsX6dWrF7feeisAvr6+2u/c3NwAcHV11fk9nzNnDu+++y733nsvULoFfPr0aVauXMn48eO17aZNm8bo0aNxcnIiIiKCbdu2sWrVKl5++eUaL7vaMOlie6OSkhLWr19Pbm4uwcHB5b6Pi4vj2LFjrFixotq+oqOjcXd3p0WLFgwZMoQFCxZUeeeXwsJCCgsLtZ+zsrKA0guc63ORc9m4rsMHkLZzP5lHTlOQcoWzK76kw8zx1Yzd8Mry6utG64YmeQ1L8v7br6IoaDQaNBqNdnhRegaFV67Vu3+dAlxDN+aoiqIoDB48WGcL097eXjt+2XyV+fTTT+ncuTNmZmacOHECRVFQFIVjx46Rk5ODv7+/Tv/5+fmcO3cOjUbDmTNnmDRpkk5/t912G9HR0ZXmnTx5Mg888ABHjhwhNDSU8PBwbr/9dp15vHG55+bmkpCQwMSJE3n66ae1/RQXF+Ps7KzTtl+/ftp5NDMzIygoiNOnT1eYRaPRoCgKarW63B2karo+mXyxPXHiBMHBwRQUFODg4MDGjRu1uy1utGrVKrp06aL9h6jM8OHDuffee/Hz8yMhIYHXXnuNESNGEBsbW+ltuBYvXsy8efPKDf/ll1/0cteYHTt2oOobgN3xeFTFJfz93XbO2SlovN3r3bchREVFGTtCrUhew2rueS0sLPDw8CAnJ4eioiLtcHMXRyyVmhU9KN2yLb6eVb7/Fk612rI1d3HUbhBUR61WY21tjbu77m9NVlYWxcXFFBUV6fQVGxtLbm4uZmZmnDt3Dnt7ewCuXr2Kh4cHP/74Y7lpODs7k5WVhaIoFBQU6PRXVFRESUlJpXn79+/P8ePHiYqKYvfu3YSGhvLUU0/x5ptvkpOTA5QW2LLxy+5hvGzZMu3WsHa5mJuTlZWlHS8vLw+A7OxsoLQgq9XqCrMUFRWRn5/Pnj17KC4u1vmurJ/qmHyx7dSpE8eOHSMzM5MNGzYwfvx4YmJidApufn4+kZGRzJ49u9r+HnroIe377t27ExgYiL+/P9HR0YSEhFQ4zqxZs3j++ee1n7OysvDx8SEsLAwnJ6c6z5tarSYqKorQ0FAsLS25bO7AXyu+RAW47jlJ0KpFmFlb1bl/fbs5r6mTvIYleUsVFBRw6dIlHBwcsLGx0Q7vt3ZprftK+nEX8W99AhoNmJnR7tnH8HtgpMHuTmVpaYmFhUWFv2MWFhZYWVlpv7t27RrTp0/ntddeIzk5mSlTpnD48GFsbW0JDg5mwYIFWFhY0K1btwrzdu3alePHjzNp0iTtsKNHj2Jubl7l76iTkxOTJ09m8uTJrFy5kldeeYXly5fTsmVLAGxsbLTjOzk54eXlRUpKSqXX75btMj9x4gT9+/fH0dGRkpISjh8/zrRp0yrMUlBQgK2tLQMHDtT5NwZq/IeNyRdbKysrOnToAEBQUBCHDh1i+fLlrFy5Uttmw4YN5OXlMW7cuFr33759e1q1asW5c+cqLbbW1tZYW5e/LMfS0lIv/9OW9eP7yCiu7j5I1ulz5F9I4vIXP+D/zCP17l/f9DXfDUXyGlZzz1tSUoJKpcLMzKzeN+D3Dh9Kq+Be5F9KwbqNO0U2ltq+DaHsiozK+r/xu6lTp+Lj48Ps2bMpLCykV69evPzyy6xYsYKwsDCCg4N59NFHefvtt+ncuTNJSUn89NNP3HPPPdx6660899xzTJgwgT59+tC/f3++/PJLTp06Rfv27Sud/htvvEFQUBDdunWjsLCQn3/+mS5dumBmZoaHhwe2trb88ssvtG3bFhsbG5ydnZk3bx7PPvssLi4uDB8+nMLCQg4fPsz169d5/vnntdOKiIjA29ub3r17s3z5cq5fv87EiRMrzGJmZoZKpapw3anpumTSZyNXRKPR6Bw/hdJdyKNHj9YeMK+Ny5cvk56eXq9HJ+mLytycLv99BpVF6e7sC+t+IPvPRCOnEkI0JBt3V1oEdcPGXX9PEKqvzz//nJ9//pkvvvgCCwsL7O3tWbduHZ988glbt25FpVKxZcsWbr/9diZOnEjHjh156KGHuHDhAq1btwbgwQcfZPbs2bz88ssEBQVx4cIFnnnmmSqna2VlxaxZswgMDGTgwIGYm5vz9ddfA6Vb3u+//z4rV67Ey8uL8PBwAJ566ik+/fRTVq9eTffu3Rk0aBBr1qzRXipUZtGiRSxbtoxevXqxd+9eNm/erD3T2SAUE/bqq68qMTExSmJionL8+HHl1VdfVVQqlfLLL79o25w9e1ZRqVTK1q1bK+yjU6dOyvfff68oiqJkZ2crL774ohIbG6skJiYqO3bsUHr37q0EBAQoBQUFNc6VmZmpAEpmZma95q+oqEjZtGmTUlRUpDM84ZNvlR397ld29LtfOfj4S0qJurhe09GXyvKaKslrWJK3VH5+vnL69GklPz9fr/2WlJQo169fV0pKSvTar6E0lryJiYkKoMTFxdU4b1X/xjWtBya9ZZuWlsa4cePo1KkTISEhHDp0iO3btxMaGqpt89lnn+Ht7U1YWFiFfcTHx5OZmQmUHiA/fvw4o0ePpmPHjkycOJGgoCB+/fXXCncTG4vv+DHY+/sAkP1nIhcjNxs5kRBCiPow6WO2q1atqrbNokWLWLRoUaXfK2VP1qH0IcDbt2/XSzZDMrO0pMtrz3D46f+CRiHx0/W4DeqLfbs2xo4mhBCiDkx6y7Y5c+4WQNuH7gZAU6TmzKKPUGp47ZwQQoiK+fr6oihKgz9tSIqtCWs/6UFsvUtPLsj8/Q8uf/+LkRMJIYSoCym2JszcxprOs6ZoPyd8+CX5yVeMmEgIUZkbD1mJpkUf/7ZSbE1cy6Bb8BozFICSvAL+eGul/E8thAkpu86ypncSEo1P2b9tfa7PNukTpESpgOmPkb7vCIVXrnHtwO+kbN2D58hBxo4lhKD0KgcXFxftrQLt7Oz0cscnjUZDUVERBQUFBruphT41xbyKopCXl0daWhouLi6V3tK3JqTYNgIWDvZ0evlpjr9U+iSPP5etpmW/Hli7uhg3mBACQPvUmbKCqw+KopCfn4+tra3BbteoT005r4uLS72fFCfFtpFwu+NWWof1J/WXfRRn5fLnu6vovugFY8cSQlB6W0NPT0/c3d319lQhtVrNnj17GDhwYKO4HWZTzWtpaVmvLdoyUmwbkY7/eYJrvx1HnZFN2q4DpO0+iPud/YwdSwjxD3Nzc738MJf1VVxcjI2NTaMoXpK3aqa/Y11oWbVwpuPzT2o/x7/zKeqsHCMmEkIIURNSbBuZ1qH9aTUgCCh9OPXZ9z83ciIhhBDVkWLbyKhUKjq9/DTm9rYAJG/Zzfl1P1CQlm7kZEIIISojxbYRsnF3JWDG49rPCf9bx74xz5C0eacRUwkhhKiMFNtGqmVwL90BGoUzSz6WLVwhhDBBUmwbqfzLKeUHajTkX6pguBBCCKOSYttI2fl4gtlNF2KrVNj61O/CayGEEPonxbaRsnF3pcurk3UKroWTvdxVSgghTJAU20bMa3QI/TdF4NTFH4DizByu/HrYyKmEEELcTIptI2fj7kr7yQ9pP1/65icjphFCCFERKbZNQMt+PbBr1waAjKNnyI5PNHIiIYQQN5Ji2wSoVCp8Hhyp/Sxbt0IIYVqk2DYRniMGYuFoD0BK1D4K0zOMG0gIIYSWFNsmwtzWhjbhQwFQ1MX8vTHKyImEEEKUkWLbhHjfPwyVeek/6d/fb0dTpJ/nagohhKgfKbZNiI2HG26DS59vW3Qtk9Sd+42cSAghBEixbXJ8xt5wotTXP6EoihHTCCGEACm2TY5zYCcc/7nJRXZ8Ipm//2HkREIIIaTYNjEqlUpn6/aiXAYkhBBGJ8W2CWo9NBirf+6RfCXmN/KTrxg3kBBCNHMmXWwjIiIIDAzEyckJJycngoOD2bp1q/b7wYMHo1KpdF5Tpkypsk9FUXjjjTfw9PTE1taWoUOHcvbsWUPPSoMys7TE+75hpR80Cpc3bK16BCGEEAZl0sXW29ubJUuWEBcXx+HDhxkyZAjh4eGcOnVK2+bpp58mOTlZ+1q6dGmVfS5dupT333+fjz76iIMHD2Jvb8+wYcMoKCgw9Ow0qDZjQjGzsgQgafMuivPyjZxICCGaL5MutqNGjWLkyJEEBATQsWNHFi5ciIODAwcOHNC2sbOzw8PDQ/tycnKqtD9FUVi2bBmvv/464eHhBAYG8vnnn5OUlMSmTZsaYI4ajlVLZ1qHDQCgODuXlJ/3GDmREEI0XxbGDlBTJSUlrF+/ntzcXIKDg7XDv/zyS9atW4eHhwejRo1i9uzZ2NnZVdhHYmIiKSkpDB06VDvM2dmZfv36ERsby0MPPVTheIWFhRQWFmo/Z2VlAaBWq1Gr637jiLJx69NHVTzvCyN5y24ALn77E+6jBqMyq/vfV4bOq2+S17Akr2FJXsPSV96ajq9STPxCzBMnThAcHExBQQEODg5ERkYycmTp2bYff/wx7dq1w8vLi+PHj/PKK6/Qt29fvv/++wr72r9/P/379ycpKQlPT0/t8LFjx6JSqfjmm28qHG/u3LnMmzev3PDIyMhKC7upsPkyCouLqQDkj72TEv82Rk4khBBNR15eHo888giZmZlV7lk1+S3bTp06cezYMTIzM9mwYQPjx48nJiaGrl27MmnSJG277t274+npSUhICAkJCfj7++stw6xZs3j++ee1n7OysvDx8SEsLKzKhVsdtVpNVFQUoaGhWFpa6iNqOVcd3Tn92nsAeJ1Pp/uMp+vcV0Pk1SfJa1iS17Akr2HpK2/Zns7qmHyxtbKyokOHDgAEBQVx6NAhli9fzsqVK8u17dev9FaF586dq7DYenh4AJCamqqzZZuamkrPnj0rzWBtbY21tXW54ZaWlnpZqfTVT0U8BvXlLy93CpLSuP7bcYoup2Lv512vPg2Z1xAkr2FJXsOSvIZV37w1HdekT5CqiEaj0Tl+eqNjx44B6BTSG/n5+eHh4cHOnTu1w7Kysjh48KDOceCmRGVurnsLx29/NmIaIYRonky62M6aNYs9e/Zw/vx5Tpw4waxZs4iOjubRRx8lISGBN998k7i4OM6fP8/mzZsZN24cAwcOJDAwUNtH586d2bhxI1B6d6WZM2eyYMECNm/ezIkTJxg3bhxeXl6MGTPGSHNpeF53D8bczgaA5J9jUGdmGzmREEI0Lya9GzktLY1x48aRnJyMs7MzgYGBbN++ndDQUC5dusSOHTtYtmwZubm5+Pj4cN999/H666/r9BEfH09mZqb288svv0xubi6TJk0iIyODAQMGsG3bNmxsbBp69hqMhYM9nnffyeVvt6IpLCJp8y7aPR5u7FhCCNFsmHSxXbVqVaXf+fj4EBMTU20fN59srVKpmD9/PvPnz693vsbE54ERXF6/DRSFSxu24vPw3ZhZmBs7lhBCNAsmvRtZ6I+djyet+vcGoDA1nSsxvxk5kRBCNB9SbJsRnwfv0r6/9LU8DUgIIRqKFNtmpMWtt2Dv7wNA5ol4sk6fM3IiIYRoHqTYNiOlz7q9Yev2G7kMSAghGoIU22bGY9gALJ0dAUjduZ/CK9eMnEgIIZo+KbbNjLmNNW3GlD6IQSku4fL3vxg5kRBCNH1SbJuhNvcNQ2VeetnP35uiKCksMnIiIYRo2qTYNkM27q64h9wGgPp6FqlR+4ycSAghmjYpts3UzZcBmfiTFoUQolGTYttMOXcLwOmWAAByzl0g48hpIycSQoimS4ptM9b2hq3bi9/ITS6EEMJQpNg2Y2539sParSUAV389TP7fqUZOJIQQTZMU22bMzMIC7weGl35QFM7+7wsK0tKNG0oIIZogKbbNXJvwoaj+efrPld0H2TfmGZI27zRyKiGEaFqk2DZzJYVFKMUl/w7QKJxZ8rFs4QohhB5JsW3m8i4llx+o0ZB/KaXhwwghRBMlxbaZs/PxBDOV7kAzM2x9PIwTSAghmiApts2cjbsrXV6dDKp/C677nf2wcXc1YiohhGhapNgKvEaH0PvDudrP+UlpxgsjhBBNkBRbAUCLXl1x7OQHQPaZBPIuVnAsVwghRJ1IsRVarcPu0L5PidprxCRCCNG0SLEVWq1Db9ceu039ZZ88nEAIIfREiq3QsnF3xaVnZwDyLvxNztnzxg0khBBNhBRboUNnV/J22ZUshBD6IMVW6Gg95DZU5qW3b0zdsQ9FozFyIiGEaPyk2Aodls6OtOwXCEBhajqZx+ONnEgIIRo/KbainNahA7TvU36RXclCCFFfUmxFOW4D+2BmbQVA2q5YNMXFRk4khBCNm0kX24iICAIDA3FycsLJyYng4GC2bt0KwLVr15gxYwadOnXC1taWtm3b8uyzz5KZmVllnxMmTEClUum8hg8f3hCz02hY2NvSakAQAOqMbK4fOmHkREII0bhZGDtAVby9vVmyZAkBAQEoisLatWsJDw/n6NGjKIpCUlIS77zzDl27duXChQtMmTKFpKQkNmzYUGW/w4cPZ/Xq1drP1tbWhp6VRscjbABpO2MBSInah2twLyMnEkKIxsuki+2oUaN0Pi9cuJCIiAgOHDjAxIkT+e6777Tf+fv7s3DhQh577DGKi4uxsKh81qytrfHwkKfaVMU1uBcWDnYU5+RxJfogJS8/DeYmvSNECCFMlkkX2xuVlJSwfv16cnNzCQ4OrrBNZmYmTk5OVRZagOjoaNzd3WnRogVDhgxhwYIFuLpW/pSbwsJCCgsLtZ+zsrIAUKvVqNXqOswN2vFv/K9JUYHroL6k/hRNSV4BqXsO4TKgN2CieStg0su3ApLXsCSvYTXXvDUdX6WY+D35Tpw4QXBwMAUFBTg4OBAZGcnIkSPLtbt69SpBQUE89thjLFy4sNL+vv76a+zs7PDz8yMhIYHXXnsNBwcHYmNjMf/n+tKbzZ07l3nz5pUbHhkZiZ2dXd1nzsSZJyZj+/VOAIo7+VBw7yAjJxJCCNOSl5fHI488ot3Yq4zJF9uioiIuXrxIZmYmGzZs4NNPPyUmJoauXbtq22RlZREaGkrLli3ZvHkzlpaWNe7/r7/+wt/fnx07dhASElJhm4q2bH18fLh69WqVC7c6arWaqKgoQkNDa5W5oSglGg7cOw31tUxUVpbcuuF9dsfuM9m8NzP15XszyWtYktewmmverKwsWrVqVW2xNfndyFZWVnTo0AGAoKAgDh06xPLly1m5ciUA2dnZDB8+HEdHRzZu3Fjrhda+fXtatWrFuXPnKi221tbWFZ5EZWlpqZeVSl/96J0ltB56O5e/3YpSpCYz9ljpYFPNWwnJa1iS17Akr2HVN29Nx210Z7xoNBrtVmZWVhZhYWFYWVmxefNmbGxsat3f5cuXSU9Px9PTU99RmwSPsH9vcHFlx34jJhFCiMbLpIvtrFmz2LNnD+fPn+fEiRPMmjWL6OhoHn30UW2hzc3NZdWqVWRlZZGSkkJKSgolJSXaPjp37szGjRsByMnJ4aWXXuLAgQOcP3+enTt3Eh4eTocOHRg2bJixZtOkOXULwMbLHYDrcSdR5eYbOZEQQjQ+Jr0bOS0tjXHjxpGcnIyzszOBgYFs376d0NBQoqOjOXjwIIB2N3OZxMREfH19AYiPj9fe6MLc3Jzjx4+zdu1aMjIy8PLyIiwsjDfffFOuta2ESqXCI7Q/59duBI2CxZmLxo4khBCNjkkX21WrVlX63eDBg2v0cPMb29ja2rJ9+3a9ZGtOWocNKC22gMXp88YNI4QQjZBJ70YWpsHBvy32/j4AmP99hYLkK0ZOJIQQjYsUW1EjHjc8VL7sNo5CCCFqRoqtqJHWobdr38tZyUIIUTtSbEWN2Hq1xrFb6YlouQkXyfnrkpETCSFE4yHFVtSY+9B/t25T5aHyQghRY1JsRY253XkbikoFQGrUvhqdDS6EEEKKragFK1cXStq1BiD/71SyTp8zciIhhGgcpNiKWinu6qt9L7uShRCiZqTYilop7tQWlWXpvVBSd+xHueHWmEIIISomxVbUjo0VLW/rCUBRegbXj542bh4hhGgEpNiKWtM5K3m77EoWQojqSLEVtdby9l6Y25U+zjAt+iCaIrWREwkhhGmTYitqzdzGGreBfQAozs4l/cAx4wYSQggTJ8VW1EnrGx4qnxq1z4hJhBDC9EmxFXXSsm8gls6OAFzZc4jiPHmovBBCVKbWxTY/P5+///673PBTp07pJZBoHMwsLHAfchsAmsIirv562MiJhBDCdNWq2G7YsIGAgADuuusuAgMDOXjwoPa7xx9/XO/hhGnT2ZX8i+xKFkKIytSq2C5YsIC4uDiOHTvG6tWrmThxIpGRkQByn9xmyKVHZ6zdXQFIP3AMdWa2kRMJIYRpqlWxVavVtG5dem/coKAg9uzZw8qVK5k/fz6qf25QL5oPlZkZrf+55lYpKSFt9wEjJxJCCNNUq2Lr7u7O8ePHtZ9btmxJVFQUZ86c0Rkumo8bdyWnyK5kIYSoUK2K7RdffIG7u7vOMCsrK7766itiYmL0Gkw0Do6d/LBr6wlAxtHTFKSlGzmREEKYnloVW29vbzw8PCr8rn///noJJBoXlUpF69B/tm4VhbQd+40bSAghTJBcZyvqrXXYv39opcgNLoQQohyLuo548eLFOo3n4uKCk5NTXScrTJB9uzY4dvIjOz6R7DMJJP8cQ4tbb8HmnzOVhRCiuatzsfX19a31OCqVijlz5vDGG2/UdbLCRLUOG0B2fCIAp+f/D8xUdHl1Ml6jQ4ycTAghjK/OxVaj0egzh2jkXHp11R2gUTiz5GNa3tZTtnCFEM1enYutn59fna6tnTlzJs8++2xdJytMVEl+QfmBGg35l1Kk2Aohmr06F9s1a9bUaby67H4Wps/OxxNUKrjxTmJmZtj6VHz2uhBCNCd1LraDBg3SZ44KRUREEBERwfnz5wHo1q0bb7zxBiNGjACgoKCAF154ga+//prCwkKGDRvGhx9+qL3LVUUURWHOnDl88sknZGRk0L9/fyIiIggICDD4/DRlNu6udJjxOOfe/1w7rPMrk2SrVggh0OOlP2q1mkuXLhEfH8+1a9f00qe3tzdLliwhLi6Ow4cPM2TIEMLDw7VPGPrPf/7Djz/+yPr164mJiSEpKYl77723yj6XLl3K+++/z0cffcTBgwext7dn2LBhFBRUsBtU1Eq7R0bh1K2D9rPzDe+FEKI5q1exzc7OJiIigkGDBuHk5ISvry9dunTBzc2Ndu3a8fTTT3Po0KE69z9q1ChGjhxJQEAAHTt2ZOHChTg4OHDgwAEyMzNZtWoV7733HkOGDCEoKIjVq1ezf/9+Dhyo+B69iqKwbNkyXn/9dcLDwwkMDOTzzz8nKSmJTZs21Tmn+JfHiH/3eKTujDViEiGEMB113o383nvvsXDhQvz9/Rk1ahSvvfYaXl5e2Nracu3aNU6ePMmvv/5KWFgY/fr144MPPqjXrtqSkhLWr19Pbm4uwcHBxMXFoVarGTp0qLZN586dadu2LbGxsdx2223l+khMTCQlJUVnHGdnZ/r160dsbCwPPfRQhdMuLCyksLBQ+zkrKwso3ZpXq9V1nqeycevTR0OqSd6WA4Lg3c9AUUjduR+fJ+412kMqmuLyNSWS17Akr2HpK29Nx1cpdXw23sMPP8zrr79Ot27dqmxXWFjI6tWrsbKy4sknn6z1dE6cOEFwcDAFBQU4ODgQGRnJyJEjiYyM5IknntApggB9+/blzjvv5K233irX1/79++nfvz9JSUl4enpqh48dOxaVSsU333xTYYa5c+cyb968csMjIyOxs7Or9Tw1dbbrfsH8UhoAeRPvQuPewsiJhBDCMPLy8njkkUfIzMys8oZNdd6y/eqrr2rUztramilTptR1MnTq1Iljx46RmZnJhg0bGD9+fIM/9GDWrFk8//zz2s9ZWVn4+PgQFhZWr7thqdVqoqKiCA0NxdLSUh9RDaqmeZMKLDj3f2sA6Fxsie/IkQ2UUFdTXb6mQvIaluQ1LH3lLdvTWZ06F1sAR0dHevXqRVBQEL1796Z379507dpVr7sNrays6NCh9ESboKAgDh06xPLly3nwwQcpKioiIyMDFxcXbfvU1NRKH5ZQNjw1NVVnyzY1NZWePXtWmsHa2hpra+tywy0tLfWyUumrn4ZSXV6PobdzbtlaUBSuRv9GhymPGPV5x01t+ZoayWtYktew6pu3puPW6gSpm7dm33rrLQICAti1axdPPvkkgYGBODo6cvvttzNjxgxWr17N77//XptJVEuj0VBYWEhQUBCWlpbs3LlT+118fDwXL14kODi4wnH9/Pzw8PDQGScrK4uDBw9WOo6oPWvXFrj07AJA3oUkcs7V7T7aQgjRVNRoyzYlJYWpU6fi4uLCww8/rB0+depU7fv8/Hzs7e2ZMWMG165d48CBA3z66acUFRVRUlJSp3CzZs1ixIgRtG3bluzsbCIjI4mOjmb79u04OzszceJEnn/+eVq2bImTkxMzZswgODhY5+Sozp07s3jxYu655x5UKhUzZ85kwYIFBAQE4Ofnx+zZs/Hy8mLMmDF1yigq5h4STMbR0wCk7YrFMaCdkRMJIYTx1KjYfvzxx6jVaj777LNK29ja2gKlJ04FBgYCUFxczOnTp+scLi0tjXHjxpGcnIyzszOBgYFs376d0NBQAP7v//4PMzMz7rvvPp2bWtwoPj6ezMxM7eeXX36Z3NxcJk2aREZGBgMGDGDbtm3Y2NjUOacoz/3Ofvz5z1nJabtiaT/pQaPuShZCCGOqUbF99tlnee6557jvvvv47rvvat65hYW28NbFqlWrqvzexsaGFStWsGLFikrb3HyytUqlYv78+cyfP7/OuUT1ynYlZxw9rd2VLFu3QojmqkbHbF1cXFi7di0TJ040dB7RhLiH/HscPG2X3OBCCNF81eoEqZE3XcLx1FNPERERwaFDh7TXu8quQlHG/c5+pQ8nANJ27i+3l0EIIZqLel36c/bsWdavX092djYWFqVdzZs3j8GDB9O7d2969uwpN31oxnR2JV9Mll3JQohmq17FtuzmEmfPniUuLo4jR45w5MgR3njjDTIyMjA3N6djx47aBweI5kfOShZCiHoW2zIBAQEEBATo3Fs4MTGRw4cPc/ToUX1MQjRSOmcl79wvZyULIZolvRTbivj5+eHn58cDDzxgqEmIRkB2JQshRD0esXfxYu3uCvT333/XdVKikdM5K3nnfiMmEUII46hzse3Tpw+TJ0+u8nm1mZmZfPLJJ9xyyy21uj5XNC06ZyXvipWzkoUQzU6ddyOfPn2ahQsXEhoaio2NDUFBQXh5eWFjY8P169c5ffo0p06donfv3ixdurTcZUOi+ZBdyUKI5q7OW7aurq689957JCcn87///Y+AgACuXr3K2bNnAXj00UeJi4sjNjZWCq2QXclCiGat3idI2draMnz4cO6//3595BFNVLl7JU9+SM5KFkI0G3Xesr2Rs7OzHJMVVdJ57N7FZHLOXTByIiGEaDh6KbaKorBy5Ur69+/PgAEDmDlzZpUnTonmSXdXstwrWQjRfOil2AIcPXqU3r17M2DAAE6dOsUdd9zBiy++qK/uRRMgZyULIZorvd3UIjIyUvucWYDjx48THh5OmzZt+M9//qOvyYhGrPxZyRdwDPA1diwhhDA4vWzZtmzZEh8fH51hgYGB/O9//yMiIkIfkxBNROuQ27XvZVeyEKK50Eux7dmzJ6tXry43vEOHDrW+05Ro2tzu7Cu7koUQzY5eiu2CBQt4//33efzxx4mNjSU3N5e0tDQWLVqEn5+fPiYhmgg5K1kI0RzppdjedtttHDhwgEuXLnHHHXfg5OSEp6cnGzZs4N1339XHJEQTIruShRDNjd7ORu7RowfR0dEkJSWxZcsWNm/ezIULF+TuUaKcG3clp+6UXclCiKavzmcjV3Ustlu3bgDk5eWVa+fi4oKTk1NdJyuagBvPSs6/JGclCyGavjoXW19f31qPo1KpmDNnDm+88UZdJyuaiNYht5Nx9DRQuitZiq0Qoimr825kjUZT61dJSYkUWgHIrmQhRPNS5y1bPz+/Ot1IfubMmTz77LN1naxoIqxdW+DSqysZR07JrmQhRJNX52K7Zs2aOo1Xl93PomlqPSSYjCOnAEjbsV+KrRCiyapzsR00aJA+c4hmyO3OvsS/uwoUhdRdB2g/5WF57J4QoknS26U/QtRW2a5kQLsrWQghmiIptsKoWg+54bF7O/YbMYkQQhiOSRfbxYsX06dPHxwdHXF3d2fMmDHEx8drvz9//jwqlarC1/r16yvtd8KECeXaDx8+vCFmSdxE56zkXQfkrGQhRJNk0sU2JiaGadOmceDAAaKiolCr1YSFhZGbmwuAj48PycnJOq958+bh4ODAiBEjqux7+PDhOuN99dVXDTFL4ibldiWfPW/cQEIIYQB6e56tIWzbtk3n85o1a3B3dycuLo6BAwdibm6Oh4eHTpuNGzcyduxYHBwcquzb2tq63LjCOHTOSt4Zi2NHeXiFEKJpMelie7PMzEyg9Pm5FYmLi+PYsWOsWLGi2r6io6Nxd3enRYsWDBkyhAULFuDq6lph28LCQgoLC7Wfs7KyAFCr1ajV6trOhlbZuPXpoyEZKm+LAb3hPRVoFFJ3xuIz8X69nJUsy9ewJK9hSV7D0lfemo6vUhrJQTKNRsPo0aPJyMhg7969FbaZOnUq0dHRnD59usq+vv76a+zs7PDz8yMhIYHXXnsNBwcHYmNjMTc3L9d+7ty5zJs3r9zwyMhI7Ozs6jZDQofNl1FYXEwFIO/JkWhaV/wHlRBCmJK8vDweeeQRMjMzq7zvf6Mpts888wxbt25l7969eHt7l/s+Pz8fT09PZs+ezQsvvFCrvv/66y/8/f3ZsWMHISEh5b6vaMvWx8eHq1ev1uuhCmq1mqioKEJDQ7G0tKxzPw3FkHmTNkZx7r3VAPg8Ho7fpAfr3acsX8OSvIYleQ1LX3mzsrJo1apVtcW2UexGnj59Olu2bGHPnj0VFlqADRs2kJeXx7hx42rdf/v27WnVqhXnzp2rsNhaW1tjbW1dbrilpaVeVip99dNQDJHXIySYc8vWgEbh6u6DBEx9VG83uJDla1iS17Akr2HVN29NxzXps5EVRWH69Ols3LiRXbt24edX+Ykzq1atYvTo0bi5udV6OpcvXyY9PR1PT8/6xBX1UPrYvX/OSr6cImclCyGaFJMuttOmTWPdunVERkbi6OhISkoKKSkp5Ofn67Q7d+4ce/bs4amnnqqwn86dO7Nx40YAcnJyeOmllzhw4ADnz59n586dhIeH06FDB4YNG2bweRKV07nBxc5YIyYRQgj9MuliGxERQWZmJoMHD8bT01P7+uabb3TaffbZZ3h7exMWFlZhP/Hx8dozmc3NzTl+/DijR4+mY8eOTJw4kaCgIH799dcKdxWLhuN2Z18wK911nPRTNPmpV42cSAgh9MOkj9nW9NytRYsWsWjRohr1Y2try/bt2+udTeiftWsLbL09yb+YRNHV6+wfM5UusybjNbr8cXQhhGhMTHrLVjQvBWnp5F9K/neAonBmyccUpKUbL5QQQuiBFFthMvIuJcPNezM0GvIvpRgnkBBC6IkUW2Ey7Hw8tcdstVQqbH3ktppCiMZNiq0wGTburnR5dbL2KUAADgG+2LhXfBtNIYRoLKTYCpPiNTqE4O/+h4WTPQC5CRcpTM8wbighhKgnKbbC5Nh5udMmfCgASkkJKdt+NXIiIYSoHym2wiR5jRqifZ/04055qLwQolGTYitMkl1bL5x7dAYg7/zfZJ3808iJhBCi7qTYCpOls3W7eZcRkwghRP1IsRUmq3VIMOZ2NgCk7txPcV5+NWMIIYRpkmIrTJa5rQ2th/YHoCSvgLRdB4ycSAgh6kaKrTBpXqNv3JW804hJhBCi7qTYCpPm1C0Aez9vADKPx5N74W8jJxJCiNqTYitMmkql0jlRKvnH3UZMI4QQdSPFVpg8j+EDUZmbA5D8czSa4mIjJxJCiNqRYitMnlVLZ1rdcSsARdcySd9/1MiJhBCidqTYikZB50SpH+WaWyFE4yLFVjQKrv16YO3WEoD0/UcovHrdyImEEKLmpNiKRkFlbo7nyEEAKCUakn+OMXIiIYSoOSm2otHwvPGs5C275OEEQohGQ4qtaDTsvD1w6d0NgLyLyWT+/oeREwkhRM1IsRWNiteoO7Xv5UQpIURjIcVWNCrud96Gub0tAKk7YynOzTNyIiGEqJ4UW9GomNtY4xE2AABNQSGpO2KNnEgIIaonxVY0Ol6jQ7Tvk36UhxMIIUyfFFvR6Dh2bo+Df1sAsk6eJSfxkpETCSFE1aTYikZHpVLhOVoeTiCEaDyk2IpGyXP4QFSWFgAkb41Bo1YbOZEQQlTOpIvt4sWL6dOnD46Ojri7uzNmzBji4+N12gwePBiVSqXzmjJlSpX9KorCG2+8gaenJ7a2tgwdOpSzZ88aclaEnlk6O+I2sA8A6utZXN17xMiJhBCiciZdbGNiYpg2bRoHDhwgKioKtVpNWFgYubm5Ou2efvppkpOTta+lS5dW2e/SpUt5//33+eijjzh48CD29vYMGzaMgoICQ86O0LMbn3MrJ0oJIUyZhbEDVGXbtm06n9esWYO7uztxcXEMHDhQO9zOzg4PD48a9akoCsuWLeP1118nPDwcgM8//5zWrVuzadMmHnroIf3NgDColn26Y93alcLUdNIPHKMgLR0bd1djxxJCiHJMutjeLDMzE4CWLVvqDP/yyy9Zt24dHh4ejBo1itmzZ2NnZ1dhH4mJiaSkpDB06FDtMGdnZ/r160dsbGyFxbawsJDCwkLt56ysLADUajXqehwrLBu3Pn00JFPM23rEIC6u+R40Cn//uIu248ZovzPFvFWRvIYleQ2rueat6fgqpZHczV2j0TB69GgyMjLYu3evdvjHH39Mu3bt8PLy4vjx47zyyiv07duX77//vsJ+9u/fT//+/UlKSsLT01M7fOzYsahUKr755pty48ydO5d58+aVGx4ZGVlpURcNQ5WRg33EJgA0Lg7kTQkHlcq4oYQQzUZeXh6PPPIImZmZODk5Vdqu0WzZTps2jZMnT+oUWoBJkyZp33fv3h1PT09CQkJISEjA399fL9OeNWsWzz//vPZzVlYWPj4+hIWFVblwq6NWq4mKiiI0NBRLS0t9RDUoU817/LezZMSdwiwjh9u92uPSqwtgunkrI3kNS/IaVnPNW7anszqNothOnz6dLVu2sGfPHry9vats269fPwDOnTtXYbEtO7abmpqqs2WbmppKz549K+zT2toaa2vrcsMtLS31slLpq5+GYmp524QPJSPuFABpW2Nw6xuo872p5a2O5DUsyWtYzS1vTcc16bORFUVh+vTpbNy4kV27duHn51ftOMeOHQPQKaQ38vPzw8PDg507/z17NSsri4MHDxIcHKyX3KJhuQ3qi4WjPQBpuw5QnJNbzRhCCNGwTLrYTps2jXXr1hEZGYmjoyMpKSmkpKSQn58PQEJCAm+++SZxcXGcP3+ezZs3M27cOAYOHEhg4L9bN507d2bjxo1A6d2HZs6cyYIFC9i8eTMnTpxg3LhxeHl5MWbMGGPMpqgnc2srPIb983CCwiJSftln5ERCCKHLpIttREQEmZmZDB48GE9PT+2r7CQmKysrduzYQVhYGJ07d+aFF17gvvvu48cff9TpJz4+XnsmM8DLL7/MjBkzmDRpEn369CEnJ4dt27ZhY2PToPMn9Mdr1L8PJ0jeIs+5FUKYFpM+ZlvdidI+Pj7ExMTUuh+VSsX8+fOZP39+vfIJ0+HYyQ/Hjn5k/5lI1ukEcs5dwLqdl7FjCSEEYOJbtkLUxo0PJ0j6UbZuhRCmQ4qtaDI8wgZgZlV6ZmDy1j1oihrHxfVCiKZPiq1oMiydHHAb3BeA4qwc0vfFGTmREEKUkmIrmpQbT5RK2RJtvCBCCHEDKbaiSWkR1A0bTzcArh86gSpTrrkVQhifFFvRpKjMzPC8+87SD4qC5a+/U5iWbtxQQohmT4qtaHK87hqsfW914i8OPvAsSZvlebdCCOORYiuaHrObVmuNwpklH1MgW7hCCCORYiuanLxLyeUHajTkX0pp+DBCCIEUW9EE2fl4gtlNz7RVga2Ph3ECCSGaPSm2osmxcXely6uTdXcnK1CYetV4oYQQzZoUW9EkeY0Ood/65RT17qgd9sfST9AUlxgxlRCiuZJiK5osa3dXikJvxT6gHQA5Zy9wef1WI6cSQjRHUmxF02ZmRsALE0FVegz3r0++kbOShRANToqtaPKcunWgzZihAJTkFXB22RrjBhJCNDtSbEWz4P/MI1i2cAIgbdcB0mOPGjmREKI5kWIrmgVLJwcCZozTfo5/ZxUlBYVGTCSEaE6k2Ipmw2PEQFx6dQUg/+9Uzn++0ciJhBDNhRRb0WyoVCo6vfwUKnNzAC588QN5F5OMnEoI0RxIsRXNioOfD20fHQWAoi7mj6WfoiiKkVMJIZo6Kbai2fF78n5sPP555u3hE6RG7TNyIiFEUyfFVjQ75jbWdHzhSe3ns8vXUpwjD5kXQhiOFFvRLLndcSutBvYBoCg9g4SPvjZyIiFEUybFVjRbnZ5/AjMbawAuf7+drD8SjJxICNFUSbEVzZaNhxvtJz5Q+kGj8Mdbn6CUyIMKhBD6J8VWNGs+D9+FfXsfALLPJPD3xh1GTiSEaIqk2IpmzczCgk4vP639nPBRJIXp142YSAjRFEmxFc1ei55d8LxrMADFOXmce/8L4wYSQjQ5Jl1sFy9eTJ8+fXB0dMTd3Z0xY8YQHx+v/f7atWvMmDGDTp06YWtrS9u2bXn22WfJzMysst8JEyagUql0XsOHDzf07AgT1mH6Y1g42QOQsv1Xrh0+YeREQoimxKSLbUxMDNOmTePAgQNERUWhVqsJCwsjN7f0msikpCSSkpJ45513OHnyJGvWrGHbtm1MnDix2r6HDx9OcnKy9vXVV18ZenaECbNq4UyHqY9pP8e//SmaIrUREwkhmhILYweoyrZt23Q+r1mzBnd3d+Li4hg4cCC33HIL3333nfZ7f39/Fi5cyGOPPUZxcTEWFpXPnrW1NR4eHgbLLhofr9FDSP5pN5kn/iTvQhIXvtyM3xP3GTuWEKIJMOkt25uV7R5u2bJllW2cnJyqLLQA0dHRuLu706lTJ5555hnS09P1mlU0PiozMzq9/DQq89L/Lc6v+Y78v1ONnEoI0RSY9JbtjTQaDTNnzqR///7ccsstFba5evUqb775JpMmTaqyr+HDh3Pvvffi5+dHQkICr732GiNGjCA2Nhbzf54Ic6PCwkIKC/999mlWVhYAarUatbruuxrLxq1PHw2pOeS18W2D133D+PvbrWgK1fzx9qd0W/oSKpXKUDG1msPyNSbJa1jNNW9Nx1cpjeSRJ8888wxbt25l7969eHt7l/s+KyuL0NBQWrZsyebNm7G0tKxx33/99Rf+/v7s2LGDkJCQct/PnTuXefPmlRseGRmJnZ1d7WZEmL5CNXaf/IhZdl7pxzsCKQ70R/nnBCohhCiTl5fHI488ot2rWplGUWynT5/ODz/8wJ49e/Dz8yv3fXZ2NsOGDcPOzo4tW7ZgY2NT62m4ubmxYMECJk+eXO67irZsfXx8uHr1apULtzpqtZqoqChCQ0Nr9ceBsTSnvFd2H+TMG8v/HWCmIuClp/C8+049p/xXc1q+xiB5Dau55s3KyqJVq1bVFluT3o2sKAozZsxg48aNREdHV1hos7KyGDZsGNbW1mzevLlOhfby5cukp6fj6elZ4ffW1tZYW1uXG25paamXlUpf/TSU5pC3RY/OugM0CmffXoV7/yBs3F31mK685rB8jUnyGlZzy1vTcU36BKlp06axbt06IiMjcXR0JCUlhZSUFPLz84HSQlt2KdCqVavIysrStim54R63nTt3ZuPGjQDk5OTw0ksvceDAAc6fP8/OnTsJDw+nQ4cODBs2zCjzKUxP/uWU8gM1GlJ3xjZ8GCFEo2fSW7YREREADB48WGf46tWrmTBhAkeOHOHgwYMAdOjQQadNYmIivr6+AMTHx2vPZDY3N+f48eOsXbuWjIwMvLy8CAsL480336xw61U0T3Y+nmCmAo3uUZZzy9dSmHoV/ykPY24j64sQomZMuthWdzh58ODB1ba5uR9bW1u2b99e72yiabNxd6XLq5M5s+Rj0Gh0vrv09U+kxx6j25zpOHXtUEkPQgjxL5MutkIYk9foEFre1pP8SynYtHHnyu6DJEREoilSk3fhbw4//V/ajb8Hvyfvx6ya67qFEM2bSR+zFcLYbNxdaRHUDVsPN9o+fDd91y7FsYs/AEqJhvOffcfhia+R89clIycVQpgyKbZC1IK9nze3frIAv6fGovrnBijZ8YkcmvAKF77cLA+fF0JUSIqtELVkZmFB+6ce4NZVC7H3K73BiqZIzbkPvuDItHlyi0chRDlSbIWoI6fO/vRZ8xZtHxkF/9zOMePYGQ4+9gJ/b4qq0cl7QojmQYqtEPVgbm1FwLPj6P3hXGw83QAoyS/kjyUf8/vziym8cs3ICYUQpkCKrRB60KJXV/qtexev8H/vrZ0ee5QDjz5PStQ+CtLSuRZ3koI0ebqUEM2RXK8ghJ5Y2NvSZdYU3Ab15cyijyi6ep3irFxOzV72byMzFV1enYzX6PIPvBBCNF2yZSuEnrW6vTe3ffku7kNvL/+lRuHM4pUk/RyDOjO74cMJIYxCtmyFMABLZ0e6L/gPCT6enF/9ne6XisKZ+f/jDGDr7YFztwCcugVg18kXiuXSISGaIim2QhhQm3tCOb/me6jkzOT8yynkX04hZfuvANibm3H0p0M43xKAc9cAnLp1wNbbQ/vw+oK0dPIuJWPn42nwpw8JIfRHiq0QBmTj7kqXWTfcY9nMDK/RQzC3sSbr1Fmy4xPRFKm17VUlGrJPnyP79DkusxUo3Up26toBlYU5V/fGlRZulYqOzz+J9/3DtIW4pvRVsAvT0jG/kEJhWjqWbTzq3I8QzYEUWyEM7MZ7LNv6eOgUOI1aTc65i2SeOkvGiXhSDh3H7FqWzvjqzGzSY4/qdqoo/PnuKv58bxUWjvZY2Nth4fDPS/ve/t9h/wzPPPknl77dqi3Y7ac8jEfYAFTmZqjMzTGzMC99b2GhHaYyK39qR9LmnZxZshJbjcLBr3fW66QvfRR/ff0BYUpZ9PXHjCnNkyllaeg/FlWKXHlfa1lZWTg7O5OZmYmTk1Od+1Gr1fz888+MHDmyUTxsWfIaVlne0DsGkn/2IlmnzpJ56ixZp86izjDiyVQq1b+F19wczKAkJ79cM2u3FqjMLUD1zzgqVel7VKWfzVT/vOef71Sos3MounL93z7cW2Lp5PjPeP9OXyfOjV+qoCgjm8LUq//24dEKK5fy/18qikJmZibOzs4V7g0oysiiMOWGfjzdKuynKkUZWRQmX6lXH6bWT037UBSFzIwMnF1cKl++DZSlVv2oVHSZVfc/FmtaD2TLVggTY+nogF2/Hrj26wH88yN2Ip64yW+UO/Zr36EdmoJCinPzKM7JQ1EX6z+QoqAUl6BUc/JW4Q1Fs64K065RmFa/G4EUplzVKZo3MgdyUmrWf2HyFZ0f9jpl0UMfptZPVX00yuWrKJxZ8jEtb+tp0PMgpNgKYeJUKhUugZ3LHfvt8uqkcn+NlxQWUZyTR3FO7j//zaM4N4+SnDzyk69wfs13cNO+LNf+vTGztEQpKfnnpfn3fbGm3PCSwiIKktLK5bR0cUJlpvrn7wGl9A8Dpex50v+812j+mX5pAb/xeLV2fi1v2HV9U1adHXGKgqJRyj1vGAAzswq3rjSKBjNV+d3iilJZP6oaHxMv7aOCHYW16MPU+qlNHwqgaBRUZjr7HoySpdb9aDSlj9KUYiuEqOrYbxlzayvMra2wdnWpsA9bT7dqC3ZNlB6zrV8/BWnp7BvzjO4Pn5kZt3+3osY/epX10X/Th+X6qOqwQm360UeWxtJPc1q+tj6GPW4rN7UQohEpe75uXf8C9xodQv9NH9J7xVz6b/qwzsepvEaH0G/9cvIfGUq/9cvr1I+NuytdXp0MZVux/xTt2sybPvpoiln01Y9k0R/ZshWimbFxd9XLD4u1uysl7TywrkdfNdlab4g+TDGLU1A3or/7gcH3heNQx7NlTW2eTCmLPpZvbUixFUIYlT6Kv77+gDClLPr4Y0ZfeWT51p/sRhZCCCEMTIqtEEIIYWBSbIUQQggDk2IrhBBCGJgUWyGEEMLApNgKIYQQBibFVgghhDAwuc62Dsruz5qVlVVNy6qp1Wry8vLIyspqNE+lkbyGI3kNS/IaVnPNW1YHqnuAnhTbOsjOLn3cmY+Pj5GTCCGEMAXZ2dk4OztX+r08z7YONBoNSUlJODo61uppEzfLysrCx8eHS5cu1eu5uA1F8hqW5DUsyWtYzTWvoihkZ2fj5eWFmVnlR2Zly7YOzMzM8Pb21lt/Tk5OjWLlLCN5DUvyGpbkNazmmLeqLdoycoKUEEIIYWBSbIUQQggDk2JrRNbW1syZMwdra2tjR6kRyWtYktewJK9hSd6qyQlSQgghhIHJlq0QQghhYFJshRBCCAOTYiuEEEIYmBRbIYQQwsCk2BrYihUr8PX1xcbGhn79+vHbb79V2X79+vV07twZGxsbunfvzs8//9wgORcvXkyfPn1wdHTE3d2dMWPGEB8fX+U4a9asQaVS6bxsbGwaJO/cuXPLTbtz585VjmOsZQvg6+tbLq9KpWLatGkVtm/oZbtnzx5GjRqFl5cXKpWKTZs26XyvKApvvPEGnp6e2NraMnToUM6ePVttv7Vd//WRV61W88orr9C9e3fs7e3x8vJi3LhxJCUlVdlnXdYpfeQFmDBhQrlpDx8+vNp+jbF8gQrXZZVKxdtvv11pn4ZcvjX5/SooKGDatGm4urri4ODAfffdR2pqapX91nW9r4gUWwP65ptveP7555kzZw5HjhyhR48eDBs2jLS0tArb79+/n4cffpiJEydy9OhRxowZw5gxYzh58qTBs8bExDBt2jQOHDhAVFQUarWasLAwcnNzqxzPycmJ5ORk7evChQsGz1qmW7duOtPeu3dvpW2NuWwBDh06pJM1KioKgAceeKDScRpy2ebm5tKjRw9WrFhR4fdLly7l/fff56OPPuLgwYPY29szbNgwCgoKKu2ztuu/vvLm5eVx5MgRZs+ezZEjR/j++++Jj49n9OjR1fZbm3VKX3nLDB8+XGfaX331VZV9Gmv5Ajo5k5OT+eyzz1CpVNx3331V9muo5VuT36///Oc//Pjjj6xfv56YmBiSkpK49957q+y3Lut9pRRhMH379lWmTZum/VxSUqJ4eXkpixcvrrD92LFjlbvuuktnWL9+/ZTJkycbNGdF0tLSFECJiYmptM3q1asVZ2fnhgt1gzlz5ig9evSocXtTWraKoijPPfec4u/vr2g0mgq/N+ayBZSNGzdqP2s0GsXDw0N5++23tcMyMjIUa2tr5auvvqq0n9qu//rKW5HffvtNAZQLFy5U2qa261RdVZR3/PjxSnh4eK36MaXlGx4ergwZMqTKNg21fBWl/O9XRkaGYmlpqaxfv17b5syZMwqgxMbGVthHXdf7ysiWrYEUFRURFxfH0KFDtcPMzMwYOnQosbGxFY4TGxur0x5g2LBhlbY3pMzMTABatmxZZbucnBzatWuHj48P4eHhnDp1qiHiAXD27Fm8vLxo3749jz76KBcvXqy0rSkt26KiItatW8eTTz5Z5YMsjLlsb5SYmEhKSorO8nN2dqZfv36VLr+6rP+GlJmZiUqlwsXFpcp2tVmn9C06Ohp3d3c6derEM888Q3p6eqVtTWn5pqam8tNPPzFx4sRq2zbU8r359ysuLg61Wq2zvDp37kzbtm0rXV51We+rIsXWQK5evUpJSQmtW7fWGd66dWtSUlIqHCclJaVW7Q1Fo9Ewc+ZM+vfvzy233FJpu06dOvHZZ5/xww8/sG7dOjQaDbfffjuXL182eMZ+/fqxZs0atm3bRkREBImJidxxxx3axx/ezFSWLcCmTZvIyMhgwoQJlbYx5rK9Wdkyqs3yq8v6bygFBQW88sorPPzww1XecL6265Q+DR8+nM8//5ydO3fy1ltvERMTw4gRIygpKamwvSkt37Vr1+Lo6FjtLtmGWr4V/X6lpKRgZWVV7o+t6n6Py9rUdJyqyFN/RDnTpk3j5MmT1R5PCQ4OJjg4WPv59ttvp0uXLqxcuZI333zToBlHjBihfR8YGEi/fv1o164d3377bY3+wjamVatWMWLECLy8vCptY8xl25So1WrGjh2LoihERERU2daY69RDDz2kfd+9e3cCAwPx9/cnOjqakJAQg067vj777DMeffTRak/ga6jlW9Pfr4YmW7YG0qpVK8zNzcud7ZaamoqHh0eF43h4eNSqvSFMnz6dLVu2sHv37lo/RtDS0pJevXpx7tw5A6WrnIuLCx07dqx02qawbAEuXLjAjh07eOqpp2o1njGXbdkyqs3yq8v6r29lhfbChQtERUXV+jFq1a1ThtS+fXtatWpV6bRNYfkC/Prrr8THx9d6fQbDLN/Kfr88PDwoKioiIyNDp311v8dlbWo6TlWk2BqIlZUVQUFB7Ny5UztMo9Gwc+dOnS2WGwUHB+u0B4iKiqq0vT4pisL06dPZuHEju3btws/Pr9Z9lJSUcOLECTw9PQ2QsGo5OTkkJCRUOm1jLtsbrV69Gnd3d+66665ajWfMZevn54eHh4fO8svKyuLgwYOVLr+6rP/6VFZoz549y44dO3B1da11H9WtU4Z0+fJl0tPTK522sZdvmVWrVhEUFESPHj1qPa4+l291v19BQUFYWlrqLK/4+HguXrxY6fKqy3pfXUhhIF9//bVibW2trFmzRjl9+rQyadIkxcXFRUlJSVEURVEef/xx5dVXX9W237dvn2JhYaG88847ypkzZ5Q5c+YolpaWyokTJwye9ZlnnlGcnZ2V6OhoJTk5WfvKy8vTtrk577x585Tt27crCQkJSlxcnPLQQw8pNjY2yqlTpwye94UXXlCio6OVxMREZd++fcrQoUOVVq1aKWlpaRVmNeayLVNSUqK0bdtWeeWVV8p9Z+xlm52drRw9elQ5evSoAijvvfeecvToUe3Zu0uWLFFcXFyUH374QTl+/LgSHh6u+Pn5Kfn5+do+hgwZonzwwQfaz9Wt/4bKW1RUpIwePVrx9vZWjh07prM+FxYWVpq3unXKUHmzs7OVF198UYmNjVUSExOVHTt2KL1791YCAgKUgoKCSvMaa/mWyczMVOzs7JSIiIgK+2jI5VuT368pU6Yobdu2VXbt2qUcPnxYCQ4OVoKDg3X66dSpk/L9999rP9dkva8pKbYG9sEHHyht27ZVrKyslL59+yoHDhzQfjdo0CBl/PjxOu2//fZbpWPHjoqVlZXSrVs35aeffmqQnECFr9WrV1ead+bMmdp5a926tTJy5EjlyJEjDZL3wQcfVDw9PRUrKyulTZs2yoMPPqicO3eu0qyKYrxlW2b79u0KoMTHx5f7ztjLdvfu3RX++5dl0mg0yuzZs5XWrVsr1tbWSkhISLn5aNeunTJnzhydYVWt/4bKm5iYWOn6vHv37krzVrdOGSpvXl6eEhYWpri5uSmWlpZKu3btlKeffrpc0TSV5Vtm5cqViq2trZKRkVFhHw25fGvy+5Wfn69MnTpVadGihWJnZ6fcc889SnJycrl+bhynJut9Tckj9oQQQggDk2O2QgghhIFJsRVCCCEMTIqtEEIIYWBSbIUQQggDk2IrhBBCGJgUWyGEEMLApNgKIYQQBibFVgihV76+vixbtszYMYQwKVJshWjEJkyYwJgxYwAYPHgwM2fObLBpr1mzpsLnwx46dIhJkyY1WA4hGgN5xJ4QQkdRURFWVlZ1Ht/NzU2PaYRoGmTLVogmYMKECcTExLB8+XJUKhUqlYrz588DcPLkSUaMGIGDgwOtW7fm8ccf5+rVq9pxBw8ezPTp05k5cyatWrVi2LBhALz33nt0794de3t7fHx8mDp1Kjk5OQBER0fzxBNPkJmZqZ3e3LlzgfK7kS9evEh4eDgODg44OTkxduxYnceWzZ07l549e/LFF1/g6+uLs7MzDz30kM5DxTds2ED37t2xtbXF1dWVoUOHkpuba6ClKYT+SbEVoglYvnw5wcHBPP300yQnJ5OcnIyPjw8ZGRkMGTKEXr16cfjwYbZt20Zqaipjx47VGX/t2rVYWVmxb98+PvroIwDMzMx4//33OXXqFGvXrmXXrl28/PLLQOnD7JctW4aTk5N2ei+++GK5XBqNhvDwcK5du0ZMTAxRUVH89ddfPPjggzrtEhIS2LRpE1u2bGHLli3ExMSwZMkSAJKTk3n44Yd58sknOXPmDNHR0dx7773Ibd1FYyK7kYVoApydnbGyssLOzk7nwdb/+9//6NWrF4sWLdIO++yzz/Dx8eHPP/+kY8eOAAQEBLB06VKdPm88/uvr68uCBQuYMmUKH374IVZWVjg7O6NSqap8kPbOnTs5ceIEiYmJ+Pj4APD555/TrVs3Dh06RJ8+fYDSorxmzRocHR0BePzxx9m5cycLFy4kOTmZ4uJi7r33Xtq1awdA9+7d67G0hGh4smUrRBP2+++/s3v3bhwcHLSvzp07A6Vbk2WCgoLKjbtjxw5CQkJo06YNjo6OPP7446Snp5OXl1fj6Z85cwYfHx9toQXo2rUrLi4unDlzRjvM19dXW2gBPD09SUtLA6BHjx6EhITQvXt3HnjgAT755BOuX79e84UghAmQYitEE5aTk8OoUaM4duyYzuvs2bMMHDhQ287e3l5nvPPnz3P33XcTGBjId999R1xcHCtWrABKT6DSN0tLS53PKpUKjUYDgLm5OVFRUWzdupWuXbvywQcf0KlTJxITE/WeQwhDkWIrRBNhZWVFSUmJzrDevXtz6tQpfH196dChg87r5gJ7o7i4ODQaDe+++y633XYbHTt2JCkpqdrp3axLly5cunSJS5cuaYedPn2ajIwMunbtWuN5U6lU9O/fn3nz5nH06FGsrKzYuHFjjccXwtik2ArRRPj6+nLw4EHOnz/P1atX0Wg0TJs2jWvXrvHwww9z6NAhEhIS2L59O0888USVhbJDhw6o1Wo++OAD/vrrL7744gvtiVM3Ti8nJ4edO3dy9erVCncvDx06lO7du/Poo49y5MgRfvvtN8aNG8egQYO49dZbazRfBw8eZNGiRRw+fJiLFy/y/fffc+XKFbp06VK7BSSEEUmxFaKJePHFFzE3N6dr1664ublx8eJFvLy82LdvHyUlJYSFhdG9e3dmzpyJi4sLZmaV/+/fo0cP3nvvPd566y1uueUWvvzySxYvXqzT5vbbb2fKlCk8+OCDuLm5lTvBCkq3SH/44QdatGjBwIEDGTp0KO3bt+ebb76p8Xw5OTmxZ88eRo4cSceOHXn99dd59913GTFiRM0XjhBGplLk/HkhhBDCoGTLVgghhDAwKbZCCCGEgUmxFUIIIQxMiq0QQghhYFJshRBCCAOTYiuEEEIYmBRbIYQQwsCk2AohhBAGJsVWCCGEMDAptkIIIYSBSbEVQgghDEyKrRBCCGFg/w9HROke1BbT7AAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_histories(histories, labels)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "233ba431",
+ "metadata": {},
+ "source": [
+ "#### Method 2: optimizing the step"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "4e0fc1c2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "New optimized step at iteration 1/20: 0.012285383759689142\n",
+ "New optimized step at iteration 2/20: 0.008447438941779167\n",
+ "New optimized step at iteration 3/20: 0.007844606075793771\n",
+ "New optimized step at iteration 4/20: 0.005843560327876751\n",
+ "New optimized step at iteration 5/20: 0.010251929595700982\n",
+ "New optimized step at iteration 6/20: 0.005841082475272156\n",
+ "New optimized step at iteration 7/20: 0.004631288000301592\n",
+ "New optimized step at iteration 8/20: 0.006817597467464319\n",
+ "New optimized step at iteration 9/20: 0.010226644173464028\n",
+ "New optimized step at iteration 10/20: 0.007818628910859757\n",
+ "New optimized step at iteration 11/20: 0.005826089136496147\n",
+ "New optimized step at iteration 12/20: 0.016243987144888304\n",
+ "New optimized step at iteration 13/20: 0.0012186965495374139\n",
+ "New optimized step at iteration 14/20: 0.0013670062367486478\n",
+ "New optimized step at iteration 15/20: 0.014667317020784512\n",
+ "New optimized step at iteration 16/20: 0.007361713849428013\n",
+ "New optimized step at iteration 17/20: 0.03387196372964355\n",
+ "New optimized step at iteration 18/20: 0.00385715363849674\n",
+ "New optimized step at iteration 19/20: 0.00441489412311609\n"
+ ]
+ }
+ ],
+ "source": [
+ "# restart\n",
+ "dbf_2 = DoubleBracketIteration(hamiltonian=deepcopy(h), mode=iterationtype, scheduling=DoubleBracketScheduling.hyperopt)\n",
+ "off_diagonal_norm_history = [dbf_2.off_diagonal_norm]\n",
+ "\n",
+ "# set the number of evolution steps\n",
+ "NSTEPS = 20\n",
+ "\n",
+ "# optimize first step\n",
+ "step = dbf_2.choose_step(\n",
+ " step_min = 1e-5,\n",
+ " step_max = 1,\n",
+ " space = hp.uniform,\n",
+ " optimizer = tpe,\n",
+ " max_evals = 500,\n",
+ ")\n",
+ "\n",
+ "for s in range(NSTEPS):\n",
+ " if s != 0:\n",
+ " step = dbf_2.choose_step(\n",
+ " step_min = 1e-5,\n",
+ " step_max = 1,\n",
+ " space = hp.uniform,\n",
+ " optimizer = tpe,\n",
+ " max_evals = 100,\n",
+ " )\n",
+ " print(f\"New optimized step at iteration {s}/{NSTEPS}: {step}\")\n",
+ " dbf_2(step=step)\n",
+ " off_diagonal_norm_history.append(dbf_2.off_diagonal_norm)\n",
+ "\n",
+ "histories.append(off_diagonal_norm_history)\n",
+ "labels.append(\"Optimizing step\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "40e31e97",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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nejXrx8dO3bk7rvv5rPPPuPq1auVPwlC2ABJtkLUY9euXWPYsGHs37/f5HXy5El69epl3M7V1dVkv4SEBG677TYiIiL44Ycf2LNnD/PmzQMKO1CZm729vcmyoigYDAYAtFotMTExrF+/nnbt2vHRRx/Rpk0b4uPjzR6HEJYiyVaIesLBwQG9Xm+yrmvXrhw5coSQkBBatWpl8ro+wRa3Z88eDAYD7777LjfeeCOtW7cmMTGxwuNdr23btpw/f57z588b1x09epS0tDTatWtX6e+mKAo9e/Zk2rRp7Nu3DwcHB1auXFnp/YWwNkm2QtQTISEh7Ny5k4SEBC5fvozBYGDChAlcuXKFe++9l127dnH69Gk2btzIQw89VG6ibNWqFTqdjo8++ogzZ87w9ddfGztOFT/etWvX2Lx5M5cvXy61ebl///507NiR+++/n7179/Lnn38yevRoevfuzQ033FCp77Vz507eeOMNdu/ezblz5/jxxx+5dOkSbdu2rdoJEsKKJNkKUU88//zzaLVa2rVrh6+vL+fOnSMwMJBt27ah1+sZOHAgHTt2ZNKkSXh5eaHRlP2/f6dOnXjvvfd466236NChA99++y2zZ8822eamm27i8ccf55577sHX17dEBysorJGuXr0ab29vevXqRf/+/WnRogVLly6t9Pfy8PBgy5YtDB06lNatW/Paa6/x7rvvMmTIkMqfHCGsTFGl/7wQQghhUVKzFUIIISxMkq0QQghhYZJshRBCCAuTZCuEEEJYmCRbIYQQwsIk2QohhBAWJslWCCGEsDBJtkIIIYSFSbIVQgghLEySrRBCCGFhkmyFEEIIC5NkK4QQQljY/wOkVbm4wjj4AgAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_histories(histories, labels)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0de78acd",
+ "metadata": {},
+ "source": [
+ "The hyperoptimization can lead to a faster convergence of the algorithm."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "baab0ab5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize_matrix(dbf_1.h.matrix)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "2bc9ac69",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize_matrix(dbf_2.h.matrix)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "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.10.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/falqon/README.md b/examples/falqon/README.md
new file mode 100644
index 000000000..feca37acb
--- /dev/null
+++ b/examples/falqon/README.md
@@ -0,0 +1,50 @@
+# Feedback-based ALgorithm for Quantum OptimizatioN - FALQON
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/falqon](https://github.com/qiboteam/qibo/tree/master/examples/falqon)
+
+Quantum Approximate Optimisation Algorithm (QAOA) is considered as one of the most important algorithms for optimisation in Quantum Computers, see [arXiv:1411.4028](https://arxiv.org/abs/1411.4028) by Farhi, Goldstone and Gutmann for more information.
+
+In this QAOA algorithm, the aim is to have a problem Hamiltonian H_P and a mixer Hamiltonian H_B. Then, starting with the ground state of H_B, the goal is to repeatedly apply e^(i H_P c) and e^(i H_B b), where c and b are tunable parameters. The values of such parameters are to be found via classical optimization
+
+In the FALQON algorithm, [arXiv:2103.08619](https://arxiv.org/abs/2103.08619) by Magann, Rudinger, Grace and Sarovan, they propose a similar although conceptually different algorithm. The proposal consists in evolving the initial state using the Schrödinger equation
+
+![schrodingerequation](images/schrodinger_equation.png)
+
+This equation satisfies that the expectation value of H_P is monotonically decreasing. This feature is used to create a Hamiltonian evolution with 1 layer using e^(i H_P c) and e^(i H_B b). In the first layer, b=0, and c is a parameter to be defined. Then, the quantity A = i[H_P, H_B] is measured. Its expectation value is then taken to be the parameter b for the next layer. As more layers are added, the
+approximation to the ground state of the problem Hamiltonian H_P is more and more accurate.
+
+![scheme](images/scheme.png)
+
+### Running the code
+
+This example contains just one file
+- `main.py` is the file where the algorithm is run. The main class `FALQON` is now introduced in `QIBO`
+
+The `FALQON` class behaves similarly to the `QAOA` one. It admits the following parameters:
+- `hamiltonian`: problem Hamiltonian
+ whose ground state is sought.
+- `mixer`: mixer Hamiltonian.
+ If ``None``, `qibo.hamiltonians.X` is used.
+- `solver`: solver used to apply the exponential operators.
+ Default solver is 'exp'.
+- `callbacks`: List of callbacks to calculate during evolution.
+- `accelerators`: Dictionary of devices to use for distributed
+ execution. See `qibo.core.distcircuit.DistributedCircuit`
+ for more details. This option is available only when ``hamiltonian``
+ is a `qibo.abstractions.hamiltonians.TrotterHamiltonian`.
+- `memory_device`: Name of device where the full state will be saved.
+ Relevant only for distributed execution (when ``accelerators`` is
+ given).
+
+When performing the execution of the problem, the following variables are to be set:
+
+- `delta_t`: initial guess for the time step. A too large delta_t will make the algorithm fail.
+- `max_layers`: maximum number of layers allowed for the FALQON.
+- `initial_state`: initial state vector of the FALQON.
+- `tol`: Tolerance of energy change. If not specified, no check is done.
+- `callback`: Called after each iteration for scipy optimizers.
+- `options`: a dictionary with options for the different optimizers.
+- `compile`: whether the TensorFlow graph should be compiled.
+- `processes`: number of processes when using the paralle BFGS method.
+
+The attached example provides an easy implementation of the FALQON method for a Heisenberg XXZ model.
diff --git a/examples/falqon/images/scheme.png b/examples/falqon/images/scheme.png
new file mode 100644
index 000000000..3e5785185
Binary files /dev/null and b/examples/falqon/images/scheme.png differ
diff --git a/examples/falqon/images/schrodinger_equation.png b/examples/falqon/images/schrodinger_equation.png
new file mode 100644
index 000000000..520ed1a9f
Binary files /dev/null and b/examples/falqon/images/schrodinger_equation.png differ
diff --git a/examples/falqon/main.py b/examples/falqon/main.py
new file mode 100644
index 000000000..b27c8af7a
--- /dev/null
+++ b/examples/falqon/main.py
@@ -0,0 +1,32 @@
+import argparse
+
+from qibo import hamiltonians, models
+
+
+def main(nqubits, delta_t=0.1, max_layers=100):
+ # create XXZ Hamiltonian for nqubits qubits
+ hamiltonian = hamiltonians.XXZ(nqubits)
+ # create FALQON model for this Hamiltonian
+ falqon = models.FALQON(hamiltonian)
+
+ best_energy, final_parameters = falqon.minimize(delta_t, max_layers)[:2]
+
+ print("The optimal energy found is", best_energy)
+
+ return best_energy, final_parameters
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=5, type=int, help="Number of qubits.")
+ parser.add_argument(
+ "--delta_t",
+ default=0.1,
+ type=float,
+ help="Optimization parameter, time step for the first layer",
+ )
+ parser.add_argument(
+ "--max_layers", default=100, type=int, help="Maximum number of layers"
+ )
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/grover/README.md b/examples/grover/README.md
new file mode 100644
index 000000000..98fc283ae
--- /dev/null
+++ b/examples/grover/README.md
@@ -0,0 +1,125 @@
+# A General Grover Model
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/grover](https://github.com/qiboteam/qibo/tree/master/examples/grover).
+
+The examples presented here provide information to run a general Grover model accessible as
+`from qibo.models import Grover`. This model allows to construct a general
+circuit to make use of generalized Grover models to search for states on an
+unstructured database. References can be checked at
+
+- For Grover's original search algorithm: [arXiv:quant-ph/9605043](https://arxiv.org/abs/quant-ph/9605043)
+- For the iterative version with unknown solutions:[arXiv:quant-ph/9605034](https://arxiv.org/abs/quant-ph/9605034)
+- For the Grover algorithm with any superposition:[arXiv:quant-ph/9712011](https://arxiv.org/abs/quant-ph/9712011)
+
+The arguments of the model are
+
+- oracle (`qibo.core.circuit.Circuit`): quantum circuit that flips
+ the sign using a Grover ancilla initialized with -X-H-. Grover ancilla
+ expected to be the last qubit of oracle circuit.
+- superposition_circuit (`qibo.core.circuit.Circuit`): quantum circuit that
+ takes an initial state to a superposition. Expected to use the first
+ set of qubits to store the relevant superposition.
+- initial_state_circuit (`qibo.core.circuit.Circuit`): quantum circuit
+ that initializes the state. If empty defaults to |000..00>
+- superposition_qubits (int): number of qubits that store the relevant superposition.
+ Leave empty if superposition does not use ancillas.
+- superposition_size (int): how many states are in a superposition.
+ Leave empty if its an equal superposition of quantum states.
+- number_solutions (int): number of expected solutions. Needed for normal Grover.
+ Leave empty for iterative version.
+- target_amplitude (float): absolute value of the amplitude of the target state. Only for
+ advanced use and known systems.
+- check (function): function that returns True if the solution has been
+ found. Required of iterative approach.
+ First argument should be the bitstring to check.
+- check_args (tuple): arguments needed for the check function.
+ The found bitstring not included.
+- iterative (bool): force the use of the iterative Grover
+
+We provide three different examples to run Grover with:
+
+### Example 1: standard Hadamard superposition and simple oracle.
+
+In this first example we show how to use a standard Grover search where the
+search space is an equally weighted superposition of quantum states and the oracle
+is simply defined through a layer of Hadamard gates. This example makes no use of
+ancilla qubits, so the command lines simplify greatly.
+1. First we create a superposition circuit. It is done by just initializing a 5-qubit circuit,
+and adding Hadamard gates
+```python
+superposition = Circuit(5)
+superposition.add([gates.H(i) for i in range(5)])
+```
+
+2. The next step is creating the oracle. In this case, we look for the states where all the
+qubits are in the `1` state
+```python
+oracle = Circuit(5 + 1)
+oracle.add(gates.X(5).controlled_by(*range(5)))
+```
+
+3. Now we create the Grover model.
+```python
+grover = Grover(oracle, superposition_circuit=superposition, number_solutions=1)
+solution, iterations = grover()
+```
+ In this case there are no ancilla qubits, and it is straightforward to see that there is only
+one possible solution, so we do not have to use any further information as the input for the circuit.
+
+### Example 2: standard Hadamard superposition and oracle with ancillas
+
+This second example is more complicated since we create an oracle function that makes use of ancilla qubits.
+We want to create a Grover model such that it searches all the basis states with `num_1` qubits in the
+`1` state. The search space is all the possibilities with `qubits` qubits. Those parameters
+are to be defined in the script. Functions `one_sum, sum_circuit, oracle` create the corresponding circuits for the oracle.
+The superposition circuit is the standard Hadamard one and is therefore not specified. However, note that since
+there are ancilla qubits in the oracle, we need to give the information of the size of the search space through the
+argument `superposition_qubits`. We also provide a `check` function counting the number of `1` in a bit string to be used
+in the iterative approach.
+
+In the non-iterative standard case we must write
+```python
+grover = Grover(oracle, superposition_qubits=qubits, number_solutions=int(binom(qubits, num_1)))
+solution, iterations = grover()
+```
+
+For the iterative case, the corresponding code is
+```python
+grover = Grover(oracle, superposition_qubits=qubits, check=check, check_args=(num_1,))
+solution, iterations = grover()
+```
+
+
+### Example 3: Ancillas for superposition and oracle, setting size of search space
+
+In this third example we create a Grover model with two components:
+- A superposition circuit creating all the elements in the computational basis with `num_1` qubits in the `1` state.
+- An oracle checking that the first `num_1` qubits are in the `1` state and does not care about any other qubit. This
+oracle is written in such a way that it needs ancilla qubits. This is not necessary in Qibo, it is done in this way for
+ illustrating the model.
+
+The joint action of both elements looks for the |1...10...0> element. The size of the search space is not 2^n in this case,
+but smaller.
+
+1. First, the superposition circuit is created
+```python
+superposition = superposition_circuit(qubits, num_1)
+```
+This circuit has `qubits` superposition qubits and some ancillas.
+
+2. Then the oracle is created
+```python
+oracle = oracle(qubits, num_1)
+or_circuit = Circuit(oracle.nqubits)
+or_circuit.add(oracle.on_qubits(*(list(range(qubits)) + [oracle.nqubits - 1] + list(range(qubits, oracle.nqubits - 1)))))
+```
+The `oracle` object has `qubits` qubits for the superposition, an ancilla qubit detecting whether the conditions are
+fulfilled or not, and some other auxiliary ancillas. In order to relabel the qubits so that the important ancilla is
+at the bottom of the circuit, we must add two more lines and use a feature provided by Qibo.
+
+Again, calling the Grover model is enough to execute the circuit. The binomial function allows to obtain the exact size
+of the search space.
+```python
+grover = Grover(or_circuit, superposition_circuit=superposition, superposition_qubits=qubits, number_solutions=1,
+ superposition_size=int(binomial(qubits, num_1)))
+```
diff --git a/examples/grover/example1.py b/examples/grover/example1.py
new file mode 100644
index 000000000..8299b245f
--- /dev/null
+++ b/examples/grover/example1.py
@@ -0,0 +1,39 @@
+import argparse
+
+from qibo import Circuit, gates
+from qibo.models.grover import Grover
+
+
+def main(nqubits):
+ """Create an oracle, find state |11...11> for a number of qubits.
+
+ Args:
+ nqubits (int): number of qubits
+
+ Returns:
+ solution (str): found string
+ iterations (int): number of iterations needed
+ """
+ superposition = Circuit(nqubits)
+ superposition.add([gates.H(i) for i in range(nqubits)])
+
+ oracle = Circuit(nqubits + 1)
+ oracle.add(gates.X(nqubits).controlled_by(*range(nqubits)))
+ # Create superoposition circuit: Full superposition over the selected number qubits.
+
+ # Generate and execute Grover class
+ grover = Grover(oracle, superposition_circuit=superposition, number_solutions=1)
+
+ solution, iterations = grover()
+
+ print("The solution is", solution)
+ print("Number of iterations needed:", iterations)
+
+ return solution, iterations
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=10, type=int, help="Number of qubits.")
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/grover/example2.py b/examples/grover/example2.py
new file mode 100644
index 000000000..998f8d862
--- /dev/null
+++ b/examples/grover/example2.py
@@ -0,0 +1,129 @@
+import argparse
+
+import numpy as np
+from scipy.special import binom
+
+from qibo import Circuit, gates
+from qibo.models.grover import Grover
+
+
+def one_sum(sum_qubits):
+ c = Circuit(sum_qubits + 1)
+
+ for q in range(sum_qubits, 0, -1):
+ c.add(gates.X(q).controlled_by(*range(0, q)))
+
+ return c
+
+
+def sum_circuit(qubits):
+ sum_qubits = int(np.ceil(np.log2(qubits))) + 1
+ sum_circuit = Circuit(qubits + sum_qubits)
+ sum_circuit.add(gates.X(qubits).controlled_by(0))
+ sum_circuit.add(gates.X(qubits).controlled_by(1))
+ sum_circuit.add(gates.X(qubits + 1).controlled_by(*[0, 1]))
+
+ for qub in range(2, qubits):
+ sum_circuit.add(
+ one_sum(sum_qubits).on_qubits(
+ *([qub] + list(range(qubits, qubits + sum_qubits)))
+ )
+ )
+
+ return sum_circuit
+
+
+def oracle(qubits, num_1):
+ sum = sum_circuit(qubits)
+ oracle = Circuit(sum.nqubits + 1)
+ oracle.add(sum.on_qubits(*range(sum.nqubits)))
+
+ booleans = np.binary_repr(num_1, int(np.ceil(np.log2(qubits)) + 1))
+
+ for i, b in enumerate(booleans[::-1]):
+ if b == "0":
+ oracle.add(gates.X(qubits + i))
+
+ oracle.add(gates.X(sum.nqubits).controlled_by(*range(qubits, sum.nqubits)))
+
+ for i, b in enumerate(booleans[::-1]):
+ if b == "0":
+ oracle.add(gates.X(qubits + i))
+
+ oracle.add(sum.invert().on_qubits(*range(sum.nqubits)))
+
+ return oracle
+
+
+def check(instance, num_1):
+ res = instance.count("1") == num_1
+ return res
+
+
+def main(nqubits, num_1, iterative=False):
+ """Create an oracle, find the states with some 1's among all the states with a fixed number of qubits
+ Args:
+ nqubits (int): number of qubits
+ num_1 (int): number of 1's to find
+ iterative (bool): use iterative model
+
+ Returns:
+ solution (str): found string
+ iterations (int): number of iterations needed
+ """
+ oracle_circuit = oracle(nqubits, num_1)
+
+ #################################################################
+ ###################### NON ITERATIVE MODEL ######################
+ #################################################################
+
+ if not iterative:
+ grover = Grover(
+ oracle_circuit,
+ superposition_qubits=nqubits,
+ number_solutions=int(binom(nqubits, num_1)),
+ )
+
+ solution, iterations = grover()
+
+ print("\nNON ITERATIVE MODEL: \n")
+
+ print("The solution is", solution)
+ print("Number of iterations needed:", iterations)
+ print(
+ "\nFound number of solutions: ",
+ len(solution),
+ "\nTheoretical number of solutions:",
+ int(binom(nqubits, num_1)),
+ )
+
+ return solution, iterations
+
+ #################################################################
+ ######################## ITERATIVE MODEL ########################
+ #################################################################
+
+ print("\nITERATIVE MODEL: \n")
+
+ if iterative:
+ grover = Grover(
+ oracle_circuit,
+ superposition_qubits=nqubits,
+ check=check,
+ check_args=(num_1,),
+ )
+ solution, iterations = grover()
+
+ print("Found solution:", solution)
+ print("Number of iterations needed:", iterations)
+
+ return solution, iterations
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=10, type=int, help="Number of qubits.")
+ parser.add_argument("--num_1", default=2, type=int, help="Number of 1's to find.")
+ parser.add_argument("--iterative", action="store_true", help="Use iterative model")
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/grover/example3.py b/examples/grover/example3.py
new file mode 100644
index 000000000..2ab4d6675
--- /dev/null
+++ b/examples/grover/example3.py
@@ -0,0 +1,159 @@
+import argparse
+
+import numpy as np
+from scipy.special import binom as binomial
+
+from qibo import Circuit, gates
+from qibo.models import Grover
+
+
+def set_ancillas_to_num(ancillas, num):
+ """Set a quantum register to a specific number."""
+ ind = 0
+ for i in reversed(bin(num)[2:]):
+ if int(i) == 1:
+ yield gates.X(ancillas[ind])
+ ind += 1
+
+
+def add_negates_for_check(ancillas, num):
+ """Adds the negates needed for control-on-zero."""
+ ind = 0
+ for i in reversed(bin(num)[2:]):
+ if int(i) == 0:
+ yield gates.X(ancillas[ind])
+ ind += 1
+ for i in range(len(bin(num)[2:]), len(ancillas)):
+ yield gates.X(ancillas[i])
+
+
+def sub_one(ancillas, controls):
+ """Subtract 1 bit by bit."""
+ a = ancillas
+ yield gates.X(a[0]).controlled_by(*controls)
+ for i in range(1, len(a)):
+ controls.append(a[i - 1])
+ yield gates.X(a[i]).controlled_by(*controls)
+
+
+def superposition_probabilities(n, r):
+ """Computes the probabilities to set the initial superposition."""
+
+ def split_weights(n, r):
+ """Auxiliary function that gets the required binomials."""
+ v0 = binomial(n - 1, r)
+ v1 = binomial(n - 1, r - 1)
+ return v0 / (v0 + v1), v1 / (v0 + v1)
+
+ L = []
+ for i in range(n):
+ for j in range(min(i, r - 1), -1, -1):
+ if n - i >= r - j:
+ L.append([n - i, r - j, split_weights(n - i, r - j)])
+ return L
+
+
+def superposition_circuit(n, r):
+ """Creates an equal quantum superposition over the column choices."""
+ n_anc = int(np.ceil(np.log2(r + 1)))
+ ancillas = [i for i in range(n, n + n_anc)]
+ c = Circuit(n + n_anc)
+ c.add(set_ancillas_to_num(ancillas, r))
+ tmp = n
+ L = superposition_probabilities(n, r)
+ for i in L:
+ if tmp != i[0]:
+ c.add(sub_one(ancillas, [n - tmp]))
+ tmp = i[0]
+
+ if i[2] == (0, 1):
+ c.add(add_negates_for_check(ancillas, i[1]))
+ c.add(gates.X(n - i[0]).controlled_by(*ancillas))
+ c.add(add_negates_for_check(ancillas, i[1]))
+ else:
+ if i[0] != n:
+ c.add(add_negates_for_check(ancillas, i[1]))
+ c.add(
+ gates.RY(
+ n - i[0], float(2 * np.arccos(np.sqrt(i[2][0])))
+ ).controlled_by(*ancillas)
+ )
+ c.add(add_negates_for_check(ancillas, i[1]))
+ else:
+ c.add(gates.RY(0, float(2 * np.arccos(np.sqrt(i[2][0])))))
+ c.add(sub_one(ancillas, [n - 1]))
+ return c
+
+
+def oracle(n, s):
+ """Oracle checks whether the first s terms are 1."""
+ if s > 2:
+ n_anc = s - 2
+ oracle = Circuit(n + n_anc + 1)
+ oracle_1 = Circuit(n + n_anc + 1)
+ oracle_1.add(gates.X(n + 1).controlled_by(*(0, 1)))
+ for q in range(2, s - 1):
+ oracle_1.add(gates.X(n + q).controlled_by(*(q, n + q - 1)))
+
+ oracle.add(oracle_1.on_qubits(*(range(n + n_anc + 1))))
+ oracle.add(gates.X(n).controlled_by(*(s - 1, n + n_anc)))
+ oracle.add(oracle_1.invert().on_qubits(*(range(n + n_anc + 1))))
+
+ return oracle
+
+ else:
+ oracle = Circuit(n + int(np.ceil(np.log2(s + 1))) + 1)
+ oracle.add(gates.X(n).controlled_by(*range(s)))
+
+ return oracle
+
+
+def main(nqubits, num_1):
+ """Creates a superposition circuit that finds all states with num_1 1's in a
+ fixed number of qubits, then the oracle find that state where all the 1's
+ are at the beginning of the bitstring. This oracle has got ancillas
+
+ Args:
+ nqubits (int): number of qubits
+ num_1 (int): number of 1's to find
+
+ Returns:
+ solution (str): found string
+ iterations (int): number of iterations needed
+ """
+ superposition = superposition_circuit(nqubits, num_1)
+
+ oracle_circuit = oracle(nqubits, num_1)
+ or_circuit = Circuit(oracle_circuit.nqubits)
+ or_circuit.add(
+ oracle_circuit.on_qubits(
+ *(
+ list(range(nqubits))
+ + [oracle_circuit.nqubits - 1]
+ + list(range(nqubits, oracle_circuit.nqubits - 1))
+ )
+ )
+ )
+
+ grover = Grover(
+ or_circuit,
+ superposition_circuit=superposition,
+ superposition_qubits=nqubits,
+ number_solutions=1,
+ superposition_size=int(binomial(nqubits, num_1)),
+ )
+
+ solution, iterations = grover()
+
+ print("The solution is", solution)
+ print("Number of iterations needed:", iterations)
+
+ return solution, iterations
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=10, type=int, help="Number of qubits.")
+ parser.add_argument("--num_1", default=2, type=int, help="Number of 1's to find.")
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/grover3sat/README.md b/examples/grover3sat/README.md
new file mode 100644
index 000000000..50066b717
--- /dev/null
+++ b/examples/grover3sat/README.md
@@ -0,0 +1,80 @@
+# Grover's Algorithm for solving Satisfiability Problems
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/grover3sat](https://github.com/qiboteam/qibo/tree/master/examples/grover3sat)
+
+## Introduction
+
+Grover's Algorithm is an example of the advantages a quantum computer has over a classical computer in the task of searching databases. This program applies a Grover search on a Satisfiability problem, more precisely an Exact Cover instance of a 3SAT problem.
+
+
+An Exact Cover instance of a 3SAT problem is characterized by a set of clauses containing 3 bits that are considered satisfied if one of them is in position 1, while the other remain at 0. The solution of this instance is bitstring that fulfills all the clauses at the same time.
+
+## Grover's search algorithm
+
+The algorithm proposed by Grover [arXiv:quant-ph/9605043](https://arxiv.org/abs/quant-ph/9605043) achieves a quadratic speed-up on a brute-force search of this satisfiability problem.
+
+This program builds the necessary parts of the algorithm in order to simulate this algorithm. Crucially, Grover's algorithm requires an oracle that is problem dependent, which changes the sign of the solution of the problem.
+
+### Oracle
+
+The Exact Cover oracle is built using the same number of ancillary qubits as clauses are in the system, as to have a way to track that all clauses are satisfied. For each clause in the system, a CNOT gate from each of the qubits to its ancilla, followed by a multi-Toffoli gates controlled by the three qubits targeting the ancilla, activates the ancilla only if the clause is satisfied.
+
+A multi-Toffoli gate on all "clause" ancilla targeting the Grover ancilla (qubit initialized by an X gate followed by a Hadamard gate) changes the sign of the solution of the system.
+
+Then, all ancillas must be uncomputed for the oracle to be complete. This is a crucial step as these ancillas have to be decoupled from the system to be used in the next iteration of the Grover algorithm.
+
+### Diffusion transform
+
+After the sign change of the solution states, an inversion about average amplifies their amplitude. This is at the core of Grover's search algorithm. This operator, also known as Diffusion transform, can be constructed by a set of Hadamard gates on all qubits, a multi-qubit gate that changes the sign of the |0000...00> state, and another set of H gates.
+
+### Building the algorithm
+
+The quantum register is started with Hadamard gates on all qubits encoding the problem, and an X gate followed by a Hadamard gate on the Grover ancilla, used to change the sign of the solution.
+
+Then the oracle and diffusion transform are applied (π/4)sqrt(N/M) times, where N is the total number of possible outcomes and M is the number of solutions.
+
+After this has been applied, measuring the quantum registers outputs the solution of the problem.
+
+The quantum circuit for the Grover search algorithm with any oracle takes the form:
+
+![grovercircuit](images/grover-circuit-image.png)
+
+## How to run the example?
+
+Run the file `main.py` from the console to find the solution of an instance of 10 qubits.
+
+Adding the argument `--nqubits` (int) and `--instance` (int) allows for different Exact Cover instances with different number of qubits.
+
+The program returns:
+
+- Number of qubits encoding the solution.
+- Total number of qubits to define the problem. Including ancillary qubits used in the oracle.
+- Most common bitstring measured after all iterations.
+- Target solution of the problem (if included).
+
+Initially supported instances are of [4, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30] qubits, with 10 different instances for qubits 8 and up.
+
+The functions used in this example, including gate by gate decompositions of both the oracle and diffusion transform are included in `functions.py`.
+
+## Create your own instances
+
+An example of an instance for 4 qubits reads:
+
+```text
+ 4 3 1
+0 1 0 0
+ 1 2 3
+ 2 3 4
+ 1 2 4
+```
+
+The first line includes:
+- number of qubits
+- number of clauses
+- number of 1's in the solution
+
+The second line is the solution of the instance.
+
+The following lines correspond to the three qubits present in each clause.
+
+Should the solution not be known, leave an empty line in place of the solution as well as remove the number of 1's in the solution.
diff --git a/examples/grover3sat/functions.py b/examples/grover3sat/functions.py
new file mode 100644
index 000000000..74adb146a
--- /dev/null
+++ b/examples/grover3sat/functions.py
@@ -0,0 +1,131 @@
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+def read_file(file_name, instance):
+ """Collect data from .txt file that characterizes the problem instance.
+ Args:
+ file_name (str): name of the file that contains the instance information.
+ instance (str): number of intance to use.
+
+ Returns:
+ control (list): important parameters of the instance.
+ [number of qubits, number of clauses, number of ones in the solution]
+ solution (list): list of the correct outputs of the instance for testing.
+ clauses (list): list of all clauses, with the qubits each clause acts upon.
+ """
+ file = open(f"../data3sat/{file_name}bit/n{file_name}i{instance}.txt")
+ control = list(map(int, file.readline().split()))
+ solution = list(map(str, file.readline().split()))
+ clauses = [list(map(int, file.readline().split())) for _ in range(control[1])]
+ return control, solution, clauses
+
+
+def create_qc(qubits, clause_num):
+ """Create the quantum circuit necessary to solve the problem.
+ Args:
+ qubits (int): qubits needed to encode the problem.
+ clause_num (int): number of clauses of the problem.
+
+ Returns:
+ q (list): quantum register that encodes the problem.
+ c (list): quantum register that that records the satisfies clauses.
+ ancilla (int): Grover ancillary qubit.
+ circuit (Circuit): quantum circuit where the gates will be allocated.
+ """
+ q = [i for i in range(qubits)]
+ ancilla = qubits
+ c = [i + qubits + 1 for i in range(clause_num)]
+ circuit = Circuit(qubits + clause_num + 1)
+ return q, c, ancilla, circuit
+
+
+def start_grover(q, ancilla):
+ """Generator that performs the starting step in Grover's search algorithm.
+ Args:
+ q (list): quantum register that encodes the problem.
+ ancilla (int): Grover ancillary qubit.
+
+ Returns:
+ quantum gate generator for the first step of Grover.
+ """
+ yield gates.X(ancilla)
+ yield gates.H(ancilla)
+ for i in q:
+ yield gates.H(i)
+
+
+def oracle(q, c, ancilla, clauses):
+ """Generator that acts as the oracle for a 3SAT problem. Changes the sign of the amplitude of the quantum
+ states that encode the solution.
+ Args:
+ q (list): quantum register that encodes the problem.
+ c (list): quantum register that that records the satisfies clauses.
+ ancilla (int): Grover ancillary qubit. Used to change the sign of the
+ correct amplitudes.
+ clauses (list): list of all clauses, with the qubits each clause acts upon.
+
+ Returns:
+ quantum gate generator for the 3SAT oracle
+ """
+ k = 0
+ for clause in clauses:
+ yield gates.CNOT(q[clause[0] - 1], c[k])
+ yield gates.CNOT(q[clause[1] - 1], c[k])
+ yield gates.CNOT(q[clause[2] - 1], c[k])
+ yield gates.X(c[k]).controlled_by(
+ q[clause[0] - 1], q[clause[1] - 1], q[clause[2] - 1]
+ )
+ k += 1
+ yield gates.X(ancilla).controlled_by(*c)
+ k = 0
+ for clause in clauses:
+ yield gates.CNOT(q[clause[0] - 1], c[k])
+ yield gates.CNOT(q[clause[1] - 1], c[k])
+ yield gates.CNOT(q[clause[2] - 1], c[k])
+ yield gates.X(c[k]).controlled_by(
+ q[clause[0] - 1], q[clause[1] - 1], q[clause[2] - 1]
+ )
+ k += 1
+
+
+def diffusion(q):
+ """Generator that performs the inversion over the average step in Grover's search algorithm.
+ Args:
+ q (list): quantum register that encodes the problem.
+
+ Returns:
+ quantum gate geenrator that applies the diffusion step.
+ """
+ for i in q:
+ yield gates.H(i)
+ yield gates.X(i)
+ yield gates.H(q[0])
+ yield gates.X(q[0]).controlled_by(*q[1 : len(q)])
+ yield gates.H(q[0])
+ for i in q:
+ yield gates.X(i)
+ yield gates.H(i)
+
+
+def grover(circuit, q, c, ancilla, clauses, steps):
+ """Generator that performs the inversion over the average step in Grover's search algorithm.
+ Args:
+ circuit (Circuit): empty quantum circuit onto which the grover algorithm is performed
+ q (list): quantum register that encodes the problem.
+ c (list): quantum register that that records the satisfies clauses.
+ ancilla (int): Grover ancillary qubit.
+ clauses (list): list of all clauses, with the qubits each clause acts upon.
+ steps (int): number of times the oracle+diffuser operators have to be applied
+ in order to find the solution. Grover's search algorihtm dictates O(sqrt(2**qubits)).
+
+ Returns:
+ circuit (Circuit): circuit with the full grover algorithm applied.
+ """
+ circuit.add(start_grover(q, ancilla))
+ for i in range(steps):
+ circuit.add(oracle(q, c, ancilla, clauses))
+ circuit.add(diffusion(q))
+ circuit.add(gates.M(*(q), register_name="result"))
+ return circuit
diff --git a/examples/grover3sat/images/grover-circuit-image.png b/examples/grover3sat/images/grover-circuit-image.png
new file mode 100644
index 000000000..ca45f1511
Binary files /dev/null and b/examples/grover3sat/images/grover-circuit-image.png differ
diff --git a/examples/grover3sat/main.py b/examples/grover3sat/main.py
new file mode 100644
index 000000000..c1c338690
--- /dev/null
+++ b/examples/grover3sat/main.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+import argparse
+
+import functions
+import numpy as np
+
+
+def main(nqubits, instance):
+ """Grover search for the instance defined by the file_name.
+ Args:
+ nqubits (int): number of qubits for the file that contains the information of an Exact Cover instance.
+ instance (int): intance used for the desired number of qubits.
+
+ Returns:
+ result of the Grover search and comparison with the expected solution if given.
+ """
+ control, solution, clauses = functions.read_file(nqubits, instance)
+ qubits = control[0]
+ clauses_num = control[1]
+ steps = int((np.pi / 4) * np.sqrt(2**qubits))
+ print(f"Qubits encoding the solution: {qubits}\n")
+ print(f"Total number of qubits used: {qubits + clauses_num + 1}\n")
+ q, c, ancilla, circuit = functions.create_qc(qubits, clauses_num)
+ circuit = functions.grover(circuit, q, c, ancilla, clauses, steps)
+ result = circuit(nshots=100)
+ frequencies = result.frequencies(binary=True, registers=False)
+ most_common_bitstring = frequencies.most_common(1)[0][0]
+ print(f"Most common bitstring: {most_common_bitstring}\n")
+ if solution:
+ print(f"Exact cover solution: {''.join(solution)}\n")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--nqubits", default=10, type=int)
+ parser.add_argument("--instance", default=1, type=int)
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/hash-grover/README.md b/examples/hash-grover/README.md
new file mode 100644
index 000000000..70d3facf5
--- /dev/null
+++ b/examples/hash-grover/README.md
@@ -0,0 +1,81 @@
+# Grover's Algorithm for solving a Toy Sponge Hash function
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/hash-grover](https://github.com/qiboteam/qibo/tree/master/examples/hash-grover)
+
+## Introduction
+
+Grover's Algorithm is an example of the advantages a quantum computer has over a classical computer in the task of searching databases. This program applies a Grover search on a brute force check of preimages for a Toy Sponge Hash construction.
+
+Symmetric cryptographic primitives, such as hash functions, are believed to be quantum resistant. The security of hash functions is measured in terms of resistance against collision finding, preimage and second preimage finding, and their multi-target variants. Therefore the first approach is to gain the quadratic speed-up from Grover's algorithm. In the following we present an explicit construction for a Toy Sponge Hash function based on the Chacha family of permutations in order to study the scaling of Addition-Rotation-eXclusive (ARX) hash functions.
+
+## Grover's search algorithm
+
+The algorithm proposed by Grover [arXiv:quant-ph/9605043](https://arxiv.org/abs/quant-ph/9605043) achieves a quadratic speed-up on a brute-force search of this preimage finding problem.
+
+This program builds the necessary parts of the algorithm in order to simulate this algorithm. Crucially, Grover's algorithm requires an oracle that is problem dependent, which changes the sign of the solution of the problem. More details can be found on the original paper but the outline goes a follows.
+
+### Oracle
+
+The Grover Oracle needs to apply the Toy Sponge Hash construction in a reversible way.
+
+The Chacha permutation [chacah-20080120](https://cr.yp.to/chacha/chacha-20080120.pdf) used as the base for the Toy Sponge Hash construction is built on top of a QuarterRound module. As a a quantum circuit this Quarter Round can be constructed using a quantum adder modulo `2^n`, CNOT gates between registers and a qubit shuffling, represented as a gate with a circle-arrow. This shuffle is achived as a qubit relabelling and does not incur any quantum cost. This circuit can be represented as
+
+![quarter-round](images/quarter-round.png)
+
+where any version of a quantum adder can be used. For this circuit we use a qubit-efficient adder put forward in [arXiv:quant-ph/0410184](https://arxiv.org/abs/quant-ph/0410184) where only one ancilla is necessary and is reused throughout the circuit. For the specific case of an adder modulo `2^n` the quantum circuit used can be seen in the following image.
+
+![addermod2n](images/adder-mod2n.png)
+
+With the Quarter Round constructed we can arrange the Chacha permutation in a reversible way through a quantum circuit by arranging them as needed.
+
+![chacha-perm](images/chacha-perm.png)
+
+The full oracle requires of a classical wire that stores the first, deterministic permutation. Also, once the Chacha permutation is performed and the amplitude for the solution state is flipped, the permutation must be undone as the diffusion transform has to be applied on the message space.
+
+![sponge-oracle](images/sponge-oracle.png)
+
+### Diffusion transform
+
+After the sign change of the solution states, an inversion about average amplifies their amplitude. This is at the core of Grover's search algorithm. This operator, also known as Diffusion transform, can be constructed by a set of Hadamard gates on all qubits, a multi-qubit gate that changes the sign of the |0000...00> state, and another set of H gates.
+
+### Building the algorithm
+
+The quantum register is started with Hadamard gates on all qubits encoding the problem, and an X gate followed by a Hadamard gate on the Grover ancilla, used to change the sign of the solution.
+
+Should the total number of preimages be known, the oracle and diffusion transform are applied (π/4)sqrt(N/M) times, where N is the total number of possible outcomes and M is the number of preimages.
+
+If that is not the case, the code follows the algorithm put forward in [arXiv:quant-ph/9605034](https://arxiv.org/abs/quant-ph/9605034).
+
+- The circuit is initialized with `m = 1` and `lambda = 6/5`.
+- Choose `j` randomly as an integer equal or lower than `m`.
+- Apply `j` Grover steps and measure.
+- If the measured state outputs the desired hash: `exit`.
+- If not, set `m = min(lamda*m, sqrt(N))` and repeat from the second step.
+
+The quantum circuit for the Grover search algorithm with any oracle takes the form:
+
+![grovercircuit](images/grover-circuit-image.png)
+
+## How to run the example?
+
+Run the file `main.py` from the console to find a preimage for hash value '10100011' (163) using 18 qubits.
+
+Changing the argument `--hash` (int) allows find preimages of different hash values. Keep the hash value equal or below 8 bits.
+
+Changing the argument `--bits` (int) allows to fine tune the maximum number of bits of the hash function. If less than needed, the minimum amount of bits required will be used.
+
+Should the number of preimages be known, please add them in `--collisions` (int), which is set to `None` by default.
+
+Some examples for hash values with known collisions:
+
+- 187 (1 collision)
+- 163 (2 collisions)
+- 133 (3 collisions)
+- 113 (4 collisions)
+
+The program returns:
+
+- If a solution has been found and the value of the preimage.
+- Total number of function calls required.
+
+The functions used in this example, including gate by gate decompositions of both the oracle and diffusion transform are included in `functions.py`.
diff --git a/examples/hash-grover/functions.py b/examples/hash-grover/functions.py
new file mode 100644
index 000000000..12c614214
--- /dev/null
+++ b/examples/hash-grover/functions.py
@@ -0,0 +1,516 @@
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+def n_mCNOT(controls, target, work):
+ """Decomposition of a multi-controlled NOT gate with m qubits of work space.
+ Args:
+ controls (list): quantum register used as a control for the gate.
+ target (int): qubit where the NOT gate is applied.
+ work (list): quantum register used as work space.
+
+ Returns:
+ quantum gate generator for the multi-controlled NOT gate with m qubits of work space.
+ """
+ i = 0
+ yield gates.TOFFOLI(controls[-1], work[-1], target)
+ for i in range(1, len(controls) - 2):
+ yield gates.TOFFOLI(controls[-1 - i], work[-1 - i], work[-1 - i + 1])
+ yield gates.TOFFOLI(controls[0], controls[1], work[-1 - i])
+ for i in reversed(range(1, len(controls) - 2)):
+ yield gates.TOFFOLI(controls[-1 - i], work[-1 - i], work[-1 - i + 1])
+ yield gates.TOFFOLI(controls[-1], work[-1], target)
+ for i in range(1, len(controls) - 2):
+ yield gates.TOFFOLI(controls[-1 - i], work[-1 - i], work[-1 - i + 1])
+ yield gates.TOFFOLI(controls[0], controls[1], work[-1 - i])
+ for i in reversed(range(1, len(controls) - 2)):
+ yield gates.TOFFOLI(controls[-1 - i], work[-1 - i], work[-1 - i + 1])
+
+
+def n_2CNOT(controls, target, work):
+ """Decomposition up to Toffoli gates of a multi-controlled NOT gate with one work qubit.
+ Args:
+ controls (list): quantum register used as a control for the gate.
+ target (int): qubit where the NOT gate is applied.
+ work (int): qubit used as work space.
+
+ Returns:
+ quantum gate generator for the multi-controlled NOT gate with one work qubit.
+ """
+ m1 = int(((len(controls) + 2) / 2) + 0.5)
+ m2 = int(len(controls) + 2 - m1 - 1)
+ yield n_mCNOT(controls[0:m1], work, controls[m1 : len(controls)] + [target])
+ yield n_mCNOT((controls + [work])[m1 : m1 + m2], target, controls[0:m1])
+ yield n_mCNOT(controls[0:m1], work, controls[m1 : len(controls)] + [target])
+ yield n_mCNOT((controls + [work])[m1 : m1 + m2], target, controls[0:m1])
+
+
+def adder_mod2n(a, b, x):
+ """Quantum circuit for the adder modulo 2^n operation.
+ Args:
+ a (list): quantum register for the first number to be added.
+ b (list): quantum register for the second number to be added, will be replaced by solution.
+ x (int): ancillary qubit needed for the adder circuit.
+
+ Returns:
+ quantum gate generator that applies the quantum gates for addition modulo 2^n.
+ """
+ n = int(len(a))
+ for i in range(n - 2, -1, -1):
+ yield gates.CNOT(a[i], b[i])
+ yield gates.CNOT(a[n - 2], x)
+ yield gates.TOFFOLI(a[n - 1], b[n - 1], x)
+ yield gates.CNOT(a[n - 3], a[n - 2])
+ yield gates.TOFFOLI(x, b[n - 2], a[n - 2])
+ yield gates.CNOT(a[n - 4], a[n - 3])
+ for i in range(n - 3, 1, -1):
+ yield gates.TOFFOLI(a[i + 1], b[i], a[i])
+ yield gates.CNOT(a[i - 2], a[i - 1])
+ yield gates.TOFFOLI(a[2], b[1], a[1])
+ for i in range(n - 2, 0, -1):
+ yield gates.X(b[i])
+ yield gates.CNOT(x, b[n - 2])
+ for i in range(n - 3, -1, -1):
+ yield gates.CNOT(a[i + 1], b[i])
+ yield gates.TOFFOLI(a[2], b[1], a[1])
+ for i in range(2, n - 2):
+ yield gates.TOFFOLI(a[i + 1], b[i], a[i])
+ yield gates.CNOT(a[i - 2], a[i - 1])
+ yield gates.X(b[i - 1])
+ yield gates.TOFFOLI(x, b[n - 2], a[n - 2])
+ yield gates.CNOT(a[n - 4], a[n - 3])
+ yield gates.X(b[n - 3])
+ yield gates.TOFFOLI(a[n - 1], b[n - 1], x)
+ yield gates.CNOT(a[n - 3], a[n - 2])
+ yield gates.X(b[n - 2])
+ yield gates.CNOT(a[n - 2], x)
+ for i in range(n - 1, -1, -1):
+ yield gates.CNOT(a[i], b[i])
+
+
+def r_adder_mod2n(a, b, x):
+ """Reversed quantum circuit for the adder modulo 2^n operation.
+ Args:
+ a (list): quantum register for the first number to be added.
+ b (list): quantum register for result of the addition.
+ x (int): ancillary qubit needed for the adder circuit.
+
+ Returns:
+ quantum gate generator that applies the quantum gates for addition modulo 2^n in reverse.
+ """
+ n = int(len(a))
+ for i in reversed(range(n - 1, -1, -1)):
+ yield gates.CNOT(a[i], b[i])
+ yield gates.CNOT(a[n - 2], x)
+ yield gates.X(b[n - 2])
+ yield gates.CNOT(a[n - 3], a[n - 2])
+ yield gates.TOFFOLI(a[n - 1], b[n - 1], x)
+ yield gates.X(b[n - 3])
+ yield gates.CNOT(a[n - 4], a[n - 3])
+ yield gates.TOFFOLI(x, b[n - 2], a[n - 2])
+ for i in reversed(range(2, n - 2)):
+ yield gates.X(b[i - 1])
+ yield gates.CNOT(a[i - 2], a[i - 1])
+ yield gates.TOFFOLI(a[i + 1], b[i], a[i])
+ yield gates.TOFFOLI(a[2], b[1], a[1])
+ for i in reversed(range(n - 3, -1, -1)):
+ yield gates.CNOT(a[i + 1], b[i])
+ yield gates.CNOT(x, b[n - 2])
+ for i in reversed(range(n - 2, 0, -1)):
+ yield gates.X(b[i])
+ yield gates.TOFFOLI(a[2], b[1], a[1])
+ for i in reversed(range(n - 3, 1, -1)):
+ yield gates.CNOT(a[i - 2], a[i - 1])
+ yield gates.TOFFOLI(a[i + 1], b[i], a[i])
+ yield gates.CNOT(a[n - 4], a[n - 3])
+ yield gates.TOFFOLI(x, b[n - 2], a[n - 2])
+ yield gates.CNOT(a[n - 3], a[n - 2])
+ yield gates.TOFFOLI(a[n - 1], b[n - 1], x)
+ yield gates.CNOT(a[n - 2], x)
+ for i in reversed(range(n - 2, -1, -1)):
+ yield gates.CNOT(a[i], b[i])
+
+
+def qr(a, b, x, rot):
+ """Circuit for the quantum quarter round for the toy Chacha permutation.
+ Args:
+ a (list): quantum register of a site in the permutation matrix.
+ b (list): quantum register of a site in the permutation matrix.
+ x (int): ancillary qubit needed for the adder circuit.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ quantum gate generator that applies the quantum gates for the Chacha quarter round.
+ """
+ n = int(len(a))
+ for r in range(len(rot)):
+ yield adder_mod2n(b, a, x)
+ for i in range(n):
+ yield gates.CNOT(a[i], b[i])
+ for i in range(rot[r]):
+ b = b[1:] + [b[0]]
+
+
+def r_qr(a, b, x, rot):
+ """Reverse circuit for the quantum quarter round for the toy Chacha permutation.
+ Args:
+ a (list): quantum register of a site in the permutation matrix.
+ b (list): quantum register of a site in the permutation matrix.
+ x (int): ancillary qubit needed for the adder circuit.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ quantum gate generator that applies the reversed quantum gates for the Chacha quarter round.
+ """
+ n = int(len(a))
+ for r in reversed(range(len(rot))):
+ for i in range(rot[r]):
+ b = [b[-1]] + b[:-1]
+ for i in reversed(range(n)):
+ yield gates.CNOT(a[i], b[i])
+ yield r_adder_mod2n(b, a, x)
+
+
+def diffusion(q, work):
+ """Generator that performs the inversion over the average step in Grover's search algorithm.
+ Args:
+ q (list): quantum register that encodes the problem.
+
+ Returns:
+ quantum gate generator that applies the diffusion step.
+ """
+ for i in q:
+ yield gates.H(i)
+ yield gates.X(i)
+ yield gates.H(q[0])
+ yield n_2CNOT(q[1:], q[0], work)
+ yield gates.H(q[0])
+ for i in q:
+ yield gates.X(i)
+ yield gates.H(i)
+
+
+def start_grover(q, ancilla):
+ """Generator that performs the starting step in Grover's search algorithm.
+ Args:
+ q (list): quantum register that encodes the problem.
+ ancilla (int): Grover ancillary qubit.
+
+ Returns:
+ quantum gate generator for the first step of Grover.
+ """
+ yield gates.X(ancilla)
+ yield gates.H(ancilla)
+ for i in q:
+ yield gates.H(i)
+
+
+def create_qc(q):
+ """Create the quantum circuit necessary to solve the problem.
+ Args:
+ q (int): q (int): number of qubits of a site in the permutation matrix.
+
+ Returns:
+ A (list): quantum register of a site in the permutation matrix.
+ B (list): quantum register of a site in the permutation matrix.
+ C (list): quantum register of a site in the permutation matrix.
+ D (list): quantum register of a site in the permutation matrix.
+ x (int): ancillary qubit needed for the adder circuit.
+ ancilla (int): Grover ancilla.
+ circuit (Circuit): quantum circuit object for Grover's algorithm.
+ qubits (int): total number of qubits in the system.
+ """
+ A = [i for i in range(q)]
+ B = [i + q for i in range(q)]
+ C = [i + 2 * q for i in range(q)]
+ D = [i + 3 * q for i in range(q)]
+ x = 4 * q
+ ancilla = 4 * q + 1
+ qubits = 4 * q + 2
+ circuit = Circuit(qubits)
+ return A, B, C, D, x, ancilla, circuit, qubits
+
+
+def chacha_qr(q, A, B, rot):
+ """Classical implementation of the Chacha quarter round
+ Args:
+ q (int): number of bits of a site in the permutation matrix.
+ A (str): classical bitstring for a site of the permutation matrix.
+ B (str): classical bitstring for a site of the permutation matrix.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ A (str): updated classical bitstring for a site of the permutation matrix.
+ B (str): updated classical bitstring for a site of the permutation matrix.
+ """
+ for r in range(len(rot)):
+ a = 0
+ b = 0
+ for i in range(q):
+ a += int(A[i]) * 2 ** (q - 1 - i)
+ b += int(B[i]) * 2 ** (q - 1 - i)
+ a = (a + b) % (2**q)
+ b = b ^ a
+ A = "{0:0{bits}b}".format(a, bits=q)
+ B = "{0:0{bits}b}".format(b, bits=q)
+ for i in range(rot[r]):
+ B = B[1:] + B[0]
+ return A, B
+
+
+def initial_step(q, constant_1, constant_2, rot):
+ """Perform the first step of the algorithm classically.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ constant_1 (int): constant that defines the hash construction.
+ constant_2 (int): constant that defines the hash construction.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ a (str): classical bitstring for a site of the permutation matrix.
+ b (str): classical bitstring for a site of the permutation matrix.
+ c (str): classical bitstring for a site of the permutation matrix.
+ d (str): classical bitstring for a site of the permutation matrix.
+ """
+ a = "{0:0{bits}b}".format(0, bits=q)
+ b = "{0:0{bits}b}".format(0, bits=q)
+ c = "{0:0{bits}b}".format(constant_1, bits=q)
+ d = "{0:0{bits}b}".format(constant_2, bits=q)
+ for i in range(10):
+ a, c = chacha_qr(q, a, c, rot)
+ b, d = chacha_qr(q, b, d, rot)
+ a, d = chacha_qr(q, a, d, rot)
+ b, c = chacha_qr(q, b, c, rot)
+ return a, b, c, d
+
+
+def QhaQha(q, A, B, C, D, x, rot):
+ """Circuit that performs the quantum Chacha permutation for the toy model.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ A (list): quantum register of a site in the permutation matrix.
+ B (list): quantum register of a site in the permutation matrix.
+ C (list): quantum register of a site in the permutation matrix.
+ D (list): quantum register of a site in the permutation matrix.
+ x (int): ancillary qubit needed for the adder circuit.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ generator that applies the ChaCha permutation as a quantum circuit
+ """
+ for i in range(10):
+ yield qr(A, C, x, rot)
+ for i in range(sum(rot)):
+ C = C[1:] + [C[0]]
+ yield qr(B, D, x, rot)
+ for i in range(sum(rot)):
+ D = D[1:] + [D[0]]
+ yield qr(A, D, x, rot)
+ for i in range(sum(rot)):
+ D = D[1:] + [D[0]]
+ yield qr(B, C, x, rot)
+ for i in range(sum(rot)):
+ C = C[1:] + [C[0]]
+
+
+def r_QhaQha(q, A, B, C, D, x, rot):
+ """Reversed circuit that performs the quantum Chacha permutation for the toy model.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ A (list): quantum register of a site in the permutation matrix.
+ B (list): quantum register of a site in the permutation matrix.
+ C (list): quantum register of a site in the permutation matrix.
+ D (list): quantum register of a site in the permutation matrix.
+ x (int): ancillary qubit needed for the adder circuit.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ generator that applies the reverse ChaCha permutation as a quantum circuit
+ """
+ for i in range(10):
+ yield r_qr(B, C, x, rot)
+ for i in range(sum(rot)):
+ C = [C[-1]] + C[:-1]
+ yield r_qr(A, D, x, rot)
+ for i in range(sum(rot)):
+ D = [D[-1]] + D[:-1]
+ yield r_qr(B, D, x, rot)
+ for i in range(sum(rot)):
+ D = [D[-1]] + D[:-1]
+ yield r_qr(A, C, x, rot)
+ for i in range(sum(rot)):
+ C = [C[-1]] + C[:-1]
+
+
+def grover_step(q, c, circuit, A, B, C, D, x, ancilla, h, rot):
+ """Add a full grover step to solve a Sponge Hash construction to a quantum circuit.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ c (list): classical register that contains the initial step
+ circuit (Circuit): quantum circuit where the Grover step is added.
+ A (list): quantum register of a site in the permutation matrix.
+ B (list): quantum register of a site in the permutation matrix.
+ C (list): quantum register of a site in the permutation matrix.
+ D (list): quantum register of a site in the permutation matrix.
+ h (str): hash value that one wants to find preimages of.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ circuit (Circuit): quantum circuit where the Grover step is added.
+ """
+ n = int(len(h))
+ for i in range(q):
+ if int(c[0][i]) == 1:
+ circuit.add(gates.X(A[i]))
+ if int(c[1][i]) == 1:
+ circuit.add(gates.X(B[i]))
+ if int(c[2][i]) == 1:
+ circuit.add(gates.X(C[i]))
+ if int(c[3][i]) == 1:
+ circuit.add(gates.X(D[i]))
+ circuit.add(QhaQha(q, A, B, C, D, x, rot))
+ for i in range(10):
+ for i in range(sum(rot)):
+ C = C[1:] + [C[0]]
+ for i in range(sum(rot)):
+ D = D[1:] + [D[0]]
+ for i in range(sum(rot)):
+ D = D[1:] + [D[0]]
+ for i in range(sum(rot)):
+ C = C[1:] + [C[0]]
+ for i in range(n):
+ if int(h[i]) != 1:
+ circuit.add(gates.X((A + B)[i]))
+ circuit.add(n_2CNOT((A + B)[:n], ancilla, x))
+ for i in range(n):
+ if int(h[i]) != 1:
+ circuit.add(gates.X((A + B)[i]))
+ circuit.add(r_QhaQha(q, A, B, C, D, x, rot))
+ for i in range(10):
+ for i in range(sum(rot)):
+ C = [C[-1]] + C[:-1]
+ for i in range(sum(rot)):
+ D = [D[-1]] + D[:-1]
+ for i in range(sum(rot)):
+ D = [D[-1]] + D[:-1]
+ for i in range(sum(rot)):
+ C = [C[-1]] + C[:-1]
+ for i in range(q):
+ if int(c[0][i]) == 1:
+ circuit.add(gates.X(A[i]))
+ if int(c[1][i]) == 1:
+ circuit.add(gates.X(B[i]))
+ if int(c[2][i]) == 1:
+ circuit.add(gates.X(C[i]))
+ if int(c[3][i]) == 1:
+ circuit.add(gates.X(D[i]))
+ circuit.add(diffusion(A + B, x))
+ return circuit
+
+
+def check_hash(q, message, h, constant_1, constant_2, rot):
+ """Check if a given output message is a preimage of a given hash value.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ message (str): output message that we want to check.
+ h (str): hash value that one wants to find preimages of.
+ constant_1 (int): constant that defines the hash construction.
+ constant_2 (int): constant that defines the hash construction.
+ rot (list): characterization of the rotation part of the algorithm.
+
+ Returns:
+ True of False if the message correspongs to a preimage.
+ """
+ n = int(len(h))
+ m1 = 0
+ m2 = 0
+ for i in range(q):
+ m1 += int(message[i]) * 2 ** (q - 1 - i)
+ m2 += int(message[q + i]) * 2 ** (q - 1 - i)
+ a = "{0:0{bits}b}".format(0, bits=q)
+ b = "{0:0{bits}b}".format(0, bits=q)
+ c = "{0:0{bits}b}".format(constant_1, bits=q)
+ d = "{0:0{bits}b}".format(constant_2, bits=q)
+ for i in range(10):
+ a, c = chacha_qr(q, a, c, rot)
+ b, d = chacha_qr(q, b, d, rot)
+ a, d = chacha_qr(q, a, d, rot)
+ b, c = chacha_qr(q, b, c, rot)
+
+ A = 0
+ B = 0
+ for i in range(q):
+ A += int(a[i]) * 2 ** (q - 1 - i)
+ B += int(b[i]) * 2 ** (q - 1 - i)
+ A = A ^ m1
+ B = B ^ m2
+
+ a = "{0:0{bits}b}".format(A, bits=q)
+ b = "{0:0{bits}b}".format(B, bits=q)
+
+ for i in range(10):
+ a, c = chacha_qr(q, a, c, rot)
+ b, d = chacha_qr(q, b, d, rot)
+ a, d = chacha_qr(q, a, d, rot)
+ b, c = chacha_qr(q, b, c, rot)
+
+ output = (a + b)[:n]
+ return h == output
+
+
+def grover(q, constant_1, constant_2, rot, h, grover_it, nshots=100):
+ """Run the full Grover's search algorithm to find a preimage of a hash function.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ constant_1 (int): constant that defines the hash construction.
+ constant_2 (int): constant that defines the hash construction.
+ rot (list): characterization of the rotation part of the algorithm.
+ h (str): hash value that one wants to find preimages of.
+ grover_it (int): number of Grover steps to be performed.
+
+ Returns:
+ result (dict): counts of the output generated by the algorithm
+ """
+ A, B, C, D, x, ancilla, circuit, qubits = create_qc(q)
+ c1, c2, c3, c4 = initial_step(q, constant_1, constant_2, rot)
+ c = []
+ c.append(c1)
+ c.append(c2)
+ c.append(c3)
+ c.append(c4)
+ circuit.add(start_grover(A + B, ancilla))
+ for i in range(grover_it):
+ circuit = grover_step(q, c, circuit, A, B, C, D, x, ancilla, h, rot)
+ circuit.add(gates.M(*(A + B), register_name="preimages"))
+ result = circuit(nshots=nshots)
+ return result.frequencies(binary=True)
+
+
+def grover_unknown_M(q, constant_1, constant_2, rot, h):
+ """Run an iterative Grover's search algorithm to find a preimage of a hash function when the
+ total number of solutions is unknown.
+ Args:
+ q (int): number of qubits of a site in the permutation matrix.
+ constant_1 (int): constant that defines the hash construction.
+ constant_2 (int): constant that defines the hash construction.
+ rot (list): characterization of the rotation part of the algorithm.
+ h (str): hash value that one wants to find preimages of.
+
+ Returns:
+ measured (str): output of a preimage for the given hash value.
+ total_iterations: number of total Grover steps performed to encounter a solution.
+ """
+ k = 1
+ lamda = 6 / 5
+ total_iterations = 0
+ while True:
+ it = np.random.randint(k + 1)
+ if it != 0:
+ total_iterations += it
+ result = grover(q, constant_1, constant_2, rot, h, it, nshots=1)
+ measured = result.most_common(1)[0][0]
+ if check_hash(q, measured, h, constant_1, constant_2, rot):
+ break
+ k = min(lamda * k, np.sqrt(2 ** (2 * q)))
+ return measured, total_iterations
diff --git a/examples/hash-grover/images/adder-mod2n.png b/examples/hash-grover/images/adder-mod2n.png
new file mode 100644
index 000000000..3950a0874
Binary files /dev/null and b/examples/hash-grover/images/adder-mod2n.png differ
diff --git a/examples/hash-grover/images/chacha-perm.png b/examples/hash-grover/images/chacha-perm.png
new file mode 100644
index 000000000..9dc3c1c32
Binary files /dev/null and b/examples/hash-grover/images/chacha-perm.png differ
diff --git a/examples/hash-grover/images/grover-circuit-image.png b/examples/hash-grover/images/grover-circuit-image.png
new file mode 100644
index 000000000..ca45f1511
Binary files /dev/null and b/examples/hash-grover/images/grover-circuit-image.png differ
diff --git a/examples/hash-grover/images/quarter-round.png b/examples/hash-grover/images/quarter-round.png
new file mode 100644
index 000000000..366dae8c1
Binary files /dev/null and b/examples/hash-grover/images/quarter-round.png differ
diff --git a/examples/hash-grover/images/sponge-oracle.png b/examples/hash-grover/images/sponge-oracle.png
new file mode 100644
index 000000000..d51e58872
Binary files /dev/null and b/examples/hash-grover/images/sponge-oracle.png differ
diff --git a/examples/hash-grover/main.py b/examples/hash-grover/main.py
new file mode 100644
index 000000000..237f9b585
--- /dev/null
+++ b/examples/hash-grover/main.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+import argparse
+
+import functions
+import numpy as np
+
+
+def main(h_value, collisions, b):
+ """Grover search for preimages of a given hash value
+ Args:
+ h_value (int): hash value to be converted to binary string.
+ collisions (int): number of collisions or None if unknown.
+ b (int): number of bits to be used for the hash string.
+
+ Returns:
+ result of the Grover search and checks if it has found a correct preimage.
+ """
+ q = 4
+ m = 8
+ rot = [1, 2]
+ constant_1 = 5
+ constant_2 = 9
+ h = "{0:0{bits}b}".format(h_value, bits=b)
+ if len(h) > 8:
+ raise ValueError(
+ f"Hash should be at maximum an 8-bit number but given value contains {len(h)} bits."
+ )
+ print(f"Target hash: {h}\n")
+ if collisions:
+ grover_it = int(np.pi * np.sqrt((2**8) / collisions) / 4)
+ result = functions.grover(q, constant_1, constant_2, rot, h, grover_it)
+ most_common = result.most_common(collisions)
+ print("Solutions found:\n")
+ print("Preimages:")
+ for i in most_common:
+ if functions.check_hash(q, i[0], h, constant_1, constant_2, rot):
+ print(f" - {i[0]}\n")
+ else:
+ print(
+ " Incorrect preimage found, number of given collisions might not match.\n"
+ )
+ print(f"Total iterations taken: {grover_it}\n")
+ else:
+ measured, total_iterations = functions.grover_unknown_M(
+ q, constant_1, constant_2, rot, h
+ )
+ print("Solution found in an iterative process.\n")
+ print(f"Preimage: {measured}\n")
+ print(f"Total iterations taken: {total_iterations}\n")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--hash", default=163, type=int)
+ parser.add_argument("--bits", default=7, type=int)
+ parser.add_argument("--collisions", default=None, type=int)
+ args = vars(parser.parse_args())
+ main(args.get("hash"), args.get("collisions"), args.get("bits"))
diff --git a/examples/mvc/README.md b/examples/mvc/README.md
new file mode 100644
index 000000000..b849042e5
--- /dev/null
+++ b/examples/mvc/README.md
@@ -0,0 +1,219 @@
+# Minimum Vertex Cover (MVC)
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/mvc](https://github.com/qiboteam/qibo/tree/master/examples/mvc)
+
+Given an undirected graph with a set of nodes V and edges E, A vertex cover of a graph is a set of nodes that includes at
+least one endpoint of every edge of the graph. The Minimum Vertex Cover (MVC) problem is an optimisation problem
+that find smallest vertex cover of a given graph.
+
+## Load graph from csv
+
+
+```python
+import networkx as nx
+import csv
+import matplotlib.pyplot as plt
+
+from mvc import qubo_mvc, qubo_mvc_penalty, mvc_feasibility, mvc_easy_fix, mvc_energy
+
+
+def load_csv(filename:str):
+ """ Load graph from csv file
+ """
+
+ with open(filename, 'r', newline='') as f:
+ reader = csv.reader(f)
+ data = [[int(row[0]), int(row[1]), float(row[2])] for row in reader]
+
+ nodes = [k0 for k0, k1, v in data if k0 == k1]
+ edges = [[k0, k1] for k0, k1, v in data if k0 != k1]
+
+ g = nx.Graph()
+ g.add_nodes_from(nodes)
+ g.add_edges_from(edges)
+
+ node_weights = {k0: {'weight': v} for k0, k1, v in data if k0 == k1}
+ edge_weights = {(k0, k1): {'weight': v} for k0, k1, v in data if k0 != k1}
+
+ nx.set_edge_attributes(g, edge_weights)
+ nx.set_node_attributes(g, node_weights)
+
+ return g
+```
+
+csv Format:
+
+0,0,0.4696822179283675
+1,1,0.6917392308135916
+2,2,0.7130420958314817
+3,3,0.49342677332980056
+4,4,0.49661600571433673
+5,5,0.55601135361347
+6,6,0.2831095711244086
+7,7,0.18737823232636885
+0,1,0.98602725407379
+1,2,0.935853268681996
+2,3,0.23080434023289664
+4,5,0.25521731195623476
+5,6,0.37096743935296606
+6,7,0.04408120693490547
+0,2,0.5619060764177991
+4,7,0.48402095290884917
+1,6,0.7106584134580318
+
+
+
+```python
+g = load_csv('mvc.csv')
+```
+
+
+```python
+pos = nx.spring_layout(g, seed=1234)
+nx.draw(g, pos)
+```
+
+
+```python
+print(f'Number of node is {len(g.nodes)}')
+print(f'Number of edge is {len(g.edges)}')
+```
+
+ Number of node is 8
+ Number of edge is 9
+
+
+## QUBO formulation
+
+### Estimate penalty
+
+
+```python
+penalty = qubo_mvc_penalty(g)
+print(f'The penalty is {penalty}')
+```
+
+ The penalty is 1.6548482220717595
+
+
+### Formulate QUBO (weighted minimum vertex cover)
+
+
+```python
+linear, quadratic = qubo_mvc(g, penalty=penalty)
+print(f'Number of linear terms: {len(linear)}')
+print(f'Number of quadratic terms: {len(quadratic)}\n')
+```
+
+ Number of linear terms: 8
+ Number of quadratic terms: 9
+
+
+
+### Generate random solutions
+
+
+```python
+import numpy as np
+rng = np.random.default_rng(seed=1234)
+random_solution = {i: rng.integers(2) for i in g.nodes}
+print(f'The random solution is {random_solution}\n')
+```
+
+ The random solution is {0: 1, 1: 1, 2: 1, 3: 0, 4: 0, 5: 1, 6: 0, 7: 0}
+
+
+
+### Check feasibility
+
+
+```python
+feasibility = mvc_feasibility(g, random_solution)
+print(f'The feasibility is {feasibility}\n')
+```
+
+ The feasibility is False
+
+
+
+### Fix broken constraints
+
+
+```python
+fixed_solution = mvc_easy_fix(g, random_solution)
+print(f'After fixation, the solution is {fixed_solution}\n')
+```
+
+ After fixation, the solution is {0: 1, 1: 1, 2: 1, 3: 0, 4: 1, 5: 1, 6: 1, 7: 0}
+
+
+
+
+```python
+feasibility = mvc_feasibility(g, fixed_solution)
+print(f'The feasibility is {feasibility}\n')
+```
+
+ The feasibility is True
+
+
+
+### Visualisation
+
+
+```python
+fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(12.8,4.8))
+colour_b4fix = ['C1' if random_solution[n] else 'C0' for n in g.nodes]
+colour_fixed = ['C1' if fixed_solution[n] else 'C0' for n in g.nodes]
+nx.draw(g, pos, node_color=colour_b4fix, ax=ax0)
+ax0.set_title(f'Vertex cover in orange before fix')
+nx.draw(g, pos, node_color=colour_fixed, ax=ax1)
+ax1.set_title(f'Vertex cover in orange after fix')
+```
+
+
+
+
+ Text(0.5, 1.0, 'Vertex cover in orange after fix')
+
+
+
+
+
+
+### Calculate energy
+
+
+```python
+energy = mvc_energy(g, fixed_solution)
+print(f'The energy is {energy}')
+```
+
+ The energy is 3.2102004750256556
+
+
+## Hamiltonian of the MVC problem
+
+
+```python
+ham = hamiltonian_mvc(g, penalty=penalty, dense=True)
+```
+
+ [Qibo 0.1.8|INFO|2022-11-01 10:26:19]: Using numpy backend on /CPU:0
+
+
+## Solve the hamiltonian with QAOA
+
+
+```python
+from qibo import models, hamiltonians
+
+# Create QAOA model
+qaoa = models.QAOA(ham)
+
+# Optimize starting from a random guess for the variational parameters
+initial_parameters = 0.01 * np.random.uniform(0,1,2)
+best_energy, final_parameters, extra = qaoa.minimize(initial_parameters, method="BFGS")
+```
+
+ [Qibo 0.1.8|WARNING|2022-10-31 14:14:37]: Calculating the dense form of a symbolic Hamiltonian. This operation is memory inefficient.
diff --git a/examples/mvc/main.py b/examples/mvc/main.py
new file mode 100755
index 000000000..2cfe3202f
--- /dev/null
+++ b/examples/mvc/main.py
@@ -0,0 +1,82 @@
+"""Minimum Vertex Cover"""
+
+import argparse
+import csv
+
+import networkx as nx
+from mvc import (
+ hamiltonian_mvc,
+ mvc_easy_fix,
+ mvc_energy,
+ mvc_feasibility,
+ qubo_mvc,
+ qubo_mvc_penalty,
+)
+from qubo_utils import binary2spin, spin2QiboHamiltonian
+
+from qibo import callbacks, hamiltonians, models
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--filename", default="./mvc.csv", type=str)
+
+
+def load_csv(filename: str):
+ """Load graph from csv file"""
+ with open(filename, newline="") as f:
+ reader = csv.reader(f)
+ data = [[int(row[0]), int(row[1]), float(row[2])] for row in reader]
+
+ nodes = [k0 for k0, k1, v in data if k0 == k1]
+ edges = [[k0, k1] for k0, k1, v in data if k0 != k1]
+
+ g = nx.Graph()
+ g.add_nodes_from(nodes)
+ g.add_edges_from(edges)
+
+ node_weights = {k0: {"weight": v} for k0, k1, v in data if k0 == k1}
+ edge_weights = {(k0, k1): {"weight": v} for k0, k1, v in data if k0 != k1}
+
+ nx.set_edge_attributes(g, edge_weights)
+ nx.set_node_attributes(g, node_weights)
+
+ return g
+
+
+def main(filename: str = "./mvc.csv"):
+ print(f"Load a graph from {filename} and make it a QUBO")
+ g = load_csv(filename)
+ penalty = qubo_mvc_penalty(g)
+ linear, quadratic = qubo_mvc(g, penalty=penalty)
+
+ print("A random solution with seed 1234 must be infeasible")
+ import numpy as np
+
+ rng = np.random.default_rng(seed=1234)
+ random_solution = {i: rng.integers(2) for i in g.nodes}
+ feasibility = mvc_feasibility(g, random_solution)
+ assert not feasibility, "The random solution should be infeasible."
+
+ print("Make a naive fix to the violation of the constraint")
+ fixed_solution = mvc_easy_fix(g, random_solution)
+ feasibility = mvc_feasibility(g, fixed_solution)
+ assert feasibility, "The fixed solution should be feasible."
+
+ print("Calculate the energy of the solution")
+ energy = mvc_energy(g, fixed_solution)
+
+ print("Construct a hamiltonian based on the QUBO representation")
+ h, J, _ = binary2spin(linear, quadratic)
+ h = {k: -v for k, v in h.items()}
+ J = {k: -v for k, v in J.items()}
+ ham = spin2QiboHamiltonian(h, J)
+
+ print("Construct a hamiltonian directly from a networkx graph")
+ ham = hamiltonian_mvc(g)
+
+ print("done.")
+
+
+if __name__ == "__main__":
+ # by defualt, test on the mvc.csv in the same directory
+ args = parser.parse_args()
+ main(args.filename)
diff --git a/examples/mvc/mvc.csv b/examples/mvc/mvc.csv
new file mode 100644
index 000000000..71ffdc638
--- /dev/null
+++ b/examples/mvc/mvc.csv
@@ -0,0 +1,17 @@
+0,0,0.4696822179283675
+1,1,0.6917392308135916
+2,2,0.7130420958314817
+3,3,0.49342677332980056
+4,4,0.49661600571433673
+5,5,0.55601135361347
+6,6,0.2831095711244086
+7,7,0.18737823232636885
+0,1,0.98602725407379
+1,2,0.935853268681996
+2,3,0.23080434023289664
+4,5,0.25521731195623476
+5,6,0.37096743935296606
+6,7,0.04408120693490547
+0,2,0.5619060764177991
+4,7,0.48402095290884917
+1,6,0.7106584134580318
diff --git a/examples/mvc/mvc.py b/examples/mvc/mvc.py
new file mode 100644
index 000000000..2ef1eea5a
--- /dev/null
+++ b/examples/mvc/mvc.py
@@ -0,0 +1,139 @@
+import networkx as nx
+from qubo_utils import binary2spin, spin2QiboHamiltonian
+
+
+def qubo_mvc(g: nx.Graph, penalty=None):
+ """Given a graph g, return the QUBO of Minimum Vertex Cover (MVC) problem.
+
+ If penalty is not given, it is inferred from g.
+
+ Args:
+ g (nx.Graph): graph
+ penalty (float): penalty weight of the constraints
+
+ Returns:
+ linear (Dict[Any, float]): linear terms
+ quadratic (Dict[(Any, Any), float]): quadratic terms
+
+ """
+
+ q = {(k, k): v for k, v in g.nodes(data="weight")}
+
+ if penalty is None:
+ penalty = qubo_mvc_penalty(g)
+
+ for s, d in g.edges:
+ q[(s, s)] -= penalty
+ q[(d, d)] -= penalty
+
+ if s > d:
+ s, d = d, s
+
+ if (s, d) not in q:
+ q[(s, d)] = penalty
+ else:
+ q[(s, d)] += penalty
+
+ linear = {k0: v for (k0, k1), v in q.items() if k0 == k1}
+ quadratic = {(k0, k1): v for (k0, k1), v in q.items() if k0 != k1 and v != 0}
+
+ return linear, quadratic
+
+
+def qubo_mvc_penalty(g: nx.Graph):
+ """Find appropriate penalty weight to ensure feasibility
+
+ Args:
+ g (nx.Graph): graph
+
+ Returns:
+ penalth (float): penalty
+
+ """
+
+ # For NISQ devices, you cannot randomly choose a positive penalty weight, otherwise
+ # you get infeasible solutions very likely. If all weight equals to 1,
+ # set penalty to the largest degree will ensure high feasibility.
+
+ highest_degree = max(
+ sum(g.nodes[k]["weight"] for k in g[i].keys()) for i in g.nodes()
+ )
+ return highest_degree
+
+
+def mvc_feasibility(g: nx.Graph, solution):
+ """Check if the solution violates the constraints of the problem
+
+ Args:
+ g (nx.Graph): graph
+ solution (Dict[Any, int]): the solution
+
+ Returns:
+ feasibility (bool): whether the solution meet the constraints
+ """
+ for k0, k1 in g.edges:
+ if solution[k0] == solution[k1] == 0:
+ return False
+ return True
+
+
+def mvc_energy(g: nx.Graph, solution):
+ """Calculate the energy of the solution on a MVC problem
+
+ The result is based on the assumption that soltuion is feasible.
+
+ Args:
+ g (nx.Graph): graph
+ solution (Dict[Any, int]): the solution
+
+ Returns:
+ energy (float): the energy of the solution
+ """
+ return sum(solution[k] * v for k, v in g.nodes.data("weight"))
+
+
+def mvc_easy_fix(g: nx.Graph, solution):
+ """Naively fix violation in an out-of--place manner
+
+ Args:
+ g (nx.Graph): graph
+ solution (Dict[Any, int]): the solution
+
+ Returns:
+ solution (Dict[Any, int]): fixed solution
+ """
+
+ t = {k: v for k, v in solution.items()}
+
+ for k0, k1 in g.edges:
+ if t[k0] == t[k1] == 0:
+ t[k0] = 1
+
+ return t
+
+
+def hamiltonian_mvc(g: nx.Graph, penalty: float = None, dense: bool = False):
+ """Given a graph g, return the hamiltonian of Minimum Vertex Cover (MVC)
+ problem.
+
+ If penalty is not given, it is inferred from g.
+
+ Args:
+ g (nx.Graph): graph
+ penalty (float): penalty weight of the constraints
+ dense (bool): sparse or dense hamiltonian
+
+ Returns:
+ ham: qibo hamiltonian
+
+ """
+
+ linear, quadratic = qubo_mvc(g, penalty)
+
+ h, J, _ = binary2spin(linear, quadratic)
+ h = {k: -v for k, v in h.items()}
+ J = {k: -v for k, v in J.items()}
+
+ ham = spin2QiboHamiltonian(h, J, dense=dense)
+
+ return ham
diff --git a/examples/mvc/qubo_utils.py b/examples/mvc/qubo_utils.py
new file mode 100644
index 000000000..b8805fc8a
--- /dev/null
+++ b/examples/mvc/qubo_utils.py
@@ -0,0 +1,123 @@
+from typing import Dict, Tuple
+
+
+def binary2spin(
+ linear: Dict[int, float],
+ quadratic: Dict[Tuple[int, int], float],
+ offset: float = 0,
+):
+ """Convert binary model to spin model
+
+ Please remember to put a negative sign to h and J after using this
+ function , if you are going to form a mamiltonian from the spin
+ model. Hamiltonians usually have a leading negative sign in them,
+ but QUBOs don't.
+
+ Args:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+
+ Returns:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ offset (float): offset of the spin model
+ """
+
+ h = {x: 0.5 * w for x, w in linear.items()}
+
+ J = []
+ for (x, y), w in quadratic.items():
+ J.append(((x, y), 0.25 * w))
+ h[x] += 0.25 * w
+ h[y] += 0.25 * w
+ J = dict(J)
+
+ offset += 0.5 * sum(linear.values())
+ offset += 0.25 * sum(quadratic.values())
+
+ return h, J, offset
+
+
+def spin2binary(
+ h: Dict[int, float],
+ J: Dict[Tuple[int, int], float],
+ offset: float = 0,
+):
+ """Convert spin model to binary model
+
+ Please remember to put a negative sign to h and J before using this
+ function if you extract them from a hamiltonian. Hamiltonians
+ usually have a leading negative sign in them, but QUBOs don't.
+
+ Args:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ offset (float): offset of the spin model
+
+ Returns:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+ """
+
+ linear = {s: 2.0 * bias for s, bias in h.items()}
+
+ quadratic = []
+ for (s, t), bias in J.items():
+ quadratic.append(((s, t), 4.0 * bias))
+ linear[s] -= 2.0 * bias
+ linear[t] -= 2.0 * bias
+ quadratic = dict(quadratic)
+
+ offset -= sum(linear.values())
+ offset += sum(quadratic.values())
+
+ return linear, quadratic, offset
+
+
+def spin2QiboHamiltonian(
+ h: Dict[int, float],
+ J: Dict[Tuple[int, int], float],
+ dense: bool = True,
+):
+ """Convert spin model to qibo Hamiltonian
+
+ Mixer is not included.
+
+ Please remember to put a negative sign to h and J if you get h and J
+ from binary2spin. Hamiltonians usually have a leading negative sign
+ in them, but QUBOs don't.
+
+ .. math::
+ H = - \\sum_{i=0}^N \\sum _{j=0}^N J_{ij} Z_i Z_j - \\sum_{i=0}^N h_i Z_i.
+
+ Args:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+
+ Returns:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+ """
+
+ from qibo import hamiltonians
+ from qibo.symbols import Z
+
+ symbolic_ham = sum(Z(k, commutative=True) * v for k, v in h.items())
+ symbolic_ham += sum(
+ Z(k0, commutative=True) * Z(k1, commutative=True) * v
+ for (k0, k1), v in J.items()
+ )
+ symbolic_ham = -symbolic_ham
+
+ ham = hamiltonians.SymbolicHamiltonian(symbolic_ham)
+
+ if dense:
+ return ham.dense
+ else:
+ return ham
diff --git a/examples/qPDF/data/full/8evols.dat b/examples/qPDF/data/full/8evols.dat
new file mode 100644
index 000000000..0661c9fb9
--- /dev/null
+++ b/examples/qPDF/data/full/8evols.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.906051619342992609e+00 7.462119819026813428e-01 1.436073027852281747e+00 1.410829786450314760e+00 2.409332433385497074e-02 6.139939433975893923e-04 3.709153843587331689e-03 -1.294460527052412879e-03 3.142570116364562516e-02 1.279459685050776674e-03 2.178039037871642991e-02 -5.501273989189520153e-02 2.967891727365936649e-01 -5.351913518991966434e-01 9.543745599019025317e-04 8.933750230847316287e-02
+1.151395399326446927e-04 2.860722503238625869e+00 6.988723163291101770e-01 1.364958626022871213e+00 1.322403944448146795e+00 2.563399484389794480e-02 7.368417313359243437e-04 4.000960575745893255e-03 -1.272189925989306025e-03 3.308435479737814600e-02 1.205367363498355271e-03 2.360494800229695489e-02 -5.079617646067179082e-02 3.064897739743315253e-01 -5.228070936938105095e-01 3.636680441961381328e-03 7.412584225106703950e-02
+1.325711365590109543e-04 2.815405728981031697e+00 6.586280524367921219e-01 1.308744557998403835e+00 1.246856327363375705e+00 2.729566618448603377e-02 8.940427551678564733e-04 4.318183834072841787e-03 -1.251820952761883987e-03 3.485989265135536197e-02 1.151260142898680172e-03 2.614203360893901928e-02 -4.664551928313108442e-02 3.158794264026344445e-01 -5.090709612371046022e-01 4.933524249824835194e-03 6.407245802265681967e-02
+1.526417967175233259e-04 2.770324102867665061e+00 6.244195241115695350e-01 1.266716265313918344e+00 1.181035820270940784e+00 2.908113496268388554e-02 1.071826646856594012e-03 4.666986511230430956e-03 -1.229507452258457589e-03 3.674532391650664476e-02 1.111022581747167504e-03 2.828035986310167837e-02 -4.300222704272022023e-02 3.248707472090531789e-01 -4.941991141595850068e-01 5.655341068586110242e-03 5.779131852221603555e-02
+1.757510624854791170e-04 2.725517322797724074e+00 5.944487582232559353e-01 1.238632415751813820e+00 1.124100495485785611e+00 3.099065793148558789e-02 1.266470151572163250e-03 5.049239129196692044e-03 -1.211296513348857073e-03 3.873956456544158655e-02 1.090102566206563317e-03 2.991495798307713949e-02 -3.997435589083222357e-02 3.334714075036346870e-01 -4.785649375008670381e-01 5.894428576734326262e-03 5.400134138166334807e-02
+2.023589647725157589e-04 2.681196838693734374e+00 5.641250926612668692e-01 1.223304323506445579e+00 1.082280689395135109e+00 3.301849624366737235e-02 1.425823533246772801e-03 5.477156013369621768e-03 -1.203517031224531075e-03 4.083481205164418704e-02 1.028231719495176222e-03 3.126187635942873078e-02 -3.730603537237335565e-02 3.418424717767015730e-01 -4.624912367226028387e-01 6.105220955743916912e-03 4.880768069883391347e-02
+2.329951810515371805e-04 2.637384376323162360e+00 5.350509620256458376e-01 1.221403075342442968e+00 1.049932598530862737e+00 3.516589939286773880e-02 1.547203870220559274e-03 5.952690748948608057e-03 -1.212797163812409651e-03 4.303053129517608877e-02 9.237437726121178194e-04 3.235494070724337368e-02 -3.489195915358409006e-02 3.500848868970291017e-01 -4.458210154938854197e-01 6.235027687244114672e-03 4.349911980376581361e-02
+2.682695795279724476e-04 2.594164003900461424e+00 5.091953271630039746e-01 1.234246661261282352e+00 1.018495848476023369e+00 3.743142390222492510e-02 1.633470266189929188e-03 6.497803876925334343e-03 -1.261760471277802165e-03 4.532139767855858370e-02 7.648154326440614881e-04 3.330068004701974838e-02 -3.256661292586237461e-02 3.584611086702909066e-01 -4.283162187443461111e-01 6.268431070720082224e-03 3.967181908221596026e-02
+3.088843596477481527e-04 2.551560515699526466e+00 4.856781521167745641e-01 1.261118092261666934e+00 9.820269772796574870e-01 3.982648473418481938e-02 1.675470149689939348e-03 7.049611997319960022e-03 -1.328185468408182857e-03 4.771595411720908064e-02 5.754847871185519992e-04 3.405999944634308108e-02 -3.032459936431707720e-02 3.668684233087414759e-01 -4.105069411107938326e-01 6.217020373943566401e-03 3.686609060461188553e-02
+3.556480306223128661e-04 2.509599140891932478e+00 4.637532165071666101e-01 1.301809107355178963e+00 9.380345387151233227e-01 4.235414178804519647e-02 1.671334622949022242e-03 7.560114271729201363e-03 -1.407078063365894971e-03 5.020537728988849757e-02 3.766318389149536650e-04 3.462309337997493408e-02 -2.816141756988261441e-02 3.751541695227437190e-01 -3.927431014729541592e-01 6.047944115574644883e-03 3.465683609043487129e-02
+4.094915062380427508e-04 2.468315423913098439e+00 4.422539326639124968e-01 1.355955018492741582e+00 8.893711185159632882e-01 4.504059339772775727e-02 1.597829467800016001e-03 8.107519750108815870e-03 -1.462992208397947969e-03 5.287559229861948928e-02 1.838154797281776887e-04 3.499154103999518872e-02 -2.598801321550247428e-02 3.844380513026034452e-01 -3.753661775630095510e-01 6.086277759868033259e-03 3.255440885733468931e-02
+4.714866363457394672e-04 2.427712402325765240e+00 4.210862448528221447e-01 1.423415028768741042e+00 8.375259875883908078e-01 4.789331366448362770e-02 1.452527053360908038e-03 8.703536492151719983e-03 -1.484257969507851715e-03 5.575168050733947389e-02 1.465493068023016043e-05 3.517161650060213196e-02 -2.380730015382700321e-02 3.947170717208153823e-01 -3.586138022321409546e-01 6.353607011376051841e-03 3.045711151395184141e-02
+5.428675439323859403e-04 2.387743918408224975e+00 4.011382559420967331e-01 1.503558397812429392e+00 7.741246897837354979e-01 5.095521483434201082e-02 1.274214129943732066e-03 9.386648087601701107e-03 -1.435538982956133247e-03 5.905295167943280799e-02 -1.122443418009322347e-04 3.526360047455268099e-02 -2.200751087594812055e-02 4.033746393990121915e-01 -3.417970480025358526e-01 6.428561308584479321e-03 2.867200099971633012e-02
+6.250551925273975734e-04 2.348387102320955488e+00 3.820577280180124102e-01 1.594760490544748555e+00 7.053377733669865535e-01 5.425861882146287840e-02 1.071538590392259738e-03 1.015403625715427527e-02 -1.310693900552002689e-03 6.273289354146038832e-02 -1.870994471893372424e-04 3.527563526439142505e-02 -2.051531149610450605e-02 4.105281640533186005e-01 -3.249129270673927206e-01 6.372404207167801926e-03 2.704047344363377792e-02
+7.196856730011521675e-04 2.309572722275872181e+00 3.635766358785286201e-01 1.694368883930322145e+00 6.381482642538304217e-01 5.786287214154184344e-02 8.629408661428861205e-04 1.098327016996020200e-02 -1.159094632399902680e-03 6.675717155153770843e-02 -2.477665381384885901e-04 3.524445607586629281e-02 -1.937188316215548295e-02 4.155169660940215559e-01 -3.074495584179327556e-01 6.258801326906005338e-03 2.571516118777497986e-02
+8.286427728546842068e-04 2.271337215556608058e+00 3.459199706961187948e-01 1.797933073006341420e+00 5.770645929332395285e-01 6.185468967407886653e-02 7.165830880065393593e-04 1.197364937573081045e-02 -8.301396807884886542e-04 7.101790912098437936e-02 -1.808658096041915186e-04 3.517307215238224538e-02 -1.824551761460532773e-02 4.201191315065782028e-01 -2.900762778330903124e-01 6.047430101503004259e-03 2.473241740672309924e-02
+9.540954763499943534e-04 2.233683502841355306e+00 3.289489556794900627e-01 1.902288016649845925e+00 5.213202772176038780e-01 6.629607483904109699e-02 7.256939172376880576e-04 1.321516338060824136e-02 -1.816803400257541168e-04 7.542725486775037069e-02 1.455492417572479269e-04 3.506902486917523598e-02 -1.682295858235231117e-02 4.257318112086013739e-01 -2.732669519568579064e-01 5.727973869072462351e-03 2.400751060148542324e-02
+1.098541141987558400e-03 2.196685734288134739e+00 3.130908018498047563e-01 2.004993449735332334e+00 4.718001852409516306e-01 7.124663100199635224e-02 7.133753846192275594e-04 1.453771611306603484e-02 4.205133818923796618e-04 8.004122211560438238e-02 5.778067293672195515e-04 3.494190035771210789e-02 -1.546271661467520792e-02 4.303743647380635640e-01 -2.570325180831006806e-01 5.217025855255588707e-03 2.331562484684581577e-02
+1.264855216855295715e-03 2.160359142346439221e+00 2.982326008119254857e-01 2.105514952847052701e+00 4.273401137522174764e-01 7.671529760119794039e-02 6.444353030430755025e-04 1.591814324213158827e-02 8.821673442042203550e-04 8.486754986269084977e-02 1.085921038826487273e-03 3.479091040879289665e-02 -1.425412340907782419e-02 4.338180042770034550e-01 -2.413071011501616847e-01 4.520400786895952851e-03 2.287525171199299504e-02
+1.456348477501244378e-03 2.124768531205242450e+00 2.835482339955216480e-01 2.204919381213297669e+00 3.857407874870817932e-01 8.262935522867614901e-02 4.856455467324855002e-04 1.735972497538579296e-02 1.159102321058695650e-03 8.995269323481064339e-02 1.551038267477017341e-03 3.452956214373470445e-02 -1.330042022227230156e-02 4.368136614232921433e-01 -2.261871807747988239e-01 4.104105861430898221e-03 2.251634616463276534e-02
+1.676832936811008387e-03 2.089886636778342410e+00 2.689103658428349530e-01 2.302838545246236102e+00 3.527675463927992805e-01 8.893381826859464434e-02 2.505936105514777745e-04 1.890294494622801746e-02 1.320227417617819005e-03 9.533611385627405177e-02 1.920592288084971289e-03 3.422874677965193069e-02 -1.251282444079887882e-02 4.392256045319101743e-01 -2.114730824142506405e-01 3.888985899011146528e-03 2.211418539704611622e-02
+1.930697728883249645e-03 2.055685659097348683e+00 2.544796379090471516e-01 2.401250317650158284e+00 3.292460342654914274e-01 9.547436464437430992e-02 -8.790577109937802724e-05 2.065299596330588683e-02 1.539167098646945786e-03 1.010784585245643741e-01 1.991745703567415759e-03 3.407769421897388629e-02 -1.162179965122341410e-02 4.406562962078498513e-01 -1.967104472715614505e-01 3.777269112706165780e-03 2.186241065726403712e-02
+2.222996482526195736e-03 2.022064289211034094e+00 2.409054177577455991e-01 2.489413722921670402e+00 3.241005711624331820e-01 1.022795720491102012e-01 -4.603984101919089689e-04 2.257944996142652316e-02 1.740399217905457679e-03 1.073616461393115951e-01 1.680105010706117730e-03 3.395628867277111773e-02 -1.067351857293324935e-02 4.417490464017018725e-01 -1.821002756467193229e-01 3.716552809729335802e-03 2.186462296014471396e-02
+2.559547922699535825e-03 1.988934505919132345e+00 2.278774653941089690e-01 2.558364682243282839e+00 3.028155002281870400e-01 1.093206187992075695e-01 -8.090187731591494402e-04 2.468029651314496142e-02 1.906687386700762349e-03 1.144250649828131294e-01 7.126072051936299667e-04 3.379958717023096026e-02 -9.689595645125753665e-03 4.426500358456443296e-01 -1.675258441897166661e-01 3.668101774664294951e-03 2.202517508457559753e-02
+2.947051702551809708e-03 1.956332697014812272e+00 2.162392065064994151e-01 2.616224206212616199e+00 2.820170227437128752e-01 1.167424380035106957e-01 -1.173662947933473699e-03 2.692533578467010136e-02 1.884801149395712999e-03 1.216295418116605798e-01 -1.600852960845777773e-04 3.363598642204607136e-02 -8.576650766480675703e-03 4.450971951998125009e-01 -1.538982966950086517e-01 3.547940999186200654e-03 2.200550651002718128e-02
+3.393221771895329857e-03 1.924269027649759600e+00 2.058747170265479709e-01 2.664071453832157932e+00 2.591501164691561443e-01 1.245776234560457274e-01 -1.564786627264586061e-03 2.931276187023079682e-02 1.646727139411188062e-03 1.288851220549040999e-01 -8.648794816137200803e-04 3.347228843142929788e-02 -7.335712491089114995e-03 4.494439335672644198e-01 -1.412161338165656699e-01 3.334206195109535448e-03 2.177519424557845459e-02
+3.906939937054616958e-03 1.892982336078212091e+00 1.961808772479244345e-01 2.703361738998359520e+00 2.348943198277512467e-01 1.330186832997240498e-01 -1.865460585694908580e-03 3.196121877494878527e-02 1.239021758940781620e-03 1.367438239253928600e-01 -1.648270078091540469e-03 3.346546911966319371e-02 -6.382514472699104408e-03 4.561658895047046025e-01 -1.292383936954070722e-01 2.882653201686987635e-03 2.111194539755851737e-02
+4.498432668969444201e-03 1.862538821228059138e+00 1.875839665940923495e-01 2.734359509219411066e+00 2.093647295324295721e-01 1.421172032673739816e-01 -1.988769325223599393e-03 3.489718879934639029e-02 7.566765492925710268e-04 1.452562744974513764e-01 -2.392961982128752330e-03 3.363083117111598241e-02 -5.638327509948526384e-03 4.651977385458023750e-01 -1.180918926972806537e-01 2.266223600348241541e-03 2.030995470319845245e-02
+5.179474679231212825e-03 1.833140068419793378e+00 1.807254197101701987e-01 2.758211866006374358e+00 1.861954455819890830e-01 1.520710936709788463e-01 -1.619494149053143928e-03 3.817972938715480380e-02 2.741198153758152289e-04 1.545971215307738178e-01 -2.815199467855744597e-03 3.404927746485519879e-02 -5.019592136273724747e-03 4.768204251285368955e-01 -1.083340128914820760e-01 1.650199402710971321e-03 1.966948473734408259e-02
+5.963623316594642357e-03 1.804839151717607448e+00 1.756910118391523667e-01 2.774504985860168027e+00 1.666400687266494929e-01 1.627637530673841926e-01 -7.765553156137069979e-04 4.192220621672132497e-02 -1.715396287274330300e-04 1.649156620097197434e-01 -2.572308184336874470e-03 3.466819563282846905e-02 -4.454395194833919713e-03 4.897136399974937881e-01 -1.001283229540880920e-01 1.045156606308592952e-03 1.936423761741078858e-02
+6.866488450042998112e-03 1.777705814340383661e+00 1.721635560558101297e-01 2.783256351062659117e+00 1.509792672160396587e-01 1.739226180828475354e-01 5.577445779148607063e-04 4.619519012868572583e-02 -5.470628568529194158e-04 1.762648730972930511e-01 -1.149657365379141982e-03 3.547883554846648080e-02 -3.933647755103783505e-03 5.028157826833805633e-01 -9.246198104295999531e-02 4.393240182844387388e-04 1.955534016925687679e-02
+7.906043210907700777e-03 1.751789858031411518e+00 1.685009977035207829e-01 2.783431601575428349e+00 1.393981853977233343e-01 1.868863751184585698e-01 9.541796375784616924e-04 5.119958305791566522e-02 -7.619644953021954825e-04 1.892853762447305743e-01 4.401529215211918977e-04 3.636258386306334689e-02 -3.485432043680698061e-03 5.162903347157841161e-01 -8.530814794740892992e-02 1.293074295599358806e-04 1.970672652804380393e-02
+9.102981779915217050e-03 1.727107965448127613e+00 1.641839731575882588e-01 2.774862150044652420e+00 1.356247879444966997e-01 2.019142266763930271e-01 -5.237241789141783288e-05 5.698017618741429979e-02 -7.593404022438916823e-04 2.041088128276559721e-01 2.034377610009172055e-03 3.729492238222775757e-02 -3.060382179671066599e-03 5.301498020885356821e-01 -7.836694087990382640e-02 1.761382065781277049e-04 1.994970249637265797e-02
+1.048113134154685273e-02 1.703752463217196089e+00 1.606642201493900246e-01 2.760566329600861124e+00 1.342862511369074530e-01 2.183944932810497896e-01 -1.257472541037134295e-03 6.343513280541218657e-02 -5.374237122598470029e-04 2.201442450352001368e-01 4.015140635969749461e-03 3.829244166622192136e-02 -2.979236290565519252e-03 5.468735475869694884e-01 -7.269911721980590324e-02 6.342964865634353891e-04 2.000374435605717371e-02
+1.206792640639328847e-02 1.681741841466589227e+00 1.574115297323718798e-01 2.740601584470428342e+00 1.316902676015173357e-01 2.365436214462611486e-01 -2.674044620900754843e-03 7.045226773299656520e-02 -1.633807364653898364e-04 2.376396273306931595e-01 6.053503906537527346e-03 3.950383879785190899e-02 -3.016866291674448475e-03 5.669838951047392328e-01 -6.756187116634179701e-02 1.159859179318486122e-03 2.010314301162413955e-02
+1.389495494373137359e-02 1.661180872498956518e+00 1.506227976403611279e-01 2.715933591822299764e+00 1.253457936888159319e-01 2.567822757776673792e-01 -4.644127065801376131e-03 7.766657557742634443e-02 1.842972812932802684e-04 2.571257492992121696e-01 7.495432120503693618e-03 4.140581276527505183e-02 -2.531192989213757599e-03 5.925765651394002687e-01 -6.127512509837636234e-02 6.229645941732639153e-04 1.893838285154697196e-02
+1.599858719606057217e-02 1.641936295425339809e+00 1.438243804779347768e-01 2.685154192466012724e+00 1.193572415535045228e-01 2.796240510536457391e-01 -6.365348338658552430e-03 8.518033946297082215e-02 5.596041918141149429e-04 2.790094146813046350e-01 8.701290807122591398e-03 4.382995926841448675e-02 -2.114565963955858385e-03 6.228247868785594665e-01 -5.433207365904305780e-02 -4.528610396075364209e-04 1.816499523496944643e-02
+1.842069969326716461e-02 1.623973998634635985e+00 1.377901334833385172e-01 2.647431992894222486e+00 1.132596856420066828e-01 3.054700108621894450e-01 -6.912084008575233168e-03 9.297027474706190397e-02 7.289304187375958421e-04 3.033964367566540088e-01 9.829992110299348718e-03 4.667630562357144575e-02 -2.149637273613766442e-03 6.575990713816981525e-01 -4.618559625605917623e-02 -2.040862806432878866e-03 1.837584791020675096e-02
+2.120950887920190417e-02 1.606952748332304948e+00 1.316949735372383856e-01 2.601477236862831166e+00 1.124669224631803938e-01 3.349749004169555455e-01 -7.036393485510699514e-03 1.012927420441265547e-01 8.083684380427794136e-04 3.322628483256136467e-01 1.095988242511616861e-02 5.016488550414333059e-02 -2.299801919725254912e-03 6.958361640459556074e-01 -3.991318766042432908e-02 -2.764948712256495682e-03 1.852305431254810605e-02
+2.442053094548649744e-02 1.590788822310530204e+00 1.243226889588570361e-01 2.546935186302070164e+00 1.199475922069823675e-01 3.683216411752401886e-01 -6.973376376096449003e-03 1.102071391038152715e-01 9.352470145973686144e-04 3.660824188829614467e-01 1.163864064014223654e-02 5.434134892004915951e-02 -2.427710292972059397e-03 7.372735601027718966e-01 -3.546250788524608205e-02 -2.301153740602655817e-03 1.806891834668321664e-02
+2.811768697974230749e-02 1.575168016170775287e+00 1.171832076468105194e-01 2.486491029640843564e+00 1.260482655459385648e-01 4.059319752305156070e-01 -5.956858707028535316e-03 1.199096929299429404e-01 1.248678625053845082e-03 4.047733064186829766e-01 9.793079756769386413e-03 5.932381167525677124e-02 -2.475451532886998457e-03 7.804951593565594425e-01 -3.015335398039294124e-02 -2.065953710478731550e-03 1.794252940578616382e-02
+3.237457542817643447e-02 1.560097899196016380e+00 1.101710796826544381e-01 2.421072694716483120e+00 1.342529033015319173e-01 4.473054430221816702e-01 -4.321418927177179264e-03 1.304129113505142490e-01 1.350929717884905013e-03 4.474908425623155828e-01 6.329012901206645947e-03 6.517335407775615153e-02 -2.568332880654382311e-03 8.259908637776820495e-01 -2.470326180773166347e-02 -1.879599560993519027e-03 1.738587176486946009e-02
+3.727593720314938130e-02 1.545542572083943966e+00 1.036093899612278157e-01 2.356120187280772438e+00 1.228918399554665974e-01 4.911395046954560861e-01 -1.644464513056727306e-03 1.417181886734582563e-01 1.233953045166377022e-03 4.916881760456623396e-01 2.223557066318652964e-03 7.198498858163970837e-02 -2.797588503884113412e-03 8.752730218901014769e-01 -2.141317462140131939e-02 -1.281647868885260524e-03 1.660475291361803579e-02
+4.291934260128778267e-02 1.531572602123358662e+00 9.728127487724623490e-02 2.283609481004460040e+00 1.135313148672207062e-01 5.366104690169182234e-01 6.711598535814773669e-04 1.540653255514338738e-01 9.678512345811188938e-04 5.369653061250165837e-01 -2.050408583142968033e-03 8.018165078823452641e-02 -2.908308329307534440e-03 9.279416482263004307e-01 -1.821674977036366605e-02 -4.063840395447936319e-04 1.568866817153680149e-02
+4.941713361323833015e-02 1.518341324472995302e+00 8.950734238211086047e-02 2.197861404801443985e+00 1.111765296966798489e-01 5.827091421608007193e-01 7.533135499223631015e-04 1.677295485913511963e-01 7.172473457791612300e-04 5.829455097283484655e-01 -3.057864349947345822e-03 9.018829008283218984e-02 -2.935030353911056641e-03 9.863701585885369383e-01 -1.249541199529381405e-02 8.499527604997306370e-04 1.472870517349008160e-02
+5.689866029018292998e-02 1.505307307266029904e+00 8.810148335699352629e-02 2.094792655510467227e+00 1.086382056511527999e-01 6.294815536387448063e-01 9.901955224021086882e-04 1.823584676492381318e-01 7.061995826628166689e-04 6.270243384803549924e-01 -1.840824609249589600e-03 1.019564162987246503e-01 -3.211644617809455982e-03 1.036754676663268926e+00 -1.089222945444151547e-02 1.635985800273112058e-03 1.478982758405596870e-02
+6.551285568595509312e-02 1.492338872126097726e+00 8.673219802223879060e-02 1.972943483478049043e+00 1.016059983620012930e-01 6.768664823281200782e-01 1.962040869776884161e-03 1.978628347794686160e-01 1.160841317695027533e-03 6.684809678354665108e-01 -1.080701264518587473e-03 1.155101083814724927e-01 -3.284794786471789338e-03 1.075629503163584744e+00 -6.736547651658684327e-03 1.735828455516659138e-03 1.592381199680020440e-02
+7.543120063354614990e-02 1.479666262045960856e+00 8.141922715679962563e-02 1.828797090456429908e+00 8.228479680656052009e-02 7.246611517554401027e-01 2.234599803021370193e-03 2.159454643005864760e-01 9.173334911300231459e-04 7.080199161214449921e-01 -1.115911766339468258e-03 1.303237510513822750e-01 -3.442816632195635704e-03 1.105939016479328574e+00 -3.621107408948803785e-03 9.190323300996633893e-04 1.504578981460611906e-02
+8.685113737513520948e-02 1.467067443862011933e+00 7.605632207591291050e-02 1.665052707183172664e+00 7.019163919958561204e-02 7.729086809360399268e-01 2.632184566432160810e-03 2.362269267061525013e-01 6.591315587753197175e-04 7.462538002564987893e-01 -1.809542377258981788e-03 1.465592044397492633e-01 -3.910815628155301857e-03 1.127803464297723046e+00 -2.163644186741874725e-03 -5.170461293543338402e-05 1.330416744770090282e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02 1.486650900000000108e+00 6.906649307998630194e-02 8.219631450000000061e-01 4.472744027894770547e-03 2.579254300000000111e-01 3.332937874284975371e-04 7.856684999999999919e-01 -2.671446105093899792e-04 1.654803099999999638e-01 -3.591407057756365248e-03 1.138711483999999885e+00 -2.740669875090178537e-03 2.390608399999999932e-03 1.207505639463098135e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02 1.486650900000000108e+00 6.906649307998630194e-02 8.219631450000000061e-01 4.472744027894770547e-03 2.579254300000000111e-01 3.332937874284975371e-04 7.856684999999999919e-01 -2.671446105093899792e-04 1.654803099999999638e-01 -3.591407057756365248e-03 1.138711483999999885e+00 -2.740669875090178537e-03 2.390608399999999932e-03 1.207505639463098135e-02
+1.163265306122449050e-01 1.436257699431471968e+00 6.600913866769045346e-02 1.278363607352488662e+00 5.851715503791719841e-02 8.760245558745264338e-01 7.629865271835317708e-03 2.832269254732402985e-01 -1.758261191725733058e-04 8.293545442242232779e-01 4.533990903847666745e-03 1.891290223954812943e-01 -3.207517653019667861e-03 1.151017244962964980e+00 1.037318243192690559e-03 2.021799069942414612e-03 1.029101446289395344e-02
+1.326530612244898044e-01 1.415413753481877368e+00 6.242141770875467982e-02 1.093272570071426530e+00 6.495189916810374664e-02 9.207873942133834033e-01 1.087930043521478582e-02 3.053929728929584142e-01 -2.117451617313862583e-04 8.665748494692329107e-01 5.263570222115704703e-03 2.130861595075765058e-01 -2.462179552547698672e-03 1.159271728947077085e+00 2.573872010971296406e-03 2.194705307789258843e-03 1.011183417614163436e-02
+1.489795918367347038e-01 1.390723868801176488e+00 6.004590985567208328e-02 9.445472028682869725e-01 6.800362366808562764e-02 9.555260855881660964e-01 1.311758721701782038e-02 3.238159043446356411e-01 -2.168866785661948890e-04 8.997274462522860672e-01 6.573654107431214921e-03 2.373766657780576605e-01 -1.248902758435248284e-03 1.159782281727252728e+00 3.152171299635436741e-03 3.357213460984731675e-03 1.035859280245138986e-02
+1.653061224489796033e-01 1.361917477796027676e+00 5.855238285923737573e-02 8.244352867442255040e-01 6.036562576446031364e-02 9.811175998983059365e-01 1.436268096786381260e-02 3.408284603992981499e-01 8.299497311290288115e-04 9.262887531129980756e-01 7.024598183513044991e-03 2.626463541241408350e-01 -1.215334683369711719e-03 1.153505937395747249e+00 4.939946258408016086e-03 4.568155960819259183e-03 1.030152646918971841e-02
+1.816326530612245027e-01 1.328624546252491712e+00 5.909126204572044683e-02 7.266001875331934423e-01 4.804631794363146274e-02 9.982561169995157746e-01 1.670857810347954955e-02 3.529769485411317631e-01 1.813881046560281499e-03 9.465454900669407756e-01 7.708172608863731248e-03 2.861035556266112168e-01 -1.197192788463815639e-03 1.141451524305949761e+00 6.268876466634652408e-03 5.220820476545198416e-03 9.887806050869172442e-03
+1.979591836734694021e-01 1.291182364995507870e+00 5.799461876770821422e-02 6.460934389251358212e-01 5.956328597727415797e-02 1.005614729418596198e+00 1.927636214968469694e-02 3.625207622508588856e-01 1.753782301094962068e-03 9.579642078598071819e-01 5.947898850618089928e-03 3.058368520521582146e-01 -1.459083957057752401e-03 1.123688166046398207e+00 8.248278204087602233e-03 5.934829898366457586e-03 9.809191509951561971e-03
+2.142857142857143016e-01 1.250364307046662793e+00 5.729005890456270039e-02 5.754064633997411082e-01 5.481677133626022824e-02 1.004729452204575324e+00 2.069043281326496744e-02 3.667695907052059834e-01 2.146097855119245834e-03 9.635234739688496308e-01 3.361656410241223129e-03 3.219613004354836550e-01 -1.747063702520791628e-03 1.100877192739575072e+00 9.326223083890011062e-03 6.531736775880660727e-03 9.816070343834169126e-03
+2.306122448979592010e-01 1.206927192547645911e+00 5.632383651387896928e-02 5.137991377127972825e-01 5.361156055870076875e-02 9.964514094860162929e-01 2.040614599634831342e-02 3.692429999459841961e-01 2.928125248630514786e-03 9.633955920550227869e-01 1.826650838476845222e-03 3.344292412524122460e-01 -2.023823820231218130e-03 1.073872463500070706e+00 9.320583008029778571e-03 7.073477024250686418e-03 9.873718139427525159e-03
+2.469387755102041004e-01 1.161434662449474020e+00 5.575873697984527755e-02 4.552507567815178291e-01 5.065601192147932419e-02 9.817668338111157356e-01 1.840051143596008881e-02 3.696344609371332579e-01 3.230467079454401279e-03 9.575494535041556565e-01 2.250644860223596777e-03 3.450778886737556617e-01 -1.786909755980518356e-03 1.043560859401786445e+00 8.268252049006056398e-03 7.675799938353641827e-03 9.943834213998184110e-03
+2.632653061224490276e-01 1.114255548834317322e+00 5.499105653975491781e-02 3.996382053409509028e-01 4.755273215299854472e-02 9.616221603847134869e-01 1.591738512518598228e-02 3.687094707430881857e-01 3.658556769999159360e-03 9.455629882815539400e-01 4.706934512374225804e-03 3.532490117204427071e-01 -1.842688617728312390e-03 1.010507768078387603e+00 6.727057482457301368e-03 8.223640364209331907e-03 9.997248271147583798e-03
+2.795918367346938993e-01 1.065729549351170924e+00 5.406308092778352309e-02 3.411444192178936241e-01 4.570775803372454510e-02 9.368664327965776639e-01 1.306867091356006538e-02 3.648377142879627444e-01 3.339683048581209776e-03 9.277487215459292447e-01 8.390992685674466753e-03 3.575574858304151338e-01 -1.747100400113369215e-03 9.743783295117218746e-01 5.463220273594844231e-03 8.723184340334453474e-03 1.011600243934571673e-02
+2.959183673469387710e-01 1.016247959667206491e+00 5.288154012402173504e-02 2.941440729953202493e-01 5.179924323811337800e-02 9.077384819671754368e-01 1.086561841248540050e-02 3.614681618638949656e-01 4.009541118659402763e-03 9.047705543775533821e-01 1.166704002588283065e-02 3.589366151255772852e-01 -1.904646522060764700e-03 9.364607041507967411e-01 5.229266156563059167e-03 9.354520468648605883e-03 9.985168140465173342e-03
+3.122448979591836982e-01 9.661707426172632118e-01 5.305346802331420419e-02 2.601290037822677204e-01 4.622670247373922986e-02 8.753125791436111580e-01 1.014237327275376715e-02 3.562591328441856442e-01 3.252942299916778898e-03 8.792659459325451454e-01 1.373745204075244551e-02 3.576319049134298877e-01 -2.463442409355897852e-03 8.954439656483303978e-01 6.817122484342049218e-03 9.938869133245136628e-03 1.003625880427465485e-02
+3.285714285714286254e-01 9.157971486255552795e-01 5.662310358643761476e-02 2.302641998343407104e-01 4.244869960571524825e-02 8.405443498693302429e-01 7.664957427717792628e-03 3.452605536981402379e-01 2.300327065724727324e-03 8.482835465847984313e-01 1.393058433297254296e-02 3.543977802625193352e-01 -2.605198663765615798e-03 8.529476078765321212e-01 1.228989874997294014e-02 9.944464743138389448e-03 9.923830319159009788e-03
+3.448979591836734970e-01 8.653596641887024710e-01 5.083797877077313176e-02 2.048947603867307399e-01 3.692175416096520058e-02 8.034485871899764708e-01 8.234582604494007640e-03 3.396375617630511545e-01 3.377340007886578920e-03 8.103442551639935543e-01 1.622016117467551741e-02 3.490851536982226833e-01 -7.624224416734248430e-04 8.094105788772453236e-01 9.422659712189892026e-03 9.973770093789352878e-03 9.734503659314653473e-03
+3.612244897959183687e-01 8.150424664509627881e-01 5.361559377675658572e-02 1.820567154916601094e-01 3.491710247794299615e-02 7.645300106285628505e-01 6.934557417598144480e-03 3.341288504450065644e-01 2.697371197079972624e-03 7.660321149119501127e-01 1.350631075750425951e-02 3.420271078042709800e-01 -2.663121160636718401e-03 7.652022029235848732e-01 1.475014803148823675e-02 1.038996123676954632e-02 9.343466147424542984e-03
+3.775510204081632404e-01 7.650336795089378583e-01 5.504084064016197264e-02 1.654109241587360668e-01 2.958093259719593235e-02 7.234981658492036782e-01 8.142519083330676533e-03 3.264499482306293388e-01 3.058979013150607204e-03 7.174059443946417813e-01 8.934531421016229588e-03 3.324091991918894928e-01 -5.429737640515337402e-03 7.209794008570916679e-01 1.343544728946480075e-02 1.022449502788700705e-02 9.067401904482772640e-03
+3.938775510204082231e-01 7.155107322841964823e-01 5.815762181981395940e-02 1.480349508988084706e-01 2.827395372375652097e-02 6.797301550560850281e-01 7.301918226727460715e-03 3.176871303691626847e-01 2.507018641180281358e-03 6.693607196533725157e-01 5.868688883468267670e-03 3.205915700716052696e-01 -7.710384988422315164e-03 6.761035453466663903e-01 1.424031491769873342e-02 9.687755045023086972e-03 8.708677675773364088e-03
+4.102040816326530948e-01 6.666148092756788435e-01 5.388739088185211246e-02 1.325798271673375717e-01 2.828630531659322850e-02 6.347498261678146125e-01 7.564475821368022400e-03 3.051681204691203964e-01 3.199912002429109933e-03 6.220495054927962508e-01 7.117667135200277453e-03 3.076833015555935913e-01 -9.268378632880202098e-03 6.303581793950283618e-01 1.208802642013349048e-02 9.162324623002576079e-03 8.366970908929851139e-03
+4.265306122448979664e-01 6.184808244113076947e-01 5.672644041609450211e-02 1.182493409930097245e-01 2.916021507299379825e-02 5.902870051375984639e-01 6.476244337488319333e-03 2.916682104815467858e-01 2.974320558817333468e-03 5.735828503298960879e-01 5.598590526945397550e-03 2.931440885295590881e-01 -9.289541957659729413e-03 5.824717232695867386e-01 1.567762681743947381e-02 9.151804666231467059e-03 8.160535659671936878e-03
+4.428571428571428381e-01 5.712833756315996991e-01 6.581076428913901211e-02 1.066391781871470490e-01 2.847724416602226416e-02 5.469390334167323342e-01 6.503510318900787394e-03 2.806805010781118770e-01 2.902858707303770588e-03 5.234709192018245361e-01 3.726986947742665437e-03 2.776505733554623379e-01 -1.212442063030644800e-02 5.359270605702105561e-01 2.373088270821131773e-02 9.399011025721361323e-03 8.620527635960733084e-03
+4.591836734693878208e-01 5.252968102940863693e-01 6.702217841793813025e-02 1.011350726109566528e-01 3.463682731939739246e-02 5.054220839413947619e-01 7.157137997245991562e-03 2.585982451427741302e-01 3.467688305903465773e-03 4.737391775997533694e-01 3.792968907112018173e-03 2.618972221858207328e-01 -1.416672222868416153e-02 4.926155228680057507e-01 2.277983998031588367e-02 8.823754218570113023e-03 8.056682849491073745e-03
+4.755102040816326925e-01 4.808391912274195823e-01 6.858931665301323810e-02 9.174674898459940497e-02 3.317719061506606976e-02 4.655735881486708361e-01 6.151644386349899504e-03 2.413939489550236128e-01 4.081741466172366176e-03 4.275087629415363910e-01 3.444260923021804158e-03 2.451230091093854757e-01 -1.350860278242318330e-02 4.511776631645700864e-01 2.119289716436993654e-02 7.824004843225812814e-03 7.981915893287276631e-03
+4.918367346938775642e-01 4.381130722623843288e-01 6.718507026803152704e-02 8.739403248853304385e-02 3.458909252220616903e-02 4.273494270197562561e-01 5.741698175921050237e-03 2.294557993863459988e-01 4.240931700175741906e-03 3.851674631693069450e-01 3.963425607039449544e-03 2.290377420641576400e-01 -1.413445531461066280e-02 4.101274867609975283e-01 1.857514540571357162e-02 7.016749592106577386e-03 7.892538800058142290e-03
+5.081632653061224358e-01 3.972125375443600337e-01 6.965800777247084519e-02 7.986314864670505942e-02 3.453373395965902981e-02 3.907562983066515394e-01 4.228438900749689408e-03 2.219616674650559951e-01 3.788903873166361846e-03 3.463307418523625669e-01 3.472681758940058400e-03 2.135512008039863463e-01 -1.453468428403530480e-02 3.688274588658959630e-01 2.153776701042124306e-02 6.598559841394757187e-03 7.539189377298772046e-03
+5.244897959183674185e-01 3.581097245266221529e-01 6.844653703788193544e-02 7.216510147179601431e-02 2.968070892373265643e-02 3.555452126726142970e-01 4.476964374474977013e-03 2.085937770482449993e-01 5.029172127092289973e-03 3.107052457833348602e-01 4.210241830234665722e-03 1.984985872720557709e-01 -1.397634559474913822e-02 3.299857352693796320e-01 2.057477160238923436e-02 6.900297183520782453e-03 7.420117289685977842e-03
+5.408163265306122902e-01 3.211058522457649334e-01 6.801459714625253394e-02 6.645926254942295663e-02 2.713087028010289364e-02 3.216816336632508655e-01 5.101654155733479669e-03 1.932228583245677311e-01 5.421371354795442507e-03 2.776810767211733144e-01 4.434284480829685238e-03 1.841811580961135175e-01 -1.309493151133219596e-02 2.947601101555517578e-01 2.147811573443057787e-02 7.099558957836545926e-03 7.396966799181269384e-03
+5.571428571428571619e-01 2.864888840811441462e-01 6.623243611783007079e-02 5.877459162780401120e-02 2.555514304330760988e-02 2.893248444780310846e-01 5.733630698988551912e-03 1.819244310888655236e-01 4.698266066874413546e-03 2.457190340297982223e-01 3.354360902640613970e-03 1.704403134171777467e-01 -1.406728234143836236e-02 2.622506614519226620e-01 2.184512970481299748e-02 6.790721355450353609e-03 7.098078746040513073e-03
+5.734693877551020336e-01 2.542592546856339286e-01 6.508281122168510513e-02 5.338086792246633794e-02 2.502004244557643961e-02 2.586220812087101084e-01 5.062289292442913834e-03 1.686736884711234896e-01 3.290103082771974122e-03 2.181877137322736082e-01 1.503434250308174902e-03 1.562671367343420326e-01 -1.400188817532591751e-02 2.319658116797040648e-01 2.164059264137865174e-02 5.384298384279484137e-03 6.473503045793584446e-03
+5.897959183673470163e-01 2.245039172586136000e-01 6.817903370165791233e-02 4.876099631660012895e-02 2.707226383643828588e-02 2.295356732539315381e-01 4.546381244334137504e-03 1.494528926901955990e-01 1.731732789425769262e-03 1.937577703554055342e-01 8.774567316059845440e-04 1.418647467121848249e-01 -1.070000414607550666e-02 2.038300728024848851e-01 2.499849853906035979e-02 3.669951832429759093e-03 6.640741958232684525e-03
+6.061224489795918879e-01 1.971904898479003854e-01 6.724857957251778595e-02 4.207712352396885541e-02 2.305135101626111169e-02 2.020682303291666815e-01 4.700315925673483226e-03 1.353149002042534077e-01 1.073033203524085752e-03 1.712446205776504971e-01 1.392177204712607627e-03 1.278475110007968318e-01 -8.516135160069529877e-03 1.779961534041439308e-01 2.436415844152281518e-02 2.869011282850813822e-03 6.458241817368222566e-03
+6.224489795918367596e-01 1.722555534350652118e-01 6.682717720305531017e-02 3.580485555936888109e-02 2.185199910362296480e-02 1.770966967495726796e-01 4.545380858232235186e-03 1.196382305481427932e-01 5.675822726855447864e-04 1.503386543935546216e-01 1.187608494908782078e-03 1.138655403828967849e-01 -5.058930938126255175e-03 1.545427884236979243e-01 2.516110110112216094e-02 2.511121700851877674e-03 6.031679775970608809e-03
+6.387755102040816313e-01 1.496087563872702808e-01 6.578562391334155657e-02 3.021147633166515814e-02 2.208234850326066048e-02 1.543235307319828775e-01 3.719669291221451568e-03 1.099870114073757815e-01 1.099755908800202631e-04 1.307211636005658761e-01 -2.569527151626404410e-04 1.001273020151677018e-01 -4.123350891244225574e-03 1.335632662539781024e-01 2.590675883781437538e-02 3.022869112719256315e-03 5.836083715696130554e-03
+6.551020408163265030e-01 1.292460451082127204e-01 6.357257318602752005e-02 2.484776981462179582e-02 2.198756275659718984e-02 1.327929417946560886e-01 3.262410228304992409e-03 9.857771370782425491e-02 -4.010140583726017754e-04 1.133059520694710415e-01 -1.572737721869544442e-04 8.643471418070733381e-02 -4.074131125049942556e-03 1.152348253417438545e-01 2.597207634522478992e-02 2.751824022379022617e-03 5.287353443280242218e-03
+6.714285714285714857e-01 1.111797350094870934e-01 6.506130007688437633e-02 1.988113039811458330e-02 2.200991807516783014e-02 1.133598162492658462e-01 2.783517477014238982e-03 8.728924623054287735e-02 -5.498324803594051291e-04 9.865830665947733502e-02 4.856473648199733512e-04 7.191910637408532869e-02 -4.635433453179224617e-03 9.875996651570564944e-02 2.776972279037007857e-02 2.126691438879552924e-03 4.706042950694613002e-03
+6.877551020408163573e-01 9.523361075388962760e-02 6.047832185377364361e-02 1.400636390703255754e-02 2.365106623106593212e-02 9.675785167022572797e-02 2.667898688008315394e-03 7.932002060431865564e-02 -1.006125028624939299e-03 8.358483896113802492e-02 7.396423703803483435e-04 5.815207638918958727e-02 -4.533647393946745041e-03 8.407398278488814602e-02 2.090721250125920222e-02 1.378634377812767509e-03 4.302552030458201351e-03
+7.040816326530612290e-01 8.122659700001326477e-02 6.162435588265317843e-02 1.145974167737420432e-02 1.887010255106317397e-02 8.187050455239161528e-02 2.400835956446172731e-03 7.048546113961080828e-02 -1.296709283692231221e-03 7.082531695668828342e-02 5.281490075120173766e-04 4.612698979303683777e-02 -3.528400028220969945e-03 7.106167117549450074e-02 1.884473803010927595e-02 4.801378255429795180e-04 4.188115301054883347e-03
+7.204081632653062117e-01 6.893218067535411553e-02 6.606274324296279588e-02 7.837588375768960586e-03 1.719052997285148640e-02 6.602299540318257165e-02 1.746335188707009989e-03 6.295347641224970092e-02 -1.016031134913187098e-03 6.232248730722956404e-02 -1.595583001111778448e-04 3.750154254323265907e-02 -3.197032357921831755e-03 5.909780060400191687e-02 1.396685124656719609e-02 -2.405201575052475134e-05 4.107351465178039722e-03
+7.367346938775510834e-01 5.814267735253492858e-02 7.631079155029231453e-02 5.019045671291559423e-03 1.703959974523763451e-02 5.190213556870747202e-02 1.667785682315762671e-03 5.302574765414624913e-02 -9.547079292404926643e-04 5.817900121811786507e-02 -2.416162017449992316e-04 3.051268156801009712e-02 -1.996130669194155424e-03 4.837950241516492111e-02 7.232931761620698535e-03 -3.026026130038219253e-04 3.824171123570902983e-03
+7.530612244897959551e-01 4.869566766134052205e-02 7.742097813387080474e-02 3.380609462740184409e-03 1.479054695636173103e-02 4.021748440642871486e-02 1.635198177865507946e-03 4.434534000993487629e-02 -4.222279995945016351e-04 4.997561476490473831e-02 -6.147511373023085979e-04 2.418628742139881244e-02 1.089112760743771516e-03 3.878217089483680657e-02 3.172534007081652396e-03 -4.110413969063750187e-04 3.709084790485463190e-03
+7.693877551020408267e-01 4.044250240680334618e-02 7.505508510155267654e-02 9.324618436404591567e-05 1.621917485475198040e-02 3.209025564326395408e-02 1.754316217031409025e-03 4.040000792708368232e-02 1.790811079042664566e-04 4.429882453685759364e-02 -6.119451888814027296e-04 1.892921581226242669e-02 -9.156765312932045597e-05 3.091164592705523267e-02 -1.811811273991242865e-03 -4.659984273595200793e-04 3.625770725960355580e-03
+7.857142857142856984e-01 3.327547602901389373e-02 7.594730521129608447e-02 -1.538774680937169846e-03 1.427911879458286688e-02 2.585250344320047869e-02 1.818274116936699838e-03 3.768412261083003284e-02 5.076365411593800059e-04 4.066132817562254087e-02 -4.103494689009987151e-04 1.498062986787362071e-02 -1.567888516314374855e-03 2.478933121691693359e-02 -7.057285602139504871e-03 -4.054769709337468055e-04 3.274674784559306398e-03
+8.020408163265306811e-01 2.710706020503510574e-02 6.967577716341799565e-02 -3.292689738769052563e-03 1.302317972988607450e-02 1.859930309235167598e-02 1.440673587944269612e-03 3.027078564631083962e-02 5.499912192063957650e-04 3.414662672179843983e-02 -8.418719129241747368e-04 1.205208052239571496e-02 -7.717307690063845982e-04 1.955018661238028949e-02 -9.803192065236304709e-03 -2.850385614523395263e-04 3.066331520608849698e-03
+8.183673469387755528e-01 2.182777327792014693e-02 6.432099519023265488e-02 -4.768501625455401523e-03 1.556994705567193819e-02 1.346632027765258706e-02 1.215897313679950575e-03 2.510188890796036243e-02 6.099389384973605277e-04 2.826004328751220360e-02 -8.105932931825078624e-04 9.719048837170184835e-03 -8.520002326649605412e-04 1.531750868718114930e-02 -8.836503694312693169e-03 -2.277311817526648716e-04 2.897488817883252590e-03
+8.346938775510204245e-01 1.732718253187195967e-02 5.710674947523330935e-02 -5.230190570117304073e-03 1.414505616898533748e-02 9.667061410177868463e-03 1.098713889135851177e-03 2.109925377869088445e-02 3.205401471248325168e-04 2.465244783664412298e-02 -4.693055364111756167e-04 7.902985505951989897e-03 -8.459919359694485774e-04 1.191668445291303551e-02 -7.980204458145706559e-03 -2.518029966257508655e-04 2.439864690347969683e-03
+8.510204081632654072e-01 1.350721403879344071e-02 5.190565017544805648e-02 -5.504081810805650996e-03 1.310886165854219070e-02 6.733249885862054029e-03 9.985308212159260677e-04 1.750471058246003825e-02 3.773376656582864627e-04 2.128855753071882961e-02 -1.004985303370321825e-04 6.098078701810978955e-03 -7.748156400943020938e-04 9.051946361661511453e-03 -8.472351544024037118e-03 -2.539742218870363779e-04 2.011351503123325751e-03
+8.673469387755102789e-01 1.028418426810574250e-02 4.576728749949458830e-02 -5.565775093366548236e-03 1.283554913783929025e-02 4.661214622425208925e-03 8.824090909300211227e-04 1.371875731792381557e-02 4.023935127803295042e-04 1.742398657884901975e-02 -1.376806251658334312e-04 4.597830936409629324e-03 -7.584395675897092487e-04 6.748583785838472258e-03 -9.243666320254941698e-03 -2.282090620477647091e-04 1.617226165581661483e-03
+8.836734693877551505e-01 7.589738733240949248e-03 4.101680475501470746e-02 -5.301125605810848816e-03 1.208554103662637380e-02 3.395664268329615099e-03 7.578399836952638691e-04 1.135151875260490516e-02 2.739934372502006096e-04 1.527690382772810447e-02 -2.119651147850891426e-04 3.303316269833258753e-03 -1.187778836739436863e-03 4.891974298810318825e-03 -1.233570620581838501e-02 -1.624589630119514685e-04 1.292898474252045194e-03
+9.000000000000000222e-01 5.365500585267322931e-03 4.142048728873987118e-02 -4.864826018318149127e-03 1.070561871116822161e-02 2.906583183801734593e-03 3.342223288867705866e-05 9.921337035406383245e-03 -3.139617846961637038e-04 1.476776805162933331e-02 -8.138580850603621686e-04 2.478024997064359140e-03 -1.682045282580753136e-03 3.465658109240731845e-03 -1.309887794038555250e-02 -5.209738777082163698e-05 1.235071236380816525e-03
diff --git a/examples/qPDF/data/full/8flavours.dat b/examples/qPDF/data/full/8flavours.dat
new file mode 100644
index 000000000..0214909c0
--- /dev/null
+++ b/examples/qPDF/data/full/8flavours.dat
@@ -0,0 +1,100 @@
+# x, sbar, sbar_err, ubar, ubar_err, dbar, dbar_err, gluon, gluon_err, d, d_err, u_err, s, s_err, c, c_err
+1.000000000000000048e-04 4.359400748127145730e-01 1.987885495391763235e-01 5.067987518397338853e-01 5.124127680190377482e-02 4.977631335721693362e-01 7.810041648432516914e-02 1.436073027852281747e+00 1.410829786450314760e+00 5.091772816222682740e-01 7.916555467649237510e-02 5.219220537334201548e-01 5.101195446701856789e-02 4.334959492027843364e-01 1.985667276252919278e-01 4.771872799509512659e-04 4.466875115423658144e-02
+1.151395399326446927e-04 4.263410681293022808e-01 1.913370155673360107e-01 4.995934197801784049e-01 4.795290366933818715e-02 4.897914260669028463e-01 7.271489693667942955e-02 1.364958626022871213e+00 1.322403944448146795e+00 5.018496698598922112e-01 7.379750037070245683e-02 5.156526241489136630e-01 4.776331717737190841e-02 4.238576148114755471e-01 1.911808403566152004e-01 1.818340220980690664e-03 3.706292112553351975e-02
+1.325711365590109543e-04 4.170261674659069828e-01 1.839806288398284639e-01 4.927370133474432978e-01 4.507664891223533282e-02 4.818250884600102091e-01 6.777349807741993304e-02 1.308744557998403835e+00 1.246856327363375705e+00 4.945745340463617024e-01 6.888929949600661184e-02 5.098046427678676329e-01 4.494062937806012764e-02 4.145047586436171883e-01 1.838948897105848468e-01 2.466762124912417597e-03 3.203622901132840983e-02
+1.526417967175233259e-04 4.079103672573080597e-01 1.768110859473048524e-01 4.858450664184125745e-01 4.254037186435637163e-02 4.740383797424769230e-01 6.342673165958724601e-02 1.266716265313918344e+00 1.181035820270940784e+00 4.875228187938407398e-01 6.458393136495986508e-02 5.039964919810068222e-01 4.246806411747052618e-02 4.053556376060338251e-01 1.767980206356746520e-01 2.827670534293055121e-03 2.889565926110801777e-02
+1.757510624854791170e-04 3.989833988918851659e-01 1.698059977928489950e-01 4.788827893399035807e-01 4.020147293170975994e-02 4.664499299129633569e-01 5.958300262045144319e-02 1.238632415751813820e+00 1.124100495485785611e+00 4.807121237531004621e-01 6.079249135535101656e-02 4.981942223092373778e-01 4.019966515326047624e-02 3.964004300138998516e-01 1.698647869879708616e-01 2.947214288367163131e-03 2.700067069083167404e-02
+2.023589647725157589e-04 3.901775769615442591e-01 1.629018428118806927e-01 4.718756719793233967e-01 3.799634611606899498e-02 4.589833118062938144e-01 5.604760528664341074e-02 1.223304323506445579e+00 1.082280689395135109e+00 4.740567012227721588e-01 5.729601026658712876e-02 4.924262174091713629e-01 3.804123406478818192e-02 3.875721383588853208e-01 1.630343734164645675e-01 3.052610477871958456e-03 2.440384034941695673e-02
+2.329951810515371805e-04 3.814881822735329275e-01 1.561248996030261682e-01 4.648523336635698722e-01 3.601891928895885331e-02 4.516512086844224894e-01 5.285850028384469351e-02 1.221403075342442968e+00 1.049932598530862737e+00 4.675685849901001223e-01 5.413459078459310209e-02 4.867224007181961132e-01 3.608221262589485223e-02 3.788666383060967924e-01 1.563327196355623061e-01 3.117513843622057336e-03 2.174955990188290680e-02
+2.682695795279724476e-04 3.728874063226307323e-01 1.494951786652647108e-01 4.578730531130874581e-01 3.439870700737103493e-02 4.444716150280402234e-01 5.005113323466332115e-02 1.234246661261282352e+00 1.018495848476023369e+00 4.612534206700789619e-01 5.135397279780620772e-02 4.811526626320515310e-01 3.443978609923611933e-02 3.702574150638528461e-01 1.497847302764466759e-01 3.134215535360041112e-03 1.983590954110798013e-02
+3.088843596477481527e-04 3.643940902333109855e-01 1.430365028767308644e-01 4.509348043934499062e-01 3.301679292827554291e-02 4.374296106689383179e-01 4.751499987622998661e-02 1.261118092261666934e+00 9.820269772796574870e-01 4.551329586012081152e-01 4.883349679151714751e-02 4.756877643230396635e-01 3.300710437515452095e-02 3.617642671056362502e-01 1.434031646642546509e-01 3.108510186971783201e-03 1.843304530230594276e-02
+3.556480306223128661e-04 3.560413771259096083e-01 1.367574631842753130e-01 4.440443199611848746e-01 3.178894497967885840e-02 4.305128304070620082e-01 4.516611473293721812e-02 1.301809107355178963e+00 9.380345387151233227e-01 4.492183834155272226e-01 4.648953727875566183e-02 4.703099872413792903e-01 3.170528946213140714e-02 3.534242986252951746e-01 1.371890307789533359e-01 3.023972057787322441e-03 1.732841804521743564e-02
+4.094915062380427508e-04 3.476043489585864399e-01 1.306086145635859119e-01 4.372159690320540082e-01 3.058986105112534645e-02 4.237739583871108495e-01 4.285237155467760961e-02 1.355955018492741582e+00 8.893711185159632882e-01 4.435463283610689422e-01 4.414711339476462548e-02 4.650958587561209168e-01 3.042161068281441436e-02 3.449926826582891959e-01 1.310799525596098580e-01 3.043138879934016629e-03 1.627720442866734465e-02
+4.714866363457394672e-04 3.390833484060716274e-01 1.246341772413884219e-01 4.304417162115531936e-01 2.939630240460333957e-02 4.172076762073279599e-01 4.055782349676291532e-02 1.423415028768741042e+00 8.375259875883908078e-01 4.381122926006365548e-01 4.178657065441718221e-02 4.600498690970135085e-01 2.914079159274975475e-02 3.364638927917863453e-01 1.251134679489486479e-01 3.176803505688025921e-03 1.522855575697592070e-02
+5.428675439323859403e-04 3.310064090909531909e-01 1.188128074121952704e-01 4.235560691175858095e-01 2.824861352376353829e-02 4.106175929241103195e-01 3.853459947025952848e-02 1.503558397812429392e+00 7.741246897837354979e-01 4.327514991049956072e-01 3.965839961475201708e-02 4.550766233860727983e-01 2.793687468529989018e-02 3.283071634759229251e-01 1.192749602361101585e-01 3.214280654292239661e-03 1.433600049985816506e-02
+6.250551925273975734e-04 3.233268014634110288e-01 1.131119239340316190e-01 4.165560188431848831e-01 2.712931263009265431e-02 4.039952193395663360e-01 3.673162142786890599e-02 1.594760490544748555e+00 7.053377733669865535e-01 4.274598897417202226e-01 3.771296466707743655e-02 4.501747255024930450e-01 2.679996196874918565e-02 3.205020432234118588e-01 1.135314699465588134e-01 3.186202103583900963e-03 1.352023672181688896e-02
+7.196856730011521675e-04 3.161152090441567419e-01 1.073667209507341569e-01 4.093704541562542265e-01 2.601307584834669853e-02 3.972398612033011811e-01 3.511947011322449214e-02 1.694368883930322145e+00 6.381482642538304217e-01 4.221620454240913034e-01 3.594536996178232524e-02 4.452759085470045508e-01 2.567988106450463243e-02 3.131504425741581388e-01 1.077369567521612864e-01 3.129400663453002669e-03 1.285758059388748993e-02
+8.286427728546842068e-04 3.090556455659053681e-01 1.017277303708125358e-01 4.021308068564666827e-01 2.490003288148680619e-02 3.905310954681409652e-01 3.360772184839522225e-02 1.797933073006341420e+00 5.770645929332395285e-01 4.169988188584659028e-01 3.423150841652428111e-02 4.405721796225224307e-01 2.469367976882737292e-02 3.060012390836035490e-01 1.020268800033494461e-01 3.023715050751502129e-03 1.236620870336154962e-02
+9.540954763499943534e-04 3.018924829653984454e-01 9.627137539323034343e-02 3.949320874227529310e-01 2.375793053299492252e-02 3.840051566784694059e-01 3.207856965415820105e-02 1.902288016649845925e+00 5.213202772176038780e-01 4.120674757458040460e-01 3.243556600354317865e-02 4.362095698706958125e-01 2.393324654235414600e-02 2.988487562891620208e-01 9.646475695172382347e-02 2.863986934536231176e-03 1.200375530074271162e-02
+1.098541141987558400e-03 2.949814891347372070e-01 9.111202107180126075e-02 3.876658208515135762e-01 2.273863236954239489e-02 3.774637287291905396e-01 3.068024736782619041e-02 2.004993449735332334e+00 4.718001852409516306e-01 4.072839513592570460e-01 3.080408359331427912e-02 4.320237595946461173e-01 2.328298197692286325e-02 2.920499587635345118e-01 9.115721062355193471e-02 2.608512927627794353e-03 1.165781242342290788e-02
+1.264855216855295715e-03 2.883621649240054596e-01 8.618432266431293176e-02 3.803179703192489658e-01 2.180199442874538110e-02 3.708815867359183116e-01 2.937013980538640337e-02 2.105514952847052701e+00 4.273401137522174764e-01 4.026388726257003059e-01 2.932485474076973464e-02 4.279933994511625484e-01 2.263887670833293272e-02 2.856447475035078232e-01 8.603716075238512784e-02 2.260200393447976425e-03 1.143762585599649752e-02
+1.456348477501244378e-03 2.818623503211090120e-01 8.138074355440537422e-02 3.728700516603166148e-01 2.083130815800366548e-02 3.642851330761421313e-01 2.806106942966916581e-02 2.204919381213297669e+00 3.857407874870817932e-01 3.981405045371430584e-01 2.790190649596348271e-02 4.240851480967033349e-01 2.183124754535667802e-02 2.794212376523975139e-01 8.102561264749053027e-02 2.052052930715449110e-03 1.125817308231638267e-02
+1.676832936811008387e-03 2.755290569891833963e-01 7.665654358959549108e-02 3.653328801164484330e-01 1.984986194940741369e-02 3.576699791997364763e-01 2.676638787861576260e-02 2.302838545246236102e+00 3.527675463927992805e-01 3.937524651255330244e-01 2.650990408800484033e-02 4.203183109884729984e-01 2.091360557641431042e-02 2.733949584599569271e-01 7.609987736375099787e-02 1.944492949505573264e-03 1.105709269852305811e-02
+1.930697728883249645e-03 2.694760312761637633e-01 7.190122099966855806e-02 3.577266652609021325e-01 1.893543106841875240e-02 3.510143161330681605e-01 2.551591444335393061e-02 2.401250317650158284e+00 3.292460342654914274e-01 3.893590161203007161e-01 2.504898658759189753e-02 4.167243612114405749e-01 2.000767031130366511e-02 2.676079999827670752e-01 7.120800384144629347e-02 1.888634556353082890e-03 1.093120532863201856e-02
+2.222996482526195736e-03 2.636134606816340487e-01 6.721359564420303911e-02 3.500545204250677389e-01 1.816502103213700134e-02 3.443661010693954694e-01 2.437197992755635312e-02 2.489413722921670402e+00 3.241005711624331820e-01 3.850631744616042074e-01 2.362833168365734149e-02 4.133310437787030001e-01 1.916177200614344739e-02 2.619194359849002507e-01 6.650009450390369226e-02 1.858276404864667901e-03 1.093231148007235698e-02
+2.559547922699535825e-03 2.579534690804048958e-01 6.248329341293380051e-02 3.422895344603841816e-01 1.752460980624959835e-02 3.377298891318412100e-01 2.332275132216285810e-02 2.558364682243282839e+00 3.028155002281870400e-01 3.809007913054734407e-01 2.221850257195836717e-02 4.101407331471613738e-01 1.832704844274586978e-02 2.562519870192030425e-01 6.197608475348287405e-02 1.834050887332147476e-03 1.101258754228779876e-02
+2.947051702551809708e-03 2.520957774373272509e-01 5.819092905722163256e-02 3.346403534437092375e-01 1.714101322852151776e-02 3.312850281250212525e-01 2.237173918645971038e-02 2.616224206212616199e+00 2.820170227437128752e-01 3.770080965357998637e-01 2.101143674643660142e-02 4.072887576391579501e-01 1.766551193789412180e-02 2.504667428346106228e-01 5.785306983993866725e-02 1.773970499593100327e-03 1.100275325501359064e-02
+3.393221771895329857e-03 2.459663977477073438e-01 5.433592729053100812e-02 3.271459822660970351e-01 1.700250379916794230e-02 3.250662189854978124e-01 2.149372361441809556e-02 2.664071453832157932e+00 2.591501164691561443e-01 3.734165662115483397e-01 2.000461792202202008e-02 4.048090913623783593e-01 1.716012524618305488e-02 2.445305648814212196e-01 5.410262490864738844e-02 1.667103097554767724e-03 1.088759712278922730e-02
+3.906939937054616958e-03 2.396098223329148902e-01 5.075408600889605437e-02 3.198414013139214696e-01 1.695117104665720340e-02 3.190892761415642376e-01 2.076193916247714641e-02 2.703361738998359520e+00 2.348943198277512467e-01 3.702388651748966808e-01 1.924589640809319657e-02 4.029522091222026980e-01 1.667415005121403518e-02 2.383681087910252960e-01 5.068168917302826731e-02 1.441326600843493818e-03 1.055597269877925869e-02
+4.498432668969444201e-03 2.330356550519976366e-01 4.749361865521156689e-02 3.127044316570278260e-01 1.697013496161331458e-02 3.133376104711430021e-01 2.016763699123386502e-02 2.734359509219411066e+00 2.093647295324295721e-01 3.674707962435030395e-01 1.872754861115825606e-02 4.017348062287342536e-01 1.628672313083027665e-02 2.319892979753051809e-01 4.762834954084661787e-02 1.133111800174120770e-03 1.015497735159922622e-02
+5.179474679231212825e-03 2.261992452913900997e-01 4.469904042425093538e-02 3.057224582102559540e-01 1.699590908043196319e-02 3.077876841714057288e-01 1.964276505625673491e-02 2.758211866006374358e+00 1.861954455819890830e-01 3.651543709566169027e-01 1.849667385424182131e-02 4.012688743826219318e-01 1.612393769379286482e-02 2.253572360047917666e-01 4.509760886385179995e-02 8.250997013554856605e-04 9.834742368672041296e-03
+5.963623316594642357e-03 2.193720440093568091e-01 4.244322405118441249e-02 2.987580358603271202e-01 1.698350646721998730e-02 3.023850411522735482e-01 1.912493425027323238e-02 2.774504985860168027e+00 1.666400687266494929e-01 3.631644660679942405e-01 1.852313426204290139e-02 4.014596669927691375e-01 1.621016685026222329e-02 2.186547410285782922e-01 4.304180834075880396e-02 5.225783031542964759e-04 9.682118808705394292e-03
+6.866488450042998112e-03 2.127988271088606664e-01 4.056046583103654207e-02 2.917574658603277471e-01 1.688573868707861358e-02 2.971156431504373696e-01 1.857903113620404389e-02 2.783256351062659117e+00 1.509792672160396587e-01 3.613697329632591937e-01 1.884686786303893524e-02 4.022067458018352970e-01 1.660651255706058552e-02 2.120180754373788279e-01 4.112959981213454297e-02 2.196620091422193694e-04 9.777670084628438396e-03
+7.906043210907700777e-03 2.062948695020566103e-01 3.893139873447144794e-02 2.843368593211069162e-01 1.681345663014428837e-02 2.917553589185331031e-01 1.817519040433353966e-02 2.783431601575428349e+00 1.393981853977233343e-01 3.599985884698498895e-01 1.894759135143099169e-02 4.037796719303393678e-01 1.682389308193954491e-02 2.054952024599659421e-01 3.910274097315720454e-02 6.465371477996794032e-05 9.853363264021901963e-03
+9.102981779915217050e-03 1.998294352173794475e-01 3.744799359483667184e-02 2.764183690813027394e-01 1.677240352800162676e-02 2.862609959838960383e-01 1.792292441671521422e-02 2.774862150044652420e+00 1.356247879444966997e-01 3.590937855869292261e-01 1.862420008020821555e-02 4.062313348717502270e-01 1.671433878925073641e-02 1.990979065002917991e-01 3.675241025220313956e-02 8.806910328906385243e-05 9.974851248186328984e-03
+1.048113134154685273e-02 1.929990618163023031e-01 3.643870103169093849e-02 2.683957146694470608e-01 1.664968342317060812e-02 2.809670602390422212e-01 1.787058971232344598e-02 2.760566329600861124e+00 1.342862511369074530e-01 3.587383657692194139e-01 1.838933416076928354e-02 4.096021530050364401e-01 1.663100415935659868e-02 1.924158112315855207e-01 3.468116330602197955e-02 3.171482432817176946e-04 1.000187217802858686e-02
+1.206792640639328847e-02 1.857823488444939330e-01 3.559963440242130267e-02 2.603813085542192596e-01 1.648241924539920605e-02 2.758555230217916154e-01 1.790916202300373711e-02 2.740601584470428342e+00 1.316902676015173357e-01 3.590838675258292145e-01 1.810842150202576772e-02 4.140619207912534239e-01 1.651829798795584683e-02 1.854170135496832628e-01 3.269045155994187296e-02 5.799295896592430609e-04 1.005157150581206978e-02
+1.389495494373137359e-02 1.780541360478212765e-01 3.418318317891592961e-02 2.528516493048304503e-01 1.631167130656915876e-02 2.709820307109060966e-01 1.766941644182267596e-02 2.715933591822299764e+00 1.253457936888159319e-01 3.605971263979507735e-01 1.727846413265952605e-02 4.201333205693014716e-01 1.610501627869928912e-02 1.779396448739730130e-01 3.013666345014757303e-02 3.114822970866319577e-04 9.469191425773485979e-03
+1.599858719606057217e-02 1.698249555356744211e-01 3.250968300796492488e-02 2.454412035363491285e-01 1.608279935248101838e-02 2.661163936336272684e-01 1.741988443036600678e-02 2.685154192466012724e+00 1.193572415535045228e-01 3.632358100335745243e-01 1.646851468942652982e-02 4.277409593992672066e-01 1.569103380335565637e-02 1.700298343264547984e-01 2.748746995937121143e-02 -2.264305198037682104e-04 9.082497617484723215e-03
+1.842069969326716461e-02 1.610570359923058636e-01 3.039032632467758566e-02 2.380342023676943197e-01 1.566678731000011385e-02 2.611811869294395350e-01 1.710607115617579499e-02 2.647431992894222486e+00 1.132596856420066828e-01 3.670854593027473856e-01 1.607590996233181080e-02 4.369087494880640743e-01 1.536555653489372723e-02 1.617482273608176757e-01 2.480963428505272503e-02 -1.020431403216439433e-03 9.187923955103375478e-03
+2.120950887920190417e-02 1.518615801512106234e-01 2.851356379929024729e-02 2.299729449463003006e-01 1.501825368846311515e-02 2.555368732162919265e-01 1.657233886734713058e-02 2.601477236862831166e+00 1.124669224631803938e-01 3.719259437208161101e-01 1.564933722400820362e-02 4.476547574949510389e-01 1.490362048316696587e-02 1.527655975149912471e-01 2.251480516241462573e-02 -1.382474356128247841e-03 9.261527156274053027e-03
+2.442053094548649744e-02 1.422628656093470334e-01 2.672138258560976393e-02 2.210942033683580832e-01 1.410535612737032901e-02 2.490270984602411253e-01 1.578683478115504302e-02 2.546935186302070164e+00 1.199475922069823675e-01 3.777111457805737982e-01 1.493452592184791455e-02 4.599853897925060275e-01 1.418829428266056916e-02 1.430092730401066048e-01 2.051737691353020324e-02 -1.150576870301327909e-03 9.034459173341608321e-03
+2.811768697974230749e-02 1.325966902854770202e-01 2.419066178086920135e-02 2.113806832062746222e-01 1.327302080709340769e-02 2.416736238336177067e-01 1.513508588606382946e-02 2.486491029640843564e+00 1.260482655459385648e-01 3.844916535152652637e-01 1.415730696398895955e-02 4.741084058178651195e-01 1.354392051007238286e-02 1.329829132227545729e-01 1.894068229293656078e-02 -1.032976855239365775e-03 8.971264702893081908e-03
+3.237457542817643447e-02 1.226953390865770377e-01 2.135648359231674676e-02 2.010104550722290884e-01 1.261859926544734757e-02 2.336302337086081371e-01 1.457823056471699123e-02 2.421072694716483120e+00 1.342529033015319173e-01 3.921073994677974905e-01 1.351712821358325330e-02 4.899005321819326908e-01 1.290842663219851465e-02 1.226335392398657242e-01 1.780633964952213835e-02 -9.397997804967595133e-04 8.692935882434730047e-03
+3.727593720314938130e-02 1.120166449021556820e-01 1.871430554139774660e-02 1.904795563403108294e-01 1.179723723220513047e-02 2.253461563862200756e-01 1.381300800673037656e-02 2.356120187280772438e+00 1.228918399554665974e-01 4.001482596222533661e-01 1.301846949084805444e-02 5.069998482498023762e-01 1.223665176148918537e-02 1.118337544520869309e-01 1.742496501493928651e-02 -6.408239344426302619e-04 8.302376456809017896e-03
+4.291934260128778267e-02 1.007320291741168883e-01 1.618129800655810843e-02 1.800051960086379299e-01 1.117067281878077190e-02 2.169470333902376313e-01 1.310875260072509770e-02 2.283609481004460040e+00 1.135313148672207062e-01 4.082787446409962273e-01 1.250681217077120357e-02 5.254022328108304274e-01 1.153658362340799666e-02 1.006137501380840876e-01 1.708848748546625690e-02 -2.031920197723968160e-04 7.844334085768400744e-03
+4.941713361323833015e-02 8.855959678191771545e-02 1.391047855067414733e-02 1.700304443698350820e-01 1.063802221441497581e-02 2.088010736240945575e-01 1.246416106426008474e-02 2.197861404801443985e+00 1.111765296966798489e-01 4.163302650034106378e-01 1.184699784968673390e-02 5.452891843405023309e-01 1.073810634562078620e-02 8.848080759273516116e-02 1.518087118396405089e-02 4.249763802498653185e-04 7.364352586745040799e-03
+5.689866029018292998e-02 7.740990494068301919e-02 1.356214418262123057e-02 1.597419766487830906e-01 1.031983192120517964e-02 1.999430023240398313e-01 1.227875402144131683e-02 2.094792655510467227e+00 1.086382056511527999e-01 4.230950094590615329e-01 1.194891530270234675e-02 5.652524514330429239e-01 1.069619278512902623e-02 7.822897666014630436e-02 1.450581755983846276e-02 8.179929001365560289e-04 7.394913792027984351e-03
+6.551285568595509312e-02 6.776467100239043362e-02 1.241703192380030039e-02 1.489636232349209610e-01 9.891661300973571963e-03 1.901399864339190227e-01 1.211447935305698040e-02 1.972943483478049043e+00 1.016059983620012930e-01 4.282442244594691916e-01 1.200795544004866315e-02 5.849306960399397459e-01 1.094597870566028225e-02 7.055984249994165514e-02 1.343127930189879152e-02 8.679142277583295692e-04 7.961905998400102200e-03
+7.543120063354614990e-02 5.936116293375629654e-02 1.110733886363024503e-02 1.374355097109261348e-01 9.391002422146497303e-03 1.802463663355282075e-01 1.157107748380932673e-02 1.828797090456429908e+00 8.228479680656052009e-02 4.318306707906224839e-01 1.167129204486152746e-02 6.049652784666068595e-01 1.040855047432872119e-02 6.490824147842133574e-02 1.222417605341719060e-02 4.595161650498316946e-04 7.522894907303059528e-03
+8.685113737513520948e-02 5.211013391628055436e-02 1.007901197854379170e-02 1.250806193599857952e-01 8.847999729440413075e-03 1.699144804931874420e-01 1.113297332290572386e-02 1.665052707183172664e+00 7.019163919958561204e-02 4.338128774948742938e-01 1.137921200278562035e-02 6.252059430678251761e-01 9.753369968095629283e-03 6.099509414279424141e-02 1.155958762644083981e-02 -2.585230646771669201e-05 6.652083723850451408e-03
+1.000000000000000056e-01 4.602117399999999803e-02 9.779521697310451789e-03 1.111706400000000011e-01 8.179965169869038086e-03 1.573932000000000109e-01 1.014231559246146948e-02 1.486650900000000108e+00 6.906649307998630194e-02 4.333629500000000245e-01 1.142205927296057952e-02 6.450658200000000120e-01 9.793002637796645662e-03 5.811938900000000047e-02 1.135948457677850472e-02 1.195304199999999966e-03 6.037528197315490675e-03
+1.163265306122449050e-01 3.942477395804350887e-02 8.597488598936026127e-03 9.631597337328927366e-02 6.965397550310508876e-03 1.433649249121687341e-01 8.481243317234056153e-03 1.278363607352488662e+00 5.851715503791719841e-02 4.319854048377612665e-01 1.186810995140672592e-02 6.681633787721220630e-01 1.017643806531060534e-02 5.498144450814458628e-02 9.629446721598575870e-03 1.010899534971207306e-03 5.145507231446976720e-03
+1.326530612244898044e-01 3.328912908047675090e-02 7.353330218090444077e-03 8.338664560359232425e-02 6.118297067600263900e-03 1.295400522962832646e-01 7.243514263008420107e-03 1.093272570071426530e+00 6.495189916810374664e-02 4.282018388324707603e-01 1.185308202596499320e-02 6.874414050327382064e-01 1.051611966882545074e-02 5.135997732852693592e-02 9.225240289123470827e-03 1.097352653894629422e-03 5.055917088070817182e-03
+1.489795918367347038e-01 2.863095571284317298e-02 6.665202107333106224e-03 7.203485493994034317e-02 5.551827385416897680e-03 1.152544742232293196e-01 6.067835425351424811e-03 9.445472028682869725e-01 6.800362366808562764e-02 4.218097916223478294e-01 1.164441685487899864e-02 7.024060766836944802e-01 1.091152213637827662e-02 4.723050215813651143e-02 8.846513143861974710e-03 1.678606730492365837e-03 5.179296401225694929e-03
+1.653061224489796033e-01 2.483575627569225078e-02 5.995471224548147050e-03 6.209452577759019820e-02 4.962987139647203824e-03 1.011855789151688834e-01 5.985629346896574957e-03 8.244352867442255040e-01 6.036562576446031364e-02 4.121920075337880407e-01 1.152898116787217167e-02 7.139294147955075331e-01 1.133628869175182935e-02 4.311203853746148340e-02 8.441498819331736542e-03 2.284077980409629591e-03 5.150763234594859206e-03
+1.816326530612245027e-01 2.170692908957032666e-02 5.655695672266800901e-03 5.371508942069232184e-02 4.543103177953506690e-03 8.715178587795263665e-02 6.048640095465555692e-03 7.266001875331934423e-01 4.804631794363146274e-02 4.011729322850488000e-01 1.199592104148921971e-02 7.207131843689202899e-01 1.230426517053745221e-02 3.894380473376195945e-02 8.655830837138740047e-03 2.610410238272599208e-03 4.943903025434586221e-03
+1.979591836734694021e-01 1.898480791532573819e-02 4.434780959100406011e-03 4.624481991229831279e-02 4.206659232929604170e-03 7.458677501164867607e-02 5.813092362005962271e-03 6.460934389251358212e-01 5.956328597727415797e-02 3.881920050023857649e-01 1.235297173645639665e-02 7.223708121538943150e-01 1.250032090847500062e-02 3.486831510158874825e-02 8.877602058789275594e-03 2.967414949183228793e-03 4.904595754975780986e-03
+2.142857142857143016e-01 1.695823321591360261e-02 3.469831512302461781e-03 4.009459034116432535e-02 3.987682791304650483e-03 6.249873547602553120e-02 5.934263570124668347e-03 5.754064633997411082e-01 5.481677133626022824e-02 3.746110031864225420e-01 1.231830164869357133e-02 7.189764487567673612e-01 1.251781872499279843e-02 3.069355929448877585e-02 9.246090313310376119e-03 3.265868387940330363e-03 4.908035171917084563e-03
+2.306122448979592010e-01 1.548757243205519177e-02 3.091673368091698692e-03 3.440335061992415289e-02 3.727169143264499451e-03 5.181022996671015568e-02 6.203143677695365475e-03 5.137991377127972825e-01 5.361156055870076875e-02 3.599051318315605874e-01 1.184557152524235400e-02 7.117412524307588084e-01 1.229772223944200320e-02 2.650617824238633846e-02 9.284838420715521567e-03 3.536738512125343209e-03 4.936859069713762579e-03
+2.469387755102041004e-01 1.433010380039563374e-02 3.566130690184089388e-03 2.969381220895894735e-02 3.816188278520973951e-03 4.197209834064773853e-02 6.324876696238434202e-03 4.552507567815178291e-01 5.065601192147932419e-02 3.440020547264790451e-01 1.121825444520186364e-02 7.013582295319235049e-01 1.194003310693880424e-02 2.240256390271565257e-02 8.949419548762920645e-03 3.837899969176820913e-03 4.971917106999092055e-03
+2.632653061224490276e-01 1.324416138142683191e-02 4.509383362223045449e-03 2.561524157497386753e-02 3.639102757811946837e-03 3.334547108629661721e-02 6.389725451675682712e-03 3.996382053409509028e-01 4.755273215299854472e-02 3.271252872232493503e-01 1.065073119380046708e-02 6.881045284550147967e-01 1.155866526993589057e-02 1.859721874914666007e-02 8.246200233160297030e-03 4.111820182104665954e-03 4.998624135573791899e-03
+2.795918367346938993e-01 1.225172070807768916e-02 5.634363331159562317e-03 2.208906558513888674e-02 3.630724365966011072e-03 2.572917981391273021e-02 6.174116090313300567e-03 3.411444192178936241e-01 4.570775803372454510e-02 3.102239205264454847e-01 1.025899698482179469e-02 6.714215205856344237e-01 1.105528830905571497e-02 1.529095779162716572e-02 7.193589407121427949e-03 4.361592170167226737e-03 5.058001219672858366e-03
+2.959183673469387710e-01 1.124413457635646540e-02 6.411421240065154868e-03 1.853378533508788159e-02 3.323430862555169698e-03 1.979955870424673567e-02 6.280524682915253863e-03 2.941440729953202493e-01 5.179924323811337800e-02 2.924400641576166415e-01 9.842133598727825436e-03 6.526424526523527669e-01 1.089458089702714447e-02 1.223344377289717171e-02 6.144280702266012063e-03 4.677260234324302941e-03 4.992584070232586671e-03
+3.122448979591836982e-01 1.079021243743692043e-02 6.632527583782697571e-03 1.517791038369432736e-02 3.473349017361928009e-03 1.449152434907221426e-02 6.331541371998266818e-03 2.601290037822677204e-01 4.622670247373922986e-02 2.746771419636072631e-01 1.037543665308320734e-02 6.316226608424150291e-01 1.077018659836364786e-02 9.472423507792282829e-03 5.434167994449804785e-03 4.969434566622568314e-03 5.018129402137327423e-03
+3.285714285714286254e-01 1.010737878689214712e-02 6.779166903760069050e-03 1.355770075092034913e-02 5.142614075637582348e-03 8.989087468730809147e-03 7.595376940382754342e-03 2.302641998343407104e-01 4.244869960571524825e-02 2.579208516735705392e-01 1.132196327225507872e-02 6.077500186539003257e-01 1.116952747323463405e-02 7.527646548402747509e-03 4.690624602008485605e-03 4.972232371569194724e-03 4.961915159579504894e-03
+3.448979591836734970e-01 8.811830531947489192e-03 6.611065661575015427e-03 1.094030944405336402e-02 3.876749683563606840e-03 6.216513476467545873e-03 5.946630908343609155e-03 2.048947603867307399e-01 3.692175416096520058e-02 2.392713041855997202e-01 9.706181968344242822e-03 5.836326619162366391e-01 1.101364075145081986e-02 6.513274540608479310e-03 3.949206138181179417e-03 4.986885046894676439e-03 4.867251829657326737e-03
+3.612244897959183687e-01 6.825401095665947424e-03 6.015622156291653214e-03 8.592487438390646048e-03 4.986458385358475201e-03 4.643358758758497212e-03 7.666704564216820714e-03 1.820567154916601094e-01 3.491710247794299615e-02 2.200942895644345532e-01 1.088058989779359248e-02 5.581722686890733254e-01 1.089771491601521959e-02 6.324699667870168490e-03 3.825037709656281537e-03 5.194980618384773159e-03 4.671733073712271492e-03
+3.775510204081632404e-01 4.622927028232834239e-03 5.555000630649990605e-03 7.006103884160290625e-03 4.558050434345648980e-03 4.026478403530274301e-03 8.802408761178621283e-03 1.654109241587360668e-01 2.958093259719593235e-02 2.015352169703905072e-01 1.147618085254958030e-02 5.309647906816499230e-01 1.029080153886721520e-02 6.653667513086839770e-03 5.290996518088139297e-03 5.112247513943503525e-03 4.533700952241386320e-03
+3.938775510204082231e-01 3.224999414965747189e-03 5.629233313847112341e-03 5.636815763899808741e-03 5.167788915004078130e-03 4.184595912678558752e-03 1.027649072980537683e-02 1.480349508988084706e-01 2.827395372375652097e-02 1.834778690223543285e-01 1.243506896536910114e-02 5.026172192427382779e-01 9.833385791748083804e-03 6.681477882536569768e-03 6.106976428266843067e-03 4.843877522511543486e-03 4.354338837886682044e-03
+4.102040816326530948e-01 2.398997430438228398e-03 5.497597477770171416e-03 5.104961177614576566e-03 3.623114640176146664e-03 3.847370634377959581e-03 9.857259957830802680e-03 1.325798271673375717e-01 2.828630531659322850e-02 1.665215033712220405e-01 1.196507375293896780e-02 4.729472143835790066e-01 8.930840437713421717e-03 6.632437655444375565e-03 5.646533706492752776e-03 4.581162311501288040e-03 4.183485454464925569e-03
+4.265306122448979664e-01 1.692190277964546229e-03 5.335104021406694361e-03 4.283378024890269545e-03 4.788897464910948573e-03 3.545439000884104044e-03 1.092082872314948001e-02 1.182493409930097245e-01 2.916021507299379825e-02 1.500708105276261917e-01 1.252551497739448627e-02 4.424769600331791564e-01 9.367904277973288299e-03 7.260241880531962191e-03 5.627655291587668578e-03 4.575902333115733529e-03 4.080267829835968439e-03
+4.428571428571428381e-01 4.148649701266470154e-04 5.113805095634805543e-03 2.771418381560752930e-03 6.357959201349411803e-03 4.286382242885526835e-03 1.387159887015452196e-02 1.066391781871470490e-01 2.847724416602226416e-02 1.335042960430444514e-01 1.520917078076000975e-02 4.126698332598315866e-01 1.059838981925867105e-02 8.237569708429254636e-03 6.039312886020845907e-03 4.699505512860680662e-03 4.310263817980366542e-03
+4.591836734693878208e-01 -1.304228855688523885e-03 5.470247749666199907e-03 4.239601722136225777e-03 5.808362884320261189e-03 2.590113200612944883e-03 1.462556815161407398e-02 1.011350726109566528e-01 3.463682731939739246e-02 1.207215482096496983e-01 1.590959814892967464e-02 3.809692818739471298e-01 1.056008118753932676e-02 9.256739924858644469e-03 6.591637446377524370e-03 4.411877109285056511e-03 4.028341424745536872e-03
+4.755102040816326925e-01 -2.704550331251799004e-03 6.117853355337987686e-03 4.144939763097098794e-03 6.157426353526132545e-03 2.280409685916162145e-03 1.495259847782390815e-02 9.174674898459940497e-02 3.317719061506606976e-02 1.080260917482173538e-01 1.553631936069132559e-02 3.512845707804219120e-01 1.082288870256591616e-02 9.983724737793020498e-03 7.020314509780686135e-03 3.912002421612906407e-03 3.990957946643638315e-03
+4.918367346938775642e-01 -3.535521323528168034e-03 6.489852248896367873e-03 2.599970243847376378e-03 5.548935444868298081e-03 2.808998904941583091e-03 1.473662895226150044e-02 8.739403248853304385e-02 3.458909252220616903e-02 9.472548541323846938e-02 1.519063342865388695e-02 3.239722561384902888e-01 1.024387162143642650e-02 1.052513329328826422e-02 7.082609771856901437e-03 3.508374796053288693e-03 3.946269400029071145e-03
+5.081632653061224358e-01 -3.773172936203268539e-03 6.637549040490199267e-03 -2.516103480873571000e-04 6.572923314060078771e-03 3.953622982447484863e-03 1.573471739266091296e-02 7.986314864670505942e-02 3.453373395965902981e-02 8.094667899419712154e-02 1.582852538281763824e-02 2.987031131287182784e-01 1.045563517738316676e-02 1.103534588189305880e-02 6.889468087760076270e-03 3.299279920697378594e-03 3.769594688649386023e-03
+5.244897959183674185e-01 -3.936045802592954765e-03 6.697487600261068513e-03 -1.639720875129115768e-03 6.037240612839354058e-03 3.407874012965536603e-03 1.553999947376006815e-02 7.216510147179601431e-02 2.968070892373265643e-02 6.941026401027031167e-02 1.521944184007802646e-02 2.729564461704206968e-01 1.074585510624960147e-02 1.101060982716685691e-02 6.786395115007838943e-03 3.450148591760391226e-03 3.710058644842988921e-03
+5.408163265306122902e-01 -4.125728968283484156e-03 6.412024156289482767e-03 -2.116395666802422821e-03 6.043906253157798475e-03 2.404454447424684421e-03 1.530205768622161684e-02 6.645926254942295663e-02 2.713087028010289364e-02 5.930041595975332064e-02 1.503097080754000302e-02 2.480024241700939380e-01 1.119419072927162716e-02 1.054112334574236129e-02 6.634480714590747577e-03 3.549779478918272963e-03 3.698483399590634692e-03
+5.571428571428571619e-01 -4.359718195743628177e-03 5.818326311771436325e-03 -3.097840758134474178e-03 5.749631410236337795e-03 2.644218077709412518e-03 1.513240561439272575e-02 5.877459162780401120e-02 2.555514304330760988e-02 4.907678969758672710e-02 1.525354296439180614e-02 2.252591619506083553e-01 1.056903482710983173e-02 1.017555195366732883e-02 6.611416243887415639e-03 3.395360677725176805e-03 3.549039373020256537e-03
+5.734693877551020336e-01 -3.920870475797691088e-03 5.568310082063020368e-03 -3.577983923135422562e-03 6.279601865306166236e-03 2.625291945255302500e-03 1.492559749435511118e-02 5.338086792246633794e-02 2.502004244557643961e-02 4.086042706797585189e-02 1.521854809216812571e-02 2.033308396707086130e-01 9.862655545891154882e-03 9.557252016347817652e-03 6.754595096107933634e-03 2.692149192139742069e-03 3.236751522896792223e-03
+5.897959183673470163e-01 -3.129001712471175985e-03 5.478478115272785408e-03 -2.507962595204027423e-03 8.400804333261060672e-03 1.286110393801345947e-03 1.461667280101169863e-02 4.876099631660012895e-02 2.707226383643828588e-02 3.536451685924833083e-02 1.541250960967785640e-02 1.810233365604385358e-01 1.092837393135298769e-02 8.796965920370821562e-03 6.701452952848836395e-03 1.834975916214879547e-03 3.320370979116342262e-03
+6.061224489795918879e-01 -2.416380765101757214e-03 5.519673432110980792e-03 -2.595344859342544035e-03 8.865376650415127471e-03 1.138349742385753871e-03 1.365996083221193529e-02 4.207712352396885541e-02 2.305135101626111169e-02 2.937774651292302322e-02 1.492224573979315447e-02 1.609589521154481395e-01 1.120069476152043240e-02 7.858155818736972664e-03 6.622386339097939036e-03 1.434505641425406911e-03 3.229120908684111283e-03
+6.224489795918367596e-01 -1.926066507583772255e-03 5.379437327106514552e-03 -2.318205541359436277e-03 9.966182175956909278e-03 5.681395412635522647e-04 1.277943878136280839e-02 3.580485555936888109e-02 2.185199910362296480e-02 2.483769891597548476e-02 1.420870934691557778e-02 1.415895843814952704e-01 1.196303501419522346e-02 6.993280944422240221e-03 6.498694781547665299e-03 1.255560850425938837e-03 3.015839887985304404e-03
+6.387755102040816313e-01 -1.763291018474010029e-03 5.011026558907828027e-03 -3.517692703172981862e-03 1.049362282662101922e-02 1.412161992931055187e-03 1.261028606768314214e-02 3.021147633166515814e-02 2.208234850326066048e-02 1.964669379999844923e-02 1.375236258345650942e-02 1.247038505112701839e-01 1.174567493327440676e-02 6.104164691998323691e-03 6.336567227702525652e-03 1.511434556359628157e-03 2.918041857848065277e-03
+6.551020408163265030e-01 -1.371265663515872871e-03 4.815576566171758145e-03 -3.924797227226901192e-03 1.042963482885535947e-02 2.146702536331560659e-03 1.226619336219402986e-02 2.484776981462179582e-02 2.198756275659718984e-02 1.600648495888330053e-02 1.352795817211750219e-02 1.085126989031491018e-01 1.129038558040623003e-02 5.124397578212483255e-03 5.955471233002406807e-03 1.375912011189511308e-03 2.643676721640121109e-03
+6.714285714285714857e-01 -7.347387558144375915e-04 5.047944037270904634e-03 -4.551858755871746043e-03 1.084756265045347563e-02 3.133211172357025683e-03 1.289036313686338538e-02 1.988113039811458330e-02 2.200991807516783014e-02 1.371824458342043099e-02 1.417405976351782983e-02 9.332242088573453054e-02 1.158142679674851495e-02 4.165764440781732046e-03 5.813900741335659844e-03 1.063345719439776462e-03 2.353021475347306501e-03
+6.877551020408163573e-01 -5.653365196498312453e-04 5.556716834071377675e-03 -5.735036617494569146e-03 9.716728775460639275e-03 4.848935490069965042e-03 1.148048995812154215e-02 1.400636390703255754e-02 2.365106623106593212e-02 1.137234890484222112e-02 1.299612576350017527e-02 8.010839740159633737e-02 1.022623955221433310e-02 3.825667716712735211e-03 6.199468939947366981e-03 6.893171889063837545e-04 2.151276015229100676e-03
+7.040816326530612290e-01 -2.267332661212563918e-04 6.119802600425810341e-03 -6.257262548063195068e-03 1.014102716994294030e-02 5.921973125223791920e-03 1.125687254220730966e-02 1.145974167737420432e-02 1.887010255106317397e-02 9.773630232330304465e-03 1.279353067078748546e-02 6.807985569865412923e-02 1.038097601483088488e-02 3.454995932446520372e-03 6.744031583403861836e-03 2.400689127714897590e-04 2.094057650527441673e-03
+7.204081632653062117e-01 1.026320665191617987e-03 7.680441173733227114e-03 -6.142834477869558822e-03 1.066679325465065900e-02 6.583132456638965575e-03 1.175729386615498133e-02 7.837588375768960586e-03 1.719052997285148640e-02 7.501140602779903140e-03 1.282082811316204857e-02 5.772865008052108921e-02 1.071429636674453914e-02 2.259823363842622911e-03 8.315739003339289726e-03 -1.202600787526237567e-05 2.053675732589019861e-03
+7.367346938775510834e-01 2.723773866630701743e-03 1.055738113017332461e-02 -5.354367355641570514e-03 1.216566243602633379e-02 5.902165687426506359e-03 1.268637380600316431e-02 5.019045671291559423e-03 1.703959974523763451e-02 6.386503919608849111e-03 1.367938696443783166e-02 4.815571853068702224e-02 1.220396766522050848e-02 6.314853168272392377e-04 1.119384842486024524e-02 -1.513013065019109627e-04 1.912085561785451492e-03
+7.530612244897959551e-01 3.347111420314353037e-03 1.138156833685597703e-02 -4.491012694336645426e-03 1.270622481303653774e-02 5.588513599931386501e-03 1.195055443286740030e-02 3.380609462740184409e-03 1.479054695636173103e-02 5.150940857924307670e-03 1.260427596906943566e-02 3.941675457359115203e-02 1.293771834964407147e-02 9.440130082234371250e-05 1.213155144191191588e-02 -2.055206984531875094e-04 1.854542395242731595e-03
+7.693877551020408267e-01 3.700903966783544458e-03 1.181247737394512318e-02 -5.013588714372360991e-03 1.144484866240926858e-02 5.721807343038266824e-03 1.158017304292606117e-02 9.324618436404591567e-05 1.621917485475198040e-02 3.601692683394015077e-03 1.197341369650416383e-02 3.326630455306706785e-02 1.201717042389163770e-02 -3.686189977476679873e-04 1.260123117591606043e-02 -2.329992136797600396e-04 1.812885362980177790e-03
+7.857142857142856984e-01 3.950074419242127592e-03 1.291688207383976522e-02 -5.693798006173376829e-03 1.073626677616170040e-02 5.657948365304832708e-03 1.177402930489857696e-02 -1.538774680937169846e-03 1.427911879458286688e-02 2.210276236893728759e-03 1.205791082848095379e-02 2.854265247624555857e-02 1.152778484090345723e-02 -9.862004915652249504e-04 1.365975660245233141e-02 -2.027384854668734027e-04 1.637337392279653199e-03
+8.020408163265306811e-01 3.898205963925653779e-03 1.235501536786283439e-02 -4.305580344407663670e-03 9.784254832731606802e-03 4.803772217549897791e-03 1.044511582683799698e-02 -3.292689738769052563e-03 1.302317972988607450e-02 1.559251545478106691e-03 1.051003276106219318e-02 2.272068462983138637e-02 1.039916298616219877e-02 -1.284235245889934389e-03 1.311586386815231584e-02 -1.425192807261697631e-04 1.533165760304424849e-03
+8.183673469387755528e-01 3.588619797058547126e-03 1.137225324329994153e-02 -3.492723870721761714e-03 9.000290850226813633e-03 4.198696164673327083e-03 9.731260435807975034e-03 -4.768501625455401523e-03 1.556994705567193819e-02 8.465323511627092547e-04 9.696491188922193652e-03 1.825700122372798245e-02 9.575460541838392778e-03 -1.342621206227992588e-03 1.204775011225409434e-02 -1.138655908763324358e-04 1.448744408941626295e-03
+8.346938775510204245e-01 3.441281250341821953e-03 1.017984496958066962e-02 -3.041726663775672686e-03 8.010487218373465307e-03 3.556407472593775022e-03 8.593753259920606721e-03 -5.230190570117304073e-03 1.414505616898533748e-02 3.378756927483065947e-04 8.721503560001611297e-03 1.483899533506974518e-02 8.458777665579302399e-03 -1.553847558480263465e-03 1.070251811142967913e-02 -1.259014983128754327e-04 1.219932345173984842e-03
+8.510204081632654072e-01 3.210758257312623220e-03 9.544603477465968266e-03 -2.700052505113971144e-03 7.163601897606069677e-03 3.003263435210558937e-03 7.739678550482364389e-03 -5.504081810805650996e-03 1.310886165854219070e-02 4.341769438769502301e-05 7.867103569669024049e-03 1.184481233652320484e-02 7.668364582451015800e-03 -1.641010957639634947e-03 9.910946594650287683e-03 -1.269871109435181890e-04 1.005675751561662876e-03
+8.673469387755102789e-01 2.754430250123140935e-03 8.728939656345335515e-03 -2.194652043508042282e-03 6.162234962480548327e-03 2.365811147249049975e-03 6.742651502665567703e-03 -5.565775093366548236e-03 1.283554913783929025e-02 -3.583154109628419639e-05 6.812644339057771087e-03 9.122462586070438292e-03 6.634621311653081215e-03 -1.499827068684795933e-03 9.068969561710620367e-03 -1.141045310238823546e-04 8.086130827908307415e-04
+8.836734693877551505e-01 2.456910492806845328e-03 8.514967898016780751e-03 -2.151372510115513118e-03 5.118589556761047633e-03 1.872728731270310734e-03 5.849475693755866804e-03 -5.301125605810848816e-03 1.208554103662637380e-02 -1.249919176342527058e-04 5.929764783898339164e-03 7.202425593584829006e-03 5.472872084153720604e-03 -1.503502693659317579e-03 8.838236264176898421e-03 -8.122948150597573426e-05 6.464492371260225972e-04
+9.000000000000000222e-01 2.302187455270835164e-03 8.739502279132595197e-03 -2.384168039911821121e-03 5.326226440830149171e-03 1.337487979259191582e-03 6.010268189772444321e-03 -4.864826018318149127e-03 1.070561871116822161e-02 -1.930248019052008528e-04 6.042746812240025120e-03 6.006656214330170773e-03 5.044743278601566266e-03 -1.651540834005031004e-03 9.021929051782274361e-03 -2.604869388541081849e-05 6.175356181904082626e-04
diff --git a/examples/qPDF/data/full/c+.dat b/examples/qPDF/data/full/c+.dat
new file mode 100644
index 000000000..0f80bcc1a
--- /dev/null
+++ b/examples/qPDF/data/full/c+.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 9.543745599019025317e-04 8.933750230847316287e-02
+1.151395399326446927e-04 3.636680441961381328e-03 7.412584225106703950e-02
+1.325711365590109543e-04 4.933524249824835194e-03 6.407245802265681967e-02
+1.526417967175233259e-04 5.655341068586110242e-03 5.779131852221603555e-02
+1.757510624854791170e-04 5.894428576734326262e-03 5.400134138166334807e-02
+2.023589647725157589e-04 6.105220955743916912e-03 4.880768069883391347e-02
+2.329951810515371805e-04 6.235027687244114672e-03 4.349911980376581361e-02
+2.682695795279724476e-04 6.268431070720082224e-03 3.967181908221596026e-02
+3.088843596477481527e-04 6.217020373943566401e-03 3.686609060461188553e-02
+3.556480306223128661e-04 6.047944115574644883e-03 3.465683609043487129e-02
+4.094915062380427508e-04 6.086277759868033259e-03 3.255440885733468931e-02
+4.714866363457394672e-04 6.353607011376051841e-03 3.045711151395184141e-02
+5.428675439323859403e-04 6.428561308584479321e-03 2.867200099971633012e-02
+6.250551925273975734e-04 6.372404207167801926e-03 2.704047344363377792e-02
+7.196856730011521675e-04 6.258801326906005338e-03 2.571516118777497986e-02
+8.286427728546842068e-04 6.047430101503004259e-03 2.473241740672309924e-02
+9.540954763499943534e-04 5.727973869072462351e-03 2.400751060148542324e-02
+1.098541141987558400e-03 5.217025855255588707e-03 2.331562484684581577e-02
+1.264855216855295715e-03 4.520400786895952851e-03 2.287525171199299504e-02
+1.456348477501244378e-03 4.104105861430898221e-03 2.251634616463276534e-02
+1.676832936811008387e-03 3.888985899011146528e-03 2.211418539704611622e-02
+1.930697728883249645e-03 3.777269112706165780e-03 2.186241065726403712e-02
+2.222996482526195736e-03 3.716552809729335802e-03 2.186462296014471396e-02
+2.559547922699535825e-03 3.668101774664294951e-03 2.202517508457559753e-02
+2.947051702551809708e-03 3.547940999186200654e-03 2.200550651002718128e-02
+3.393221771895329857e-03 3.334206195109535448e-03 2.177519424557845459e-02
+3.906939937054616958e-03 2.882653201686987635e-03 2.111194539755851737e-02
+4.498432668969444201e-03 2.266223600348241541e-03 2.030995470319845245e-02
+5.179474679231212825e-03 1.650199402710971321e-03 1.966948473734408259e-02
+5.963623316594642357e-03 1.045156606308592952e-03 1.936423761741078858e-02
+6.866488450042998112e-03 4.393240182844387388e-04 1.955534016925687679e-02
+7.906043210907700777e-03 1.293074295599358806e-04 1.970672652804380393e-02
+9.102981779915217050e-03 1.761382065781277049e-04 1.994970249637265797e-02
+1.048113134154685273e-02 6.342964865634353891e-04 2.000374435605717371e-02
+1.206792640639328847e-02 1.159859179318486122e-03 2.010314301162413955e-02
+1.389495494373137359e-02 6.229645941732639153e-04 1.893838285154697196e-02
+1.599858719606057217e-02 -4.528610396075364209e-04 1.816499523496944643e-02
+1.842069969326716461e-02 -2.040862806432878866e-03 1.837584791020675096e-02
+2.120950887920190417e-02 -2.764948712256495682e-03 1.852305431254810605e-02
+2.442053094548649744e-02 -2.301153740602655817e-03 1.806891834668321664e-02
+2.811768697974230749e-02 -2.065953710478731550e-03 1.794252940578616382e-02
+3.237457542817643447e-02 -1.879599560993519027e-03 1.738587176486946009e-02
+3.727593720314938130e-02 -1.281647868885260524e-03 1.660475291361803579e-02
+4.291934260128778267e-02 -4.063840395447936319e-04 1.568866817153680149e-02
+4.941713361323833015e-02 8.499527604997306370e-04 1.472870517349008160e-02
+5.689866029018292998e-02 1.635985800273112058e-03 1.478982758405596870e-02
+6.551285568595509312e-02 1.735828455516659138e-03 1.592381199680020440e-02
+7.543120063354614990e-02 9.190323300996633893e-04 1.504578981460611906e-02
+8.685113737513520948e-02 -5.170461293543338402e-05 1.330416744770090282e-02
+1.000000000000000056e-01 2.390608399999999932e-03 1.207505639463098135e-02
+1.000000000000000056e-01 2.390608399999999932e-03 1.207505639463098135e-02
+1.163265306122449050e-01 2.021799069942414612e-03 1.029101446289395344e-02
+1.326530612244898044e-01 2.194705307789258843e-03 1.011183417614163436e-02
+1.489795918367347038e-01 3.357213460984731675e-03 1.035859280245138986e-02
+1.653061224489796033e-01 4.568155960819259183e-03 1.030152646918971841e-02
+1.816326530612245027e-01 5.220820476545198416e-03 9.887806050869172442e-03
+1.979591836734694021e-01 5.934829898366457586e-03 9.809191509951561971e-03
+2.142857142857143016e-01 6.531736775880660727e-03 9.816070343834169126e-03
+2.306122448979592010e-01 7.073477024250686418e-03 9.873718139427525159e-03
+2.469387755102041004e-01 7.675799938353641827e-03 9.943834213998184110e-03
+2.632653061224490276e-01 8.223640364209331907e-03 9.997248271147583798e-03
+2.795918367346938993e-01 8.723184340334453474e-03 1.011600243934571673e-02
+2.959183673469387710e-01 9.354520468648605883e-03 9.985168140465173342e-03
+3.122448979591836982e-01 9.938869133245136628e-03 1.003625880427465485e-02
+3.285714285714286254e-01 9.944464743138389448e-03 9.923830319159009788e-03
+3.448979591836734970e-01 9.973770093789352878e-03 9.734503659314653473e-03
+3.612244897959183687e-01 1.038996123676954632e-02 9.343466147424542984e-03
+3.775510204081632404e-01 1.022449502788700705e-02 9.067401904482772640e-03
+3.938775510204082231e-01 9.687755045023086972e-03 8.708677675773364088e-03
+4.102040816326530948e-01 9.162324623002576079e-03 8.366970908929851139e-03
+4.265306122448979664e-01 9.151804666231467059e-03 8.160535659671936878e-03
+4.428571428571428381e-01 9.399011025721361323e-03 8.620527635960733084e-03
+4.591836734693878208e-01 8.823754218570113023e-03 8.056682849491073745e-03
+4.755102040816326925e-01 7.824004843225812814e-03 7.981915893287276631e-03
+4.918367346938775642e-01 7.016749592106577386e-03 7.892538800058142290e-03
+5.081632653061224358e-01 6.598559841394757187e-03 7.539189377298772046e-03
+5.244897959183674185e-01 6.900297183520782453e-03 7.420117289685977842e-03
+5.408163265306122902e-01 7.099558957836545926e-03 7.396966799181269384e-03
+5.571428571428571619e-01 6.790721355450353609e-03 7.098078746040513073e-03
+5.734693877551020336e-01 5.384298384279484137e-03 6.473503045793584446e-03
+5.897959183673470163e-01 3.669951832429759093e-03 6.640741958232684525e-03
+6.061224489795918879e-01 2.869011282850813822e-03 6.458241817368222566e-03
+6.224489795918367596e-01 2.511121700851877674e-03 6.031679775970608809e-03
+6.387755102040816313e-01 3.022869112719256315e-03 5.836083715696130554e-03
+6.551020408163265030e-01 2.751824022379022617e-03 5.287353443280242218e-03
+6.714285714285714857e-01 2.126691438879552924e-03 4.706042950694613002e-03
+6.877551020408163573e-01 1.378634377812767509e-03 4.302552030458201351e-03
+7.040816326530612290e-01 4.801378255429795180e-04 4.188115301054883347e-03
+7.204081632653062117e-01 -2.405201575052475134e-05 4.107351465178039722e-03
+7.367346938775510834e-01 -3.026026130038219253e-04 3.824171123570902983e-03
+7.530612244897959551e-01 -4.110413969063750187e-04 3.709084790485463190e-03
+7.693877551020408267e-01 -4.659984273595200793e-04 3.625770725960355580e-03
+7.857142857142856984e-01 -4.054769709337468055e-04 3.274674784559306398e-03
+8.020408163265306811e-01 -2.850385614523395263e-04 3.066331520608849698e-03
+8.183673469387755528e-01 -2.277311817526648716e-04 2.897488817883252590e-03
+8.346938775510204245e-01 -2.518029966257508655e-04 2.439864690347969683e-03
+8.510204081632654072e-01 -2.539742218870363779e-04 2.011351503123325751e-03
+8.673469387755102789e-01 -2.282090620477647091e-04 1.617226165581661483e-03
+8.836734693877551505e-01 -1.624589630119514685e-04 1.292898474252045194e-03
+9.000000000000000222e-01 -5.209738777082163698e-05 1.235071236380816525e-03
diff --git a/examples/qPDF/data/full/c.dat b/examples/qPDF/data/full/c.dat
new file mode 100644
index 000000000..7d4ec6ac5
--- /dev/null
+++ b/examples/qPDF/data/full/c.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 4.771872799509512659e-04 4.466875115423658144e-02
+1.151395399326446927e-04 1.818340220980690664e-03 3.706292112553351975e-02
+1.325711365590109543e-04 2.466762124912417597e-03 3.203622901132840983e-02
+1.526417967175233259e-04 2.827670534293055121e-03 2.889565926110801777e-02
+1.757510624854791170e-04 2.947214288367163131e-03 2.700067069083167404e-02
+2.023589647725157589e-04 3.052610477871958456e-03 2.440384034941695673e-02
+2.329951810515371805e-04 3.117513843622057336e-03 2.174955990188290680e-02
+2.682695795279724476e-04 3.134215535360041112e-03 1.983590954110798013e-02
+3.088843596477481527e-04 3.108510186971783201e-03 1.843304530230594276e-02
+3.556480306223128661e-04 3.023972057787322441e-03 1.732841804521743564e-02
+4.094915062380427508e-04 3.043138879934016629e-03 1.627720442866734465e-02
+4.714866363457394672e-04 3.176803505688025921e-03 1.522855575697592070e-02
+5.428675439323859403e-04 3.214280654292239661e-03 1.433600049985816506e-02
+6.250551925273975734e-04 3.186202103583900963e-03 1.352023672181688896e-02
+7.196856730011521675e-04 3.129400663453002669e-03 1.285758059388748993e-02
+8.286427728546842068e-04 3.023715050751502129e-03 1.236620870336154962e-02
+9.540954763499943534e-04 2.863986934536231176e-03 1.200375530074271162e-02
+1.098541141987558400e-03 2.608512927627794353e-03 1.165781242342290788e-02
+1.264855216855295715e-03 2.260200393447976425e-03 1.143762585599649752e-02
+1.456348477501244378e-03 2.052052930715449110e-03 1.125817308231638267e-02
+1.676832936811008387e-03 1.944492949505573264e-03 1.105709269852305811e-02
+1.930697728883249645e-03 1.888634556353082890e-03 1.093120532863201856e-02
+2.222996482526195736e-03 1.858276404864667901e-03 1.093231148007235698e-02
+2.559547922699535825e-03 1.834050887332147476e-03 1.101258754228779876e-02
+2.947051702551809708e-03 1.773970499593100327e-03 1.100275325501359064e-02
+3.393221771895329857e-03 1.667103097554767724e-03 1.088759712278922730e-02
+3.906939937054616958e-03 1.441326600843493818e-03 1.055597269877925869e-02
+4.498432668969444201e-03 1.133111800174120770e-03 1.015497735159922622e-02
+5.179474679231212825e-03 8.250997013554856605e-04 9.834742368672041296e-03
+5.963623316594642357e-03 5.225783031542964759e-04 9.682118808705394292e-03
+6.866488450042998112e-03 2.196620091422193694e-04 9.777670084628438396e-03
+7.906043210907700777e-03 6.465371477996794032e-05 9.853363264021901963e-03
+9.102981779915217050e-03 8.806910328906385243e-05 9.974851248186328984e-03
+1.048113134154685273e-02 3.171482432817176946e-04 1.000187217802858686e-02
+1.206792640639328847e-02 5.799295896592430609e-04 1.005157150581206978e-02
+1.389495494373137359e-02 3.114822970866319577e-04 9.469191425773485979e-03
+1.599858719606057217e-02 -2.264305198037682104e-04 9.082497617484723215e-03
+1.842069969326716461e-02 -1.020431403216439433e-03 9.187923955103375478e-03
+2.120950887920190417e-02 -1.382474356128247841e-03 9.261527156274053027e-03
+2.442053094548649744e-02 -1.150576870301327909e-03 9.034459173341608321e-03
+2.811768697974230749e-02 -1.032976855239365775e-03 8.971264702893081908e-03
+3.237457542817643447e-02 -9.397997804967595133e-04 8.692935882434730047e-03
+3.727593720314938130e-02 -6.408239344426302619e-04 8.302376456809017896e-03
+4.291934260128778267e-02 -2.031920197723968160e-04 7.844334085768400744e-03
+4.941713361323833015e-02 4.249763802498653185e-04 7.364352586745040799e-03
+5.689866029018292998e-02 8.179929001365560289e-04 7.394913792027984351e-03
+6.551285568595509312e-02 8.679142277583295692e-04 7.961905998400102200e-03
+7.543120063354614990e-02 4.595161650498316946e-04 7.522894907303059528e-03
+8.685113737513520948e-02 -2.585230646771669201e-05 6.652083723850451408e-03
+1.000000000000000056e-01 1.195304199999999966e-03 6.037528197315490675e-03
+1.163265306122449050e-01 1.010899534971207306e-03 5.145507231446976720e-03
+1.326530612244898044e-01 1.097352653894629422e-03 5.055917088070817182e-03
+1.489795918367347038e-01 1.678606730492365837e-03 5.179296401225694929e-03
+1.653061224489796033e-01 2.284077980409629591e-03 5.150763234594859206e-03
+1.816326530612245027e-01 2.610410238272599208e-03 4.943903025434586221e-03
+1.979591836734694021e-01 2.967414949183228793e-03 4.904595754975780986e-03
+2.142857142857143016e-01 3.265868387940330363e-03 4.908035171917084563e-03
+2.306122448979592010e-01 3.536738512125343209e-03 4.936859069713762579e-03
+2.469387755102041004e-01 3.837899969176820913e-03 4.971917106999092055e-03
+2.632653061224490276e-01 4.111820182104665954e-03 4.998624135573791899e-03
+2.795918367346938993e-01 4.361592170167226737e-03 5.058001219672858366e-03
+2.959183673469387710e-01 4.677260234324302941e-03 4.992584070232586671e-03
+3.122448979591836982e-01 4.969434566622568314e-03 5.018129402137327423e-03
+3.285714285714286254e-01 4.972232371569194724e-03 4.961915159579504894e-03
+3.448979591836734970e-01 4.986885046894676439e-03 4.867251829657326737e-03
+3.612244897959183687e-01 5.194980618384773159e-03 4.671733073712271492e-03
+3.775510204081632404e-01 5.112247513943503525e-03 4.533700952241386320e-03
+3.938775510204082231e-01 4.843877522511543486e-03 4.354338837886682044e-03
+4.102040816326530948e-01 4.581162311501288040e-03 4.183485454464925569e-03
+4.265306122448979664e-01 4.575902333115733529e-03 4.080267829835968439e-03
+4.428571428571428381e-01 4.699505512860680662e-03 4.310263817980366542e-03
+4.591836734693878208e-01 4.411877109285056511e-03 4.028341424745536872e-03
+4.755102040816326925e-01 3.912002421612906407e-03 3.990957946643638315e-03
+4.918367346938775642e-01 3.508374796053288693e-03 3.946269400029071145e-03
+5.081632653061224358e-01 3.299279920697378594e-03 3.769594688649386023e-03
+5.244897959183674185e-01 3.450148591760391226e-03 3.710058644842988921e-03
+5.408163265306122902e-01 3.549779478918272963e-03 3.698483399590634692e-03
+5.571428571428571619e-01 3.395360677725176805e-03 3.549039373020256537e-03
+5.734693877551020336e-01 2.692149192139742069e-03 3.236751522896792223e-03
+5.897959183673470163e-01 1.834975916214879547e-03 3.320370979116342262e-03
+6.061224489795918879e-01 1.434505641425406911e-03 3.229120908684111283e-03
+6.224489795918367596e-01 1.255560850425938837e-03 3.015839887985304404e-03
+6.387755102040816313e-01 1.511434556359628157e-03 2.918041857848065277e-03
+6.551020408163265030e-01 1.375912011189511308e-03 2.643676721640121109e-03
+6.714285714285714857e-01 1.063345719439776462e-03 2.353021475347306501e-03
+6.877551020408163573e-01 6.893171889063837545e-04 2.151276015229100676e-03
+7.040816326530612290e-01 2.400689127714897590e-04 2.094057650527441673e-03
+7.204081632653062117e-01 -1.202600787526237567e-05 2.053675732589019861e-03
+7.367346938775510834e-01 -1.513013065019109627e-04 1.912085561785451492e-03
+7.530612244897959551e-01 -2.055206984531875094e-04 1.854542395242731595e-03
+7.693877551020408267e-01 -2.329992136797600396e-04 1.812885362980177790e-03
+7.857142857142856984e-01 -2.027384854668734027e-04 1.637337392279653199e-03
+8.020408163265306811e-01 -1.425192807261697631e-04 1.533165760304424849e-03
+8.183673469387755528e-01 -1.138655908763324358e-04 1.448744408941626295e-03
+8.346938775510204245e-01 -1.259014983128754327e-04 1.219932345173984842e-03
+8.510204081632654072e-01 -1.269871109435181890e-04 1.005675751561662876e-03
+8.673469387755102789e-01 -1.141045310238823546e-04 8.086130827908307415e-04
+8.836734693877551505e-01 -8.122948150597573426e-05 6.464492371260225972e-04
+9.000000000000000222e-01 -2.604869388541081849e-05 6.175356181904082626e-04
diff --git a/examples/qPDF/data/full/d.dat b/examples/qPDF/data/full/d.dat
new file mode 100644
index 000000000..270f7208f
--- /dev/null
+++ b/examples/qPDF/data/full/d.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 5.091772816222682740e-01 7.916555467649237510e-02
+1.151395399326446927e-04 5.018496698598922112e-01 7.379750037070245683e-02
+1.325711365590109543e-04 4.945745340463617024e-01 6.888929949600661184e-02
+1.526417967175233259e-04 4.875228187938407398e-01 6.458393136495986508e-02
+1.757510624854791170e-04 4.807121237531004621e-01 6.079249135535101656e-02
+2.023589647725157589e-04 4.740567012227721588e-01 5.729601026658712876e-02
+2.329951810515371805e-04 4.675685849901001223e-01 5.413459078459310209e-02
+2.682695795279724476e-04 4.612534206700789619e-01 5.135397279780620772e-02
+3.088843596477481527e-04 4.551329586012081152e-01 4.883349679151714751e-02
+3.556480306223128661e-04 4.492183834155272226e-01 4.648953727875566183e-02
+4.094915062380427508e-04 4.435463283610689422e-01 4.414711339476462548e-02
+4.714866363457394672e-04 4.381122926006365548e-01 4.178657065441718221e-02
+5.428675439323859403e-04 4.327514991049956072e-01 3.965839961475201708e-02
+6.250551925273975734e-04 4.274598897417202226e-01 3.771296466707743655e-02
+7.196856730011521675e-04 4.221620454240913034e-01 3.594536996178232524e-02
+8.286427728546842068e-04 4.169988188584659028e-01 3.423150841652428111e-02
+9.540954763499943534e-04 4.120674757458040460e-01 3.243556600354317865e-02
+1.098541141987558400e-03 4.072839513592570460e-01 3.080408359331427912e-02
+1.264855216855295715e-03 4.026388726257003059e-01 2.932485474076973464e-02
+1.456348477501244378e-03 3.981405045371430584e-01 2.790190649596348271e-02
+1.676832936811008387e-03 3.937524651255330244e-01 2.650990408800484033e-02
+1.930697728883249645e-03 3.893590161203007161e-01 2.504898658759189753e-02
+2.222996482526195736e-03 3.850631744616042074e-01 2.362833168365734149e-02
+2.559547922699535825e-03 3.809007913054734407e-01 2.221850257195836717e-02
+2.947051702551809708e-03 3.770080965357998637e-01 2.101143674643660142e-02
+3.393221771895329857e-03 3.734165662115483397e-01 2.000461792202202008e-02
+3.906939937054616958e-03 3.702388651748966808e-01 1.924589640809319657e-02
+4.498432668969444201e-03 3.674707962435030395e-01 1.872754861115825606e-02
+5.179474679231212825e-03 3.651543709566169027e-01 1.849667385424182131e-02
+5.963623316594642357e-03 3.631644660679942405e-01 1.852313426204290139e-02
+6.866488450042998112e-03 3.613697329632591937e-01 1.884686786303893524e-02
+7.906043210907700777e-03 3.599985884698498895e-01 1.894759135143099169e-02
+9.102981779915217050e-03 3.590937855869292261e-01 1.862420008020821555e-02
+1.048113134154685273e-02 3.587383657692194139e-01 1.838933416076928354e-02
+1.206792640639328847e-02 3.590838675258292145e-01 1.810842150202576772e-02
+1.389495494373137359e-02 3.605971263979507735e-01 1.727846413265952605e-02
+1.599858719606057217e-02 3.632358100335745243e-01 1.646851468942652982e-02
+1.842069969326716461e-02 3.670854593027473856e-01 1.607590996233181080e-02
+2.120950887920190417e-02 3.719259437208161101e-01 1.564933722400820362e-02
+2.442053094548649744e-02 3.777111457805737982e-01 1.493452592184791455e-02
+2.811768697974230749e-02 3.844916535152652637e-01 1.415730696398895955e-02
+3.237457542817643447e-02 3.921073994677974905e-01 1.351712821358325330e-02
+3.727593720314938130e-02 4.001482596222533661e-01 1.301846949084805444e-02
+4.291934260128778267e-02 4.082787446409962273e-01 1.250681217077120357e-02
+4.941713361323833015e-02 4.163302650034106378e-01 1.184699784968673390e-02
+5.689866029018292998e-02 4.230950094590615329e-01 1.194891530270234675e-02
+6.551285568595509312e-02 4.282442244594691916e-01 1.200795544004866315e-02
+7.543120063354614990e-02 4.318306707906224839e-01 1.167129204486152746e-02
+8.685113737513520948e-02 4.338128774948742938e-01 1.137921200278562035e-02
+1.000000000000000056e-01 4.333629500000000245e-01 1.142205927296057952e-02
+1.163265306122449050e-01 4.319854048377612665e-01 1.186810995140672592e-02
+1.326530612244898044e-01 4.282018388324707603e-01 1.185308202596499320e-02
+1.489795918367347038e-01 4.218097916223478294e-01 1.164441685487899864e-02
+1.653061224489796033e-01 4.121920075337880407e-01 1.152898116787217167e-02
+1.816326530612245027e-01 4.011729322850488000e-01 1.199592104148921971e-02
+1.979591836734694021e-01 3.881920050023857649e-01 1.235297173645639665e-02
+2.142857142857143016e-01 3.746110031864225420e-01 1.231830164869357133e-02
+2.306122448979592010e-01 3.599051318315605874e-01 1.184557152524235400e-02
+2.469387755102041004e-01 3.440020547264790451e-01 1.121825444520186364e-02
+2.632653061224490276e-01 3.271252872232493503e-01 1.065073119380046708e-02
+2.795918367346938993e-01 3.102239205264454847e-01 1.025899698482179469e-02
+2.959183673469387710e-01 2.924400641576166415e-01 9.842133598727825436e-03
+3.122448979591836982e-01 2.746771419636072631e-01 1.037543665308320734e-02
+3.285714285714286254e-01 2.579208516735705392e-01 1.132196327225507872e-02
+3.448979591836734970e-01 2.392713041855997202e-01 9.706181968344242822e-03
+3.612244897959183687e-01 2.200942895644345532e-01 1.088058989779359248e-02
+3.775510204081632404e-01 2.015352169703905072e-01 1.147618085254958030e-02
+3.938775510204082231e-01 1.834778690223543285e-01 1.243506896536910114e-02
+4.102040816326530948e-01 1.665215033712220405e-01 1.196507375293896780e-02
+4.265306122448979664e-01 1.500708105276261917e-01 1.252551497739448627e-02
+4.428571428571428381e-01 1.335042960430444514e-01 1.520917078076000975e-02
+4.591836734693878208e-01 1.207215482096496983e-01 1.590959814892967464e-02
+4.755102040816326925e-01 1.080260917482173538e-01 1.553631936069132559e-02
+4.918367346938775642e-01 9.472548541323846938e-02 1.519063342865388695e-02
+5.081632653061224358e-01 8.094667899419712154e-02 1.582852538281763824e-02
+5.244897959183674185e-01 6.941026401027031167e-02 1.521944184007802646e-02
+5.408163265306122902e-01 5.930041595975332064e-02 1.503097080754000302e-02
+5.571428571428571619e-01 4.907678969758672710e-02 1.525354296439180614e-02
+5.734693877551020336e-01 4.086042706797585189e-02 1.521854809216812571e-02
+5.897959183673470163e-01 3.536451685924833083e-02 1.541250960967785640e-02
+6.061224489795918879e-01 2.937774651292302322e-02 1.492224573979315447e-02
+6.224489795918367596e-01 2.483769891597548476e-02 1.420870934691557778e-02
+6.387755102040816313e-01 1.964669379999844923e-02 1.375236258345650942e-02
+6.551020408163265030e-01 1.600648495888330053e-02 1.352795817211750219e-02
+6.714285714285714857e-01 1.371824458342043099e-02 1.417405976351782983e-02
+6.877551020408163573e-01 1.137234890484222112e-02 1.299612576350017527e-02
+7.040816326530612290e-01 9.773630232330304465e-03 1.279353067078748546e-02
+7.204081632653062117e-01 7.501140602779903140e-03 1.282082811316204857e-02
+7.367346938775510834e-01 6.386503919608849111e-03 1.367938696443783166e-02
+7.530612244897959551e-01 5.150940857924307670e-03 1.260427596906943566e-02
+7.693877551020408267e-01 3.601692683394015077e-03 1.197341369650416383e-02
+7.857142857142856984e-01 2.210276236893728759e-03 1.205791082848095379e-02
+8.020408163265306811e-01 1.559251545478106691e-03 1.051003276106219318e-02
+8.183673469387755528e-01 8.465323511627092547e-04 9.696491188922193652e-03
+8.346938775510204245e-01 3.378756927483065947e-04 8.721503560001611297e-03
+8.510204081632654072e-01 4.341769438769502301e-05 7.867103569669024049e-03
+8.673469387755102789e-01 -3.583154109628419639e-05 6.812644339057771087e-03
+8.836734693877551505e-01 -1.249919176342527058e-04 5.929764783898339164e-03
+9.000000000000000222e-01 -1.930248019052008528e-04 6.042746812240025120e-03
diff --git a/examples/qPDF/data/full/dbar.dat b/examples/qPDF/data/full/dbar.dat
new file mode 100644
index 000000000..bca605bcd
--- /dev/null
+++ b/examples/qPDF/data/full/dbar.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 4.977631335721693362e-01 7.810041648432516914e-02
+1.151395399326446927e-04 4.897914260669028463e-01 7.271489693667942955e-02
+1.325711365590109543e-04 4.818250884600102091e-01 6.777349807741993304e-02
+1.526417967175233259e-04 4.740383797424769230e-01 6.342673165958724601e-02
+1.757510624854791170e-04 4.664499299129633569e-01 5.958300262045144319e-02
+2.023589647725157589e-04 4.589833118062938144e-01 5.604760528664341074e-02
+2.329951810515371805e-04 4.516512086844224894e-01 5.285850028384469351e-02
+2.682695795279724476e-04 4.444716150280402234e-01 5.005113323466332115e-02
+3.088843596477481527e-04 4.374296106689383179e-01 4.751499987622998661e-02
+3.556480306223128661e-04 4.305128304070620082e-01 4.516611473293721812e-02
+4.094915062380427508e-04 4.237739583871108495e-01 4.285237155467760961e-02
+4.714866363457394672e-04 4.172076762073279599e-01 4.055782349676291532e-02
+5.428675439323859403e-04 4.106175929241103195e-01 3.853459947025952848e-02
+6.250551925273975734e-04 4.039952193395663360e-01 3.673162142786890599e-02
+7.196856730011521675e-04 3.972398612033011811e-01 3.511947011322449214e-02
+8.286427728546842068e-04 3.905310954681409652e-01 3.360772184839522225e-02
+9.540954763499943534e-04 3.840051566784694059e-01 3.207856965415820105e-02
+1.098541141987558400e-03 3.774637287291905396e-01 3.068024736782619041e-02
+1.264855216855295715e-03 3.708815867359183116e-01 2.937013980538640337e-02
+1.456348477501244378e-03 3.642851330761421313e-01 2.806106942966916581e-02
+1.676832936811008387e-03 3.576699791997364763e-01 2.676638787861576260e-02
+1.930697728883249645e-03 3.510143161330681605e-01 2.551591444335393061e-02
+2.222996482526195736e-03 3.443661010693954694e-01 2.437197992755635312e-02
+2.559547922699535825e-03 3.377298891318412100e-01 2.332275132216285810e-02
+2.947051702551809708e-03 3.312850281250212525e-01 2.237173918645971038e-02
+3.393221771895329857e-03 3.250662189854978124e-01 2.149372361441809556e-02
+3.906939937054616958e-03 3.190892761415642376e-01 2.076193916247714641e-02
+4.498432668969444201e-03 3.133376104711430021e-01 2.016763699123386502e-02
+5.179474679231212825e-03 3.077876841714057288e-01 1.964276505625673491e-02
+5.963623316594642357e-03 3.023850411522735482e-01 1.912493425027323238e-02
+6.866488450042998112e-03 2.971156431504373696e-01 1.857903113620404389e-02
+7.906043210907700777e-03 2.917553589185331031e-01 1.817519040433353966e-02
+9.102981779915217050e-03 2.862609959838960383e-01 1.792292441671521422e-02
+1.048113134154685273e-02 2.809670602390422212e-01 1.787058971232344598e-02
+1.206792640639328847e-02 2.758555230217916154e-01 1.790916202300373711e-02
+1.389495494373137359e-02 2.709820307109060966e-01 1.766941644182267596e-02
+1.599858719606057217e-02 2.661163936336272684e-01 1.741988443036600678e-02
+1.842069969326716461e-02 2.611811869294395350e-01 1.710607115617579499e-02
+2.120950887920190417e-02 2.555368732162919265e-01 1.657233886734713058e-02
+2.442053094548649744e-02 2.490270984602411253e-01 1.578683478115504302e-02
+2.811768697974230749e-02 2.416736238336177067e-01 1.513508588606382946e-02
+3.237457542817643447e-02 2.336302337086081371e-01 1.457823056471699123e-02
+3.727593720314938130e-02 2.253461563862200756e-01 1.381300800673037656e-02
+4.291934260128778267e-02 2.169470333902376313e-01 1.310875260072509770e-02
+4.941713361323833015e-02 2.088010736240945575e-01 1.246416106426008474e-02
+5.689866029018292998e-02 1.999430023240398313e-01 1.227875402144131683e-02
+6.551285568595509312e-02 1.901399864339190227e-01 1.211447935305698040e-02
+7.543120063354614990e-02 1.802463663355282075e-01 1.157107748380932673e-02
+8.685113737513520948e-02 1.699144804931874420e-01 1.113297332290572386e-02
+1.000000000000000056e-01 1.573932000000000109e-01 1.014231559246146948e-02
+1.163265306122449050e-01 1.433649249121687341e-01 8.481243317234056153e-03
+1.326530612244898044e-01 1.295400522962832646e-01 7.243514263008420107e-03
+1.489795918367347038e-01 1.152544742232293196e-01 6.067835425351424811e-03
+1.653061224489796033e-01 1.011855789151688834e-01 5.985629346896574957e-03
+1.816326530612245027e-01 8.715178587795263665e-02 6.048640095465555692e-03
+1.979591836734694021e-01 7.458677501164867607e-02 5.813092362005962271e-03
+2.142857142857143016e-01 6.249873547602553120e-02 5.934263570124668347e-03
+2.306122448979592010e-01 5.181022996671015568e-02 6.203143677695365475e-03
+2.469387755102041004e-01 4.197209834064773853e-02 6.324876696238434202e-03
+2.632653061224490276e-01 3.334547108629661721e-02 6.389725451675682712e-03
+2.795918367346938993e-01 2.572917981391273021e-02 6.174116090313300567e-03
+2.959183673469387710e-01 1.979955870424673567e-02 6.280524682915253863e-03
+3.122448979591836982e-01 1.449152434907221426e-02 6.331541371998266818e-03
+3.285714285714286254e-01 8.989087468730809147e-03 7.595376940382754342e-03
+3.448979591836734970e-01 6.216513476467545873e-03 5.946630908343609155e-03
+3.612244897959183687e-01 4.643358758758497212e-03 7.666704564216820714e-03
+3.775510204081632404e-01 4.026478403530274301e-03 8.802408761178621283e-03
+3.938775510204082231e-01 4.184595912678558752e-03 1.027649072980537683e-02
+4.102040816326530948e-01 3.847370634377959581e-03 9.857259957830802680e-03
+4.265306122448979664e-01 3.545439000884104044e-03 1.092082872314948001e-02
+4.428571428571428381e-01 4.286382242885526835e-03 1.387159887015452196e-02
+4.591836734693878208e-01 2.590113200612944883e-03 1.462556815161407398e-02
+4.755102040816326925e-01 2.280409685916162145e-03 1.495259847782390815e-02
+4.918367346938775642e-01 2.808998904941583091e-03 1.473662895226150044e-02
+5.081632653061224358e-01 3.953622982447484863e-03 1.573471739266091296e-02
+5.244897959183674185e-01 3.407874012965536603e-03 1.553999947376006815e-02
+5.408163265306122902e-01 2.404454447424684421e-03 1.530205768622161684e-02
+5.571428571428571619e-01 2.644218077709412518e-03 1.513240561439272575e-02
+5.734693877551020336e-01 2.625291945255302500e-03 1.492559749435511118e-02
+5.897959183673470163e-01 1.286110393801345947e-03 1.461667280101169863e-02
+6.061224489795918879e-01 1.138349742385753871e-03 1.365996083221193529e-02
+6.224489795918367596e-01 5.681395412635522647e-04 1.277943878136280839e-02
+6.387755102040816313e-01 1.412161992931055187e-03 1.261028606768314214e-02
+6.551020408163265030e-01 2.146702536331560659e-03 1.226619336219402986e-02
+6.714285714285714857e-01 3.133211172357025683e-03 1.289036313686338538e-02
+6.877551020408163573e-01 4.848935490069965042e-03 1.148048995812154215e-02
+7.040816326530612290e-01 5.921973125223791920e-03 1.125687254220730966e-02
+7.204081632653062117e-01 6.583132456638965575e-03 1.175729386615498133e-02
+7.367346938775510834e-01 5.902165687426506359e-03 1.268637380600316431e-02
+7.530612244897959551e-01 5.588513599931386501e-03 1.195055443286740030e-02
+7.693877551020408267e-01 5.721807343038266824e-03 1.158017304292606117e-02
+7.857142857142856984e-01 5.657948365304832708e-03 1.177402930489857696e-02
+8.020408163265306811e-01 4.803772217549897791e-03 1.044511582683799698e-02
+8.183673469387755528e-01 4.198696164673327083e-03 9.731260435807975034e-03
+8.346938775510204245e-01 3.556407472593775022e-03 8.593753259920606721e-03
+8.510204081632654072e-01 3.003263435210558937e-03 7.739678550482364389e-03
+8.673469387755102789e-01 2.365811147249049975e-03 6.742651502665567703e-03
+8.836734693877551505e-01 1.872728731270310734e-03 5.849475693755866804e-03
+9.000000000000000222e-01 1.337487979259191582e-03 6.010268189772444321e-03
diff --git a/examples/qPDF/data/full/gluon.dat b/examples/qPDF/data/full/gluon.dat
new file mode 100644
index 000000000..373896f14
--- /dev/null
+++ b/examples/qPDF/data/full/gluon.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 1.436073027852281747e+00 1.410829786450314760e+00
+1.151395399326446927e-04 1.364958626022871213e+00 1.322403944448146795e+00
+1.325711365590109543e-04 1.308744557998403835e+00 1.246856327363375705e+00
+1.526417967175233259e-04 1.266716265313918344e+00 1.181035820270940784e+00
+1.757510624854791170e-04 1.238632415751813820e+00 1.124100495485785611e+00
+2.023589647725157589e-04 1.223304323506445579e+00 1.082280689395135109e+00
+2.329951810515371805e-04 1.221403075342442968e+00 1.049932598530862737e+00
+2.682695795279724476e-04 1.234246661261282352e+00 1.018495848476023369e+00
+3.088843596477481527e-04 1.261118092261666934e+00 9.820269772796574870e-01
+3.556480306223128661e-04 1.301809107355178963e+00 9.380345387151233227e-01
+4.094915062380427508e-04 1.355955018492741582e+00 8.893711185159632882e-01
+4.714866363457394672e-04 1.423415028768741042e+00 8.375259875883908078e-01
+5.428675439323859403e-04 1.503558397812429392e+00 7.741246897837354979e-01
+6.250551925273975734e-04 1.594760490544748555e+00 7.053377733669865535e-01
+7.196856730011521675e-04 1.694368883930322145e+00 6.381482642538304217e-01
+8.286427728546842068e-04 1.797933073006341420e+00 5.770645929332395285e-01
+9.540954763499943534e-04 1.902288016649845925e+00 5.213202772176038780e-01
+1.098541141987558400e-03 2.004993449735332334e+00 4.718001852409516306e-01
+1.264855216855295715e-03 2.105514952847052701e+00 4.273401137522174764e-01
+1.456348477501244378e-03 2.204919381213297669e+00 3.857407874870817932e-01
+1.676832936811008387e-03 2.302838545246236102e+00 3.527675463927992805e-01
+1.930697728883249645e-03 2.401250317650158284e+00 3.292460342654914274e-01
+2.222996482526195736e-03 2.489413722921670402e+00 3.241005711624331820e-01
+2.559547922699535825e-03 2.558364682243282839e+00 3.028155002281870400e-01
+2.947051702551809708e-03 2.616224206212616199e+00 2.820170227437128752e-01
+3.393221771895329857e-03 2.664071453832157932e+00 2.591501164691561443e-01
+3.906939937054616958e-03 2.703361738998359520e+00 2.348943198277512467e-01
+4.498432668969444201e-03 2.734359509219411066e+00 2.093647295324295721e-01
+5.179474679231212825e-03 2.758211866006374358e+00 1.861954455819890830e-01
+5.963623316594642357e-03 2.774504985860168027e+00 1.666400687266494929e-01
+6.866488450042998112e-03 2.783256351062659117e+00 1.509792672160396587e-01
+7.906043210907700777e-03 2.783431601575428349e+00 1.393981853977233343e-01
+9.102981779915217050e-03 2.774862150044652420e+00 1.356247879444966997e-01
+1.048113134154685273e-02 2.760566329600861124e+00 1.342862511369074530e-01
+1.206792640639328847e-02 2.740601584470428342e+00 1.316902676015173357e-01
+1.389495494373137359e-02 2.715933591822299764e+00 1.253457936888159319e-01
+1.599858719606057217e-02 2.685154192466012724e+00 1.193572415535045228e-01
+1.842069969326716461e-02 2.647431992894222486e+00 1.132596856420066828e-01
+2.120950887920190417e-02 2.601477236862831166e+00 1.124669224631803938e-01
+2.442053094548649744e-02 2.546935186302070164e+00 1.199475922069823675e-01
+2.811768697974230749e-02 2.486491029640843564e+00 1.260482655459385648e-01
+3.237457542817643447e-02 2.421072694716483120e+00 1.342529033015319173e-01
+3.727593720314938130e-02 2.356120187280772438e+00 1.228918399554665974e-01
+4.291934260128778267e-02 2.283609481004460040e+00 1.135313148672207062e-01
+4.941713361323833015e-02 2.197861404801443985e+00 1.111765296966798489e-01
+5.689866029018292998e-02 2.094792655510467227e+00 1.086382056511527999e-01
+6.551285568595509312e-02 1.972943483478049043e+00 1.016059983620012930e-01
+7.543120063354614990e-02 1.828797090456429908e+00 8.228479680656052009e-02
+8.685113737513520948e-02 1.665052707183172664e+00 7.019163919958561204e-02
+1.000000000000000056e-01 1.486650900000000108e+00 6.906649307998630194e-02
+1.000000000000000056e-01 1.486650900000000108e+00 6.906649307998630194e-02
+1.163265306122449050e-01 1.278363607352488662e+00 5.851715503791719841e-02
+1.326530612244898044e-01 1.093272570071426530e+00 6.495189916810374664e-02
+1.489795918367347038e-01 9.445472028682869725e-01 6.800362366808562764e-02
+1.653061224489796033e-01 8.244352867442255040e-01 6.036562576446031364e-02
+1.816326530612245027e-01 7.266001875331934423e-01 4.804631794363146274e-02
+1.979591836734694021e-01 6.460934389251358212e-01 5.956328597727415797e-02
+2.142857142857143016e-01 5.754064633997411082e-01 5.481677133626022824e-02
+2.306122448979592010e-01 5.137991377127972825e-01 5.361156055870076875e-02
+2.469387755102041004e-01 4.552507567815178291e-01 5.065601192147932419e-02
+2.632653061224490276e-01 3.996382053409509028e-01 4.755273215299854472e-02
+2.795918367346938993e-01 3.411444192178936241e-01 4.570775803372454510e-02
+2.959183673469387710e-01 2.941440729953202493e-01 5.179924323811337800e-02
+3.122448979591836982e-01 2.601290037822677204e-01 4.622670247373922986e-02
+3.285714285714286254e-01 2.302641998343407104e-01 4.244869960571524825e-02
+3.448979591836734970e-01 2.048947603867307399e-01 3.692175416096520058e-02
+3.612244897959183687e-01 1.820567154916601094e-01 3.491710247794299615e-02
+3.775510204081632404e-01 1.654109241587360668e-01 2.958093259719593235e-02
+3.938775510204082231e-01 1.480349508988084706e-01 2.827395372375652097e-02
+4.102040816326530948e-01 1.325798271673375717e-01 2.828630531659322850e-02
+4.265306122448979664e-01 1.182493409930097245e-01 2.916021507299379825e-02
+4.428571428571428381e-01 1.066391781871470490e-01 2.847724416602226416e-02
+4.591836734693878208e-01 1.011350726109566528e-01 3.463682731939739246e-02
+4.755102040816326925e-01 9.174674898459940497e-02 3.317719061506606976e-02
+4.918367346938775642e-01 8.739403248853304385e-02 3.458909252220616903e-02
+5.081632653061224358e-01 7.986314864670505942e-02 3.453373395965902981e-02
+5.244897959183674185e-01 7.216510147179601431e-02 2.968070892373265643e-02
+5.408163265306122902e-01 6.645926254942295663e-02 2.713087028010289364e-02
+5.571428571428571619e-01 5.877459162780401120e-02 2.555514304330760988e-02
+5.734693877551020336e-01 5.338086792246633794e-02 2.502004244557643961e-02
+5.897959183673470163e-01 4.876099631660012895e-02 2.707226383643828588e-02
+6.061224489795918879e-01 4.207712352396885541e-02 2.305135101626111169e-02
+6.224489795918367596e-01 3.580485555936888109e-02 2.185199910362296480e-02
+6.387755102040816313e-01 3.021147633166515814e-02 2.208234850326066048e-02
+6.551020408163265030e-01 2.484776981462179582e-02 2.198756275659718984e-02
+6.714285714285714857e-01 1.988113039811458330e-02 2.200991807516783014e-02
+6.877551020408163573e-01 1.400636390703255754e-02 2.365106623106593212e-02
+7.040816326530612290e-01 1.145974167737420432e-02 1.887010255106317397e-02
+7.204081632653062117e-01 7.837588375768960586e-03 1.719052997285148640e-02
+7.367346938775510834e-01 5.019045671291559423e-03 1.703959974523763451e-02
+7.530612244897959551e-01 3.380609462740184409e-03 1.479054695636173103e-02
+7.693877551020408267e-01 9.324618436404591567e-05 1.621917485475198040e-02
+7.857142857142856984e-01 -1.538774680937169846e-03 1.427911879458286688e-02
+8.020408163265306811e-01 -3.292689738769052563e-03 1.302317972988607450e-02
+8.183673469387755528e-01 -4.768501625455401523e-03 1.556994705567193819e-02
+8.346938775510204245e-01 -5.230190570117304073e-03 1.414505616898533748e-02
+8.510204081632654072e-01 -5.504081810805650996e-03 1.310886165854219070e-02
+8.673469387755102789e-01 -5.565775093366548236e-03 1.283554913783929025e-02
+8.836734693877551505e-01 -5.301125605810848816e-03 1.208554103662637380e-02
+9.000000000000000222e-01 -4.864826018318149127e-03 1.070561871116822161e-02
diff --git a/examples/qPDF/data/full/s.dat b/examples/qPDF/data/full/s.dat
new file mode 100644
index 000000000..c07a18b31
--- /dev/null
+++ b/examples/qPDF/data/full/s.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 4.334959492027843364e-01 1.985667276252919278e-01
+1.151395399326446927e-04 4.238576148114755471e-01 1.911808403566152004e-01
+1.325711365590109543e-04 4.145047586436171883e-01 1.838948897105848468e-01
+1.526417967175233259e-04 4.053556376060338251e-01 1.767980206356746520e-01
+1.757510624854791170e-04 3.964004300138998516e-01 1.698647869879708616e-01
+2.023589647725157589e-04 3.875721383588853208e-01 1.630343734164645675e-01
+2.329951810515371805e-04 3.788666383060967924e-01 1.563327196355623061e-01
+2.682695795279724476e-04 3.702574150638528461e-01 1.497847302764466759e-01
+3.088843596477481527e-04 3.617642671056362502e-01 1.434031646642546509e-01
+3.556480306223128661e-04 3.534242986252951746e-01 1.371890307789533359e-01
+4.094915062380427508e-04 3.449926826582891959e-01 1.310799525596098580e-01
+4.714866363457394672e-04 3.364638927917863453e-01 1.251134679489486479e-01
+5.428675439323859403e-04 3.283071634759229251e-01 1.192749602361101585e-01
+6.250551925273975734e-04 3.205020432234118588e-01 1.135314699465588134e-01
+7.196856730011521675e-04 3.131504425741581388e-01 1.077369567521612864e-01
+8.286427728546842068e-04 3.060012390836035490e-01 1.020268800033494461e-01
+9.540954763499943534e-04 2.988487562891620208e-01 9.646475695172382347e-02
+1.098541141987558400e-03 2.920499587635345118e-01 9.115721062355193471e-02
+1.264855216855295715e-03 2.856447475035078232e-01 8.603716075238512784e-02
+1.456348477501244378e-03 2.794212376523975139e-01 8.102561264749053027e-02
+1.676832936811008387e-03 2.733949584599569271e-01 7.609987736375099787e-02
+1.930697728883249645e-03 2.676079999827670752e-01 7.120800384144629347e-02
+2.222996482526195736e-03 2.619194359849002507e-01 6.650009450390369226e-02
+2.559547922699535825e-03 2.562519870192030425e-01 6.197608475348287405e-02
+2.947051702551809708e-03 2.504667428346106228e-01 5.785306983993866725e-02
+3.393221771895329857e-03 2.445305648814212196e-01 5.410262490864738844e-02
+3.906939937054616958e-03 2.383681087910252960e-01 5.068168917302826731e-02
+4.498432668969444201e-03 2.319892979753051809e-01 4.762834954084661787e-02
+5.179474679231212825e-03 2.253572360047917666e-01 4.509760886385179995e-02
+5.963623316594642357e-03 2.186547410285782922e-01 4.304180834075880396e-02
+6.866488450042998112e-03 2.120180754373788279e-01 4.112959981213454297e-02
+7.906043210907700777e-03 2.054952024599659421e-01 3.910274097315720454e-02
+9.102981779915217050e-03 1.990979065002917991e-01 3.675241025220313956e-02
+1.048113134154685273e-02 1.924158112315855207e-01 3.468116330602197955e-02
+1.206792640639328847e-02 1.854170135496832628e-01 3.269045155994187296e-02
+1.389495494373137359e-02 1.779396448739730130e-01 3.013666345014757303e-02
+1.599858719606057217e-02 1.700298343264547984e-01 2.748746995937121143e-02
+1.842069969326716461e-02 1.617482273608176757e-01 2.480963428505272503e-02
+2.120950887920190417e-02 1.527655975149912471e-01 2.251480516241462573e-02
+2.442053094548649744e-02 1.430092730401066048e-01 2.051737691353020324e-02
+2.811768697974230749e-02 1.329829132227545729e-01 1.894068229293656078e-02
+3.237457542817643447e-02 1.226335392398657242e-01 1.780633964952213835e-02
+3.727593720314938130e-02 1.118337544520869309e-01 1.742496501493928651e-02
+4.291934260128778267e-02 1.006137501380840876e-01 1.708848748546625690e-02
+4.941713361323833015e-02 8.848080759273516116e-02 1.518087118396405089e-02
+5.689866029018292998e-02 7.822897666014630436e-02 1.450581755983846276e-02
+6.551285568595509312e-02 7.055984249994165514e-02 1.343127930189879152e-02
+7.543120063354614990e-02 6.490824147842133574e-02 1.222417605341719060e-02
+8.685113737513520948e-02 6.099509414279424141e-02 1.155958762644083981e-02
+1.000000000000000056e-01 5.811938900000000047e-02 1.135948457677850472e-02
+1.163265306122449050e-01 5.498144450814458628e-02 9.629446721598575870e-03
+1.326530612244898044e-01 5.135997732852693592e-02 9.225240289123470827e-03
+1.489795918367347038e-01 4.723050215813651143e-02 8.846513143861974710e-03
+1.653061224489796033e-01 4.311203853746148340e-02 8.441498819331736542e-03
+1.816326530612245027e-01 3.894380473376195945e-02 8.655830837138740047e-03
+1.979591836734694021e-01 3.486831510158874825e-02 8.877602058789275594e-03
+2.142857142857143016e-01 3.069355929448877585e-02 9.246090313310376119e-03
+2.306122448979592010e-01 2.650617824238633846e-02 9.284838420715521567e-03
+2.469387755102041004e-01 2.240256390271565257e-02 8.949419548762920645e-03
+2.632653061224490276e-01 1.859721874914666007e-02 8.246200233160297030e-03
+2.795918367346938993e-01 1.529095779162716572e-02 7.193589407121427949e-03
+2.959183673469387710e-01 1.223344377289717171e-02 6.144280702266012063e-03
+3.122448979591836982e-01 9.472423507792282829e-03 5.434167994449804785e-03
+3.285714285714286254e-01 7.527646548402747509e-03 4.690624602008485605e-03
+3.448979591836734970e-01 6.513274540608479310e-03 3.949206138181179417e-03
+3.612244897959183687e-01 6.324699667870168490e-03 3.825037709656281537e-03
+3.775510204081632404e-01 6.653667513086839770e-03 5.290996518088139297e-03
+3.938775510204082231e-01 6.681477882536569768e-03 6.106976428266843067e-03
+4.102040816326530948e-01 6.632437655444375565e-03 5.646533706492752776e-03
+4.265306122448979664e-01 7.260241880531962191e-03 5.627655291587668578e-03
+4.428571428571428381e-01 8.237569708429254636e-03 6.039312886020845907e-03
+4.591836734693878208e-01 9.256739924858644469e-03 6.591637446377524370e-03
+4.755102040816326925e-01 9.983724737793020498e-03 7.020314509780686135e-03
+4.918367346938775642e-01 1.052513329328826422e-02 7.082609771856901437e-03
+5.081632653061224358e-01 1.103534588189305880e-02 6.889468087760076270e-03
+5.244897959183674185e-01 1.101060982716685691e-02 6.786395115007838943e-03
+5.408163265306122902e-01 1.054112334574236129e-02 6.634480714590747577e-03
+5.571428571428571619e-01 1.017555195366732883e-02 6.611416243887415639e-03
+5.734693877551020336e-01 9.557252016347817652e-03 6.754595096107933634e-03
+5.897959183673470163e-01 8.796965920370821562e-03 6.701452952848836395e-03
+6.061224489795918879e-01 7.858155818736972664e-03 6.622386339097939036e-03
+6.224489795918367596e-01 6.993280944422240221e-03 6.498694781547665299e-03
+6.387755102040816313e-01 6.104164691998323691e-03 6.336567227702525652e-03
+6.551020408163265030e-01 5.124397578212483255e-03 5.955471233002406807e-03
+6.714285714285714857e-01 4.165764440781732046e-03 5.813900741335659844e-03
+6.877551020408163573e-01 3.825667716712735211e-03 6.199468939947366981e-03
+7.040816326530612290e-01 3.454995932446520372e-03 6.744031583403861836e-03
+7.204081632653062117e-01 2.259823363842622911e-03 8.315739003339289726e-03
+7.367346938775510834e-01 6.314853168272392377e-04 1.119384842486024524e-02
+7.530612244897959551e-01 9.440130082234371250e-05 1.213155144191191588e-02
+7.693877551020408267e-01 -3.686189977476679873e-04 1.260123117591606043e-02
+7.857142857142856984e-01 -9.862004915652249504e-04 1.365975660245233141e-02
+8.020408163265306811e-01 -1.284235245889934389e-03 1.311586386815231584e-02
+8.183673469387755528e-01 -1.342621206227992588e-03 1.204775011225409434e-02
+8.346938775510204245e-01 -1.553847558480263465e-03 1.070251811142967913e-02
+8.510204081632654072e-01 -1.641010957639634947e-03 9.910946594650287683e-03
+8.673469387755102789e-01 -1.499827068684795933e-03 9.068969561710620367e-03
+8.836734693877551505e-01 -1.503502693659317579e-03 8.838236264176898421e-03
+9.000000000000000222e-01 -1.651540834005031004e-03 9.021929051782274361e-03
diff --git a/examples/qPDF/data/full/sbar.dat b/examples/qPDF/data/full/sbar.dat
new file mode 100644
index 000000000..4ebc925ef
--- /dev/null
+++ b/examples/qPDF/data/full/sbar.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 4.359400748127145730e-01 1.987885495391763235e-01
+1.151395399326446927e-04 4.263410681293022808e-01 1.913370155673360107e-01
+1.325711365590109543e-04 4.170261674659069828e-01 1.839806288398284639e-01
+1.526417967175233259e-04 4.079103672573080597e-01 1.768110859473048524e-01
+1.757510624854791170e-04 3.989833988918851659e-01 1.698059977928489950e-01
+2.023589647725157589e-04 3.901775769615442591e-01 1.629018428118806927e-01
+2.329951810515371805e-04 3.814881822735329275e-01 1.561248996030261682e-01
+2.682695795279724476e-04 3.728874063226307323e-01 1.494951786652647108e-01
+3.088843596477481527e-04 3.643940902333109855e-01 1.430365028767308644e-01
+3.556480306223128661e-04 3.560413771259096083e-01 1.367574631842753130e-01
+4.094915062380427508e-04 3.476043489585864399e-01 1.306086145635859119e-01
+4.714866363457394672e-04 3.390833484060716274e-01 1.246341772413884219e-01
+5.428675439323859403e-04 3.310064090909531909e-01 1.188128074121952704e-01
+6.250551925273975734e-04 3.233268014634110288e-01 1.131119239340316190e-01
+7.196856730011521675e-04 3.161152090441567419e-01 1.073667209507341569e-01
+8.286427728546842068e-04 3.090556455659053681e-01 1.017277303708125358e-01
+9.540954763499943534e-04 3.018924829653984454e-01 9.627137539323034343e-02
+1.098541141987558400e-03 2.949814891347372070e-01 9.111202107180126075e-02
+1.264855216855295715e-03 2.883621649240054596e-01 8.618432266431293176e-02
+1.456348477501244378e-03 2.818623503211090120e-01 8.138074355440537422e-02
+1.676832936811008387e-03 2.755290569891833963e-01 7.665654358959549108e-02
+1.930697728883249645e-03 2.694760312761637633e-01 7.190122099966855806e-02
+2.222996482526195736e-03 2.636134606816340487e-01 6.721359564420303911e-02
+2.559547922699535825e-03 2.579534690804048958e-01 6.248329341293380051e-02
+2.947051702551809708e-03 2.520957774373272509e-01 5.819092905722163256e-02
+3.393221771895329857e-03 2.459663977477073438e-01 5.433592729053100812e-02
+3.906939937054616958e-03 2.396098223329148902e-01 5.075408600889605437e-02
+4.498432668969444201e-03 2.330356550519976366e-01 4.749361865521156689e-02
+5.179474679231212825e-03 2.261992452913900997e-01 4.469904042425093538e-02
+5.963623316594642357e-03 2.193720440093568091e-01 4.244322405118441249e-02
+6.866488450042998112e-03 2.127988271088606664e-01 4.056046583103654207e-02
+7.906043210907700777e-03 2.062948695020566103e-01 3.893139873447144794e-02
+9.102981779915217050e-03 1.998294352173794475e-01 3.744799359483667184e-02
+1.048113134154685273e-02 1.929990618163023031e-01 3.643870103169093849e-02
+1.206792640639328847e-02 1.857823488444939330e-01 3.559963440242130267e-02
+1.389495494373137359e-02 1.780541360478212765e-01 3.418318317891592961e-02
+1.599858719606057217e-02 1.698249555356744211e-01 3.250968300796492488e-02
+1.842069969326716461e-02 1.610570359923058636e-01 3.039032632467758566e-02
+2.120950887920190417e-02 1.518615801512106234e-01 2.851356379929024729e-02
+2.442053094548649744e-02 1.422628656093470334e-01 2.672138258560976393e-02
+2.811768697974230749e-02 1.325966902854770202e-01 2.419066178086920135e-02
+3.237457542817643447e-02 1.226953390865770377e-01 2.135648359231674676e-02
+3.727593720314938130e-02 1.120166449021556820e-01 1.871430554139774660e-02
+4.291934260128778267e-02 1.007320291741168883e-01 1.618129800655810843e-02
+4.941713361323833015e-02 8.855959678191771545e-02 1.391047855067414733e-02
+5.689866029018292998e-02 7.740990494068301919e-02 1.356214418262123057e-02
+6.551285568595509312e-02 6.776467100239043362e-02 1.241703192380030039e-02
+7.543120063354614990e-02 5.936116293375629654e-02 1.110733886363024503e-02
+8.685113737513520948e-02 5.211013391628055436e-02 1.007901197854379170e-02
+1.000000000000000056e-01 4.602117399999999803e-02 9.779521697310451789e-03
+1.163265306122449050e-01 3.942477395804350887e-02 8.597488598936026127e-03
+1.326530612244898044e-01 3.328912908047675090e-02 7.353330218090444077e-03
+1.489795918367347038e-01 2.863095571284317298e-02 6.665202107333106224e-03
+1.653061224489796033e-01 2.483575627569225078e-02 5.995471224548147050e-03
+1.816326530612245027e-01 2.170692908957032666e-02 5.655695672266800901e-03
+1.979591836734694021e-01 1.898480791532573819e-02 4.434780959100406011e-03
+2.142857142857143016e-01 1.695823321591360261e-02 3.469831512302461781e-03
+2.306122448979592010e-01 1.548757243205519177e-02 3.091673368091698692e-03
+2.469387755102041004e-01 1.433010380039563374e-02 3.566130690184089388e-03
+2.632653061224490276e-01 1.324416138142683191e-02 4.509383362223045449e-03
+2.795918367346938993e-01 1.225172070807768916e-02 5.634363331159562317e-03
+2.959183673469387710e-01 1.124413457635646540e-02 6.411421240065154868e-03
+3.122448979591836982e-01 1.079021243743692043e-02 6.632527583782697571e-03
+3.285714285714286254e-01 1.010737878689214712e-02 6.779166903760069050e-03
+3.448979591836734970e-01 8.811830531947489192e-03 6.611065661575015427e-03
+3.612244897959183687e-01 6.825401095665947424e-03 6.015622156291653214e-03
+3.775510204081632404e-01 4.622927028232834239e-03 5.555000630649990605e-03
+3.938775510204082231e-01 3.224999414965747189e-03 5.629233313847112341e-03
+4.102040816326530948e-01 2.398997430438228398e-03 5.497597477770171416e-03
+4.265306122448979664e-01 1.692190277964546229e-03 5.335104021406694361e-03
+4.428571428571428381e-01 4.148649701266470154e-04 5.113805095634805543e-03
+4.591836734693878208e-01 -1.304228855688523885e-03 5.470247749666199907e-03
+4.755102040816326925e-01 -2.704550331251799004e-03 6.117853355337987686e-03
+4.918367346938775642e-01 -3.535521323528168034e-03 6.489852248896367873e-03
+5.081632653061224358e-01 -3.773172936203268539e-03 6.637549040490199267e-03
+5.244897959183674185e-01 -3.936045802592954765e-03 6.697487600261068513e-03
+5.408163265306122902e-01 -4.125728968283484156e-03 6.412024156289482767e-03
+5.571428571428571619e-01 -4.359718195743628177e-03 5.818326311771436325e-03
+5.734693877551020336e-01 -3.920870475797691088e-03 5.568310082063020368e-03
+5.897959183673470163e-01 -3.129001712471175985e-03 5.478478115272785408e-03
+6.061224489795918879e-01 -2.416380765101757214e-03 5.519673432110980792e-03
+6.224489795918367596e-01 -1.926066507583772255e-03 5.379437327106514552e-03
+6.387755102040816313e-01 -1.763291018474010029e-03 5.011026558907828027e-03
+6.551020408163265030e-01 -1.371265663515872871e-03 4.815576566171758145e-03
+6.714285714285714857e-01 -7.347387558144375915e-04 5.047944037270904634e-03
+6.877551020408163573e-01 -5.653365196498312453e-04 5.556716834071377675e-03
+7.040816326530612290e-01 -2.267332661212563918e-04 6.119802600425810341e-03
+7.204081632653062117e-01 1.026320665191617987e-03 7.680441173733227114e-03
+7.367346938775510834e-01 2.723773866630701743e-03 1.055738113017332461e-02
+7.530612244897959551e-01 3.347111420314353037e-03 1.138156833685597703e-02
+7.693877551020408267e-01 3.700903966783544458e-03 1.181247737394512318e-02
+7.857142857142856984e-01 3.950074419242127592e-03 1.291688207383976522e-02
+8.020408163265306811e-01 3.898205963925653779e-03 1.235501536786283439e-02
+8.183673469387755528e-01 3.588619797058547126e-03 1.137225324329994153e-02
+8.346938775510204245e-01 3.441281250341821953e-03 1.017984496958066962e-02
+8.510204081632654072e-01 3.210758257312623220e-03 9.544603477465968266e-03
+8.673469387755102789e-01 2.754430250123140935e-03 8.728939656345335515e-03
+8.836734693877551505e-01 2.456910492806845328e-03 8.514967898016780751e-03
+9.000000000000000222e-01 2.302187455270835164e-03 8.739502279132595197e-03
diff --git a/examples/qPDF/data/full/singlet.dat b/examples/qPDF/data/full/singlet.dat
new file mode 100644
index 000000000..e7ddf95dd
--- /dev/null
+++ b/examples/qPDF/data/full/singlet.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.906051619342992609e+00 7.462119819026813428e-01
+1.151395399326446927e-04 2.860722503238625869e+00 6.988723163291101770e-01
+1.325711365590109543e-04 2.815405728981031697e+00 6.586280524367921219e-01
+1.526417967175233259e-04 2.770324102867665061e+00 6.244195241115695350e-01
+1.757510624854791170e-04 2.725517322797724074e+00 5.944487582232559353e-01
+2.023589647725157589e-04 2.681196838693734374e+00 5.641250926612668692e-01
+2.329951810515371805e-04 2.637384376323162360e+00 5.350509620256458376e-01
+2.682695795279724476e-04 2.594164003900461424e+00 5.091953271630039746e-01
+3.088843596477481527e-04 2.551560515699526466e+00 4.856781521167745641e-01
+3.556480306223128661e-04 2.509599140891932478e+00 4.637532165071666101e-01
+4.094915062380427508e-04 2.468315423913098439e+00 4.422539326639124968e-01
+4.714866363457394672e-04 2.427712402325765240e+00 4.210862448528221447e-01
+5.428675439323859403e-04 2.387743918408224975e+00 4.011382559420967331e-01
+6.250551925273975734e-04 2.348387102320955488e+00 3.820577280180124102e-01
+7.196856730011521675e-04 2.309572722275872181e+00 3.635766358785286201e-01
+8.286427728546842068e-04 2.271337215556608058e+00 3.459199706961187948e-01
+9.540954763499943534e-04 2.233683502841355306e+00 3.289489556794900627e-01
+1.098541141987558400e-03 2.196685734288134739e+00 3.130908018498047563e-01
+1.264855216855295715e-03 2.160359142346439221e+00 2.982326008119254857e-01
+1.456348477501244378e-03 2.124768531205242450e+00 2.835482339955216480e-01
+1.676832936811008387e-03 2.089886636778342410e+00 2.689103658428349530e-01
+1.930697728883249645e-03 2.055685659097348683e+00 2.544796379090471516e-01
+2.222996482526195736e-03 2.022064289211034094e+00 2.409054177577455991e-01
+2.559547922699535825e-03 1.988934505919132345e+00 2.278774653941089690e-01
+2.947051702551809708e-03 1.956332697014812272e+00 2.162392065064994151e-01
+3.393221771895329857e-03 1.924269027649759600e+00 2.058747170265479709e-01
+3.906939937054616958e-03 1.892982336078212091e+00 1.961808772479244345e-01
+4.498432668969444201e-03 1.862538821228059138e+00 1.875839665940923495e-01
+5.179474679231212825e-03 1.833140068419793378e+00 1.807254197101701987e-01
+5.963623316594642357e-03 1.804839151717607448e+00 1.756910118391523667e-01
+6.866488450042998112e-03 1.777705814340383661e+00 1.721635560558101297e-01
+7.906043210907700777e-03 1.751789858031411518e+00 1.685009977035207829e-01
+9.102981779915217050e-03 1.727107965448127613e+00 1.641839731575882588e-01
+1.048113134154685273e-02 1.703752463217196089e+00 1.606642201493900246e-01
+1.206792640639328847e-02 1.681741841466589227e+00 1.574115297323718798e-01
+1.389495494373137359e-02 1.661180872498956518e+00 1.506227976403611279e-01
+1.599858719606057217e-02 1.641936295425339809e+00 1.438243804779347768e-01
+1.842069969326716461e-02 1.623973998634635985e+00 1.377901334833385172e-01
+2.120950887920190417e-02 1.606952748332304948e+00 1.316949735372383856e-01
+2.442053094548649744e-02 1.590788822310530204e+00 1.243226889588570361e-01
+2.811768697974230749e-02 1.575168016170775287e+00 1.171832076468105194e-01
+3.237457542817643447e-02 1.560097899196016380e+00 1.101710796826544381e-01
+3.727593720314938130e-02 1.545542572083943966e+00 1.036093899612278157e-01
+4.291934260128778267e-02 1.531572602123358662e+00 9.728127487724623490e-02
+4.941713361323833015e-02 1.518341324472995302e+00 8.950734238211086047e-02
+5.689866029018292998e-02 1.505307307266029904e+00 8.810148335699352629e-02
+6.551285568595509312e-02 1.492338872126097726e+00 8.673219802223879060e-02
+7.543120063354614990e-02 1.479666262045960856e+00 8.141922715679962563e-02
+8.685113737513520948e-02 1.467067443862011933e+00 7.605632207591291050e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02
+1.163265306122449050e-01 1.436257699431471968e+00 6.600913866769045346e-02
+1.326530612244898044e-01 1.415413753481877368e+00 6.242141770875467982e-02
+1.489795918367347038e-01 1.390723868801176488e+00 6.004590985567208328e-02
+1.653061224489796033e-01 1.361917477796027676e+00 5.855238285923737573e-02
+1.816326530612245027e-01 1.328624546252491712e+00 5.909126204572044683e-02
+1.979591836734694021e-01 1.291182364995507870e+00 5.799461876770821422e-02
+2.142857142857143016e-01 1.250364307046662793e+00 5.729005890456270039e-02
+2.306122448979592010e-01 1.206927192547645911e+00 5.632383651387896928e-02
+2.469387755102041004e-01 1.161434662449474020e+00 5.575873697984527755e-02
+2.632653061224490276e-01 1.114255548834317322e+00 5.499105653975491781e-02
+2.795918367346938993e-01 1.065729549351170924e+00 5.406308092778352309e-02
+2.959183673469387710e-01 1.016247959667206491e+00 5.288154012402173504e-02
+3.122448979591836982e-01 9.661707426172632118e-01 5.305346802331420419e-02
+3.285714285714286254e-01 9.157971486255552795e-01 5.662310358643761476e-02
+3.448979591836734970e-01 8.653596641887024710e-01 5.083797877077313176e-02
+3.612244897959183687e-01 8.150424664509627881e-01 5.361559377675658572e-02
+3.775510204081632404e-01 7.650336795089378583e-01 5.504084064016197264e-02
+3.938775510204082231e-01 7.155107322841964823e-01 5.815762181981395940e-02
+4.102040816326530948e-01 6.666148092756788435e-01 5.388739088185211246e-02
+4.265306122448979664e-01 6.184808244113076947e-01 5.672644041609450211e-02
+4.428571428571428381e-01 5.712833756315996991e-01 6.581076428913901211e-02
+4.591836734693878208e-01 5.252968102940863693e-01 6.702217841793813025e-02
+4.755102040816326925e-01 4.808391912274195823e-01 6.858931665301323810e-02
+4.918367346938775642e-01 4.381130722623843288e-01 6.718507026803152704e-02
+5.081632653061224358e-01 3.972125375443600337e-01 6.965800777247084519e-02
+5.244897959183674185e-01 3.581097245266221529e-01 6.844653703788193544e-02
+5.408163265306122902e-01 3.211058522457649334e-01 6.801459714625253394e-02
+5.571428571428571619e-01 2.864888840811441462e-01 6.623243611783007079e-02
+5.734693877551020336e-01 2.542592546856339286e-01 6.508281122168510513e-02
+5.897959183673470163e-01 2.245039172586136000e-01 6.817903370165791233e-02
+6.061224489795918879e-01 1.971904898479003854e-01 6.724857957251778595e-02
+6.224489795918367596e-01 1.722555534350652118e-01 6.682717720305531017e-02
+6.387755102040816313e-01 1.496087563872702808e-01 6.578562391334155657e-02
+6.551020408163265030e-01 1.292460451082127204e-01 6.357257318602752005e-02
+6.714285714285714857e-01 1.111797350094870934e-01 6.506130007688437633e-02
+6.877551020408163573e-01 9.523361075388962760e-02 6.047832185377364361e-02
+7.040816326530612290e-01 8.122659700001326477e-02 6.162435588265317843e-02
+7.204081632653062117e-01 6.893218067535411553e-02 6.606274324296279588e-02
+7.367346938775510834e-01 5.814267735253492858e-02 7.631079155029231453e-02
+7.530612244897959551e-01 4.869566766134052205e-02 7.742097813387080474e-02
+7.693877551020408267e-01 4.044250240680334618e-02 7.505508510155267654e-02
+7.857142857142856984e-01 3.327547602901389373e-02 7.594730521129608447e-02
+8.020408163265306811e-01 2.710706020503510574e-02 6.967577716341799565e-02
+8.183673469387755528e-01 2.182777327792014693e-02 6.432099519023265488e-02
+8.346938775510204245e-01 1.732718253187195967e-02 5.710674947523330935e-02
+8.510204081632654072e-01 1.350721403879344071e-02 5.190565017544805648e-02
+8.673469387755102789e-01 1.028418426810574250e-02 4.576728749949458830e-02
+8.836734693877551505e-01 7.589738733240949248e-03 4.101680475501470746e-02
+9.000000000000000222e-01 5.365500585267322931e-03 4.142048728873987118e-02
diff --git a/examples/qPDF/data/full/t3.dat b/examples/qPDF/data/full/t3.dat
new file mode 100644
index 000000000..5544fdfca
--- /dev/null
+++ b/examples/qPDF/data/full/t3.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.178039037871642991e-02 -5.501273989189520153e-02
+1.151395399326446927e-04 2.360494800229695489e-02 -5.079617646067179082e-02
+1.325711365590109543e-04 2.614203360893901928e-02 -4.664551928313108442e-02
+1.526417967175233259e-04 2.828035986310167837e-02 -4.300222704272022023e-02
+1.757510624854791170e-04 2.991495798307713949e-02 -3.997435589083222357e-02
+2.023589647725157589e-04 3.126187635942873078e-02 -3.730603537237335565e-02
+2.329951810515371805e-04 3.235494070724337368e-02 -3.489195915358409006e-02
+2.682695795279724476e-04 3.330068004701974838e-02 -3.256661292586237461e-02
+3.088843596477481527e-04 3.405999944634308108e-02 -3.032459936431707720e-02
+3.556480306223128661e-04 3.462309337997493408e-02 -2.816141756988261441e-02
+4.094915062380427508e-04 3.499154103999518872e-02 -2.598801321550247428e-02
+4.714866363457394672e-04 3.517161650060213196e-02 -2.380730015382700321e-02
+5.428675439323859403e-04 3.526360047455268099e-02 -2.200751087594812055e-02
+6.250551925273975734e-04 3.527563526439142505e-02 -2.051531149610450605e-02
+7.196856730011521675e-04 3.524445607586629281e-02 -1.937188316215548295e-02
+8.286427728546842068e-04 3.517307215238224538e-02 -1.824551761460532773e-02
+9.540954763499943534e-04 3.506902486917523598e-02 -1.682295858235231117e-02
+1.098541141987558400e-03 3.494190035771210789e-02 -1.546271661467520792e-02
+1.264855216855295715e-03 3.479091040879289665e-02 -1.425412340907782419e-02
+1.456348477501244378e-03 3.452956214373470445e-02 -1.330042022227230156e-02
+1.676832936811008387e-03 3.422874677965193069e-02 -1.251282444079887882e-02
+1.930697728883249645e-03 3.407769421897388629e-02 -1.162179965122341410e-02
+2.222996482526195736e-03 3.395628867277111773e-02 -1.067351857293324935e-02
+2.559547922699535825e-03 3.379958717023096026e-02 -9.689595645125753665e-03
+2.947051702551809708e-03 3.363598642204607136e-02 -8.576650766480675703e-03
+3.393221771895329857e-03 3.347228843142929788e-02 -7.335712491089114995e-03
+3.906939937054616958e-03 3.346546911966319371e-02 -6.382514472699104408e-03
+4.498432668969444201e-03 3.363083117111598241e-02 -5.638327509948526384e-03
+5.179474679231212825e-03 3.404927746485519879e-02 -5.019592136273724747e-03
+5.963623316594642357e-03 3.466819563282846905e-02 -4.454395194833919713e-03
+6.866488450042998112e-03 3.547883554846648080e-02 -3.933647755103783505e-03
+7.906043210907700777e-03 3.636258386306334689e-02 -3.485432043680698061e-03
+9.102981779915217050e-03 3.729492238222775757e-02 -3.060382179671066599e-03
+1.048113134154685273e-02 3.829244166622192136e-02 -2.979236290565519252e-03
+1.206792640639328847e-02 3.950383879785190899e-02 -3.016866291674448475e-03
+1.389495494373137359e-02 4.140581276527505183e-02 -2.531192989213757599e-03
+1.599858719606057217e-02 4.382995926841448675e-02 -2.114565963955858385e-03
+1.842069969326716461e-02 4.667630562357144575e-02 -2.149637273613766442e-03
+2.120950887920190417e-02 5.016488550414333059e-02 -2.299801919725254912e-03
+2.442053094548649744e-02 5.434134892004915951e-02 -2.427710292972059397e-03
+2.811768697974230749e-02 5.932381167525677124e-02 -2.475451532886998457e-03
+3.237457542817643447e-02 6.517335407775615153e-02 -2.568332880654382311e-03
+3.727593720314938130e-02 7.198498858163970837e-02 -2.797588503884113412e-03
+4.291934260128778267e-02 8.018165078823452641e-02 -2.908308329307534440e-03
+4.941713361323833015e-02 9.018829008283218984e-02 -2.935030353911056641e-03
+5.689866029018292998e-02 1.019564162987246503e-01 -3.211644617809455982e-03
+6.551285568595509312e-02 1.155101083814724927e-01 -3.284794786471789338e-03
+7.543120063354614990e-02 1.303237510513822750e-01 -3.442816632195635704e-03
+8.685113737513520948e-02 1.465592044397492633e-01 -3.910815628155301857e-03
+1.000000000000000056e-01 1.654803099999999638e-01 -3.591407057756365248e-03
+1.000000000000000056e-01 1.654803099999999638e-01 -3.591407057756365248e-03
+1.163265306122449050e-01 1.891290223954812943e-01 -3.207517653019667861e-03
+1.326530612244898044e-01 2.130861595075765058e-01 -2.462179552547698672e-03
+1.489795918367347038e-01 2.373766657780576605e-01 -1.248902758435248284e-03
+1.653061224489796033e-01 2.626463541241408350e-01 -1.215334683369711719e-03
+1.816326530612245027e-01 2.861035556266112168e-01 -1.197192788463815639e-03
+1.979591836734694021e-01 3.058368520521582146e-01 -1.459083957057752401e-03
+2.142857142857143016e-01 3.219613004354836550e-01 -1.747063702520791628e-03
+2.306122448979592010e-01 3.344292412524122460e-01 -2.023823820231218130e-03
+2.469387755102041004e-01 3.450778886737556617e-01 -1.786909755980518356e-03
+2.632653061224490276e-01 3.532490117204427071e-01 -1.842688617728312390e-03
+2.795918367346938993e-01 3.575574858304151338e-01 -1.747100400113369215e-03
+2.959183673469387710e-01 3.589366151255772852e-01 -1.904646522060764700e-03
+3.122448979591836982e-01 3.576319049134298877e-01 -2.463442409355897852e-03
+3.285714285714286254e-01 3.543977802625193352e-01 -2.605198663765615798e-03
+3.448979591836734970e-01 3.490851536982226833e-01 -7.624224416734248430e-04
+3.612244897959183687e-01 3.420271078042709800e-01 -2.663121160636718401e-03
+3.775510204081632404e-01 3.324091991918894928e-01 -5.429737640515337402e-03
+3.938775510204082231e-01 3.205915700716052696e-01 -7.710384988422315164e-03
+4.102040816326530948e-01 3.076833015555935913e-01 -9.268378632880202098e-03
+4.265306122448979664e-01 2.931440885295590881e-01 -9.289541957659729413e-03
+4.428571428571428381e-01 2.776505733554623379e-01 -1.212442063030644800e-02
+4.591836734693878208e-01 2.618972221858207328e-01 -1.416672222868416153e-02
+4.755102040816326925e-01 2.451230091093854757e-01 -1.350860278242318330e-02
+4.918367346938775642e-01 2.290377420641576400e-01 -1.413445531461066280e-02
+5.081632653061224358e-01 2.135512008039863463e-01 -1.453468428403530480e-02
+5.244897959183674185e-01 1.984985872720557709e-01 -1.397634559474913822e-02
+5.408163265306122902e-01 1.841811580961135175e-01 -1.309493151133219596e-02
+5.571428571428571619e-01 1.704403134171777467e-01 -1.406728234143836236e-02
+5.734693877551020336e-01 1.562671367343420326e-01 -1.400188817532591751e-02
+5.897959183673470163e-01 1.418647467121848249e-01 -1.070000414607550666e-02
+6.061224489795918879e-01 1.278475110007968318e-01 -8.516135160069529877e-03
+6.224489795918367596e-01 1.138655403828967849e-01 -5.058930938126255175e-03
+6.387755102040816313e-01 1.001273020151677018e-01 -4.123350891244225574e-03
+6.551020408163265030e-01 8.643471418070733381e-02 -4.074131125049942556e-03
+6.714285714285714857e-01 7.191910637408532869e-02 -4.635433453179224617e-03
+6.877551020408163573e-01 5.815207638918958727e-02 -4.533647393946745041e-03
+7.040816326530612290e-01 4.612698979303683777e-02 -3.528400028220969945e-03
+7.204081632653062117e-01 3.750154254323265907e-02 -3.197032357921831755e-03
+7.367346938775510834e-01 3.051268156801009712e-02 -1.996130669194155424e-03
+7.530612244897959551e-01 2.418628742139881244e-02 1.089112760743771516e-03
+7.693877551020408267e-01 1.892921581226242669e-02 -9.156765312932045597e-05
+7.857142857142856984e-01 1.498062986787362071e-02 -1.567888516314374855e-03
+8.020408163265306811e-01 1.205208052239571496e-02 -7.717307690063845982e-04
+8.183673469387755528e-01 9.719048837170184835e-03 -8.520002326649605412e-04
+8.346938775510204245e-01 7.902985505951989897e-03 -8.459919359694485774e-04
+8.510204081632654072e-01 6.098078701810978955e-03 -7.748156400943020938e-04
+8.673469387755102789e-01 4.597830936409629324e-03 -7.584395675897092487e-04
+8.836734693877551505e-01 3.303316269833258753e-03 -1.187778836739436863e-03
+9.000000000000000222e-01 2.478024997064359140e-03 -1.682045282580753136e-03
diff --git a/examples/qPDF/data/full/t8.dat b/examples/qPDF/data/full/t8.dat
new file mode 100644
index 000000000..7a2732bdb
--- /dev/null
+++ b/examples/qPDF/data/full/t8.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.967891727365936649e-01 -5.351913518991966434e-01
+1.151395399326446927e-04 3.064897739743315253e-01 -5.228070936938105095e-01
+1.325711365590109543e-04 3.158794264026344445e-01 -5.090709612371046022e-01
+1.526417967175233259e-04 3.248707472090531789e-01 -4.941991141595850068e-01
+1.757510624854791170e-04 3.334714075036346870e-01 -4.785649375008670381e-01
+2.023589647725157589e-04 3.418424717767015730e-01 -4.624912367226028387e-01
+2.329951810515371805e-04 3.500848868970291017e-01 -4.458210154938854197e-01
+2.682695795279724476e-04 3.584611086702909066e-01 -4.283162187443461111e-01
+3.088843596477481527e-04 3.668684233087414759e-01 -4.105069411107938326e-01
+3.556480306223128661e-04 3.751541695227437190e-01 -3.927431014729541592e-01
+4.094915062380427508e-04 3.844380513026034452e-01 -3.753661775630095510e-01
+4.714866363457394672e-04 3.947170717208153823e-01 -3.586138022321409546e-01
+5.428675439323859403e-04 4.033746393990121915e-01 -3.417970480025358526e-01
+6.250551925273975734e-04 4.105281640533186005e-01 -3.249129270673927206e-01
+7.196856730011521675e-04 4.155169660940215559e-01 -3.074495584179327556e-01
+8.286427728546842068e-04 4.201191315065782028e-01 -2.900762778330903124e-01
+9.540954763499943534e-04 4.257318112086013739e-01 -2.732669519568579064e-01
+1.098541141987558400e-03 4.303743647380635640e-01 -2.570325180831006806e-01
+1.264855216855295715e-03 4.338180042770034550e-01 -2.413071011501616847e-01
+1.456348477501244378e-03 4.368136614232921433e-01 -2.261871807747988239e-01
+1.676832936811008387e-03 4.392256045319101743e-01 -2.114730824142506405e-01
+1.930697728883249645e-03 4.406562962078498513e-01 -1.967104472715614505e-01
+2.222996482526195736e-03 4.417490464017018725e-01 -1.821002756467193229e-01
+2.559547922699535825e-03 4.426500358456443296e-01 -1.675258441897166661e-01
+2.947051702551809708e-03 4.450971951998125009e-01 -1.538982966950086517e-01
+3.393221771895329857e-03 4.494439335672644198e-01 -1.412161338165656699e-01
+3.906939937054616958e-03 4.561658895047046025e-01 -1.292383936954070722e-01
+4.498432668969444201e-03 4.651977385458023750e-01 -1.180918926972806537e-01
+5.179474679231212825e-03 4.768204251285368955e-01 -1.083340128914820760e-01
+5.963623316594642357e-03 4.897136399974937881e-01 -1.001283229540880920e-01
+6.866488450042998112e-03 5.028157826833805633e-01 -9.246198104295999531e-02
+7.906043210907700777e-03 5.162903347157841161e-01 -8.530814794740892992e-02
+9.102981779915217050e-03 5.301498020885356821e-01 -7.836694087990382640e-02
+1.048113134154685273e-02 5.468735475869694884e-01 -7.269911721980590324e-02
+1.206792640639328847e-02 5.669838951047392328e-01 -6.756187116634179701e-02
+1.389495494373137359e-02 5.925765651394002687e-01 -6.127512509837636234e-02
+1.599858719606057217e-02 6.228247868785594665e-01 -5.433207365904305780e-02
+1.842069969326716461e-02 6.575990713816981525e-01 -4.618559625605917623e-02
+2.120950887920190417e-02 6.958361640459556074e-01 -3.991318766042432908e-02
+2.442053094548649744e-02 7.372735601027718966e-01 -3.546250788524608205e-02
+2.811768697974230749e-02 7.804951593565594425e-01 -3.015335398039294124e-02
+3.237457542817643447e-02 8.259908637776820495e-01 -2.470326180773166347e-02
+3.727593720314938130e-02 8.752730218901014769e-01 -2.141317462140131939e-02
+4.291934260128778267e-02 9.279416482263004307e-01 -1.821674977036366605e-02
+4.941713361323833015e-02 9.863701585885369383e-01 -1.249541199529381405e-02
+5.689866029018292998e-02 1.036754676663268926e+00 -1.089222945444151547e-02
+6.551285568595509312e-02 1.075629503163584744e+00 -6.736547651658684327e-03
+7.543120063354614990e-02 1.105939016479328574e+00 -3.621107408948803785e-03
+8.685113737513520948e-02 1.127803464297723046e+00 -2.163644186741874725e-03
+1.000000000000000056e-01 1.138711483999999885e+00 -2.740669875090178537e-03
+1.000000000000000056e-01 1.138711483999999885e+00 -2.740669875090178537e-03
+1.163265306122449050e-01 1.151017244962964980e+00 1.037318243192690559e-03
+1.326530612244898044e-01 1.159271728947077085e+00 2.573872010971296406e-03
+1.489795918367347038e-01 1.159782281727252728e+00 3.152171299635436741e-03
+1.653061224489796033e-01 1.153505937395747249e+00 4.939946258408016086e-03
+1.816326530612245027e-01 1.141451524305949761e+00 6.268876466634652408e-03
+1.979591836734694021e-01 1.123688166046398207e+00 8.248278204087602233e-03
+2.142857142857143016e-01 1.100877192739575072e+00 9.326223083890011062e-03
+2.306122448979592010e-01 1.073872463500070706e+00 9.320583008029778571e-03
+2.469387755102041004e-01 1.043560859401786445e+00 8.268252049006056398e-03
+2.632653061224490276e-01 1.010507768078387603e+00 6.727057482457301368e-03
+2.795918367346938993e-01 9.743783295117218746e-01 5.463220273594844231e-03
+2.959183673469387710e-01 9.364607041507967411e-01 5.229266156563059167e-03
+3.122448979591836982e-01 8.954439656483303978e-01 6.817122484342049218e-03
+3.285714285714286254e-01 8.529476078765321212e-01 1.228989874997294014e-02
+3.448979591836734970e-01 8.094105788772453236e-01 9.422659712189892026e-03
+3.612244897959183687e-01 7.652022029235848732e-01 1.475014803148823675e-02
+3.775510204081632404e-01 7.209794008570916679e-01 1.343544728946480075e-02
+3.938775510204082231e-01 6.761035453466663903e-01 1.424031491769873342e-02
+4.102040816326530948e-01 6.303581793950283618e-01 1.208802642013349048e-02
+4.265306122448979664e-01 5.824717232695867386e-01 1.567762681743947381e-02
+4.428571428571428381e-01 5.359270605702105561e-01 2.373088270821131773e-02
+4.591836734693878208e-01 4.926155228680057507e-01 2.277983998031588367e-02
+4.755102040816326925e-01 4.511776631645700864e-01 2.119289716436993654e-02
+4.918367346938775642e-01 4.101274867609975283e-01 1.857514540571357162e-02
+5.081632653061224358e-01 3.688274588658959630e-01 2.153776701042124306e-02
+5.244897959183674185e-01 3.299857352693796320e-01 2.057477160238923436e-02
+5.408163265306122902e-01 2.947601101555517578e-01 2.147811573443057787e-02
+5.571428571428571619e-01 2.622506614519226620e-01 2.184512970481299748e-02
+5.734693877551020336e-01 2.319658116797040648e-01 2.164059264137865174e-02
+5.897959183673470163e-01 2.038300728024848851e-01 2.499849853906035979e-02
+6.061224489795918879e-01 1.779961534041439308e-01 2.436415844152281518e-02
+6.224489795918367596e-01 1.545427884236979243e-01 2.516110110112216094e-02
+6.387755102040816313e-01 1.335632662539781024e-01 2.590675883781437538e-02
+6.551020408163265030e-01 1.152348253417438545e-01 2.597207634522478992e-02
+6.714285714285714857e-01 9.875996651570564944e-02 2.776972279037007857e-02
+6.877551020408163573e-01 8.407398278488814602e-02 2.090721250125920222e-02
+7.040816326530612290e-01 7.106167117549450074e-02 1.884473803010927595e-02
+7.204081632653062117e-01 5.909780060400191687e-02 1.396685124656719609e-02
+7.367346938775510834e-01 4.837950241516492111e-02 7.232931761620698535e-03
+7.530612244897959551e-01 3.878217089483680657e-02 3.172534007081652396e-03
+7.693877551020408267e-01 3.091164592705523267e-02 -1.811811273991242865e-03
+7.857142857142856984e-01 2.478933121691693359e-02 -7.057285602139504871e-03
+8.020408163265306811e-01 1.955018661238028949e-02 -9.803192065236304709e-03
+8.183673469387755528e-01 1.531750868718114930e-02 -8.836503694312693169e-03
+8.346938775510204245e-01 1.191668445291303551e-02 -7.980204458145706559e-03
+8.510204081632654072e-01 9.051946361661511453e-03 -8.472351544024037118e-03
+8.673469387755102789e-01 6.748583785838472258e-03 -9.243666320254941698e-03
+8.836734693877551505e-01 4.891974298810318825e-03 -1.233570620581838501e-02
+9.000000000000000222e-01 3.465658109240731845e-03 -1.309887794038555250e-02
diff --git a/examples/qPDF/data/full/u.dat b/examples/qPDF/data/full/u.dat
new file mode 100644
index 000000000..5009d9747
--- /dev/null
+++ b/examples/qPDF/data/full/u.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 5.219220537334201548e-01 5.101195446701856789e-02
+1.151395399326446927e-04 5.156526241489136630e-01 4.776331717737190841e-02
+1.325711365590109543e-04 5.098046427678676329e-01 4.494062937806012764e-02
+1.526417967175233259e-04 5.039964919810068222e-01 4.246806411747052618e-02
+1.757510624854791170e-04 4.981942223092373778e-01 4.019966515326047624e-02
+2.023589647725157589e-04 4.924262174091713629e-01 3.804123406478818192e-02
+2.329951810515371805e-04 4.867224007181961132e-01 3.608221262589485223e-02
+2.682695795279724476e-04 4.811526626320515310e-01 3.443978609923611933e-02
+3.088843596477481527e-04 4.756877643230396635e-01 3.300710437515452095e-02
+3.556480306223128661e-04 4.703099872413792903e-01 3.170528946213140714e-02
+4.094915062380427508e-04 4.650958587561209168e-01 3.042161068281441436e-02
+4.714866363457394672e-04 4.600498690970135085e-01 2.914079159274975475e-02
+5.428675439323859403e-04 4.550766233860727983e-01 2.793687468529989018e-02
+6.250551925273975734e-04 4.501747255024930450e-01 2.679996196874918565e-02
+7.196856730011521675e-04 4.452759085470045508e-01 2.567988106450463243e-02
+8.286427728546842068e-04 4.405721796225224307e-01 2.469367976882737292e-02
+9.540954763499943534e-04 4.362095698706958125e-01 2.393324654235414600e-02
+1.098541141987558400e-03 4.320237595946461173e-01 2.328298197692286325e-02
+1.264855216855295715e-03 4.279933994511625484e-01 2.263887670833293272e-02
+1.456348477501244378e-03 4.240851480967033349e-01 2.183124754535667802e-02
+1.676832936811008387e-03 4.203183109884729984e-01 2.091360557641431042e-02
+1.930697728883249645e-03 4.167243612114405749e-01 2.000767031130366511e-02
+2.222996482526195736e-03 4.133310437787030001e-01 1.916177200614344739e-02
+2.559547922699535825e-03 4.101407331471613738e-01 1.832704844274586978e-02
+2.947051702551809708e-03 4.072887576391579501e-01 1.766551193789412180e-02
+3.393221771895329857e-03 4.048090913623783593e-01 1.716012524618305488e-02
+3.906939937054616958e-03 4.029522091222026980e-01 1.667415005121403518e-02
+4.498432668969444201e-03 4.017348062287342536e-01 1.628672313083027665e-02
+5.179474679231212825e-03 4.012688743826219318e-01 1.612393769379286482e-02
+5.963623316594642357e-03 4.014596669927691375e-01 1.621016685026222329e-02
+6.866488450042998112e-03 4.022067458018352970e-01 1.660651255706058552e-02
+7.906043210907700777e-03 4.037796719303393678e-01 1.682389308193954491e-02
+9.102981779915217050e-03 4.062313348717502270e-01 1.671433878925073641e-02
+1.048113134154685273e-02 4.096021530050364401e-01 1.663100415935659868e-02
+1.206792640639328847e-02 4.140619207912534239e-01 1.651829798795584683e-02
+1.389495494373137359e-02 4.201333205693014716e-01 1.610501627869928912e-02
+1.599858719606057217e-02 4.277409593992672066e-01 1.569103380335565637e-02
+1.842069969326716461e-02 4.369087494880640743e-01 1.536555653489372723e-02
+2.120950887920190417e-02 4.476547574949510389e-01 1.490362048316696587e-02
+2.442053094548649744e-02 4.599853897925060275e-01 1.418829428266056916e-02
+2.811768697974230749e-02 4.741084058178651195e-01 1.354392051007238286e-02
+3.237457542817643447e-02 4.899005321819326908e-01 1.290842663219851465e-02
+3.727593720314938130e-02 5.069998482498023762e-01 1.223665176148918537e-02
+4.291934260128778267e-02 5.254022328108304274e-01 1.153658362340799666e-02
+4.941713361323833015e-02 5.452891843405023309e-01 1.073810634562078620e-02
+5.689866029018292998e-02 5.652524514330429239e-01 1.069619278512902623e-02
+6.551285568595509312e-02 5.849306960399397459e-01 1.094597870566028225e-02
+7.543120063354614990e-02 6.049652784666068595e-01 1.040855047432872119e-02
+8.685113737513520948e-02 6.252059430678251761e-01 9.753369968095629283e-03
+1.000000000000000056e-01 6.450658200000000120e-01 9.793002637796645662e-03
+1.163265306122449050e-01 6.681633787721220630e-01 1.017643806531060534e-02
+1.326530612244898044e-01 6.874414050327382064e-01 1.051611966882545074e-02
+1.489795918367347038e-01 7.024060766836944802e-01 1.091152213637827662e-02
+1.653061224489796033e-01 7.139294147955075331e-01 1.133628869175182935e-02
+1.816326530612245027e-01 7.207131843689202899e-01 1.230426517053745221e-02
+1.979591836734694021e-01 7.223708121538943150e-01 1.250032090847500062e-02
+2.142857142857143016e-01 7.189764487567673612e-01 1.251781872499279843e-02
+2.306122448979592010e-01 7.117412524307588084e-01 1.229772223944200320e-02
+2.469387755102041004e-01 7.013582295319235049e-01 1.194003310693880424e-02
+2.632653061224490276e-01 6.881045284550147967e-01 1.155866526993589057e-02
+2.795918367346938993e-01 6.714215205856344237e-01 1.105528830905571497e-02
+2.959183673469387710e-01 6.526424526523527669e-01 1.089458089702714447e-02
+3.122448979591836982e-01 6.316226608424150291e-01 1.077018659836364786e-02
+3.285714285714286254e-01 6.077500186539003257e-01 1.116952747323463405e-02
+3.448979591836734970e-01 5.836326619162366391e-01 1.101364075145081986e-02
+3.612244897959183687e-01 5.581722686890733254e-01 1.089771491601521959e-02
+3.775510204081632404e-01 5.309647906816499230e-01 1.029080153886721520e-02
+3.938775510204082231e-01 5.026172192427382779e-01 9.833385791748083804e-03
+4.102040816326530948e-01 4.729472143835790066e-01 8.930840437713421717e-03
+4.265306122448979664e-01 4.424769600331791564e-01 9.367904277973288299e-03
+4.428571428571428381e-01 4.126698332598315866e-01 1.059838981925867105e-02
+4.591836734693878208e-01 3.809692818739471298e-01 1.056008118753932676e-02
+4.755102040816326925e-01 3.512845707804219120e-01 1.082288870256591616e-02
+4.918367346938775642e-01 3.239722561384902888e-01 1.024387162143642650e-02
+5.081632653061224358e-01 2.987031131287182784e-01 1.045563517738316676e-02
+5.244897959183674185e-01 2.729564461704206968e-01 1.074585510624960147e-02
+5.408163265306122902e-01 2.480024241700939380e-01 1.119419072927162716e-02
+5.571428571428571619e-01 2.252591619506083553e-01 1.056903482710983173e-02
+5.734693877551020336e-01 2.033308396707086130e-01 9.862655545891154882e-03
+5.897959183673470163e-01 1.810233365604385358e-01 1.092837393135298769e-02
+6.061224489795918879e-01 1.609589521154481395e-01 1.120069476152043240e-02
+6.224489795918367596e-01 1.415895843814952704e-01 1.196303501419522346e-02
+6.387755102040816313e-01 1.247038505112701839e-01 1.174567493327440676e-02
+6.551020408163265030e-01 1.085126989031491018e-01 1.129038558040623003e-02
+6.714285714285714857e-01 9.332242088573453054e-02 1.158142679674851495e-02
+6.877551020408163573e-01 8.010839740159633737e-02 1.022623955221433310e-02
+7.040816326530612290e-01 6.807985569865412923e-02 1.038097601483088488e-02
+7.204081632653062117e-01 5.772865008052108921e-02 1.071429636674453914e-02
+7.367346938775510834e-01 4.815571853068702224e-02 1.220396766522050848e-02
+7.530612244897959551e-01 3.941675457359115203e-02 1.293771834964407147e-02
+7.693877551020408267e-01 3.326630455306706785e-02 1.201717042389163770e-02
+7.857142857142856984e-01 2.854265247624555857e-02 1.152778484090345723e-02
+8.020408163265306811e-01 2.272068462983138637e-02 1.039916298616219877e-02
+8.183673469387755528e-01 1.825700122372798245e-02 9.575460541838392778e-03
+8.346938775510204245e-01 1.483899533506974518e-02 8.458777665579302399e-03
+8.510204081632654072e-01 1.184481233652320484e-02 7.668364582451015800e-03
+8.673469387755102789e-01 9.122462586070438292e-03 6.634621311653081215e-03
+8.836734693877551505e-01 7.202425593584829006e-03 5.472872084153720604e-03
+9.000000000000000222e-01 6.006656214330170773e-03 5.044743278601566266e-03
diff --git a/examples/qPDF/data/full/ubar.dat b/examples/qPDF/data/full/ubar.dat
new file mode 100644
index 000000000..cc0d9676b
--- /dev/null
+++ b/examples/qPDF/data/full/ubar.dat
@@ -0,0 +1,99 @@
+1.000000000000000048e-04 5.067987518397338853e-01 5.124127680190377482e-02
+1.151395399326446927e-04 4.995934197801784049e-01 4.795290366933818715e-02
+1.325711365590109543e-04 4.927370133474432978e-01 4.507664891223533282e-02
+1.526417967175233259e-04 4.858450664184125745e-01 4.254037186435637163e-02
+1.757510624854791170e-04 4.788827893399035807e-01 4.020147293170975994e-02
+2.023589647725157589e-04 4.718756719793233967e-01 3.799634611606899498e-02
+2.329951810515371805e-04 4.648523336635698722e-01 3.601891928895885331e-02
+2.682695795279724476e-04 4.578730531130874581e-01 3.439870700737103493e-02
+3.088843596477481527e-04 4.509348043934499062e-01 3.301679292827554291e-02
+3.556480306223128661e-04 4.440443199611848746e-01 3.178894497967885840e-02
+4.094915062380427508e-04 4.372159690320540082e-01 3.058986105112534645e-02
+4.714866363457394672e-04 4.304417162115531936e-01 2.939630240460333957e-02
+5.428675439323859403e-04 4.235560691175858095e-01 2.824861352376353829e-02
+6.250551925273975734e-04 4.165560188431848831e-01 2.712931263009265431e-02
+7.196856730011521675e-04 4.093704541562542265e-01 2.601307584834669853e-02
+8.286427728546842068e-04 4.021308068564666827e-01 2.490003288148680619e-02
+9.540954763499943534e-04 3.949320874227529310e-01 2.375793053299492252e-02
+1.098541141987558400e-03 3.876658208515135762e-01 2.273863236954239489e-02
+1.264855216855295715e-03 3.803179703192489658e-01 2.180199442874538110e-02
+1.456348477501244378e-03 3.728700516603166148e-01 2.083130815800366548e-02
+1.676832936811008387e-03 3.653328801164484330e-01 1.984986194940741369e-02
+1.930697728883249645e-03 3.577266652609021325e-01 1.893543106841875240e-02
+2.222996482526195736e-03 3.500545204250677389e-01 1.816502103213700134e-02
+2.559547922699535825e-03 3.422895344603841816e-01 1.752460980624959835e-02
+2.947051702551809708e-03 3.346403534437092375e-01 1.714101322852151776e-02
+3.393221771895329857e-03 3.271459822660970351e-01 1.700250379916794230e-02
+3.906939937054616958e-03 3.198414013139214696e-01 1.695117104665720340e-02
+4.498432668969444201e-03 3.127044316570278260e-01 1.697013496161331458e-02
+5.179474679231212825e-03 3.057224582102559540e-01 1.699590908043196319e-02
+5.963623316594642357e-03 2.987580358603271202e-01 1.698350646721998730e-02
+6.866488450042998112e-03 2.917574658603277471e-01 1.688573868707861358e-02
+7.906043210907700777e-03 2.843368593211069162e-01 1.681345663014428837e-02
+9.102981779915217050e-03 2.764183690813027394e-01 1.677240352800162676e-02
+1.048113134154685273e-02 2.683957146694470608e-01 1.664968342317060812e-02
+1.206792640639328847e-02 2.603813085542192596e-01 1.648241924539920605e-02
+1.389495494373137359e-02 2.528516493048304503e-01 1.631167130656915876e-02
+1.599858719606057217e-02 2.454412035363491285e-01 1.608279935248101838e-02
+1.842069969326716461e-02 2.380342023676943197e-01 1.566678731000011385e-02
+2.120950887920190417e-02 2.299729449463003006e-01 1.501825368846311515e-02
+2.442053094548649744e-02 2.210942033683580832e-01 1.410535612737032901e-02
+2.811768697974230749e-02 2.113806832062746222e-01 1.327302080709340769e-02
+3.237457542817643447e-02 2.010104550722290884e-01 1.261859926544734757e-02
+3.727593720314938130e-02 1.904795563403108294e-01 1.179723723220513047e-02
+4.291934260128778267e-02 1.800051960086379299e-01 1.117067281878077190e-02
+4.941713361323833015e-02 1.700304443698350820e-01 1.063802221441497581e-02
+5.689866029018292998e-02 1.597419766487830906e-01 1.031983192120517964e-02
+6.551285568595509312e-02 1.489636232349209610e-01 9.891661300973571963e-03
+7.543120063354614990e-02 1.374355097109261348e-01 9.391002422146497303e-03
+8.685113737513520948e-02 1.250806193599857952e-01 8.847999729440413075e-03
+1.000000000000000056e-01 1.111706400000000011e-01 8.179965169869038086e-03
+1.163265306122449050e-01 9.631597337328927366e-02 6.965397550310508876e-03
+1.326530612244898044e-01 8.338664560359232425e-02 6.118297067600263900e-03
+1.489795918367347038e-01 7.203485493994034317e-02 5.551827385416897680e-03
+1.653061224489796033e-01 6.209452577759019820e-02 4.962987139647203824e-03
+1.816326530612245027e-01 5.371508942069232184e-02 4.543103177953506690e-03
+1.979591836734694021e-01 4.624481991229831279e-02 4.206659232929604170e-03
+2.142857142857143016e-01 4.009459034116432535e-02 3.987682791304650483e-03
+2.306122448979592010e-01 3.440335061992415289e-02 3.727169143264499451e-03
+2.469387755102041004e-01 2.969381220895894735e-02 3.816188278520973951e-03
+2.632653061224490276e-01 2.561524157497386753e-02 3.639102757811946837e-03
+2.795918367346938993e-01 2.208906558513888674e-02 3.630724365966011072e-03
+2.959183673469387710e-01 1.853378533508788159e-02 3.323430862555169698e-03
+3.122448979591836982e-01 1.517791038369432736e-02 3.473349017361928009e-03
+3.285714285714286254e-01 1.355770075092034913e-02 5.142614075637582348e-03
+3.448979591836734970e-01 1.094030944405336402e-02 3.876749683563606840e-03
+3.612244897959183687e-01 8.592487438390646048e-03 4.986458385358475201e-03
+3.775510204081632404e-01 7.006103884160290625e-03 4.558050434345648980e-03
+3.938775510204082231e-01 5.636815763899808741e-03 5.167788915004078130e-03
+4.102040816326530948e-01 5.104961177614576566e-03 3.623114640176146664e-03
+4.265306122448979664e-01 4.283378024890269545e-03 4.788897464910948573e-03
+4.428571428571428381e-01 2.771418381560752930e-03 6.357959201349411803e-03
+4.591836734693878208e-01 4.239601722136225777e-03 5.808362884320261189e-03
+4.755102040816326925e-01 4.144939763097098794e-03 6.157426353526132545e-03
+4.918367346938775642e-01 2.599970243847376378e-03 5.548935444868298081e-03
+5.081632653061224358e-01 -2.516103480873571000e-04 6.572923314060078771e-03
+5.244897959183674185e-01 -1.639720875129115768e-03 6.037240612839354058e-03
+5.408163265306122902e-01 -2.116395666802422821e-03 6.043906253157798475e-03
+5.571428571428571619e-01 -3.097840758134474178e-03 5.749631410236337795e-03
+5.734693877551020336e-01 -3.577983923135422562e-03 6.279601865306166236e-03
+5.897959183673470163e-01 -2.507962595204027423e-03 8.400804333261060672e-03
+6.061224489795918879e-01 -2.595344859342544035e-03 8.865376650415127471e-03
+6.224489795918367596e-01 -2.318205541359436277e-03 9.966182175956909278e-03
+6.387755102040816313e-01 -3.517692703172981862e-03 1.049362282662101922e-02
+6.551020408163265030e-01 -3.924797227226901192e-03 1.042963482885535947e-02
+6.714285714285714857e-01 -4.551858755871746043e-03 1.084756265045347563e-02
+6.877551020408163573e-01 -5.735036617494569146e-03 9.716728775460639275e-03
+7.040816326530612290e-01 -6.257262548063195068e-03 1.014102716994294030e-02
+7.204081632653062117e-01 -6.142834477869558822e-03 1.066679325465065900e-02
+7.367346938775510834e-01 -5.354367355641570514e-03 1.216566243602633379e-02
+7.530612244897959551e-01 -4.491012694336645426e-03 1.270622481303653774e-02
+7.693877551020408267e-01 -5.013588714372360991e-03 1.144484866240926858e-02
+7.857142857142856984e-01 -5.693798006173376829e-03 1.073626677616170040e-02
+8.020408163265306811e-01 -4.305580344407663670e-03 9.784254832731606802e-03
+8.183673469387755528e-01 -3.492723870721761714e-03 9.000290850226813633e-03
+8.346938775510204245e-01 -3.041726663775672686e-03 8.010487218373465307e-03
+8.510204081632654072e-01 -2.700052505113971144e-03 7.163601897606069677e-03
+8.673469387755102789e-01 -2.194652043508042282e-03 6.162234962480548327e-03
+8.836734693877551505e-01 -2.151372510115513118e-03 5.118589556761047633e-03
+9.000000000000000222e-01 -2.384168039911821121e-03 5.326226440830149171e-03
diff --git a/examples/qPDF/data/full/v.dat b/examples/qPDF/data/full/v.dat
new file mode 100644
index 000000000..b25221a4c
--- /dev/null
+++ b/examples/qPDF/data/full/v.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.409332433385497074e-02 6.139939433975893923e-04
+1.151395399326446927e-04 2.563399484389794480e-02 7.368417313359243437e-04
+1.325711365590109543e-04 2.729566618448603377e-02 8.940427551678564733e-04
+1.526417967175233259e-04 2.908113496268388554e-02 1.071826646856594012e-03
+1.757510624854791170e-04 3.099065793148558789e-02 1.266470151572163250e-03
+2.023589647725157589e-04 3.301849624366737235e-02 1.425823533246772801e-03
+2.329951810515371805e-04 3.516589939286773880e-02 1.547203870220559274e-03
+2.682695795279724476e-04 3.743142390222492510e-02 1.633470266189929188e-03
+3.088843596477481527e-04 3.982648473418481938e-02 1.675470149689939348e-03
+3.556480306223128661e-04 4.235414178804519647e-02 1.671334622949022242e-03
+4.094915062380427508e-04 4.504059339772775727e-02 1.597829467800016001e-03
+4.714866363457394672e-04 4.789331366448362770e-02 1.452527053360908038e-03
+5.428675439323859403e-04 5.095521483434201082e-02 1.274214129943732066e-03
+6.250551925273975734e-04 5.425861882146287840e-02 1.071538590392259738e-03
+7.196856730011521675e-04 5.786287214154184344e-02 8.629408661428861205e-04
+8.286427728546842068e-04 6.185468967407886653e-02 7.165830880065393593e-04
+9.540954763499943534e-04 6.629607483904109699e-02 7.256939172376880576e-04
+1.098541141987558400e-03 7.124663100199635224e-02 7.133753846192275594e-04
+1.264855216855295715e-03 7.671529760119794039e-02 6.444353030430755025e-04
+1.456348477501244378e-03 8.262935522867614901e-02 4.856455467324855002e-04
+1.676832936811008387e-03 8.893381826859464434e-02 2.505936105514777745e-04
+1.930697728883249645e-03 9.547436464437430992e-02 -8.790577109937802724e-05
+2.222996482526195736e-03 1.022795720491102012e-01 -4.603984101919089689e-04
+2.559547922699535825e-03 1.093206187992075695e-01 -8.090187731591494402e-04
+2.947051702551809708e-03 1.167424380035106957e-01 -1.173662947933473699e-03
+3.393221771895329857e-03 1.245776234560457274e-01 -1.564786627264586061e-03
+3.906939937054616958e-03 1.330186832997240498e-01 -1.865460585694908580e-03
+4.498432668969444201e-03 1.421172032673739816e-01 -1.988769325223599393e-03
+5.179474679231212825e-03 1.520710936709788463e-01 -1.619494149053143928e-03
+5.963623316594642357e-03 1.627637530673841926e-01 -7.765553156137069979e-04
+6.866488450042998112e-03 1.739226180828475354e-01 5.577445779148607063e-04
+7.906043210907700777e-03 1.868863751184585698e-01 9.541796375784616924e-04
+9.102981779915217050e-03 2.019142266763930271e-01 -5.237241789141783288e-05
+1.048113134154685273e-02 2.183944932810497896e-01 -1.257472541037134295e-03
+1.206792640639328847e-02 2.365436214462611486e-01 -2.674044620900754843e-03
+1.389495494373137359e-02 2.567822757776673792e-01 -4.644127065801376131e-03
+1.599858719606057217e-02 2.796240510536457391e-01 -6.365348338658552430e-03
+1.842069969326716461e-02 3.054700108621894450e-01 -6.912084008575233168e-03
+2.120950887920190417e-02 3.349749004169555455e-01 -7.036393485510699514e-03
+2.442053094548649744e-02 3.683216411752401886e-01 -6.973376376096449003e-03
+2.811768697974230749e-02 4.059319752305156070e-01 -5.956858707028535316e-03
+3.237457542817643447e-02 4.473054430221816702e-01 -4.321418927177179264e-03
+3.727593720314938130e-02 4.911395046954560861e-01 -1.644464513056727306e-03
+4.291934260128778267e-02 5.366104690169182234e-01 6.711598535814773669e-04
+4.941713361323833015e-02 5.827091421608007193e-01 7.533135499223631015e-04
+5.689866029018292998e-02 6.294815536387448063e-01 9.901955224021086882e-04
+6.551285568595509312e-02 6.768664823281200782e-01 1.962040869776884161e-03
+7.543120063354614990e-02 7.246611517554401027e-01 2.234599803021370193e-03
+8.685113737513520948e-02 7.729086809360399268e-01 2.632184566432160810e-03
+1.000000000000000056e-01 8.219631450000000061e-01 4.472744027894770547e-03
+1.000000000000000056e-01 8.219631450000000061e-01 4.472744027894770547e-03
+1.163265306122449050e-01 8.760245558745264338e-01 7.629865271835317708e-03
+1.326530612244898044e-01 9.207873942133834033e-01 1.087930043521478582e-02
+1.489795918367347038e-01 9.555260855881660964e-01 1.311758721701782038e-02
+1.653061224489796033e-01 9.811175998983059365e-01 1.436268096786381260e-02
+1.816326530612245027e-01 9.982561169995157746e-01 1.670857810347954955e-02
+1.979591836734694021e-01 1.005614729418596198e+00 1.927636214968469694e-02
+2.142857142857143016e-01 1.004729452204575324e+00 2.069043281326496744e-02
+2.306122448979592010e-01 9.964514094860162929e-01 2.040614599634831342e-02
+2.469387755102041004e-01 9.817668338111157356e-01 1.840051143596008881e-02
+2.632653061224490276e-01 9.616221603847134869e-01 1.591738512518598228e-02
+2.795918367346938993e-01 9.368664327965776639e-01 1.306867091356006538e-02
+2.959183673469387710e-01 9.077384819671754368e-01 1.086561841248540050e-02
+3.122448979591836982e-01 8.753125791436111580e-01 1.014237327275376715e-02
+3.285714285714286254e-01 8.405443498693302429e-01 7.664957427717792628e-03
+3.448979591836734970e-01 8.034485871899764708e-01 8.234582604494007640e-03
+3.612244897959183687e-01 7.645300106285628505e-01 6.934557417598144480e-03
+3.775510204081632404e-01 7.234981658492036782e-01 8.142519083330676533e-03
+3.938775510204082231e-01 6.797301550560850281e-01 7.301918226727460715e-03
+4.102040816326530948e-01 6.347498261678146125e-01 7.564475821368022400e-03
+4.265306122448979664e-01 5.902870051375984639e-01 6.476244337488319333e-03
+4.428571428571428381e-01 5.469390334167323342e-01 6.503510318900787394e-03
+4.591836734693878208e-01 5.054220839413947619e-01 7.157137997245991562e-03
+4.755102040816326925e-01 4.655735881486708361e-01 6.151644386349899504e-03
+4.918367346938775642e-01 4.273494270197562561e-01 5.741698175921050237e-03
+5.081632653061224358e-01 3.907562983066515394e-01 4.228438900749689408e-03
+5.244897959183674185e-01 3.555452126726142970e-01 4.476964374474977013e-03
+5.408163265306122902e-01 3.216816336632508655e-01 5.101654155733479669e-03
+5.571428571428571619e-01 2.893248444780310846e-01 5.733630698988551912e-03
+5.734693877551020336e-01 2.586220812087101084e-01 5.062289292442913834e-03
+5.897959183673470163e-01 2.295356732539315381e-01 4.546381244334137504e-03
+6.061224489795918879e-01 2.020682303291666815e-01 4.700315925673483226e-03
+6.224489795918367596e-01 1.770966967495726796e-01 4.545380858232235186e-03
+6.387755102040816313e-01 1.543235307319828775e-01 3.719669291221451568e-03
+6.551020408163265030e-01 1.327929417946560886e-01 3.262410228304992409e-03
+6.714285714285714857e-01 1.133598162492658462e-01 2.783517477014238982e-03
+6.877551020408163573e-01 9.675785167022572797e-02 2.667898688008315394e-03
+7.040816326530612290e-01 8.187050455239161528e-02 2.400835956446172731e-03
+7.204081632653062117e-01 6.602299540318257165e-02 1.746335188707009989e-03
+7.367346938775510834e-01 5.190213556870747202e-02 1.667785682315762671e-03
+7.530612244897959551e-01 4.021748440642871486e-02 1.635198177865507946e-03
+7.693877551020408267e-01 3.209025564326395408e-02 1.754316217031409025e-03
+7.857142857142856984e-01 2.585250344320047869e-02 1.818274116936699838e-03
+8.020408163265306811e-01 1.859930309235167598e-02 1.440673587944269612e-03
+8.183673469387755528e-01 1.346632027765258706e-02 1.215897313679950575e-03
+8.346938775510204245e-01 9.667061410177868463e-03 1.098713889135851177e-03
+8.510204081632654072e-01 6.733249885862054029e-03 9.985308212159260677e-04
+8.673469387755102789e-01 4.661214622425208925e-03 8.824090909300211227e-04
+8.836734693877551505e-01 3.395664268329615099e-03 7.578399836952638691e-04
+9.000000000000000222e-01 2.906583183801734593e-03 3.342223288867705866e-05
diff --git a/examples/qPDF/data/full/v3.dat b/examples/qPDF/data/full/v3.dat
new file mode 100644
index 000000000..e2ad757d0
--- /dev/null
+++ b/examples/qPDF/data/full/v3.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 3.709153843587331689e-03 -1.294460527052412879e-03
+1.151395399326446927e-04 4.000960575745893255e-03 -1.272189925989306025e-03
+1.325711365590109543e-04 4.318183834072841787e-03 -1.251820952761883987e-03
+1.526417967175233259e-04 4.666986511230430956e-03 -1.229507452258457589e-03
+1.757510624854791170e-04 5.049239129196692044e-03 -1.211296513348857073e-03
+2.023589647725157589e-04 5.477156013369621768e-03 -1.203517031224531075e-03
+2.329951810515371805e-04 5.952690748948608057e-03 -1.212797163812409651e-03
+2.682695795279724476e-04 6.497803876925334343e-03 -1.261760471277802165e-03
+3.088843596477481527e-04 7.049611997319960022e-03 -1.328185468408182857e-03
+3.556480306223128661e-04 7.560114271729201363e-03 -1.407078063365894971e-03
+4.094915062380427508e-04 8.107519750108815870e-03 -1.462992208397947969e-03
+4.714866363457394672e-04 8.703536492151719983e-03 -1.484257969507851715e-03
+5.428675439323859403e-04 9.386648087601701107e-03 -1.435538982956133247e-03
+6.250551925273975734e-04 1.015403625715427527e-02 -1.310693900552002689e-03
+7.196856730011521675e-04 1.098327016996020200e-02 -1.159094632399902680e-03
+8.286427728546842068e-04 1.197364937573081045e-02 -8.301396807884886542e-04
+9.540954763499943534e-04 1.321516338060824136e-02 -1.816803400257541168e-04
+1.098541141987558400e-03 1.453771611306603484e-02 4.205133818923796618e-04
+1.264855216855295715e-03 1.591814324213158827e-02 8.821673442042203550e-04
+1.456348477501244378e-03 1.735972497538579296e-02 1.159102321058695650e-03
+1.676832936811008387e-03 1.890294494622801746e-02 1.320227417617819005e-03
+1.930697728883249645e-03 2.065299596330588683e-02 1.539167098646945786e-03
+2.222996482526195736e-03 2.257944996142652316e-02 1.740399217905457679e-03
+2.559547922699535825e-03 2.468029651314496142e-02 1.906687386700762349e-03
+2.947051702551809708e-03 2.692533578467010136e-02 1.884801149395712999e-03
+3.393221771895329857e-03 2.931276187023079682e-02 1.646727139411188062e-03
+3.906939937054616958e-03 3.196121877494878527e-02 1.239021758940781620e-03
+4.498432668969444201e-03 3.489718879934639029e-02 7.566765492925710268e-04
+5.179474679231212825e-03 3.817972938715480380e-02 2.741198153758152289e-04
+5.963623316594642357e-03 4.192220621672132497e-02 -1.715396287274330300e-04
+6.866488450042998112e-03 4.619519012868572583e-02 -5.470628568529194158e-04
+7.906043210907700777e-03 5.119958305791566522e-02 -7.619644953021954825e-04
+9.102981779915217050e-03 5.698017618741429979e-02 -7.593404022438916823e-04
+1.048113134154685273e-02 6.343513280541218657e-02 -5.374237122598470029e-04
+1.206792640639328847e-02 7.045226773299656520e-02 -1.633807364653898364e-04
+1.389495494373137359e-02 7.766657557742634443e-02 1.842972812932802684e-04
+1.599858719606057217e-02 8.518033946297082215e-02 5.596041918141149429e-04
+1.842069969326716461e-02 9.297027474706190397e-02 7.289304187375958421e-04
+2.120950887920190417e-02 1.012927420441265547e-01 8.083684380427794136e-04
+2.442053094548649744e-02 1.102071391038152715e-01 9.352470145973686144e-04
+2.811768697974230749e-02 1.199096929299429404e-01 1.248678625053845082e-03
+3.237457542817643447e-02 1.304129113505142490e-01 1.350929717884905013e-03
+3.727593720314938130e-02 1.417181886734582563e-01 1.233953045166377022e-03
+4.291934260128778267e-02 1.540653255514338738e-01 9.678512345811188938e-04
+4.941713361323833015e-02 1.677295485913511963e-01 7.172473457791612300e-04
+5.689866029018292998e-02 1.823584676492381318e-01 7.061995826628166689e-04
+6.551285568595509312e-02 1.978628347794686160e-01 1.160841317695027533e-03
+7.543120063354614990e-02 2.159454643005864760e-01 9.173334911300231459e-04
+8.685113737513520948e-02 2.362269267061525013e-01 6.591315587753197175e-04
+1.000000000000000056e-01 2.579254300000000111e-01 3.332937874284975371e-04
+1.000000000000000056e-01 2.579254300000000111e-01 3.332937874284975371e-04
+1.163265306122449050e-01 2.832269254732402985e-01 -1.758261191725733058e-04
+1.326530612244898044e-01 3.053929728929584142e-01 -2.117451617313862583e-04
+1.489795918367347038e-01 3.238159043446356411e-01 -2.168866785661948890e-04
+1.653061224489796033e-01 3.408284603992981499e-01 8.299497311290288115e-04
+1.816326530612245027e-01 3.529769485411317631e-01 1.813881046560281499e-03
+1.979591836734694021e-01 3.625207622508588856e-01 1.753782301094962068e-03
+2.142857142857143016e-01 3.667695907052059834e-01 2.146097855119245834e-03
+2.306122448979592010e-01 3.692429999459841961e-01 2.928125248630514786e-03
+2.469387755102041004e-01 3.696344609371332579e-01 3.230467079454401279e-03
+2.632653061224490276e-01 3.687094707430881857e-01 3.658556769999159360e-03
+2.795918367346938993e-01 3.648377142879627444e-01 3.339683048581209776e-03
+2.959183673469387710e-01 3.614681618638949656e-01 4.009541118659402763e-03
+3.122448979591836982e-01 3.562591328441856442e-01 3.252942299916778898e-03
+3.285714285714286254e-01 3.452605536981402379e-01 2.300327065724727324e-03
+3.448979591836734970e-01 3.396375617630511545e-01 3.377340007886578920e-03
+3.612244897959183687e-01 3.341288504450065644e-01 2.697371197079972624e-03
+3.775510204081632404e-01 3.264499482306293388e-01 3.058979013150607204e-03
+3.938775510204082231e-01 3.176871303691626847e-01 2.507018641180281358e-03
+4.102040816326530948e-01 3.051681204691203964e-01 3.199912002429109933e-03
+4.265306122448979664e-01 2.916682104815467858e-01 2.974320558817333468e-03
+4.428571428571428381e-01 2.806805010781118770e-01 2.902858707303770588e-03
+4.591836734693878208e-01 2.585982451427741302e-01 3.467688305903465773e-03
+4.755102040816326925e-01 2.413939489550236128e-01 4.081741466172366176e-03
+4.918367346938775642e-01 2.294557993863459988e-01 4.240931700175741906e-03
+5.081632653061224358e-01 2.219616674650559951e-01 3.788903873166361846e-03
+5.244897959183674185e-01 2.085937770482449993e-01 5.029172127092289973e-03
+5.408163265306122902e-01 1.932228583245677311e-01 5.421371354795442507e-03
+5.571428571428571619e-01 1.819244310888655236e-01 4.698266066874413546e-03
+5.734693877551020336e-01 1.686736884711234896e-01 3.290103082771974122e-03
+5.897959183673470163e-01 1.494528926901955990e-01 1.731732789425769262e-03
+6.061224489795918879e-01 1.353149002042534077e-01 1.073033203524085752e-03
+6.224489795918367596e-01 1.196382305481427932e-01 5.675822726855447864e-04
+6.387755102040816313e-01 1.099870114073757815e-01 1.099755908800202631e-04
+6.551020408163265030e-01 9.857771370782425491e-02 -4.010140583726017754e-04
+6.714285714285714857e-01 8.728924623054287735e-02 -5.498324803594051291e-04
+6.877551020408163573e-01 7.932002060431865564e-02 -1.006125028624939299e-03
+7.040816326530612290e-01 7.048546113961080828e-02 -1.296709283692231221e-03
+7.204081632653062117e-01 6.295347641224970092e-02 -1.016031134913187098e-03
+7.367346938775510834e-01 5.302574765414624913e-02 -9.547079292404926643e-04
+7.530612244897959551e-01 4.434534000993487629e-02 -4.222279995945016351e-04
+7.693877551020408267e-01 4.040000792708368232e-02 1.790811079042664566e-04
+7.857142857142856984e-01 3.768412261083003284e-02 5.076365411593800059e-04
+8.020408163265306811e-01 3.027078564631083962e-02 5.499912192063957650e-04
+8.183673469387755528e-01 2.510188890796036243e-02 6.099389384973605277e-04
+8.346938775510204245e-01 2.109925377869088445e-02 3.205401471248325168e-04
+8.510204081632654072e-01 1.750471058246003825e-02 3.773376656582864627e-04
+8.673469387755102789e-01 1.371875731792381557e-02 4.023935127803295042e-04
+8.836734693877551505e-01 1.135151875260490516e-02 2.739934372502006096e-04
+9.000000000000000222e-01 9.921337035406383245e-03 -3.139617846961637038e-04
diff --git a/examples/qPDF/data/full/v8.dat b/examples/qPDF/data/full/v8.dat
new file mode 100644
index 000000000..dfdeb756a
--- /dev/null
+++ b/examples/qPDF/data/full/v8.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 3.142570116364562516e-02 1.279459685050776674e-03
+1.151395399326446927e-04 3.308435479737814600e-02 1.205367363498355271e-03
+1.325711365590109543e-04 3.485989265135536197e-02 1.151260142898680172e-03
+1.526417967175233259e-04 3.674532391650664476e-02 1.111022581747167504e-03
+1.757510624854791170e-04 3.873956456544158655e-02 1.090102566206563317e-03
+2.023589647725157589e-04 4.083481205164418704e-02 1.028231719495176222e-03
+2.329951810515371805e-04 4.303053129517608877e-02 9.237437726121178194e-04
+2.682695795279724476e-04 4.532139767855858370e-02 7.648154326440614881e-04
+3.088843596477481527e-04 4.771595411720908064e-02 5.754847871185519992e-04
+3.556480306223128661e-04 5.020537728988849757e-02 3.766318389149536650e-04
+4.094915062380427508e-04 5.287559229861948928e-02 1.838154797281776887e-04
+4.714866363457394672e-04 5.575168050733947389e-02 1.465493068023016043e-05
+5.428675439323859403e-04 5.905295167943280799e-02 -1.122443418009322347e-04
+6.250551925273975734e-04 6.273289354146038832e-02 -1.870994471893372424e-04
+7.196856730011521675e-04 6.675717155153770843e-02 -2.477665381384885901e-04
+8.286427728546842068e-04 7.101790912098437936e-02 -1.808658096041915186e-04
+9.540954763499943534e-04 7.542725486775037069e-02 1.455492417572479269e-04
+1.098541141987558400e-03 8.004122211560438238e-02 5.778067293672195515e-04
+1.264855216855295715e-03 8.486754986269084977e-02 1.085921038826487273e-03
+1.456348477501244378e-03 8.995269323481064339e-02 1.551038267477017341e-03
+1.676832936811008387e-03 9.533611385627405177e-02 1.920592288084971289e-03
+1.930697728883249645e-03 1.010784585245643741e-01 1.991745703567415759e-03
+2.222996482526195736e-03 1.073616461393115951e-01 1.680105010706117730e-03
+2.559547922699535825e-03 1.144250649828131294e-01 7.126072051936299667e-04
+2.947051702551809708e-03 1.216295418116605798e-01 -1.600852960845777773e-04
+3.393221771895329857e-03 1.288851220549040999e-01 -8.648794816137200803e-04
+3.906939937054616958e-03 1.367438239253928600e-01 -1.648270078091540469e-03
+4.498432668969444201e-03 1.452562744974513764e-01 -2.392961982128752330e-03
+5.179474679231212825e-03 1.545971215307738178e-01 -2.815199467855744597e-03
+5.963623316594642357e-03 1.649156620097197434e-01 -2.572308184336874470e-03
+6.866488450042998112e-03 1.762648730972930511e-01 -1.149657365379141982e-03
+7.906043210907700777e-03 1.892853762447305743e-01 4.401529215211918977e-04
+9.102981779915217050e-03 2.041088128276559721e-01 2.034377610009172055e-03
+1.048113134154685273e-02 2.201442450352001368e-01 4.015140635969749461e-03
+1.206792640639328847e-02 2.376396273306931595e-01 6.053503906537527346e-03
+1.389495494373137359e-02 2.571257492992121696e-01 7.495432120503693618e-03
+1.599858719606057217e-02 2.790094146813046350e-01 8.701290807122591398e-03
+1.842069969326716461e-02 3.033964367566540088e-01 9.829992110299348718e-03
+2.120950887920190417e-02 3.322628483256136467e-01 1.095988242511616861e-02
+2.442053094548649744e-02 3.660824188829614467e-01 1.163864064014223654e-02
+2.811768697974230749e-02 4.047733064186829766e-01 9.793079756769386413e-03
+3.237457542817643447e-02 4.474908425623155828e-01 6.329012901206645947e-03
+3.727593720314938130e-02 4.916881760456623396e-01 2.223557066318652964e-03
+4.291934260128778267e-02 5.369653061250165837e-01 -2.050408583142968033e-03
+4.941713361323833015e-02 5.829455097283484655e-01 -3.057864349947345822e-03
+5.689866029018292998e-02 6.270243384803549924e-01 -1.840824609249589600e-03
+6.551285568595509312e-02 6.684809678354665108e-01 -1.080701264518587473e-03
+7.543120063354614990e-02 7.080199161214449921e-01 -1.115911766339468258e-03
+8.685113737513520948e-02 7.462538002564987893e-01 -1.809542377258981788e-03
+1.000000000000000056e-01 7.856684999999999919e-01 -2.671446105093899792e-04
+1.000000000000000056e-01 7.856684999999999919e-01 -2.671446105093899792e-04
+1.163265306122449050e-01 8.293545442242232779e-01 4.533990903847666745e-03
+1.326530612244898044e-01 8.665748494692329107e-01 5.263570222115704703e-03
+1.489795918367347038e-01 8.997274462522860672e-01 6.573654107431214921e-03
+1.653061224489796033e-01 9.262887531129980756e-01 7.024598183513044991e-03
+1.816326530612245027e-01 9.465454900669407756e-01 7.708172608863731248e-03
+1.979591836734694021e-01 9.579642078598071819e-01 5.947898850618089928e-03
+2.142857142857143016e-01 9.635234739688496308e-01 3.361656410241223129e-03
+2.306122448979592010e-01 9.633955920550227869e-01 1.826650838476845222e-03
+2.469387755102041004e-01 9.575494535041556565e-01 2.250644860223596777e-03
+2.632653061224490276e-01 9.455629882815539400e-01 4.706934512374225804e-03
+2.795918367346938993e-01 9.277487215459292447e-01 8.390992685674466753e-03
+2.959183673469387710e-01 9.047705543775533821e-01 1.166704002588283065e-02
+3.122448979591836982e-01 8.792659459325451454e-01 1.373745204075244551e-02
+3.285714285714286254e-01 8.482835465847984313e-01 1.393058433297254296e-02
+3.448979591836734970e-01 8.103442551639935543e-01 1.622016117467551741e-02
+3.612244897959183687e-01 7.660321149119501127e-01 1.350631075750425951e-02
+3.775510204081632404e-01 7.174059443946417813e-01 8.934531421016229588e-03
+3.938775510204082231e-01 6.693607196533725157e-01 5.868688883468267670e-03
+4.102040816326530948e-01 6.220495054927962508e-01 7.117667135200277453e-03
+4.265306122448979664e-01 5.735828503298960879e-01 5.598590526945397550e-03
+4.428571428571428381e-01 5.234709192018245361e-01 3.726986947742665437e-03
+4.591836734693878208e-01 4.737391775997533694e-01 3.792968907112018173e-03
+4.755102040816326925e-01 4.275087629415363910e-01 3.444260923021804158e-03
+4.918367346938775642e-01 3.851674631693069450e-01 3.963425607039449544e-03
+5.081632653061224358e-01 3.463307418523625669e-01 3.472681758940058400e-03
+5.244897959183674185e-01 3.107052457833348602e-01 4.210241830234665722e-03
+5.408163265306122902e-01 2.776810767211733144e-01 4.434284480829685238e-03
+5.571428571428571619e-01 2.457190340297982223e-01 3.354360902640613970e-03
+5.734693877551020336e-01 2.181877137322736082e-01 1.503434250308174902e-03
+5.897959183673470163e-01 1.937577703554055342e-01 8.774567316059845440e-04
+6.061224489795918879e-01 1.712446205776504971e-01 1.392177204712607627e-03
+6.224489795918367596e-01 1.503386543935546216e-01 1.187608494908782078e-03
+6.387755102040816313e-01 1.307211636005658761e-01 -2.569527151626404410e-04
+6.551020408163265030e-01 1.133059520694710415e-01 -1.572737721869544442e-04
+6.714285714285714857e-01 9.865830665947733502e-02 4.856473648199733512e-04
+6.877551020408163573e-01 8.358483896113802492e-02 7.396423703803483435e-04
+7.040816326530612290e-01 7.082531695668828342e-02 5.281490075120173766e-04
+7.204081632653062117e-01 6.232248730722956404e-02 -1.595583001111778448e-04
+7.367346938775510834e-01 5.817900121811786507e-02 -2.416162017449992316e-04
+7.530612244897959551e-01 4.997561476490473831e-02 -6.147511373023085979e-04
+7.693877551020408267e-01 4.429882453685759364e-02 -6.119451888814027296e-04
+7.857142857142856984e-01 4.066132817562254087e-02 -4.103494689009987151e-04
+8.020408163265306811e-01 3.414662672179843983e-02 -8.418719129241747368e-04
+8.183673469387755528e-01 2.826004328751220360e-02 -8.105932931825078624e-04
+8.346938775510204245e-01 2.465244783664412298e-02 -4.693055364111756167e-04
+8.510204081632654072e-01 2.128855753071882961e-02 -1.004985303370321825e-04
+8.673469387755102789e-01 1.742398657884901975e-02 -1.376806251658334312e-04
+8.836734693877551505e-01 1.527690382772810447e-02 -2.119651147850891426e-04
+9.000000000000000222e-01 1.476776805162933331e-02 -8.138580850603621686e-04
diff --git a/examples/qPDF/data/partial/8evols.dat b/examples/qPDF/data/partial/8evols.dat
new file mode 100644
index 000000000..2b0d10efd
--- /dev/null
+++ b/examples/qPDF/data/partial/8evols.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 2.906051619342992609e+00 7.462119819026813428e-01 1.436073027852281747e+00 1.410829786450314760e+00 2.409332433385497074e-02 6.139939433975893923e-04 3.709153843587331689e-03 -1.294460527052412879e-03 3.142570116364562516e-02 1.279459685050776674e-03 2.178039037871642991e-02 -5.501273989189520153e-02 2.967891727365936649e-01 -5.351913518991966434e-01 9.543745599019025317e-04 8.933750230847316287e-02
+1.151395399326446927e-04 2.860722503238625869e+00 6.988723163291101770e-01 1.364958626022871213e+00 1.322403944448146795e+00 2.563399484389794480e-02 7.368417313359243437e-04 4.000960575745893255e-03 -1.272189925989306025e-03 3.308435479737814600e-02 1.205367363498355271e-03 2.360494800229695489e-02 -5.079617646067179082e-02 3.064897739743315253e-01 -5.228070936938105095e-01 3.636680441961381328e-03 7.412584225106703950e-02
+1.325711365590109543e-04 2.815405728981031697e+00 6.586280524367921219e-01 1.308744557998403835e+00 1.246856327363375705e+00 2.729566618448603377e-02 8.940427551678564733e-04 4.318183834072841787e-03 -1.251820952761883987e-03 3.485989265135536197e-02 1.151260142898680172e-03 2.614203360893901928e-02 -4.664551928313108442e-02 3.158794264026344445e-01 -5.090709612371046022e-01 4.933524249824835194e-03 6.407245802265681967e-02
+1.526417967175233259e-04 2.770324102867665061e+00 6.244195241115695350e-01 1.266716265313918344e+00 1.181035820270940784e+00 2.908113496268388554e-02 1.071826646856594012e-03 4.666986511230430956e-03 -1.229507452258457589e-03 3.674532391650664476e-02 1.111022581747167504e-03 2.828035986310167837e-02 -4.300222704272022023e-02 3.248707472090531789e-01 -4.941991141595850068e-01 5.655341068586110242e-03 5.779131852221603555e-02
+1.757510624854791170e-04 2.725517322797724074e+00 5.944487582232559353e-01 1.238632415751813820e+00 1.124100495485785611e+00 3.099065793148558789e-02 1.266470151572163250e-03 5.049239129196692044e-03 -1.211296513348857073e-03 3.873956456544158655e-02 1.090102566206563317e-03 2.991495798307713949e-02 -3.997435589083222357e-02 3.334714075036346870e-01 -4.785649375008670381e-01 5.894428576734326262e-03 5.400134138166334807e-02
+2.023589647725157589e-04 2.681196838693734374e+00 5.641250926612668692e-01 1.223304323506445579e+00 1.082280689395135109e+00 3.301849624366737235e-02 1.425823533246772801e-03 5.477156013369621768e-03 -1.203517031224531075e-03 4.083481205164418704e-02 1.028231719495176222e-03 3.126187635942873078e-02 -3.730603537237335565e-02 3.418424717767015730e-01 -4.624912367226028387e-01 6.105220955743916912e-03 4.880768069883391347e-02
+2.329951810515371805e-04 2.637384376323162360e+00 5.350509620256458376e-01 1.221403075342442968e+00 1.049932598530862737e+00 3.516589939286773880e-02 1.547203870220559274e-03 5.952690748948608057e-03 -1.212797163812409651e-03 4.303053129517608877e-02 9.237437726121178194e-04 3.235494070724337368e-02 -3.489195915358409006e-02 3.500848868970291017e-01 -4.458210154938854197e-01 6.235027687244114672e-03 4.349911980376581361e-02
+2.682695795279724476e-04 2.594164003900461424e+00 5.091953271630039746e-01 1.234246661261282352e+00 1.018495848476023369e+00 3.743142390222492510e-02 1.633470266189929188e-03 6.497803876925334343e-03 -1.261760471277802165e-03 4.532139767855858370e-02 7.648154326440614881e-04 3.330068004701974838e-02 -3.256661292586237461e-02 3.584611086702909066e-01 -4.283162187443461111e-01 6.268431070720082224e-03 3.967181908221596026e-02
+3.088843596477481527e-04 2.551560515699526466e+00 4.856781521167745641e-01 1.261118092261666934e+00 9.820269772796574870e-01 3.982648473418481938e-02 1.675470149689939348e-03 7.049611997319960022e-03 -1.328185468408182857e-03 4.771595411720908064e-02 5.754847871185519992e-04 3.405999944634308108e-02 -3.032459936431707720e-02 3.668684233087414759e-01 -4.105069411107938326e-01 6.217020373943566401e-03 3.686609060461188553e-02
+3.556480306223128661e-04 2.509599140891932478e+00 4.637532165071666101e-01 1.301809107355178963e+00 9.380345387151233227e-01 4.235414178804519647e-02 1.671334622949022242e-03 7.560114271729201363e-03 -1.407078063365894971e-03 5.020537728988849757e-02 3.766318389149536650e-04 3.462309337997493408e-02 -2.816141756988261441e-02 3.751541695227437190e-01 -3.927431014729541592e-01 6.047944115574644883e-03 3.465683609043487129e-02
+4.094915062380427508e-04 2.468315423913098439e+00 4.422539326639124968e-01 1.355955018492741582e+00 8.893711185159632882e-01 4.504059339772775727e-02 1.597829467800016001e-03 8.107519750108815870e-03 -1.462992208397947969e-03 5.287559229861948928e-02 1.838154797281776887e-04 3.499154103999518872e-02 -2.598801321550247428e-02 3.844380513026034452e-01 -3.753661775630095510e-01 6.086277759868033259e-03 3.255440885733468931e-02
+4.714866363457394672e-04 2.427712402325765240e+00 4.210862448528221447e-01 1.423415028768741042e+00 8.375259875883908078e-01 4.789331366448362770e-02 1.452527053360908038e-03 8.703536492151719983e-03 -1.484257969507851715e-03 5.575168050733947389e-02 1.465493068023016043e-05 3.517161650060213196e-02 -2.380730015382700321e-02 3.947170717208153823e-01 -3.586138022321409546e-01 6.353607011376051841e-03 3.045711151395184141e-02
+5.428675439323859403e-04 2.387743918408224975e+00 4.011382559420967331e-01 1.503558397812429392e+00 7.741246897837354979e-01 5.095521483434201082e-02 1.274214129943732066e-03 9.386648087601701107e-03 -1.435538982956133247e-03 5.905295167943280799e-02 -1.122443418009322347e-04 3.526360047455268099e-02 -2.200751087594812055e-02 4.033746393990121915e-01 -3.417970480025358526e-01 6.428561308584479321e-03 2.867200099971633012e-02
+6.250551925273975734e-04 2.348387102320955488e+00 3.820577280180124102e-01 1.594760490544748555e+00 7.053377733669865535e-01 5.425861882146287840e-02 1.071538590392259738e-03 1.015403625715427527e-02 -1.310693900552002689e-03 6.273289354146038832e-02 -1.870994471893372424e-04 3.527563526439142505e-02 -2.051531149610450605e-02 4.105281640533186005e-01 -3.249129270673927206e-01 6.372404207167801926e-03 2.704047344363377792e-02
+7.196856730011521675e-04 2.309572722275872181e+00 3.635766358785286201e-01 1.694368883930322145e+00 6.381482642538304217e-01 5.786287214154184344e-02 8.629408661428861205e-04 1.098327016996020200e-02 -1.159094632399902680e-03 6.675717155153770843e-02 -2.477665381384885901e-04 3.524445607586629281e-02 -1.937188316215548295e-02 4.155169660940215559e-01 -3.074495584179327556e-01 6.258801326906005338e-03 2.571516118777497986e-02
+8.286427728546842068e-04 2.271337215556608058e+00 3.459199706961187948e-01 1.797933073006341420e+00 5.770645929332395285e-01 6.185468967407886653e-02 7.165830880065393593e-04 1.197364937573081045e-02 -8.301396807884886542e-04 7.101790912098437936e-02 -1.808658096041915186e-04 3.517307215238224538e-02 -1.824551761460532773e-02 4.201191315065782028e-01 -2.900762778330903124e-01 6.047430101503004259e-03 2.473241740672309924e-02
+9.540954763499943534e-04 2.233683502841355306e+00 3.289489556794900627e-01 1.902288016649845925e+00 5.213202772176038780e-01 6.629607483904109699e-02 7.256939172376880576e-04 1.321516338060824136e-02 -1.816803400257541168e-04 7.542725486775037069e-02 1.455492417572479269e-04 3.506902486917523598e-02 -1.682295858235231117e-02 4.257318112086013739e-01 -2.732669519568579064e-01 5.727973869072462351e-03 2.400751060148542324e-02
+1.098541141987558400e-03 2.196685734288134739e+00 3.130908018498047563e-01 2.004993449735332334e+00 4.718001852409516306e-01 7.124663100199635224e-02 7.133753846192275594e-04 1.453771611306603484e-02 4.205133818923796618e-04 8.004122211560438238e-02 5.778067293672195515e-04 3.494190035771210789e-02 -1.546271661467520792e-02 4.303743647380635640e-01 -2.570325180831006806e-01 5.217025855255588707e-03 2.331562484684581577e-02
+1.264855216855295715e-03 2.160359142346439221e+00 2.982326008119254857e-01 2.105514952847052701e+00 4.273401137522174764e-01 7.671529760119794039e-02 6.444353030430755025e-04 1.591814324213158827e-02 8.821673442042203550e-04 8.486754986269084977e-02 1.085921038826487273e-03 3.479091040879289665e-02 -1.425412340907782419e-02 4.338180042770034550e-01 -2.413071011501616847e-01 4.520400786895952851e-03 2.287525171199299504e-02
+1.456348477501244378e-03 2.124768531205242450e+00 2.835482339955216480e-01 2.204919381213297669e+00 3.857407874870817932e-01 8.262935522867614901e-02 4.856455467324855002e-04 1.735972497538579296e-02 1.159102321058695650e-03 8.995269323481064339e-02 1.551038267477017341e-03 3.452956214373470445e-02 -1.330042022227230156e-02 4.368136614232921433e-01 -2.261871807747988239e-01 4.104105861430898221e-03 2.251634616463276534e-02
+1.676832936811008387e-03 2.089886636778342410e+00 2.689103658428349530e-01 2.302838545246236102e+00 3.527675463927992805e-01 8.893381826859464434e-02 2.505936105514777745e-04 1.890294494622801746e-02 1.320227417617819005e-03 9.533611385627405177e-02 1.920592288084971289e-03 3.422874677965193069e-02 -1.251282444079887882e-02 4.392256045319101743e-01 -2.114730824142506405e-01 3.888985899011146528e-03 2.211418539704611622e-02
+1.930697728883249645e-03 2.055685659097348683e+00 2.544796379090471516e-01 2.401250317650158284e+00 3.292460342654914274e-01 9.547436464437430992e-02 -8.790577109937802724e-05 2.065299596330588683e-02 1.539167098646945786e-03 1.010784585245643741e-01 1.991745703567415759e-03 3.407769421897388629e-02 -1.162179965122341410e-02 4.406562962078498513e-01 -1.967104472715614505e-01 3.777269112706165780e-03 2.186241065726403712e-02
+2.222996482526195736e-03 2.022064289211034094e+00 2.409054177577455991e-01 2.489413722921670402e+00 3.241005711624331820e-01 1.022795720491102012e-01 -4.603984101919089689e-04 2.257944996142652316e-02 1.740399217905457679e-03 1.073616461393115951e-01 1.680105010706117730e-03 3.395628867277111773e-02 -1.067351857293324935e-02 4.417490464017018725e-01 -1.821002756467193229e-01 3.716552809729335802e-03 2.186462296014471396e-02
+2.559547922699535825e-03 1.988934505919132345e+00 2.278774653941089690e-01 2.558364682243282839e+00 3.028155002281870400e-01 1.093206187992075695e-01 -8.090187731591494402e-04 2.468029651314496142e-02 1.906687386700762349e-03 1.144250649828131294e-01 7.126072051936299667e-04 3.379958717023096026e-02 -9.689595645125753665e-03 4.426500358456443296e-01 -1.675258441897166661e-01 3.668101774664294951e-03 2.202517508457559753e-02
+2.947051702551809708e-03 1.956332697014812272e+00 2.162392065064994151e-01 2.616224206212616199e+00 2.820170227437128752e-01 1.167424380035106957e-01 -1.173662947933473699e-03 2.692533578467010136e-02 1.884801149395712999e-03 1.216295418116605798e-01 -1.600852960845777773e-04 3.363598642204607136e-02 -8.576650766480675703e-03 4.450971951998125009e-01 -1.538982966950086517e-01 3.547940999186200654e-03 2.200550651002718128e-02
+3.393221771895329857e-03 1.924269027649759600e+00 2.058747170265479709e-01 2.664071453832157932e+00 2.591501164691561443e-01 1.245776234560457274e-01 -1.564786627264586061e-03 2.931276187023079682e-02 1.646727139411188062e-03 1.288851220549040999e-01 -8.648794816137200803e-04 3.347228843142929788e-02 -7.335712491089114995e-03 4.494439335672644198e-01 -1.412161338165656699e-01 3.334206195109535448e-03 2.177519424557845459e-02
+3.906939937054616958e-03 1.892982336078212091e+00 1.961808772479244345e-01 2.703361738998359520e+00 2.348943198277512467e-01 1.330186832997240498e-01 -1.865460585694908580e-03 3.196121877494878527e-02 1.239021758940781620e-03 1.367438239253928600e-01 -1.648270078091540469e-03 3.346546911966319371e-02 -6.382514472699104408e-03 4.561658895047046025e-01 -1.292383936954070722e-01 2.882653201686987635e-03 2.111194539755851737e-02
+4.498432668969444201e-03 1.862538821228059138e+00 1.875839665940923495e-01 2.734359509219411066e+00 2.093647295324295721e-01 1.421172032673739816e-01 -1.988769325223599393e-03 3.489718879934639029e-02 7.566765492925710268e-04 1.452562744974513764e-01 -2.392961982128752330e-03 3.363083117111598241e-02 -5.638327509948526384e-03 4.651977385458023750e-01 -1.180918926972806537e-01 2.266223600348241541e-03 2.030995470319845245e-02
+5.179474679231212825e-03 1.833140068419793378e+00 1.807254197101701987e-01 2.758211866006374358e+00 1.861954455819890830e-01 1.520710936709788463e-01 -1.619494149053143928e-03 3.817972938715480380e-02 2.741198153758152289e-04 1.545971215307738178e-01 -2.815199467855744597e-03 3.404927746485519879e-02 -5.019592136273724747e-03 4.768204251285368955e-01 -1.083340128914820760e-01 1.650199402710971321e-03 1.966948473734408259e-02
+5.963623316594642357e-03 1.804839151717607448e+00 1.756910118391523667e-01 2.774504985860168027e+00 1.666400687266494929e-01 1.627637530673841926e-01 -7.765553156137069979e-04 4.192220621672132497e-02 -1.715396287274330300e-04 1.649156620097197434e-01 -2.572308184336874470e-03 3.466819563282846905e-02 -4.454395194833919713e-03 4.897136399974937881e-01 -1.001283229540880920e-01 1.045156606308592952e-03 1.936423761741078858e-02
+6.866488450042998112e-03 1.777705814340383661e+00 1.721635560558101297e-01 2.783256351062659117e+00 1.509792672160396587e-01 1.739226180828475354e-01 5.577445779148607063e-04 4.619519012868572583e-02 -5.470628568529194158e-04 1.762648730972930511e-01 -1.149657365379141982e-03 3.547883554846648080e-02 -3.933647755103783505e-03 5.028157826833805633e-01 -9.246198104295999531e-02 4.393240182844387388e-04 1.955534016925687679e-02
+7.906043210907700777e-03 1.751789858031411518e+00 1.685009977035207829e-01 2.783431601575428349e+00 1.393981853977233343e-01 1.868863751184585698e-01 9.541796375784616924e-04 5.119958305791566522e-02 -7.619644953021954825e-04 1.892853762447305743e-01 4.401529215211918977e-04 3.636258386306334689e-02 -3.485432043680698061e-03 5.162903347157841161e-01 -8.530814794740892992e-02 1.293074295599358806e-04 1.970672652804380393e-02
+9.102981779915217050e-03 1.727107965448127613e+00 1.641839731575882588e-01 2.774862150044652420e+00 1.356247879444966997e-01 2.019142266763930271e-01 -5.237241789141783288e-05 5.698017618741429979e-02 -7.593404022438916823e-04 2.041088128276559721e-01 2.034377610009172055e-03 3.729492238222775757e-02 -3.060382179671066599e-03 5.301498020885356821e-01 -7.836694087990382640e-02 1.761382065781277049e-04 1.994970249637265797e-02
+1.048113134154685273e-02 1.703752463217196089e+00 1.606642201493900246e-01 2.760566329600861124e+00 1.342862511369074530e-01 2.183944932810497896e-01 -1.257472541037134295e-03 6.343513280541218657e-02 -5.374237122598470029e-04 2.201442450352001368e-01 4.015140635969749461e-03 3.829244166622192136e-02 -2.979236290565519252e-03 5.468735475869694884e-01 -7.269911721980590324e-02 6.342964865634353891e-04 2.000374435605717371e-02
+1.206792640639328847e-02 1.681741841466589227e+00 1.574115297323718798e-01 2.740601584470428342e+00 1.316902676015173357e-01 2.365436214462611486e-01 -2.674044620900754843e-03 7.045226773299656520e-02 -1.633807364653898364e-04 2.376396273306931595e-01 6.053503906537527346e-03 3.950383879785190899e-02 -3.016866291674448475e-03 5.669838951047392328e-01 -6.756187116634179701e-02 1.159859179318486122e-03 2.010314301162413955e-02
+1.389495494373137359e-02 1.661180872498956518e+00 1.506227976403611279e-01 2.715933591822299764e+00 1.253457936888159319e-01 2.567822757776673792e-01 -4.644127065801376131e-03 7.766657557742634443e-02 1.842972812932802684e-04 2.571257492992121696e-01 7.495432120503693618e-03 4.140581276527505183e-02 -2.531192989213757599e-03 5.925765651394002687e-01 -6.127512509837636234e-02 6.229645941732639153e-04 1.893838285154697196e-02
+1.599858719606057217e-02 1.641936295425339809e+00 1.438243804779347768e-01 2.685154192466012724e+00 1.193572415535045228e-01 2.796240510536457391e-01 -6.365348338658552430e-03 8.518033946297082215e-02 5.596041918141149429e-04 2.790094146813046350e-01 8.701290807122591398e-03 4.382995926841448675e-02 -2.114565963955858385e-03 6.228247868785594665e-01 -5.433207365904305780e-02 -4.528610396075364209e-04 1.816499523496944643e-02
+1.842069969326716461e-02 1.623973998634635985e+00 1.377901334833385172e-01 2.647431992894222486e+00 1.132596856420066828e-01 3.054700108621894450e-01 -6.912084008575233168e-03 9.297027474706190397e-02 7.289304187375958421e-04 3.033964367566540088e-01 9.829992110299348718e-03 4.667630562357144575e-02 -2.149637273613766442e-03 6.575990713816981525e-01 -4.618559625605917623e-02 -2.040862806432878866e-03 1.837584791020675096e-02
+2.120950887920190417e-02 1.606952748332304948e+00 1.316949735372383856e-01 2.601477236862831166e+00 1.124669224631803938e-01 3.349749004169555455e-01 -7.036393485510699514e-03 1.012927420441265547e-01 8.083684380427794136e-04 3.322628483256136467e-01 1.095988242511616861e-02 5.016488550414333059e-02 -2.299801919725254912e-03 6.958361640459556074e-01 -3.991318766042432908e-02 -2.764948712256495682e-03 1.852305431254810605e-02
+2.442053094548649744e-02 1.590788822310530204e+00 1.243226889588570361e-01 2.546935186302070164e+00 1.199475922069823675e-01 3.683216411752401886e-01 -6.973376376096449003e-03 1.102071391038152715e-01 9.352470145973686144e-04 3.660824188829614467e-01 1.163864064014223654e-02 5.434134892004915951e-02 -2.427710292972059397e-03 7.372735601027718966e-01 -3.546250788524608205e-02 -2.301153740602655817e-03 1.806891834668321664e-02
+2.811768697974230749e-02 1.575168016170775287e+00 1.171832076468105194e-01 2.486491029640843564e+00 1.260482655459385648e-01 4.059319752305156070e-01 -5.956858707028535316e-03 1.199096929299429404e-01 1.248678625053845082e-03 4.047733064186829766e-01 9.793079756769386413e-03 5.932381167525677124e-02 -2.475451532886998457e-03 7.804951593565594425e-01 -3.015335398039294124e-02 -2.065953710478731550e-03 1.794252940578616382e-02
+3.237457542817643447e-02 1.560097899196016380e+00 1.101710796826544381e-01 2.421072694716483120e+00 1.342529033015319173e-01 4.473054430221816702e-01 -4.321418927177179264e-03 1.304129113505142490e-01 1.350929717884905013e-03 4.474908425623155828e-01 6.329012901206645947e-03 6.517335407775615153e-02 -2.568332880654382311e-03 8.259908637776820495e-01 -2.470326180773166347e-02 -1.879599560993519027e-03 1.738587176486946009e-02
+3.727593720314938130e-02 1.545542572083943966e+00 1.036093899612278157e-01 2.356120187280772438e+00 1.228918399554665974e-01 4.911395046954560861e-01 -1.644464513056727306e-03 1.417181886734582563e-01 1.233953045166377022e-03 4.916881760456623396e-01 2.223557066318652964e-03 7.198498858163970837e-02 -2.797588503884113412e-03 8.752730218901014769e-01 -2.141317462140131939e-02 -1.281647868885260524e-03 1.660475291361803579e-02
+4.291934260128778267e-02 1.531572602123358662e+00 9.728127487724623490e-02 2.283609481004460040e+00 1.135313148672207062e-01 5.366104690169182234e-01 6.711598535814773669e-04 1.540653255514338738e-01 9.678512345811188938e-04 5.369653061250165837e-01 -2.050408583142968033e-03 8.018165078823452641e-02 -2.908308329307534440e-03 9.279416482263004307e-01 -1.821674977036366605e-02 -4.063840395447936319e-04 1.568866817153680149e-02
+4.941713361323833015e-02 1.518341324472995302e+00 8.950734238211086047e-02 2.197861404801443985e+00 1.111765296966798489e-01 5.827091421608007193e-01 7.533135499223631015e-04 1.677295485913511963e-01 7.172473457791612300e-04 5.829455097283484655e-01 -3.057864349947345822e-03 9.018829008283218984e-02 -2.935030353911056641e-03 9.863701585885369383e-01 -1.249541199529381405e-02 8.499527604997306370e-04 1.472870517349008160e-02
+5.689866029018292998e-02 1.505307307266029904e+00 8.810148335699352629e-02 2.094792655510467227e+00 1.086382056511527999e-01 6.294815536387448063e-01 9.901955224021086882e-04 1.823584676492381318e-01 7.061995826628166689e-04 6.270243384803549924e-01 -1.840824609249589600e-03 1.019564162987246503e-01 -3.211644617809455982e-03 1.036754676663268926e+00 -1.089222945444151547e-02 1.635985800273112058e-03 1.478982758405596870e-02
+6.551285568595509312e-02 1.492338872126097726e+00 8.673219802223879060e-02 1.972943483478049043e+00 1.016059983620012930e-01 6.768664823281200782e-01 1.962040869776884161e-03 1.978628347794686160e-01 1.160841317695027533e-03 6.684809678354665108e-01 -1.080701264518587473e-03 1.155101083814724927e-01 -3.284794786471789338e-03 1.075629503163584744e+00 -6.736547651658684327e-03 1.735828455516659138e-03 1.592381199680020440e-02
+7.543120063354614990e-02 1.479666262045960856e+00 8.141922715679962563e-02 1.828797090456429908e+00 8.228479680656052009e-02 7.246611517554401027e-01 2.234599803021370193e-03 2.159454643005864760e-01 9.173334911300231459e-04 7.080199161214449921e-01 -1.115911766339468258e-03 1.303237510513822750e-01 -3.442816632195635704e-03 1.105939016479328574e+00 -3.621107408948803785e-03 9.190323300996633893e-04 1.504578981460611906e-02
+8.685113737513520948e-02 1.467067443862011933e+00 7.605632207591291050e-02 1.665052707183172664e+00 7.019163919958561204e-02 7.729086809360399268e-01 2.632184566432160810e-03 2.362269267061525013e-01 6.591315587753197175e-04 7.462538002564987893e-01 -1.809542377258981788e-03 1.465592044397492633e-01 -3.910815628155301857e-03 1.127803464297723046e+00 -2.163644186741874725e-03 -5.170461293543338402e-05 1.330416744770090282e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02 1.486650900000000108e+00 6.906649307998630194e-02 8.219631450000000061e-01 4.472744027894770547e-03 2.579254300000000111e-01 3.332937874284975371e-04 7.856684999999999919e-01 -2.671446105093899792e-04 1.654803099999999638e-01 -3.591407057756365248e-03 1.138711483999999885e+00 -2.740669875090178537e-03 2.390608399999999932e-03 1.207505639463098135e-02
diff --git a/examples/qPDF/data/partial/8flavours.dat b/examples/qPDF/data/partial/8flavours.dat
new file mode 100644
index 000000000..c3e627cce
--- /dev/null
+++ b/examples/qPDF/data/partial/8flavours.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 4.602117399999999803e-02 9.779521697310451789e-03 1.111706400000000011e-01 8.179965169869038086e-03 1.573932000000000109e-01 1.014231559246146948e-02 1.486650900000000108e+00 6.906649307998630194e-02 4.333629500000000245e-01 1.142205927296057952e-02 6.450658200000000120e-01 9.793002637796645662e-03 5.811938900000000047e-02 1.135948457677850472e-02 1.195304199999999966e-03 6.037528197315490675e-03
+1.163265306122449050e-01 3.942477395804350887e-02 8.597488598936026127e-03 9.631597337328927366e-02 6.965397550310508876e-03 1.433649249121687341e-01 8.481243317234056153e-03 1.278363607352488662e+00 5.851715503791719841e-02 4.319854048377612665e-01 1.186810995140672592e-02 6.681633787721220630e-01 1.017643806531060534e-02 5.498144450814458628e-02 9.629446721598575870e-03 1.010899534971207306e-03 5.145507231446976720e-03
+1.326530612244898044e-01 3.328912908047675090e-02 7.353330218090444077e-03 8.338664560359232425e-02 6.118297067600263900e-03 1.295400522962832646e-01 7.243514263008420107e-03 1.093272570071426530e+00 6.495189916810374664e-02 4.282018388324707603e-01 1.185308202596499320e-02 6.874414050327382064e-01 1.051611966882545074e-02 5.135997732852693592e-02 9.225240289123470827e-03 1.097352653894629422e-03 5.055917088070817182e-03
+1.489795918367347038e-01 2.863095571284317298e-02 6.665202107333106224e-03 7.203485493994034317e-02 5.551827385416897680e-03 1.152544742232293196e-01 6.067835425351424811e-03 9.445472028682869725e-01 6.800362366808562764e-02 4.218097916223478294e-01 1.164441685487899864e-02 7.024060766836944802e-01 1.091152213637827662e-02 4.723050215813651143e-02 8.846513143861974710e-03 1.678606730492365837e-03 5.179296401225694929e-03
+1.653061224489796033e-01 2.483575627569225078e-02 5.995471224548147050e-03 6.209452577759019820e-02 4.962987139647203824e-03 1.011855789151688834e-01 5.985629346896574957e-03 8.244352867442255040e-01 6.036562576446031364e-02 4.121920075337880407e-01 1.152898116787217167e-02 7.139294147955075331e-01 1.133628869175182935e-02 4.311203853746148340e-02 8.441498819331736542e-03 2.284077980409629591e-03 5.150763234594859206e-03
+1.816326530612245027e-01 2.170692908957032666e-02 5.655695672266800901e-03 5.371508942069232184e-02 4.543103177953506690e-03 8.715178587795263665e-02 6.048640095465555692e-03 7.266001875331934423e-01 4.804631794363146274e-02 4.011729322850488000e-01 1.199592104148921971e-02 7.207131843689202899e-01 1.230426517053745221e-02 3.894380473376195945e-02 8.655830837138740047e-03 2.610410238272599208e-03 4.943903025434586221e-03
+1.979591836734694021e-01 1.898480791532573819e-02 4.434780959100406011e-03 4.624481991229831279e-02 4.206659232929604170e-03 7.458677501164867607e-02 5.813092362005962271e-03 6.460934389251358212e-01 5.956328597727415797e-02 3.881920050023857649e-01 1.235297173645639665e-02 7.223708121538943150e-01 1.250032090847500062e-02 3.486831510158874825e-02 8.877602058789275594e-03 2.967414949183228793e-03 4.904595754975780986e-03
+2.142857142857143016e-01 1.695823321591360261e-02 3.469831512302461781e-03 4.009459034116432535e-02 3.987682791304650483e-03 6.249873547602553120e-02 5.934263570124668347e-03 5.754064633997411082e-01 5.481677133626022824e-02 3.746110031864225420e-01 1.231830164869357133e-02 7.189764487567673612e-01 1.251781872499279843e-02 3.069355929448877585e-02 9.246090313310376119e-03 3.265868387940330363e-03 4.908035171917084563e-03
+2.306122448979592010e-01 1.548757243205519177e-02 3.091673368091698692e-03 3.440335061992415289e-02 3.727169143264499451e-03 5.181022996671015568e-02 6.203143677695365475e-03 5.137991377127972825e-01 5.361156055870076875e-02 3.599051318315605874e-01 1.184557152524235400e-02 7.117412524307588084e-01 1.229772223944200320e-02 2.650617824238633846e-02 9.284838420715521567e-03 3.536738512125343209e-03 4.936859069713762579e-03
+2.469387755102041004e-01 1.433010380039563374e-02 3.566130690184089388e-03 2.969381220895894735e-02 3.816188278520973951e-03 4.197209834064773853e-02 6.324876696238434202e-03 4.552507567815178291e-01 5.065601192147932419e-02 3.440020547264790451e-01 1.121825444520186364e-02 7.013582295319235049e-01 1.194003310693880424e-02 2.240256390271565257e-02 8.949419548762920645e-03 3.837899969176820913e-03 4.971917106999092055e-03
+2.632653061224490276e-01 1.324416138142683191e-02 4.509383362223045449e-03 2.561524157497386753e-02 3.639102757811946837e-03 3.334547108629661721e-02 6.389725451675682712e-03 3.996382053409509028e-01 4.755273215299854472e-02 3.271252872232493503e-01 1.065073119380046708e-02 6.881045284550147967e-01 1.155866526993589057e-02 1.859721874914666007e-02 8.246200233160297030e-03 4.111820182104665954e-03 4.998624135573791899e-03
+2.795918367346938993e-01 1.225172070807768916e-02 5.634363331159562317e-03 2.208906558513888674e-02 3.630724365966011072e-03 2.572917981391273021e-02 6.174116090313300567e-03 3.411444192178936241e-01 4.570775803372454510e-02 3.102239205264454847e-01 1.025899698482179469e-02 6.714215205856344237e-01 1.105528830905571497e-02 1.529095779162716572e-02 7.193589407121427949e-03 4.361592170167226737e-03 5.058001219672858366e-03
+2.959183673469387710e-01 1.124413457635646540e-02 6.411421240065154868e-03 1.853378533508788159e-02 3.323430862555169698e-03 1.979955870424673567e-02 6.280524682915253863e-03 2.941440729953202493e-01 5.179924323811337800e-02 2.924400641576166415e-01 9.842133598727825436e-03 6.526424526523527669e-01 1.089458089702714447e-02 1.223344377289717171e-02 6.144280702266012063e-03 4.677260234324302941e-03 4.992584070232586671e-03
+3.122448979591836982e-01 1.079021243743692043e-02 6.632527583782697571e-03 1.517791038369432736e-02 3.473349017361928009e-03 1.449152434907221426e-02 6.331541371998266818e-03 2.601290037822677204e-01 4.622670247373922986e-02 2.746771419636072631e-01 1.037543665308320734e-02 6.316226608424150291e-01 1.077018659836364786e-02 9.472423507792282829e-03 5.434167994449804785e-03 4.969434566622568314e-03 5.018129402137327423e-03
+3.285714285714286254e-01 1.010737878689214712e-02 6.779166903760069050e-03 1.355770075092034913e-02 5.142614075637582348e-03 8.989087468730809147e-03 7.595376940382754342e-03 2.302641998343407104e-01 4.244869960571524825e-02 2.579208516735705392e-01 1.132196327225507872e-02 6.077500186539003257e-01 1.116952747323463405e-02 7.527646548402747509e-03 4.690624602008485605e-03 4.972232371569194724e-03 4.961915159579504894e-03
+3.448979591836734970e-01 8.811830531947489192e-03 6.611065661575015427e-03 1.094030944405336402e-02 3.876749683563606840e-03 6.216513476467545873e-03 5.946630908343609155e-03 2.048947603867307399e-01 3.692175416096520058e-02 2.392713041855997202e-01 9.706181968344242822e-03 5.836326619162366391e-01 1.101364075145081986e-02 6.513274540608479310e-03 3.949206138181179417e-03 4.986885046894676439e-03 4.867251829657326737e-03
+3.612244897959183687e-01 6.825401095665947424e-03 6.015622156291653214e-03 8.592487438390646048e-03 4.986458385358475201e-03 4.643358758758497212e-03 7.666704564216820714e-03 1.820567154916601094e-01 3.491710247794299615e-02 2.200942895644345532e-01 1.088058989779359248e-02 5.581722686890733254e-01 1.089771491601521959e-02 6.324699667870168490e-03 3.825037709656281537e-03 5.194980618384773159e-03 4.671733073712271492e-03
+3.775510204081632404e-01 4.622927028232834239e-03 5.555000630649990605e-03 7.006103884160290625e-03 4.558050434345648980e-03 4.026478403530274301e-03 8.802408761178621283e-03 1.654109241587360668e-01 2.958093259719593235e-02 2.015352169703905072e-01 1.147618085254958030e-02 5.309647906816499230e-01 1.029080153886721520e-02 6.653667513086839770e-03 5.290996518088139297e-03 5.112247513943503525e-03 4.533700952241386320e-03
+3.938775510204082231e-01 3.224999414965747189e-03 5.629233313847112341e-03 5.636815763899808741e-03 5.167788915004078130e-03 4.184595912678558752e-03 1.027649072980537683e-02 1.480349508988084706e-01 2.827395372375652097e-02 1.834778690223543285e-01 1.243506896536910114e-02 5.026172192427382779e-01 9.833385791748083804e-03 6.681477882536569768e-03 6.106976428266843067e-03 4.843877522511543486e-03 4.354338837886682044e-03
+4.102040816326530948e-01 2.398997430438228398e-03 5.497597477770171416e-03 5.104961177614576566e-03 3.623114640176146664e-03 3.847370634377959581e-03 9.857259957830802680e-03 1.325798271673375717e-01 2.828630531659322850e-02 1.665215033712220405e-01 1.196507375293896780e-02 4.729472143835790066e-01 8.930840437713421717e-03 6.632437655444375565e-03 5.646533706492752776e-03 4.581162311501288040e-03 4.183485454464925569e-03
+4.265306122448979664e-01 1.692190277964546229e-03 5.335104021406694361e-03 4.283378024890269545e-03 4.788897464910948573e-03 3.545439000884104044e-03 1.092082872314948001e-02 1.182493409930097245e-01 2.916021507299379825e-02 1.500708105276261917e-01 1.252551497739448627e-02 4.424769600331791564e-01 9.367904277973288299e-03 7.260241880531962191e-03 5.627655291587668578e-03 4.575902333115733529e-03 4.080267829835968439e-03
+4.428571428571428381e-01 4.148649701266470154e-04 5.113805095634805543e-03 2.771418381560752930e-03 6.357959201349411803e-03 4.286382242885526835e-03 1.387159887015452196e-02 1.066391781871470490e-01 2.847724416602226416e-02 1.335042960430444514e-01 1.520917078076000975e-02 4.126698332598315866e-01 1.059838981925867105e-02 8.237569708429254636e-03 6.039312886020845907e-03 4.699505512860680662e-03 4.310263817980366542e-03
+4.591836734693878208e-01 -1.304228855688523885e-03 5.470247749666199907e-03 4.239601722136225777e-03 5.808362884320261189e-03 2.590113200612944883e-03 1.462556815161407398e-02 1.011350726109566528e-01 3.463682731939739246e-02 1.207215482096496983e-01 1.590959814892967464e-02 3.809692818739471298e-01 1.056008118753932676e-02 9.256739924858644469e-03 6.591637446377524370e-03 4.411877109285056511e-03 4.028341424745536872e-03
+4.755102040816326925e-01 -2.704550331251799004e-03 6.117853355337987686e-03 4.144939763097098794e-03 6.157426353526132545e-03 2.280409685916162145e-03 1.495259847782390815e-02 9.174674898459940497e-02 3.317719061506606976e-02 1.080260917482173538e-01 1.553631936069132559e-02 3.512845707804219120e-01 1.082288870256591616e-02 9.983724737793020498e-03 7.020314509780686135e-03 3.912002421612906407e-03 3.990957946643638315e-03
+4.918367346938775642e-01 -3.535521323528168034e-03 6.489852248896367873e-03 2.599970243847376378e-03 5.548935444868298081e-03 2.808998904941583091e-03 1.473662895226150044e-02 8.739403248853304385e-02 3.458909252220616903e-02 9.472548541323846938e-02 1.519063342865388695e-02 3.239722561384902888e-01 1.024387162143642650e-02 1.052513329328826422e-02 7.082609771856901437e-03 3.508374796053288693e-03 3.946269400029071145e-03
+5.081632653061224358e-01 -3.773172936203268539e-03 6.637549040490199267e-03 -2.516103480873571000e-04 6.572923314060078771e-03 3.953622982447484863e-03 1.573471739266091296e-02 7.986314864670505942e-02 3.453373395965902981e-02 8.094667899419712154e-02 1.582852538281763824e-02 2.987031131287182784e-01 1.045563517738316676e-02 1.103534588189305880e-02 6.889468087760076270e-03 3.299279920697378594e-03 3.769594688649386023e-03
+5.244897959183674185e-01 -3.936045802592954765e-03 6.697487600261068513e-03 -1.639720875129115768e-03 6.037240612839354058e-03 3.407874012965536603e-03 1.553999947376006815e-02 7.216510147179601431e-02 2.968070892373265643e-02 6.941026401027031167e-02 1.521944184007802646e-02 2.729564461704206968e-01 1.074585510624960147e-02 1.101060982716685691e-02 6.786395115007838943e-03 3.450148591760391226e-03 3.710058644842988921e-03
+5.408163265306122902e-01 -4.125728968283484156e-03 6.412024156289482767e-03 -2.116395666802422821e-03 6.043906253157798475e-03 2.404454447424684421e-03 1.530205768622161684e-02 6.645926254942295663e-02 2.713087028010289364e-02 5.930041595975332064e-02 1.503097080754000302e-02 2.480024241700939380e-01 1.119419072927162716e-02 1.054112334574236129e-02 6.634480714590747577e-03 3.549779478918272963e-03 3.698483399590634692e-03
+5.571428571428571619e-01 -4.359718195743628177e-03 5.818326311771436325e-03 -3.097840758134474178e-03 5.749631410236337795e-03 2.644218077709412518e-03 1.513240561439272575e-02 5.877459162780401120e-02 2.555514304330760988e-02 4.907678969758672710e-02 1.525354296439180614e-02 2.252591619506083553e-01 1.056903482710983173e-02 1.017555195366732883e-02 6.611416243887415639e-03 3.395360677725176805e-03 3.549039373020256537e-03
+5.734693877551020336e-01 -3.920870475797691088e-03 5.568310082063020368e-03 -3.577983923135422562e-03 6.279601865306166236e-03 2.625291945255302500e-03 1.492559749435511118e-02 5.338086792246633794e-02 2.502004244557643961e-02 4.086042706797585189e-02 1.521854809216812571e-02 2.033308396707086130e-01 9.862655545891154882e-03 9.557252016347817652e-03 6.754595096107933634e-03 2.692149192139742069e-03 3.236751522896792223e-03
+5.897959183673470163e-01 -3.129001712471175985e-03 5.478478115272785408e-03 -2.507962595204027423e-03 8.400804333261060672e-03 1.286110393801345947e-03 1.461667280101169863e-02 4.876099631660012895e-02 2.707226383643828588e-02 3.536451685924833083e-02 1.541250960967785640e-02 1.810233365604385358e-01 1.092837393135298769e-02 8.796965920370821562e-03 6.701452952848836395e-03 1.834975916214879547e-03 3.320370979116342262e-03
+6.061224489795918879e-01 -2.416380765101757214e-03 5.519673432110980792e-03 -2.595344859342544035e-03 8.865376650415127471e-03 1.138349742385753871e-03 1.365996083221193529e-02 4.207712352396885541e-02 2.305135101626111169e-02 2.937774651292302322e-02 1.492224573979315447e-02 1.609589521154481395e-01 1.120069476152043240e-02 7.858155818736972664e-03 6.622386339097939036e-03 1.434505641425406911e-03 3.229120908684111283e-03
+6.224489795918367596e-01 -1.926066507583772255e-03 5.379437327106514552e-03 -2.318205541359436277e-03 9.966182175956909278e-03 5.681395412635522647e-04 1.277943878136280839e-02 3.580485555936888109e-02 2.185199910362296480e-02 2.483769891597548476e-02 1.420870934691557778e-02 1.415895843814952704e-01 1.196303501419522346e-02 6.993280944422240221e-03 6.498694781547665299e-03 1.255560850425938837e-03 3.015839887985304404e-03
+6.387755102040816313e-01 -1.763291018474010029e-03 5.011026558907828027e-03 -3.517692703172981862e-03 1.049362282662101922e-02 1.412161992931055187e-03 1.261028606768314214e-02 3.021147633166515814e-02 2.208234850326066048e-02 1.964669379999844923e-02 1.375236258345650942e-02 1.247038505112701839e-01 1.174567493327440676e-02 6.104164691998323691e-03 6.336567227702525652e-03 1.511434556359628157e-03 2.918041857848065277e-03
+6.551020408163265030e-01 -1.371265663515872871e-03 4.815576566171758145e-03 -3.924797227226901192e-03 1.042963482885535947e-02 2.146702536331560659e-03 1.226619336219402986e-02 2.484776981462179582e-02 2.198756275659718984e-02 1.600648495888330053e-02 1.352795817211750219e-02 1.085126989031491018e-01 1.129038558040623003e-02 5.124397578212483255e-03 5.955471233002406807e-03 1.375912011189511308e-03 2.643676721640121109e-03
+6.714285714285714857e-01 -7.347387558144375915e-04 5.047944037270904634e-03 -4.551858755871746043e-03 1.084756265045347563e-02 3.133211172357025683e-03 1.289036313686338538e-02 1.988113039811458330e-02 2.200991807516783014e-02 1.371824458342043099e-02 1.417405976351782983e-02 9.332242088573453054e-02 1.158142679674851495e-02 4.165764440781732046e-03 5.813900741335659844e-03 1.063345719439776462e-03 2.353021475347306501e-03
+6.877551020408163573e-01 -5.653365196498312453e-04 5.556716834071377675e-03 -5.735036617494569146e-03 9.716728775460639275e-03 4.848935490069965042e-03 1.148048995812154215e-02 1.400636390703255754e-02 2.365106623106593212e-02 1.137234890484222112e-02 1.299612576350017527e-02 8.010839740159633737e-02 1.022623955221433310e-02 3.825667716712735211e-03 6.199468939947366981e-03 6.893171889063837545e-04 2.151276015229100676e-03
+7.040816326530612290e-01 -2.267332661212563918e-04 6.119802600425810341e-03 -6.257262548063195068e-03 1.014102716994294030e-02 5.921973125223791920e-03 1.125687254220730966e-02 1.145974167737420432e-02 1.887010255106317397e-02 9.773630232330304465e-03 1.279353067078748546e-02 6.807985569865412923e-02 1.038097601483088488e-02 3.454995932446520372e-03 6.744031583403861836e-03 2.400689127714897590e-04 2.094057650527441673e-03
+7.204081632653062117e-01 1.026320665191617987e-03 7.680441173733227114e-03 -6.142834477869558822e-03 1.066679325465065900e-02 6.583132456638965575e-03 1.175729386615498133e-02 7.837588375768960586e-03 1.719052997285148640e-02 7.501140602779903140e-03 1.282082811316204857e-02 5.772865008052108921e-02 1.071429636674453914e-02 2.259823363842622911e-03 8.315739003339289726e-03 -1.202600787526237567e-05 2.053675732589019861e-03
+7.367346938775510834e-01 2.723773866630701743e-03 1.055738113017332461e-02 -5.354367355641570514e-03 1.216566243602633379e-02 5.902165687426506359e-03 1.268637380600316431e-02 5.019045671291559423e-03 1.703959974523763451e-02 6.386503919608849111e-03 1.367938696443783166e-02 4.815571853068702224e-02 1.220396766522050848e-02 6.314853168272392377e-04 1.119384842486024524e-02 -1.513013065019109627e-04 1.912085561785451492e-03
+7.530612244897959551e-01 3.347111420314353037e-03 1.138156833685597703e-02 -4.491012694336645426e-03 1.270622481303653774e-02 5.588513599931386501e-03 1.195055443286740030e-02 3.380609462740184409e-03 1.479054695636173103e-02 5.150940857924307670e-03 1.260427596906943566e-02 3.941675457359115203e-02 1.293771834964407147e-02 9.440130082234371250e-05 1.213155144191191588e-02 -2.055206984531875094e-04 1.854542395242731595e-03
+7.693877551020408267e-01 3.700903966783544458e-03 1.181247737394512318e-02 -5.013588714372360991e-03 1.144484866240926858e-02 5.721807343038266824e-03 1.158017304292606117e-02 9.324618436404591567e-05 1.621917485475198040e-02 3.601692683394015077e-03 1.197341369650416383e-02 3.326630455306706785e-02 1.201717042389163770e-02 -3.686189977476679873e-04 1.260123117591606043e-02 -2.329992136797600396e-04 1.812885362980177790e-03
+7.857142857142856984e-01 3.950074419242127592e-03 1.291688207383976522e-02 -5.693798006173376829e-03 1.073626677616170040e-02 5.657948365304832708e-03 1.177402930489857696e-02 -1.538774680937169846e-03 1.427911879458286688e-02 2.210276236893728759e-03 1.205791082848095379e-02 2.854265247624555857e-02 1.152778484090345723e-02 -9.862004915652249504e-04 1.365975660245233141e-02 -2.027384854668734027e-04 1.637337392279653199e-03
+8.020408163265306811e-01 3.898205963925653779e-03 1.235501536786283439e-02 -4.305580344407663670e-03 9.784254832731606802e-03 4.803772217549897791e-03 1.044511582683799698e-02 -3.292689738769052563e-03 1.302317972988607450e-02 1.559251545478106691e-03 1.051003276106219318e-02 2.272068462983138637e-02 1.039916298616219877e-02 -1.284235245889934389e-03 1.311586386815231584e-02 -1.425192807261697631e-04 1.533165760304424849e-03
+8.183673469387755528e-01 3.588619797058547126e-03 1.137225324329994153e-02 -3.492723870721761714e-03 9.000290850226813633e-03 4.198696164673327083e-03 9.731260435807975034e-03 -4.768501625455401523e-03 1.556994705567193819e-02 8.465323511627092547e-04 9.696491188922193652e-03 1.825700122372798245e-02 9.575460541838392778e-03 -1.342621206227992588e-03 1.204775011225409434e-02 -1.138655908763324358e-04 1.448744408941626295e-03
+8.346938775510204245e-01 3.441281250341821953e-03 1.017984496958066962e-02 -3.041726663775672686e-03 8.010487218373465307e-03 3.556407472593775022e-03 8.593753259920606721e-03 -5.230190570117304073e-03 1.414505616898533748e-02 3.378756927483065947e-04 8.721503560001611297e-03 1.483899533506974518e-02 8.458777665579302399e-03 -1.553847558480263465e-03 1.070251811142967913e-02 -1.259014983128754327e-04 1.219932345173984842e-03
+8.510204081632654072e-01 3.210758257312623220e-03 9.544603477465968266e-03 -2.700052505113971144e-03 7.163601897606069677e-03 3.003263435210558937e-03 7.739678550482364389e-03 -5.504081810805650996e-03 1.310886165854219070e-02 4.341769438769502301e-05 7.867103569669024049e-03 1.184481233652320484e-02 7.668364582451015800e-03 -1.641010957639634947e-03 9.910946594650287683e-03 -1.269871109435181890e-04 1.005675751561662876e-03
+8.673469387755102789e-01 2.754430250123140935e-03 8.728939656345335515e-03 -2.194652043508042282e-03 6.162234962480548327e-03 2.365811147249049975e-03 6.742651502665567703e-03 -5.565775093366548236e-03 1.283554913783929025e-02 -3.583154109628419639e-05 6.812644339057771087e-03 9.122462586070438292e-03 6.634621311653081215e-03 -1.499827068684795933e-03 9.068969561710620367e-03 -1.141045310238823546e-04 8.086130827908307415e-04
+8.836734693877551505e-01 2.456910492806845328e-03 8.514967898016780751e-03 -2.151372510115513118e-03 5.118589556761047633e-03 1.872728731270310734e-03 5.849475693755866804e-03 -5.301125605810848816e-03 1.208554103662637380e-02 -1.249919176342527058e-04 5.929764783898339164e-03 7.202425593584829006e-03 5.472872084153720604e-03 -1.503502693659317579e-03 8.838236264176898421e-03 -8.122948150597573426e-05 6.464492371260225972e-04
+9.000000000000000222e-01 2.302187455270835164e-03 8.739502279132595197e-03 -2.384168039911821121e-03 5.326226440830149171e-03 1.337487979259191582e-03 6.010268189772444321e-03 -4.864826018318149127e-03 1.070561871116822161e-02 -1.930248019052008528e-04 6.042746812240025120e-03 6.006656214330170773e-03 5.044743278601566266e-03 -1.651540834005031004e-03 9.021929051782274361e-03 -2.604869388541081849e-05 6.175356181904082626e-04
diff --git a/examples/qPDF/data/partial/c+.dat b/examples/qPDF/data/partial/c+.dat
new file mode 100644
index 000000000..b6f4b1509
--- /dev/null
+++ b/examples/qPDF/data/partial/c+.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 9.543745599019025317e-04 8.933750230847316287e-02
+1.151395399326446927e-04 3.636680441961381328e-03 7.412584225106703950e-02
+1.325711365590109543e-04 4.933524249824835194e-03 6.407245802265681967e-02
+1.526417967175233259e-04 5.655341068586110242e-03 5.779131852221603555e-02
+1.757510624854791170e-04 5.894428576734326262e-03 5.400134138166334807e-02
+2.023589647725157589e-04 6.105220955743916912e-03 4.880768069883391347e-02
+2.329951810515371805e-04 6.235027687244114672e-03 4.349911980376581361e-02
+2.682695795279724476e-04 6.268431070720082224e-03 3.967181908221596026e-02
+3.088843596477481527e-04 6.217020373943566401e-03 3.686609060461188553e-02
+3.556480306223128661e-04 6.047944115574644883e-03 3.465683609043487129e-02
+4.094915062380427508e-04 6.086277759868033259e-03 3.255440885733468931e-02
+4.714866363457394672e-04 6.353607011376051841e-03 3.045711151395184141e-02
+5.428675439323859403e-04 6.428561308584479321e-03 2.867200099971633012e-02
+6.250551925273975734e-04 6.372404207167801926e-03 2.704047344363377792e-02
+7.196856730011521675e-04 6.258801326906005338e-03 2.571516118777497986e-02
+8.286427728546842068e-04 6.047430101503004259e-03 2.473241740672309924e-02
+9.540954763499943534e-04 5.727973869072462351e-03 2.400751060148542324e-02
+1.098541141987558400e-03 5.217025855255588707e-03 2.331562484684581577e-02
+1.264855216855295715e-03 4.520400786895952851e-03 2.287525171199299504e-02
+1.456348477501244378e-03 4.104105861430898221e-03 2.251634616463276534e-02
+1.676832936811008387e-03 3.888985899011146528e-03 2.211418539704611622e-02
+1.930697728883249645e-03 3.777269112706165780e-03 2.186241065726403712e-02
+2.222996482526195736e-03 3.716552809729335802e-03 2.186462296014471396e-02
+2.559547922699535825e-03 3.668101774664294951e-03 2.202517508457559753e-02
+2.947051702551809708e-03 3.547940999186200654e-03 2.200550651002718128e-02
+3.393221771895329857e-03 3.334206195109535448e-03 2.177519424557845459e-02
+3.906939937054616958e-03 2.882653201686987635e-03 2.111194539755851737e-02
+4.498432668969444201e-03 2.266223600348241541e-03 2.030995470319845245e-02
+5.179474679231212825e-03 1.650199402710971321e-03 1.966948473734408259e-02
+5.963623316594642357e-03 1.045156606308592952e-03 1.936423761741078858e-02
+6.866488450042998112e-03 4.393240182844387388e-04 1.955534016925687679e-02
+7.906043210907700777e-03 1.293074295599358806e-04 1.970672652804380393e-02
+9.102981779915217050e-03 1.761382065781277049e-04 1.994970249637265797e-02
+1.048113134154685273e-02 6.342964865634353891e-04 2.000374435605717371e-02
+1.206792640639328847e-02 1.159859179318486122e-03 2.010314301162413955e-02
+1.389495494373137359e-02 6.229645941732639153e-04 1.893838285154697196e-02
+1.599858719606057217e-02 -4.528610396075364209e-04 1.816499523496944643e-02
+1.842069969326716461e-02 -2.040862806432878866e-03 1.837584791020675096e-02
+2.120950887920190417e-02 -2.764948712256495682e-03 1.852305431254810605e-02
+2.442053094548649744e-02 -2.301153740602655817e-03 1.806891834668321664e-02
+2.811768697974230749e-02 -2.065953710478731550e-03 1.794252940578616382e-02
+3.237457542817643447e-02 -1.879599560993519027e-03 1.738587176486946009e-02
+3.727593720314938130e-02 -1.281647868885260524e-03 1.660475291361803579e-02
+4.291934260128778267e-02 -4.063840395447936319e-04 1.568866817153680149e-02
+4.941713361323833015e-02 8.499527604997306370e-04 1.472870517349008160e-02
+5.689866029018292998e-02 1.635985800273112058e-03 1.478982758405596870e-02
+6.551285568595509312e-02 1.735828455516659138e-03 1.592381199680020440e-02
+7.543120063354614990e-02 9.190323300996633893e-04 1.504578981460611906e-02
+8.685113737513520948e-02 -5.170461293543338402e-05 1.330416744770090282e-02
+1.000000000000000056e-01 2.390608399999999932e-03 1.207505639463098135e-02
diff --git a/examples/qPDF/data/partial/c.dat b/examples/qPDF/data/partial/c.dat
new file mode 100644
index 000000000..f38cd191f
--- /dev/null
+++ b/examples/qPDF/data/partial/c.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 1.195304199999999966e-03 6.037528197315490675e-03
+1.163265306122449050e-01 1.010899534971207306e-03 5.145507231446976720e-03
+1.326530612244898044e-01 1.097352653894629422e-03 5.055917088070817182e-03
+1.489795918367347038e-01 1.678606730492365837e-03 5.179296401225694929e-03
+1.653061224489796033e-01 2.284077980409629591e-03 5.150763234594859206e-03
+1.816326530612245027e-01 2.610410238272599208e-03 4.943903025434586221e-03
+1.979591836734694021e-01 2.967414949183228793e-03 4.904595754975780986e-03
+2.142857142857143016e-01 3.265868387940330363e-03 4.908035171917084563e-03
+2.306122448979592010e-01 3.536738512125343209e-03 4.936859069713762579e-03
+2.469387755102041004e-01 3.837899969176820913e-03 4.971917106999092055e-03
+2.632653061224490276e-01 4.111820182104665954e-03 4.998624135573791899e-03
+2.795918367346938993e-01 4.361592170167226737e-03 5.058001219672858366e-03
+2.959183673469387710e-01 4.677260234324302941e-03 4.992584070232586671e-03
+3.122448979591836982e-01 4.969434566622568314e-03 5.018129402137327423e-03
+3.285714285714286254e-01 4.972232371569194724e-03 4.961915159579504894e-03
+3.448979591836734970e-01 4.986885046894676439e-03 4.867251829657326737e-03
+3.612244897959183687e-01 5.194980618384773159e-03 4.671733073712271492e-03
+3.775510204081632404e-01 5.112247513943503525e-03 4.533700952241386320e-03
+3.938775510204082231e-01 4.843877522511543486e-03 4.354338837886682044e-03
+4.102040816326530948e-01 4.581162311501288040e-03 4.183485454464925569e-03
+4.265306122448979664e-01 4.575902333115733529e-03 4.080267829835968439e-03
+4.428571428571428381e-01 4.699505512860680662e-03 4.310263817980366542e-03
+4.591836734693878208e-01 4.411877109285056511e-03 4.028341424745536872e-03
+4.755102040816326925e-01 3.912002421612906407e-03 3.990957946643638315e-03
+4.918367346938775642e-01 3.508374796053288693e-03 3.946269400029071145e-03
+5.081632653061224358e-01 3.299279920697378594e-03 3.769594688649386023e-03
+5.244897959183674185e-01 3.450148591760391226e-03 3.710058644842988921e-03
+5.408163265306122902e-01 3.549779478918272963e-03 3.698483399590634692e-03
+5.571428571428571619e-01 3.395360677725176805e-03 3.549039373020256537e-03
+5.734693877551020336e-01 2.692149192139742069e-03 3.236751522896792223e-03
+5.897959183673470163e-01 1.834975916214879547e-03 3.320370979116342262e-03
+6.061224489795918879e-01 1.434505641425406911e-03 3.229120908684111283e-03
+6.224489795918367596e-01 1.255560850425938837e-03 3.015839887985304404e-03
+6.387755102040816313e-01 1.511434556359628157e-03 2.918041857848065277e-03
+6.551020408163265030e-01 1.375912011189511308e-03 2.643676721640121109e-03
+6.714285714285714857e-01 1.063345719439776462e-03 2.353021475347306501e-03
+6.877551020408163573e-01 6.893171889063837545e-04 2.151276015229100676e-03
+7.040816326530612290e-01 2.400689127714897590e-04 2.094057650527441673e-03
+7.204081632653062117e-01 -1.202600787526237567e-05 2.053675732589019861e-03
+7.367346938775510834e-01 -1.513013065019109627e-04 1.912085561785451492e-03
+7.530612244897959551e-01 -2.055206984531875094e-04 1.854542395242731595e-03
+7.693877551020408267e-01 -2.329992136797600396e-04 1.812885362980177790e-03
+7.857142857142856984e-01 -2.027384854668734027e-04 1.637337392279653199e-03
+8.020408163265306811e-01 -1.425192807261697631e-04 1.533165760304424849e-03
+8.183673469387755528e-01 -1.138655908763324358e-04 1.448744408941626295e-03
+8.346938775510204245e-01 -1.259014983128754327e-04 1.219932345173984842e-03
+8.510204081632654072e-01 -1.269871109435181890e-04 1.005675751561662876e-03
+8.673469387755102789e-01 -1.141045310238823546e-04 8.086130827908307415e-04
+8.836734693877551505e-01 -8.122948150597573426e-05 6.464492371260225972e-04
+9.000000000000000222e-01 -2.604869388541081849e-05 6.175356181904082626e-04
diff --git a/examples/qPDF/data/partial/d.dat b/examples/qPDF/data/partial/d.dat
new file mode 100644
index 000000000..c5973a044
--- /dev/null
+++ b/examples/qPDF/data/partial/d.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 4.333629500000000245e-01 1.142205927296057952e-02
+1.163265306122449050e-01 4.319854048377612665e-01 1.186810995140672592e-02
+1.326530612244898044e-01 4.282018388324707603e-01 1.185308202596499320e-02
+1.489795918367347038e-01 4.218097916223478294e-01 1.164441685487899864e-02
+1.653061224489796033e-01 4.121920075337880407e-01 1.152898116787217167e-02
+1.816326530612245027e-01 4.011729322850488000e-01 1.199592104148921971e-02
+1.979591836734694021e-01 3.881920050023857649e-01 1.235297173645639665e-02
+2.142857142857143016e-01 3.746110031864225420e-01 1.231830164869357133e-02
+2.306122448979592010e-01 3.599051318315605874e-01 1.184557152524235400e-02
+2.469387755102041004e-01 3.440020547264790451e-01 1.121825444520186364e-02
+2.632653061224490276e-01 3.271252872232493503e-01 1.065073119380046708e-02
+2.795918367346938993e-01 3.102239205264454847e-01 1.025899698482179469e-02
+2.959183673469387710e-01 2.924400641576166415e-01 9.842133598727825436e-03
+3.122448979591836982e-01 2.746771419636072631e-01 1.037543665308320734e-02
+3.285714285714286254e-01 2.579208516735705392e-01 1.132196327225507872e-02
+3.448979591836734970e-01 2.392713041855997202e-01 9.706181968344242822e-03
+3.612244897959183687e-01 2.200942895644345532e-01 1.088058989779359248e-02
+3.775510204081632404e-01 2.015352169703905072e-01 1.147618085254958030e-02
+3.938775510204082231e-01 1.834778690223543285e-01 1.243506896536910114e-02
+4.102040816326530948e-01 1.665215033712220405e-01 1.196507375293896780e-02
+4.265306122448979664e-01 1.500708105276261917e-01 1.252551497739448627e-02
+4.428571428571428381e-01 1.335042960430444514e-01 1.520917078076000975e-02
+4.591836734693878208e-01 1.207215482096496983e-01 1.590959814892967464e-02
+4.755102040816326925e-01 1.080260917482173538e-01 1.553631936069132559e-02
+4.918367346938775642e-01 9.472548541323846938e-02 1.519063342865388695e-02
+5.081632653061224358e-01 8.094667899419712154e-02 1.582852538281763824e-02
+5.244897959183674185e-01 6.941026401027031167e-02 1.521944184007802646e-02
+5.408163265306122902e-01 5.930041595975332064e-02 1.503097080754000302e-02
+5.571428571428571619e-01 4.907678969758672710e-02 1.525354296439180614e-02
+5.734693877551020336e-01 4.086042706797585189e-02 1.521854809216812571e-02
+5.897959183673470163e-01 3.536451685924833083e-02 1.541250960967785640e-02
+6.061224489795918879e-01 2.937774651292302322e-02 1.492224573979315447e-02
+6.224489795918367596e-01 2.483769891597548476e-02 1.420870934691557778e-02
+6.387755102040816313e-01 1.964669379999844923e-02 1.375236258345650942e-02
+6.551020408163265030e-01 1.600648495888330053e-02 1.352795817211750219e-02
+6.714285714285714857e-01 1.371824458342043099e-02 1.417405976351782983e-02
+6.877551020408163573e-01 1.137234890484222112e-02 1.299612576350017527e-02
+7.040816326530612290e-01 9.773630232330304465e-03 1.279353067078748546e-02
+7.204081632653062117e-01 7.501140602779903140e-03 1.282082811316204857e-02
+7.367346938775510834e-01 6.386503919608849111e-03 1.367938696443783166e-02
+7.530612244897959551e-01 5.150940857924307670e-03 1.260427596906943566e-02
+7.693877551020408267e-01 3.601692683394015077e-03 1.197341369650416383e-02
+7.857142857142856984e-01 2.210276236893728759e-03 1.205791082848095379e-02
+8.020408163265306811e-01 1.559251545478106691e-03 1.051003276106219318e-02
+8.183673469387755528e-01 8.465323511627092547e-04 9.696491188922193652e-03
+8.346938775510204245e-01 3.378756927483065947e-04 8.721503560001611297e-03
+8.510204081632654072e-01 4.341769438769502301e-05 7.867103569669024049e-03
+8.673469387755102789e-01 -3.583154109628419639e-05 6.812644339057771087e-03
+8.836734693877551505e-01 -1.249919176342527058e-04 5.929764783898339164e-03
+9.000000000000000222e-01 -1.930248019052008528e-04 6.042746812240025120e-03
diff --git a/examples/qPDF/data/partial/dbar.dat b/examples/qPDF/data/partial/dbar.dat
new file mode 100644
index 000000000..a5a492399
--- /dev/null
+++ b/examples/qPDF/data/partial/dbar.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 1.573932000000000109e-01 1.014231559246146948e-02
+1.163265306122449050e-01 1.433649249121687341e-01 8.481243317234056153e-03
+1.326530612244898044e-01 1.295400522962832646e-01 7.243514263008420107e-03
+1.489795918367347038e-01 1.152544742232293196e-01 6.067835425351424811e-03
+1.653061224489796033e-01 1.011855789151688834e-01 5.985629346896574957e-03
+1.816326530612245027e-01 8.715178587795263665e-02 6.048640095465555692e-03
+1.979591836734694021e-01 7.458677501164867607e-02 5.813092362005962271e-03
+2.142857142857143016e-01 6.249873547602553120e-02 5.934263570124668347e-03
+2.306122448979592010e-01 5.181022996671015568e-02 6.203143677695365475e-03
+2.469387755102041004e-01 4.197209834064773853e-02 6.324876696238434202e-03
+2.632653061224490276e-01 3.334547108629661721e-02 6.389725451675682712e-03
+2.795918367346938993e-01 2.572917981391273021e-02 6.174116090313300567e-03
+2.959183673469387710e-01 1.979955870424673567e-02 6.280524682915253863e-03
+3.122448979591836982e-01 1.449152434907221426e-02 6.331541371998266818e-03
+3.285714285714286254e-01 8.989087468730809147e-03 7.595376940382754342e-03
+3.448979591836734970e-01 6.216513476467545873e-03 5.946630908343609155e-03
+3.612244897959183687e-01 4.643358758758497212e-03 7.666704564216820714e-03
+3.775510204081632404e-01 4.026478403530274301e-03 8.802408761178621283e-03
+3.938775510204082231e-01 4.184595912678558752e-03 1.027649072980537683e-02
+4.102040816326530948e-01 3.847370634377959581e-03 9.857259957830802680e-03
+4.265306122448979664e-01 3.545439000884104044e-03 1.092082872314948001e-02
+4.428571428571428381e-01 4.286382242885526835e-03 1.387159887015452196e-02
+4.591836734693878208e-01 2.590113200612944883e-03 1.462556815161407398e-02
+4.755102040816326925e-01 2.280409685916162145e-03 1.495259847782390815e-02
+4.918367346938775642e-01 2.808998904941583091e-03 1.473662895226150044e-02
+5.081632653061224358e-01 3.953622982447484863e-03 1.573471739266091296e-02
+5.244897959183674185e-01 3.407874012965536603e-03 1.553999947376006815e-02
+5.408163265306122902e-01 2.404454447424684421e-03 1.530205768622161684e-02
+5.571428571428571619e-01 2.644218077709412518e-03 1.513240561439272575e-02
+5.734693877551020336e-01 2.625291945255302500e-03 1.492559749435511118e-02
+5.897959183673470163e-01 1.286110393801345947e-03 1.461667280101169863e-02
+6.061224489795918879e-01 1.138349742385753871e-03 1.365996083221193529e-02
+6.224489795918367596e-01 5.681395412635522647e-04 1.277943878136280839e-02
+6.387755102040816313e-01 1.412161992931055187e-03 1.261028606768314214e-02
+6.551020408163265030e-01 2.146702536331560659e-03 1.226619336219402986e-02
+6.714285714285714857e-01 3.133211172357025683e-03 1.289036313686338538e-02
+6.877551020408163573e-01 4.848935490069965042e-03 1.148048995812154215e-02
+7.040816326530612290e-01 5.921973125223791920e-03 1.125687254220730966e-02
+7.204081632653062117e-01 6.583132456638965575e-03 1.175729386615498133e-02
+7.367346938775510834e-01 5.902165687426506359e-03 1.268637380600316431e-02
+7.530612244897959551e-01 5.588513599931386501e-03 1.195055443286740030e-02
+7.693877551020408267e-01 5.721807343038266824e-03 1.158017304292606117e-02
+7.857142857142856984e-01 5.657948365304832708e-03 1.177402930489857696e-02
+8.020408163265306811e-01 4.803772217549897791e-03 1.044511582683799698e-02
+8.183673469387755528e-01 4.198696164673327083e-03 9.731260435807975034e-03
+8.346938775510204245e-01 3.556407472593775022e-03 8.593753259920606721e-03
+8.510204081632654072e-01 3.003263435210558937e-03 7.739678550482364389e-03
+8.673469387755102789e-01 2.365811147249049975e-03 6.742651502665567703e-03
+8.836734693877551505e-01 1.872728731270310734e-03 5.849475693755866804e-03
+9.000000000000000222e-01 1.337487979259191582e-03 6.010268189772444321e-03
diff --git a/examples/qPDF/data/partial/gluon.dat b/examples/qPDF/data/partial/gluon.dat
new file mode 100644
index 000000000..a37b35c1a
--- /dev/null
+++ b/examples/qPDF/data/partial/gluon.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 1.436073027852281747e+00 1.410829786450314760e+00
+1.151395399326446927e-04 1.364958626022871213e+00 1.322403944448146795e+00
+1.325711365590109543e-04 1.308744557998403835e+00 1.246856327363375705e+00
+1.526417967175233259e-04 1.266716265313918344e+00 1.181035820270940784e+00
+1.757510624854791170e-04 1.238632415751813820e+00 1.124100495485785611e+00
+2.023589647725157589e-04 1.223304323506445579e+00 1.082280689395135109e+00
+2.329951810515371805e-04 1.221403075342442968e+00 1.049932598530862737e+00
+2.682695795279724476e-04 1.234246661261282352e+00 1.018495848476023369e+00
+3.088843596477481527e-04 1.261118092261666934e+00 9.820269772796574870e-01
+3.556480306223128661e-04 1.301809107355178963e+00 9.380345387151233227e-01
+4.094915062380427508e-04 1.355955018492741582e+00 8.893711185159632882e-01
+4.714866363457394672e-04 1.423415028768741042e+00 8.375259875883908078e-01
+5.428675439323859403e-04 1.503558397812429392e+00 7.741246897837354979e-01
+6.250551925273975734e-04 1.594760490544748555e+00 7.053377733669865535e-01
+7.196856730011521675e-04 1.694368883930322145e+00 6.381482642538304217e-01
+8.286427728546842068e-04 1.797933073006341420e+00 5.770645929332395285e-01
+9.540954763499943534e-04 1.902288016649845925e+00 5.213202772176038780e-01
+1.098541141987558400e-03 2.004993449735332334e+00 4.718001852409516306e-01
+1.264855216855295715e-03 2.105514952847052701e+00 4.273401137522174764e-01
+1.456348477501244378e-03 2.204919381213297669e+00 3.857407874870817932e-01
+1.676832936811008387e-03 2.302838545246236102e+00 3.527675463927992805e-01
+1.930697728883249645e-03 2.401250317650158284e+00 3.292460342654914274e-01
+2.222996482526195736e-03 2.489413722921670402e+00 3.241005711624331820e-01
+2.559547922699535825e-03 2.558364682243282839e+00 3.028155002281870400e-01
+2.947051702551809708e-03 2.616224206212616199e+00 2.820170227437128752e-01
+3.393221771895329857e-03 2.664071453832157932e+00 2.591501164691561443e-01
+3.906939937054616958e-03 2.703361738998359520e+00 2.348943198277512467e-01
+4.498432668969444201e-03 2.734359509219411066e+00 2.093647295324295721e-01
+5.179474679231212825e-03 2.758211866006374358e+00 1.861954455819890830e-01
+5.963623316594642357e-03 2.774504985860168027e+00 1.666400687266494929e-01
+6.866488450042998112e-03 2.783256351062659117e+00 1.509792672160396587e-01
+7.906043210907700777e-03 2.783431601575428349e+00 1.393981853977233343e-01
+9.102981779915217050e-03 2.774862150044652420e+00 1.356247879444966997e-01
+1.048113134154685273e-02 2.760566329600861124e+00 1.342862511369074530e-01
+1.206792640639328847e-02 2.740601584470428342e+00 1.316902676015173357e-01
+1.389495494373137359e-02 2.715933591822299764e+00 1.253457936888159319e-01
+1.599858719606057217e-02 2.685154192466012724e+00 1.193572415535045228e-01
+1.842069969326716461e-02 2.647431992894222486e+00 1.132596856420066828e-01
+2.120950887920190417e-02 2.601477236862831166e+00 1.124669224631803938e-01
+2.442053094548649744e-02 2.546935186302070164e+00 1.199475922069823675e-01
+2.811768697974230749e-02 2.486491029640843564e+00 1.260482655459385648e-01
+3.237457542817643447e-02 2.421072694716483120e+00 1.342529033015319173e-01
+3.727593720314938130e-02 2.356120187280772438e+00 1.228918399554665974e-01
+4.291934260128778267e-02 2.283609481004460040e+00 1.135313148672207062e-01
+4.941713361323833015e-02 2.197861404801443985e+00 1.111765296966798489e-01
+5.689866029018292998e-02 2.094792655510467227e+00 1.086382056511527999e-01
+6.551285568595509312e-02 1.972943483478049043e+00 1.016059983620012930e-01
+7.543120063354614990e-02 1.828797090456429908e+00 8.228479680656052009e-02
+8.685113737513520948e-02 1.665052707183172664e+00 7.019163919958561204e-02
+1.000000000000000056e-01 1.486650900000000108e+00 6.906649307998630194e-02
diff --git a/examples/qPDF/data/partial/s.dat b/examples/qPDF/data/partial/s.dat
new file mode 100644
index 000000000..8a8a21814
--- /dev/null
+++ b/examples/qPDF/data/partial/s.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 5.811938900000000047e-02 1.135948457677850472e-02
+1.163265306122449050e-01 5.498144450814458628e-02 9.629446721598575870e-03
+1.326530612244898044e-01 5.135997732852693592e-02 9.225240289123470827e-03
+1.489795918367347038e-01 4.723050215813651143e-02 8.846513143861974710e-03
+1.653061224489796033e-01 4.311203853746148340e-02 8.441498819331736542e-03
+1.816326530612245027e-01 3.894380473376195945e-02 8.655830837138740047e-03
+1.979591836734694021e-01 3.486831510158874825e-02 8.877602058789275594e-03
+2.142857142857143016e-01 3.069355929448877585e-02 9.246090313310376119e-03
+2.306122448979592010e-01 2.650617824238633846e-02 9.284838420715521567e-03
+2.469387755102041004e-01 2.240256390271565257e-02 8.949419548762920645e-03
+2.632653061224490276e-01 1.859721874914666007e-02 8.246200233160297030e-03
+2.795918367346938993e-01 1.529095779162716572e-02 7.193589407121427949e-03
+2.959183673469387710e-01 1.223344377289717171e-02 6.144280702266012063e-03
+3.122448979591836982e-01 9.472423507792282829e-03 5.434167994449804785e-03
+3.285714285714286254e-01 7.527646548402747509e-03 4.690624602008485605e-03
+3.448979591836734970e-01 6.513274540608479310e-03 3.949206138181179417e-03
+3.612244897959183687e-01 6.324699667870168490e-03 3.825037709656281537e-03
+3.775510204081632404e-01 6.653667513086839770e-03 5.290996518088139297e-03
+3.938775510204082231e-01 6.681477882536569768e-03 6.106976428266843067e-03
+4.102040816326530948e-01 6.632437655444375565e-03 5.646533706492752776e-03
+4.265306122448979664e-01 7.260241880531962191e-03 5.627655291587668578e-03
+4.428571428571428381e-01 8.237569708429254636e-03 6.039312886020845907e-03
+4.591836734693878208e-01 9.256739924858644469e-03 6.591637446377524370e-03
+4.755102040816326925e-01 9.983724737793020498e-03 7.020314509780686135e-03
+4.918367346938775642e-01 1.052513329328826422e-02 7.082609771856901437e-03
+5.081632653061224358e-01 1.103534588189305880e-02 6.889468087760076270e-03
+5.244897959183674185e-01 1.101060982716685691e-02 6.786395115007838943e-03
+5.408163265306122902e-01 1.054112334574236129e-02 6.634480714590747577e-03
+5.571428571428571619e-01 1.017555195366732883e-02 6.611416243887415639e-03
+5.734693877551020336e-01 9.557252016347817652e-03 6.754595096107933634e-03
+5.897959183673470163e-01 8.796965920370821562e-03 6.701452952848836395e-03
+6.061224489795918879e-01 7.858155818736972664e-03 6.622386339097939036e-03
+6.224489795918367596e-01 6.993280944422240221e-03 6.498694781547665299e-03
+6.387755102040816313e-01 6.104164691998323691e-03 6.336567227702525652e-03
+6.551020408163265030e-01 5.124397578212483255e-03 5.955471233002406807e-03
+6.714285714285714857e-01 4.165764440781732046e-03 5.813900741335659844e-03
+6.877551020408163573e-01 3.825667716712735211e-03 6.199468939947366981e-03
+7.040816326530612290e-01 3.454995932446520372e-03 6.744031583403861836e-03
+7.204081632653062117e-01 2.259823363842622911e-03 8.315739003339289726e-03
+7.367346938775510834e-01 6.314853168272392377e-04 1.119384842486024524e-02
+7.530612244897959551e-01 9.440130082234371250e-05 1.213155144191191588e-02
+7.693877551020408267e-01 -3.686189977476679873e-04 1.260123117591606043e-02
+7.857142857142856984e-01 -9.862004915652249504e-04 1.365975660245233141e-02
+8.020408163265306811e-01 -1.284235245889934389e-03 1.311586386815231584e-02
+8.183673469387755528e-01 -1.342621206227992588e-03 1.204775011225409434e-02
+8.346938775510204245e-01 -1.553847558480263465e-03 1.070251811142967913e-02
+8.510204081632654072e-01 -1.641010957639634947e-03 9.910946594650287683e-03
+8.673469387755102789e-01 -1.499827068684795933e-03 9.068969561710620367e-03
+8.836734693877551505e-01 -1.503502693659317579e-03 8.838236264176898421e-03
+9.000000000000000222e-01 -1.651540834005031004e-03 9.021929051782274361e-03
diff --git a/examples/qPDF/data/partial/sbar.dat b/examples/qPDF/data/partial/sbar.dat
new file mode 100644
index 000000000..9dad223b1
--- /dev/null
+++ b/examples/qPDF/data/partial/sbar.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 4.602117399999999803e-02 9.779521697310451789e-03
+1.163265306122449050e-01 3.942477395804350887e-02 8.597488598936026127e-03
+1.326530612244898044e-01 3.328912908047675090e-02 7.353330218090444077e-03
+1.489795918367347038e-01 2.863095571284317298e-02 6.665202107333106224e-03
+1.653061224489796033e-01 2.483575627569225078e-02 5.995471224548147050e-03
+1.816326530612245027e-01 2.170692908957032666e-02 5.655695672266800901e-03
+1.979591836734694021e-01 1.898480791532573819e-02 4.434780959100406011e-03
+2.142857142857143016e-01 1.695823321591360261e-02 3.469831512302461781e-03
+2.306122448979592010e-01 1.548757243205519177e-02 3.091673368091698692e-03
+2.469387755102041004e-01 1.433010380039563374e-02 3.566130690184089388e-03
+2.632653061224490276e-01 1.324416138142683191e-02 4.509383362223045449e-03
+2.795918367346938993e-01 1.225172070807768916e-02 5.634363331159562317e-03
+2.959183673469387710e-01 1.124413457635646540e-02 6.411421240065154868e-03
+3.122448979591836982e-01 1.079021243743692043e-02 6.632527583782697571e-03
+3.285714285714286254e-01 1.010737878689214712e-02 6.779166903760069050e-03
+3.448979591836734970e-01 8.811830531947489192e-03 6.611065661575015427e-03
+3.612244897959183687e-01 6.825401095665947424e-03 6.015622156291653214e-03
+3.775510204081632404e-01 4.622927028232834239e-03 5.555000630649990605e-03
+3.938775510204082231e-01 3.224999414965747189e-03 5.629233313847112341e-03
+4.102040816326530948e-01 2.398997430438228398e-03 5.497597477770171416e-03
+4.265306122448979664e-01 1.692190277964546229e-03 5.335104021406694361e-03
+4.428571428571428381e-01 4.148649701266470154e-04 5.113805095634805543e-03
+4.591836734693878208e-01 -1.304228855688523885e-03 5.470247749666199907e-03
+4.755102040816326925e-01 -2.704550331251799004e-03 6.117853355337987686e-03
+4.918367346938775642e-01 -3.535521323528168034e-03 6.489852248896367873e-03
+5.081632653061224358e-01 -3.773172936203268539e-03 6.637549040490199267e-03
+5.244897959183674185e-01 -3.936045802592954765e-03 6.697487600261068513e-03
+5.408163265306122902e-01 -4.125728968283484156e-03 6.412024156289482767e-03
+5.571428571428571619e-01 -4.359718195743628177e-03 5.818326311771436325e-03
+5.734693877551020336e-01 -3.920870475797691088e-03 5.568310082063020368e-03
+5.897959183673470163e-01 -3.129001712471175985e-03 5.478478115272785408e-03
+6.061224489795918879e-01 -2.416380765101757214e-03 5.519673432110980792e-03
+6.224489795918367596e-01 -1.926066507583772255e-03 5.379437327106514552e-03
+6.387755102040816313e-01 -1.763291018474010029e-03 5.011026558907828027e-03
+6.551020408163265030e-01 -1.371265663515872871e-03 4.815576566171758145e-03
+6.714285714285714857e-01 -7.347387558144375915e-04 5.047944037270904634e-03
+6.877551020408163573e-01 -5.653365196498312453e-04 5.556716834071377675e-03
+7.040816326530612290e-01 -2.267332661212563918e-04 6.119802600425810341e-03
+7.204081632653062117e-01 1.026320665191617987e-03 7.680441173733227114e-03
+7.367346938775510834e-01 2.723773866630701743e-03 1.055738113017332461e-02
+7.530612244897959551e-01 3.347111420314353037e-03 1.138156833685597703e-02
+7.693877551020408267e-01 3.700903966783544458e-03 1.181247737394512318e-02
+7.857142857142856984e-01 3.950074419242127592e-03 1.291688207383976522e-02
+8.020408163265306811e-01 3.898205963925653779e-03 1.235501536786283439e-02
+8.183673469387755528e-01 3.588619797058547126e-03 1.137225324329994153e-02
+8.346938775510204245e-01 3.441281250341821953e-03 1.017984496958066962e-02
+8.510204081632654072e-01 3.210758257312623220e-03 9.544603477465968266e-03
+8.673469387755102789e-01 2.754430250123140935e-03 8.728939656345335515e-03
+8.836734693877551505e-01 2.456910492806845328e-03 8.514967898016780751e-03
+9.000000000000000222e-01 2.302187455270835164e-03 8.739502279132595197e-03
diff --git a/examples/qPDF/data/partial/singlet.dat b/examples/qPDF/data/partial/singlet.dat
new file mode 100644
index 000000000..328533e4f
--- /dev/null
+++ b/examples/qPDF/data/partial/singlet.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 2.906051619342992609e+00 7.462119819026813428e-01
+1.151395399326446927e-04 2.860722503238625869e+00 6.988723163291101770e-01
+1.325711365590109543e-04 2.815405728981031697e+00 6.586280524367921219e-01
+1.526417967175233259e-04 2.770324102867665061e+00 6.244195241115695350e-01
+1.757510624854791170e-04 2.725517322797724074e+00 5.944487582232559353e-01
+2.023589647725157589e-04 2.681196838693734374e+00 5.641250926612668692e-01
+2.329951810515371805e-04 2.637384376323162360e+00 5.350509620256458376e-01
+2.682695795279724476e-04 2.594164003900461424e+00 5.091953271630039746e-01
+3.088843596477481527e-04 2.551560515699526466e+00 4.856781521167745641e-01
+3.556480306223128661e-04 2.509599140891932478e+00 4.637532165071666101e-01
+4.094915062380427508e-04 2.468315423913098439e+00 4.422539326639124968e-01
+4.714866363457394672e-04 2.427712402325765240e+00 4.210862448528221447e-01
+5.428675439323859403e-04 2.387743918408224975e+00 4.011382559420967331e-01
+6.250551925273975734e-04 2.348387102320955488e+00 3.820577280180124102e-01
+7.196856730011521675e-04 2.309572722275872181e+00 3.635766358785286201e-01
+8.286427728546842068e-04 2.271337215556608058e+00 3.459199706961187948e-01
+9.540954763499943534e-04 2.233683502841355306e+00 3.289489556794900627e-01
+1.098541141987558400e-03 2.196685734288134739e+00 3.130908018498047563e-01
+1.264855216855295715e-03 2.160359142346439221e+00 2.982326008119254857e-01
+1.456348477501244378e-03 2.124768531205242450e+00 2.835482339955216480e-01
+1.676832936811008387e-03 2.089886636778342410e+00 2.689103658428349530e-01
+1.930697728883249645e-03 2.055685659097348683e+00 2.544796379090471516e-01
+2.222996482526195736e-03 2.022064289211034094e+00 2.409054177577455991e-01
+2.559547922699535825e-03 1.988934505919132345e+00 2.278774653941089690e-01
+2.947051702551809708e-03 1.956332697014812272e+00 2.162392065064994151e-01
+3.393221771895329857e-03 1.924269027649759600e+00 2.058747170265479709e-01
+3.906939937054616958e-03 1.892982336078212091e+00 1.961808772479244345e-01
+4.498432668969444201e-03 1.862538821228059138e+00 1.875839665940923495e-01
+5.179474679231212825e-03 1.833140068419793378e+00 1.807254197101701987e-01
+5.963623316594642357e-03 1.804839151717607448e+00 1.756910118391523667e-01
+6.866488450042998112e-03 1.777705814340383661e+00 1.721635560558101297e-01
+7.906043210907700777e-03 1.751789858031411518e+00 1.685009977035207829e-01
+9.102981779915217050e-03 1.727107965448127613e+00 1.641839731575882588e-01
+1.048113134154685273e-02 1.703752463217196089e+00 1.606642201493900246e-01
+1.206792640639328847e-02 1.681741841466589227e+00 1.574115297323718798e-01
+1.389495494373137359e-02 1.661180872498956518e+00 1.506227976403611279e-01
+1.599858719606057217e-02 1.641936295425339809e+00 1.438243804779347768e-01
+1.842069969326716461e-02 1.623973998634635985e+00 1.377901334833385172e-01
+2.120950887920190417e-02 1.606952748332304948e+00 1.316949735372383856e-01
+2.442053094548649744e-02 1.590788822310530204e+00 1.243226889588570361e-01
+2.811768697974230749e-02 1.575168016170775287e+00 1.171832076468105194e-01
+3.237457542817643447e-02 1.560097899196016380e+00 1.101710796826544381e-01
+3.727593720314938130e-02 1.545542572083943966e+00 1.036093899612278157e-01
+4.291934260128778267e-02 1.531572602123358662e+00 9.728127487724623490e-02
+4.941713361323833015e-02 1.518341324472995302e+00 8.950734238211086047e-02
+5.689866029018292998e-02 1.505307307266029904e+00 8.810148335699352629e-02
+6.551285568595509312e-02 1.492338872126097726e+00 8.673219802223879060e-02
+7.543120063354614990e-02 1.479666262045960856e+00 8.141922715679962563e-02
+8.685113737513520948e-02 1.467067443862011933e+00 7.605632207591291050e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02
diff --git a/examples/qPDF/data/partial/t3.dat b/examples/qPDF/data/partial/t3.dat
new file mode 100644
index 000000000..4d035ef06
--- /dev/null
+++ b/examples/qPDF/data/partial/t3.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 2.178039037871642991e-02 -5.501273989189520153e-02
+1.151395399326446927e-04 2.360494800229695489e-02 -5.079617646067179082e-02
+1.325711365590109543e-04 2.614203360893901928e-02 -4.664551928313108442e-02
+1.526417967175233259e-04 2.828035986310167837e-02 -4.300222704272022023e-02
+1.757510624854791170e-04 2.991495798307713949e-02 -3.997435589083222357e-02
+2.023589647725157589e-04 3.126187635942873078e-02 -3.730603537237335565e-02
+2.329951810515371805e-04 3.235494070724337368e-02 -3.489195915358409006e-02
+2.682695795279724476e-04 3.330068004701974838e-02 -3.256661292586237461e-02
+3.088843596477481527e-04 3.405999944634308108e-02 -3.032459936431707720e-02
+3.556480306223128661e-04 3.462309337997493408e-02 -2.816141756988261441e-02
+4.094915062380427508e-04 3.499154103999518872e-02 -2.598801321550247428e-02
+4.714866363457394672e-04 3.517161650060213196e-02 -2.380730015382700321e-02
+5.428675439323859403e-04 3.526360047455268099e-02 -2.200751087594812055e-02
+6.250551925273975734e-04 3.527563526439142505e-02 -2.051531149610450605e-02
+7.196856730011521675e-04 3.524445607586629281e-02 -1.937188316215548295e-02
+8.286427728546842068e-04 3.517307215238224538e-02 -1.824551761460532773e-02
+9.540954763499943534e-04 3.506902486917523598e-02 -1.682295858235231117e-02
+1.098541141987558400e-03 3.494190035771210789e-02 -1.546271661467520792e-02
+1.264855216855295715e-03 3.479091040879289665e-02 -1.425412340907782419e-02
+1.456348477501244378e-03 3.452956214373470445e-02 -1.330042022227230156e-02
+1.676832936811008387e-03 3.422874677965193069e-02 -1.251282444079887882e-02
+1.930697728883249645e-03 3.407769421897388629e-02 -1.162179965122341410e-02
+2.222996482526195736e-03 3.395628867277111773e-02 -1.067351857293324935e-02
+2.559547922699535825e-03 3.379958717023096026e-02 -9.689595645125753665e-03
+2.947051702551809708e-03 3.363598642204607136e-02 -8.576650766480675703e-03
+3.393221771895329857e-03 3.347228843142929788e-02 -7.335712491089114995e-03
+3.906939937054616958e-03 3.346546911966319371e-02 -6.382514472699104408e-03
+4.498432668969444201e-03 3.363083117111598241e-02 -5.638327509948526384e-03
+5.179474679231212825e-03 3.404927746485519879e-02 -5.019592136273724747e-03
+5.963623316594642357e-03 3.466819563282846905e-02 -4.454395194833919713e-03
+6.866488450042998112e-03 3.547883554846648080e-02 -3.933647755103783505e-03
+7.906043210907700777e-03 3.636258386306334689e-02 -3.485432043680698061e-03
+9.102981779915217050e-03 3.729492238222775757e-02 -3.060382179671066599e-03
+1.048113134154685273e-02 3.829244166622192136e-02 -2.979236290565519252e-03
+1.206792640639328847e-02 3.950383879785190899e-02 -3.016866291674448475e-03
+1.389495494373137359e-02 4.140581276527505183e-02 -2.531192989213757599e-03
+1.599858719606057217e-02 4.382995926841448675e-02 -2.114565963955858385e-03
+1.842069969326716461e-02 4.667630562357144575e-02 -2.149637273613766442e-03
+2.120950887920190417e-02 5.016488550414333059e-02 -2.299801919725254912e-03
+2.442053094548649744e-02 5.434134892004915951e-02 -2.427710292972059397e-03
+2.811768697974230749e-02 5.932381167525677124e-02 -2.475451532886998457e-03
+3.237457542817643447e-02 6.517335407775615153e-02 -2.568332880654382311e-03
+3.727593720314938130e-02 7.198498858163970837e-02 -2.797588503884113412e-03
+4.291934260128778267e-02 8.018165078823452641e-02 -2.908308329307534440e-03
+4.941713361323833015e-02 9.018829008283218984e-02 -2.935030353911056641e-03
+5.689866029018292998e-02 1.019564162987246503e-01 -3.211644617809455982e-03
+6.551285568595509312e-02 1.155101083814724927e-01 -3.284794786471789338e-03
+7.543120063354614990e-02 1.303237510513822750e-01 -3.442816632195635704e-03
+8.685113737513520948e-02 1.465592044397492633e-01 -3.910815628155301857e-03
+1.000000000000000056e-01 1.654803099999999638e-01 -3.591407057756365248e-03
diff --git a/examples/qPDF/data/partial/t8.dat b/examples/qPDF/data/partial/t8.dat
new file mode 100644
index 000000000..bab4df2f4
--- /dev/null
+++ b/examples/qPDF/data/partial/t8.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 2.967891727365936649e-01 -5.351913518991966434e-01
+1.151395399326446927e-04 3.064897739743315253e-01 -5.228070936938105095e-01
+1.325711365590109543e-04 3.158794264026344445e-01 -5.090709612371046022e-01
+1.526417967175233259e-04 3.248707472090531789e-01 -4.941991141595850068e-01
+1.757510624854791170e-04 3.334714075036346870e-01 -4.785649375008670381e-01
+2.023589647725157589e-04 3.418424717767015730e-01 -4.624912367226028387e-01
+2.329951810515371805e-04 3.500848868970291017e-01 -4.458210154938854197e-01
+2.682695795279724476e-04 3.584611086702909066e-01 -4.283162187443461111e-01
+3.088843596477481527e-04 3.668684233087414759e-01 -4.105069411107938326e-01
+3.556480306223128661e-04 3.751541695227437190e-01 -3.927431014729541592e-01
+4.094915062380427508e-04 3.844380513026034452e-01 -3.753661775630095510e-01
+4.714866363457394672e-04 3.947170717208153823e-01 -3.586138022321409546e-01
+5.428675439323859403e-04 4.033746393990121915e-01 -3.417970480025358526e-01
+6.250551925273975734e-04 4.105281640533186005e-01 -3.249129270673927206e-01
+7.196856730011521675e-04 4.155169660940215559e-01 -3.074495584179327556e-01
+8.286427728546842068e-04 4.201191315065782028e-01 -2.900762778330903124e-01
+9.540954763499943534e-04 4.257318112086013739e-01 -2.732669519568579064e-01
+1.098541141987558400e-03 4.303743647380635640e-01 -2.570325180831006806e-01
+1.264855216855295715e-03 4.338180042770034550e-01 -2.413071011501616847e-01
+1.456348477501244378e-03 4.368136614232921433e-01 -2.261871807747988239e-01
+1.676832936811008387e-03 4.392256045319101743e-01 -2.114730824142506405e-01
+1.930697728883249645e-03 4.406562962078498513e-01 -1.967104472715614505e-01
+2.222996482526195736e-03 4.417490464017018725e-01 -1.821002756467193229e-01
+2.559547922699535825e-03 4.426500358456443296e-01 -1.675258441897166661e-01
+2.947051702551809708e-03 4.450971951998125009e-01 -1.538982966950086517e-01
+3.393221771895329857e-03 4.494439335672644198e-01 -1.412161338165656699e-01
+3.906939937054616958e-03 4.561658895047046025e-01 -1.292383936954070722e-01
+4.498432668969444201e-03 4.651977385458023750e-01 -1.180918926972806537e-01
+5.179474679231212825e-03 4.768204251285368955e-01 -1.083340128914820760e-01
+5.963623316594642357e-03 4.897136399974937881e-01 -1.001283229540880920e-01
+6.866488450042998112e-03 5.028157826833805633e-01 -9.246198104295999531e-02
+7.906043210907700777e-03 5.162903347157841161e-01 -8.530814794740892992e-02
+9.102981779915217050e-03 5.301498020885356821e-01 -7.836694087990382640e-02
+1.048113134154685273e-02 5.468735475869694884e-01 -7.269911721980590324e-02
+1.206792640639328847e-02 5.669838951047392328e-01 -6.756187116634179701e-02
+1.389495494373137359e-02 5.925765651394002687e-01 -6.127512509837636234e-02
+1.599858719606057217e-02 6.228247868785594665e-01 -5.433207365904305780e-02
+1.842069969326716461e-02 6.575990713816981525e-01 -4.618559625605917623e-02
+2.120950887920190417e-02 6.958361640459556074e-01 -3.991318766042432908e-02
+2.442053094548649744e-02 7.372735601027718966e-01 -3.546250788524608205e-02
+2.811768697974230749e-02 7.804951593565594425e-01 -3.015335398039294124e-02
+3.237457542817643447e-02 8.259908637776820495e-01 -2.470326180773166347e-02
+3.727593720314938130e-02 8.752730218901014769e-01 -2.141317462140131939e-02
+4.291934260128778267e-02 9.279416482263004307e-01 -1.821674977036366605e-02
+4.941713361323833015e-02 9.863701585885369383e-01 -1.249541199529381405e-02
+5.689866029018292998e-02 1.036754676663268926e+00 -1.089222945444151547e-02
+6.551285568595509312e-02 1.075629503163584744e+00 -6.736547651658684327e-03
+7.543120063354614990e-02 1.105939016479328574e+00 -3.621107408948803785e-03
+8.685113737513520948e-02 1.127803464297723046e+00 -2.163644186741874725e-03
+1.000000000000000056e-01 1.138711483999999885e+00 -2.740669875090178537e-03
diff --git a/examples/qPDF/data/partial/u.dat b/examples/qPDF/data/partial/u.dat
new file mode 100644
index 000000000..bdb953efc
--- /dev/null
+++ b/examples/qPDF/data/partial/u.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 6.450658200000000120e-01 9.793002637796645662e-03
+1.163265306122449050e-01 6.681633787721220630e-01 1.017643806531060534e-02
+1.326530612244898044e-01 6.874414050327382064e-01 1.051611966882545074e-02
+1.489795918367347038e-01 7.024060766836944802e-01 1.091152213637827662e-02
+1.653061224489796033e-01 7.139294147955075331e-01 1.133628869175182935e-02
+1.816326530612245027e-01 7.207131843689202899e-01 1.230426517053745221e-02
+1.979591836734694021e-01 7.223708121538943150e-01 1.250032090847500062e-02
+2.142857142857143016e-01 7.189764487567673612e-01 1.251781872499279843e-02
+2.306122448979592010e-01 7.117412524307588084e-01 1.229772223944200320e-02
+2.469387755102041004e-01 7.013582295319235049e-01 1.194003310693880424e-02
+2.632653061224490276e-01 6.881045284550147967e-01 1.155866526993589057e-02
+2.795918367346938993e-01 6.714215205856344237e-01 1.105528830905571497e-02
+2.959183673469387710e-01 6.526424526523527669e-01 1.089458089702714447e-02
+3.122448979591836982e-01 6.316226608424150291e-01 1.077018659836364786e-02
+3.285714285714286254e-01 6.077500186539003257e-01 1.116952747323463405e-02
+3.448979591836734970e-01 5.836326619162366391e-01 1.101364075145081986e-02
+3.612244897959183687e-01 5.581722686890733254e-01 1.089771491601521959e-02
+3.775510204081632404e-01 5.309647906816499230e-01 1.029080153886721520e-02
+3.938775510204082231e-01 5.026172192427382779e-01 9.833385791748083804e-03
+4.102040816326530948e-01 4.729472143835790066e-01 8.930840437713421717e-03
+4.265306122448979664e-01 4.424769600331791564e-01 9.367904277973288299e-03
+4.428571428571428381e-01 4.126698332598315866e-01 1.059838981925867105e-02
+4.591836734693878208e-01 3.809692818739471298e-01 1.056008118753932676e-02
+4.755102040816326925e-01 3.512845707804219120e-01 1.082288870256591616e-02
+4.918367346938775642e-01 3.239722561384902888e-01 1.024387162143642650e-02
+5.081632653061224358e-01 2.987031131287182784e-01 1.045563517738316676e-02
+5.244897959183674185e-01 2.729564461704206968e-01 1.074585510624960147e-02
+5.408163265306122902e-01 2.480024241700939380e-01 1.119419072927162716e-02
+5.571428571428571619e-01 2.252591619506083553e-01 1.056903482710983173e-02
+5.734693877551020336e-01 2.033308396707086130e-01 9.862655545891154882e-03
+5.897959183673470163e-01 1.810233365604385358e-01 1.092837393135298769e-02
+6.061224489795918879e-01 1.609589521154481395e-01 1.120069476152043240e-02
+6.224489795918367596e-01 1.415895843814952704e-01 1.196303501419522346e-02
+6.387755102040816313e-01 1.247038505112701839e-01 1.174567493327440676e-02
+6.551020408163265030e-01 1.085126989031491018e-01 1.129038558040623003e-02
+6.714285714285714857e-01 9.332242088573453054e-02 1.158142679674851495e-02
+6.877551020408163573e-01 8.010839740159633737e-02 1.022623955221433310e-02
+7.040816326530612290e-01 6.807985569865412923e-02 1.038097601483088488e-02
+7.204081632653062117e-01 5.772865008052108921e-02 1.071429636674453914e-02
+7.367346938775510834e-01 4.815571853068702224e-02 1.220396766522050848e-02
+7.530612244897959551e-01 3.941675457359115203e-02 1.293771834964407147e-02
+7.693877551020408267e-01 3.326630455306706785e-02 1.201717042389163770e-02
+7.857142857142856984e-01 2.854265247624555857e-02 1.152778484090345723e-02
+8.020408163265306811e-01 2.272068462983138637e-02 1.039916298616219877e-02
+8.183673469387755528e-01 1.825700122372798245e-02 9.575460541838392778e-03
+8.346938775510204245e-01 1.483899533506974518e-02 8.458777665579302399e-03
+8.510204081632654072e-01 1.184481233652320484e-02 7.668364582451015800e-03
+8.673469387755102789e-01 9.122462586070438292e-03 6.634621311653081215e-03
+8.836734693877551505e-01 7.202425593584829006e-03 5.472872084153720604e-03
+9.000000000000000222e-01 6.006656214330170773e-03 5.044743278601566266e-03
diff --git a/examples/qPDF/data/partial/ubar.dat b/examples/qPDF/data/partial/ubar.dat
new file mode 100644
index 000000000..aa7f141a7
--- /dev/null
+++ b/examples/qPDF/data/partial/ubar.dat
@@ -0,0 +1,50 @@
+1.000000000000000056e-01 1.111706400000000011e-01 8.179965169869038086e-03
+1.163265306122449050e-01 9.631597337328927366e-02 6.965397550310508876e-03
+1.326530612244898044e-01 8.338664560359232425e-02 6.118297067600263900e-03
+1.489795918367347038e-01 7.203485493994034317e-02 5.551827385416897680e-03
+1.653061224489796033e-01 6.209452577759019820e-02 4.962987139647203824e-03
+1.816326530612245027e-01 5.371508942069232184e-02 4.543103177953506690e-03
+1.979591836734694021e-01 4.624481991229831279e-02 4.206659232929604170e-03
+2.142857142857143016e-01 4.009459034116432535e-02 3.987682791304650483e-03
+2.306122448979592010e-01 3.440335061992415289e-02 3.727169143264499451e-03
+2.469387755102041004e-01 2.969381220895894735e-02 3.816188278520973951e-03
+2.632653061224490276e-01 2.561524157497386753e-02 3.639102757811946837e-03
+2.795918367346938993e-01 2.208906558513888674e-02 3.630724365966011072e-03
+2.959183673469387710e-01 1.853378533508788159e-02 3.323430862555169698e-03
+3.122448979591836982e-01 1.517791038369432736e-02 3.473349017361928009e-03
+3.285714285714286254e-01 1.355770075092034913e-02 5.142614075637582348e-03
+3.448979591836734970e-01 1.094030944405336402e-02 3.876749683563606840e-03
+3.612244897959183687e-01 8.592487438390646048e-03 4.986458385358475201e-03
+3.775510204081632404e-01 7.006103884160290625e-03 4.558050434345648980e-03
+3.938775510204082231e-01 5.636815763899808741e-03 5.167788915004078130e-03
+4.102040816326530948e-01 5.104961177614576566e-03 3.623114640176146664e-03
+4.265306122448979664e-01 4.283378024890269545e-03 4.788897464910948573e-03
+4.428571428571428381e-01 2.771418381560752930e-03 6.357959201349411803e-03
+4.591836734693878208e-01 4.239601722136225777e-03 5.808362884320261189e-03
+4.755102040816326925e-01 4.144939763097098794e-03 6.157426353526132545e-03
+4.918367346938775642e-01 2.599970243847376378e-03 5.548935444868298081e-03
+5.081632653061224358e-01 -2.516103480873571000e-04 6.572923314060078771e-03
+5.244897959183674185e-01 -1.639720875129115768e-03 6.037240612839354058e-03
+5.408163265306122902e-01 -2.116395666802422821e-03 6.043906253157798475e-03
+5.571428571428571619e-01 -3.097840758134474178e-03 5.749631410236337795e-03
+5.734693877551020336e-01 -3.577983923135422562e-03 6.279601865306166236e-03
+5.897959183673470163e-01 -2.507962595204027423e-03 8.400804333261060672e-03
+6.061224489795918879e-01 -2.595344859342544035e-03 8.865376650415127471e-03
+6.224489795918367596e-01 -2.318205541359436277e-03 9.966182175956909278e-03
+6.387755102040816313e-01 -3.517692703172981862e-03 1.049362282662101922e-02
+6.551020408163265030e-01 -3.924797227226901192e-03 1.042963482885535947e-02
+6.714285714285714857e-01 -4.551858755871746043e-03 1.084756265045347563e-02
+6.877551020408163573e-01 -5.735036617494569146e-03 9.716728775460639275e-03
+7.040816326530612290e-01 -6.257262548063195068e-03 1.014102716994294030e-02
+7.204081632653062117e-01 -6.142834477869558822e-03 1.066679325465065900e-02
+7.367346938775510834e-01 -5.354367355641570514e-03 1.216566243602633379e-02
+7.530612244897959551e-01 -4.491012694336645426e-03 1.270622481303653774e-02
+7.693877551020408267e-01 -5.013588714372360991e-03 1.144484866240926858e-02
+7.857142857142856984e-01 -5.693798006173376829e-03 1.073626677616170040e-02
+8.020408163265306811e-01 -4.305580344407663670e-03 9.784254832731606802e-03
+8.183673469387755528e-01 -3.492723870721761714e-03 9.000290850226813633e-03
+8.346938775510204245e-01 -3.041726663775672686e-03 8.010487218373465307e-03
+8.510204081632654072e-01 -2.700052505113971144e-03 7.163601897606069677e-03
+8.673469387755102789e-01 -2.194652043508042282e-03 6.162234962480548327e-03
+8.836734693877551505e-01 -2.151372510115513118e-03 5.118589556761047633e-03
+9.000000000000000222e-01 -2.384168039911821121e-03 5.326226440830149171e-03
diff --git a/examples/qPDF/data/partial/v.dat b/examples/qPDF/data/partial/v.dat
new file mode 100644
index 000000000..3a1f1252b
--- /dev/null
+++ b/examples/qPDF/data/partial/v.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 2.409332433385497074e-02 6.139939433975893923e-04
+1.151395399326446927e-04 2.563399484389794480e-02 7.368417313359243437e-04
+1.325711365590109543e-04 2.729566618448603377e-02 8.940427551678564733e-04
+1.526417967175233259e-04 2.908113496268388554e-02 1.071826646856594012e-03
+1.757510624854791170e-04 3.099065793148558789e-02 1.266470151572163250e-03
+2.023589647725157589e-04 3.301849624366737235e-02 1.425823533246772801e-03
+2.329951810515371805e-04 3.516589939286773880e-02 1.547203870220559274e-03
+2.682695795279724476e-04 3.743142390222492510e-02 1.633470266189929188e-03
+3.088843596477481527e-04 3.982648473418481938e-02 1.675470149689939348e-03
+3.556480306223128661e-04 4.235414178804519647e-02 1.671334622949022242e-03
+4.094915062380427508e-04 4.504059339772775727e-02 1.597829467800016001e-03
+4.714866363457394672e-04 4.789331366448362770e-02 1.452527053360908038e-03
+5.428675439323859403e-04 5.095521483434201082e-02 1.274214129943732066e-03
+6.250551925273975734e-04 5.425861882146287840e-02 1.071538590392259738e-03
+7.196856730011521675e-04 5.786287214154184344e-02 8.629408661428861205e-04
+8.286427728546842068e-04 6.185468967407886653e-02 7.165830880065393593e-04
+9.540954763499943534e-04 6.629607483904109699e-02 7.256939172376880576e-04
+1.098541141987558400e-03 7.124663100199635224e-02 7.133753846192275594e-04
+1.264855216855295715e-03 7.671529760119794039e-02 6.444353030430755025e-04
+1.456348477501244378e-03 8.262935522867614901e-02 4.856455467324855002e-04
+1.676832936811008387e-03 8.893381826859464434e-02 2.505936105514777745e-04
+1.930697728883249645e-03 9.547436464437430992e-02 -8.790577109937802724e-05
+2.222996482526195736e-03 1.022795720491102012e-01 -4.603984101919089689e-04
+2.559547922699535825e-03 1.093206187992075695e-01 -8.090187731591494402e-04
+2.947051702551809708e-03 1.167424380035106957e-01 -1.173662947933473699e-03
+3.393221771895329857e-03 1.245776234560457274e-01 -1.564786627264586061e-03
+3.906939937054616958e-03 1.330186832997240498e-01 -1.865460585694908580e-03
+4.498432668969444201e-03 1.421172032673739816e-01 -1.988769325223599393e-03
+5.179474679231212825e-03 1.520710936709788463e-01 -1.619494149053143928e-03
+5.963623316594642357e-03 1.627637530673841926e-01 -7.765553156137069979e-04
+6.866488450042998112e-03 1.739226180828475354e-01 5.577445779148607063e-04
+7.906043210907700777e-03 1.868863751184585698e-01 9.541796375784616924e-04
+9.102981779915217050e-03 2.019142266763930271e-01 -5.237241789141783288e-05
+1.048113134154685273e-02 2.183944932810497896e-01 -1.257472541037134295e-03
+1.206792640639328847e-02 2.365436214462611486e-01 -2.674044620900754843e-03
+1.389495494373137359e-02 2.567822757776673792e-01 -4.644127065801376131e-03
+1.599858719606057217e-02 2.796240510536457391e-01 -6.365348338658552430e-03
+1.842069969326716461e-02 3.054700108621894450e-01 -6.912084008575233168e-03
+2.120950887920190417e-02 3.349749004169555455e-01 -7.036393485510699514e-03
+2.442053094548649744e-02 3.683216411752401886e-01 -6.973376376096449003e-03
+2.811768697974230749e-02 4.059319752305156070e-01 -5.956858707028535316e-03
+3.237457542817643447e-02 4.473054430221816702e-01 -4.321418927177179264e-03
+3.727593720314938130e-02 4.911395046954560861e-01 -1.644464513056727306e-03
+4.291934260128778267e-02 5.366104690169182234e-01 6.711598535814773669e-04
+4.941713361323833015e-02 5.827091421608007193e-01 7.533135499223631015e-04
+5.689866029018292998e-02 6.294815536387448063e-01 9.901955224021086882e-04
+6.551285568595509312e-02 6.768664823281200782e-01 1.962040869776884161e-03
+7.543120063354614990e-02 7.246611517554401027e-01 2.234599803021370193e-03
+8.685113737513520948e-02 7.729086809360399268e-01 2.632184566432160810e-03
+1.000000000000000056e-01 8.219631450000000061e-01 4.472744027894770547e-03
diff --git a/examples/qPDF/data/partial/v3.dat b/examples/qPDF/data/partial/v3.dat
new file mode 100644
index 000000000..b139f92fd
--- /dev/null
+++ b/examples/qPDF/data/partial/v3.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 3.709153843587331689e-03 -1.294460527052412879e-03
+1.151395399326446927e-04 4.000960575745893255e-03 -1.272189925989306025e-03
+1.325711365590109543e-04 4.318183834072841787e-03 -1.251820952761883987e-03
+1.526417967175233259e-04 4.666986511230430956e-03 -1.229507452258457589e-03
+1.757510624854791170e-04 5.049239129196692044e-03 -1.211296513348857073e-03
+2.023589647725157589e-04 5.477156013369621768e-03 -1.203517031224531075e-03
+2.329951810515371805e-04 5.952690748948608057e-03 -1.212797163812409651e-03
+2.682695795279724476e-04 6.497803876925334343e-03 -1.261760471277802165e-03
+3.088843596477481527e-04 7.049611997319960022e-03 -1.328185468408182857e-03
+3.556480306223128661e-04 7.560114271729201363e-03 -1.407078063365894971e-03
+4.094915062380427508e-04 8.107519750108815870e-03 -1.462992208397947969e-03
+4.714866363457394672e-04 8.703536492151719983e-03 -1.484257969507851715e-03
+5.428675439323859403e-04 9.386648087601701107e-03 -1.435538982956133247e-03
+6.250551925273975734e-04 1.015403625715427527e-02 -1.310693900552002689e-03
+7.196856730011521675e-04 1.098327016996020200e-02 -1.159094632399902680e-03
+8.286427728546842068e-04 1.197364937573081045e-02 -8.301396807884886542e-04
+9.540954763499943534e-04 1.321516338060824136e-02 -1.816803400257541168e-04
+1.098541141987558400e-03 1.453771611306603484e-02 4.205133818923796618e-04
+1.264855216855295715e-03 1.591814324213158827e-02 8.821673442042203550e-04
+1.456348477501244378e-03 1.735972497538579296e-02 1.159102321058695650e-03
+1.676832936811008387e-03 1.890294494622801746e-02 1.320227417617819005e-03
+1.930697728883249645e-03 2.065299596330588683e-02 1.539167098646945786e-03
+2.222996482526195736e-03 2.257944996142652316e-02 1.740399217905457679e-03
+2.559547922699535825e-03 2.468029651314496142e-02 1.906687386700762349e-03
+2.947051702551809708e-03 2.692533578467010136e-02 1.884801149395712999e-03
+3.393221771895329857e-03 2.931276187023079682e-02 1.646727139411188062e-03
+3.906939937054616958e-03 3.196121877494878527e-02 1.239021758940781620e-03
+4.498432668969444201e-03 3.489718879934639029e-02 7.566765492925710268e-04
+5.179474679231212825e-03 3.817972938715480380e-02 2.741198153758152289e-04
+5.963623316594642357e-03 4.192220621672132497e-02 -1.715396287274330300e-04
+6.866488450042998112e-03 4.619519012868572583e-02 -5.470628568529194158e-04
+7.906043210907700777e-03 5.119958305791566522e-02 -7.619644953021954825e-04
+9.102981779915217050e-03 5.698017618741429979e-02 -7.593404022438916823e-04
+1.048113134154685273e-02 6.343513280541218657e-02 -5.374237122598470029e-04
+1.206792640639328847e-02 7.045226773299656520e-02 -1.633807364653898364e-04
+1.389495494373137359e-02 7.766657557742634443e-02 1.842972812932802684e-04
+1.599858719606057217e-02 8.518033946297082215e-02 5.596041918141149429e-04
+1.842069969326716461e-02 9.297027474706190397e-02 7.289304187375958421e-04
+2.120950887920190417e-02 1.012927420441265547e-01 8.083684380427794136e-04
+2.442053094548649744e-02 1.102071391038152715e-01 9.352470145973686144e-04
+2.811768697974230749e-02 1.199096929299429404e-01 1.248678625053845082e-03
+3.237457542817643447e-02 1.304129113505142490e-01 1.350929717884905013e-03
+3.727593720314938130e-02 1.417181886734582563e-01 1.233953045166377022e-03
+4.291934260128778267e-02 1.540653255514338738e-01 9.678512345811188938e-04
+4.941713361323833015e-02 1.677295485913511963e-01 7.172473457791612300e-04
+5.689866029018292998e-02 1.823584676492381318e-01 7.061995826628166689e-04
+6.551285568595509312e-02 1.978628347794686160e-01 1.160841317695027533e-03
+7.543120063354614990e-02 2.159454643005864760e-01 9.173334911300231459e-04
+8.685113737513520948e-02 2.362269267061525013e-01 6.591315587753197175e-04
+1.000000000000000056e-01 2.579254300000000111e-01 3.332937874284975371e-04
diff --git a/examples/qPDF/data/partial/v8.dat b/examples/qPDF/data/partial/v8.dat
new file mode 100644
index 000000000..5ad26b7b7
--- /dev/null
+++ b/examples/qPDF/data/partial/v8.dat
@@ -0,0 +1,50 @@
+1.000000000000000048e-04 3.142570116364562516e-02 1.279459685050776674e-03
+1.151395399326446927e-04 3.308435479737814600e-02 1.205367363498355271e-03
+1.325711365590109543e-04 3.485989265135536197e-02 1.151260142898680172e-03
+1.526417967175233259e-04 3.674532391650664476e-02 1.111022581747167504e-03
+1.757510624854791170e-04 3.873956456544158655e-02 1.090102566206563317e-03
+2.023589647725157589e-04 4.083481205164418704e-02 1.028231719495176222e-03
+2.329951810515371805e-04 4.303053129517608877e-02 9.237437726121178194e-04
+2.682695795279724476e-04 4.532139767855858370e-02 7.648154326440614881e-04
+3.088843596477481527e-04 4.771595411720908064e-02 5.754847871185519992e-04
+3.556480306223128661e-04 5.020537728988849757e-02 3.766318389149536650e-04
+4.094915062380427508e-04 5.287559229861948928e-02 1.838154797281776887e-04
+4.714866363457394672e-04 5.575168050733947389e-02 1.465493068023016043e-05
+5.428675439323859403e-04 5.905295167943280799e-02 -1.122443418009322347e-04
+6.250551925273975734e-04 6.273289354146038832e-02 -1.870994471893372424e-04
+7.196856730011521675e-04 6.675717155153770843e-02 -2.477665381384885901e-04
+8.286427728546842068e-04 7.101790912098437936e-02 -1.808658096041915186e-04
+9.540954763499943534e-04 7.542725486775037069e-02 1.455492417572479269e-04
+1.098541141987558400e-03 8.004122211560438238e-02 5.778067293672195515e-04
+1.264855216855295715e-03 8.486754986269084977e-02 1.085921038826487273e-03
+1.456348477501244378e-03 8.995269323481064339e-02 1.551038267477017341e-03
+1.676832936811008387e-03 9.533611385627405177e-02 1.920592288084971289e-03
+1.930697728883249645e-03 1.010784585245643741e-01 1.991745703567415759e-03
+2.222996482526195736e-03 1.073616461393115951e-01 1.680105010706117730e-03
+2.559547922699535825e-03 1.144250649828131294e-01 7.126072051936299667e-04
+2.947051702551809708e-03 1.216295418116605798e-01 -1.600852960845777773e-04
+3.393221771895329857e-03 1.288851220549040999e-01 -8.648794816137200803e-04
+3.906939937054616958e-03 1.367438239253928600e-01 -1.648270078091540469e-03
+4.498432668969444201e-03 1.452562744974513764e-01 -2.392961982128752330e-03
+5.179474679231212825e-03 1.545971215307738178e-01 -2.815199467855744597e-03
+5.963623316594642357e-03 1.649156620097197434e-01 -2.572308184336874470e-03
+6.866488450042998112e-03 1.762648730972930511e-01 -1.149657365379141982e-03
+7.906043210907700777e-03 1.892853762447305743e-01 4.401529215211918977e-04
+9.102981779915217050e-03 2.041088128276559721e-01 2.034377610009172055e-03
+1.048113134154685273e-02 2.201442450352001368e-01 4.015140635969749461e-03
+1.206792640639328847e-02 2.376396273306931595e-01 6.053503906537527346e-03
+1.389495494373137359e-02 2.571257492992121696e-01 7.495432120503693618e-03
+1.599858719606057217e-02 2.790094146813046350e-01 8.701290807122591398e-03
+1.842069969326716461e-02 3.033964367566540088e-01 9.829992110299348718e-03
+2.120950887920190417e-02 3.322628483256136467e-01 1.095988242511616861e-02
+2.442053094548649744e-02 3.660824188829614467e-01 1.163864064014223654e-02
+2.811768697974230749e-02 4.047733064186829766e-01 9.793079756769386413e-03
+3.237457542817643447e-02 4.474908425623155828e-01 6.329012901206645947e-03
+3.727593720314938130e-02 4.916881760456623396e-01 2.223557066318652964e-03
+4.291934260128778267e-02 5.369653061250165837e-01 -2.050408583142968033e-03
+4.941713361323833015e-02 5.829455097283484655e-01 -3.057864349947345822e-03
+5.689866029018292998e-02 6.270243384803549924e-01 -1.840824609249589600e-03
+6.551285568595509312e-02 6.684809678354665108e-01 -1.080701264518587473e-03
+7.543120063354614990e-02 7.080199161214449921e-01 -1.115911766339468258e-03
+8.685113737513520948e-02 7.462538002564987893e-01 -1.809542377258981788e-03
+1.000000000000000056e-01 7.856684999999999919e-01 -2.671446105093899792e-04
diff --git a/examples/qPDF/data/pdf_data.dat b/examples/qPDF/data/pdf_data.dat
new file mode 100644
index 000000000..2f9f32774
--- /dev/null
+++ b/examples/qPDF/data/pdf_data.dat
@@ -0,0 +1,50 @@
+1.000000000000000021e-02 2.765986106986625614e+00 1.348948981797258584e-01
+2.816326530612244666e-02 2.485758578723257006e+00 1.261321606925193406e-01
+4.632653061224489832e-02 2.238780806309143667e+00 1.116173627023625964e-01
+6.448979591836734304e-02 1.987549850779426475e+00 1.026761902704922680e-01
+8.265306122448978776e-02 1.723942291566691321e+00 7.132907796311850623e-02
+1.008163265306122325e-01 1.475908624317577189e+00 6.854736822671136931e-02
+1.189795918367346911e-01 1.245588663665847085e+00 5.907254075025149265e-02
+1.371428571428571497e-01 1.049015761589518148e+00 6.589860432977409643e-02
+1.553061224489795944e-01 8.952917735617587081e-01 6.746180997134297586e-02
+1.734693877551020391e-01 7.724199196709109483e-01 5.133415018360887316e-02
+1.916326530612244838e-01 6.758865210947631041e-01 5.584343788264279490e-02
+2.097959183673469286e-01 5.936451955028620997e-01 5.658905900755074880e-02
+2.279591836734694010e-01 5.234495030146305572e-01 5.347066481400952753e-02
+2.461224489795918458e-01 4.581125949597144031e-01 5.084892212139895118e-02
+2.642857142857142905e-01 3.962749944360354770e-01 4.734194184365002334e-02
+2.824489795918367352e-01 3.313422630365456811e-01 5.058062904250364838e-02
+3.006122448979591799e-01 2.839883734745246580e-01 5.086744959275122518e-02
+3.187755102040816246e-01 2.471767676753837961e-01 4.499310451340012701e-02
+3.369387755102040694e-01 2.168695568083809366e-01 3.959095570759064292e-02
+3.551020408163265141e-01 1.905891134272340304e-01 3.410572308939294195e-02
+3.732653061224489588e-01 1.688817514899391137e-01 3.024063920978757938e-02
+3.914285714285714035e-01 1.505076606989460275e-01 2.838664032285428268e-02
+4.095918367346938482e-01 1.331617615667257670e-01 2.830508514778251605e-02
+4.277551020408162930e-01 1.172633162018371228e-01 2.925500733859964439e-02
+4.459183673469387932e-01 1.057377460987353052e-01 2.877646223511087786e-02
+4.640816326530612379e-01 9.872081147109411370e-02 3.504078861137879114e-02
+4.822448979591836826e-01 8.966772154476171375e-02 3.224736919730514240e-02
+5.004081632653061273e-01 8.388436154310217885e-02 3.552743851948359649e-02
+5.185714285714285721e-01 7.467272147042511299e-02 3.166757696681550549e-02
+5.367346938775510168e-01 6.811473957428268444e-02 2.727189854406495209e-02
+5.548979591836734615e-01 5.985778382064777664e-02 2.580892085716824866e-02
+5.730612244897959062e-01 5.348727534288520019e-02 2.496247682597954251e-02
+5.912244897959183509e-01 4.810731292132282982e-02 2.616700757385116011e-02
+6.093877551020407957e-01 4.095731388878246643e-02 2.293249172482235768e-02
+6.275510204081632404e-01 3.400226330004344627e-02 2.183785277053256271e-02
+6.457142857142856851e-01 2.783045181404299184e-02 2.250430201556932394e-02
+6.638775510204081298e-01 2.221325265192166873e-02 2.183474733153495384e-02
+6.820408163265305745e-01 1.542478233879566486e-02 2.290772587919922068e-02
+7.002040816326530193e-01 1.214751804439175020e-02 1.951296868339153986e-02
+7.183673469387754640e-01 8.283353840295002227e-03 1.719329200134269917e-02
+7.365306122448979087e-01 5.041955533421473533e-03 1.705952653697170862e-02
+7.546938775510203534e-01 3.181927606473519601e-03 1.452023874009237582e-02
+7.728571428571427981e-01 -4.039832581420538054e-04 1.635473491921834305e-02
+7.910204081632652429e-01 -1.823365802263343617e-03 1.287983014715655961e-02
+8.091836734693876876e-01 -4.120357001949629533e-03 1.464145257887157665e-02
+8.273469387755101323e-01 -5.081018355908304210e-03 1.491692863521352011e-02
+8.455102040816325770e-01 -5.395880063568041923e-03 1.330090000857934128e-02
+8.636734693877550217e-01 -5.582958320940293283e-03 1.290931163654367164e-02
+8.818367346938775775e-01 -5.342994408291224830e-03 1.220894887228822674e-02
+9.000000000000000222e-01 -4.864826018318149127e-03 1.070561871116822161e-02
diff --git a/examples/qPDF/data/pdf_data_full.dat b/examples/qPDF/data/pdf_data_full.dat
new file mode 100644
index 000000000..373896f14
--- /dev/null
+++ b/examples/qPDF/data/pdf_data_full.dat
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 1.436073027852281747e+00 1.410829786450314760e+00
+1.151395399326446927e-04 1.364958626022871213e+00 1.322403944448146795e+00
+1.325711365590109543e-04 1.308744557998403835e+00 1.246856327363375705e+00
+1.526417967175233259e-04 1.266716265313918344e+00 1.181035820270940784e+00
+1.757510624854791170e-04 1.238632415751813820e+00 1.124100495485785611e+00
+2.023589647725157589e-04 1.223304323506445579e+00 1.082280689395135109e+00
+2.329951810515371805e-04 1.221403075342442968e+00 1.049932598530862737e+00
+2.682695795279724476e-04 1.234246661261282352e+00 1.018495848476023369e+00
+3.088843596477481527e-04 1.261118092261666934e+00 9.820269772796574870e-01
+3.556480306223128661e-04 1.301809107355178963e+00 9.380345387151233227e-01
+4.094915062380427508e-04 1.355955018492741582e+00 8.893711185159632882e-01
+4.714866363457394672e-04 1.423415028768741042e+00 8.375259875883908078e-01
+5.428675439323859403e-04 1.503558397812429392e+00 7.741246897837354979e-01
+6.250551925273975734e-04 1.594760490544748555e+00 7.053377733669865535e-01
+7.196856730011521675e-04 1.694368883930322145e+00 6.381482642538304217e-01
+8.286427728546842068e-04 1.797933073006341420e+00 5.770645929332395285e-01
+9.540954763499943534e-04 1.902288016649845925e+00 5.213202772176038780e-01
+1.098541141987558400e-03 2.004993449735332334e+00 4.718001852409516306e-01
+1.264855216855295715e-03 2.105514952847052701e+00 4.273401137522174764e-01
+1.456348477501244378e-03 2.204919381213297669e+00 3.857407874870817932e-01
+1.676832936811008387e-03 2.302838545246236102e+00 3.527675463927992805e-01
+1.930697728883249645e-03 2.401250317650158284e+00 3.292460342654914274e-01
+2.222996482526195736e-03 2.489413722921670402e+00 3.241005711624331820e-01
+2.559547922699535825e-03 2.558364682243282839e+00 3.028155002281870400e-01
+2.947051702551809708e-03 2.616224206212616199e+00 2.820170227437128752e-01
+3.393221771895329857e-03 2.664071453832157932e+00 2.591501164691561443e-01
+3.906939937054616958e-03 2.703361738998359520e+00 2.348943198277512467e-01
+4.498432668969444201e-03 2.734359509219411066e+00 2.093647295324295721e-01
+5.179474679231212825e-03 2.758211866006374358e+00 1.861954455819890830e-01
+5.963623316594642357e-03 2.774504985860168027e+00 1.666400687266494929e-01
+6.866488450042998112e-03 2.783256351062659117e+00 1.509792672160396587e-01
+7.906043210907700777e-03 2.783431601575428349e+00 1.393981853977233343e-01
+9.102981779915217050e-03 2.774862150044652420e+00 1.356247879444966997e-01
+1.048113134154685273e-02 2.760566329600861124e+00 1.342862511369074530e-01
+1.206792640639328847e-02 2.740601584470428342e+00 1.316902676015173357e-01
+1.389495494373137359e-02 2.715933591822299764e+00 1.253457936888159319e-01
+1.599858719606057217e-02 2.685154192466012724e+00 1.193572415535045228e-01
+1.842069969326716461e-02 2.647431992894222486e+00 1.132596856420066828e-01
+2.120950887920190417e-02 2.601477236862831166e+00 1.124669224631803938e-01
+2.442053094548649744e-02 2.546935186302070164e+00 1.199475922069823675e-01
+2.811768697974230749e-02 2.486491029640843564e+00 1.260482655459385648e-01
+3.237457542817643447e-02 2.421072694716483120e+00 1.342529033015319173e-01
+3.727593720314938130e-02 2.356120187280772438e+00 1.228918399554665974e-01
+4.291934260128778267e-02 2.283609481004460040e+00 1.135313148672207062e-01
+4.941713361323833015e-02 2.197861404801443985e+00 1.111765296966798489e-01
+5.689866029018292998e-02 2.094792655510467227e+00 1.086382056511527999e-01
+6.551285568595509312e-02 1.972943483478049043e+00 1.016059983620012930e-01
+7.543120063354614990e-02 1.828797090456429908e+00 8.228479680656052009e-02
+8.685113737513520948e-02 1.665052707183172664e+00 7.019163919958561204e-02
+1.000000000000000056e-01 1.486650900000000108e+00 6.906649307998630194e-02
+1.000000000000000056e-01 1.486650900000000108e+00 6.906649307998630194e-02
+1.163265306122449050e-01 1.278363607352488662e+00 5.851715503791719841e-02
+1.326530612244898044e-01 1.093272570071426530e+00 6.495189916810374664e-02
+1.489795918367347038e-01 9.445472028682869725e-01 6.800362366808562764e-02
+1.653061224489796033e-01 8.244352867442255040e-01 6.036562576446031364e-02
+1.816326530612245027e-01 7.266001875331934423e-01 4.804631794363146274e-02
+1.979591836734694021e-01 6.460934389251358212e-01 5.956328597727415797e-02
+2.142857142857143016e-01 5.754064633997411082e-01 5.481677133626022824e-02
+2.306122448979592010e-01 5.137991377127972825e-01 5.361156055870076875e-02
+2.469387755102041004e-01 4.552507567815178291e-01 5.065601192147932419e-02
+2.632653061224490276e-01 3.996382053409509028e-01 4.755273215299854472e-02
+2.795918367346938993e-01 3.411444192178936241e-01 4.570775803372454510e-02
+2.959183673469387710e-01 2.941440729953202493e-01 5.179924323811337800e-02
+3.122448979591836982e-01 2.601290037822677204e-01 4.622670247373922986e-02
+3.285714285714286254e-01 2.302641998343407104e-01 4.244869960571524825e-02
+3.448979591836734970e-01 2.048947603867307399e-01 3.692175416096520058e-02
+3.612244897959183687e-01 1.820567154916601094e-01 3.491710247794299615e-02
+3.775510204081632404e-01 1.654109241587360668e-01 2.958093259719593235e-02
+3.938775510204082231e-01 1.480349508988084706e-01 2.827395372375652097e-02
+4.102040816326530948e-01 1.325798271673375717e-01 2.828630531659322850e-02
+4.265306122448979664e-01 1.182493409930097245e-01 2.916021507299379825e-02
+4.428571428571428381e-01 1.066391781871470490e-01 2.847724416602226416e-02
+4.591836734693878208e-01 1.011350726109566528e-01 3.463682731939739246e-02
+4.755102040816326925e-01 9.174674898459940497e-02 3.317719061506606976e-02
+4.918367346938775642e-01 8.739403248853304385e-02 3.458909252220616903e-02
+5.081632653061224358e-01 7.986314864670505942e-02 3.453373395965902981e-02
+5.244897959183674185e-01 7.216510147179601431e-02 2.968070892373265643e-02
+5.408163265306122902e-01 6.645926254942295663e-02 2.713087028010289364e-02
+5.571428571428571619e-01 5.877459162780401120e-02 2.555514304330760988e-02
+5.734693877551020336e-01 5.338086792246633794e-02 2.502004244557643961e-02
+5.897959183673470163e-01 4.876099631660012895e-02 2.707226383643828588e-02
+6.061224489795918879e-01 4.207712352396885541e-02 2.305135101626111169e-02
+6.224489795918367596e-01 3.580485555936888109e-02 2.185199910362296480e-02
+6.387755102040816313e-01 3.021147633166515814e-02 2.208234850326066048e-02
+6.551020408163265030e-01 2.484776981462179582e-02 2.198756275659718984e-02
+6.714285714285714857e-01 1.988113039811458330e-02 2.200991807516783014e-02
+6.877551020408163573e-01 1.400636390703255754e-02 2.365106623106593212e-02
+7.040816326530612290e-01 1.145974167737420432e-02 1.887010255106317397e-02
+7.204081632653062117e-01 7.837588375768960586e-03 1.719052997285148640e-02
+7.367346938775510834e-01 5.019045671291559423e-03 1.703959974523763451e-02
+7.530612244897959551e-01 3.380609462740184409e-03 1.479054695636173103e-02
+7.693877551020408267e-01 9.324618436404591567e-05 1.621917485475198040e-02
+7.857142857142856984e-01 -1.538774680937169846e-03 1.427911879458286688e-02
+8.020408163265306811e-01 -3.292689738769052563e-03 1.302317972988607450e-02
+8.183673469387755528e-01 -4.768501625455401523e-03 1.556994705567193819e-02
+8.346938775510204245e-01 -5.230190570117304073e-03 1.414505616898533748e-02
+8.510204081632654072e-01 -5.504081810805650996e-03 1.310886165854219070e-02
+8.673469387755102789e-01 -5.565775093366548236e-03 1.283554913783929025e-02
+8.836734693877551505e-01 -5.301125605810848816e-03 1.208554103662637380e-02
+9.000000000000000222e-01 -4.864826018318149127e-03 1.070561871116822161e-02
diff --git a/examples/qPDF/data/pdf_data_full_evolution_basis.txt b/examples/qPDF/data/pdf_data_full_evolution_basis.txt
new file mode 100644
index 000000000..0661c9fb9
--- /dev/null
+++ b/examples/qPDF/data/pdf_data_full_evolution_basis.txt
@@ -0,0 +1,100 @@
+1.000000000000000048e-04 2.906051619342992609e+00 7.462119819026813428e-01 1.436073027852281747e+00 1.410829786450314760e+00 2.409332433385497074e-02 6.139939433975893923e-04 3.709153843587331689e-03 -1.294460527052412879e-03 3.142570116364562516e-02 1.279459685050776674e-03 2.178039037871642991e-02 -5.501273989189520153e-02 2.967891727365936649e-01 -5.351913518991966434e-01 9.543745599019025317e-04 8.933750230847316287e-02
+1.151395399326446927e-04 2.860722503238625869e+00 6.988723163291101770e-01 1.364958626022871213e+00 1.322403944448146795e+00 2.563399484389794480e-02 7.368417313359243437e-04 4.000960575745893255e-03 -1.272189925989306025e-03 3.308435479737814600e-02 1.205367363498355271e-03 2.360494800229695489e-02 -5.079617646067179082e-02 3.064897739743315253e-01 -5.228070936938105095e-01 3.636680441961381328e-03 7.412584225106703950e-02
+1.325711365590109543e-04 2.815405728981031697e+00 6.586280524367921219e-01 1.308744557998403835e+00 1.246856327363375705e+00 2.729566618448603377e-02 8.940427551678564733e-04 4.318183834072841787e-03 -1.251820952761883987e-03 3.485989265135536197e-02 1.151260142898680172e-03 2.614203360893901928e-02 -4.664551928313108442e-02 3.158794264026344445e-01 -5.090709612371046022e-01 4.933524249824835194e-03 6.407245802265681967e-02
+1.526417967175233259e-04 2.770324102867665061e+00 6.244195241115695350e-01 1.266716265313918344e+00 1.181035820270940784e+00 2.908113496268388554e-02 1.071826646856594012e-03 4.666986511230430956e-03 -1.229507452258457589e-03 3.674532391650664476e-02 1.111022581747167504e-03 2.828035986310167837e-02 -4.300222704272022023e-02 3.248707472090531789e-01 -4.941991141595850068e-01 5.655341068586110242e-03 5.779131852221603555e-02
+1.757510624854791170e-04 2.725517322797724074e+00 5.944487582232559353e-01 1.238632415751813820e+00 1.124100495485785611e+00 3.099065793148558789e-02 1.266470151572163250e-03 5.049239129196692044e-03 -1.211296513348857073e-03 3.873956456544158655e-02 1.090102566206563317e-03 2.991495798307713949e-02 -3.997435589083222357e-02 3.334714075036346870e-01 -4.785649375008670381e-01 5.894428576734326262e-03 5.400134138166334807e-02
+2.023589647725157589e-04 2.681196838693734374e+00 5.641250926612668692e-01 1.223304323506445579e+00 1.082280689395135109e+00 3.301849624366737235e-02 1.425823533246772801e-03 5.477156013369621768e-03 -1.203517031224531075e-03 4.083481205164418704e-02 1.028231719495176222e-03 3.126187635942873078e-02 -3.730603537237335565e-02 3.418424717767015730e-01 -4.624912367226028387e-01 6.105220955743916912e-03 4.880768069883391347e-02
+2.329951810515371805e-04 2.637384376323162360e+00 5.350509620256458376e-01 1.221403075342442968e+00 1.049932598530862737e+00 3.516589939286773880e-02 1.547203870220559274e-03 5.952690748948608057e-03 -1.212797163812409651e-03 4.303053129517608877e-02 9.237437726121178194e-04 3.235494070724337368e-02 -3.489195915358409006e-02 3.500848868970291017e-01 -4.458210154938854197e-01 6.235027687244114672e-03 4.349911980376581361e-02
+2.682695795279724476e-04 2.594164003900461424e+00 5.091953271630039746e-01 1.234246661261282352e+00 1.018495848476023369e+00 3.743142390222492510e-02 1.633470266189929188e-03 6.497803876925334343e-03 -1.261760471277802165e-03 4.532139767855858370e-02 7.648154326440614881e-04 3.330068004701974838e-02 -3.256661292586237461e-02 3.584611086702909066e-01 -4.283162187443461111e-01 6.268431070720082224e-03 3.967181908221596026e-02
+3.088843596477481527e-04 2.551560515699526466e+00 4.856781521167745641e-01 1.261118092261666934e+00 9.820269772796574870e-01 3.982648473418481938e-02 1.675470149689939348e-03 7.049611997319960022e-03 -1.328185468408182857e-03 4.771595411720908064e-02 5.754847871185519992e-04 3.405999944634308108e-02 -3.032459936431707720e-02 3.668684233087414759e-01 -4.105069411107938326e-01 6.217020373943566401e-03 3.686609060461188553e-02
+3.556480306223128661e-04 2.509599140891932478e+00 4.637532165071666101e-01 1.301809107355178963e+00 9.380345387151233227e-01 4.235414178804519647e-02 1.671334622949022242e-03 7.560114271729201363e-03 -1.407078063365894971e-03 5.020537728988849757e-02 3.766318389149536650e-04 3.462309337997493408e-02 -2.816141756988261441e-02 3.751541695227437190e-01 -3.927431014729541592e-01 6.047944115574644883e-03 3.465683609043487129e-02
+4.094915062380427508e-04 2.468315423913098439e+00 4.422539326639124968e-01 1.355955018492741582e+00 8.893711185159632882e-01 4.504059339772775727e-02 1.597829467800016001e-03 8.107519750108815870e-03 -1.462992208397947969e-03 5.287559229861948928e-02 1.838154797281776887e-04 3.499154103999518872e-02 -2.598801321550247428e-02 3.844380513026034452e-01 -3.753661775630095510e-01 6.086277759868033259e-03 3.255440885733468931e-02
+4.714866363457394672e-04 2.427712402325765240e+00 4.210862448528221447e-01 1.423415028768741042e+00 8.375259875883908078e-01 4.789331366448362770e-02 1.452527053360908038e-03 8.703536492151719983e-03 -1.484257969507851715e-03 5.575168050733947389e-02 1.465493068023016043e-05 3.517161650060213196e-02 -2.380730015382700321e-02 3.947170717208153823e-01 -3.586138022321409546e-01 6.353607011376051841e-03 3.045711151395184141e-02
+5.428675439323859403e-04 2.387743918408224975e+00 4.011382559420967331e-01 1.503558397812429392e+00 7.741246897837354979e-01 5.095521483434201082e-02 1.274214129943732066e-03 9.386648087601701107e-03 -1.435538982956133247e-03 5.905295167943280799e-02 -1.122443418009322347e-04 3.526360047455268099e-02 -2.200751087594812055e-02 4.033746393990121915e-01 -3.417970480025358526e-01 6.428561308584479321e-03 2.867200099971633012e-02
+6.250551925273975734e-04 2.348387102320955488e+00 3.820577280180124102e-01 1.594760490544748555e+00 7.053377733669865535e-01 5.425861882146287840e-02 1.071538590392259738e-03 1.015403625715427527e-02 -1.310693900552002689e-03 6.273289354146038832e-02 -1.870994471893372424e-04 3.527563526439142505e-02 -2.051531149610450605e-02 4.105281640533186005e-01 -3.249129270673927206e-01 6.372404207167801926e-03 2.704047344363377792e-02
+7.196856730011521675e-04 2.309572722275872181e+00 3.635766358785286201e-01 1.694368883930322145e+00 6.381482642538304217e-01 5.786287214154184344e-02 8.629408661428861205e-04 1.098327016996020200e-02 -1.159094632399902680e-03 6.675717155153770843e-02 -2.477665381384885901e-04 3.524445607586629281e-02 -1.937188316215548295e-02 4.155169660940215559e-01 -3.074495584179327556e-01 6.258801326906005338e-03 2.571516118777497986e-02
+8.286427728546842068e-04 2.271337215556608058e+00 3.459199706961187948e-01 1.797933073006341420e+00 5.770645929332395285e-01 6.185468967407886653e-02 7.165830880065393593e-04 1.197364937573081045e-02 -8.301396807884886542e-04 7.101790912098437936e-02 -1.808658096041915186e-04 3.517307215238224538e-02 -1.824551761460532773e-02 4.201191315065782028e-01 -2.900762778330903124e-01 6.047430101503004259e-03 2.473241740672309924e-02
+9.540954763499943534e-04 2.233683502841355306e+00 3.289489556794900627e-01 1.902288016649845925e+00 5.213202772176038780e-01 6.629607483904109699e-02 7.256939172376880576e-04 1.321516338060824136e-02 -1.816803400257541168e-04 7.542725486775037069e-02 1.455492417572479269e-04 3.506902486917523598e-02 -1.682295858235231117e-02 4.257318112086013739e-01 -2.732669519568579064e-01 5.727973869072462351e-03 2.400751060148542324e-02
+1.098541141987558400e-03 2.196685734288134739e+00 3.130908018498047563e-01 2.004993449735332334e+00 4.718001852409516306e-01 7.124663100199635224e-02 7.133753846192275594e-04 1.453771611306603484e-02 4.205133818923796618e-04 8.004122211560438238e-02 5.778067293672195515e-04 3.494190035771210789e-02 -1.546271661467520792e-02 4.303743647380635640e-01 -2.570325180831006806e-01 5.217025855255588707e-03 2.331562484684581577e-02
+1.264855216855295715e-03 2.160359142346439221e+00 2.982326008119254857e-01 2.105514952847052701e+00 4.273401137522174764e-01 7.671529760119794039e-02 6.444353030430755025e-04 1.591814324213158827e-02 8.821673442042203550e-04 8.486754986269084977e-02 1.085921038826487273e-03 3.479091040879289665e-02 -1.425412340907782419e-02 4.338180042770034550e-01 -2.413071011501616847e-01 4.520400786895952851e-03 2.287525171199299504e-02
+1.456348477501244378e-03 2.124768531205242450e+00 2.835482339955216480e-01 2.204919381213297669e+00 3.857407874870817932e-01 8.262935522867614901e-02 4.856455467324855002e-04 1.735972497538579296e-02 1.159102321058695650e-03 8.995269323481064339e-02 1.551038267477017341e-03 3.452956214373470445e-02 -1.330042022227230156e-02 4.368136614232921433e-01 -2.261871807747988239e-01 4.104105861430898221e-03 2.251634616463276534e-02
+1.676832936811008387e-03 2.089886636778342410e+00 2.689103658428349530e-01 2.302838545246236102e+00 3.527675463927992805e-01 8.893381826859464434e-02 2.505936105514777745e-04 1.890294494622801746e-02 1.320227417617819005e-03 9.533611385627405177e-02 1.920592288084971289e-03 3.422874677965193069e-02 -1.251282444079887882e-02 4.392256045319101743e-01 -2.114730824142506405e-01 3.888985899011146528e-03 2.211418539704611622e-02
+1.930697728883249645e-03 2.055685659097348683e+00 2.544796379090471516e-01 2.401250317650158284e+00 3.292460342654914274e-01 9.547436464437430992e-02 -8.790577109937802724e-05 2.065299596330588683e-02 1.539167098646945786e-03 1.010784585245643741e-01 1.991745703567415759e-03 3.407769421897388629e-02 -1.162179965122341410e-02 4.406562962078498513e-01 -1.967104472715614505e-01 3.777269112706165780e-03 2.186241065726403712e-02
+2.222996482526195736e-03 2.022064289211034094e+00 2.409054177577455991e-01 2.489413722921670402e+00 3.241005711624331820e-01 1.022795720491102012e-01 -4.603984101919089689e-04 2.257944996142652316e-02 1.740399217905457679e-03 1.073616461393115951e-01 1.680105010706117730e-03 3.395628867277111773e-02 -1.067351857293324935e-02 4.417490464017018725e-01 -1.821002756467193229e-01 3.716552809729335802e-03 2.186462296014471396e-02
+2.559547922699535825e-03 1.988934505919132345e+00 2.278774653941089690e-01 2.558364682243282839e+00 3.028155002281870400e-01 1.093206187992075695e-01 -8.090187731591494402e-04 2.468029651314496142e-02 1.906687386700762349e-03 1.144250649828131294e-01 7.126072051936299667e-04 3.379958717023096026e-02 -9.689595645125753665e-03 4.426500358456443296e-01 -1.675258441897166661e-01 3.668101774664294951e-03 2.202517508457559753e-02
+2.947051702551809708e-03 1.956332697014812272e+00 2.162392065064994151e-01 2.616224206212616199e+00 2.820170227437128752e-01 1.167424380035106957e-01 -1.173662947933473699e-03 2.692533578467010136e-02 1.884801149395712999e-03 1.216295418116605798e-01 -1.600852960845777773e-04 3.363598642204607136e-02 -8.576650766480675703e-03 4.450971951998125009e-01 -1.538982966950086517e-01 3.547940999186200654e-03 2.200550651002718128e-02
+3.393221771895329857e-03 1.924269027649759600e+00 2.058747170265479709e-01 2.664071453832157932e+00 2.591501164691561443e-01 1.245776234560457274e-01 -1.564786627264586061e-03 2.931276187023079682e-02 1.646727139411188062e-03 1.288851220549040999e-01 -8.648794816137200803e-04 3.347228843142929788e-02 -7.335712491089114995e-03 4.494439335672644198e-01 -1.412161338165656699e-01 3.334206195109535448e-03 2.177519424557845459e-02
+3.906939937054616958e-03 1.892982336078212091e+00 1.961808772479244345e-01 2.703361738998359520e+00 2.348943198277512467e-01 1.330186832997240498e-01 -1.865460585694908580e-03 3.196121877494878527e-02 1.239021758940781620e-03 1.367438239253928600e-01 -1.648270078091540469e-03 3.346546911966319371e-02 -6.382514472699104408e-03 4.561658895047046025e-01 -1.292383936954070722e-01 2.882653201686987635e-03 2.111194539755851737e-02
+4.498432668969444201e-03 1.862538821228059138e+00 1.875839665940923495e-01 2.734359509219411066e+00 2.093647295324295721e-01 1.421172032673739816e-01 -1.988769325223599393e-03 3.489718879934639029e-02 7.566765492925710268e-04 1.452562744974513764e-01 -2.392961982128752330e-03 3.363083117111598241e-02 -5.638327509948526384e-03 4.651977385458023750e-01 -1.180918926972806537e-01 2.266223600348241541e-03 2.030995470319845245e-02
+5.179474679231212825e-03 1.833140068419793378e+00 1.807254197101701987e-01 2.758211866006374358e+00 1.861954455819890830e-01 1.520710936709788463e-01 -1.619494149053143928e-03 3.817972938715480380e-02 2.741198153758152289e-04 1.545971215307738178e-01 -2.815199467855744597e-03 3.404927746485519879e-02 -5.019592136273724747e-03 4.768204251285368955e-01 -1.083340128914820760e-01 1.650199402710971321e-03 1.966948473734408259e-02
+5.963623316594642357e-03 1.804839151717607448e+00 1.756910118391523667e-01 2.774504985860168027e+00 1.666400687266494929e-01 1.627637530673841926e-01 -7.765553156137069979e-04 4.192220621672132497e-02 -1.715396287274330300e-04 1.649156620097197434e-01 -2.572308184336874470e-03 3.466819563282846905e-02 -4.454395194833919713e-03 4.897136399974937881e-01 -1.001283229540880920e-01 1.045156606308592952e-03 1.936423761741078858e-02
+6.866488450042998112e-03 1.777705814340383661e+00 1.721635560558101297e-01 2.783256351062659117e+00 1.509792672160396587e-01 1.739226180828475354e-01 5.577445779148607063e-04 4.619519012868572583e-02 -5.470628568529194158e-04 1.762648730972930511e-01 -1.149657365379141982e-03 3.547883554846648080e-02 -3.933647755103783505e-03 5.028157826833805633e-01 -9.246198104295999531e-02 4.393240182844387388e-04 1.955534016925687679e-02
+7.906043210907700777e-03 1.751789858031411518e+00 1.685009977035207829e-01 2.783431601575428349e+00 1.393981853977233343e-01 1.868863751184585698e-01 9.541796375784616924e-04 5.119958305791566522e-02 -7.619644953021954825e-04 1.892853762447305743e-01 4.401529215211918977e-04 3.636258386306334689e-02 -3.485432043680698061e-03 5.162903347157841161e-01 -8.530814794740892992e-02 1.293074295599358806e-04 1.970672652804380393e-02
+9.102981779915217050e-03 1.727107965448127613e+00 1.641839731575882588e-01 2.774862150044652420e+00 1.356247879444966997e-01 2.019142266763930271e-01 -5.237241789141783288e-05 5.698017618741429979e-02 -7.593404022438916823e-04 2.041088128276559721e-01 2.034377610009172055e-03 3.729492238222775757e-02 -3.060382179671066599e-03 5.301498020885356821e-01 -7.836694087990382640e-02 1.761382065781277049e-04 1.994970249637265797e-02
+1.048113134154685273e-02 1.703752463217196089e+00 1.606642201493900246e-01 2.760566329600861124e+00 1.342862511369074530e-01 2.183944932810497896e-01 -1.257472541037134295e-03 6.343513280541218657e-02 -5.374237122598470029e-04 2.201442450352001368e-01 4.015140635969749461e-03 3.829244166622192136e-02 -2.979236290565519252e-03 5.468735475869694884e-01 -7.269911721980590324e-02 6.342964865634353891e-04 2.000374435605717371e-02
+1.206792640639328847e-02 1.681741841466589227e+00 1.574115297323718798e-01 2.740601584470428342e+00 1.316902676015173357e-01 2.365436214462611486e-01 -2.674044620900754843e-03 7.045226773299656520e-02 -1.633807364653898364e-04 2.376396273306931595e-01 6.053503906537527346e-03 3.950383879785190899e-02 -3.016866291674448475e-03 5.669838951047392328e-01 -6.756187116634179701e-02 1.159859179318486122e-03 2.010314301162413955e-02
+1.389495494373137359e-02 1.661180872498956518e+00 1.506227976403611279e-01 2.715933591822299764e+00 1.253457936888159319e-01 2.567822757776673792e-01 -4.644127065801376131e-03 7.766657557742634443e-02 1.842972812932802684e-04 2.571257492992121696e-01 7.495432120503693618e-03 4.140581276527505183e-02 -2.531192989213757599e-03 5.925765651394002687e-01 -6.127512509837636234e-02 6.229645941732639153e-04 1.893838285154697196e-02
+1.599858719606057217e-02 1.641936295425339809e+00 1.438243804779347768e-01 2.685154192466012724e+00 1.193572415535045228e-01 2.796240510536457391e-01 -6.365348338658552430e-03 8.518033946297082215e-02 5.596041918141149429e-04 2.790094146813046350e-01 8.701290807122591398e-03 4.382995926841448675e-02 -2.114565963955858385e-03 6.228247868785594665e-01 -5.433207365904305780e-02 -4.528610396075364209e-04 1.816499523496944643e-02
+1.842069969326716461e-02 1.623973998634635985e+00 1.377901334833385172e-01 2.647431992894222486e+00 1.132596856420066828e-01 3.054700108621894450e-01 -6.912084008575233168e-03 9.297027474706190397e-02 7.289304187375958421e-04 3.033964367566540088e-01 9.829992110299348718e-03 4.667630562357144575e-02 -2.149637273613766442e-03 6.575990713816981525e-01 -4.618559625605917623e-02 -2.040862806432878866e-03 1.837584791020675096e-02
+2.120950887920190417e-02 1.606952748332304948e+00 1.316949735372383856e-01 2.601477236862831166e+00 1.124669224631803938e-01 3.349749004169555455e-01 -7.036393485510699514e-03 1.012927420441265547e-01 8.083684380427794136e-04 3.322628483256136467e-01 1.095988242511616861e-02 5.016488550414333059e-02 -2.299801919725254912e-03 6.958361640459556074e-01 -3.991318766042432908e-02 -2.764948712256495682e-03 1.852305431254810605e-02
+2.442053094548649744e-02 1.590788822310530204e+00 1.243226889588570361e-01 2.546935186302070164e+00 1.199475922069823675e-01 3.683216411752401886e-01 -6.973376376096449003e-03 1.102071391038152715e-01 9.352470145973686144e-04 3.660824188829614467e-01 1.163864064014223654e-02 5.434134892004915951e-02 -2.427710292972059397e-03 7.372735601027718966e-01 -3.546250788524608205e-02 -2.301153740602655817e-03 1.806891834668321664e-02
+2.811768697974230749e-02 1.575168016170775287e+00 1.171832076468105194e-01 2.486491029640843564e+00 1.260482655459385648e-01 4.059319752305156070e-01 -5.956858707028535316e-03 1.199096929299429404e-01 1.248678625053845082e-03 4.047733064186829766e-01 9.793079756769386413e-03 5.932381167525677124e-02 -2.475451532886998457e-03 7.804951593565594425e-01 -3.015335398039294124e-02 -2.065953710478731550e-03 1.794252940578616382e-02
+3.237457542817643447e-02 1.560097899196016380e+00 1.101710796826544381e-01 2.421072694716483120e+00 1.342529033015319173e-01 4.473054430221816702e-01 -4.321418927177179264e-03 1.304129113505142490e-01 1.350929717884905013e-03 4.474908425623155828e-01 6.329012901206645947e-03 6.517335407775615153e-02 -2.568332880654382311e-03 8.259908637776820495e-01 -2.470326180773166347e-02 -1.879599560993519027e-03 1.738587176486946009e-02
+3.727593720314938130e-02 1.545542572083943966e+00 1.036093899612278157e-01 2.356120187280772438e+00 1.228918399554665974e-01 4.911395046954560861e-01 -1.644464513056727306e-03 1.417181886734582563e-01 1.233953045166377022e-03 4.916881760456623396e-01 2.223557066318652964e-03 7.198498858163970837e-02 -2.797588503884113412e-03 8.752730218901014769e-01 -2.141317462140131939e-02 -1.281647868885260524e-03 1.660475291361803579e-02
+4.291934260128778267e-02 1.531572602123358662e+00 9.728127487724623490e-02 2.283609481004460040e+00 1.135313148672207062e-01 5.366104690169182234e-01 6.711598535814773669e-04 1.540653255514338738e-01 9.678512345811188938e-04 5.369653061250165837e-01 -2.050408583142968033e-03 8.018165078823452641e-02 -2.908308329307534440e-03 9.279416482263004307e-01 -1.821674977036366605e-02 -4.063840395447936319e-04 1.568866817153680149e-02
+4.941713361323833015e-02 1.518341324472995302e+00 8.950734238211086047e-02 2.197861404801443985e+00 1.111765296966798489e-01 5.827091421608007193e-01 7.533135499223631015e-04 1.677295485913511963e-01 7.172473457791612300e-04 5.829455097283484655e-01 -3.057864349947345822e-03 9.018829008283218984e-02 -2.935030353911056641e-03 9.863701585885369383e-01 -1.249541199529381405e-02 8.499527604997306370e-04 1.472870517349008160e-02
+5.689866029018292998e-02 1.505307307266029904e+00 8.810148335699352629e-02 2.094792655510467227e+00 1.086382056511527999e-01 6.294815536387448063e-01 9.901955224021086882e-04 1.823584676492381318e-01 7.061995826628166689e-04 6.270243384803549924e-01 -1.840824609249589600e-03 1.019564162987246503e-01 -3.211644617809455982e-03 1.036754676663268926e+00 -1.089222945444151547e-02 1.635985800273112058e-03 1.478982758405596870e-02
+6.551285568595509312e-02 1.492338872126097726e+00 8.673219802223879060e-02 1.972943483478049043e+00 1.016059983620012930e-01 6.768664823281200782e-01 1.962040869776884161e-03 1.978628347794686160e-01 1.160841317695027533e-03 6.684809678354665108e-01 -1.080701264518587473e-03 1.155101083814724927e-01 -3.284794786471789338e-03 1.075629503163584744e+00 -6.736547651658684327e-03 1.735828455516659138e-03 1.592381199680020440e-02
+7.543120063354614990e-02 1.479666262045960856e+00 8.141922715679962563e-02 1.828797090456429908e+00 8.228479680656052009e-02 7.246611517554401027e-01 2.234599803021370193e-03 2.159454643005864760e-01 9.173334911300231459e-04 7.080199161214449921e-01 -1.115911766339468258e-03 1.303237510513822750e-01 -3.442816632195635704e-03 1.105939016479328574e+00 -3.621107408948803785e-03 9.190323300996633893e-04 1.504578981460611906e-02
+8.685113737513520948e-02 1.467067443862011933e+00 7.605632207591291050e-02 1.665052707183172664e+00 7.019163919958561204e-02 7.729086809360399268e-01 2.632184566432160810e-03 2.362269267061525013e-01 6.591315587753197175e-04 7.462538002564987893e-01 -1.809542377258981788e-03 1.465592044397492633e-01 -3.910815628155301857e-03 1.127803464297723046e+00 -2.163644186741874725e-03 -5.170461293543338402e-05 1.330416744770090282e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02 1.486650900000000108e+00 6.906649307998630194e-02 8.219631450000000061e-01 4.472744027894770547e-03 2.579254300000000111e-01 3.332937874284975371e-04 7.856684999999999919e-01 -2.671446105093899792e-04 1.654803099999999638e-01 -3.591407057756365248e-03 1.138711483999999885e+00 -2.740669875090178537e-03 2.390608399999999932e-03 1.207505639463098135e-02
+1.000000000000000056e-01 1.453523781400000159e+00 7.275140534180765672e-02 1.486650900000000108e+00 6.906649307998630194e-02 8.219631450000000061e-01 4.472744027894770547e-03 2.579254300000000111e-01 3.332937874284975371e-04 7.856684999999999919e-01 -2.671446105093899792e-04 1.654803099999999638e-01 -3.591407057756365248e-03 1.138711483999999885e+00 -2.740669875090178537e-03 2.390608399999999932e-03 1.207505639463098135e-02
+1.163265306122449050e-01 1.436257699431471968e+00 6.600913866769045346e-02 1.278363607352488662e+00 5.851715503791719841e-02 8.760245558745264338e-01 7.629865271835317708e-03 2.832269254732402985e-01 -1.758261191725733058e-04 8.293545442242232779e-01 4.533990903847666745e-03 1.891290223954812943e-01 -3.207517653019667861e-03 1.151017244962964980e+00 1.037318243192690559e-03 2.021799069942414612e-03 1.029101446289395344e-02
+1.326530612244898044e-01 1.415413753481877368e+00 6.242141770875467982e-02 1.093272570071426530e+00 6.495189916810374664e-02 9.207873942133834033e-01 1.087930043521478582e-02 3.053929728929584142e-01 -2.117451617313862583e-04 8.665748494692329107e-01 5.263570222115704703e-03 2.130861595075765058e-01 -2.462179552547698672e-03 1.159271728947077085e+00 2.573872010971296406e-03 2.194705307789258843e-03 1.011183417614163436e-02
+1.489795918367347038e-01 1.390723868801176488e+00 6.004590985567208328e-02 9.445472028682869725e-01 6.800362366808562764e-02 9.555260855881660964e-01 1.311758721701782038e-02 3.238159043446356411e-01 -2.168866785661948890e-04 8.997274462522860672e-01 6.573654107431214921e-03 2.373766657780576605e-01 -1.248902758435248284e-03 1.159782281727252728e+00 3.152171299635436741e-03 3.357213460984731675e-03 1.035859280245138986e-02
+1.653061224489796033e-01 1.361917477796027676e+00 5.855238285923737573e-02 8.244352867442255040e-01 6.036562576446031364e-02 9.811175998983059365e-01 1.436268096786381260e-02 3.408284603992981499e-01 8.299497311290288115e-04 9.262887531129980756e-01 7.024598183513044991e-03 2.626463541241408350e-01 -1.215334683369711719e-03 1.153505937395747249e+00 4.939946258408016086e-03 4.568155960819259183e-03 1.030152646918971841e-02
+1.816326530612245027e-01 1.328624546252491712e+00 5.909126204572044683e-02 7.266001875331934423e-01 4.804631794363146274e-02 9.982561169995157746e-01 1.670857810347954955e-02 3.529769485411317631e-01 1.813881046560281499e-03 9.465454900669407756e-01 7.708172608863731248e-03 2.861035556266112168e-01 -1.197192788463815639e-03 1.141451524305949761e+00 6.268876466634652408e-03 5.220820476545198416e-03 9.887806050869172442e-03
+1.979591836734694021e-01 1.291182364995507870e+00 5.799461876770821422e-02 6.460934389251358212e-01 5.956328597727415797e-02 1.005614729418596198e+00 1.927636214968469694e-02 3.625207622508588856e-01 1.753782301094962068e-03 9.579642078598071819e-01 5.947898850618089928e-03 3.058368520521582146e-01 -1.459083957057752401e-03 1.123688166046398207e+00 8.248278204087602233e-03 5.934829898366457586e-03 9.809191509951561971e-03
+2.142857142857143016e-01 1.250364307046662793e+00 5.729005890456270039e-02 5.754064633997411082e-01 5.481677133626022824e-02 1.004729452204575324e+00 2.069043281326496744e-02 3.667695907052059834e-01 2.146097855119245834e-03 9.635234739688496308e-01 3.361656410241223129e-03 3.219613004354836550e-01 -1.747063702520791628e-03 1.100877192739575072e+00 9.326223083890011062e-03 6.531736775880660727e-03 9.816070343834169126e-03
+2.306122448979592010e-01 1.206927192547645911e+00 5.632383651387896928e-02 5.137991377127972825e-01 5.361156055870076875e-02 9.964514094860162929e-01 2.040614599634831342e-02 3.692429999459841961e-01 2.928125248630514786e-03 9.633955920550227869e-01 1.826650838476845222e-03 3.344292412524122460e-01 -2.023823820231218130e-03 1.073872463500070706e+00 9.320583008029778571e-03 7.073477024250686418e-03 9.873718139427525159e-03
+2.469387755102041004e-01 1.161434662449474020e+00 5.575873697984527755e-02 4.552507567815178291e-01 5.065601192147932419e-02 9.817668338111157356e-01 1.840051143596008881e-02 3.696344609371332579e-01 3.230467079454401279e-03 9.575494535041556565e-01 2.250644860223596777e-03 3.450778886737556617e-01 -1.786909755980518356e-03 1.043560859401786445e+00 8.268252049006056398e-03 7.675799938353641827e-03 9.943834213998184110e-03
+2.632653061224490276e-01 1.114255548834317322e+00 5.499105653975491781e-02 3.996382053409509028e-01 4.755273215299854472e-02 9.616221603847134869e-01 1.591738512518598228e-02 3.687094707430881857e-01 3.658556769999159360e-03 9.455629882815539400e-01 4.706934512374225804e-03 3.532490117204427071e-01 -1.842688617728312390e-03 1.010507768078387603e+00 6.727057482457301368e-03 8.223640364209331907e-03 9.997248271147583798e-03
+2.795918367346938993e-01 1.065729549351170924e+00 5.406308092778352309e-02 3.411444192178936241e-01 4.570775803372454510e-02 9.368664327965776639e-01 1.306867091356006538e-02 3.648377142879627444e-01 3.339683048581209776e-03 9.277487215459292447e-01 8.390992685674466753e-03 3.575574858304151338e-01 -1.747100400113369215e-03 9.743783295117218746e-01 5.463220273594844231e-03 8.723184340334453474e-03 1.011600243934571673e-02
+2.959183673469387710e-01 1.016247959667206491e+00 5.288154012402173504e-02 2.941440729953202493e-01 5.179924323811337800e-02 9.077384819671754368e-01 1.086561841248540050e-02 3.614681618638949656e-01 4.009541118659402763e-03 9.047705543775533821e-01 1.166704002588283065e-02 3.589366151255772852e-01 -1.904646522060764700e-03 9.364607041507967411e-01 5.229266156563059167e-03 9.354520468648605883e-03 9.985168140465173342e-03
+3.122448979591836982e-01 9.661707426172632118e-01 5.305346802331420419e-02 2.601290037822677204e-01 4.622670247373922986e-02 8.753125791436111580e-01 1.014237327275376715e-02 3.562591328441856442e-01 3.252942299916778898e-03 8.792659459325451454e-01 1.373745204075244551e-02 3.576319049134298877e-01 -2.463442409355897852e-03 8.954439656483303978e-01 6.817122484342049218e-03 9.938869133245136628e-03 1.003625880427465485e-02
+3.285714285714286254e-01 9.157971486255552795e-01 5.662310358643761476e-02 2.302641998343407104e-01 4.244869960571524825e-02 8.405443498693302429e-01 7.664957427717792628e-03 3.452605536981402379e-01 2.300327065724727324e-03 8.482835465847984313e-01 1.393058433297254296e-02 3.543977802625193352e-01 -2.605198663765615798e-03 8.529476078765321212e-01 1.228989874997294014e-02 9.944464743138389448e-03 9.923830319159009788e-03
+3.448979591836734970e-01 8.653596641887024710e-01 5.083797877077313176e-02 2.048947603867307399e-01 3.692175416096520058e-02 8.034485871899764708e-01 8.234582604494007640e-03 3.396375617630511545e-01 3.377340007886578920e-03 8.103442551639935543e-01 1.622016117467551741e-02 3.490851536982226833e-01 -7.624224416734248430e-04 8.094105788772453236e-01 9.422659712189892026e-03 9.973770093789352878e-03 9.734503659314653473e-03
+3.612244897959183687e-01 8.150424664509627881e-01 5.361559377675658572e-02 1.820567154916601094e-01 3.491710247794299615e-02 7.645300106285628505e-01 6.934557417598144480e-03 3.341288504450065644e-01 2.697371197079972624e-03 7.660321149119501127e-01 1.350631075750425951e-02 3.420271078042709800e-01 -2.663121160636718401e-03 7.652022029235848732e-01 1.475014803148823675e-02 1.038996123676954632e-02 9.343466147424542984e-03
+3.775510204081632404e-01 7.650336795089378583e-01 5.504084064016197264e-02 1.654109241587360668e-01 2.958093259719593235e-02 7.234981658492036782e-01 8.142519083330676533e-03 3.264499482306293388e-01 3.058979013150607204e-03 7.174059443946417813e-01 8.934531421016229588e-03 3.324091991918894928e-01 -5.429737640515337402e-03 7.209794008570916679e-01 1.343544728946480075e-02 1.022449502788700705e-02 9.067401904482772640e-03
+3.938775510204082231e-01 7.155107322841964823e-01 5.815762181981395940e-02 1.480349508988084706e-01 2.827395372375652097e-02 6.797301550560850281e-01 7.301918226727460715e-03 3.176871303691626847e-01 2.507018641180281358e-03 6.693607196533725157e-01 5.868688883468267670e-03 3.205915700716052696e-01 -7.710384988422315164e-03 6.761035453466663903e-01 1.424031491769873342e-02 9.687755045023086972e-03 8.708677675773364088e-03
+4.102040816326530948e-01 6.666148092756788435e-01 5.388739088185211246e-02 1.325798271673375717e-01 2.828630531659322850e-02 6.347498261678146125e-01 7.564475821368022400e-03 3.051681204691203964e-01 3.199912002429109933e-03 6.220495054927962508e-01 7.117667135200277453e-03 3.076833015555935913e-01 -9.268378632880202098e-03 6.303581793950283618e-01 1.208802642013349048e-02 9.162324623002576079e-03 8.366970908929851139e-03
+4.265306122448979664e-01 6.184808244113076947e-01 5.672644041609450211e-02 1.182493409930097245e-01 2.916021507299379825e-02 5.902870051375984639e-01 6.476244337488319333e-03 2.916682104815467858e-01 2.974320558817333468e-03 5.735828503298960879e-01 5.598590526945397550e-03 2.931440885295590881e-01 -9.289541957659729413e-03 5.824717232695867386e-01 1.567762681743947381e-02 9.151804666231467059e-03 8.160535659671936878e-03
+4.428571428571428381e-01 5.712833756315996991e-01 6.581076428913901211e-02 1.066391781871470490e-01 2.847724416602226416e-02 5.469390334167323342e-01 6.503510318900787394e-03 2.806805010781118770e-01 2.902858707303770588e-03 5.234709192018245361e-01 3.726986947742665437e-03 2.776505733554623379e-01 -1.212442063030644800e-02 5.359270605702105561e-01 2.373088270821131773e-02 9.399011025721361323e-03 8.620527635960733084e-03
+4.591836734693878208e-01 5.252968102940863693e-01 6.702217841793813025e-02 1.011350726109566528e-01 3.463682731939739246e-02 5.054220839413947619e-01 7.157137997245991562e-03 2.585982451427741302e-01 3.467688305903465773e-03 4.737391775997533694e-01 3.792968907112018173e-03 2.618972221858207328e-01 -1.416672222868416153e-02 4.926155228680057507e-01 2.277983998031588367e-02 8.823754218570113023e-03 8.056682849491073745e-03
+4.755102040816326925e-01 4.808391912274195823e-01 6.858931665301323810e-02 9.174674898459940497e-02 3.317719061506606976e-02 4.655735881486708361e-01 6.151644386349899504e-03 2.413939489550236128e-01 4.081741466172366176e-03 4.275087629415363910e-01 3.444260923021804158e-03 2.451230091093854757e-01 -1.350860278242318330e-02 4.511776631645700864e-01 2.119289716436993654e-02 7.824004843225812814e-03 7.981915893287276631e-03
+4.918367346938775642e-01 4.381130722623843288e-01 6.718507026803152704e-02 8.739403248853304385e-02 3.458909252220616903e-02 4.273494270197562561e-01 5.741698175921050237e-03 2.294557993863459988e-01 4.240931700175741906e-03 3.851674631693069450e-01 3.963425607039449544e-03 2.290377420641576400e-01 -1.413445531461066280e-02 4.101274867609975283e-01 1.857514540571357162e-02 7.016749592106577386e-03 7.892538800058142290e-03
+5.081632653061224358e-01 3.972125375443600337e-01 6.965800777247084519e-02 7.986314864670505942e-02 3.453373395965902981e-02 3.907562983066515394e-01 4.228438900749689408e-03 2.219616674650559951e-01 3.788903873166361846e-03 3.463307418523625669e-01 3.472681758940058400e-03 2.135512008039863463e-01 -1.453468428403530480e-02 3.688274588658959630e-01 2.153776701042124306e-02 6.598559841394757187e-03 7.539189377298772046e-03
+5.244897959183674185e-01 3.581097245266221529e-01 6.844653703788193544e-02 7.216510147179601431e-02 2.968070892373265643e-02 3.555452126726142970e-01 4.476964374474977013e-03 2.085937770482449993e-01 5.029172127092289973e-03 3.107052457833348602e-01 4.210241830234665722e-03 1.984985872720557709e-01 -1.397634559474913822e-02 3.299857352693796320e-01 2.057477160238923436e-02 6.900297183520782453e-03 7.420117289685977842e-03
+5.408163265306122902e-01 3.211058522457649334e-01 6.801459714625253394e-02 6.645926254942295663e-02 2.713087028010289364e-02 3.216816336632508655e-01 5.101654155733479669e-03 1.932228583245677311e-01 5.421371354795442507e-03 2.776810767211733144e-01 4.434284480829685238e-03 1.841811580961135175e-01 -1.309493151133219596e-02 2.947601101555517578e-01 2.147811573443057787e-02 7.099558957836545926e-03 7.396966799181269384e-03
+5.571428571428571619e-01 2.864888840811441462e-01 6.623243611783007079e-02 5.877459162780401120e-02 2.555514304330760988e-02 2.893248444780310846e-01 5.733630698988551912e-03 1.819244310888655236e-01 4.698266066874413546e-03 2.457190340297982223e-01 3.354360902640613970e-03 1.704403134171777467e-01 -1.406728234143836236e-02 2.622506614519226620e-01 2.184512970481299748e-02 6.790721355450353609e-03 7.098078746040513073e-03
+5.734693877551020336e-01 2.542592546856339286e-01 6.508281122168510513e-02 5.338086792246633794e-02 2.502004244557643961e-02 2.586220812087101084e-01 5.062289292442913834e-03 1.686736884711234896e-01 3.290103082771974122e-03 2.181877137322736082e-01 1.503434250308174902e-03 1.562671367343420326e-01 -1.400188817532591751e-02 2.319658116797040648e-01 2.164059264137865174e-02 5.384298384279484137e-03 6.473503045793584446e-03
+5.897959183673470163e-01 2.245039172586136000e-01 6.817903370165791233e-02 4.876099631660012895e-02 2.707226383643828588e-02 2.295356732539315381e-01 4.546381244334137504e-03 1.494528926901955990e-01 1.731732789425769262e-03 1.937577703554055342e-01 8.774567316059845440e-04 1.418647467121848249e-01 -1.070000414607550666e-02 2.038300728024848851e-01 2.499849853906035979e-02 3.669951832429759093e-03 6.640741958232684525e-03
+6.061224489795918879e-01 1.971904898479003854e-01 6.724857957251778595e-02 4.207712352396885541e-02 2.305135101626111169e-02 2.020682303291666815e-01 4.700315925673483226e-03 1.353149002042534077e-01 1.073033203524085752e-03 1.712446205776504971e-01 1.392177204712607627e-03 1.278475110007968318e-01 -8.516135160069529877e-03 1.779961534041439308e-01 2.436415844152281518e-02 2.869011282850813822e-03 6.458241817368222566e-03
+6.224489795918367596e-01 1.722555534350652118e-01 6.682717720305531017e-02 3.580485555936888109e-02 2.185199910362296480e-02 1.770966967495726796e-01 4.545380858232235186e-03 1.196382305481427932e-01 5.675822726855447864e-04 1.503386543935546216e-01 1.187608494908782078e-03 1.138655403828967849e-01 -5.058930938126255175e-03 1.545427884236979243e-01 2.516110110112216094e-02 2.511121700851877674e-03 6.031679775970608809e-03
+6.387755102040816313e-01 1.496087563872702808e-01 6.578562391334155657e-02 3.021147633166515814e-02 2.208234850326066048e-02 1.543235307319828775e-01 3.719669291221451568e-03 1.099870114073757815e-01 1.099755908800202631e-04 1.307211636005658761e-01 -2.569527151626404410e-04 1.001273020151677018e-01 -4.123350891244225574e-03 1.335632662539781024e-01 2.590675883781437538e-02 3.022869112719256315e-03 5.836083715696130554e-03
+6.551020408163265030e-01 1.292460451082127204e-01 6.357257318602752005e-02 2.484776981462179582e-02 2.198756275659718984e-02 1.327929417946560886e-01 3.262410228304992409e-03 9.857771370782425491e-02 -4.010140583726017754e-04 1.133059520694710415e-01 -1.572737721869544442e-04 8.643471418070733381e-02 -4.074131125049942556e-03 1.152348253417438545e-01 2.597207634522478992e-02 2.751824022379022617e-03 5.287353443280242218e-03
+6.714285714285714857e-01 1.111797350094870934e-01 6.506130007688437633e-02 1.988113039811458330e-02 2.200991807516783014e-02 1.133598162492658462e-01 2.783517477014238982e-03 8.728924623054287735e-02 -5.498324803594051291e-04 9.865830665947733502e-02 4.856473648199733512e-04 7.191910637408532869e-02 -4.635433453179224617e-03 9.875996651570564944e-02 2.776972279037007857e-02 2.126691438879552924e-03 4.706042950694613002e-03
+6.877551020408163573e-01 9.523361075388962760e-02 6.047832185377364361e-02 1.400636390703255754e-02 2.365106623106593212e-02 9.675785167022572797e-02 2.667898688008315394e-03 7.932002060431865564e-02 -1.006125028624939299e-03 8.358483896113802492e-02 7.396423703803483435e-04 5.815207638918958727e-02 -4.533647393946745041e-03 8.407398278488814602e-02 2.090721250125920222e-02 1.378634377812767509e-03 4.302552030458201351e-03
+7.040816326530612290e-01 8.122659700001326477e-02 6.162435588265317843e-02 1.145974167737420432e-02 1.887010255106317397e-02 8.187050455239161528e-02 2.400835956446172731e-03 7.048546113961080828e-02 -1.296709283692231221e-03 7.082531695668828342e-02 5.281490075120173766e-04 4.612698979303683777e-02 -3.528400028220969945e-03 7.106167117549450074e-02 1.884473803010927595e-02 4.801378255429795180e-04 4.188115301054883347e-03
+7.204081632653062117e-01 6.893218067535411553e-02 6.606274324296279588e-02 7.837588375768960586e-03 1.719052997285148640e-02 6.602299540318257165e-02 1.746335188707009989e-03 6.295347641224970092e-02 -1.016031134913187098e-03 6.232248730722956404e-02 -1.595583001111778448e-04 3.750154254323265907e-02 -3.197032357921831755e-03 5.909780060400191687e-02 1.396685124656719609e-02 -2.405201575052475134e-05 4.107351465178039722e-03
+7.367346938775510834e-01 5.814267735253492858e-02 7.631079155029231453e-02 5.019045671291559423e-03 1.703959974523763451e-02 5.190213556870747202e-02 1.667785682315762671e-03 5.302574765414624913e-02 -9.547079292404926643e-04 5.817900121811786507e-02 -2.416162017449992316e-04 3.051268156801009712e-02 -1.996130669194155424e-03 4.837950241516492111e-02 7.232931761620698535e-03 -3.026026130038219253e-04 3.824171123570902983e-03
+7.530612244897959551e-01 4.869566766134052205e-02 7.742097813387080474e-02 3.380609462740184409e-03 1.479054695636173103e-02 4.021748440642871486e-02 1.635198177865507946e-03 4.434534000993487629e-02 -4.222279995945016351e-04 4.997561476490473831e-02 -6.147511373023085979e-04 2.418628742139881244e-02 1.089112760743771516e-03 3.878217089483680657e-02 3.172534007081652396e-03 -4.110413969063750187e-04 3.709084790485463190e-03
+7.693877551020408267e-01 4.044250240680334618e-02 7.505508510155267654e-02 9.324618436404591567e-05 1.621917485475198040e-02 3.209025564326395408e-02 1.754316217031409025e-03 4.040000792708368232e-02 1.790811079042664566e-04 4.429882453685759364e-02 -6.119451888814027296e-04 1.892921581226242669e-02 -9.156765312932045597e-05 3.091164592705523267e-02 -1.811811273991242865e-03 -4.659984273595200793e-04 3.625770725960355580e-03
+7.857142857142856984e-01 3.327547602901389373e-02 7.594730521129608447e-02 -1.538774680937169846e-03 1.427911879458286688e-02 2.585250344320047869e-02 1.818274116936699838e-03 3.768412261083003284e-02 5.076365411593800059e-04 4.066132817562254087e-02 -4.103494689009987151e-04 1.498062986787362071e-02 -1.567888516314374855e-03 2.478933121691693359e-02 -7.057285602139504871e-03 -4.054769709337468055e-04 3.274674784559306398e-03
+8.020408163265306811e-01 2.710706020503510574e-02 6.967577716341799565e-02 -3.292689738769052563e-03 1.302317972988607450e-02 1.859930309235167598e-02 1.440673587944269612e-03 3.027078564631083962e-02 5.499912192063957650e-04 3.414662672179843983e-02 -8.418719129241747368e-04 1.205208052239571496e-02 -7.717307690063845982e-04 1.955018661238028949e-02 -9.803192065236304709e-03 -2.850385614523395263e-04 3.066331520608849698e-03
+8.183673469387755528e-01 2.182777327792014693e-02 6.432099519023265488e-02 -4.768501625455401523e-03 1.556994705567193819e-02 1.346632027765258706e-02 1.215897313679950575e-03 2.510188890796036243e-02 6.099389384973605277e-04 2.826004328751220360e-02 -8.105932931825078624e-04 9.719048837170184835e-03 -8.520002326649605412e-04 1.531750868718114930e-02 -8.836503694312693169e-03 -2.277311817526648716e-04 2.897488817883252590e-03
+8.346938775510204245e-01 1.732718253187195967e-02 5.710674947523330935e-02 -5.230190570117304073e-03 1.414505616898533748e-02 9.667061410177868463e-03 1.098713889135851177e-03 2.109925377869088445e-02 3.205401471248325168e-04 2.465244783664412298e-02 -4.693055364111756167e-04 7.902985505951989897e-03 -8.459919359694485774e-04 1.191668445291303551e-02 -7.980204458145706559e-03 -2.518029966257508655e-04 2.439864690347969683e-03
+8.510204081632654072e-01 1.350721403879344071e-02 5.190565017544805648e-02 -5.504081810805650996e-03 1.310886165854219070e-02 6.733249885862054029e-03 9.985308212159260677e-04 1.750471058246003825e-02 3.773376656582864627e-04 2.128855753071882961e-02 -1.004985303370321825e-04 6.098078701810978955e-03 -7.748156400943020938e-04 9.051946361661511453e-03 -8.472351544024037118e-03 -2.539742218870363779e-04 2.011351503123325751e-03
+8.673469387755102789e-01 1.028418426810574250e-02 4.576728749949458830e-02 -5.565775093366548236e-03 1.283554913783929025e-02 4.661214622425208925e-03 8.824090909300211227e-04 1.371875731792381557e-02 4.023935127803295042e-04 1.742398657884901975e-02 -1.376806251658334312e-04 4.597830936409629324e-03 -7.584395675897092487e-04 6.748583785838472258e-03 -9.243666320254941698e-03 -2.282090620477647091e-04 1.617226165581661483e-03
+8.836734693877551505e-01 7.589738733240949248e-03 4.101680475501470746e-02 -5.301125605810848816e-03 1.208554103662637380e-02 3.395664268329615099e-03 7.578399836952638691e-04 1.135151875260490516e-02 2.739934372502006096e-04 1.527690382772810447e-02 -2.119651147850891426e-04 3.303316269833258753e-03 -1.187778836739436863e-03 4.891974298810318825e-03 -1.233570620581838501e-02 -1.624589630119514685e-04 1.292898474252045194e-03
+9.000000000000000222e-01 5.365500585267322931e-03 4.142048728873987118e-02 -4.864826018318149127e-03 1.070561871116822161e-02 2.906583183801734593e-03 3.342223288867705866e-05 9.921337035406383245e-03 -3.139617846961637038e-04 1.476776805162933331e-02 -8.138580850603621686e-04 2.478024997064359140e-03 -1.682045282580753136e-03 3.465658109240731845e-03 -1.309887794038555250e-02 -5.209738777082163698e-05 1.235071236380816525e-03
diff --git a/examples/qPDF/qPDF.ipynb b/examples/qPDF/qPDF.ipynb
new file mode 100644
index 000000000..3af96d3ee
--- /dev/null
+++ b/examples/qPDF/qPDF.ipynb
@@ -0,0 +1,351 @@
+{
+ "cells": [
+ {
+ "attachments": {
+ "image-4.png": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgwAAADUCAYAAADwdHtkAAAABHNCSVQICAgIfAhkiAAAIABJREFUeF7tnQn8DdX//498kS0SEQqJRFR2Uookqai+ZQtFJRUp7VSKtPEtaVERkmzJkmSJQiJLSUUL2UpRUdb4Yv7ndX7f+fzvZ+6ZuXOXmTtz7+s8Hp8H98yZszzPzJn3eZ/3eZ88hgyCgQRIgARIgARIgAQcCBzncI2XSIAESIAESIAESEARoMDAB4EESIAESIAESCAmAQoMMRExAQmQAAmQAAmQAAUGPgMkQAIkQAIkQAIxCVBgiImICUiABEiABEiABCgw8BkgARIgARIgARKISYACQ0xETEACJEACJEACJECBgc8ACZAACZAACZBATAIUGGIiYgISIAESIAESIAEKDHwGSIAESIAESIAEYhKgwBATEROQAAmQAAmQAAlQYOAzQAIkQAIkQAIkEJMABYaYiJiABEiABEiABEiAAgOfARIgARIgARIggZgEKDDERMQEJEACJEACJEACFBj4DJAACZAACZAACcQkQIEhJiImIAESIAESIAESoMDAZ4AESIAESIAESCAmAQoMMRExAQmQAAmQAAmQAAUGPgMkQAIkQAIkQAIxCVBgiImICUiABEiABEiABCgw8BkgARIgARIgARKISYACQ0xETEACJEACJEACJPAvIiABEiABEgg2gXz58okjR44Eu5Kydv/617/Ef//738DXkxVMjEAeQ4bEbuVdJEACJEACfhDIkyePCMNQHZZ6+tFnmVgGlyQysVfZJhIgARIgARJIMQEKDCkGyuxIgARIgARIIBMJUGDIxF5lm0iABEgghQQ+//xzMXPmzBTmyKzCSMAzgWHdunVi0KBBSTHp37+/WLVqVVJ58GYSIAESIIHECRw7dkz06NFDHD16NPFMeGdGEPBMYNi2bZtYvXp1UpAqVKgghg8fnlQeyd4M62QY8vAvNwNwYSABEsh8AmPGjBGFChUSbdu2zfzGsoWOBNK2rXLGjBli3LhxomzZsmLPnj2icOHC4vHHHxclS5bMqXD79u1Fv3791PUTTjjBsSFeXcRWpjBYJ3vVfrt8IUAxkAAJZDaBgwcPqnF5woQJatLEkN0E0iIwDBs2TLz55pti0aJFonjx4qoHxo8fL+rWrStWrlwpSpUqpeKKFi0qWrdurR5W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QAIkEFwCnhk9BrfJrBkJkAAJhIsAdophnTvogTvagt5DydWPAkNy/Hg3CZAACXhOAK70GUgg3QS4JJHuHmD5JEACJEACJBACAhQYQtBJrCIJkAAJkAAJpJsABYZ09wDLJwESIAESIIEQEKDAEIJOYhVJgARIgARIIN0EKDCkuwdYPgmQAAmQAAmEgAAFhhB0EqtIAiRAAiRAAukmQIEh3T3A8kmABEiABEggBAQoMISgk1hFEiABEiABEkg3AQoM6e4Blk8CJEACJEACISBAgSEEncQqkgAJkAAJkEC6CdA1dLp7gOWTAAmQQAwC+fLlE0eOHImRKv2XcZYE3Vinvx+8qgEFBq/IMl8SIAESSBEBCAuGYaQoN++yCcMBWd61PvNz5pJE5vcxW0gCJEACJEACSROgwJA0QmZAAiRAAiRAAplPgAJD5vcxW0gCJEACSRH46KOPxIQJE5LKgzeHn4BnAsO6devEoEGDkiLUv39/sWrVqqTyyKSb16xZI8aNG5dwk9566y2BfmEgARIgAbcEYMR4++23i1KlSrm9hekylIBnAsO2bdvE6tWrk8JWoUIFMXz48KTySNfNe/fuFYsWLRKQzN2EQ4cOiZUrV9omXbJkibjtttvEtddea5vm77//Fg8//LC46667RN++fcWePXtypb3++uvFfffdJ1asWGGbBy+QAAmQQCSBV155RVStWlVccsklBBMQAu+++674888/c2pz8OBBgQmh50Fa3noS5syZY7Rt29Y27+nTpxvy42f06tXL6Nq1qyElWOP333/PlV5+8Awp1RryQ2ibj9cXZAfEVQTqKj/YxkknnQSTZvUnBSfHPNDOc88917j11lu16aTwZZxxxhnGhg0btNcRiWuVKlUy5s+fr9JIQcto06ZNVPpffvnFkC+/8dtvv0VdiyciXi7x5M20JEACuQmk633bvXu3Ubp0aePrr7921SXpqqerygUkkY6RLs6pui1atDA+/fTTnCQY/2vWrOl0S0queaZhkABsw7Bhw8Sjjz4qRo4cKV588UUxZswY0bhxY1G3bl0hhYac+4oWLSpat24dqrWzDz74QHTs2FHs3LlTNGzYUGCbETQlTgHSIpYbrrnmGm2y3r17C/xVrlxZe/2vv/4S8gES3bp1y5kFNGnSRMyYMUNs3bo11z1ly5YVN998s7j33nu1eTGSBEiA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"
+ },
+ "image-6.png": {
+ "image/png": "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muWy0GKs8/XXXzdbbrml2XrrrYsxPGNCAAIQgAAEIAABCECgpAn89NNPNqpp9erVgYoC/vbbb+aTTz4x++yzj+u63nrrLTNgwADz6quvup7P98Hly5ebYcOGmVWrVpmaNWuawYMHm/r16+d7WNv/iSeeaE444QQ7ZuvWrQsyJoMUj8BLL71knn76adO0aVOb8my//fYzZ555ZvEmxMieBEhL54mGExCoNgE8NauN0LsDGTWz8dIs111lb4LeZ9iJ9mbDGQhAAAIQgAAEIFDqBFasWGEuvvhiu/l/yCGHmPbt22dM2SQDqAw3999/v+vy5s+fb8444wzz73//29StW9e1TT4PKvXUHnvsYQ477DBz22232bX16tUrn0NW6Xu99dYz9913nznllFPM999/X7BxGajwBGQkk/F65MiR5tprr7V6f+GFF8y//vWvwk+GEX0JkJbOFw8nIVBtAhQKSkGol86YMWPszmqtWrWMkrSqWpZ2hQ844AC7E+bIxIkTjb6Q1a5d26xZs8a2Oeigg0yTJk1sk+22285cddVV5rTTTsuoqORdZa8vYcXc+c24gAI3+PXXX0PbiSYRr7/y9Bzo/kZKm4DeWfJyySToMxOh/JwPqp/8jJ7/XnmP5p9xdUZAP9Whx7VBCWR7nz3++OOmRYsWZu+99zb777+/effdd83ixYvNxhtv7DnkQw89ZI2W48ePNx07dkxrd/zxx1uHggsvvDDtXL4PLFu2zOy55552fgMHDrTDyWFBRs6vv/7aRm8VSoYPH269WasTgp+tPgu1tlIap5iMjj76aHu/DRkypBKJng95CX/55Zc5YSrmenKacMCLSmFd8qSVHeH//u//coriLIU1BMSddbM4ry1rGH9eAJPsyOGpmcKroqLCHpkxY4bRh8VRRx1ltBO2cuXKNLIyYupLldrcfvvt9ouYbkDJwoULzcyZM+3OcyYJsqtc7J3fTGso9Hl2ogtHXIYyPRf8K20GQQyaumvQZ3H0GFQ/hXuyGQkCEIBAcQmcdNJJNkRaeeh/+OEHs8suu/gaNDVbeaNtuOGG1hMyVd577z0zadIkW6SzGCKvU+XSV+i7I5tsson937fffrugU5JR97nnnrOGYiR+BPSdXPfURhttVGVxut+++uors2jRovgtOuIrIi2dtwK18S8bCv/+YiAmSHAC0Ephte6669p8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RYkMtQqJUDh1sVx99dVGYflu4oTDF/KIup2u6bFXX33V6lOh/oXcmWVjOaH0q6yySnMqABn5ilOHrLvuutZA/Je//MV6wCpE/bzzzivry0/nZY1LDsybN6+MqZpE0bsMdL17927uWd6f0q28S72MmvXUTRCTJM7/5Cc/sXulkEe3bDh5bOveRyAAAQhAAAIQgAAEIFANgWqdZ4pTjMm5Qt/zq00xVs16uDZ+Ahg142dKjxBoOAIKEe7fv781BJUa6BSaLE84haEecsghgWxkXJKhSvkM3WTgwIFGBkVHzj//fNu3WyXmuXPnmlmzZoUyaspw99e//tWcddZZRnl/NGcZbPQ0z01kKC0UFTJ33HGH22nzzDPP2Grhqhouufzyy023bt1aGHVfe+01azRVPkc3UW5PffCWGkLd2sZ9zFmXU7m9tH/H2Ke8ko4oJLk417HC5tXulltusTxk2CzNhaprg3ReOnbx608//dQceOCBLZpE0Xtp3/JQHTx4sJ3TmDFjSk83v66nbjwnlfCJww8/3Dz33HPNlc733ntvM27cOPP000+blVdeOeHZMBwEIAABCEAAAhCAQN4IxOE8oxRjL774oo2Omzhxotlwww3zhqmh14NRs6HVz+IhEB+BSZMm2fBzGQT1FGz69OnWiLXPPvtYbz8ZC8OIvLw6d+5sDSNuouTPixcvtkbCE0880Ro/p06dajp06FDWXCHIPXr0MCoiESQypK633notmsnYeNttt9k8nqWy++6721AGGevcRHk/N9lkE1vk5txzzzUdO3a0Rl95OMqLTQazG264wddwpvyT4hE2p6bbPKIeU/GdHXbYwT7B1JyVuF15UBU6XywqpLTNNtuYOXPm2C8I8jhVuLY8+BzRk1UV8pGByynC889//rNsSkE6L7ug6ECnTp3K9BtF78V9S8+FyvRmu+22s8ZoNw9Vp309dOPHoV7nlCdVxaIk8ko+9dRTfQuB1WuejAsBCEAAAhCAAAQgkD0CcTnP1DLFWPao5mvGrQpeKfkra/dfHZEYP183a61Ww33iTzYqHxntZs6caT02VUFaT9fcQlT9RpWH4/z5820BGDeRN+Arr7xi81MqH6OXHHvssUbh6TK4yvDmZxyUAVPh6xdffHFzdwprv/TSS43yXrqFIavv008/3YwfP95rCnZcGQedEHLNXblhZABWjlA/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3dr07NnTrL/++qGxrL322mbXXXc1I0eODH1NNQ1btWplunbtao2Oku7du9u/Tp062T95nW699dZmxIgR5rrrrrNG2iii/tdcc02z2WabtbjM63iUvp22N998szUcy+v07rvvrqSLRK5ZunSp9dxFIACBbBDAqJkNPTFLCECgDgQUonT00Uebs846q6LRhwwZYk4++WTz6quvVnR9tRd9/fXXZuzYsWaPPfYwBx54oDnhhBPsF/nBgwebF154odruY7leT+r32Wcf07t3b6OQJAQCEIAABCAAAQjUgsC4ceOsl6aXyIAXRU488URz0003GXkgJiVh5jhw4MCK81Z69e91PMq6H330UXPttdfaS/70pz9FuTTRto8//rj5/vvvEx2TwSAAgcoJYNSsnB1XQgACOSeg8J7TTjutOedS1OW2b9/e5mw69thjzeLFi6NeXlV7hc0oNOndd981/7+98w6Rq+zi8Pt9foqixNhA/CNK1CiKgkElRmMvYIIlBlTsEkUFO3asiQ2sxFiwhCgWNAZLTFSw98Q/LBiNikZFYwOjBrH8cb/3eb/vDnfvzs7Mzu7M7s48BxZ37tz7lucGPHve8ztn3rx5SUrFyf2zzz4bjj766DB16tRw/vnnByRXQ2lkHdx+++0pc2L58uVDuRTnloAEJCABCUigQwksWbIkLF26NEyePHnQdrjJJpukQ1kCm0NtHBLj+2Fknm600UZDvaQe83OYTpYmNUh32mmn8NxzzyUJ+nAzEgLM0hxub8X1SKA2gf/U/tpvJSABCXQngb4kSv2lUZQnEbxrhy1btixJosgUve6663pNSeYmwU0kRn/++Wfdgvi9BhjEC0VZ00cffTSIIzuUBCQgAQlIQAIS+B8Bgmi77rpr4MC5ni1evLiSSfjPP/+EKVOmhIMPPrjqYzQWWrhwYToEH0qjBiY1MfM6kNTTRJlD4x9qfuZ1QmmKdMMNNyQJOHL7W265pS3Lnjt3boXR8ccfHwgyEwy+4IILes3PPi688MK09q222irMnDkz3HbbbeHnn38O3377bdhjjz1SXdE8e/Tzzz8Pd911Vxg9enQ6JP/777/TfbNnzw4bbrhhZXzW8Oabb4ZRo0aFFStWhHHjxoWzzjorfcZIRLj88stTQgC1P0866aR0HaaonXJD4v/rr7+mj/x7IojMnPyccMIJKdCtSUAC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"
+ }
+ },
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Determining the proton content with a quantum computer\n",
+ "\n",
+ "Code at: https://github.com/qiboteam/qibo/tree/master/examples/qPDF.\n",
+ "\n",
+ "In this tutorial we show how to use the `qPDF` model implemented in Qibo to create a set of Parton Distribution Functions (PDFs), parameterized by a variational quantum circuit. In the context of High Energy Physics, parton distribution functions estimate the momentum fraction of the proton carried by partons i.e. quarks, antiquarks and gluon. Here we simulate a quantum computer to encode within a circuit the data from these PDFs in such a way that, if we measure the output of the aforementioned quantum circuit, we obtain the corresponding PDFs values.\n",
+ "\n",
+ "In order to accomplish our goal, we use a Variational Quantum Circuit (VQC): \n",
+ "\n",
+ "![image-4.png](attachment:image-4.png)\n",
+ "\n",
+ "### Circuit\n",
+ "\n",
+ "We consider two different Ansätze. Those Ansätze depend on tunable parameters and a variable $x$ that also serves as the independent variables for the PDF $f_i(x, Q)$, where $Q$ is fixed. \n",
+ "\n",
+ "The first one is the Ansatz _Weighted_. Its basic single qubit gate is \n",
+ "\n",
+ "$$\n",
+ "U_w (\\alpha, x) = R_z(\\alpha_3 \\log(x) + \\alpha_4) R_y(\\alpha_1 x + \\alpha_2).\n",
+ "$$\n",
+ "\n",
+ "The second Ansatz is the fourier one, whose basic single-qubit gate is, on demand\n",
+ "\n",
+ "$$\n",
+ "U_f(\\alpha, x) = R_y(\\alpha_4)R_z(\\alpha_3)R_y(-\\pi/2 \\log x)R_y(\\alpha_2)R_z(\\alpha_1)R_y(\\pi x)\n",
+ "$$\n",
+ "\n",
+ "Both Ansätze have a layered structure with entangling gates among different qubits depicted in the following circuit\n",
+ "\n",
+ "![image-6.png](attachment:image-6.png)\n",
+ "\n",
+ "The Ansatz is constructed in one qubit per parton. We only let one and all flavours $(s, \\bar s, c, u, \\bar u, d, \\bar d, g)$ to be optimized simultaneously, thus only circuits with one and eight qubits are available.\n",
+ "\n",
+ "### Cost function\n",
+ "\n",
+ "The cost function driving the optimization process of this circuit is defined through several pieces. First, we need a Hamiltonian to measure. We choose a different hamiltonian for every parton, namely \n",
+ "\n",
+ "$$\n",
+ "Z_i = \\bigotimes_{j=0}^{n} Z^{\\delta_{ij}}.\n",
+ "$$\n",
+ "\n",
+ "This family of hamiltonians allows for the definition of their expected values, depending on $\\theta$ and $x$\n",
+ "\n",
+ "$$\n",
+ "z_i (\\theta, x) = \\langle \\psi(\\theta, x) | Z_i | \\psi(\\theta, x) \\rangle.\n",
+ "$$\n",
+ "\n",
+ "The relation between the $z(\\theta, x)$ quantities and PDFs is\n",
+ "\n",
+ "$$\n",
+ "f_i (x, Q_0) = \\frac{1 - z_i(\\theta, x)}{1 + z_i (\\theta, x)}.\n",
+ "$$\n",
+ "\n",
+ "Using this definition, we can just use the usual Pearson's chi-squared quantity\n",
+ "\n",
+ "$$\n",
+ "\\chi^2 = \\frac{1}{N}\\sum_{i=1}^N \\int_{x\\in[0, 1]} dx \\frac{\\left( f_i (x, \\theta) - \\frac{1 - z(x, \\theta)}{1 + z(x, \\theta)}\\right)^2}{\\sigma^2}.\n",
+ "$$\n",
+ "\n",
+ "This is the loss function for our minimization procedure."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Code\n",
+ "\n",
+ "First, we must decide the variables for our problem. The meaning of them are\n",
+ "\n",
+ "- `ansatz`: Which one is chosen, *Weighted* or *Fourier*.\n",
+ "\n",
+ "- `multi_output`: If *True*, all partons are fitted in the same circuit.\n",
+ "\n",
+ "- `parton`: which parton is to be fit. Ignored if `multi_output = True`.\n",
+ "\n",
+ "- `mode`: if *full*, data is fitted for $x \\in [10^{-4}, 1]$, if *partial* only large $x$ is considered.\n",
+ "\n",
+ "- `layers`: number of layers."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Create a qPDF model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# import requirements\n",
+ "import numpy as np\n",
+ "from qibo.models.hep import qPDF\n",
+ "\n",
+ "# our setup\n",
+ "ansatz = 'Weighted'\n",
+ "multi_output = True\n",
+ "parton = '8flavours' # or gluon\n",
+ "mode = 'full' # or partial\n",
+ "layers = 3"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Extract reference data and auxiliary variables. These cell controls the importing of different sets of data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Read input data\n",
+ "def load_data_and_setup():\n",
+ " if multi_output:\n",
+ " data_file = f'data/{mode}/8flavours.dat'\n",
+ " nqubits = 8\n",
+ " else:\n",
+ " data_file = f'data/{mode}/{parton}.dat'\n",
+ " nqubits = 1 \n",
+ " return np.loadtxt(data_file), nqubits\n",
+ "\n",
+ "# load data\n",
+ "data, nqubits = load_data_and_setup()\n",
+ "\n",
+ "# load qPDF model\n",
+ "mypdf = qPDF(ansatz, layers, nqubits, multi_output=multi_output)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we define a way to compute the loss function \n",
+ "\n",
+ "$$\n",
+ "\\chi^2 = \\frac{1}{N}\\sum_{i=1}^N\\sum_{j} \\frac{\\left( f_i (x_j, \\theta) - \\frac{1 - z(x_j, \\theta)}{1 + z(x_j, \\theta)}\\right)^2}{\\sigma^2}\n",
+ "$$\n",
+ "\n",
+ "is defined. For multi-flavour fits, a mean is considered."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define loss function\n",
+ "def loss(params):\n",
+ " \"\"\"Compute loss for a given set of parameters.\n",
+ "\n",
+ " Args:\n",
+ " parameters (np.array): the list of parameters for the gates.\n",
+ "\n",
+ " Returns:\n",
+ " The loss function.\n",
+ " \"\"\"\n",
+ " xtrain = data[:, 0]\n",
+ " if multi_output:\n",
+ " cf = 0\n",
+ " i = 1\n",
+ " for ypred in mypdf.predict(params, xtrain).transpose():\n",
+ " ytrain = data[:, i]\n",
+ " ysigma = data[:, i + 1]\n",
+ " cf += np.mean(np.square(ytrain - ypred) / ysigma ** 2)\n",
+ " i += 2\n",
+ " cf /= 8\n",
+ " else:\n",
+ " ytrain = data[:, 1]\n",
+ " ysigma = data[:, 2]\n",
+ " ypred = mypdf.predict(params, xtrain).flatten()\n",
+ " cf = np.mean(np.square(ytrain - ypred) / ysigma ** 2)\n",
+ " return cf"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Optimization procedure extracted from standard techniques must be used to look for the optimal configuration of $\\theta$ parameters. In this case, we will implement optimizers from `scipy` and a genetic one.\n",
+ "\n",
+ "\n",
+ "```python\n",
+ "# Optimizing\n",
+ "from qibo.optimizers import optimize\n",
+ "\n",
+ "np.random.seed(10)\n",
+ "params = np.random.rand(mypdf.nparams)\n",
+ "_, params, _ = optimize(loss, params, method='cma')\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The optimization may be costly in some cases. In order to save time, we provide some precomputed results that will let you to see the performance of this algorithm in several circumstances. Precomputed results include the ones detailed in the corresponding paper."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# For taking old results\n",
+ "import pickle\n",
+ "with open(f'results/{mode}/{parton}/{ansatz}_{nqubits}_q_{layers}_l_result.pkl', 'rb') as f:\n",
+ " results = pickle.load(f)\n",
+ " \n",
+ "params = results['x']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now take a look at the results! These graphs compare the reference data (black) and the current optimized fit (blue)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Auxiliary plotting function\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "def plot_PDF(params, chi2):\n",
+ " if multi_output:\n",
+ " fig, axs = plt.subplots(2, 4, figsize=(13, 9), sharex=True, sharey=True)\n",
+ " i = 1\n",
+ " partons = ['sbar', 'ubar', 'dbar', 'gluon', 'd', 'u', 's', 'c']\n",
+ " partons_name = [r'$\\bar s$', r'$\\bar u$', r'$\\bar d$', r'$g$', r'$d$', r'$u$', r'$s$', r'$c$']\n",
+ " xtrain = data[:, 0]\n",
+ " for ax, yprediction in zip(axs.flatten(), mypdf.predict(params, xtrain).transpose()):\n",
+ " ytrain = data[:, i].copy()\n",
+ " ysigma = data[:, i + 1].copy()\n",
+ " if i == 7:\n",
+ " ax.set(title=partons_name[(i - 1) // 2] + ' / 3', xscale='log')\n",
+ " yprediction /= 3\n",
+ " ytrain /= 3\n",
+ " ysigma /= 3\n",
+ " elif i == 15:\n",
+ " ax.set(title=partons_name[(i - 1) // 2] + r' $\\times$ 10', xscale='log')\n",
+ " yprediction *= 10\n",
+ " ytrain *= 10\n",
+ " ysigma *= 10\n",
+ " else:\n",
+ " ax.set(title=partons_name[(i - 1) // 2], xscale='log')\n",
+ " if (i - 1) // 2 % 4 == 0:\n",
+ " ax.set(ylabel='PDF')\n",
+ " if (i - 1) // 2 > 3:\n",
+ " ax.set(xlabel='x')\n",
+ "\n",
+ " ax.plot(xtrain, ytrain, label='Classical PDF', color='black')\n",
+ " ax.fill_between(xtrain, ytrain + ysigma, ytrain - ysigma, alpha=0.3, color='black')\n",
+ " ax.plot(xtrain, yprediction.flatten(), label=r'qPDF model', color='orange', linewidth=2, zorder=10)\n",
+ " ax.set(ylim=[-0.05, 1])\n",
+ " i += 2\n",
+ " ax.grid(True)\n",
+ " fig.suptitle(f'$\\chi^2 = $ {chi2:.4f}')\n",
+ " plt.legend()\n",
+ " else:\n",
+ " fig, ax = plt.subplots(figsize = (8, 6))\n",
+ " ax.set(title=f'$\\chi^2 = $ {chi2:.2f}', xlabel='x', ylabel='PDF',\n",
+ " xscale='log')\n",
+ " xtrain = data[:, 0]\n",
+ " ytrain = data[:, 1]\n",
+ " ysigma = data[:, 2]\n",
+ " yprediction = mypdf.predict(params, xtrain).flatten()\n",
+ " ax.plot(xtrain, ytrain, label='Classical '+ parton + ' PDF', color='black')\n",
+ " ax.fill_between(xtrain, ytrain + ysigma, ytrain - ysigma, alpha=0.3, color='black')\n",
+ " ax.plot(xtrain, yprediction.flatten(), label=r'Quantum PDF model', zorder=10)\n",
+ " ax.legend()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Plot results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAwgAAAJbCAYAAABAaLP6AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdd3ydZ33//9d1pvbewzMe8cqwHcfZZJFFTAgEEmgIlF1oafky2l/aMkrLLIQSCLSUECgJ2U6M4yReie14xHsPecmytrV1NM64fn8cWziOlmUdHZ2j9/Px0OMh6Vz3fb91dNs6n3MtY61FREREREQEwBHtACIiIiIiMnqoQBARERERkR4qEEREREREpIcKBBERERER6aECQUREREREeqhAEBERERGRHioQRERERESkhwoEEZEYY4xZaIxZb4x5wxjzpDHGHe1MIiISP1QgiIjEnuPAjdba64EjwKIo5xERkTjiinYAERE5P9bayrO+DAChaGUREZH4ox4EEZEYZYyZCNwOLInAubOMMS8YY9qNMceNMQ8Mta0x5g/GmCpjTIsx5qAx5lNnPeY1xvzm9HGtxphtxpjbh5JDRESGh3oQRERikDEmDfgd8FfW2u4IXOJRoBvIBy4F/myM2WGt3TOEtv8B/LW1tssYMx1YbYzZZq3dQvjv0AngeqAcuAN42hgz21p77DxziIjIMDDW2mhnEBGRcxhjDhB+YXyLtbbaGDMLWAdcCRwCFgM/ttaujMC1k4FGYJa19uDp7/0eOGmt/cZQ255+bBqwGvg7a+3TfVx/J/AtYNn5nFtERIaHhhiJiIxOlwJtwPuMMS7CvQX/bq3dB9wPLAD+xRiz2hjz4XMPNsYsMcY09fEx0JCkqUDwzIvy03YAM4fa1hjzC2OMD9gPVAFLe7uwMSb/9Dn3nGcOEREZJhpiJCIyCllrO4wxrwKzgYcJT0b+0enHfg/8foDj77qAy6cAzed8rxlIHWpba+0XjDFfAhYCNwBd557o9HKt/wf8zlq73xhz7XnkEBGRYaIeBBGR0Ws3cBfwFeAha21whK7bBqSd8700oPVC2lprg9batUAJ8PmzHzPGOAgXPd3AF4eQQ0REhokKBBGR0WsvMBH4zumhRYNmjHnFGNPWx8crAxx+EHAZY6ac9b1LCA/7uZC2Z7iAyWdlNcBvCE9Evtda67+Ac4uIyAXSJGURkVHKGPM94OtAnrW2boSv/RRggU8Rng+xFLiqt9WD+mtrjMkDbiS8FGsHcDPwPPCAtXbx6eMfO33czdbatqHmEBGR4aEeBBGRUcgYcwXwBaACuDgKEb4AJAK1wJPA589+UX66h+KfBtHWEh5OVEF4RaIfAV8+qzgYD3yW8Iv/6rN6OT46mBwiIjL81IMgIjLKGGO8wFbgMeAKYIO19tHophIRkbFCPQgiIqPPt4Aa4OfATuBOY4wnupFERGSsUA+CiMgoYoyZD7wGXGqtPW6MmQi8Cpyy1i6MbjoRERkLVCCIiIiIiEgPDTESEREREZEeKhBERERERKSHCgQREREREemhAkFERERERHqoQBARERERkR4qEEREREREpIcKBBERERER6aECQUREREREeqhAEBERERGRHioQRERERESkhwoEERERERHpoQJBRERERER6qEAQEREREZEeKhBERERERKSHCgQREREREemhAkFERERERHqoQBARERERkR4qEEREREREpIcKBBERERER6aECQUREREREeqhAEBERERGRHioQRERERESkhwoEERERERHpoQJBRERERER6qEAQEREREZEeKhBERERERKSHCgQREREREemhAkFERERERHqoQBAREYkDxpiXjTFtpz9WRjuPiMQuY62NdgYRERERERkl1IMgIiIiIiI9VCCMAep2lrFI972MdcaYfzHG/DzaOUQk9miIkYiISBwyxjwFrLLW/iraWUQktqgHQUREJD7NBHZGO4RIpBljiowxFX089gdjTJUxpsUYc9AY86mRzheLVCCMAcaYScaYJcaYemNMszHm9WhnEokkY8x3jTE/PevrEmNMuzFG/+dJXDLGOIwx/2iMKTfGVBpjPgJcBOyOdjaREXAHsKyPx/4DmGCtTQPuBv7NGDN3xJLFKP2xHBueAF4B8k9/fDOqaUQi71Jgx1lfXwLssdaGopRHJNL+BbgLuBa4GPgSUGWtbY1qKpEhOF3w/osx5sTpgvd9xphuY0xmH4fcASzt7QFr7R5rbdeZL09/TI5A7LiiAmFsmAw4Aae1ttNauy7agUQi7FLeObTiEjTUQuKUMSYX+H/Ag9ba49baZuDP6J6X2PVN4GbgSmAG8E9AjbW28dyGxhg3cB3Q5+gIY8wvjDE+YD9QRR/F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WilBEBERERGRVkoQRERERESklc8WSvMVY0wxcMDfcVyABKDE30F0YYFSP72stYn+OLGu+W4nUOrHb9c8BNx1HyifqT8FQh3pmj9/gfB5+lug1FGb133AJQiBxhiz3p8rM3Z1qp/uR5/pual+uh99pu1THXUv+jzbF+h1pC5GIiIiIiLSSgmCiIiIiIi0UoLge8/5O4AuTvXT/egzPTfVT/ejz7R9qqPuRZ9n+wK6jjQGQUREREREWqkFQUREREREWilBEBERERGRVkoQRERERESklRIEERERERFppQRBRERERERaKUEQEREREZFWShBERERERKSVEgQREREREWmlBEFERERERFopQRARERERkVZKEEREREREpJUSBBERERERaaUEQUREREREWilBEBERERGRVkoQRERERESklRIEERERERFppQRBRERERERaKUEQEREREZFWShBERERERKSVEgQREREREWmlBEFERERERFopQRARERERkVZKEEREREREpJUShB7IGPNzY8zv/R2HiIiIiHQ9ShB6pqHAFn8HISIiIuILxpgHjTHrjTENxpgXTnsvzhjzljGmxhhzwBhzh5/C7LKUIPRMlwGb/R2EiIiIyLmYFqPa2D7SGOM8x6GHgceBv7bx3jNAI5AMfBn4ozHmso6It7tQgtDNGWMcxph/N8YcNMYcNsbcBvQHtvo7NhFfMcb80hjz9EmvM44/KdLvPOm2jDF9jTHvGmNKjDEVxpjF/o5J5FyMMbcbYzYcv173GmNmtrFbb2CRMebak46bBiwFhpytbGvtm9bat4HS084ZDtwE/Ie1ttpauwqYC9x5yT9QNxLk7wDE534OfAaYBhwD5gOF1toqv0Yl4lvZwBsnvR4JbLPWev0Uj0hn+DvwCjALcAFj/BuOyNkZY74PfJWWG/NcWno3nHFvYq3db4y5CXjLGHMzUAO8CXzFWnsxDzsHAh5r7e6Ttm0CZlxEWd2WEoRuzBiTCPwAGGmtPXB823vAeL8GJuJ72bQkxyeMRN3qpPvrBzgBp7W2Hljt53hE2nT8/uQRYJq1dtPxzWcdG2mtXXF8nMAbgAe4z1r7/kWePgKoOG1bBRB5keV1S2pu796uBHZYa/eetC0ZDVCWbuz4H55kYNtJm0fS8oRIpDv7Mi2tB4eNMc8bY+L8HZDIWVwFbDkpOTgfB4FmwAB5l3DuaiDqtG1RtNF60ZMpQejeEoCiEy+MMS7g8+hJqnRvlwGfHn+CijEmCLgcXffSzVlrP7DWXknLTHUjgXv8G5HIWcXR0u35vBhj+gGLgR8BDwDzL2FQ8W4gyBgz4KRtIzn1oVKPpwShe9sFTDXGDDTGRAN/BLJQC4J0bwYIM8YEHR+U/GsgESUI0o0ZY75ojBlgjDG0dJWIpaVft0hXtJGW+5ORx2cpGmCMaXPAsTEmjZYByb+01r5grZ1DS/fpRcaYvmc7wfG/ASEc73ZnjAkxxgRZa0+MYfhPY0y4MWYKLS1v/+jgnzGgKUHoxqy1i4HZwHpgHVAM1AOf+jMuER9bSUsysJOWJ04HgXxrbblfoxLxranAclq6ScwHfmWt/cC/IYm0zVq7hpYpSN+l5Zp9Cwg9y+6lwPettX886fiXgG9xUi+JNvwMqAN+DHzl+Pc/O/7et46fr4iWgf3ftNaqBeEkxlrr7xhERERERKSLUAuCiIiIiIi08lmCYIz5qzGmyBjT5hy1x/uc/c4Ys8cYs9kYM9pXsYiIiIiIyPnxZQvCC8C153j/OmDA8a/7aBlAKyIiIiIifuSzBMFauwIoO8cus4C/2xYfATHGmFRfxSMiIiIiIu3z5xiEdODQSa/zj28TERERERE/CfLjuU0b29qcUskYcx8t3ZAIDQ0dk5mZ6cu4OpTX68Xh0FjwswmU+tm9e3eJtTaxs86na777CpT66exrHgL3ug+Uz9SfAqGOdM2fv0D4PP0tUOrobNe9T6c5Ncb0Bt611g5r471ngRxr7SvHX+8CZlprC89V5tixY+369et9EK1v5OTkMHPmTH+H0WUFSv0YYzZYa8f649y65ruXQKkff17zEFjXfaB8pv4UCHWka/78BcLn6W+BUkdnu+79mdrMBe46PpvRRKCiveRARERERER8y2ddjIwxrwAzgQRjTD7wCOACsNb+iZaVHq8H9gC1wL2+ikVERERERM6PzxIEa+3t7bxvgW/76vwiIiIiInLh/DlIWURERES6kaamJvLz86mvr/d3KH4VHR3Njh07/B1Gq5CQEDIyMnC5XOe1vxIEEREREekQ+fn5REZG0rt3b4xpa8LKnqGqqorIyEh/hwGAtZbS0lLy8/Pp06fPeR3T9edfEhEREZGAUF9fT3x8fI9ODroaYwzx8fEX1KqjBEFEREREOoySg67nQj8TJQgiIiIi0m0cOXKE2267jX79+jF06FCuv/56du/eTV5eHsOGnbE010X7+c9/zpIlSy74uLPFkZeXR2hoKNnZ2QwdOpQHHngAr9fbun3UqFEMGTKE8ePH8+KLL7Ye98ILL5CYmEh2djbZ2dncddddl/RzgcYgiIiIiEg3Ya3lC1/4AnfffTezZ88GIDc3l6NHj9LRK1X/53/+Z4eWB9CvXz9yc3Npbm7miiuu4O2332b06NH069ePjRs3ArBv3z6++MUv4vV6uffellUCbr31Vn7/+993WBxqQRARERGRbmHZsmW4XC4eeOCB1m3Z2dlMmzbtlP3y8vKYNm0ao0ePZvTo0axZswaAwsJCpk+fTnZ2NsOGDWPlypV4PB7uuecehg0bxvDhw3nqqacAuOeee3jjjTcAWLduHZMnT2bkyJGMHz+eqqqqs57jfAQFBTF58mT27Nlzxnt9+/blf//3f/nd7353wfVz3uf3WckiIiIi0mN997vfJTc3t0PLzM7O5umnnz7r+1u3bmXMmDHtlpOUlMTixYsJCQnh008/5fbbb2f9+vW8/PLLXHPNNfz0pz/F4/FQW1tLbm4uBQUFbN26FYBjx46dUlZjYyO33norr776KuPGjaOyshKPx0NMTEyb5zgftbW1LF269KytFKNHj2bnzp2tr1999VVWrVoFwHe+853WloWLpQRBRERERHqUpqYmHnzwQXJzc3E6nezevRuAcePG8dWvfpWmpiY+//nPk52dTd++fdm3bx8PPfQQN9xwA1dfffUpZe3atYvU1FTGjRsHQFRUFFVVVWc9x7ns3buX7OxsjDHMmjWL6667jry8vDP2a1lv+F86uouREgQRERER6XDnetLvK5dddllrt59zeeqpp0hOTmbTpk14vV5CQkIAmD59OitWrOC9997jzjvv5OGHH+auu+5i06ZNLFy4kGeeeYbXXnuNv/71r61lWWvbnCXobOc4lxNjENqzceNGhgwZ0u5+F0tjEERERESkW7jiiitoaGjgz3/+c+u2devWsXz58lP2q6ioIDU1FYfDwT/+8Q88Hg8ABw4cICkpiW984xt87Wtf45NPPqGkpASv18tNN93EY489xieffHJKWYMHD+bw4cOsW7cOaFkkrbm5+aznuFR5eXn84Ac/4KGHHuqQ8tqiFgQRERER6RaMMbz11lt897vf5Ve/+hUhISH07t37jNaMb33rW9x00028/vrrXH755YSHhwOQk5PDk08+icvlIiIigr///e8UFBRw77334vV6AXjiiSdOKcvtdvPqq6/y0EMPUVdXR2hoKG+99dZZz3Ex9u7dy6hRo6ivrycyMpKHHnrokscZnIsSBBERERHpNtLS0njttdfafO/EQOMBAwawefPm1u0nbvrvvvtu7r777jOOO73VAFrWHzhh3LhxfPTRR62vq6qqSE1NbfMcvXv3bo3jZOfaXldX1+bPAy2zKd1zzz1nff9iqIuRiIiIiIi0UoIgIiIiIiKtlCCIiIiIiEgrJQgiIiIiItJKCYKIiIiIiLRSgiAiIiIiIq2UIIiIiIhIt3fPPffQp08fsrOzGT16NB9++OEp20eOHMnAgQO56667KCgoaD2ud+/eDB8+nOzsbLKzs1mzZo1P4+zduzclJSWXvM+lUIIgIiIiIj3Ck08+SW5uLr/61a+4//77T9m+adMmdu3axahRo7j88stpbGxsfX/ZsmXk5uaSm5vL5MmT/RF6p1KCICIiIiLdxi9/+UsGDRrEVVddxe23385vfvObM/aZPn06e/bsOWO7MYbvfe97pKSksGDBgvM6X15eHoMHD+brX/86w4YN48tf/jLLli1jypQpDBgwgLVr1wJQVlbG5z//eUaMGMHEiRNbF1ErLS3l6quvZtSoUdx///1Ya1vL/uc//8n48ePJzs7m/vvvx+PxXEyVXDCtpCwiIiIiHe9l45ty77BnfWvDhg3Mnj2bjRs30tzczOjRoxkzZswZ+82bN4/hw4eftZzRo0ezc+dOZs2aBcDll1+O0+kkODiYjz/++Iz99+zZw+uvv85zzz3HuHHjeP3111m1ahVz587lv/7rv3j77bd55JFHGDVqFG+//TYffPABd911F7m5ufziF79g6tSp/PznP+e9997jueeeA2DHjh28+uqrrF69GpfLxbe+9S1eeukl7rrrrgutsQumBEFEREREuoWVK1fyhS98gbCwMAA+97nPnfL+ww8/zOOPP05iYiLPP//8Wcs5+Sk+tHQxSkhIOOv+ffr0aU04LrvsMqZPn44xhuHDh5OXlwfAqlWrmDNnDgBXXHEFpaWlVFRUsGLFCt58800AbrjhBmJjYwFYunQpGzZsYNy4cQDU1dWRlJR0vlVxSZQgiIiIiEjH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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_PDF(params, chi2=loss(params))"
+ ]
+ }
+ ],
+ "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.8.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/examples/qPDF/results/full/8flavours/Fourier_8_q_2_l_result.pkl b/examples/qPDF/results/full/8flavours/Fourier_8_q_2_l_result.pkl
new file mode 100644
index 000000000..0e4da4104
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Fourier_8_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Fourier_8_q_3_l_result.pkl b/examples/qPDF/results/full/8flavours/Fourier_8_q_3_l_result.pkl
new file mode 100644
index 000000000..1d584b17a
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Fourier_8_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Fourier_8_q_4_l_result.pkl b/examples/qPDF/results/full/8flavours/Fourier_8_q_4_l_result.pkl
new file mode 100644
index 000000000..ff3c3adac
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Fourier_8_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Fourier_8_q_5_l_result.pkl b/examples/qPDF/results/full/8flavours/Fourier_8_q_5_l_result.pkl
new file mode 100644
index 000000000..f9cf38709
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Fourier_8_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Weighted_8_q_2_l_result.pkl b/examples/qPDF/results/full/8flavours/Weighted_8_q_2_l_result.pkl
new file mode 100644
index 000000000..018dbab97
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Weighted_8_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Weighted_8_q_3_l_result.pkl b/examples/qPDF/results/full/8flavours/Weighted_8_q_3_l_result.pkl
new file mode 100644
index 000000000..f7056ee81
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Weighted_8_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Weighted_8_q_4_l_result.pkl b/examples/qPDF/results/full/8flavours/Weighted_8_q_4_l_result.pkl
new file mode 100644
index 000000000..e888bc235
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Weighted_8_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/8flavours/Weighted_8_q_5_l_result.pkl b/examples/qPDF/results/full/8flavours/Weighted_8_q_5_l_result.pkl
new file mode 100644
index 000000000..d3055a88a
Binary files /dev/null and b/examples/qPDF/results/full/8flavours/Weighted_8_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..5fe84e0d4
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..a1b884129
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..4de075435
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..080268bb6
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..8e91d313e
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/c/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..c4005a7ad
Binary files /dev/null and b/examples/qPDF/results/full/c/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..a2e583b8e
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..8c48680c2
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..d93fe4744
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..a24abdfeb
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..e5bda2d21
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/c/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/c/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..f5bb7cb3e
Binary files /dev/null and b/examples/qPDF/results/full/c/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..c77b115c0
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..b51cf2962
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..a6439e981
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..26c20be79
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..548506816
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/d/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..5d323bf04
Binary files /dev/null and b/examples/qPDF/results/full/d/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..76397f239
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..284fe7d71
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..08195907e
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..db5556483
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..f654e24c5
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/d/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/d/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..0a85372df
Binary files /dev/null and b/examples/qPDF/results/full/d/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..9c5c2ddd9
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..6b942c8e5
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..b5f9e9ad0
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..d02330433
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..8ef3553f8
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/dbar/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..e7c80caf5
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..bc825d69a
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..e953bd7d6
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..be36a514d
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..1d05982a0
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..0df17248b
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/dbar/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/dbar/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..0d2852d98
Binary files /dev/null and b/examples/qPDF/results/full/dbar/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..86ab4ee09
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..b295fc8aa
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..0e2703806
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..f65fca96c
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..98b1196aa
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/gluon/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..82c5c02c1
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..d8ce5e2d1
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..c330a6db2
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..1d91104df
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..e65f797f5
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..1d302afe2
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/gluon/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/gluon/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..83e1284cc
Binary files /dev/null and b/examples/qPDF/results/full/gluon/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..4fc0707fc
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..dd2f9618c
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..794726142
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..492dfe117
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..8a2674d8a
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/s/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..d6f8a3782
Binary files /dev/null and b/examples/qPDF/results/full/s/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..4a67d2bae
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..a2751cc4e
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..a8b920ce7
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..937b6bdb8
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..08fd6078b
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/s/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/s/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..438549993
Binary files /dev/null and b/examples/qPDF/results/full/s/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..47a42ec9b
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..621b10f66
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..b6e31ebaf
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..d13738dee
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..c012e1f0b
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/sbar/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..0666a90ad
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..64f08a605
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..893eefb81
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..006985357
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..e63d05317
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..f5fe32d4d
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/sbar/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/sbar/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..11ce34431
Binary files /dev/null and b/examples/qPDF/results/full/sbar/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..a48de6a49
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..6889318bf
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..b256789f4
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..e876c0e3d
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..c86051280
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/u/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..55cf53e80
Binary files /dev/null and b/examples/qPDF/results/full/u/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..12fdd106d
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..c20521363
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..3c4cfe07e
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..d4e699483
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..7d3721173
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/u/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/u/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..27ab963e7
Binary files /dev/null and b/examples/qPDF/results/full/u/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_1_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_1_l_result.pkl
new file mode 100644
index 000000000..72cd9e31b
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_2_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_2_l_result.pkl
new file mode 100644
index 000000000..939bb75b5
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_3_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_3_l_result.pkl
new file mode 100644
index 000000000..1868eb181
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_4_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_4_l_result.pkl
new file mode 100644
index 000000000..235815257
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_5_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_5_l_result.pkl
new file mode 100644
index 000000000..4923c66c2
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Fourier_1_q_6_l_result.pkl b/examples/qPDF/results/full/ubar/Fourier_1_q_6_l_result.pkl
new file mode 100644
index 000000000..7c806130d
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Fourier_1_q_6_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_1_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_1_l_result.pkl
new file mode 100644
index 000000000..2a0e80178
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_1_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_2_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_2_l_result.pkl
new file mode 100644
index 000000000..a42efafd9
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_2_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_3_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_3_l_result.pkl
new file mode 100644
index 000000000..41c659cc4
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_3_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_4_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_4_l_result.pkl
new file mode 100644
index 000000000..bf432e24f
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_4_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_5_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_5_l_result.pkl
new file mode 100644
index 000000000..b85822e5e
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_5_l_result.pkl differ
diff --git a/examples/qPDF/results/full/ubar/Weighted_1_q_6_l_result.pkl b/examples/qPDF/results/full/ubar/Weighted_1_q_6_l_result.pkl
new file mode 100644
index 000000000..f991b8b08
Binary files /dev/null and b/examples/qPDF/results/full/ubar/Weighted_1_q_6_l_result.pkl differ
diff --git a/examples/qap/README.md b/examples/qap/README.md
new file mode 100644
index 000000000..d29e621de
--- /dev/null
+++ b/examples/qap/README.md
@@ -0,0 +1,177 @@
+# Quadratic assignment problem (QAP)
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/qap](https://github.com/qiboteam/qibo/tree/master/examples/qap)
+
+The quadratic assignment problem (QAP) is an important combinatorial optimization problems that was first introduced by Koopmans and Beckmann. The objective of the problem is to assign a set of facilities to a set of locations in such a way as to minimize the total assignment cost. The assignment cost for a pair of facilities is a function of the flow between the facilities and the distance between the locations of the facilities.
+
+```python
+import numpy as np
+
+from qap import qubo_qap, qubo_qap_penalty, qubo_qap_feasibility, qubo_qap_energy, hamiltonian_qap
+
+def load_qap(filename):
+ """Load qap problem from a file
+
+ The file format is compatible with the one used in QAPLIB
+
+ """
+
+ with open(filename, 'r') as fh:
+ n = int(fh.readline())
+
+ numbers = [float(n) for n in fh.read().split()]
+
+ data = np.asarray(numbers).reshape(2, n, n)
+ f = data[1]
+ d = data[0]
+
+ i = range(len(f))
+ f[i, i] = 0
+ d[i, i] = 0
+
+ return f, d
+```
+
+## Load QAP problem from a file
+
+
+```python
+F, D = load_qap('tiny04a.dat')
+print(f'The QAP instance is:')
+print(F)
+print(D)
+```
+
+ The QAP instance is:
+ [[0. 0.29541331 0.68442855 0.19882279]
+ [0.29541331 0. 0.61649225 0.16210679]
+ [0.68442855 0.61649225 0. 0.73052088]
+ [0.19882279 0.16210679 0.73052088 0. ]]
+ [[0. 0.77969778 0.43045022 0.43294055]
+ [0.77969778 0. 0.1920096 0.58829618]
+ [0.43045022 0.1920096 0. 0.47901122]
+ [0.43294055 0.58829618 0.47901122 0. ]]
+
+
+## Calculate the penalty
+
+
+```python
+penalty = qubo_qap_penalty((F, D))
+print(f'The penalty is {penalty}')
+```
+
+ The penalty is 2.2783420340595995
+
+
+## Formulate the QUBO
+
+
+```python
+linear, quadratic, offset = qubo_qap((F, D), penalty=penalty)
+print(f'linear: {linear}')
+print()
+print(f'quadratic: {quadratic}')
+print()
+print(f'offset: {offset}\n')
+```
+
+ linear: {0: -4.556684, 1: -4.556684, 2: -4.556684, 3: -4.556684, 4: -4.556684, 5: -4.556684, 6: -4.556684, 7: -4.556684, 8: -4.556684, 9: -4.556684, 10: -4.556684, 11: -4.556684, 12: -4.556684, 13: -4.556684, 14: -4.556684, 15: -4.556684}
+
+ quadratic: {(1, 0): 2.278342, (2, 0): 2.278342, (3, 0): 2.278342, (4, 0): 2.278342, (5, 0): 0.2303331, (6, 0): 0.12716073, (7, 0): 0.1278964, (8, 0): 2.278342, (9, 0): 0.5336474, (10, 0): 0.2946124, (11, 0): 0.29631686, (12, 0): 2.278342, (13, 0): 0.15502168, (14, 0): 0.085583314, (15, 0): 0.08607845, (2, 1): 2.278342, (3, 1): 2.278342, (4, 1): 0.2303331, (5, 1): 2.278342, (6, 1): 0.05672219, (7, 1): 0.17379051, (8, 1): 0.5336474, (9, 1): 2.278342, (10, 1): 0.13141686, (11, 1): 0.4026467, (12, 1): 0.15502168, (13, 1): 2.278342, (14, 1): 0.038175885, (15, 1): 0.11696669, (3, 2): 2.278342, (4, 2): 0.12716073, (5, 2): 0.05672219, (6, 2): 2.278342, (7, 2): 0.14150628, (8, 2): 0.2946124, (9, 2): 0.13141686, (10, 2): 2.278342, (11, 2): 0.32784894, (12, 2): 0.085583314, (13, 2): 0.038175885, (14, 2): 2.278342, (15, 2): 0.09523835, (4, 3): 0.1278964, (5, 3): 0.17379051, (6, 3): 0.14150628, (7, 3): 2.278342, (8, 3): 0.29631686, (9, 3): 0.4026467, (10, 3): 0.32784894, (11, 3): 2.278342, (12, 3): 0.08607845, (13, 3): 0.11696669, (14, 3): 0.09523835, (15, 3): 2.278342, (5, 4): 2.278342, (6, 4): 2.278342, (7, 4): 2.278342, (8, 4): 2.278342, (9, 4): 0.48067763, (10, 4): 0.2653692, (11, 4): 0.2669045, (12, 4): 2.278342, (13, 4): 0.1263943, (14, 4): 0.069778904, (15, 4): 0.0701826, (6, 5): 2.278342, (7, 5): 2.278342, (8, 5): 0.48067763, (9, 5): 2.278342, (10, 5): 0.11837243, (11, 5): 0.36268005, (12, 5): 0.1263943, (13, 5): 2.278342, (14, 5): 0.03112606, (15, 5): 0.095366806, (7, 6): 2.278342, (8, 6): 0.2653692, (9, 6): 0.11837243, (10, 6): 2.278342, (11, 6): 0.2953067, (12, 6): 0.069778904, (13, 6): 0.03112606, (14, 6): 2.278342, (15, 6): 0.07765097, (8, 7): 0.2669045, (9, 7): 0.36268005, (10, 7): 0.2953067, (11, 7): 2.278342, (12, 7): 0.0701826, (13, 7): 0.095366806, (14, 7): 0.07765097, (15, 7): 2.278342, (9, 8): 2.278342, (10, 8): 2.278342, (11, 8): 2.278342, (12, 8): 2.278342, (13, 8): 0.5695855, (14, 8): 0.31445286, (15, 8): 0.3162721, (10, 9): 2.278342, (11, 9): 2.278342, (12, 9): 0.5695855, (13, 9): 2.278342, (14, 9): 0.14026703, (15, 9): 0.42976263, (11, 10): 2.278342, (12, 10): 0.31445286, (13, 10): 0.14026703, (14, 10): 2.278342, (15, 10): 0.3499277, (12, 11): 0.3162721, (13, 11): 0.42976263, (14, 11): 0.3499277, (15, 11): 2.278342, (13, 12): 2.278342, (14, 12): 2.278342, (15, 12): 2.278342, (14, 13): 2.278342, (15, 13): 2.278342, (15, 14): 2.278342}
+
+ offset: 18.226736272476796
+
+
+
+## Generate a random solution and check its feasibility
+
+
+```python
+rng = np.random.default_rng(seed=1234)
+random_solution = {i: rng.integers(2) for i in range(F.size)}
+print(f'The random solution is {random_solution}\n')
+```
+
+ The random solution is {0: 1, 1: 1, 2: 1, 3: 0, 4: 0, 5: 1, 6: 0, 7: 0, 8: 0, 9: 0, 10: 1, 11: 0, 12: 1, 13: 0, 14: 1, 15: 0}
+
+
+
+
+```python
+feasibility = qubo_qap_feasibility((F, D), random_solution)
+print(f'The feasibility of the random solution is {feasibility}\n')
+```
+
+ The feasibility of the random solution is False
+
+
+
+## Generate a feasible solution and check its feasibility
+
+
+```python
+feasible_solution = np.zeros(F.shape)
+sequence = np.arange(F.shape[0])
+np.random.shuffle(sequence)
+for i in range(F.shape[0]):
+ feasible_solution[i, sequence[i]] = 1
+feasible_solution = {k:v for k, v in enumerate(feasible_solution.flatten())}
+print(f'The feasible solution is {feasible_solution}\n')
+```
+
+ The feasible solution is {0: 0.0, 1: 0.0, 2: 1.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0, 10: 0.0, 11: 0.0, 12: 1.0, 13: 0.0, 14: 0.0, 15: 0.0}
+
+
+
+
+```python
+feasibility = qubo_qap_feasibility((F, D), feasible_solution)
+print(f'The feasibility of the feasible solution is {feasibility}\n')
+```
+
+ The feasibility of the feasible solution is True
+
+
+
+## Calculate the energy of the feasible solution
+
+
+```python
+energy = qubo_qap_energy((F,D), feasible_solution)
+print(f'The energy of the feasible solution is {energy}')
+```
+
+ The energy of the feasible solution is 2.7219091992575177
+
+
+## Hamiltonian
+
+
+```python
+ham = hamiltonian_qap((F, D), dense=False)
+```
+
+ [Qibo 0.1.6|INFO|2022-05-31 14:47:26]: Using qibojit backend on /GPU:0
+
+
+## Solve the Hamiltonian with QAOA
+
+QAP of size 4 is too large for Qibo QAOA. Let's reduce the size to 3
+
+
+```python
+ham = hamiltonian_qap((F[:3,:3], D[:3,:3]), dense=False)
+
+
+from qibo import models, hamiltonians
+
+# Create QAOA model
+qaoa = models.QAOA(ham)
+
+# Optimize starting from a random guess for the variational parameters
+initial_parameters = 0.01 * np.random.uniform(0,1,2)
+best_energy, final_parameters, extra = qaoa.minimize(initial_parameters, method="BFGS")
+```
+
+ [Qibo 0.1.8|WARNING|2022-10-31 14:14:37]: Calculating the dense form of a symbolic Hamiltonian. This operation is memory inefficient.
diff --git a/examples/qap/main.py b/examples/qap/main.py
new file mode 100755
index 000000000..66146ddf1
--- /dev/null
+++ b/examples/qap/main.py
@@ -0,0 +1,79 @@
+"""Quadratic Assignment Problem"""
+
+import argparse
+
+import numpy as np
+from qap import (
+ hamiltonian_qap,
+ qubo_qap,
+ qubo_qap_energy,
+ qubo_qap_feasibility,
+ qubo_qap_penalty,
+)
+from qubo_utils import binary2spin, spin2QiboHamiltonian
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--filename", default="./tiny04a.dat", type=str)
+
+
+def load_qap(filename):
+ """Load qap problem from a file
+
+ The file format is compatible with the one used in QAPLIB
+
+ """
+
+ with open(filename) as fh:
+ n = int(fh.readline())
+
+ numbers = [float(n) for n in fh.read().split()]
+
+ data = np.asarray(numbers).reshape(2, n, n)
+ f = data[1]
+ d = data[0]
+
+ i = range(len(f))
+ f[i, i] = 0
+ d[i, i] = 0
+
+ return f, d
+
+
+def main(filename: str = "./tiny04a.dat"):
+ print(f"Load flow and distance matrices from {filename} and make a QUBO")
+ F, D = load_qap(filename)
+ penalty = qubo_qap_penalty((F, D))
+
+ linear, quadratic, offset = qubo_qap((F, D), penalty=penalty)
+
+ print("A random solution with seed 1234 must be infeasible")
+ import numpy as np
+
+ rng = np.random.default_rng(seed=1234)
+ random_solution = {i: rng.integers(2) for i in range(F.size)}
+ feasibility = qubo_qap_feasibility((F, D), random_solution)
+ assert not feasibility, "The random solution should be infeasible."
+
+ print("Generate a feasible solution and check its feasibility")
+ feasible_solution = np.zeros(F.shape)
+ sequence = np.arange(F.shape[0])
+ np.random.shuffle(sequence)
+ for i in range(F.shape[0]):
+ feasible_solution[i, sequence[i]] = 1
+ feasible_solution = {k: v for k, v in enumerate(feasible_solution.flatten())}
+ feasibility = qubo_qap_feasibility((F, D), feasible_solution)
+ assert feasibility, "The fixed solution should be feasible."
+
+ print("Calculate the energy of the solution")
+ energy = qubo_qap_energy((F, D), feasible_solution)
+
+ print("Construct a hamiltonian directly from flow and distance matrices")
+ ham = hamiltonian_qap((F, D), dense=False)
+
+ print("done.")
+
+
+if __name__ == "__main__":
+ # by defualt, test on the mvc.csv in the same directory
+ args = parser.parse_args()
+ main(args.filename)
diff --git a/examples/qap/qap.py b/examples/qap/qap.py
new file mode 100644
index 000000000..1efecae8e
--- /dev/null
+++ b/examples/qap/qap.py
@@ -0,0 +1,138 @@
+from typing import Any, Dict, Tuple
+
+import numpy as np
+from qubo_utils import binary2spin, spin2QiboHamiltonian
+
+
+def qubo_qap(g: Tuple[np.ndarray, np.ndarray], penalty: float = None):
+ """Given adjacency matrix of flow and distance, formulate the QUBO of
+ Quadratic Assignment Problem (QAP)
+
+ Args:
+ g: the adjacency matrices of flow and distance that describe the QAP
+ penalty: penalty weight for the constraints, if not given, it
+ is inferred from the adjacency matrices
+
+ Returns:
+ linear (Dict[Any, float]): linear terms
+ quadratic (Dict[(Any, Any), float]): quadratic terms
+ offset (float): the constant term
+ """
+
+ flow, distance = g
+ n = len(flow)
+ q = np.einsum("ij,kl->ikjl", flow, distance)
+
+ if penalty is None:
+ penalty = qubo_qap_penalty(g)
+
+ i = range(len(q))
+ q[i, :, i, :] += 1 * penalty
+ q[i, :, i, :] -= 2 * penalty * np.eye(n)
+ q[:, i, :, i] += 1 * penalty
+ q[:, i, :, i] -= 2 * penalty * np.eye(n)
+
+ q = q.reshape(n**2, n**2).astype(np.float32)
+
+ offset = penalty * 2 * n
+ linear = {i: q[i, i] for i in range(q.shape[0])}
+ quadratic = {
+ (i, j): q[i, j] for j in range(q.shape[1]) for i in range(q.shape[0]) if i > j
+ }
+
+ return linear, quadratic, offset
+
+
+def qubo_qap_penalty(g: Tuple[np.ndarray, np.ndarray]):
+ """Find appropriate penalty weight to ensure feasibility
+
+ Args:
+ g: the adjacency matrices of flow and distance that describe the QAP
+
+ Returns:
+ penalty (float): penalty weight for the constraints
+ """
+ F, D = g
+ q = np.einsum("ij,kl->ikjl", F, D)
+ return F.shape[0] * np.abs(q).max()
+
+
+def qubo_qap_feasibility(g: Tuple[np.ndarray, np.ndarray], solution: Dict[Any, int]):
+ """Check if the solution violates the constraints of the problem
+
+ Args:
+ g: the adjacency matrices of flow and distance that describe the QAP
+ solution (Dict[Any, int]): the binary solution
+
+ Returns:
+ feasibility (bool): whether the solution meet the constraints
+ """
+ F, D = g
+
+ solution = [[k, v] for k, v in solution.items()]
+ solution.sort(key=lambda x: x[0])
+ _, vals = zip(*solution)
+ vals = np.asarray(vals).reshape(F.shape)
+
+ assert np.all(
+ np.logical_or(vals == 0, vals == 1)
+ ), "Decision variable must be 0 or 1"
+ return np.all(vals.sum(axis=0) == 1) and np.all(vals.sum(axis=1) == 1)
+
+
+def qubo_qap_energy(g: Tuple[np.ndarray, np.ndarray], solution: Dict[Any, int]):
+ """Calculate the energy of the solution on a QAP problem
+ The result is based on the assumption that soltuion is feasible.
+
+ Args:
+ g: the adjacency matrices of flow and distance that describe the QAP
+ solution (Dict[Any, int]): the solution
+
+ Returns:
+ energy (float): the energy of the solution
+ """
+
+ F, D = g
+
+ solution = [[k, v] for k, v in solution.items()]
+ solution.sort(key=lambda x: x[0])
+ _, vals = zip(*solution)
+ vals = np.asarray(vals).reshape(F.shape)
+
+ state = np.vstack(np.where(vals == 1)).T
+ state = np.array(state).astype(np.int32)
+ energy = 0
+ for i in range(len(state)):
+ energy += np.einsum(
+ "i,i->", F[state[i, 0], state[:, 0]], D[state[i, 1], state[:, 1]]
+ )
+ return energy
+
+
+def hamiltonian_qap(
+ g: Tuple[np.ndarray, np.ndarray], penalty: float = None, dense: bool = False
+):
+ """Given a flow and distance matrices, return the hamiltonian of Quadratic
+ Assignment Problem (QAP).
+
+ If penalty is not given, it is inferred from g.
+
+ Args:
+ g (Tuple[np.ndarray, np.ndarray]): flow and distance matrices
+ penalty (float): penalty weight of the constraints
+ dense (bool): sparse or dense hamiltonian
+
+ Returns:
+ ham: qibo hamiltonian
+
+ """
+
+ linear, quadratic, offset = qubo_qap(g, penalty)
+
+ h, J, _ = binary2spin(linear, quadratic)
+ h = {k: -v for k, v in h.items()}
+ J = {k: -v for k, v in J.items()}
+
+ ham = spin2QiboHamiltonian(h, J, dense=dense)
+
+ return ham
diff --git a/examples/qap/qubo_utils.py b/examples/qap/qubo_utils.py
new file mode 100644
index 000000000..33abe5305
--- /dev/null
+++ b/examples/qap/qubo_utils.py
@@ -0,0 +1,122 @@
+from typing import Dict, Tuple
+
+
+def binary2spin(
+ linear: Dict[int, float],
+ quadratic: Dict[Tuple[int, int], float],
+ offset: float = 0,
+):
+ """Convert binary model to spin model
+
+ Please remember to put a negative sign to h and J after using this
+ function , if you are going to form a mamiltonian from the spin
+ model. Hamiltonians usually have a leading negative sign in them,
+ but QUBOs don't.
+
+ Args:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+
+ Returns:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ offset (float): offset of the spin model
+ """
+
+ h = {x: 0.5 * w for x, w in linear.items()}
+
+ J = []
+ for (x, y), w in quadratic.items():
+ J.append(((x, y), 0.25 * w))
+ h[x] += 0.25 * w
+ h[y] += 0.25 * w
+ J = dict(J)
+
+ offset += 0.5 * sum(linear.values())
+ offset += 0.25 * sum(quadratic.values())
+
+ return h, J, offset
+
+
+def spin2binary(
+ h: Dict[int, float],
+ J: Dict[Tuple[int, int], float],
+ offset: float = 0,
+):
+ """Convert spin model to binary model
+
+ Please remember to put a negative sign to h and J before using this
+ function if you extract them from a hamiltonian. Hamiltonians
+ usually have a leading negative sign in them, but QUBOs don't.
+
+ Args:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ offset (float): offset of the spin model
+
+ Returns:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+ """
+
+ linear = {s: 2.0 * bias for s, bias in h.items()}
+
+ quadratic = []
+ for (s, t), bias in J.items():
+ quadratic.append(((s, t), 4.0 * bias))
+ linear[s] -= 2.0 * bias
+ linear[t] -= 2.0 * bias
+ quadratic = dict(quadratic)
+
+ offset -= sum(linear.values())
+ offset += sum(quadratic.values())
+
+ return linear, quadratic, offset
+
+
+def spin2QiboHamiltonian(
+ h: Dict[int, float],
+ J: Dict[Tuple[int, int], float],
+ dense: bool = True,
+):
+ """Convert spin model to qibo Hamiltonian
+
+ Mixer is not included.
+
+ Please remember to put a negative sign to h and J if you get h and J
+ from binary2spin. Hamiltonians usually have a leading negative sign
+ in them, but QUBOs don't.
+
+ .. math::
+ H = - \\sum_{i=0}^N \\sum _{j=0}^N J_{ij} Z_i Z_j - \\sum_{i=0}^N h_i Z_i.
+
+ Args:
+ h (dict): bias of the spin model
+ J (dict): interaction of the spin model
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+
+ Returns:
+ linear (dict): linear term of the binary model
+ quadratic (dict): quadratic term of the binary model
+ offset (float): offset of the binary model
+ """
+
+ from qibo import hamiltonians
+ from qibo.symbols import Z
+
+ form = 0
+ for k, v in h.items():
+ form -= Z(k, commutative=True) * v
+ for (k0, k1), v in J.items():
+ form -= Z(k0, commutative=True) * Z(k1, commutative=True) * v
+
+ ham = hamiltonians.SymbolicHamiltonian(form)
+
+ if dense:
+ return ham.dense
+ else:
+ return ham
diff --git a/examples/qap/tiny04a.dat b/examples/qap/tiny04a.dat
new file mode 100644
index 000000000..39661414a
--- /dev/null
+++ b/examples/qap/tiny04a.dat
@@ -0,0 +1,11 @@
+4
+
+0.0 0.7796977849571155 0.4304502176328382 0.4329405547124155
+0.7796977849571155 0.0 0.1920096021905386 0.5882961822495594
+0.4304502176328382 0.1920096021905386 0.0 0.47901121613954656
+0.4329405547124155 0.5882961822495594 0.47901121613954656 0.0
+
+0.0 0.29541330831110774 0.6844285463729066 0.1988227908746449
+0.29541330831110774 0.0 0.6164922460559509 0.16210678581988341
+0.6844285463729066 0.6164922460559509 0.0 0.7305208755290076
+0.1988227908746449 0.16210678581988341 0.7305208755290076 0.0
diff --git a/examples/qclustering/README.md b/examples/qclustering/README.md
new file mode 100644
index 000000000..3dfe908a3
--- /dev/null
+++ b/examples/qclustering/README.md
@@ -0,0 +1,68 @@
+# Quantum k-medians clustering
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/qclustering](https://github.com/qiboteam/qibo/tree/master/examples/qclustering)
+
+Implementation of QKmedians from the paper: [2301.10780](https://arxiv.org/abs/2301.10780).\
+\
+Before using install additional package:
+ - `h5py`
+
+## Algorithm's pseudocode
+
+![pseudo](figures/pseudocode_QKmed.jpeg)
+
+## Distance calculation quantum circuit
+
+![Distance circuit](figures/DistCirc.png)
+
+## How to run an example?
+Scripts are using `qibojit` as default backend.
+
+### Download dataset
+Dataset's dimensionality is reduced by passing it through autoencoder. If you are interested more, please refer to [[\*]](https://arxiv.org/abs/2301.10780).\
+Reduced dataset can be downloaded from `Zenodo` :
+[record/7673769](https://zenodo.org/record/7673769)\
+Small portion of dataset in `data` folder:
+ - `latentrep_QCD_sig.h5`: train dataset (QCD)
+ - `latentrep_QCD_sig_testclustering.h5`: test dataset (QCD)
+ - `latentrep_RSGraviton_WW_NA_35.h5`: test dataset (Signal)
+
+### Run training
+To run a training of quantum k-medians algorithm we need to provide arguments:
+- `train_size` (int): number of samples for training
+- `read_file` (str): path to the training dataset
+- `seed` (int): seed for consistent results in training
+- `k` (int): number of clusters (`default = 2`)
+- `tolerance` (float): convergence tolerance (`default = 1.0e-3`)
+- `min_type` (str): minimization type for distance to cluster search (`default = 'classic'`)
+- `nshots` (int): number of shots for executing quantum circuit (`default = 10000`)
+- `save_dir` (str): path to save results
+- `verbose (bool)`: print log messages during the training if `True`
+- `nprint (int)`: print loss function value each `nprint` epochs if `verbose` is `True`
+
+```python
+python train_qkmedians.py --train_size 600 --read_file 'data/latentrep_QCD_sig.h5' --k 2 --seed 123 --tolerance 1e-3 --min_type 'classic' --save_dir 'output_dir' --verbose true --nprint 1
+```
+
+### Run evaluation
+To run an evaluation of quantum k-medians algorithm we need to provide arguments:
+- `centroids_file` (str): name of the file for saved centroids coordinates
+- `data_qcd_file` (str): name of the file for test QCD dataset
+- `data_signal_file` (str): name of the file for test signal dataset
+- `k` (int): number of clusters (`default = 2`)
+- `test_size` (int): number of test samples (`default = 10000`)
+- `title` (str): Title of ROC curve plot (`default = 'Anomaly detection results'`)
+- `results_dir` (str): path to file with saved centroids
+- `data_dir` (str): path to file with test datasets
+- `save_dir_roc` (str): path to directory for saving ROC plot
+- `xlabel` (str): name of x-axis in ROC plot
+- `ylabel` (str): name of y-axis in ROC plot
+
+```python
+python evaluate.py --centroids_file 'centroids.npy' --data_qcd_file 'latentrep_QCD_sig_testclustering.h5' --data_signal_file 'latentrep_RSGraviton_WW_NA_35.h5' --results_dir 'output_dir' --data_dir 'data' --save_dir_roc 'output_dir'
+```
+
+#### ROC curve plot
+
+Output of evaluation script
+![ROC_curve](figures/roc_curve.png)
diff --git a/examples/qclustering/data/latentrep_QCD_sig.h5 b/examples/qclustering/data/latentrep_QCD_sig.h5
new file mode 100644
index 000000000..a647ffe3d
Binary files /dev/null and b/examples/qclustering/data/latentrep_QCD_sig.h5 differ
diff --git a/examples/qclustering/data/latentrep_QCD_sig_testclustering.h5 b/examples/qclustering/data/latentrep_QCD_sig_testclustering.h5
new file mode 100644
index 000000000..d3085cd11
Binary files /dev/null and b/examples/qclustering/data/latentrep_QCD_sig_testclustering.h5 differ
diff --git a/examples/qclustering/data/latentrep_RSGraviton_WW_NA_35.h5 b/examples/qclustering/data/latentrep_RSGraviton_WW_NA_35.h5
new file mode 100644
index 000000000..eb8373343
Binary files /dev/null and b/examples/qclustering/data/latentrep_RSGraviton_WW_NA_35.h5 differ
diff --git a/examples/qclustering/distance_calc.py b/examples/qclustering/distance_calc.py
new file mode 100644
index 000000000..3c4ff780a
--- /dev/null
+++ b/examples/qclustering/distance_calc.py
@@ -0,0 +1,71 @@
+import math
+
+import numpy as np
+import util as u
+
+from qibo import gates
+from qibo.models import Circuit
+
+
+def pad_input(X):
+ """Adds 0s if X log2(X.dim) != round int.
+
+ Parameters
+ ----------
+ X : :class:`numpy.ndarray`
+ Input data
+
+ Returns
+ -------
+ :class:`numpy.ndarray`
+ Padded X
+ """
+ num_features = len(X)
+ if not float(np.log2(num_features)).is_integer():
+ size_needed = pow(2, math.ceil(math.log(num_features) / math.log(2)))
+ X = np.pad(X, (0, size_needed - num_features), "constant")
+ return X
+
+
+def DistCalc(a, b, nshots=10000):
+ """Distance calculation using destructive interference.
+
+ Parameters
+ ----------
+ a : :class:`numpy.ndarray`
+ First point - shape = (latent space dimension,)
+ b : :class:`numpy.ndarray`
+ Second point - shape = (latent space dimension,)
+ device_name : str
+ Name of device for executing a simulation of quantum circuit.
+ nshots : int
+ Number of shots for executing a quantum circuit - to get frequencies.
+
+ Returns
+ -------
+ (float, :class:`qibo.models.Circuit`)
+ (distance, quantum circuit)
+
+ """
+ num_features = len(a)
+ norm = np.linalg.norm(a - b)
+ a_norm = a / norm
+ b_norm = b / norm
+
+ a_norm = pad_input(a_norm)
+ b_norm = pad_input(b_norm)
+
+ amplitudes = np.concatenate((a_norm, b_norm))
+ n_qubits = int(np.log2(len(amplitudes)))
+
+ # QIBO
+ qc = Circuit(n_qubits)
+ qc.add(gates.H(0))
+ qc.add(gates.M(0))
+
+ result = qc.execute(initial_state=amplitudes, nshots=nshots)
+
+ counts = result.frequencies(binary=True)
+ distance = norm * math.sqrt(2) * math.sqrt(counts["1"] / nshots)
+
+ return distance, qc
diff --git a/examples/qclustering/evaluate.py b/examples/qclustering/evaluate.py
new file mode 100644
index 000000000..0cbb46865
--- /dev/null
+++ b/examples/qclustering/evaluate.py
@@ -0,0 +1,145 @@
+import argparse
+
+import util as u
+
+
+def evaluate_qkmedians(
+ centroids_file,
+ data_qcd_file,
+ data_signal_file,
+ k=2,
+ test_size=10000,
+ title="Anomaly detection results",
+ results_dir=None,
+ data_dir=None,
+ save_dir_roc=None,
+ xlabel="TPR",
+ ylabel="1/FPR",
+):
+ """Evaluation of quantum k-medians.
+
+ Parameters
+ ----------
+ centroids_file : str
+ Name of the file for saved centroids coordinates.
+ data_qcd_file : str
+ Name of the file for test QCD dataset.
+ data_signal_file : str
+ Name of the file for test signal dataset.
+ k : int
+ Number of classes in quantum k-medians.
+ test_size : int
+ Number of test samples.
+ title : str
+ Title of ROC curve plot.
+ results_dir : str
+ Path to file with saved centroids.
+ data_dir : str
+ Path to file with test datasets.
+ save_dir_roc : str
+ Path to directory for saving ROC plot.
+ xlabel : str
+ Name of x-axis in ROC plot.
+ ylabel : str
+ Name of y-axis in ROC plot.
+ """
+
+ # calculate anomaly detection scores
+ loss = u.calc_AD_scores(
+ centroids_file,
+ data_qcd_file,
+ data_signal_file,
+ k=k,
+ test_size=test_size,
+ results_dir=results_dir,
+ data_dir=data_dir,
+ )
+ # plot roc curve
+ u.plot_ROCs_compare(
+ loss,
+ title=title,
+ xlabel=xlabel,
+ ylabel=ylabel,
+ legend_loc="best",
+ save_dir=save_dir_roc,
+ )
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="read arguments for qkmedians evaluation"
+ )
+ parser.add_argument(
+ "--centroids_file",
+ dest="centroids_file",
+ type=str,
+ help="name of the file for saved centroids coordinates",
+ )
+ parser.add_argument(
+ "--data_qcd_file",
+ dest="data_qcd_file",
+ type=str,
+ help="name of the file for test QCD dataset",
+ )
+ parser.add_argument(
+ "--data_signal_file",
+ dest="data_signal_file",
+ type=str,
+ help="name of the file for test signal dataset",
+ )
+ parser.add_argument("--k", dest="k", type=int, default=2, help="number of classes")
+ parser.add_argument(
+ "--test_size", dest="test_size", type=int, default=10000, help="test size"
+ )
+ parser.add_argument(
+ "--title",
+ dest="title",
+ type=str,
+ default="Anomaly detection results",
+ help="title of ROC curve plot",
+ )
+ parser.add_argument(
+ "--results_dir",
+ dest="results_dir",
+ type=str,
+ help="path to file with saved centroids",
+ )
+ parser.add_argument(
+ "--data_dir", dest="data_dir", type=str, help="path to file with test datasets"
+ )
+ parser.add_argument(
+ "--save_dir_roc",
+ dest="save_dir_roc",
+ type=str,
+ help="path to directory for saving ROC plot",
+ )
+ parser.add_argument(
+ "--xlabel",
+ dest="xlabel",
+ type=str,
+ default="TPR",
+ help="name of x-axis in ROC plot",
+ )
+ parser.add_argument(
+ "--ylabel",
+ dest="ylabel",
+ type=str,
+ default="1/FPR",
+ help="name of y-axis in ROC plot",
+ )
+
+ args = parser.parse_args()
+
+ evaluate_qkmedians(
+ args.centroids_file,
+ args.data_qcd_file,
+ args.data_signal_file,
+ args.k,
+ args.test_size,
+ args.title,
+ args.results_dir,
+ args.data_dir,
+ args.save_dir_roc,
+ args.xlabel,
+ args.ylabel,
+ )
diff --git a/examples/qclustering/figures/DistCirc.png b/examples/qclustering/figures/DistCirc.png
new file mode 100644
index 000000000..fb183dd3c
Binary files /dev/null and b/examples/qclustering/figures/DistCirc.png differ
diff --git a/examples/qclustering/figures/pseudocode_QKmed.jpeg b/examples/qclustering/figures/pseudocode_QKmed.jpeg
new file mode 100644
index 000000000..966f3b7f0
Binary files /dev/null and b/examples/qclustering/figures/pseudocode_QKmed.jpeg differ
diff --git a/examples/qclustering/figures/roc_curve.png b/examples/qclustering/figures/roc_curve.png
new file mode 100644
index 000000000..dd90439f1
Binary files /dev/null and b/examples/qclustering/figures/roc_curve.png differ
diff --git a/examples/qclustering/grover.py b/examples/qclustering/grover.py
new file mode 100644
index 000000000..004435f85
--- /dev/null
+++ b/examples/qclustering/grover.py
@@ -0,0 +1,68 @@
+import math
+
+import numpy as np
+
+from qibo import gates
+from qibo.models import Circuit
+
+
+def iam_operator(n):
+ """Construct quantum circuit for 'inversion around mean' operator.
+
+ Parameters
+ ----------
+ n : int
+ Number of qubits.
+
+ Returns
+ -------
+ :class:`qibo.models.Circuit`
+ Quantum circuit.
+ """
+ qc = Circuit(n)
+
+ for qubit in range(n):
+ qc.add(gates.H(qubit)) # apply H-gate
+ qc.add(gates.X(qubit)) # apply X-gate
+
+ # apply H-gate to last qubit
+ qc.add(gates.H(n - 1))
+ # apply multi-controlled toffoli gate to last qubit controlled by the ones before it (less significant)
+ qc.add(gates.X(n - 1).controlled_by(*list(range(0, n - 1))))
+ # apply H-gate to last qubit
+ qc.add(gates.H(n - 1))
+
+ for qubit in range(n):
+ qc.add(gates.X(qubit))
+ qc.add(gates.H(qubit))
+
+ return qc
+
+
+def grover_qc(qc, n, oracle, n_indices_flip):
+ """Construct quantum circuit for Grover's algorithm.
+
+ Parameters
+ ----------
+ qc : :class:`qibo.models.Circuit`
+ Initial quantum circuit to build up on.
+ n : int
+ Number of qubits
+ oracle : :class:`qibo.models.Circuit`)
+ Quantum circuit representing an Oracle operator.
+ n_indices_flip : int
+ Number of indices which have been selected by Oracle operator.
+
+ """
+ if n_indices_flip:
+ r_i = int(math.floor(np.pi / 4 * np.sqrt(2**n / n_indices_flip)))
+ else:
+ r_i = 0
+
+ for _ in range(r_i):
+ qc.add(oracle.on_qubits(*(list(range(n))))) # apply oracle
+ qc_diff = iam_operator(n)
+ qc.add(qc_diff.on_qubits(*(list(range(n))))) # apply inversion around mean
+
+ qc.add(gates.M(*list(range(n))))
+ return qc
diff --git a/examples/qclustering/minimization.py b/examples/qclustering/minimization.py
new file mode 100644
index 000000000..35fd9f2e3
--- /dev/null
+++ b/examples/qclustering/minimization.py
@@ -0,0 +1,60 @@
+import math
+
+import numpy as np
+import tensorflow as tf
+from grover import grover_qc
+from oracle import create_oracle_circ
+
+from qibo import gates
+from qibo.models import Circuit
+
+
+def duerr_hoyer_algo(distances):
+ """Perform Duerr-Hoyer algorithm.
+
+ Parameters
+ ----------
+ distance : :class:`numpy.ndarray`
+ Array of distances a point to each cluster.
+ Shape=(1, k) where k is number of cluster centers.
+
+ Returns
+ -------
+ int
+ New cluster assigned for that point.
+ """
+
+ k = len(distances)
+ n = int(math.floor(math.log2(k)) + 1)
+
+ # choose random threshold
+ index_rand = np.random.choice(k)
+ threshold = distances[index_rand]
+ max_iters = int(math.ceil(np.sqrt(2**n)))
+
+ for _ in range(max_iters):
+ qc = Circuit(n)
+
+ for i in range(n):
+ qc.add(gates.H(i))
+
+ # create oracle
+ qc_oracle, n_indices_marked = create_oracle_circ(distances, threshold, n)
+
+ # grover circuit
+ qc = grover_qc(qc, n, qc_oracle, n_indices_marked)
+
+ counts = qc.execute(nshots=1000).frequencies(binary=True)
+
+ # measure highest probability
+ probs = counts.items()
+ sorted_probs = dict(sorted(probs, key=lambda item: item[1], reverse=True))
+ sorted_probs_keys = list(sorted_probs.keys())
+ new_ix = [
+ int(sorted_probs_keys[i], 2)
+ for i, _ in enumerate(sorted_probs_keys)
+ if int(sorted_probs_keys[i], 2) < k
+ ]
+ new_ix = new_ix[0]
+ threshold = distances[new_ix]
+ return new_ix
diff --git a/examples/qclustering/oracle.py b/examples/qclustering/oracle.py
new file mode 100644
index 000000000..76df1ed31
--- /dev/null
+++ b/examples/qclustering/oracle.py
@@ -0,0 +1,51 @@
+import numpy as np
+
+from qibo.gates import Unitary
+from qibo.models import Circuit
+
+
+def f(x, threshold):
+ return x < threshold
+
+
+def create_oracle_circ(distances, threshold, n_qubits):
+ """Create circuit for oracle - for one solution --> I - 2*|i0> distance old_centroids to new_centroids
+ loss_epoch = np.linalg.norm(centroids - new_centroids)
+ loss.append(loss_epoch)
+
+ # if verbose
+ if (i % nprint == 0) and (verbose is True):
+ print(f"Loss at epoch {i+1}: {loss[-1]:.8}")
+
+ if loss_epoch < tolerance:
+ centroids = new_centroids
+ print(f"Converged after {i+1} iterations.")
+ break
+ elif (
+ loss_epoch > tolerance and i > new_tol * 200
+ ): # if after 200*new_tol epochs, difference != 0, lower the tolerance
+ tolerance *= 10
+ new_tol += 1
+ i += 1
+ centroids = new_centroids
+
+ if save_dir:
+ # if save_dir doesn't exist it is created
+ if os.path.exists(save_dir) is False:
+ os.system(f"mkdir {save_dir}")
+
+ np.save(
+ f"{save_dir}/cluster_label.npy",
+ cluster_label,
+ )
+ np.save(f"{save_dir}/centroids.npy", centroids)
+ np.save(f"{save_dir}/loss.npy", loss)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Read arguments for qkmedians training"
+ )
+ parser.add_argument(
+ "--train_size", dest="train_size", type=int, help="Training data size"
+ )
+ parser.add_argument(
+ "--read_file", dest="read_file", type=str, help="Path to training data"
+ )
+ parser.add_argument(
+ "--seed", dest="seed", type=int, help="Seed for consistent results"
+ )
+ parser.add_argument("--k", dest="k", type=int, default=2, help="Number of classes")
+ parser.add_argument(
+ "--tolerance",
+ dest="tolerance",
+ type=float,
+ default=1.0e-3,
+ help="Convergence tolerance",
+ )
+ parser.add_argument(
+ "--save_dir", dest="save_dir", type=str, help="Directory to save results"
+ )
+ parser.add_argument(
+ "--min_type",
+ dest="min_type",
+ type=str,
+ default="classic",
+ help="Type of minimization for distance calculation, classic or quantum",
+ )
+ parser.add_argument(
+ "--nshots",
+ dest="nshots",
+ type=int,
+ default=10000,
+ help="Number of shots for executing quantum circuit",
+ )
+ parser.add_argument(
+ "--verbose",
+ dest="verbose",
+ type=bool,
+ default=False,
+ help="Verbose level during training",
+ )
+ parser.add_argument(
+ "--nprint",
+ dest="nprint",
+ type=int,
+ default=10,
+ help="Log messages are printed every nprint epochs",
+ )
+
+ args = parser.parse_args()
+
+ if args.min_type not in ["classic", "quantum"]:
+ raise ValueError("Minimization is either classic or quantum procedure.")
+
+ train_qkmedians(
+ train_size=args.train_size,
+ read_file=args.read_file,
+ seed=args.seed,
+ k=args.k,
+ tolerance=args.tolerance,
+ min_type=args.min_type,
+ nshots=args.nshots,
+ save_dir=args.save_dir,
+ verbose=args.verbose,
+ nprint=args.nprint,
+ )
diff --git a/examples/qclustering/util.py b/examples/qclustering/util.py
new file mode 100644
index 000000000..cd3522b2b
--- /dev/null
+++ b/examples/qclustering/util.py
@@ -0,0 +1,242 @@
+import os
+
+import h5py
+import matplotlib.pyplot as plt
+import numpy as np
+import qkmedians as qkmed
+from sklearn.metrics import auc, roc_curve
+
+
+def combine_loss_min(loss):
+ """Find minimum loss from loss arrays obtained from dijet events.
+
+ Parameters
+ ----------
+ loss : :class:`numpy.ndarray`
+ Loss array of shape (num_of_samples*2, ), where num_of_samples = train_size or test_size.
+
+ Returns
+ -------
+ :class:`numpy.ndarray`
+ Final loss array of shape (num_of_samples, ).
+
+ """
+ loss_j1, loss_j2 = np.split(loss, 2)
+ return np.minimum(loss_j1, loss_j2)
+
+
+def load_clustering_test_data(
+ data_qcd_file, data_signal_file, test_size=10000, k=2, read_dir=None
+):
+ """Load test dataset.
+
+ Parameters
+ ----------
+ data_qcd_file : str
+ Name of the file for test QCD dataset.
+ data_signal_file : str
+ Name of the file for test signal dataset.
+ test_size : int
+ Number of test samples.
+ k : int
+ Number of classes in quantum k-medians.
+ read_dir : str
+ Path to file with test datasets.
+
+ Returns
+ -------
+ (:class:`numpy.ndarray`, :class:`numpy.ndarray`)
+ (Test data for QCD events, Test data for signal events).
+ """
+
+ if not read_dir:
+ raise ValueError("Need to specify directory for datasets.")
+
+ # read QCD latent space data
+ with h5py.File(f"{read_dir}/{data_qcd_file}", "r") as file:
+ data = file["latent_space"]
+ l1 = data[:, 0, :]
+ l2 = data[:, 1, :]
+
+ data_test_qcd = np.vstack([l1[:test_size], l2[:test_size]])
+
+ # read SIGNAL predicted data
+ with h5py.File(f"{read_dir}/{data_signal_file}", "r") as file:
+ data = file["latent_space"]
+ l1 = data[:, 0, :]
+ l2 = data[:, 1, :]
+
+ data_test_sig = np.vstack([l1[:test_size], l2[:test_size]])
+
+ return data_test_qcd, data_test_sig
+
+
+def AD_score(cluster_assignments, distances, method="sum_all"):
+ """Calculate anomaly detection score.
+
+ Parameters
+ ----------
+ cluster_assignments : :class:`numpy.ndarray`
+ Cluster assignments for each point in space.
+ distances : :class:`numpy.ndarray`
+ Distances to cluster centroids. Shape = (N_events, k) where k = number of clusters.
+ method : str
+ Method how to calculate anomaly detection score. `'Sum_all'` = sum all distances to different clusters.
+
+ Returns
+ -------
+ :class:`numpy.ndarray`
+ Anomaly score for each event.
+ """
+ if method == "sum_all":
+ return np.sqrt(np.sum(distances**2, axis=1))
+ else:
+ return np.sqrt(distances[range(len(distances)), cluster_assignments] ** 2)
+
+
+def AD_scores(test_qcd, test_sig, centroids):
+ """Helper function to calculate anomaly scores for QCD and signal events.
+
+ Parameters
+ ----------
+ test_qcd : :class:`numpy.ndarray`
+ Test QCD dataset.
+ test_sig : :class:`numpy.ndarray`
+ Test signal dataset.
+ centroids : :class:`numpy.ndarray`
+ Coordinates for class centroids.
+
+ Returns
+ -------
+ (:class:`numpy.ndarray`, :class:`numpy.ndarray`)
+ (Anomaly score for QCD data, Anomaly score for signal data).
+ """
+
+ # find cluster assignments + distance to centroids for test data
+ cluster_assign, distances = qkmed.find_nearest_neighbour(test_qcd, centroids)
+ cluster_assign_s, distances_s = qkmed.find_nearest_neighbour(test_sig, centroids)
+
+ # calc AD scores
+ score_qcd = AD_score(cluster_assign, distances)
+ score_sig = AD_score(cluster_assign_s, distances_s)
+
+ # calculate loss from 2 jets
+ loss_qcd = combine_loss_min(score_qcd)
+ loss_sig = combine_loss_min(score_sig)
+
+ return [loss_qcd, loss_sig]
+
+
+def calc_AD_scores(
+ centroids_file,
+ data_qcd_file,
+ data_signal_file,
+ k=2,
+ test_size=10000,
+ results_dir=None,
+ data_dir=None,
+):
+ """Calculate anomaly scores for QCD and signal events.
+
+ Parameters
+ ----------
+ data_qcd_file : str
+ Name of the file for test QCD dataset.
+ data_signal_file : str
+ Name of the file for test signal dataset.
+ k : int
+ Number of classes in quantum k-medians.
+ test_size : int
+ Number of test samples.
+ results_dir : str
+ Path to file with saved centroids.
+ data_dir : str
+ Path to file with test datasets.
+
+ Returns
+ -------
+ (:class:`numpy.ndarray`, :class:`numpy.ndarray`)
+ loss = (Anomaly score for QCD data, Anomaly score for signal data).
+ """
+
+ if not results_dir:
+ raise ValueError("Need to specify directory for loading the centroids.")
+
+ # load centroids
+ centroids = np.load(f"{results_dir}/{centroids_file}")
+
+ test_qcd, test_sig = load_clustering_test_data(
+ data_qcd_file, data_signal_file, test_size=test_size, k=k, read_dir=data_dir
+ )
+
+ loss = AD_scores(test_qcd, test_sig, centroids)
+
+ return loss
+
+
+def get_roc_data(qcd, signal):
+ """Calculate ROC curve data - tpr, fpr, auc
+
+ Parameters
+ ----------
+ qcd : :class:`numpy.ndarray`
+ Anomaly score for QCD dataset.
+ signal : :class:`numpy.ndarray`
+ Anomaly score for signal dataset.
+
+ Returns
+ -------
+ (:class:`numpy.ndarray`, :class:`numpy.ndarray`, :class:`numpy.ndarray`)
+ False positive rate (fpr), True positive rate (tpr), Area under the ROC curve (auc)
+ """
+
+ true_val = np.concatenate((np.ones(signal.shape[0]), np.zeros(qcd.shape[0])))
+ pred_val = np.nan_to_num(np.concatenate((signal, qcd)))
+ fpr, tpr, _ = roc_curve(true_val, pred_val)
+ auc_data = auc(fpr, tpr)
+ return fpr, tpr, auc_data
+
+
+def plot_ROCs_compare(loss, title, xlabel="TPR", ylabel="1/FPR", save_dir=None):
+ """Plot ROC curve.
+
+ Parameters
+ ----------
+ loss : (:class:`numpy.ndarray`, :class:`numpy.ndarray`)
+ (Anomaly score for QCD data, Anomaly score for signal data).
+ title : str
+ Title of ROC curve plot.
+ xlabel : str
+ Name of x-axis in ROC plot.
+ ylabel : str
+ Name of y-axis in ROC plot.
+ save_dir : str
+ Path to directory for saving ROC plot.
+ """
+
+ fig = plt.figure(figsize=(8, 8))
+
+ loss_qcd, loss_sig = loss
+
+ # roc data
+ data = get_roc_data(loss_qcd, loss_sig)
+ tpr = data[1]
+ fpr = data[0]
+
+ plt.plot(tpr, 1.0 / fpr, label="(auc = %.2f)" % (data[2] * 100.0), linewidth=1.5)
+
+ plt.ylabel(ylabel)
+ plt.xlabel(xlabel)
+ plt.yscale("log")
+ plt.title(title)
+ plt.legend(
+ fancybox=True, frameon=True, prop={"size": 10}, bbox_to_anchor=(1.0, 1.0)
+ )
+ plt.grid(True)
+
+ if save_dir:
+ if os.path.exists(save_dir) is False:
+ os.system(f"mkdir {save_dir}")
+ plt.savefig(f"{save_dir}/roc_curve.pdf", dpi=fig.dpi, bbox_inches="tight")
+ else:
+ plt.show()
diff --git a/examples/qcnn_classifier/images/RT.PNG b/examples/qcnn_classifier/images/RT.PNG
new file mode 100644
index 000000000..1932c47d2
Binary files /dev/null and b/examples/qcnn_classifier/images/RT.PNG differ
diff --git a/examples/qcnn_classifier/images/RxRyRz.PNG b/examples/qcnn_classifier/images/RxRyRz.PNG
new file mode 100644
index 000000000..0631cc3f0
Binary files /dev/null and b/examples/qcnn_classifier/images/RxRyRz.PNG differ
diff --git a/examples/qcnn_classifier/images/U.PNG b/examples/qcnn_classifier/images/U.PNG
new file mode 100644
index 000000000..bce10de23
Binary files /dev/null and b/examples/qcnn_classifier/images/U.PNG differ
diff --git a/examples/qcnn_classifier/images/convolution_4qubits.PNG b/examples/qcnn_classifier/images/convolution_4qubits.PNG
new file mode 100644
index 000000000..0d434dbc6
Binary files /dev/null and b/examples/qcnn_classifier/images/convolution_4qubits.PNG differ
diff --git a/examples/qcnn_classifier/images/data_labels.PNG b/examples/qcnn_classifier/images/data_labels.PNG
new file mode 100644
index 000000000..b441252d3
Binary files /dev/null and b/examples/qcnn_classifier/images/data_labels.PNG differ
diff --git a/examples/qcnn_classifier/images/pooling_4qubits.PNG b/examples/qcnn_classifier/images/pooling_4qubits.PNG
new file mode 100644
index 000000000..838d12e6a
Binary files /dev/null and b/examples/qcnn_classifier/images/pooling_4qubits.PNG differ
diff --git a/examples/qcnn_classifier/images/result_confusion_matrix.PNG b/examples/qcnn_classifier/images/result_confusion_matrix.PNG
new file mode 100644
index 000000000..273dcd2e6
Binary files /dev/null and b/examples/qcnn_classifier/images/result_confusion_matrix.PNG differ
diff --git a/examples/qcnn_classifier/images/structure.PNG b/examples/qcnn_classifier/images/structure.PNG
new file mode 100644
index 000000000..2e001cd1d
Binary files /dev/null and b/examples/qcnn_classifier/images/structure.PNG differ
diff --git a/examples/qcnn_classifier/images/test b/examples/qcnn_classifier/images/test
new file mode 100644
index 000000000..9daeafb98
--- /dev/null
+++ b/examples/qcnn_classifier/images/test
@@ -0,0 +1 @@
+test
diff --git a/examples/qcnn_classifier/images/workflow.PNG b/examples/qcnn_classifier/images/workflow.PNG
new file mode 100644
index 000000000..6abcd3e0c
Binary files /dev/null and b/examples/qcnn_classifier/images/workflow.PNG differ
diff --git a/examples/qcnn_classifier/nqubits_4_data_shuffled_no0.npy b/examples/qcnn_classifier/nqubits_4_data_shuffled_no0.npy
new file mode 100644
index 000000000..7728e4bab
Binary files /dev/null and b/examples/qcnn_classifier/nqubits_4_data_shuffled_no0.npy differ
diff --git a/examples/qcnn_classifier/nqubits_4_labels_shuffled_no0.npy b/examples/qcnn_classifier/nqubits_4_labels_shuffled_no0.npy
new file mode 100644
index 000000000..0c18ef108
Binary files /dev/null and b/examples/qcnn_classifier/nqubits_4_labels_shuffled_no0.npy differ
diff --git a/examples/qcnn_classifier/qcnn_demo.ipynb b/examples/qcnn_classifier/qcnn_demo.ipynb
new file mode 100644
index 000000000..0f72938ba
--- /dev/null
+++ b/examples/qcnn_classifier/qcnn_demo.ipynb
@@ -0,0 +1,425 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "441d162d-9c1a-46eb-b213-6f63d5517676",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# Quantum Convolutional Neural Network Classifier\n",
+ "\n",
+ "Code at: [https://github.com/qiboteam/qibo/tree/master/examples/qcnn_classifier](https://github.com/qiboteam/qibo/tree/master/examples/qcnn_classifier).\n",
+ "Please note that [scikit-learn](https://scikit-learn.org/stable/install.html) is needed to visualize the results."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "514ef70a-17db-4424-804f-d735a93ce5a8",
+ "metadata": {},
+ "source": [
+ "## Problem overview\n",
+ "This tutorial implements a simple [Quantum Convolutional Neural Network](https://www.nature.com/articles/s41567-019-0648-8) (QCNN), which is a translationally invariant algorithm analogous to the classical [convolutional neural network](https://proceedings.neurips.cc/paper_files/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf). This example demonstrates the use of the QCNN as a quantum classifier, which attempts to classify ground states of a translationally invariant quantum system, the transverse field Ising model, based on whether they are in the ordered or disordered phase. The (randomized) statevector data provided are those of a 4-qubit system. Accompanying each state is a label: +1 (disordered phase) or -1 (ordered phase).\n",
+ "\n",
+ "Through the sequential reduction of entanglement, this network is able to perform classification from the final measurement of a single qubit.\n",
+ "\n",
+ "Workflow of QCNN model:\n",
+ "![workflow](images/workflow.PNG)\n",
+ "\n",
+ "Schematic of QCNN model:\n",
+ "![schematic](images/structure.PNG)\n",
+ "\n",
+ "Convolutional layer for 4 qubits as an example:\n",
+ "![convolution](images/convolution_4qubits.PNG)\n",
+ "\n",
+ "Pooling layer for 4 qubits as an example:\n",
+ "![pooling](images/pooling_4qubits.PNG)\n",
+ "\n",
+ "where in the above, $R(\\theta_{i,j,k}) = RZ(\\theta_k) RY(\\theta_j) RX(\\theta_i)$:\n",
+ "![R](images/RxRyRz.PNG)\n",
+ "\n",
+ "$U_{q_a, q_b}(\\theta_{i,j,k}) = RXX(\\theta_k) RYY(\\theta_j) RZZ(\\theta_i)$ is a two-qubit gate acting on qubits $q_a$ and $q_b$:\n",
+ "![U](images/U.PNG)\n",
+ "\n",
+ "and $R^{\\dagger}(\\theta_{i,j,k}) = RX(-\\theta_i) RY(-\\theta_j) RZ(-\\theta_k)$:\n",
+ "![RT](images/RT.PNG)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1e9427ed-3f12-47fc-8048-ea258e1ec19c",
+ "metadata": {},
+ "source": [
+ "## How to use the QCNN class\n",
+ "For more details on the QuantumCNN class, please refer to the documentation. Here we recall some of the necessary arguments when instantiating a QuantumCNN object:\n",
+ "- `nqubits` (int): number of quantum bits. It should be larger than 2 for the model to make sense.\n",
+ "- `nlayers` (int): number of layers of the QCNN variational ansatz.\n",
+ "- `nclasses` (int): number of classes of the training set (default=2).\n",
+ "- `params`: list to initialise the variational parameters (default=None).\n",
+ "\n",
+ "After creating the object, one can proceed to train the model. For this, the `QuantumCNN.minimize` method can be used with the following arguments (refer to the documentation for more details)\"\n",
+ "- `init_theta`: list or numpy.array with the angles to be used in the circuit\n",
+ "- `data`: the training data\n",
+ "- `labels`: numpy.array containing the labels for the training data\n",
+ "- `nshots` (int):number of runs of the circuit during the sampling process (default=10000)\n",
+ "- `method` (string): str 'classical optimizer for the minimization'. All methods from scipy.optimize.minmize are suported (default='Powell')."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "01e19126-53b0-49c4-8919-e6db4cb77cb0",
+ "metadata": {},
+ "source": [
+ "## QCNN Demo"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "77098dca-abbb-451e-aab7-50453a8457db",
+ "metadata": {},
+ "source": [
+ "Include necessary packages:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "5faec3db-910b-414a-8633-6c0329e0b75f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.1.13|INFO|2023-05-15 17:25:59]: Using numpy backend on /CPU:0\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import random\n",
+ "from sklearn import metrics\n",
+ "\n",
+ "import qibo\n",
+ "from qibo.models.qcnn import QuantumCNN\n",
+ "\n",
+ "qibo.set_backend(\"numpy\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b7a04e9f-dd64-4059-bbb4-9afea3da8495",
+ "metadata": {},
+ "source": [
+ "Load the provided data (ground states of 4-qubit TFIM in data folder) and labels:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "747c0503-2f77-463d-9515-ee05b254c798",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data = np.load('nqubits_4_data_shuffled_no0.npy')\n",
+ "labels = np.load('nqubits_4_labels_shuffled_no0.npy')\n",
+ "labels = np.transpose(np.array([labels])) # restructure to required array format"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "614a17b2-aee0-49fe-ad7a-4cd4b93986af",
+ "metadata": {},
+ "source": [
+ "Structure of data and labels are like:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "5938d249-32e9-4c1a-a2be-90dc11df0490",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[0.52745364+0.j, 0.19856967+0.j, 0.19856967+0.j, 0.16507377+0.j,\n",
+ " 0.19856967+0.j, 0.09784837+0.j, 0.16507377+0.j, 0.19856967+0.j,\n",
+ " 0.19856967+0.j, 0.16507377+0.j, 0.09784837+0.j, 0.19856967+0.j,\n",
+ " 0.16507377+0.j, 0.19856967+0.j, 0.19856967+0.j, 0.52745364+0.j],\n",
+ " [0.67109214+0.j, 0.10384038+0.j, 0.10384038+0.j, 0.05351362+0.j,\n",
+ " 0.10384038+0.j, 0.02786792+0.j, 0.05351362+0.j, 0.10384038+0.j,\n",
+ " 0.10384038+0.j, 0.05351362+0.j, 0.02786792+0.j, 0.10384038+0.j,\n",
+ " 0.05351362+0.j, 0.10384038+0.j, 0.10384038+0.j, 0.67109214+0.j]])"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "data[-2:]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "ab0e3965-85ba-4d00-a8b9-b9627ba4a9fe",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[ 1.],\n",
+ " [-1.]])"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "labels[-2:]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a817dceb-1097-4625-8ccf-9bdb1eda6c23",
+ "metadata": {},
+ "source": [
+ "Split the data into training/test set in the ratio 60:40"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "96882004-a479-45eb-9439-b5237d8c9500",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "split_ind = int(len(data) * 0.6)\n",
+ "train_data = data[:split_ind]\n",
+ "test_data = data[split_ind:]\n",
+ "\n",
+ "train_labels = labels[:split_ind]\n",
+ "test_labels = labels[split_ind:]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "44d641bc-8a0c-4c05-82fa-2066a070a735",
+ "metadata": {},
+ "source": [
+ "Initialize the QuantumCNN class:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "58b9d6fd-992f-4cac-9314-100d2e41a34d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "q0: ─RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ──────────────────────────────────────── ...\n",
+ "q1: ─RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ─────────────RX─RY─RZ──────────RZZ─RYY─R ...\n",
+ "q2: ──────────────────────RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ─RX─RY─RZ─RZZ─RYY─R ...\n",
+ "q3: ──────────────────────RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ─────────────────── ...\n",
+ "\n",
+ "q0: ... ───RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ─RX─RY─RZ─o───────────────────────\n",
+ "q1: ... XX─RX─RY─RZ─|───|───|─────────────────────|─RX─RY─RZ─o────────────\n",
+ "q2: ... XX─RX─RY─RZ─|───|───|───RX─RY─RZ──────────X─RZ─RY─RX─|────────────\n",
+ "q3: ... ───RX─RY─RZ─RZZ─RYY─RXX─RX─RY─RZ────────────RX─RY─RZ─X─RZ─RY─RX─M─\n"
+ ]
+ }
+ ],
+ "source": [
+ "test = QuantumCNN(nqubits=4, nlayers=1, nclasses=2)\n",
+ "testcircuit = test._circuit\n",
+ "print(testcircuit.draw())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ccebe817-906a-4328-8713-9fcd99c07972",
+ "metadata": {},
+ "source": [
+ "draw() is used to visualize the pre-constructed circuit based on input parameters for class initialization.\n",
+ "\n",
+ "Initialize model parameters:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "ea551785-f0b4-4327-8baf-0134127f0091",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "testbias = np.zeros(test.measured_qubits)\n",
+ "testangles = [random.uniform(0,2*np.pi) for i in range(21*2)]\n",
+ "init_theta = np.concatenate((testbias, testangles))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19f90348-17f0-4fda-8950-5d95a3216b8e",
+ "metadata": {},
+ "source": [
+ "Train model with optimize parameters (automatically updates model with optimized paramters at the end of training):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "da959229-14f9-410e-b7ff-02a5651f3eff",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "result = test.minimize(init_theta, data=train_data, labels=labels, nshots=10000, method='Powell')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fc3f4f2a-3388-4ce5-b0fe-0304a5be56a2",
+ "metadata": {},
+ "source": [
+ "Alternatively, update model with optimized parameters from previous training:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "4b019567-9f40-4a0e-9f52-cdd144348c64",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "saved_result_60 = (0.2026119742575817, np.array([ -0.06559061, 3.62881221, 2.39850148, 3.02493711,\n",
+ " 0.91498683, 3.25517842, 0.0759049 , 3.46049453,\n",
+ " 3.04395784, 1.55681424, 2.3665245 , 0.40291846,\n",
+ " 5.67310744, 2.27615444, 5.23403537, 0.46053411,\n",
+ " 0.69228362, 2.2308165 , 0.53323661, 4.52157388,\n",
+ " 5.31194656, 18.23511858, -1.90754635, 14.30577217,\n",
+ " 10.75135972, 19.16001316, 12.27582746, 7.47476354,\n",
+ " 23.38129141, 60.29771502, 10.02946377, 17.83945879,\n",
+ " 15.22732248, 12.34666584, 1.52634649, 1.90621517,\n",
+ " 12.71554053, -13.56379057, 34.04591253, -11.56450878,\n",
+ " 10.95038782, 3.30640208, 9.67270071]))\n",
+ "\n",
+ "test.set_circuit_params(angles=saved_result_60[1], has_bias=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a28a68bf-8246-46c4-9d3d-c1ef9c2f7813",
+ "metadata": {},
+ "source": [
+ "Generate predictions from optimized model:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "4a0c3b38-7ea3-4718-9974-ba1cead1ef53",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "predictions = []\n",
+ "for n in range(len(test_data)):\n",
+ " predictions.append(test.predict(test_data[n], nshots=10000)[0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "995b90f9-5441-4a4d-bb3f-d852c7452af4",
+ "metadata": {},
+ "source": [
+ "Visualize results via confusion matrix and accuracy:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "806960b9-eaef-4f26-bf79-35d403c14605",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "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Iy4uztiyZYvPsaNHjzZuvPFGwzAMY+rUqUaXLl18vp8yZcoZY/2YJGPlypU/+f1TTz1l9OrVy/t5xowZRmRkpPHFF19497311ltGRESEcfDgQcMwDOO8884zli1b5jPOww8/bGRkZBiGYRjFxcWGJGPnzp0/eV0AdcecO2xlzZo1io+PV01NjTwej2666Safd3J369bNZ579448/VmFhoRISEnzGqaysVFFRkY4dO6aDBw+qd+/e3u+ioqL0q1/96ozW/Gn5+fmKjIzUZZddVuu4CwsLdeLECV155ZU++6urq3XhhRdKknbv3u0ThyRlZGTU+hqnvfTSS5o3b56KiopUXl6ukydPyuVy+RzTpk0btWrVyuc6Ho9HBQUFSkhIUFFRkUaPHq0xY8Z4jzl58qQSExP9jgeA/0jusJX+/fsrJydH0dHRSk1NVVSU7/8FGjdu7PO5vLxcvXr10tKlS88Y65xzzqlTDLGxsX6fU15eLkl64403fJKqdGodgVm2bt2qESNGaNasWRo0aJASExO1fPly/fGPf/Q71ueff/6Mv2xERkaaFiuAn0Zyh600btxY7du3r/XxF110kV566SW1aNHijOr1tJYtW+qDDz5Q3759JZ2qUHfs2KGLLrrorMd369ZNHo9HeXl5GjBgwBnfn+4cuN1u774uXbrI6XRq//79P1nxd+7c2bs48LRt27b98o/8D1u2bFF6erp+//vfe/f9+9//PuO4/fv368CBA0pNTfVeJyIiQh07dlRycrJSU1P1+eefa8SIEX5dH4A5WFAH/IwRI0aoefPmGjp0qDZv3qzi4mJt3LhR99xzj7744gtJ0r333qvHH39cq1at0p49e3T33Xf/7D3q5557rrKysnTbbbdp1apV3jFffvllSVJ6erocDofWrFmjr776SuXl5UpISNDkyZM1ceJELV68WEVFRfroo4/0pz/9ybtI7c4779TevXt1//33q6CgQMuWLVNubq5fv/f888/X/v37tXz5chUVFWnevHlnXRwYExOjrKwsffzxx9q8ebPuueceXXfddUpJSZEkzZo1S9nZ2Zo3b57+9a9/6ZNPPtGiRYv09NNP+xUPgLohuQM/Iy4uTps2bVKbNm00bNgwde7cWaNHj1ZlZaW3kr/vvvt0yy23KCsrSxkZGUpISNC11177s+Pm5OTod7/7ne6++2516tRJY8aMUUVFhSSpVatWmjVrlh588EElJydr3LhxkqSHH35Y06ZNU3Z2tjp37qzBgwfrjTfeUNu2bSWdmgd/7bXXtGrVKvXo0UMLFizQY4895tfvvfrqqzVx4kSNGzdOPXv21JYtWzRt2rQzjmvfvr2GDRumq666SgMHDlT37t19bnW7/fbb9cILL2jRokXq1q2bLrvsMuXm5npjBRBcDuOnVv0AAIAGicodAACLIbkDAGAxJHcAACyG5A4AgMWQ3AEAsBiSOwAAFkNyBwDAYkjuAABYDMkdAACLIbkDAGAxJHcAACzm/wPqUkE4ozJ38wAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "actual = [np.sign(test_labels) for test_labels in test_labels]\n",
+ "predicted = [np.sign(prediction) for prediction in predictions]\n",
+ "\n",
+ "confusion_matrix = metrics.confusion_matrix(actual, predicted)\n",
+ "\n",
+ "cm_display = metrics.ConfusionMatrixDisplay(confusion_matrix = confusion_matrix, display_labels = [1, -1])\n",
+ "\n",
+ "cm_display.plot()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b2b7b792-f575-46a6-a133-9852a31eb5a1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.925"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "test.Accuracy(test_labels,predictions)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "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.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/qdp/qdp_tutorial.ipynb b/examples/qdp/qdp_tutorial.ipynb
new file mode 100644
index 000000000..29351d4ad
--- /dev/null
+++ b/examples/qdp/qdp_tutorial.ipynb
@@ -0,0 +1,295 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Density matrix exponentiation\n",
+ "## Overview\n",
+ "This is a function to simulate DME. It follows the protocol laid down by https://arxiv.org/abs/2001.08838\n",
+ "\n",
+ "Goals: implement the unitary $U=e^{-i\\rho\\theta}$ on data qubit $\\sigma$ (rotate $\\sigma$ by $\\theta$) according to instruction given by instruction qubit $\\rho$.\n",
+ "\n",
+ "DME uses the relation (rotate $\\sigma$ by small angle $\\delta = \\sigma/N$).\n",
+ "$$\\begin{align}\n",
+ "Tr_\\rho [e^{-iSWAP\\delta}\\sigma\\otimes\\rho e^{iSWAP\\delta}] &= \\sigma - i\\delta[\\rho,\\sigma] +\\mathcal{O}(\\delta^2)\\\\\n",
+ "&= e^{-i\\rho\\delta}\\sigma e^{i\\rho\\delta}+\\mathcal{O}(\\delta^2)\n",
+ "\\end{align}$$\n",
+ "\n",
+ "Reference\n",
+ "- Kjaergaard, M., Schwartz, M. E., Greene, A., Samach, G. O., Bengtsson, A., O'Keeffe, M., ... Oliver, W. D. (2020). \n",
+ " Programming a quantum computer with quantum instructions. arXiv preprint arXiv:2001.08838. \n",
+ " Retrieved from https://arxiv.org/abs/2001.08838 \n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Code exlanation\n",
+ "In this code, we set $\\theta = \\pi, N =k$ with 1 work qubit and $k$ instruction qubits (it is possible to set more instruction qubits here, but they will just be idle). \n",
+ "\n",
+ "The instruction qubits are thrown out after usage (We call it Sequential Instruction Qubit protocol). Each query use, accordingly, k qubits. Each call use 1 query.\n",
+ "\n",
+ "Our goal is to rotate qubit 0 from state $\\ket{0}$ to state $\\ket{1}$ by using an $RX(\\pi)$ pulse. We demonstrate this by showing the number of counter that return 1."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qibo.models.qdp.memory_usage_query import DensityMatrixExponentiation\n",
+ "import numpy as np\n",
+ "from qibo import gates\n",
+ "theta = np.pi/2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Expectation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.2.9|INFO|2024-07-09 09:59:56]: Using qibojit (numba) backend on /CPU:0\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Circuit for DME, q0 is target qubit, q1,q2 and q3 are instruction qubit\n",
+ "q0: ─U───o─U─o─U─o─U───\n",
+ "q1: ─X─U─Z─U─Z─U─Z─U─M─\n",
+ "Final state = 0.707*|0> + 0.707*|1>\n"
+ ]
+ }
+ ],
+ "source": [
+ "k = 1\n",
+ "my_protocol = DensityMatrixExponentiation(theta=theta/2,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ "my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ "print('Circuit for DME, q0 is target qubit, q1,q2 and q3 are instruction qubit')\n",
+ "print(my_protocol.c.draw())\n",
+ "qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ "qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ "print(f'Final state = {qubit_0[0]}*|0> + {qubit_0[1]}*|1>')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Test with 3 iterations"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Final state = 0.985*|0> + 0.174*|1>\n"
+ ]
+ }
+ ],
+ "source": [
+ "k = 20\n",
+ "my_protocol = DensityMatrixExponentiation(theta=theta/2,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ "my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ "#print('Circuit for DME, q0 is target qubit, q1,q2 and q3 are instruction qubit')\n",
+ "#print(my_protocol.c.draw())\n",
+ "qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ "qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ "print(f'Final state = {qubit_0[0]}*|0> + {qubit_0[1]}*|1>')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Fidelity with analytic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qutip import ptrace, basis, tensor, Qobj, fidelity\n",
+ "from qutip.qip.operations import swap\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def compare_dme_analytic(num_iteration,theta):\n",
+ " my_protocol = DensityMatrixExponentiation(theta=theta,N=num_iteration,num_work_qubits=1,num_instruction_qubits=num_iteration,number_muq_per_call=1)\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=num_iteration) \n",
+ " c = my_protocol.c\n",
+ " result = c(nshots=1000)\n",
+ " qibo_state = Qobj(result.state().reshape((2**(num_iteration+1),1)), dims=[[2]*(num_iteration+1),[1]*(num_iteration+1)]).ptrace(0)\n",
+ "\n",
+ " \n",
+ " delta_SWAP = (swap(2,[0,1])*theta/num_iteration).expm()\n",
+ " sigma = basis(2,0) * Qobj(np.array([[1, 0]]))\n",
+ " rho = basis(2,1) * Qobj(np.array([[0, 1]]))\n",
+ " for i in range(num_iteration):\n",
+ " current_state = tensor(sigma,rho)\n",
+ " current_state = (delta_SWAP * tensor(sigma,rho))\n",
+ " sigma = current_state.ptrace(0)\n",
+ " qutip_state = sigma/(sigma.norm())\n",
+ "\n",
+ " #delta_SWAP = (swap(2,[0,1])).expm()\n",
+ " #sigma = basis(2,0) * Qobj(np.array([[1, 0]]))\n",
+ " #rho = basis(2,1) * Qobj(np.array([[0, 1]]))\n",
+ " #current_state = tensor(sigma,rho)\n",
+ " #current_state = (delta_SWAP * tensor(sigma,rho))\n",
+ " #sigma = current_state.ptrace(0)\n",
+ " #targeted_final_state = sigma/(sigma.norm())\n",
+ " \n",
+ " return(fidelity(qutip_state,qibo_state))#, fidelity(targeted_final_state,qutip_state)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Text(0.5, 1.0, 'Final state after Density Matrix Exponentiation with qibo.models.qdp')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmUAAAHHCAYAAAD+sy9fAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACMn0lEQVR4nOzdeVhU1f8H8PewDjsomyCK4q4I5kJoipZL4r5v30Qtyy01KtNSAU1NSyLNMuvnkqaZaVaaC5Fi5r7ivuQOiIBssg4z5/cHMjky4AzOMMC8X8/DA3Pn3Dufc+/c4TPnnHuuRAghQEREREQGZWLoAIiIiIiISRkRERFRpcCkjIiIiKgSYFJGREREVAkwKSMiIiKqBJiUEREREVUCTMqIiIiIKgEmZURERESVAJMyIiIiokqgyiRlt27dgkQiwdq1a/X6Ot7e3hgzZoxeX6Mq2b17N/z9/SGVSiGRSJCenm7okCq1MWPGwNvb29BhGEx4eDgkEomhwzB6+vq8rGqfj507d0bnzp01LtuiRQv9BgRg7dq1kEgkuHXrlnKZt7c3evfurffXNrTnef9IJBKEh4frNJ7y0mcslSYpK36jqvuZOXOmocPT2sWLFxEeHq5y4mlr48aNiIqK0llM2kpNTcXQoUNhZWWFFStWYP369bCxscHChQuxffv2Co3lyfeDmZkZatSogdatW2PatGm4ePFihcaijZycHISHh2P//v063e6T58vBgwdLPC+EgJeXFyQSSbk/7A1xnJ/WuXPnUj8XmjRpYtDYKgN9fEYcOnQI4eHh1fILWEJCAsLDw3HmzBlDh0KklpmhA3javHnzUK9ePZVlLVq0QN26dZGbmwtzc3MDRaadixcvIiIiAp07dy53y8nGjRtx/vx5TJ8+Xaexaer48ePIysrC/Pnz0bVrV+XyhQsXYvDgwejfv3+FxtOtWzeMHj0aQghkZGTg7NmzWLduHb766issXrwYoaGhFRqPOt9++y0UCoXycU5ODiIiIgBA42/s2pBKpdi4cSNeeuklleWxsbG4d+8eLC0ty73t8hzn2bNn6/xLVO3atbFo0aISyx0cHHT6OlVRaZ8Rz/N5eejQIURERGDMmDFwdHRUee7KlSswMak03+Wfae/evSqPExISEBERAW9vb/j7+xskptdeew3Dhw9/rnOTqq9Kl5T17NkTbdq0UfucVCqt4GiM24MHDwCgxAezPuTl5cHCwqLMD/xGjRrhf//7n8qyTz75BH369MG7776LJk2aIDg4WN+hlqmivzQEBwdjy5YtWLZsGczM/judN27ciNatWyMlJaVC4sjOzoaNjQ3MzMxU4tAFBweHEsedyiaRSPTyeVnVEgkLCwtDh1CCqakpTE1NDR0GVVJV5iuPujESY8aMga2tLeLj49G/f3/Y2trCxcUF7733HuRyucr6n332Gdq3b4+aNWvCysoKrVu3xs8//1zueH788Ue0bt0adnZ2sLe3h6+vL7744gsARV1LQ4YMAQB06dJF2d1S3IX166+/olevXvDw8IClpSV8fHwwf/58lZg7d+6MnTt34vbt28r1n2xxy8/PR1hYGBo0aABLS0t4eXlhxowZyM/Pf2bsf//9N4YMGYI6deoo133nnXeQm5ur8vohISEAgLZt20IikWDMmDGQSCTIzs7GunXrlHE9OUYgPj4e48aNg5ubGywtLdG8eXOsXr1a5fX3798PiUSCH3/8EbNnz4anpyesra2RmZmp1TEAgJo1a+LHH3+EmZkZFixYoPKcpvtIIpFgypQp2L59O1q0aKGMe/fu3SrlsrKyMH36dHh7e8PS0hKurq7o1q0bTp06pSzz5JiyW7duwcXFBQAQERGh3F/h4eFYs2YNJBIJTp8+XaJOCxcuhKmpKeLj459Z/xEjRiA1NRXR0dHKZQUFBfj5558xcuRItetoci6UdZyLx41dvHgRI0eOhJOTk7Kl7ukxZcX1fPo9sHDhQkgkEvzxxx/PrOOz5ObmokmTJmjSpInKe/jhw4eoVasW2rdvrzy3ij8zbty4gR49esDGxgYeHh6YN28ehBAq283Ozsa7774LLy8vWFpaonHjxvjss89KlNP0/QNod3789NNPWLBgAWrXrg2pVIpXXnkF169fV5Yr6zNC3edlXFwcxowZg/r160MqlcLd3R3jxo1Damqqskx4eDjef/99AEC9evWU2y0ehqFuTNCNGzcwZMgQ1KhRA9bW1njxxRexc+fOctVJnbi4OEgkEvz222/KZSdPnoREIsELL7ygUrZnz54ICAhQ2UfFLdT79+9H27ZtAQBjx45V1u3pcXcXL15Ely5dYG1tDU9PTyxZsqTM+Irl5+fjnXfegYuLC+zs7NC3b1/cu3evxPgjdWPKiu3du1c5hrdZs2bYtm1biTKa7G91njwGERER8PT0hJ2dHQYPHoyMjAzk5+dj+vTpcHV1ha2tLcaOHVvis7KwsBDz58+Hj48PLC0t4e3tjQ8//LBEOSEEPv74Y9SuXRvW1tbo0qULLly4oDau9PR0TJ8+XXmeNWjQAIsXL1bpcVBHk8/j0hw8eBBt27aFVCqFj48PvvnmG7XjYUs7pk8rXvfy5csYOnQo7O3tUbNmTUybNg15eXnPjOdJla6lLCMjo8S3e2dn51LLy+Vy9OjRAwEBAfjss8/w559/YunSpfDx8cHEiROV5b744gv07dsXo0aNQkFBAX788UcMGTIEO3bsQK9evbSKMTo6GiNGjMArr7yCxYsXAwAuXbqEf/75B9OmTUOnTp0wdepULFu2DB9++CGaNm0KAMrfa9euha2tLUJDQ2Fra4u//voLc+fORWZmJj799FMAwEcffYSMjAzcu3cPn3/+OQDA1tYWAKBQKNC3b18cPHgQb775Jpo2bYpz587h888/x9WrV585DmjLli3IycnBxIkTUbNmTRw7dgzLly/HvXv3sGXLFuXrN27cGKtWrVJ2Kfv4+KBr165444030K5dO7z55psAAB8fHwBAUlISXnzxReU/KRcXF+zatQuvv/46MjMzS3SxzJ8/HxYWFnjvvfeQn59f7m+1derUQVBQEPbt24fMzEzY29trvY8OHjyIbdu2YdKkSbCzs8OyZcswaNAg3LlzBzVr1gQATJgwAT///DOmTJmCZs2aITU1FQcPHsSlS5dK/HMAABcXF3z99deYOHEiBgwYgIEDBwIAWrZsiXr16mHy5Mn44Ycf0KpVK5X1fvjhB3Tu3Bmenp7PrLu3tzcCAwOxadMm9OzZEwCwa9cuZGRkYPjw4Vi2bFmJdTQ5F9avX1/qcS42ZMgQNGzYEAsXLiyRqBQbO3Ystm3bhtDQUHTr1g1eXl44d+4cIiIi8Prrr2vUsimXy9W2+FlZWcHGxgZWVlZYt24dOnTogI8++giRkZEAgMmTJyMjIwNr165VaZmQy+V49dVX8eKLL2LJkiXYvXs3wsLCUFhYiHnz5gEo+qfSt29f7Nu3D6+//jr8/f2xZ88evP/++4iPj1eek8U0ef9oe3588sknMDExwXvvvYeMjAwsWbIEo0aNwtGjRwGU/RmhTnR0NG7cuIGxY8fC3d0dFy5cwKpVq3DhwgUcOXIEEokEAwcOxNWrV7Fp0yZ8/vnnys/e4i8XT0tKSkL79u2Rk5ODqVOnombNmli3bh369u2Ln3/+GQMGDNCqTuq0aNECjo6OOHDgAPr27Qug6IuliYkJzp49q3LOHzp0SPl+fVrTpk0xb948zJ07F2+++SY6duwIAGjfvr2yTFpaGl599VUMHDgQQ4cOxc8//4wPPvgAvr6+yvOrNG+88QY2bNiAkSNHon379vjrr7+0+t9y7do1DBs2DBMmTEBISAjWrFmDIUOGYPfu3ejWrRsA7fe3OosWLYKVlRVmzpyJ69evY/ny5TA3N4eJiQnS0tIQHh6OI0eOYO3atahXrx7mzp2rUsd169Zh8ODBePfdd3H06FEsWrQIly5dwi+//KIsN3fuXHz88ccIDg5GcHAwTp06he7du6OgoEAllpycHAQFBSE+Ph5vvfUW6tSpg0OHDmHWrFlITEwsc7yktp/Hxc6dO4fu3bvDxcUF4eHhKCwsRFhYGNzc3EqU1faYDh06FN7e3li0aBGOHDmCZcuWIS0tDd9//32p65QgKok1a9YIAGp/hBDi5s2bAoBYs2aNcp2QkBABQMybN09lW61atRKtW7dWWZaTk6PyuKCgQLRo0UK8/PLLKsvr1q0rQkJCyox12rRpwt7eXhQWFpZaZsuWLQKA2LdvX4nnno5FCCHeeustYW1tLfLy8pTLevXqJerWrVui7Pr164WJiYn4+++/VZavXLlSABD//PNPmfGre/1FixYJiUQibt++rVxWfEyOHz+uUtbGxkbtPnr99ddFrVq1REpKisry4cOHCwcHB+Xr7tu3TwAQ9evXVxuLOgDE5MmTS31+2rRpAoA4e/asEEK7fQRAWFhYiOvXryuXnT17VgAQy5cvVy5zcHAoMwYhit6TTx6z5ORkAUCEhYWVKDtixAjh4eEh5HK5ctmpU6dKvM/VefLYfPnll8LOzk65L4cMGSK6dOkihCh6P/fq1UtlXU3PhdKOc1hYmAAgRowYUepzT0pMTBQ1atQQ3bp1E/n5+aJVq1aiTp06IiMjo8w6CiFEUFBQqZ8Lb731lkrZWbNmCRMTE3HgwAHl+RcVFaVSpvgz4+2331YuUygUolevXsLCwkIkJycLIYTYvn27ACA+/vhjlfUHDx4sJBKJyntF0/ePtudH06ZNRX5+vrLcF198IQCIc+fOKZeV9hmh7vNS3bm2adMmAUAcOHBAuezTTz8VAMTNmzdLlH/683H69OkCgMp5lpWVJerVqye8vb2V721t6qROr169RLt27ZSPBw4cKAYOHChMTU3Frl27hBD/nTu//vqrslxQUJAICgpSPj5+/Hip51fxe+37779XLsvPzxfu7u5i0KBBZcZ35swZAUBMmjRJZfnIkSNLnP/F5+6T+7du3boCgNi6datyWUZGhqhVq5Zo1aqVcpmm+1ud4mPQokULUVBQoFw+YsQIIZFIRM+ePVXKBwYGqry3iuv4xhtvqJR77733BADx119/CSGEePDggbCwsBC9evUSCoVCWe7DDz8UAFTeP/Pnzxc2Njbi6tWrKtucOXOmMDU1FXfu3FEue3o/avJ5rE7//v2FVCpV+V938eJFYWpqqvLZpc0xLf7c69u3r0rZSZMmqfxf0kSl675csWIFoqOjVX6eZcKECSqPO3bsiBs3bqgss7KyUv6dlpaGjIwMdOzYUaOmzqc5OjoiOztbo9jUeTKWrKwspKSkoGPHjsjJycHly5efuf6WLVvQtGlTNGnSBCkpKcqfl19+GQCwb98+jV8/OzsbKSkpaN++PYQQarvTNCGEwNatW9GnTx8IIVTi6tGjBzIyMkrs65CQEJVYnkdxC0FWVhYA7fdR165dVVqCWrZsCXt7e5X3kaOjI44ePYqEhASdxDx69GgkJCSoxPLDDz/AysoKgwYN0ng7Q4cORW5uLnbs2IGsrCzs2LGj1K5LQHfnwtPnXWnc3d2V53XHjh1x5swZrF69Gvb29hqt7+3tXeIzITo6ukTLUnh4OJo3b46QkBBMmjQJQUFBmDp1qtptTpkyRfl3cctVQUEB/vzzTwDAH3/8AVNT0xLrv/vuuxBCYNeuXSrLn/X+Kc/5MXbsWJXW4+KWnac/2zT15HHPy8tDSkoKXnzxRQAo1+cgULSf2rVrp3Khia2tLd58803cunWrxJXR5a1T8fszOzsbQFHLZHBwMPz9/fH3338DKGo9k0gkJS560Yatra3K+EULCwu0a9fumfEVd8M//X7R5iItDw8PlZYue3t7jB49GqdPn8b9+/eVr6PN/lZn9OjRKmNfAwICIITAuHHjVMoFBATg7t27KCwsVKnj0xdUvfvuuwCg7EL9888/UVBQgLffflulO1DdvtiyZQs6duwIJycnlXOia9eukMvlOHDgQKn1KM/nsVwux549e9C/f3/UqVNHubxp06bo0aOHStnyHNPJkyerPH777bdVtqWJStd92a5du1IH+qsjlUpLNK07OTkhLS1NZdmOHTvw8ccf48yZMyr93+WZU2nSpEn46aef0LNnT3h6eqJ79+4YOnQoXn31VY3Wv3DhAmbPno2//vqrxDiqjIyMZ65/7do1XLp0qdQuheIB+qW5c+cO5s6di99++63EftLk9dVJTk5Geno6Vq1ahVWrVmkU19NX2T6PR48eAQDs7OwAaL+PnjxBiz39PlqyZAlCQkLg5eWF1q1bIzg4GKNHj0b9+vXLFXO3bt1Qq1Yt/PDDD3jllVegUCiwadMm9OvXT1kPTbi4uKBr167YuHEjcnJyIJfLMXjw4FLL6+pc0Ob4DR8+HBs2bMDOnTvx5ptv4pVXXtF4XRsbG5Wrf0tjYWGB1atXK8eKFI9ne5qJiUmJY9aoUSMAUI7zuX37Njw8PEoch+IhCLdv31ZZ/qz3T3nOj6e36eTkBAAlzllNPXz4EBEREfjxxx9LvFZ5z/vbt2+rjOEq9uR+enLur/LWqWPHjigsLMThw4fh5eWFBw8eoGPHjrhw4YJKUtasWTPUqFGjXHUBiq70ffo94+TkhLi4uDLXu337NkxMTEp08Tdu3Fjj127QoEGJ137yfenu7q71/lbn6WNQfBWzl5dXieUKhQIZGRmoWbOmso4NGjRQKefu7g5HR0flOVH8u2HDhirlXFxclMe72LVr1xAXF1eu/2Xl+TxOTk5Gbm5uidiAomP1ZPJUnmP69HZ9fHxgYmKi1dRYlS4p05YmV7H8/fff6Nu3Lzp16oSvvvoKtWrVgrm5OdasWYONGzdq/Zqurq44c+YM9uzZg127dmHXrl1Ys2YNRo8ejXXr1pW5bnp6OoKCgmBvb4958+bBx8cHUqkUp06dwgcffPDMwY1A0ZgyX19f5diZpz19cj1JLpejW7duePjwIT744AM0adIENjY2iI+Px5gxYzR6/dJiAoD//e9/ygsEntayZUuVx7pqJQOA8+fPw9TUVJkoaLuPSnsfiSfGSg0dOhQdO3bEL7/8gr179+LTTz/F4sWLsW3btmeON1HH1NQUI0eOxLfffouvvvoK//zzDxISEsp1peHIkSMxfvx43L9/Hz179iz1illdngvaHL/U1FScOHECQNFAaoVCoZepFfbs2QOgqCXo2rVrOk38y/Ks9095zg9N3pPaGDp0KA4dOoT3338f/v7+sLW1hUKhwKuvvlru815b5a1TmzZtIJVKceDAAdSpUweurq5o1KgROnbsiK+++gr5+fn4+++/NRpTpY/4qpLS6qhp3XU5ObRCoUC3bt0wY8YMtc8XJ6Xq6PrzWB/Ks6+qfFKmia1bt0IqlWLPnj0ql3SvWbOm3Nu0sLBAnz590KdPHygUCkyaNAnffPMN5syZo/YbT7H9+/cjNTUV27ZtQ6dOnZTLb968WaJsadvw8fHB2bNn8corr2h90M+dO4erV69i3bp1GD16tHK5Nl2x6l6z+OoUuVyuUauGLt25cwexsbEIDAxUtmw8zz4qS61atTBp0iRMmjQJDx48wAsvvIAFCxaU+iHwrNcePXo0li5dit9//x27du2Ci4tLiWZ0TQwYMABvvfUWjhw5gs2bN5daTptzQZf7bfLkycjKysKiRYswa9YsREVF6Xxeubi4OMybNw9jx47FmTNn8MYbb+DcuXMl5jNTKBS4ceOGygf+1atXAUB59WLdunXx559/IisrS6W1rHh4Qd26dbWKTV/nh6bHKC0tDTExMYiIiFAZuH3t2rVybxMo2g9Xrlwpsby8+6k0xd2If//9N+rUqaPs9uzYsSPy8/Pxww8/ICkpSeUzVR193W2ibt26UCgU+Pfff1VaUtTtm9Jcv34dQgiVGNW9Lytif6tTXMdr164pW+aAoosP0tPTla9d/PvatWsqrVbJycklWkR9fHzw6NGjcp8T2n4eu7i4wMrKSu37/un9Wp5j+vQXwevXr0OhUGg1V2mlG1OmD6amppBIJCpTTty6davcs5U/eQk5UNQdUvwtt7g7yMbGBgBKzIpd/G3kyW8fBQUF+Oqrr0q8jo2NjdpuhaFDhyI+Ph7ffvttiedyc3OV4y7UUff6QgjldB6asLGxUVuvQYMGYevWrTh//nyJdZKTkzXevjYePnyIESNGQC6X46OPPlIuf559pI5cLi9xLFxdXeHh4VHmNCTW1tYASr4PirVs2RItW7bEd999h61bt2L48OHlmufL1tYWX3/9NcLDw9GnT59Sy2lzLqg7zuXx888/Y/Pmzfjkk08wc+ZMDB8+HLNnz1b+w9EFmUyGMWPGwMPDA1988QXWrl2LpKQkvPPOO2rLf/nll8q/hRD48ssvYW5uruxWDQ4OhlwuVykHAJ9//jkkEonW38T1dX6U9hmh7vWBkq0e6q5uK+2zS53g4GAcO3YMhw8fVi7Lzs7GqlWr4O3tjWbNmj1zG5rq2LEjjh49in379imTMmdnZzRt2lR5FXzx8tJoUzdtFL8fnr7aWZu7LSQkJKhcwZiZmYnvv/8e/v7+cHd3B6D5/i4en6zLeQqLr5R+uk7FvRHFVyV27doV5ubmWL58ucr7Td2+GDp0KA4fPqxs4X5Senq6cjzb0zT9PE5JScHly5eRk5MDoOg86NGjB7Zv3447d+4oy126dKlEDOU5pitWrFB5vHz5cpVtacIoWsp69eqFyMhIvPrqqxg5ciQePHiAFStWoEGDBs8cK6DOG2+8gYcPH+Lll19G7dq1cfv2bSxfvhz+/v7KbxD+/v4wNTXF4sWLkZGRAUtLS7z88sto3749nJycEBISgqlTp0IikWD9+vVqm8dbt26NzZs3IzQ0FG3btoWtrS369OmD1157DT/99BMmTJiAffv2oUOHDpDL5bh8+TJ++ukn7Nmzp9RxeU2aNIGPjw/ee+89xMfHw97eHlu3btVqnErr1q3x559/IjIyEh4eHqhXrx4CAgLwySefYN++fQgICMD48ePRrFkzPHz4EKdOncKff/6Jhw8far2vn3T16lVs2LABQghkZmbi7Nmz2LJlCx49eqQ8vsWeZx+pk5WVhdq1a2Pw4MHw8/ODra0t/vzzTxw/fhxLly4tdT0rKys0a9YMmzdvRqNGjVCjRg20aNFCZdzH6NGj8d577wHAc02SWlq32JO0ORdKO87aePDgASZOnIguXbooB9d/+eWX2LdvH8aMGYODBw8+sxszIyMDGzZsUPtc8f4qHiMXExMDOzs7tGzZEnPnzsXs2bMxePBglak3pFIpdu/ejZCQEAQEBGDXrl3YuXMnPvzwQ+XYlj59+qBLly746KOPcOvWLfj5+WHv3r349ddfMX369BLjTDShj/OjtM+Ip9nb26NTp05YsmQJZDIZPD09sXfvXrUt9K1btwZQNOXG8OHDYW5ujj59+igTmifNnDlTOR3L1KlTUaNGDaxbtw43b97E1q1bddpF3bFjRyxYsAB3795VSb46deqEb775Bt7e3qhdu3aZ2/Dx8YGjoyNWrlwJOzs72NjYICAg4Lm7uf39/TFixAh89dVXyMjIQPv27RETE/PMOdie1KhRI7z++us4fvw43NzcsHr1aiQlJam0Ymu6v48dO4YuXbogLCxMZ/do9PPzQ0hICFatWqUchnPs2DGsW7cO/fv3R5cuXQBAOVfookWL0Lt3bwQHB+P06dPYtWtXiemt3n//ffz222/o3bs3xowZg9atWyM7Oxvnzp3Dzz//jFu3bqmdEkvTz+Mvv/wSERER2Ldvn3K+uoiICOzevRsdO3bEpEmTUFhYiOXLl6N58+Yqn4HlOaY3b95E37598eqrr+Lw4cPK6TT8/Pw039EaX6epZ6VNv1CstCkxbGxsSpRVd1n+//3f/4mGDRsKS0tL0aRJE7FmzRq15TSZEuPnn38W3bt3F66ursLCwkLUqVNHvPXWWyIxMVGl3Lfffivq16+vvNS2eHqMf/75R7z44ovCyspKeHh4iBkzZog9e/aUmELj0aNHYuTIkcLR0VEAULk8uaCgQCxevFg0b95cWFpaCicnJ9G6dWsRERHxzKkGLl68KLp27SpsbW2Fs7OzGD9+vPIS/if3b2nH5PLly6JTp07CysqqxCXOSUlJYvLkycLLy0uYm5sLd3d38corr4hVq1YpyxRfmr1ly5Yy43wSnpgKwcTERDg6OopWrVqJadOmiQsXLqhdR9N9hFKm23jyvZCfny/ef/994efnJ+zs7ISNjY3w8/MTX331lco6T0+JIYQQhw4dEq1btxYWFhZqp8dITEwUpqamolGjRhrvj2edL0/W4ekpMTQ9F0o7zsVli6ePeNLT2xk4cKCws7MTt27dUin366+/CgBi8eLFZcZf1pQYxa9z8uRJYWZmpjLNhRBCFBYWirZt2woPDw+RlpYmhPjvM+Pff/8V3bt3F9bW1sLNzU2EhYWVmE4gKytLvPPOO8LDw0OYm5uLhg0bik8//VTlMn8hNHv/FHue80PdZ2BpnxHqyt67d08MGDBAODo6CgcHBzFkyBCRkJCg9j05f/584enpKUxMTFSmb1BXp3///VcMHjxYODo6CqlUKtq1ayd27NihUkabOpUmMzNTmJqaCjs7O5XpiDZs2CAAiNdee63EOk9PiSFE0XuvWbNmwszMTOW1g4KCRPPmzUtsQ905rU5ubq6YOnWqqFmzprCxsRF9+vQRd+/e1XhKjF69eok9e/aIli1bKs9NdZ+R2uzvJ1+3tGNQ2meJuvNcJpOJiIgIUa9ePWFubi68vLzErFmzVKZyEkIIuVwuIiIiRK1atYSVlZXo3LmzOH/+vNr3T1ZWlpg1a5Zo0KCBsLCwEM7OzqJ9+/bis88+U5m648n6aPp5XFyHp6emio2NVX4m169fX6xcuVLtZ6Cmx7R43YsXL4rBgwcLOzs74eTkJKZMmSJyc3OFNiSPK0tEBpKSkoJatWph7ty5mDNnjqHDqdbGjBmDn3/+WXm1LpG+SSQSnbZYkX6Eh4cjIiKiXBd1FK+bnJxc5mT3mjCKMWVEldnatWshl8vx2muvGToUIiIyIKMYU0ZUGf3111+4ePEiFixYgP79+2t1hQ4REVU/TMqIDGTevHk4dOgQOnTooLxKh4iIjBfHlBERERFVAhxTRkRERFQJMCkjIiIiqgQ4pkwNhUKBhIQE2NnZ6e22HERERKRbQghkZWXBw8NDL/fX1TcmZWokJCSUeVNvIiIiqrzu3r37zDs8VEZMytQovgHx3bt3YW9vb9BYZDIZ9u7di+7du8Pc3NygsVQ0Y627sdYbYN2Nse7GWm+AdddH3TMzM+Hl5aX8P17VMClTo7jL0t7evlIkZdbW1rC3tzfKk9YY626s9QZYd2Osu7HWG2Dd9Vn3qjr0qOp1uBIRERFVQ0zKiIiIiCoBJmVERERElQCTMiIiIqJKgEkZERERUSXApIyIiIioEmBSRkRERFQJMCkjIiIiqgSYlBERERFVAkzKiIiIiCoBJmVERERElQCTMiIiIqJKgDckJyIiogqTJ5MjKT0XGQWGjqTyYVJGRERE5SKEwKP8QjzMLkBqdgHSHv9++Pgn9VEBHmbnK59/mF2AnAI5ACDAxQQjDBx/ZcOkjIiIiAAUJVkZuTKkPCpAWk5xUlWUWJVMtgrwMKcABYUKrV/H3FQC7deq/piUERERVWMKRXGilY/krHwkP8pHyqMC5eOUR49/sgqQmp0PmVxo/RpW5qaoYWOBmrYWqGFT9FPTxgI1bCwf/7ZADVsL1LAu+i01Edi1a5cealu1MSkjIiKqYhQKgfTHiVbK40SrKMEqUCZZxQlX6qMCFCq0S7TspGb/JVPFiZVtUaLlZP3f30XJlyWsLEy12r5MJtOqvLFgUkZERFRJyBUCqdn5eJCZj6TMPCQ9/v0g67+/y5toOViZw8XOEs62FnC2tYSzrSVc7CzhYmsJZzsL5eOaNpawMOPkDIbApIyIiEjPhBBIy5E9TrTy8CAzHwnpOTh+wwS//3AayY8KkJRZ1OIl1yLZcrQ2f5xg/ZdUOdv+l2i52ErhbGfBRKuKYFJGRET0HHIKCpGQnofEjNz/WrYy8/Ag67/WrgdZeaWM1TIBkpJVl0gAZ1tLuNlL4WZvCVd7Kdzsiv+2ZKJVjTEpIyIiKkVBoQL3M/KQkJGLxIxcJKTnISE9F4kZ//3OyNV8fFRNG4uiJMveEi62Fsi8fxftX2gODycbuNkXJWI1bSxgZspkyxgxKSMiIqMkVwgkZ+UjPr0o4UpMf5x8PW71ik8vGr+lCTtLM9RylD5u3ZIqEyxXu//+drZVbdmSyWT444/bCG7nBXNzc31Vk6oQgydlK1aswKeffor79+/Dz88Py5cvR7t27dSWlclkWLRoEdatW4f4+Hg0btwYixcvxquvvqosI5fLER4ejg0bNuD+/fvw8PDAmDFjMHv2bEgkkoqqFhERGVhOQSHupeXi7sMc3EvLRcLjlq7Exy1cSZl5Gg2WtzQzgYejFWo5SFHLwQoejv/9Ll5uJ2VSRc/PoEnZ5s2bERoaipUrVyIgIABRUVHo0aMHrly5AldX1xLlZ8+ejQ0bNuDbb79FkyZNsGfPHgwYMACHDh1Cq1atAACLFy/G119/jXXr1qF58+Y4ceIExo4dCwcHB0ydOrWiq0hERHoikyuQkJ6Luw9zcTctB3cf5uCuMgnLQcqjZ9/Hx9REAnd7KWo5PE6wHKXwcLD677GDFDVsLPilniqEQZOyyMhIjB8/HmPHjgUArFy5Ejt37sTq1asxc+bMEuXXr1+Pjz76CMHBwQCAiRMn4s8//8TSpUuxYcMGAMChQ4fQr18/9OrVCwDg7e2NTZs24dixYxVUKyIi0gWFQuBBVv5/CdcTyde9tKIux2c1dNlLzeBVwxq1nazg4WgFDwcrleTLxc4SpiZMuKhyMFhSVlBQgJMnT2LWrFnKZSYmJujatSsOHz6sdp38/HxIpVKVZVZWVjh48KDycfv27bFq1SpcvXoVjRo1wtmzZ3Hw4EFERkaWGkt+fj7y8/8bN5CZmQmgqLvU0BPcFb++oeMwBGOtu7HWG2Ddn/xtLB5m5eLuI+D3s/FIzCzAvbRc5U98Rt4zb+FjaWaC2k5WqO1kBa/Hv2s7/vfY3qrsbkWFvBAKuS5rpDljPeaA/upe1felRAih/f0UdCAhIQGenp44dOgQAgMDlctnzJiB2NhYHD16tMQ6I0eOxNmzZ7F9+3b4+PggJiYG/fr1g1wuVyZVCoUCH374IZYsWQJTU1PI5XIsWLBAJfl7Wnh4OCIiIkos37hxI6ytrXVQWyIi41UgB1LygAd5EiTnAcm5EuXfj2Rlt1KZQMDREqhpKVDDEnCWFv2uKRWoaQnYmQPsWaRiOTk5GDlyJDIyMmBvb2/ocLRm8IH+2vjiiy8wfvx4NGnSBBKJBD4+Phg7dixWr16tLPPTTz/hhx9+wMaNG9G8eXOcOXMG06dPh4eHB0JCQtRud9asWQgNDVU+zszMhJeXF7p3727wgyqTyRAdHY1u3boZ3dU5xlp3Y603wLpX5brL5ArEp+fiZkoObqXm4FZqNm49/jshI6/Mde3MBXzcHJTdjMoWLycr1LKXVtvpIar6MX8e+qp7cU9XVWWwpMzZ2RmmpqZISkpSWZ6UlAR3d3e167i4uGD79u3Iy8tDamoqPDw8MHPmTNSvX19Z5v3338fMmTMxfPhwAICvry9u376NRYsWlZqUWVpawtLSssRyc3PzSnOiVKZYKpqx1t1Y6w2w7pW17gqFwP3MPNxMyVb5uZWSjTsPc8q8ktFeaoZ6Lrao72yDes428Ha2QX1nG3g6WOBAzF4EB79Yaeutb5X5mOubrute1fejwZIyCwsLtG7dGjExMejfvz+Aoq7HmJgYTJkypcx1pVIpPD09IZPJsHXrVgwdOlT5XE5ODkxMVL9VmZqaQqEoe1wCEREVyZPJcf3BI1xNysL1B4/+S75Ss5EnK/2zVGpuAu+aNqjv8jjxUv5tCydrc7VXMFb1MUBEumTQ7svQ0FCEhISgTZs2aNeuHaKiopCdna28GnP06NHw9PTEokWLAABHjx5FfHw8/P39ER8fj/DwcCgUCsyYMUO5zT59+mDBggWoU6cOmjdvjtOnTyMyMhLjxo0zSB2JiCqrgkIFbqQ8wtWkR7h6PwtXk4p+7jzMKfWqRjMTCerUsEa9p1q86rnYwM1OChNeyUhUbuVKyq5cuYLly5fj0qVLAICmTZvi7bffRuPGjbXazrBhw5CcnIy5c+fi/v378Pf3x+7du+Hm5gYAuHPnjkqrV15eHmbPno0bN27A1tYWwcHBWL9+PRwdHZVlli9fjjlz5mDSpEl48OABPDw88NZbb2Hu3LnlqSoRUZVXKFfgVmqOMukq+ilqASvt5teO1uZo5GaHhq62qP9Et2NtJ6tqO8aLyNC0Tsq2bt2K4cOHo02bNsqrJo8cOYIWLVrgxx9/xKBBg7Ta3pQpU0rtrty/f7/K46CgIFy8eLHM7dnZ2SEqKgpRUVFaxUFEVNXJFQJ3H+aoJF5Xk7JwIzkbBXL13Y52lmZo5G6HRm62aOhqh8budmjoZgsXW0tOmEpUwbROymbMmIFZs2Zh3rx5KsvDwsIwY8YMrZMyIiLSjhACSZn5uJiY8V/X44Oi8V+ljfmytjBFQ1dbNHSzQ2O3osSrsbsd3O2lTL6IKgmtk7LExESMHj26xPL//e9/+PTTT3USFBERFRFCICEjD+fuZeBCQgbOxWfgfHxGqbcQsjAzQQMXW2WLV2M3OzRys4OnoxXHexFVclonZZ07d8bff/+NBg0aqCw/ePAgOnbsqLPAiIiMjRDA3bQcXE7Kwfn4ogTsQkImHmaXTMBMJEADV1s0UrZ8FXU91qlhzdsGEVVRWidlffv2xQcffICTJ0/ixRdfBFA0pmzLli2IiIjAb7/9plKWiIhKEkLgdmoOzj9u/Tp3Nx1nbpsi58jBEmXNTCRo6GYHX097tPB0QAtPBzR1t4eVhakBIicifdE6KZs0aRIA4KuvvsJXX32l9jkAkEgkkMsNdEMxIqJKRKEQuJmajfOPux6LW8Cy8gqfKimBuakEjd3t4OvpgOYeDvD1dEBjdztIzZmAEVV3WidlnISViKh0QgjcSs3BmbtpOHcvE+cTMnAxIROP8p9OwIrGfzV1t3vc8mWL9BtxGDvwVdhYlbzDCBFVf1Xq3pdERJWNTK7AhYRMnLj1EMdvPcTJ22lqB+FLzU3QtJY9fD0d0MKjqAuyoZstzB/P+SWTyfDHgzhYmHEOMCJjpVFStmzZMrz55puQSqVYtmxZmWWnTp2qk8CIiCqjzDwZTt1Ow8nbaTh+6yHO3E0vMQ2FhZkJfD2Luh5bPP7t42LDSVeJqEwaJWWff/45Ro0aBalUis8//7zUchKJhEkZEVUrCem5OH7rIU7cSsOJ22m4fD8T4qlJ8B2tzdGmrhPaeNdAW28ntPB0gKUZx4ARkXY0Sspu3ryp9m8ioupErhC4cj8LJ28/xPFbaThx6yESMvJKlKtb0xpt6tZAG28ntPV2Qn1nW84BRkTPTesxZfPmzcN7770Ha2trleW5ubn49NNPeY9JIqoycgvkOHM3HSduPcSJ22k4dTsNWU8NyDc1kaC5h70yCWtT1wmu9lIDRUxE1ZnWSVlERAQmTJhQIinLyclBREQEkzIiqrSy8wtx+N9UHLmRihO303A+PgOFT92Q28bCFC/UdUKbukVdkX5ejrCx5DVRRKR/Wn/SCCHU3ift7NmzqFGjhk6CIiLSBSEELiVm4cC1ZMReScaJ2w8hk6smYW72lmjrXUM5JqyJux0H5BORQWiclDk5OUEikUAikaBRo0YqiZlcLsejR48wYcIEvQRJRKSptOwC/H09BbFXkvH3tWQ8yMpXed6rhhVeauCCdvWKWsNqO1nxhtxEVClonJRFRUVBCIFx48YhIiICDg4OyucsLCzg7e2NwMBAvQRJRFSaQrkCZ++lI/ZKMmKvpSDuXrrK1ZFW5qYI9KmJTg2dEdTYFd41rZmEEVGlpHFSFhISAgCoV68e2rdvD3Nzc70FRURUloT0XBy4mowD15Jx8FoKMp+6XVETdzt0auSCoEYuaOPtxOkpiKhK0HpMWb169ZCYmFjq83Xq1HmugIiInpYnk+P4rYdFrWFXk3HtwSOV5x2szNGxoTM6NXJBp4YucHfg1ZFEVPVonZR5e3uX2fTPm5AT0fMSQuD6g0c4cLUoCTt6M1Vl1nwTCeDv5ahsDWtZ2xGmnCeMiKo4rZOy06dPqzyWyWQ4ffo0IiMjsWDBAp0FRkTGpaBQgX1XkrH5hgmWRP6N+HTVSVvd7aXo1MgZQY1c0aFBTThaWxgoUiIi/dA6KfPz8yuxrE2bNvDw8MCnn36KgQMH6iQwIqr+ZHIFDv2bih1nE7Dnwv3HY8NMAOTBwtQE7erVQFAjF3Rq5IJGbrYcoE9E1ZrOZkRs3Lgxjh8/rqvNEVE1JVcIHL2Rit/jErH7fCLScmTK59zsLNHIJheju7VGh4ausLbgpK1EZDy0/sTLzMxUeSyEQGJiIsLDw9GwYUOdBUZE1YdCIXDyThp+P5uAP87dR8qj/+YOq2ljgWDfWujdshb8Pe2we/cudG7kAnNzJmREZFy0/tRzdHQs0YUghICXlxd+/PFHnQVGRFWbEAJn7qZjR1widsYl4n7mf2PEHK3N8Wpzd/Ru6YEX69dQzqAvk8lK2xwRUbWndVK2b98+lccmJiZwcXFBgwYNYGbGb7ZExkwIgQsJmfg9LgE74xJxLy1X+ZydpRm6N3dHb79aeKmBM8x5KyMiIhVaZ1FBQUH6iIOIqrAr97OwIy4BO+IScTMlW7nc2sIUXZu6oXfLWujUyAVSc07iSkRUGq2Tst9++03jsn379tV280RURfyb/Ag7ziZiR1yCymSulmYmeKWpK3q39ECXxq6wsmAiRkSkCa2Tsv79+0MikUA8eXM5oMQyiUTCiWSJqpk7qTn4/XGL2KXE/y76sTA1QadGLujjVwuvNHWDrSWHMhARaUvrT869e/figw8+wMKFC5U3ID98+DBmz56NhQsXolu3bjoPkogMp6BQgV3nE/H94ds4eTtNudzMRIKXGjqjd0sPdGvmBgcr3g+XiOh5aJ2UTZ8+HStXrsRLL72kXNajRw9YW1vjzTffxKVLl3QaIBEZRlJmHn44egcbj95RTmFhIgECfWqid0sPvNrcHU42nFWfiEhXtE7K/v33Xzg6OpZY7uDggFu3bukgJCIyFCEETt5Ow7rDt7HrXCIKFUVDElztLDEqoC5GtPOCqz1v9k1EpA9aJ2Vt27ZFaGgo1q9fDzc3NwBAUlIS3n//fbRr107nARKR/uXJ5PjtTALWHb6FCwn/jRVrU9cJIe298WoLd05hQUSkZ1onZatXr8aAAQNQp04deHl5AQDu3r2Lhg0bYvv27bqOj4j06F5aDjYcuYPNx+8ob3dkaWaCfv4eGB3ojRaeDgaOkIjIeGidlDVo0ABxcXGIjo7G5cuXAQBNmzZF165debNgoipACIHD/6Zi7aFb+PNSEh73UMLT0QqvBdbFsDZeHCtGRGQA5bpuXSKRoHv37ujevbuu4yEiPcnOL8S20/H4/tAtlXnFOjSoiZBAb7zS1A2mJvxiRURkKJxMiKiau5mSje8P38LPJ+4hK78QQNFM+4NeqI3RgXXR0M3OwBESERHApIyoWlIoBGKvJmPtoVuIvZqsXF7P2QajA+tiUOvasJdyXjEiosqESRlRNZKRK8OWE3ex/sht3E7NAQBIJECXxq4YHVgXnRq6wIRdlERElRKTMqJq4PqDLKz55xa2nYpHrqzo9mZ2UjMMa+OF/71YF97ONgaOkIiInqVcSZlCocD169fx4MEDKBQKlec6deqkk8CI6NkS0nMRGX0VW0/dQ/GtZxu72WF0+7oY0MoT1hb83kVEVFVo/Yl95MgRjBw5Erdv31Z7U3LehJxI/zJyZfh6/79Y889N5BcWfTHq1swN4zrUw4v1a3B6GiKiKkjrKbonTJiANm3a4Pz583j48CHS0tKUPw8fPtQ6gBUrVsDb2xtSqRQBAQE4duxYqWVlMhnmzZsHHx8fSKVS+Pn5Yffu3SXKxcfH43//+x9q1qwJKysr+Pr64sSJE1rHRlTZ5BfK8d3fNxD06T6sjP0X+YUKtKtXA79Mao9vR7dBoE9NJmRERFWU1i1l165dw88//4wGDRo894tv3rwZoaGhWLlyJQICAhAVFYUePXrgypUrcHV1LVF+9uzZ2LBhA7799ls0adIEe/bswYABA3Do0CG0atUKAJCWloYOHTqgS5cu2LVrF1xcXHDt2jU4OTk9d7xEhqJQCPx6Nh6f7bmK+PRcAEBDV1t88GoTvNLUlYkYEVE1oHVSFhAQgOvXr+skKYuMjMT48eMxduxYAMDKlSuxc+dOrF69GjNnzixRfv369fjoo48QHBwMAJg4cSL+/PNPLF26FBs2bAAALF68GF5eXlizZo1yvXr16j13rESG8ve1ZCz64zIuJhbdk9LN3hKh3Rph0Au1Ycb7URIRVRsaJWVxcXHKv99++228++67uH//Pnx9fWFurjrXUcuWLTV64YKCApw8eRKzZs1SLjMxMUHXrl1x+PBhtevk5+dDKpWqLLOyssLBgweVj3/77Tf06NEDQ4YMQWxsLDw9PTFp0iSMHz++1Fjy8/ORn5+vfJyZWfTPTyaTQSaTaVQffSl+fUPHYQjGWvfi+p698xCf/3UT//ybCgCwtTTDWx29ERJYF1YWphAKOWSK6jWG01iPOWC8dTfWegOs+5O/db3dqkoinh6tr4aJiQkkEkmJgf3KjTx+TpuB/gkJCfD09MShQ4cQGBioXD5jxgzExsbi6NGjJdYZOXIkzp49i+3bt8PHxwcxMTHo168f5HK5MqkqTtpCQ0MxZMgQHD9+HNOmTcPKlSsREhKiNpbw8HBERESUWL5x40ZYW1trVB8iXUnNA3beNcHJlKJWMFOJwEvuAt09FbDlfK9ERKXKycnByJEjkZGRAXt7e0OHozWNWspu3ryp7zg08sUXX2D8+PFo0qQJJBIJfHx8MHbsWKxevVpZRqFQoE2bNli4cCEAoFWrVjh//nyZSdmsWbMQGhqqfJyZmQkvLy90797d4AdVJpMhOjoa3bp1K9EqWd0ZW93TcgrwdexNbIi7A5m86AtQb193vNO1AerUMI4vB8Z2zJ9krHU31noDrLs+6l7c01VVaZSU1a1bV/n3gQMH0L59e5iZqa5aWFiIQ4cOqZQti7OzM0xNTZGUlKSyPCkpCe7u7mrXcXFxwfbt25GXl4fU1FR4eHhg5syZqF+/vrJMrVq10KxZM5X1mjZtiq1bt5Yai6WlJSwtLUssNzc3rzQnSmWKpaJV97rnyeRY888tfLX/OrLyiu5N2chBgcUj2qOVd00DR2cY1f2Yl8VY626s9QZYd13WvarvR61HCXfp0kXt1BcZGRno0qWLxtuxsLBA69atERMTo1ymUCgQExOj0p2pjlQqhaenJwoLC7F161b069dP+VyHDh1w5coVlfJXr17VOFkkqihyhcBPJ+6iy2f7sXj3ZWTlFaJpLXusHv0CJjVVoIVn1Wt6JyKi8tP66svisWNPS01NhY2NdrdyCQ0NRUhICNq0aYN27dohKioK2dnZyqsxR48eDU9PTyxatAgAcPToUcTHx8Pf3x/x8fEIDw+HQqHAjBkzlNt855130L59eyxcuBBDhw7FsWPHsGrVKqxatUrbqhLphRAC+68k45Ndl3ElKQsA4OlohXe7N0J/f0/I5YX445qBgyQiogqncVI2cOBAAEWD+seMGaPS3SeXyxEXF4f27dtr9eLDhg1DcnIy5s6di/v378Pf3x+7d++Gm5sbAODOnTswMfmvMS8vLw+zZ8/GjRs3YGtri+DgYKxfvx6Ojo7KMm3btsUvv/yCWbNmYd68eahXrx6ioqIwatQorWIj0oezd9OxaNclHLlR1NrsYGWOyV18MDrQG1JzUwAAb4pBRGScNE7KHBwcABR9y7ezs4OVlZXyOQsLC7z44otlTjtRmilTpmDKlClqn9u/f7/K46CgIFy8ePGZ2+zduzd69+6tdSxE+nI7NRtL9lzBzrhEAICFmQnGtvfGpM4N4GBdtcdAEBGRbmiclBVPxurt7Y333ntP665KImOUWyDHp3uu4PvDt1CoEJBIgIGtaiO0eyN4Olo9ewNERGQ0tB5TFhYWpo84iKqdS4mZmLrpNK49eAQACGrkgpk9m6BpLQ7gJyKikjRKyl544QXExMTAyckJrVq1KvM+e6dOndJZcERVkRACaw/dwqJdl1FQqICLnSU+HdwSnRuXvJ8rERFRMY2Ssn79+ikH9vfr1483PyYqReqjfLz/cxz+uvwAAPBKE1csGdwSNW1LzoNHRET0JI2Ssie7LMPDw/UVC1GVduBqMt7dchbJWfmwMDPBR8FNMTqwLr/EEBGRRrQeUzZ37lx06dIFgYGBJW4OTmSMCgoV+GzvFaw6cAMA0NDVFstGtOLYMSIi0orWSdnhw4cRGRmJwsJCtG3bFkFBQejcuTM6dOigMk0GkTG4kfwIU388jfPxRfdb+9+LdTC7VzPlnGNERESa0jopi46ORmFhIY4ePYoDBw4gNjYWy5YtQ35+Ptq2bYuDBw/qI06iSkUIgS0n7yH8twvIKZDD0docSwa1RPfm6u/bSkRE9CxaJ2UAYGZmhg4dOsDFxQU1atSAnZ0dtm/fjsuXL+s6PqJKJyNXhg9/OaecCDawfk18Pswf7g7sziciovLTOilbtWoV9u/fj9jYWOTn56Njx47o3LkzZs+ejZYtW+ojRqJK48Sth5j24xnEp+fCzESC0O6N8FYnH5iacDA/ERE9H62TsgkTJsDFxQXvvvsuJk2aBFtbW33ERVSpFMoV+HLfdSyLuQaFAOrUsMayEa3g7+Vo6NCIiKiaMHl2EVXbtm3DqFGj8OOPP8LFxQXt27fHhx9+iL179yInJ0cfMRIZ1L20HIz49gii/ixKyAa28sTOqS8xISMiIp3SuqWsf//+6N+/PwAgIyMDf//9N7Zs2YLevXvDxMQEeXl5uo6RyGB2xiVi5rY4ZOUVwtbSDB/3b4H+rTwNHRYREVVD5Rron5qaitjYWOzfvx/79+/HhQsX4OTkhI4dO+o6PiKDyCkoRPhvF/DTiXsAAH8vRywb3gp1alobODIiIqqutE7KfH19cenSJTg5OaFTp04YP348goKCOMifqo3z8RmYuuk0bqRkQyIBJndugGldG8LcVOvefiIiIo2Va6B/UFAQWrRooY94iAxGoRD4v4M3sWTPZcjkAu72Unw+zB+BPjUNHRoRERkBrZOyyZMn6yMOIoN6kJWHd386i7+vpQAAujdzw+JBLeFkY2HgyIiIyFiUa0wZUXWy7/IDvLflLFKzC2BpZoI5vZthVEAd3kiciIgqFJMyMlpyhcDCPy7h/w7eBAA0cbfD8hGt0NDNzsCRERGRMWJSRkZJrhCY8XMctp4qurpyTHtvzOzZhDcSJyIig2FSRkZHrhD4YGtRQmZqIsHnw/zR18/D0GEREZGR0/ga/yVLliA3N1f5+J9//kF+fr7ycVZWFiZNmqTb6Ih0TKEQmLk1Dj+fLErIlg1vxYSMiIgqBY2TslmzZiErK0v5uGfPnoiPj1c+zsnJwTfffKPb6Ih0SPG4hWzL44Tsi+H+6NWylqHDIiIiAqBFUiaEKPMxUWWmUAjM3PZfQhY1zB+9W7KFjIiIKg9OUU7VnkIhMGvbOfx04h5MJEDUMH/0YZclERFVMkzKqFpTKAQ+/OUcNp+4W5SQDW/FhIyIiColra6+/O6772BrawsAKCwsxNq1a+Hs7AwAKuPNiCoDhULgo+3n8OPxooSMV1kSEVFlpnFSVqdOHXz77bfKx+7u7li/fn2JMkSVQVFCdh6bjhUlZJFD/dHP39PQYREREZVK46Ts1q1begyDSHcUCoHZv57HpmN3YCIBlg71Q/9WTMiIiKhy45gyqlYUCoE5v57HxqN3IHmckA1oVdvQYRERET2Txi1l33//vUblRo8eXe5giJ6HEAJzfzuPH4oTsiFMyIiIqOrQOCkbM2YMbG1tYWZmVuocZRKJhEkZGYQQRS1kG44UJWSfDfbDwBeYkBERUdWhcVLWtGlTJCUl4X//+x/GjRuHli1b6jMuIo0JITD31wvKhOzTwX4Y1JoJGRERVS0ajym7cOECdu7cidzcXHTq1Alt2rTB119/jczMTH3GR1QmIQTCfruA9UduQyIBlgxqicFMyIiIqArSaqB/QEAAvvnmGyQmJmLq1Kn46aefUKtWLYwaNUrl5uREFUEIgfDfLuD7w0UJ2eJBLTGkjZehwyIiIiqXcl19aWVlhdGjRyMiIgLt2rXDjz/+iJycHF3HRlQqIQQifr+IdcUJ2cCWGMqEjIiIqjCtk7L4+HgsXLgQDRs2xPDhw9G2bVtcuHABTk5O+oiPqITihGztoVsAHidkbZmQERFR1abxQP+ffvoJa9asQWxsLHr06IGlS5eiV69eMDU11Wd8RCqEEJi344mEbJAvEzIiIqoWNE7Khg8fjjp16uCdd96Bm5sbbt26hRUrVpQoN3XqVJ0GSFRMCIH5Oy5hzT+3AACfDPTFsLa8tRcREVUPWt37UiKRYOPGjaWWkUgkTMpIL4QQ+HjnJaz+5yYAYNFAXwxvx4SMiIiqD43HlN26dQs3b94s8+fGjRvlCmLFihXw9vaGVCpFQEAAjh07VmpZmUyGefPmwcfHB1KpFH5+fti9e3ep5T/55BNIJBJMnz69XLGR4QkhsPCPS/i/g0UJ2cIBvhjBhIyIiKoZg9/7cvPmzQgNDUVYWBhOnToFPz8/9OjRAw8ePFBbfvbs2fjmm2+wfPlyXLx4ERMmTMCAAQNw+vTpEmWPHz+Ob775hhPdVmFCAIv3XMW3fxclZAsGtMDIACZkRERU/WiVlCkUCqxevRq9e/dGixYt4Ovri759++L7778v9dZLzxIZGYnx48dj7NixaNasGVauXAlra2usXr1abfn169fjww8/RHBwMOrXr4+JEyciODgYS5cuVSn36NEjjBo1Ct9++y2vDK2ihBD47Y4J/u+f2wCAj/u3wKiAugaOioiISD80HlMmhEDfvn3xxx9/wM/PD76+vhBC4NKlSxgzZgy2bduG7du3a/XiBQUFOHnyJGbNmqVcZmJigq5du+Lw4cNq18nPz4dUKlVZZmVlhYMHD6osmzx5Mnr16oWuXbvi448/LjOO/Px8lclvi+9SIJPJIJPJtKqTrhW/vqHjqGhCCCzefQV/JRR9bwjv0xTDWnsYxX4w1mMOsO5P/jYWxlpvgHV/8reut1tVaZyUrV27FgcOHEBMTAy6dOmi8txff/2F/v374/vvv9fqhuQpKSmQy+Vwc3NTWe7m5obLly+rXadHjx6IjIxEp06d4OPjg5iYGGzbtg1yuVxZ5scff8SpU6dw/PhxjeJYtGgRIiIiSizfu3cvrK2tNa6PPkVHRxs6hAp1KkWCddeKplsZXE8Op5Rz+OOPcwaOqmIZ2zF/EutufIy13gDrrktVfSJ7jZOyTZs24cMPPyyRkAHAyy+/jJkzZ+KHH37QKikrjy+++ALjx49HkyZNIJFI4OPjg7Fjxyq7O+/evYtp06YhOjq6RItaaWbNmoXQ0FDl48zMTHh5eaF79+6wt7fXSz00JZPJEB0djW7dusHc3NygsVSU5Kx8hC0/BECGHp4KzHutq9HUHTDOY16MdTe+uhtrvQHWXR91r+r349Y4KYuLi8OSJUtKfb5nz55YtmyZVi/u7OwMU1NTJCUlqSxPSkqCu7u72nVcXFywfft25OXlITU1FR4eHpg5cybq168PADh58iQePHiAF154QbmOXC7HgQMH8OWXXyI/P7/EhLeWlpawtLQs8Vrm5uaV5kSpTLHokxAC4TvOIj1XhqbuduhRO81o6v40Y603wLobY92Ntd4A667Lulf1/ajxQP+HDx+W6GZ8kpubG9LS0rR6cQsLC7Ru3RoxMTHKZQqFAjExMQgMDCxzXalUCk9PTxQWFmLr1q3o168fAOCVV17BuXPncObMGeVPmzZtMGrUKJw5c4Z3IKjkfjubgL0Xk2BuKsGSQS1gavDrg4mIiCqGxi1lcrkcZmalFzc1NUVhYaHWAYSGhiIkJARt2rRBu3btEBUVhezsbIwdOxYAMHr0aHh6emLRokUAgKNHjyI+Ph7+/v6Ij49HeHg4FAoFZsyYAQCws7NDixYtVF7DxsYGNWvWLLGcKpcHmXmY++sFAMDbLzdEE3c7lG/mOyIioqpHq6svx4wZo7abD4DK1YvaGDZsGJKTkzF37lzcv38f/v7+2L17t7JV7s6dOzAx+a+5JC8vD7Nnz8aNGzdga2uL4OBgrF+/Ho6OjuV6faochBD48JfzyMiVoYWnPSZ29gEU8mevSEREVE1onJSFhIQ8s0x5B/lPmTIFU6ZMUfvc/v37VR4HBQXh4sWLWm3/6W1Q5bP9TDz+vFTUbfnZED+Ym5pAxqSMiIiMiMZJ2Zo1a/QZBxmxpMw8hD3utpz2SkM0cTfsFa9ERESGwGHUZFBCCHy47Rwy8wrh6+mACUE+hg6JiIjIIJiUkUFtOxWPmMsPYGFqgqVD/WDGyy2JiMhI8T8gGcz9jDyE//6427JrQzRyszNwRERERIbDpIwMQgiBWdvikJVXCL/aDnirU31Dh0RERGRQTMrIIH4+eQ/7riTDwtQEnw1htyUREZHGV18+KSEhAQcPHsSDBw+gUChUnps6dapOAqPqKzEjF/N+L5rW5J1ujdCQ3ZZERETaJ2Vr167FW2+9BQsLC9SsWRMSiUT5nEQiYVJGZRJCYObWc8jKL4S/lyPGd6xn6JCIiIgqBa2Tsjlz5mDu3LmYNWuWykz7RJrYcuIeYq8mw8KM3ZZERERP0vo/Yk5ODoYPH86EjLSWkJ6L+TuKui3f7dYIDVxtDRwRERFR5aF1ZvX6669jy5Yt+oiFqjEhBGZuK+q2bFXHEW905NWWRERET9K6+3LRokXo3bs3du/eDV9fX5ibm6s8HxkZqbPgqPrYfPwuDlxNhuXjbktTE8mzVyIiIjIi5UrK9uzZg8aNGwNAiYH+RE+LT8/FxzsvAQDe694YPi7stiQiInqa1knZ0qVLsXr1aowZM0YP4VB1U3S1ZRwe5ReidV0njHuJV1sSERGpo/WYMktLS3To0EEfsVA1tOnYXfx9LQWWZib4dHBLdlsSERGVQuukbNq0aVi+fLk+YqFq5l5aDhbsLLra8v0ejVGf3ZZERESl0rr78tixY/jrr7+wY8cONG/evMRA/23btuksOKq6hBD4YGscsgvkaOvthLEd2G1JRERUFq2TMkdHRwwcOFAfsVA18sPRO/jneiqk5ib4dDCvtiQiInoWrZOyNWvW6CMOqkbuPszBwj+Krrac0aMJvJ1tDBwRERFR5VeuG5IDQHJyMq5cuQIAaNy4MVxcXHQWFFVdCoXAjJ/jkFMgRzvvGhjT3tvQIREREVUJWg/0z87Oxrhx41CrVi106tQJnTp1goeHB15//XXk5OToI0aqQn44ehuHb6TCytwUnw5pCRN2WxIREWlE66QsNDQUsbGx+P3335Geno709HT8+uuviI2NxbvvvquPGKmKuJOag0W7LgMAPni1MerWZLclERGRprTuvty6dSt+/vlndO7cWbksODgYVlZWGDp0KL7++mtdxkdVhEIh8P7PZ5FTIEdAvRoYHeht6JCIiIiqFK1bynJycuDm5lZiuaurK7svjdj6I7dx9OZDWFuY4tPBfuy2JCIi0pLWSVlgYCDCwsKQl5enXJabm4uIiAgEBgbqNDiqGm6nZuOTx92WM3s2QZ2a1gaOiIiIqOrRuvsyKioKr776KmrXrg0/Pz8AwNmzZyGVSrFnzx6dB0iVW1G3ZRxyZXIE1q+J/wXUNXRIREREVZLWSZmvry+uXbuGH374AZcvF7WOjBgxAqNGjYKVlZXOA6TKbd3hWzj2uNtyyWBebUlERFReWiVlMpkMTZo0wY4dOzB+/Hh9xURVxK2UbCzeXZSYzwpuCq8a7LYkIiIqL63GlJmbm6uMJSPjVXy1ZZ5MgQ4NamJUuzqGDomIiKhK03qg/+TJk7F48WIUFhbqIx6qItYcuoXjt9JgY2GKxYPYbUlERPS8tB5Tdvz4ccTExGDv3r3w9fWFjY3qBKHbtm3TWXBUOcWn5+LTPUXdlh/2aoraTuy2JCIiel5aJ2WOjo4YNGiQPmKhKuKHI7eRJ1OgrbcTRrLbkoiISCc0Ssp+++039OzZE+bm5lizZo2+Y6JKrKBQgZ9O3AMAvP5SPUgk7LYkIiLSBY3GlA0YMADp6ekAAFNTUzx48ECfMVElFn0xCSmP8uFiZ4lXmpa8swMRERGVj0ZJmYuLC44cOQIAEEKwdcSI/XD0NgBgeFsvmJtqfZ0IERERlUKj7ssJEyagX79+kEgkkEgkcHd3L7WsXC7XWXBUufyb/AiH/k2FiQQYzrFkREREOqVRUhYeHo7hw4fj+vXr6Nu3L9asWQNHR0c9h0aVzaajdwAAXRq7wtORd28gIiLSJY2vvmzSpAmaNGmCsLAwDBkyBNbWnAbBmOTJ5Pj5VNEA/1EvspWMiIhI17SeEiMsLEwfcVAlt+t8ItJzZPB0tEJQI1dDh0NERFTtcKQ2aeSHI0Vdl8PbesGUs/cTERHpXKVIylasWAFvb29IpVIEBATg2LFjpZaVyWSYN28efHx8IJVK4efnh927d6uUWbRoEdq2bQs7Ozu4urqif//+uHLlir6rUW1dvp+JE7fTYGoiwbC2XoYOh4iIqFoyeFK2efNmhIaGIiwsDKdOnYKfnx969OhR6lxos2fPxjfffIPly5fj4sWLmDBhAgYMGIDTp08ry8TGxmLy5Mk4cuQIoqOjIZPJ0L17d2RnZ1dUtaqVjY8H+Hdv5gZXe6mBoyEiIqqenispy8vLe+4AIiMjMX78eIwdOxbNmjXDypUrYW1tjdWrV6stv379enz44YcIDg5G/fr1MXHiRAQHB2Pp0qXKMrt378aYMWPQvHlz+Pn5Ye3atbhz5w5Onjz53PEam5yCQvxyKh4AMCqgroGjISIiqr60TsoUCgXmz58PT09P2Nra4saNGwCAOXPm4P/+7/+02lZBQQFOnjyJrl27/heQiQm6du2Kw4cPq10nPz8fUqlqa42VlRUOHjxY6utkZGQAAGrUqKFVfAT8fjYBWfmF8K5pjfY+NQ0dDhERUbWl9dWXH3/8MdatW4clS5Zg/PjxyuUtWrRAVFQUXn/9dY23lZKSArlcDjc31dv1uLm54fLly2rX6dGjByIjI9GpUyf4+PggJiYG27ZtK3XSWoVCgenTp6NDhw5o0aKF2jL5+fnIz89XPs7MzARQNH5NJpNpXB99KH59Q8Wx4UjRDP5D23hCLi9ERc4NbOi6G4qx1htg3Z/8bSyMtd4A6/7kb11vt6qSCCGENis0aNAA33zzDV555RXY2dnh7NmzqF+/Pi5fvozAwECkpaVpvK2EhAR4enri0KFDCAwMVC6fMWMGYmNjcfTo0RLrJCcnY/z48fj9998hkUjg4+ODrl27YvXq1cjNzS1RfuLEidi1axcOHjyI2rVrq40jPDwcERERJZZv3LjRqOdju/MIWHrODKYSgXmt5bA1N3REREREpcvJycHIkSORkZEBe3t7Q4ejNa1byuLj49GgQYMSyxUKhdYZqrOzM0xNTZGUlKSyPCkpqdRbObm4uGD79u3Iy8tDamoqPDw8MHPmTNSvX79E2SlTpmDHjh04cOBAqQkZAMyaNQuhoaHKx5mZmfDy8kL37t0NflBlMhmio6PRrVs3mJtXbFb00fYLAOIR7FsLQ/u1rNDXBgxbd0My1noDrLsx1t1Y6w2w7vqoe3FPV1WldVLWrFkz/P3336hbV3XQ988//4xWrVpptS0LCwu0bt0aMTEx6N+/P4Ci5C4mJgZTpkwpc12pVApPT0/IZDJs3boVQ4cOVT4nhMDbb7+NX375Bfv370e9evXK3JalpSUsLS1LLDc3N680J0pFx5KZJ8PvcfcBAK8F1jPofqhMx6EiGWu9AdbdGOturPUGWHdd1r2q70etk7K5c+ciJCQE8fHxUCgU2LZtG65cuYLvv/8eO3bs0DqA0NBQhISEoE2bNmjXrh2ioqKQnZ2NsWPHAgBGjx4NT09PLFq0CABw9OhRxMfHw9/fH/Hx8QgPD4dCocCMGTOU25w8eTI2btyIX3/9FXZ2drh/vyi5cHBwgJUV79moiV9PxyNXJkdDV1u09XYydDhERETVntZJWb9+/fD7779j3rx5sLGxwdy5c/HCCy/g999/R7du3bQOYNiwYUhOTsbcuXNx//59+Pv7Y/fu3crB/3fu3IGJyX8Xiebl5WH27Nm4ceMGbG1tERwcjPXr16vcIP3rr78GAHTu3FnltdasWYMxY8ZoHaOxEULgh8dzk40KqAOJhDP4ExER6ZvWSRkAdOzYEdHR0ToLYsqUKaV2V+7fv1/lcVBQEC5evFjm9rS8doGecupOGi7fz4LU3AQDXih9LB4RERHpjtbzlNWvXx+pqakllqenp6sdbE9VT/F9Lvu09ICDVdXunyciIqoqtE7Kbt26pXZOsPz8fMTHx+skKDKctOwC7DiXCAAY9SJn8CciIqooGndf/vbbb8q/9+zZAwcHB+VjuVyOmJgYeHt76zQ4qnhbT91DQaECzT3s4Vfb4dkrEBERkU5onJQVT1khkUgQEhKi8py5uTm8vb1V7j9JVY8QQnnz8VEBdTnAn4iIqAJpnJQpFAoAQL169XD8+HE4OzvrLSgyjMM3UnEjJRs2Fqbo6+9h6HCIiIiMitZXX968eVMfcVAlUDwNRv9WnrC1LNeFuURERFRO5frPm52djdjYWNy5cwcFBQUqz02dOlUngVHFSs7Kx57zRZPsjgrgAH8iIqKKpnVSdvr0aQQHByMnJwfZ2dmoUaMGUlJSYG1tDVdXVyZlVdRPJ+6iUCHQqo4jmnlUvZu4EhERVXVaT4nxzjvvoE+fPkhLS4OVlRWOHDmC27dvo3Xr1vjss8/0ESPpmUIhsOnYfwP8iYiIqOJpnZSdOXMG7777LkxMTGBqaor8/Hx4eXlhyZIl+PDDD/URI+nZgWvJuJeWC3upGXq3rGXocIiIiIyS1kmZubm58l6Urq6uuHOnqIXFwcEBd+/e1W10VCGKB/gPal0bUnNTA0dDRERknLQeU9aqVSscP34cDRs2RFBQEObOnYuUlBSsX78eLVq00EeMpEeJGbmIuZQEoOjm40RERGQYWreULVy4ELVqFXVxLViwAE5OTpg4cSKSk5PxzTff6DxA0q8fj92FQgAB9WqggaudocMhIiIyWlq3lLVp00b5t6urK3bv3q3TgKjiFMoV2Hy8qMuZ97kkIiIyLK1byl5++WWkp6eXWJ6ZmYmXX35ZFzFRBfnr8gPcz8xDTRsL9GjuZuhwiIiIjJrWSdn+/ftLTBgLAHl5efj77791EhRVjOIB/oPb1IalGQf4ExERGZLG3ZdxcXHKvy9evIj79+8rH8vlcuzevRuenp66jY705k5qDg5cSwYAjGzHAf5ERESGpnFS5u/vD4lEAolEorab0srKCsuXL9dpcKQ/m47fgRBAx4bOqFvTxtDhEBERGT2Nk7KbN29CCIH69evj2LFjcHFxUT5nYWEBV1dXmJqyC6wqKChUYMuJxwP8OYM/ERFRpaBxUla3btE/b4VCobdgqGLsvXgfKY8K4GZviVeauho6HCIiIkI5BvqvW7cOO3fuVD6eMWMGHB0d0b59e9y+fVunwZF+/HCkaID/sLZ1YG6q9VuAiIiI9KBck8daWVkBAA4fPowvv/wSS5YsgbOzM9555x2dB0i6df3BIxy+kQoTCTC8rZehwyEiIqLHtJ489u7du2jQoAEAYPv27Rg8eDDefPNNdOjQAZ07d9Z1fKRjm44VtZK93MQVHo5WBo6GiIiIimndUmZra4vU1FQAwN69e9GtWzcAgFQqRW5urm6jI53Kk8nx88l7ADjAn4iIqLLRuqWsW7dueOONN9CqVStcvXoVwcHBAIALFy7A29tb1/GRDv1xLhEZuTJ4OlqhUyOXZ69AREREFUbrlrIVK1YgMDAQycnJ2Lp1K2rWrAkAOHnyJEaMGKHzAEl3imfwHxlQB6YmEgNHQ0RERE/SuqXM0dERX375ZYnlEREROgmI9ONSYiZO3k6DmYkEQ9rUNnQ4RERE9BTOh2AkNj5uJeve3A2udlIDR0NERERPY1JmBLLzC/HL6XgAHOBPRERUWTEpMwK/n03Ao/xC1HO2QWD9moYOh4iIiNRgUmYElAP829WBCQf4ExERVUpMyqq5uHvpOBefAQszEwxqzQH+RERElZVGV1+2atUKEolmLSynTp16roBIt4rvcxncwh01bCwMHA0RERGVRqOkrH///noOg/QhM0+G384mAABGvcgB/kRERJWZRklZWFiYvuMgPdh+Oh65MjkaudmiTV0nQ4dDREREZeCYsmpKCKHsuhwVUFfj7mciIiIyDK1n9JfL5fj888/x008/4c6dOygoKFB5/uHDhzoLjsrv5O00XEnKgpW5KQa84GnocIiIiOgZtG4pi4iIQGRkJIYNG4aMjAyEhoZi4MCBMDExQXh4uB5CpPIongajj18t2EvNDRwNERERPYvWSdkPP/yAb7/9Fu+++y7MzMwwYsQIfPfdd5g7dy6OHDmijxhJSw+zC7DzXCIAzuBPRERUVWidlN2/fx++vr4AAFtbW2RkZAAAevfujZ07d+o2OiqXrSfvoaBQgRae9mhZ28HQ4RAREZEGtE7KateujcTEolYYHx8f7N27FwBw/PhxWFpa6jY60poQAhuPcYA/ERFRVaN1UjZgwADExMQAAN5++23MmTMHDRs2xOjRozFu3LhyBbFixQp4e3tDKpUiICAAx44dK7WsTCbDvHnz4OPjA6lUCj8/P+zevfu5tlmdnLidhpsp2bC1NENfPw9Dh0NEREQa0vrqy08++UT597Bhw1CnTh0cPnwYDRs2RJ8+fbQOYPPmzQgNDcXKlSsREBCAqKgo9OjRA1euXIGrq2uJ8rNnz8aGDRvw7bffokmTJtizZw8GDBiAQ4cOoVWrVuXaZnVy4lYaAKBTI2fYWGp9eImIiMhAnnuessDAQISGhpYrIQOAyMhIjB8/HmPHjkWzZs2wcuVKWFtbY/Xq1WrLr1+/Hh9++CGCg4NRv359TJw4EcHBwVi6dGm5t1mdnItPBwC0rO1o0DiIiIhIO+VqSrl27Rr27duHBw8eQKFQqDw3d+5cjbdTUFCAkydPYtasWcplJiYm6Nq1Kw4fPqx2nfz8fEilUpVlVlZWOHjw4HNtMz8/X/k4MzMTQFFXqUwm07g++lD8+prGcfZuOgCgubutwWN/XtrWvbow1noDrPuTv42FsdYbYN2f/K3r7VZVWidl3377LSZOnAhnZ2e4u7urDCSXSCRaJWUpKSmQy+Vwc3NTWe7m5obLly+rXadHjx6IjIxEp06d4OPjg5iYGGzbtg1yubzc21y0aBEiIiJKLN+7dy+sra01ro8+RUdHP7PMIxkQn150SOPPH8Ef6qtb5WhS9+rIWOsNsO7GyFjrDbDuupSTk6PT7VU0rZOyjz/+GAsWLMAHH3ygj3ie6YsvvsD48ePRpEkTSCQS+Pj4YOzYsc/VNTlr1iyEhoYqH2dmZsLLywvdu3eHvb29LsIuN5lMhujoaHTr1g3m5mVPAht7NRk4cRr1na0xqO9LFRSh/mhT9+rEWOsNsO7GWHdjrTfAuuuj7sU9XVWV1klZWloahgwZopMXd3Z2hqmpKZKSklSWJyUlwd3dXe06Li4u2L59O/Ly8pCamgoPDw/MnDkT9evXL/c2LS0t1U7nYW5uXmlOFE1iuXg/G0DReLLKErcuVKbjUJGMtd4A626MdTfWegOsuy7rXtX3o9YD/YcMGaKcm+x5WVhYoHXr1sopNgBAoVAgJiYGgYGBZa4rlUrh6emJwsJCbN26Ff369XvubVZ1cffSAXCQPxERUVWkdUtZgwYNMGfOHBw5cgS+vr4lstKpU6dqtb3Q0FCEhISgTZs2aNeuHaKiopCdnY2xY8cCAEaPHg1PT08sWrQIAHD06FHEx8fD398f8fHxCA8Ph0KhwIwZMzTeZnUVd6/o7gqcxZ+IiKjq0TopW7VqFWxtbREbG4vY2FiV5yQSidZJ2bBhw5CcnIy5c+fi/v378Pf3x+7du5UD9e/cuQMTk/8a9PLy8jB79mzcuHEDtra2CA4Oxvr16+Ho6KjxNquj+xl5eJCVDxMJ0NyDSRkREVFVo3VSdvPmTZ0HMWXKFEyZMkXtc/v371d5HBQUhIsXLz7XNquj4q7LRm52sLIwNWwwREREpLXnnjyWKgd2XRIREVVtGrWUhYaGYv78+bCxsVGZOkKdyMhInQRG2omLL0rKfDnIn4iIqErSKCk7ffq0cpbcU6dOqUwY+6TSlpN+CSGU3Zd+bCkjIiKqkjRKyr744gvlJKpPj/Eiw7v7MBfpOTKYm0rQ2N3O0OEQERFROWg0pqxVq1ZISUkBANSvXx+pqal6DYq0E/f4JuRNa9nD0oyD/ImIiKoijZIyR0dH5VWXt27dKnETcjKs4kH+vp7suiQiIqqqNOq+HDRoEIKCglCrVi1IJBK0adMGpqbqW2Ru3Lih0wDp2f4bT+Zo0DiIiIio/DRKylatWoWBAwfi+vXrmDp1KsaPHw87O45dqgwUCoHz8UU3YPXlIH8iIqIqS+PJY1999VUAwMmTJzFt2jQmZZXEjZRsPMovhNTcBA1dbQ0dDhEREZWT1jP6r1mzRh9xUDkVd10293CAmSnnAiYiIqqq+F+8iuNM/kRERNUDk7IqrriljEkZERFR1cakrAorlCtwIaFokH9LXnlJRERUpTEpq8KuJj1CfqECdpZmqFfTxtDhEBER0XPQeqA/AFy7dg379u3DgwcPSkwkO3fuXJ0ERs927vFM/i08HWBiwvuOEhERVWVaJ2XffvstJk6cCGdnZ7i7u6vchFwikTApq0BnOcifiIio2tA6Kfv444+xYMECfPDBB/qIh7RwTpmUORo2ECIiInpuWo8pS0tLw5AhQ/QRC2khv1COy/eLB/mzpYyIiKiq0zopGzJkCPbu3auPWEgLlxOzIJMLOFmbo7aTlaHDISIiouekUfflsmXLlH83aNAAc+bMwZEjR+Dr6wtzc3OVslOnTtVthKRW8fxkvrUdVcb1ERERUdWkUVL2+eefqzy2tbVFbGwsYmNjVZZLJBImZRWkeCZ/P3ZdEhERVQsaJWU3b97UdxykpeKkzNeTSRkREVF1oPWYsnnz5iEnJ6fE8tzcXMybN08nQVHZcgoKce1BFgDAz8vRsMEQERGRTmidlEVERODRo0cllufk5CAiIkInQVHZLiRkQiEAVztLuNlLDR0OERER6YDWSZkQQu3A8rNnz6JGjRo6CYrKFsf5yYiIiKodjSePdXJygkQigUQiQaNGjVQSM7lcjkePHmHChAl6CZJUFV95yfnJiIiIqg+Nk7KoqCgIITBu3DhERETAweG/hMDCwgLe3t4IDAzUS5Ck6hxvr0RERFTtaJyUhYSEAADq1auH9u3bl5ifjCpGRq4MN1KyAbD7koiIqDrRKCnLzMyEvb09AKBVq1bIzc1Fbm6u2rLF5Ug/LsQXtZLVdrJCDRsLA0dDREREuqJRUubk5ITExES4urrC0VH9DPLFFwDI5XKdB0n/OaucNNbRsIEQERGRTmmUlP3111/KKyv/+usv3tbHgM7FpwMAfDmejIiIqFrRKCkLCgpS/t25c2d9xUIaOHuXg/yJiIiqI40H+hfr1KkTOnfujKCgIHTo0AFSKScvrSipj/IRn140lq8Fb69ERERUrWg9eWz37t1x5MgR9OvXD46OjnjppZcwe/ZsREdHq739EulO3ONB/vVdbGAv5dWvRERE1YnWLWWzZ88GABQWFuL48eOIjY3F/v37sWTJEpiYmCAvL0/nQVIR5fxkbCUjIiKqdrROyorduHED586dw9mzZxEXFwc7Ozt06tRJl7HRU/6byd/RoHEQERGR7mmdlI0cORKxsbHIz89Hp06dEBQUhJkzZ6Jly5a8KlPP4jiTPxERUbWldVL2448/wtnZGW+88QZefvllvPTSS7C2ttZHbPSE+5l5eJCVDxMJ0NyDSRkREVF1o/VA/9TUVHz33XcoKCjArFmz4OzsjPbt2+PDDz/E3r179REjATgfnwkAaORmBysLUwNHQ0RERLqmdVLm5OSEvn37IjIyEidPnkRcXBwaNWqETz/9FD179tRHjIT/rrxk1yUREVH1VK6Wsm3btmHq1Klo2bIlmjRpgh07dqBPnz6IjIzUOoAVK1bA29sbUqkUAQEBOHbsWJnlo6Ki0LhxY1hZWcHLywvvvPOOyhWfcrkcc+bMQb169WBlZQUfHx/Mnz8fQgitY6tMilvKfDnIn4iIqFrSekyZq6srnJ2d0bFjR4wfPx6dO3eGr69vuV588+bNCA0NxcqVKxEQEICoqCj06NEDV65cgaura4nyGzduxMyZM7F69Wq0b98eV69exZgxYyCRSJQJ4eLFi/H1119j3bp1aN68OU6cOIGxY8fCwcEBU6dOLVechiYEcO5xUubHljIiIqJqSeukLC4uDs2bN9fJi0dGRmL8+PEYO3YsAGDlypXYuXMnVq9ejZkzZ5Yof+jQIXTo0AEjR44EAHh7e2PEiBE4evSoSpl+/fqhV69eyjKbNm16ZgtcZfYwH0jPlcHcVILG7naGDoeIiIj0QOukTFcJWUFBAU6ePIlZs2Ypl5mYmKBr1644fPiw2nXat2+PDRs24NixY2jXrh1u3LiBP/74A6+99ppKmVWrVuHq1ato1KgRzp49i4MHD5bZtZqfn4/8/Hzl48zMolYpmUwGmUz2vFV9LjKZDHceFU010sTdDiZCAZlMYdCYKkrxvjf0MahoxlpvgHV/8rexMNZ6A6z7k791vd2qqtyTxz6vlJQUyOVyuLm5qSx3c3PD5cuX1a4zcuRIpKSk4KWXXoIQAoWFhZgwYQI+/PBDZZmZM2ciMzMTTZo0gampKeRyORYsWIBRo0aVGsuiRYsQERFRYvnevXsrxXQfd7KLhv7ZF6bjjz/+MHA0FS86OtrQIRiEsdYbYN2NkbHWG2Dddamq3+7RYElZeezfvx8LFy7EV199hYCAAFy/fh3Tpk3D/PnzMWfOHADATz/9hB9++AEbN25E8+bNcebMGUyfPh0eHh4ICQlRu91Zs2YhNDRU+TgzMxNeXl7o3r077O3tK6RupZHJZFgeFQMA6BXoi+DWngaNpyLJZDJER0ejW7duMDc3nnt9Gmu9AdbdGOturPUGWHd91L24p6uqMlhS5uzsDFNTUyQlJaksT0pKgru7u9p15syZg9deew1vvPEGAMDX1xfZ2dl488038dFHH8HExATvv/8+Zs6cieHDhyvL3L59G4sWLSo1KbO0tISlpWWJ5ebm5gY/URQKgbvZRd2XrerWMHg8hlAZjoMhGGu9AdbdGOturPUGWHdd1r2q70etp8S4ceOGTl7YwsICrVu3RkxMjHKZQqFATEwMAgMD1a6Tk5MDExPVkE1NiyZSLZ7yorQyCkXVHId1MzUH+XIJpOYmaOhqa+hwiIiISE+0bilr0KABgoKC8Prrr2Pw4MGQSqXlfvHQ0FCEhISgTZs2aNeuHaKiopCdna28GnP06NHw9PTEokWLAEA5F1qrVq2U3Zdz5sxBnz59lMlZnz59sGDBAtSpUwfNmzfH6dOnERkZiXHjxpU7TkM6/3jS2Ga17GFmqnUOTURERFWE1knZqVOnsGbNGoSGhmLKlCkYNmwYXn/9dbRr107rFx82bBiSk5Mxd+5c3L9/H/7+/ti9e7dy8P+dO3dUWr1mz54NiUSC2bNnIz4+Hi4uLsokrNjy5csxZ84cTJo0CQ8ePICHhwfeeustzJ07V+v4KoO44kljPQ07to2IiIj0S+ukzN/fH1988QWWLl2K3377DWvXrsVLL72ERo0aYdy4cXjttdfg4uKi8famTJmCKVOmqH1u//79qsGamSEsLAxhYWGlbs/Ozg5RUVGIiorSOIbK7HzC46TMg0kZERFRdVbu/jAzMzMMHDgQW7ZsweLFi3H9+nW899578PLywujRo5GYmKjLOI1SoVyBi4nFLWWcyZ+IiKg6K3dSduLECUyaNAm1atVCZGQk3nvvPfz777+Ijo5GQkIC+vXrp8s4jdK1B4+QJ1NAairgXdPw86URERGR/mjdfRkZGYk1a9bgypUrCA4Oxvfff4/g4GDl2K969eph7dq18Pb21nWsRifuXjoAwMtGwMREYthgiIiISK+0Tsq+/vprjBs3DmPGjEGtWrXUlnF1dcX//d//PXdwxi7uXtGVl16cCYOIiKja0zopi46ORp06dUrMBSaEwN27d1GnTh1YWFiUOlEraa44KatjKwwcCREREemb1mPKfHx8kJKSUmL5w4cPUa9ePZ0ERUB+oRyX7xcN8q9jw6SMiIioutM6KSueOf9pjx49eq6JZEnV5cQsyOQCTtbmqFHyDlBERERUzWjcfVl8w26JRIK5c+fC2vq/qwHlcjmOHj0Kf39/nQdorOIez+TfwsMeEkmugaMhIiIifdM4KTt9+jSAopayc+fOwcLCQvmchYUF/Pz88N577+k+QiMVdzcdwOP5yQqSyi5MREREVZ7GSdm+ffsAAGPHjsUXX3wBe3vOMK9P5x63lPl62qPgpoGDISIiIr3TekzZmjVrmJDpWU5BIa4mZQHgPS+JiIiMhUYtZQMHDsTatWthb2+PgQMHlll227ZtOgnMmF1MyIRCAK52lnCz58UTRERExkCjpMzBwQESiUT5N+nX2cfzk7Ws7WjYQIiIiKjCaJSUrVmzRu3fpB/nHt9eqWVtJsBERETGotw3JCf9iVO2lDEpIyIiMhYatZS1atVK2X35LKdOnXqugIxdZp4MN1KyAbD7koiIyJholJT1799fz2FQsfOPW8lqO1mhho0FZDKZgSMiIiKiiqBRUhYWFqbvOOix4pn82XVJRERkXMo1piw9PR3fffcdZs2ahYcPHwIo6raMj4/XaXDGKE45yN/RoHEQERFRxdJ4Rv9icXFx6Nq1KxwcHHDr1i2MHz8eNWrUwLZt23Dnzh18//33+ojTaHCQPxERkXHSuqUsNDQUY8aMwbVr1yCV/jexaXBwMA4cOKDT4IxN6qN83Esruvl4C08mZURERMZE66Ts+PHjeOutt0os9/T0xP3793USlLEqvt9lfRcb2EvNDRwNERERVSStkzJLS0tkZmaWWH716lW4uLjoJChjpey6ZCsZERGR0dE6Kevbty/mzZunnKpBIpHgzp07+OCDDzBo0CCdB2hM4nh7JSIiIqOldVK2dOlSPHr0CK6ursjNzUVQUBAaNGgAOzs7LFiwQB8xGo043l6JiIjIaGl99aWDgwOio6Nx8OBBxMXF4dGjR3jhhRfQtWtXfcRnNJIy8/AgKx8mEqC5B5MyIiIiY6N1UlbspZdewksvvaTLWIza2bvpAIBGbnawsjA1bDBERERU4TRKypYtW6bxBqdOnVruYIzZOc7kT0REZNQ0Sso+//xzlcfJycnIycmBo6MjgKIZ/q2treHq6sqkrJzOPh7k78tB/kREREZJo4H+N2/eVP4sWLAA/v7+uHTpEh4+fIiHDx/i0qVLeOGFFzB//nx9x1stCSFw7vEgfz+2lBERERklra++nDNnDpYvX47GjRsrlzVu3Biff/45Zs+erdPgjMW9tFyk5chgbipBY3c7Q4dDREREBqB1UpaYmIjCwsISy+VyOZKSknQSlLEpnp+saS17WJpxkD8REZEx0jope+WVV/DWW2/h1KlTymUnT57ExIkTOS1GORXPT+bLmfyJiIiMltZJ2erVq+Hu7o42bdrA0tISlpaWaNeuHdzc3PDdd9/pI8Zqr7ilzI+D/ImIiIyW1vOUubi44I8//sDVq1dx+fJlAECTJk3QqFEjnQdnDBQKgfPxxVdesqWMiIjIWJV78thGjRoxEdOBm6nZyMovhNTcBA1dbQ0dDhERERmIRklZaGgo5s+fDxsbG4SGhpZZNjIyUieBGYvi8WTNPRxgZqp1bzIRERFVExolZadPn4ZMJlP+XRqJRKKbqIxI8XgyzuRPRERk3DRKyvbt24cbN27AwcEB+/bt03dMRoVJGREREQFaXH3ZsGFDJCcnKx8PGzaM85I9p0K5AhcSipMyR8MGQ0RERAalcVImhFB5/McffyA7O/u5A1ixYgW8vb0hlUoREBCAY8eOlVk+KioKjRs3hpWVFby8vPDOO+8gLy9PpUx8fDz+97//oWbNmrCysoKvry9OnDjx3LHq2rUHj5AnU8DO0gz1atoYOhwiIiIyoHJffakLmzdvRmhoKFauXImAgABERUWhR48euHLlClxdXUuU37hxI2bOnInVq1ejffv2uHr1KsaMGQOJRKK8wCAtLQ0dOnRAly5dsGvXLri4uODatWtwcnKq6Oo907nHXZctPB1gYsLxeERERMZM46RMIpGUGMj/vAP7IyMjMX78eIwdOxYAsHLlSuzcuROrV6/GzJkzS5Q/dOgQOnTogJEjRwIAvL29MWLECBw9elRZZvHixfDy8sKaNWuUy+rVq/dccerL2cdXXnI8GREREWmclAkhMGbMGFhaWgIA8vLyMGHCBNjYqHa7bdu2TaPtFRQU4OTJk5g1a5ZymYmJCbp27YrDhw+rXad9+/bYsGEDjh07hnbt2uHGjRv4448/8NprrynL/Pbbb+jRoweGDBmC2NhYeHp6YtKkSRg/fnypseTn5yM/P1/5ODMzEwAgk8mUV53qg3I6jFq2pb5O8XJ9xlFZGWvdjbXeAOv+5G9jYaz1Blj3J3/rertVlUQ8PVisFMWtWc/yZAtVWRISEuDp6YlDhw4hMDBQuXzGjBmIjY1Vaf160rJly/Dee+9BCIHCwkJMmDABX3/9tfJ5qVQKoGhutSFDhuD48eOYNm0aVq5ciZCQELXbDA8PR0RERInlGzduhLW1tUb10VahAphxzBRyIcHcVoWoKdXLyxARERmNnJwcjBw5EhkZGbC3tzd0OFrTOCnTtfIkZfv378fw4cPx8ccfIyAgANevX8e0adMwfvx4zJkzBwBgYWGBNm3a4NChQ8r1pk6diuPHj5faAqeupczLywspKSl6O6jn4jMwcOVROFmb4+jMzqV2BctkMkRHR6Nbt24wNzfXSyyVlbHW3VjrDbDuxlh3Y603wLrro+6ZmZlwdnauskmZwQb6Ozs7w9TUtMS0GklJSXB3d1e7zpw5c/Daa6/hjTfeAAD4+voiOzsbb775Jj766COYmJigVq1aaNasmcp6TZs2xdatW0uNpfjG6k8zNzfX24ly4X7Rlau+tR1hYWHxzPL6jKWyM9a6G2u9AdbdGOturPUGWHdd1r2q70eD3dfHwsICrVu3RkxMjHKZQqFATEyMSsvZk3JycmBiohqyqakpgP+m7OjQoQOuXLmiUubq1auoW7euLsN/bucejyfz4yB/IiIigoGnxAgNDUVISAjatGmDdu3aISoqCtnZ2crxa6NHj4anpycWLVoEAOjTpw8iIyPRqlUrZfflnDlz0KdPH2Vy9s4776B9+/ZYuHAhhg4dimPHjmHVqlVYtWqVweqpTvFM/r6eTMqIiIjIwEnZsGHDkJycjLlz5+L+/fvw9/fH7t274ebmBgC4c+eOSsvY7NmzIZFIMHv2bMTHx8PFxQV9+vTBggULlGXatm2LX375BbNmzcK8efNQr149REVFYdSoURVev9LkFshxNSkLAODn5WjYYIiIiKhSMGhSBgBTpkzBlClT1D63f/9+lcdmZmYICwtDWFhYmdvs3bs3evfurasQde5CQgYUAnC1s4SbPS+7JCIiIgOOKTNm/92E3NGwgRAREVGlwaTMAOI4kz8RERE9hUmZAcTFF7eUMSkjIiKiIkzKKlhmngw3kovmKGP3JRERERVjUlbBzj9uJavtZIUaNs+eNJaIiIiMA5OyCvbfIH92XRIREdF/mJRVsHO88pKIiIjUYFJWwc4WX3nJmfyJiIjoCUzKKtDD7ALcS8sFALRg9yURERE9gUlZBSqen6y+sw3spVX7TvZERESkW0zKKlDqowLYWZpxkD8RERGVYPB7XxqTQa1rY0ArT2QXFBo6FCIiIqpk2FJWwUxMJLBj1yURERE9hUkZERERUSXApIyIiIioEmBSRkRERFQJMCkjIiIiqgSYlBERERFVAkzKiIiIiCoBJmVERERElQCTMiIiIqJKgEkZERERUSXApIyIiIioEmBSRkRERFQJMCkjIiIiqgSYlBERERFVAmaGDqAyEkIAADIzMw0cCSCTyZCTk4PMzEyYm5sbOpwKZax1N9Z6A6y7MdbdWOsNsO76qHvx/+3i/+NVDZMyNbKysgAAXl5eBo6EiIiItJWVlQUHBwdDh6E1iaiq6aQeKRQKJCQkwM7ODhKJxKCxZGZmwsvLC3fv3oW9vb1BY6loxlp3Y603wLobY92Ntd4A666PugshkJWVBQ8PD5iYVL0RWmwpU8PExAS1a9c2dBgq7O3tje6kLWasdTfWegOsuzHW3VjrDbDuuq57VWwhK1b10kgiIiKiaohJGREREVElwKSskrO0tERYWBgsLS0NHUqFM9a6G2u9AdbdGOturPUGWHdjrXtZONCfiIiIqBJgSxkRERFRJcCkjIiIiKgSYFJGREREVAkwKSMiIiKqBJiUGdCiRYvQtm1b2NnZwdXVFf3798eVK1fKXGft2rWQSCQqP1KptIIi1p3w8PAS9WjSpEmZ62zZsgVNmjSBVCqFr68v/vjjjwqKVre8vb1L1F0ikWDy5Mlqy1fVY37gwAH06dMHHh4ekEgk2L59u8rzQgjMnTsXtWrVgpWVFbp27Ypr1649c7srVqyAt7c3pFIpAgICcOzYMT3VoPzKqrtMJsMHH3wAX19f2NjYwMPDA6NHj0ZCQkKZ2yzPOWMIzzruY8aMKVGPV1999ZnbrezH/Vn1VnfOSyQSfPrpp6Vusyocc03+j+Xl5WHy5MmoWbMmbG1tMWjQICQlJZW53fJ+PlR1TMoMKDY2FpMnT8aRI0cQHR0NmUyG7t27Izs7u8z17O3tkZiYqPy5fft2BUWsW82bN1epx8GDB0ste+jQIYwYMQKvv/46Tp8+jf79+6N///44f/58BUasG8ePH1epd3R0NABgyJAhpa5TFY95dnY2/Pz8sGLFCrXPL1myBMuWLcPKlStx9OhR2NjYoEePHsjLyyt1m5s3b0ZoaCjCwsJw6tQp+Pn5oUePHnjw4IG+qlEuZdU9JycHp06dwpw5c3Dq1Cls27YNV65cQd++fZ+5XW3OGUN51nEHgFdffVWlHps2bSpzm1XhuD+r3k/WNzExEatXr4ZEIsGgQYPK3G5lP+aa/B9755138Pvvv2PLli2IjY1FQkICBg4cWOZ2y/P5UC0IqjQePHggAIjY2NhSy6xZs0Y4ODhUXFB6EhYWJvz8/DQuP3ToUNGrVy+VZQEBAeKtt97ScWQVb9q0acLHx0coFAq1z1eHYw5A/PLLL8rHCoVCuLu7i08//VS5LD09XVhaWopNmzaVup127dqJyZMnKx/L5XLh4eEhFi1apJe4deHpuqtz7NgxAUDcvn271DLanjOVgbq6h4SEiH79+mm1nap23DU55v369RMvv/xymWWq4jF/+v9Yenq6MDc3F1u2bFGWuXTpkgAgDh8+rHYb5f18qA7YUlaJZGRkAABq1KhRZrlHjx6hbt268PLyQr9+/XDhwoWKCE/nrl27Bg8PD9SvXx+jRo3CnTt3Si17+PBhdO3aVWVZjx49cPjwYX2HqVcFBQXYsGEDxo0bB4lEUmq56nLMi928eRP3799XOaYODg4ICAgo9ZgWFBTg5MmTKuuYmJiga9euVf59kJGRAYlEAkdHxzLLaXPOVGb79++Hq6srGjdujIkTJyI1NbXUstXxuCclJWHnzp14/fXXn1m2qh3zp/+PnTx5EjKZTOX4NWnSBHXq1Cn1+JXn86G6YFJWSSgUCkyfPh0dOnRAixYtSi3XuHFjrF69Gr/++is2bNgAhUKB9u3b4969exUY7fMLCAjA2rVrsXv3bnz99de4efMmOnbsiKysLLXl79+/Dzc3N5Vlbm5uuH//fkWEqzfbt29Heno6xowZU2qZ6nLMn1R83LQ5pikpKZDL5dXufZCXl4cPPvgAI0aMKPPGzNqeM5XVq6++iu+//x4xMTFYvHgxYmNj0bNnT8jlcrXlq+NxX7duHezs7J7ZhVfVjrm6/2P379+HhYVFiS8cZR2/8nw+VBdmhg6AikyePBnnz59/5niBwMBABAYGKh+3b98eTZs2xTfffIP58+frO0yd6dmzp/Lvli1bIiAgAHXr1sVPP/2k0bfH6uL//u//0LNnT3h4eJRaproccypJJpNh6NChEELg66+/LrNsdTlnhg8frvzb19cXLVu2hI+PD/bv349XXnnFgJFVnNWrV2PUqFHPvGCnqh1zTf+PUenYUlYJTJkyBTt27MC+fftQu3ZtrdY1NzdHq1atcP36dT1FVzEcHR3RqFGjUuvh7u5e4mqdpKQkuLu7V0R4enH79m38+eefeOONN7Rarzoc8+Ljps0xdXZ2hqmpabV5HxQnZLdv30Z0dHSZrWTqPOucqSrq168PZ2fnUutR3Y7733//jStXrmh93gOV+5iX9n/M3d0dBQUFSE9PVylf1vErz+dDdcGkzICEEJgyZQp++eUX/PXXX6hXr57W25DL5Th37hxq1aqlhwgrzqNHj/Dvv/+WWo/AwEDExMSoLIuOjlZpQapq1qxZA1dXV/Tq1Uur9arDMa9Xrx7c3d1VjmlmZiaOHj1a6jG1sLBA69atVdZRKBSIiYmpcu+D4oTs2rVr+PPPP1GzZk2tt/Gsc6aquHfvHlJTU0utR3U67kBR63jr1q3h5+en9bqV8Zg/6/9Y69atYW5urnL8rly5gjt37pR6/Mrz+VBtGPhCA6M2ceJE4eDgIPbv3y8SExOVPzk5Ocoyr732mpg5c6bycUREhNizZ4/4999/xcmTJ8Xw4cOFVCoVFy5cMEQVyu3dd98V+/fvFzdv3hT//POP6Nq1q3B2dhYPHjwQQpSs9z///CPMzMzEZ599Ji5duiTCwsKEubm5OHfunKGq8FzkcrmoU6eO+OCDD0o8V12OeVZWljh9+rQ4ffq0ACAiIyPF6dOnlVcYfvLJJ8LR0VH8+uuvIi4uTvTr10/Uq1dP5ObmKrfx8ssvi+XLlysf//jjj8LS0lKsXbtWXLx4Ubz55pvC0dFR3L9/v8LrV5ay6l5QUCD69u0rateuLc6cOaNy7ufn5yu38XTdn3XOVBZl1T0rK0u899574vDhw+LmzZvizz//FC+88IJo2LChyMvLU26jKh73Z73fhRAiIyNDWFtbi6+//lrtNqriMdfk/9iECRNEnTp1xF9//SVOnDghAgMDRWBgoMp2GjduLLZt26Z8rMnnQ3XEpMyAAKj9WbNmjbJMUFCQCAkJUT6ePn26qFOnjrCwsBBubm4iODhYnDp1quKDf07Dhg0TtWrVEhYWFsLT01MMGzZMXL9+Xfn80/UWQoiffvpJNGrUSFhYWIjmzZuLnTt3VnDUurNnzx4BQFy5cqXEc9XlmO/bt0/t+7u4bgqFQsyZM0e4ubkJS0tL8corr5TYH3Xr1hVhYWEqy5YvX67cH+3atRNHjhypoBpprqy637x5s9Rzf9++fcptPF33Z50zlUVZdc/JyRHdu3cXLi4uwtzcXNStW1eMHz++RHJVFY/7s97vQgjxzTffCCsrK5Genq52G1XxmGvyfyw3N1dMmjRJODk5CWtrazFgwACRmJhYYjtPrqPJ50N1JBFCCP20wRERERGRpjimjIiIiKgSYFJGREREVAkwKSMiIiKqBJiUEREREVUCTMqIiIiIKgEmZURERESVAJMyIiIiokqASRmRkbh16xYkEgnOnDlj6FCULl++jBdffBFSqRT+/v5qy3Tu3BnTp0+v0Lj0zdvbG1FRUYYOQyOa7P+qVB+iyoxJGVEFGTNmDCQSCT755BOV5du3b4dEIjFQVIYVFhYGGxsbXLlypcS9TYtt27YN8+fP19lrSiQSbN++XWfbK8vatWvh6OhYYvnx48fx5ptvVkgMFeHp+lTkPiaqTpiUEVUgqVSKxYsXIy0tzdCh6ExBQUG51/3333/x0ksvoW7duqXelLtGjRqws7Mr92uUx/PUSRMuLi6wtrbW62tUpOpWHyJDYVJGVIG6du0Kd3d3LFq0qNQy4eHhJbryoqKi4O3trXw8ZswY9O/fHwsXLoSbmxscHR0xb948FBYW4v3330eNGjVQu3ZtrFmzpsT2L1++jPbt20MqlaJFixaIjY1Vef78+fPo2bMnbG1t4ebmhtdeew0pKSnK5zt37owpU6Zg+vTpcHZ2Ro8ePdTWQ6FQYN68eahduzYsLS3h7++P3bt3K5+XSCQ4efIk5s2bB4lEgvDwcLXbebr7zNvbGwsXLsS4ceNgZ2eHOnXqYNWqVcrnCwoKMGXKFNSqVQtSqRR169ZV7u/ifThgwABIJBLl4+J9/t1336FevXqQSqXK8k93y/n7+6vEmp6ejrfeegtubm7Kfbpjxw7s378fY8eORUZGBiQSiUodn97unTt30K9fP9ja2sLe3h5Dhw5FUlKS8vni+NavXw9vb284ODhg+PDhyMrKUrvPiq1duxZ16tSBtbU1BgwYgKVLl6q03BW/j540ffp0dO7cWWVZYWEhpkyZAgcHBzg7O2POnDl48g59T9antH189uxZdOnSBXZ2drC3t0fr1q1x4sSJMuMnMjZMyogqkKmpKRYuXIjly5fj3r17z7Wtv/76CwkJCThw4AAiIyMRFhaG3r17w8nJCUePHsWECRPw1ltvlXid999/H++++y5Onz6NwMBA9OnTB6mpqQCKEoyXX34ZrVq1wokTJ7B7924kJSVh6NChKttYt24dLCws8M8//2DlypVq4/viiy+wdOlSfPbZZ4iLi0OPHj3Qt29fXLt2DQCQmJiI5s2b491330ViYiLee+89jeu+dOlStGnTBqdPn8akSZMwceJEXLlyBQCwbNky/Pbbb/jpp59w5coV/PDDD8rE4Pjx4wCANWvWIDExUfkYAK5fv46tW7di27ZtGo+7UygU6NmzJ/755x9s2LABFy9exCeffAJTU1O0b98eUVFRsLe3R2JiYql1VCgU6NevHx4+fIjY2FhER0fjxo0bGDZsmEq5f//9F9u3b8eOHTuwY8cOxMbGlugKf9LRo0fx+uuvY8qUKThz5gy6dOmCjz/+WKN6PW3dunUwMzPDsWPH8MUXXyAyMhLfffed2rKl7eNRo0ahdu3aOH78OE6ePImZM2fC3Ny8XPEQVVsGviE6kdEICQkR/fr1E0II8eKLL4px48YJIYT45ZdfxJOnYlhYmPDz81NZ9/PPPxd169ZV2VbdunWFXC5XLmvcuLHo2LGj8nFhYaGwsbERmzZtEkIIcfPmTQFAfPLJJ8oyMplM1K5dWyxevFgIIcT8+fNF9+7dVV777t27AoC4cuWKEEKIoKAg0apVq2fW18PDQyxYsEBlWdu2bcWkSZOUj/38/ERYWFiZ2wkKChLTpk1TPq5bt6743//+p3ysUCiEq6ur+Prrr4UQQrz99tvi5ZdfFgqFQu32AIhffvlFZVlYWJgwNzcXDx48UFlet25d8fnnn6ssezLmPXv2CBMTE+W+edqaNWuEg4NDieVPbnfv3r3C1NRU3LlzR/n8hQsXBABx7NgxZXzW1tYiMzNTWeb9998XAQEBal9XCCFGjBghgoODVZYNGzZMJZ4n35PFpk2bJoKCgpSPg4KCRNOmTVX25wcffCCaNm2qtj5CqN/HdnZ2Yu3ataXGS0RCsKWMyAAWL16MdevW4dKlS+XeRvPmzWFi8t8p7ObmBl9fX+VjU1NT1KxZEw8ePFBZLzAwUPm3mZkZ2rRpo4zj7Nmz2LdvH2xtbZU/TZo0AVDUUlOsdevWZcaWmZmJhIQEdOjQQWV5hw4dnqvOxVq2bKn8WyKRwN3dXVnPMWPG4MyZM2jcuDGmTp2KvXv3arTNunXrwsXFRas4zpw5g9q1a6NRo0ZarfekS5cuwcvLC15eXsplzZo1g6Ojo8q+8vb2VhlbV6tWrRLH9untBgQEqCx78thr48UXX1S5GCUwMBDXrl2DXC7XeBuhoaF444030LVrV3zyyScq7yciKsKkjMgAOnXqhB49emDWrFklnjMxMVEZrwMAMpmsRLmnu34kEonaZQqFQuO4Hj16hD59+uDMmTMqP9euXUOnTp2U5WxsbDTepj6UVc8XXngBN2/exPz585Gbm4uhQ4di8ODBz9ymujo961hYWVmVJ/xyed5jq46m7zVdCA8Px4ULF9CrVy/89ddfaNasGX755Re9vBZRVcWkjMhAPvnkE/z+++84fPiwynIXFxfcv39f5Z+lLucWO3LkiPLvwsJCnDx5Ek2bNgVQlNBcuHAB3t7eaNCggcqPNomYvb09PDw88M8//6gs/+eff9CsWTPdVOQZrz9s2DB8++232Lx5M7Zu3YqHDx8CKEpuNG3hcXFxQWJiovJxZmYmbt68qXzcsmVL3Lt3D1evXlW7voWFxTNfq2nTprh79y7u3r2rXHbx4kWkp6c/175q2rQpjh49qrLsyWMPlKwfoP69pm47DRs2hKmpqdrXLm0fN2rUCO+88w727t2LgQMHqr0QhciYMSkjMhBfX1+MGjUKy5YtU1neuXNnJCcnY8mSJfj333+xYsUK7Nq1S2evu2LFCvzyyy+4fPkyJk+ejLS0NIwbNw4AMHnyZDx8+BAjRozA8ePH8e+//2LPnj0YO3asVl1VQNEFBYsXL8bmzZtx5coVzJw5E2fOnMG0adN0Vhd1IiMjsWnTJly+fBlXr17Fli1b4O7urrzq0NvbGzExMbh///4zpyZ5+eWXsX79evz99984d+4cQkJCVBKRoKAgdOrUCYMGDUJ0dDRu3ryJXbt2Ka8y9fb2xqNHjxATE4OUlBTk5OSUeI2uXbsq3wunTp3CsWPHMHr0aAQFBaFNmzbl3g9Tp07F7t278dlnn+HatWv48ssvVa5+La7fiRMn8P333+PatWsICwvD+fPnS2zrzp07CA0NxZUrV7Bp0yYsX768zOP49D7Ozc3FlClTsH//fty+fRv//PMPjh8/rvwyQERFmJQRGdC8efNKdEE1bdoUX331FVasWIH/b+f+XU2P4ziOv2RWFKOFGJS+KV/JwCISg4ks31kpRCnFYJMymC1Wg4FBYjX5K/wDShlMOndTzjmde2/n1v12ej7mT59f06v354dhGDqdTn/1MvF3xuOxxuOxDMPQ8XjUZrOR1+uVpGd16/F4KJfLKRqNqt1uy+12v9xf+xPNZlOdTkfdblfRaFS73U6bzUahUOifreUzLpdLk8lE8XhcpmnqfD5ru90+5z+dTnU4HOT3+xWLxb7sq9/vK5PJqFQqqVgsqlwuKxgMvrRZrVYyTVO1Wk2RSES9Xu8ZYFOplOr1uqrVqnw+nyaTyYcxHA6H1uu1PB6P0um0stmsAoGAlsvlt/YhmUxqPp9rNpvJMAzt93sNBoOXNvl8XsPhUL1eT6Zp6na7ybKsD31ZlqX7/a5EIqFGo6FWq/Xl57fv99jpdOpyuciyLIXDYVUqFRUKBY1Go2+tEfhpHG/vLxQAAH6kxWKhdrut6/X6v6cC4BNUygAAAGyAUAYAAGADHF8CAADYAJUyAAAAGyCUAQAA2AChDAAAwAYIZQAAADZAKAMAALABQhkAAIANEMoAAABsgFAGAABgA4QyAAAAG/gFGVXwaDtHG0sAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "fidelities = []\n",
+ "for i in range(2,21):\n",
+ " fidelities.append(compare_dme_analytic(i,theta=np.pi/4))\n",
+ "\n",
+ "plt.plot(range(2,21),fidelities)\n",
+ "plt.grid()\n",
+ "plt.xlabel('Number of instruction qubits')\n",
+ "plt.ylabel('Fidelity with final state from DME with qutip')\n",
+ "plt.title('Final state after Density Matrix Exponentiation with qibo.models.qdp')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "iterations = 20\n",
+ "count_1 = np.zeros(iterations)\n",
+ "for k in range(1,iterations+1):\n",
+ " my_protocol = DensityMatrixExponentiation(theta=np.pi,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ " #print('Circuit for DME, q0 is target qubit, q1,q2 and q3 are instruction qubit')\n",
+ " #print(my_protocol.c.draw())\n",
+ " my_protocol.c.add(gates.M(0))\n",
+ " my_dict = my_protocol.c.execute(nshots=1000).frequencies(registers = True)\n",
+ " my_counter = my_dict['register0']\n",
+ " # print(f'Count of qubit 0: {my_counter}')\n",
+ " count_1[k-1] = my_counter['1']\n",
+ "count_1 /= 1000"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Text(0, 0.5, 'Count of $|1\\\\rangle$')"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAHHCAYAAABZbpmkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABdqklEQVR4nO3dd3zTdf4H8FeSNklnUrpbCoUyyihlIyhDrQwR4aeeiB5L4BThFDlP5E4Z6ol6iqiHoCjgOA6cqIgoVIYDgVKG7FWgQPdId7O+vz/apITuNsn3m+T1fDzykH7z/X7zTkPti8+UCYIggIiIiMhNycUugIiIiMiRGHaIiIjIrTHsEBERkVtj2CEiIiK3xrBDREREbo1hh4iIiNwaww4RERG5NYYdIiIicmsMO0REROTWGHaIPMz69eshk8lw8eLFRs/dtWsXZDIZdu3a5fC6PNWSJUsgk8nELoPIrTHsELWCTCZr0kPqYeGdd97B+vXrxS7D7tz1fdnbli1bMHr0aAQHB0OtVqNLly74+9//jvz8/FrnTps2zebvtr+/Pzp27Ij77rsPX3zxBcxmc61rRowYYXNNmzZtMGDAAKxdu7bO84nsTca9sYha7pNPPrH5+qOPPsL27dvx8ccf2xy/4447EB4e7szS6mUymWAwGKBSqawtCj179kRISEitUGY2m6HX66FUKiGXu96/jep7X1JiNBphNBqhVqtFef2nnnoKr7/+OhITE/Hggw+iTZs2SE1Nxdq1axEWFobk5GR07tzZev60adOwceNGvP/++wCA8vJyXLp0Cd9++y2OHj2KESNG4Ouvv0ZgYKD1mhEjRuD8+fNYtmwZACAnJwcfffQRDh8+jAULFuDll1927psmzyMQkd3MmTNHaMqPVWlpqROqaboePXoIw4cPF7uMBpnNZqGsrKxZ17jC+xLThg0bBADCxIkTBaPRaPPcvn37BF9fXyExMVEwGAzW41OnThX8/PzqvN+yZcsEAML9999vc3z48OFCjx49bI6VlpYKbdu2Ffz8/AS9Xm+nd0RUN9f7pxqRixkxYgR69uyJgwcPYtiwYfD19cU//vEPAMDXX3+NsWPHIioqCiqVCnFxcXjhhRdgMpnqvMeJEydw6623wtfXF9HR0Xj11Vdrvd7bb7+NHj16wNfXF0FBQejfvz82bNhgff7GMTuxsbE4fvw4du/ebe1mGDFiBID6x+x89tln6NevH3x8fBASEoI///nPuHr1qs0506ZNg7+/P65evYoJEybA398foaGheOqpp2q9v7rExsbirrvuwg8//ID+/fvDx8cH7777LgBg3bp1uO222xAWFgaVSoXu3btj1apVta6v730BQGFhIebNm4eYmBioVCp06tQJr7zySpO6VWQyGZYsWVJnzdOmTbN+bTAYsHTpUnTu3BlqtRrBwcG45ZZbsH37dus5dY3ZkclkmDt3LjZv3oyePXtCpVKhR48e2LZtW63X3LVrF/r37w+1Wo24uDi8++67TR4HtHTpUgQFBeG9996DQqGweW7gwIFYsGABjhw5gi+//LLRewHAM888g5EjR+Kzzz7DmTNnGjzX19cXN910E0pLS5GTk9Ok+xO1lJfYBRB5gry8PIwZMwYPPPAA/vznP1u7tNavXw9/f3/Mnz8f/v7++Omnn7Bo0SIUFRXh3//+t809CgoKMHr0aNxzzz24//778fnnn2PBggVISEjAmDFjAABr1qzB448/jvvuuw9PPPEEKioqcPToUezbtw8PPvhgnbWtWLECf/3rX+Hv749//vOfANBgl9v69esxffp0DBgwAMuWLUNWVhbefPNN/Prrrzh06BC0Wq31XJPJhFGjRmHQoEF47bXXsGPHDrz++uuIi4vD7NmzG/2+nT59GpMmTcIjjzyCWbNmoWvXrgCAVatWoUePHrj77rvh5eWFb7/9Fo899hjMZjPmzJnT6PsqKyvD8OHDcfXqVTzyyCNo164dfvvtNyxcuBAZGRlYsWJFo7U1xZIlS7Bs2TLMnDkTAwcORFFREVJSUpCamoo77rijwWt/+eUXfPnll3jssccQEBCAt956C/feey8uX76M4OBgAMChQ4cwevRoREZGYunSpTCZTHj++ecRGhraaG1nz57F6dOnMW3aNJsup+tNmTIFixcvxrfffov777+/Se958uTJ+PHHH7F9+3Z06dKlwXMvXLgAhUJh83eGyCHEbloicid1dWMNHz5cACCsXr261vl1dcs88sgjgq+vr1BRUVHrHh999JH1WGVlpRARESHce++91mPjx4+v1V1wo3Xr1gkAhLS0NOux+rp7du7cKQAQdu7cKQiCIOj1eiEsLEzo2bOnUF5ebj1vy5YtAgBh0aJF1mNTp04VAAjPP/+8zT379Okj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AgIwefJk5OXloaysrMmvf/LkScTExFiDDgB0794dWq0WJ0+etB6LjY21Bh0AiIyMRHZ2NgAgMTERt99+OxISEvCnP/0Ja9asQUFBQdO/CUTkMAw7RCS6kpISjBs3DocPH7Z5nD17FsOGDbOe5+fnZ3PdxYsXcdddd6FXr1744osvcPDgQaxcuRJA1QBme/P29rb5WiaTwWw2AwAUCgW2b9+O77//Ht27d8fbb7+Nrl27Ii0tze51EFHzMOwQkVMplUqYTCabY3379sXx48cRGxuLTp062TxuDDjXO3jwIMxmM15//XXcdNNN6NKlC65du9bo692oW7duSE9PR3p6uvXYiRMnUFhYiO7duzf5vclkMtx8881YunQpDh06BKVSia+++qrJ1xORYzDsEJFTxcbGYt++fbh48SJyc3NhNpsxZ84c5OfnY9KkSThw4ADOnz+PH374AdOnT28wqHTq1AkGgwFvv/02Lly4gI8//tg6cPn61yspKUFycjJyc3Pr7N5KSkpCQkICHnroIaSmpmL//v2YMmUKhg8fjv79+zfpfe3btw8vvfQSUlJScPnyZXz55ZfIyclBt27dmvcNIiK7Y9ghIqd66qmnoFAo0L17d4SGhuLy5cuIiorCr7/+CpPJhJEjRyIhIQHz5s2DVquFXF7//6YSExOxfPlyvPLKK+jZsyf++9//YtmyZTbnDBkyBI8++igmTpyI0NDQWgOcgaoWma+//hpBQUEYNmwYkpKS0LFjR2zatKnJ7yswMBB79uzBnXfeiS5duuDZZ5/F66+/jjFjxjT9m0NEDiETOC+SiIiI3BhbdoiIiMitMewQERGRW2PYISIiIrfGsENERERujWGHiIiI3BrDDhEREbk1hh0iIiJyaww7RERE5NYYdoiIiMitMewQERGRW2PYISIiIrfGsENERERu7f8By0Xy+V55vmYAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "plt.plot(count_1)\n",
+ "plt.title('Transition rate using QDP')\n",
+ "plt.xlabel('Iterations')\n",
+ "plt.ylabel(r'Count of $|1\\rangle$')"
+ ]
+ },
+ {
+ "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.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/qdp/qdp_tutorial_different_k.ipynb b/examples/qdp/qdp_tutorial_different_k.ipynb
new file mode 100644
index 000000000..9eefbb095
--- /dev/null
+++ b/examples/qdp/qdp_tutorial_different_k.ipynb
@@ -0,0 +1,282 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Density matrix exponentiation\n",
+ "## Overview\n",
+ "This is a function to simulate DME. It follows the protocol laid down by https://arxiv.org/abs/2001.08838\n",
+ "\n",
+ "Goals: implement the unitary $U=e^{-i\\rho\\theta}$ on data qubit $\\sigma$ (rotate $\\sigma$ by $\\theta$) according to instruction given by instruction qubit $\\rho$.\n",
+ "\n",
+ "DME uses the relation (rotate $\\sigma$ by small angle $\\delta = \\sigma/N$).\n",
+ "$$\\begin{align}\n",
+ "Tr_\\rho [e^{-iSWAP\\delta}\\sigma\\otimes\\rho e^{iSWAP\\delta}] &= \\sigma - i\\delta[\\rho,\\sigma] +\\mathcal{O}(\\delta^2)\\\\\n",
+ "&= e^{-i\\rho\\delta}\\sigma e^{i\\rho\\delta}+\\mathcal{O}(\\delta^2)\n",
+ "\\end{align}$$\n",
+ "\n",
+ "Reference\n",
+ "- Kjaergaard, M., Schwartz, M. E., Greene, A., Samach, G. O., Bengtsson, A., O'Keeffe, M., ... Oliver, W. D. (2020). \n",
+ " Programming a quantum computer with quantum instructions. arXiv preprint arXiv:2001.08838. \n",
+ " Retrieved from https://arxiv.org/abs/2001.08838 \n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Code exlanation\n",
+ "In this code, we set $\\theta = \\pi, N =k$ with 1 work qubit and $k$ instruction qubits (it is possible to set more instruction qubits here, but they will just be idle). \n",
+ "\n",
+ "The instruction qubits are thrown out after usage (We call it Sequential Instruction Qubit protocol). Each query use, accordingly, k qubits. Each call use 1 query.\n",
+ "\n",
+ "Our goal is to rotate qubit 0 from state $\\ket{0}$ to state $\\ket{1}$ by using an $RX(\\pi)$ pulse. We demonstrate this by showing the number of counter that return 1."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qibo.models.qdp.memory_usage_query import DensityMatrixExponentiation\n",
+ "from qibo.quantum_info.metrics import fidelity\n",
+ "import numpy as np\n",
+ "from qibo import gates\n",
+ "theta = np.pi"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### $\\rho = \\ket +,\\sigma = \\ket 0$. Expect $\\sigma_f = \\ket +$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class DME_test(DensityMatrixExponentiation):\n",
+ " def instruction_qubits_initialization(self):\n",
+ " \"\"\"Initializes the instruction qubits.\"\"\"\n",
+ " for instruction_qubit in self.list_id_current_instruction_reg:\n",
+ " self.c.add(gates.RX(instruction_qubit,np.pi/2))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.2.9|INFO|2024-07-01 11:56:30]: Using qibojit (numba) backend on /CPU:0\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Final state = 0.707*|0> + 0.707*|1>\n"
+ ]
+ }
+ ],
+ "source": [
+ "k = 10\n",
+ "my_protocol = DME_test(theta=np.pi/2,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ "#my_protocol.c.add(gates.X(0))\n",
+ "my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ "qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ "qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ "print(f'Final state = {qubit_0[0]}*|0> + {qubit_0[1]}*|1>')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Fidelity: 0.9996979999999996\n"
+ ]
+ }
+ ],
+ "source": [
+ "state_dme = np.array([qubit_0[0],qubit_0[1]])\n",
+ "ket_plus = np.array([[1/np.sqrt(2),1/np.sqrt(2)]])\n",
+ "print(f'Fidelity: {np.inner(ket_plus,state_dme)[0]**2}')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k = 5\n",
+ "ket_plus = np.array([[1/np.sqrt(2),1/np.sqrt(2)]])\n",
+ "res_5 = []\n",
+ "thetas = np.linspace(0,3*np.pi,50)\n",
+ "\n",
+ "for theta in thetas:\n",
+ " my_protocol = DME_test(theta=theta,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ " #my_protocol.c.add(gates.X(0))\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ " qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ " qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ " state_dme = np.array([qubit_0[0],qubit_0[1]])\n",
+ "\n",
+ " res_5.append(np.inner(ket_plus,state_dme)[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k = 10\n",
+ "ket_plus = np.array([[1/np.sqrt(2),1/np.sqrt(2)]])\n",
+ "res_10 = []\n",
+ "thetas = np.linspace(0,3*np.pi,50)\n",
+ "\n",
+ "for theta in thetas:\n",
+ " my_protocol = DME_test(theta=theta,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ " #my_protocol.c.add(gates.X(0))\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ " qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ " qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ " state_dme = np.array([qubit_0[0],qubit_0[1]])\n",
+ "\n",
+ " res_10.append(np.inner(ket_plus,state_dme)[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k = 15\n",
+ "ket_plus = np.array([[1/np.sqrt(2),1/np.sqrt(2)]])\n",
+ "res_15 = []\n",
+ "thetas = np.linspace(0,3*np.pi,50)\n",
+ "\n",
+ "for theta in thetas:\n",
+ " my_protocol = DME_test(theta=theta,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ " #my_protocol.c.add(gates.X(0))\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ " qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ " qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ " state_dme = np.array([qubit_0[0],qubit_0[1]])\n",
+ "\n",
+ " res_15.append(np.inner(ket_plus,state_dme)[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k = 20\n",
+ "ket_plus = np.array([[1/np.sqrt(2),1/np.sqrt(2)]])\n",
+ "res_20 = []\n",
+ "thetas = np.linspace(0,3*np.pi,50)\n",
+ "\n",
+ "for theta in thetas:\n",
+ " my_protocol = DME_test(theta=theta,N=k,num_work_qubits=1,num_instruction_qubits=k,number_muq_per_call=1)\n",
+ " #my_protocol.c.add(gates.X(0))\n",
+ " my_protocol.memory_call_circuit(num_instruction_qubits_per_query=k)\n",
+ " qubit_0 = my_protocol.c.execute(nshots=1000).probabilities([0])\n",
+ " qubit_0 = np.round(np.sqrt(qubit_0),3)\n",
+ " state_dme = np.array([qubit_0[0],qubit_0[1]])\n",
+ "\n",
+ " res_20.append(np.inner(ket_plus,state_dme)[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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4nwniZwQF5CGnsC02Wz1LtdqtfUuV5vYZ81utwNw0003MX369Hq3+f0Tl43tU9XUcwL3nddpd911F2+//Tb33nsvQ4YMITAwEEVRuP76653nfqaGzm3atGncc889nDx5ErPZzM8//1xvK9Tv7du3r8GnNDdt2gTAoEGD6l1fU1NDUVHROY8Bjj6Bv6+7o0ePYjKZePLJJxk8eHC9+zXnIYu9e/cycuRIl/cX4nwniZwQF5Dg4OB6x0E7fvy4S+WFh4fj7e3N4cOH66w7ePBgre38/f2x2WzNav2pj7vPqSFnG4bkk08+Yfr06fz73/92Lquurm7ymHPXX389c+bM4cMPP6Sqqgq9Xs9111131n2sViuVlZUNxrds2TKCg4Nr3bY+06ZNm7j00ksbFV96enqdJ4TLy8sBx+10d39vAVatWuX2MoU4n0giJ8QFJDk5mdLS0lpPBWZnZ9c77EVjaLVaxo0bx+eff05GRobzluX+/fv57rvvam33xz/+kaVLl7J37946fbny8/OdT4B6+pwa4uvrC1BvcqbVauu05r300ktnbRWsT1hYGBMmTOCDDz6gurqa8ePHO5+QbYhOpyMuLo4NGzbU6eP39ttvs379eh577DEMBkO9+ze3j1xCQgKKorBixQr++Mc/1lpntVopLy933roWQrifJHJCXECuv/56/v73vzNlyhTuvvtuTCYTr732Gp07dyY1NdWlMhcsWMDKlSsZMWIEf/3rX7Farbz00kv06NHDOSwGwDPPPMO6desYNGgQs2bNonv37hQVFZGamsqaNWsafXuvNc6pPv379wfgkUce4frrr0ev1zNp0iR8fX258soref/99wkMDKR79+5s3ryZNWvW1Br7rLGmTZvGn/70JwCefPLJRu3z97//ndmzZzNgwABuuOEG9Ho9GzduZOXKlYwcOZKHH364wX2b20cuIiKCG264gaVLl1JWVsaECROw2WykpaXx6aefsmzZsnY1NZgQ7Y0kckJcQEJDQ/nss8+YM2cODz74oHMstcOHD7uc9PTq1YvvvvuOOXPm8Pjjj9OhQwcWLFhAdnZ2rUQuMjKSrVu38sQTT/Dpp5/y6quvEhoaSo8ePRocv81T51Sfiy++mCeffJLXX3+dlStXYrfbSU9Px9fXlxdffBGtVsuSJUuorq5m2LBhrFmzxqXZBSZNmkRwcDB2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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
+ "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
+ "\u001b[1;31mClick here for more info. \n",
+ "\u001b[1;31mView Jupyter log for further details."
+ ]
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "f,ax=plt.subplots(figsize=(7,4))\n",
+ "thetas = np.linspace(0,3*np.pi,50)\n",
+ "thetas = thetas/np.pi\n",
+ "ax.plot(thetas,res_10,label='k=10',alpha=0.7)\n",
+ "plt.plot(thetas,res_15,label='k=15',alpha=0.7)\n",
+ "plt.plot(thetas,res_20,label='k=20',alpha=0.7)\n",
+ "plt.grid()\n",
+ "plt.xlabel(r'Angle of rotation $\\theta$')\n",
+ "plt.ylabel(r'Expectation value $\\langle{+|\\psi}\\rangle$')\n",
+ "plt.title(r'Using instruction state $\\rho = |+\\rangle$ to rotate $\\sigma =|0\\rangle$'+'\\n'+r'under unitary $U = e^{-i\\rho\\theta}$')\n",
+ "plt.legend()\n",
+ "import matplotlib.ticker as tck\n",
+ "ax.xaxis.set_major_formatter(tck.FormatStrFormatter('%g $\\pi$'))\n",
+ "ax.xaxis.set_major_locator(tck.MultipleLocator(base=0.5))\n",
+ "\n"
+ ]
+ },
+ {
+ "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.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/qfiae/images/iqae.png b/examples/qfiae/images/iqae.png
new file mode 100644
index 000000000..bae2fca50
Binary files /dev/null and b/examples/qfiae/images/iqae.png differ
diff --git a/examples/qfiae/images/linear_ansatz.png b/examples/qfiae/images/linear_ansatz.png
new file mode 100644
index 000000000..8be0a2875
Binary files /dev/null and b/examples/qfiae/images/linear_ansatz.png differ
diff --git a/examples/qfiae/images/sketch_qfiae.png b/examples/qfiae/images/sketch_qfiae.png
new file mode 100644
index 000000000..9408273da
Binary files /dev/null and b/examples/qfiae/images/sketch_qfiae.png differ
diff --git a/examples/qfiae/qfiae_demo.ipynb b/examples/qfiae/qfiae_demo.ipynb
new file mode 100644
index 000000000..59d78b008
--- /dev/null
+++ b/examples/qfiae/qfiae_demo.ipynb
@@ -0,0 +1,934 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "e72f8260",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# Quantum Fourier Iterative Amplitude Estimation\n",
+ "\n",
+ "Code at: [https://github.com/qiboteam/qibo/tree/master/examples/qfiae](https://github.com/qiboteam/qibo/tree/master/examples/qfiae).\n",
+ "\n",
+ "Based on the paper [arXiv:2305.01686](https://arxiv.org/abs/2305.01686)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b5207617",
+ "metadata": {},
+ "source": [
+ "## Problem overview\n",
+ "This tutorial implements an example of the application of the Quantum Fourier Iterative Amplitude Estimation (QFIAE) method, which allows to perform Quantum Monte Carlo integration of any one-dimensional function $f(x)$. \n",
+ "\n",
+ "The QFIAE algorithm functioning is the following:\n",
+ "\n",
+ " 1. The function we want to integrate $f(x)$, the probability distribution $p(x)$ and the limits of the integral $x_{min}$, $x_{max}$ are defined\n",
+ "\n",
+ " 2. A Quantum Neural Network (QNN) is employed to fit the function $f(x)$ using a linear ansatz of the following form:\n",
+ " \n",
+ "
\n",
+ "
\n",
+ " \n",
+ " where $\\mathcal{L}^{(i)}$ is the i-th layer, that for the one-dimensional case is composed of the trainable gates:\n",
+ " $$ \\mathcal{A}^{(i)}=R_z(\\theta_{i,2})R_z(\\theta_{i,1})R_z(\\theta_{i,0}),$$\n",
+ " and the encoding gate:\n",
+ " $$ \\mathcal{S}=R_z(x).$$\n",
+ " \n",
+ " Now, as demonstrated in [arXiv:2008.08605](https://arxiv.org/abs/2008.08605) the expectation value of this quantum model corresponds to a universal 1-D Fourier series:\n",
+ " $$\\langle \\textit{M} \\rangle (x, {\\vec{\\theta}}) = \\sum_{w \\in \\Omega} c_w e^{ixw}~.$$\n",
+ "\n",
+ " 3. The Fourier coefficients of the quantum circuit that fits the function $f(x)$ are extracted using [discrete Fourier transform](https://numpy.org/doc/stable/reference/generated/numpy.fft.fft.html)\n",
+ " \n",
+ " 4. The Iterative Quantum Amplitude Estimation (IQAE) model of qibo, which corresponds with the following quantum circuit:\n",
+ " \n",
+ "
\n",
+ "
\n",
+ " \n",
+ " is used to estimate the integral of each trigonometric piece, $\\sin(\\omega_i x)$ or $\\cos(\\omega_j x)$, of the Fourier series.\n",
+ " \n",
+ " 5. The integrals obtained are summed with their corresponding coefficients to derive the final integral result.\n",
+ "\n",
+ "\n",
+ "A summary of the workflow of the algorithm is the following:\n",
+ "![workflow](images/sketch_qfiae.png)\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4dc809c2",
+ "metadata": {},
+ "source": [
+ "## QFIAE Demo"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3cc8fa72",
+ "metadata": {},
+ "source": [
+ "Let's define the problem:\n",
+ "\n",
+ "We are interested in computing the following example integral:\n",
+ "$$ \\int_0^1 dx(1+x^2),$$\n",
+ "so the easiest way to define our problem variables would be:\n",
+ "\n",
+ " - $x_{min}$: $0$\n",
+ " \n",
+ " - $x_{max}$: $1$\n",
+ " \n",
+ " - $p(x)$: $dx$\n",
+ " \n",
+ " - $f(x)$: $1+x^2$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "aeb6c601",
+ "metadata": {},
+ "source": [
+ "## Code\n",
+ "\n",
+ "First, we must decide the variables for our problem. \n",
+ "\n",
+ "Regarding the definition of the problem:\n",
+ "\n",
+ "- `x_min_int`: the lower limit of the problem integral.\n",
+ "\n",
+ "- `x_max_int`: the upper limit of the problem integral.\n",
+ "\n",
+ "Regarding the QNN:\n",
+ "\n",
+ "- `nlayers`: number of layers of the QNN.\n",
+ "\n",
+ "- `nepochs`: number of epochs, i.e. iterations of the AdamDescent Optimizer.\n",
+ "\n",
+ "- `loss_treshold`: value of the desired loss function's treshold.\n",
+ "\n",
+ "- `learning_rate`: learning rate of the AdamDescent Optimizer.\n",
+ "\n",
+ "- `nbatches`: number of batches in which divide the dataset.\n",
+ "\n",
+ "- `ndata_points`: number of data points to define $f(x)$.\n",
+ "\n",
+ "- `x_max_fit`: upper limit of the function we are going to fit. \n",
+ "\n",
+ "- `x_min_fit`: lower limit of the function we are going to fit. \n",
+ "\n",
+ "Regarding the IQAE:\n",
+ "\n",
+ "- `nqubits`: number of qubits decided to build the good state in the IQAE algorithm.\n",
+ "\n",
+ "- `alpha`: confidence level, the target probability is 1 - `alpha`, has values between 0 and 1.\n",
+ "\n",
+ "- `epsilon`: target precision for estimation target $a$, has values between 0 and 0.5.\n",
+ "\n",
+ "- `nshots`: number of shots used to perform IQAE. \n",
+ "\n",
+ "- `method_iqae`: statistical method employed by IQAE. It will be *chernoff* or *beta*. \n",
+ "\n",
+ "Regardind the results:\n",
+ "\n",
+ "- `qfiae_integral`: estimated value of the integral using QFIAE.\n",
+ "\n",
+ "- `iae_error`: statistical uncertainty in the integral estimation due to IQAE.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5f8debc6",
+ "metadata": {},
+ "source": [
+ "Include necessary packages:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "351d99ae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "from qibo import Circuit, gates\n",
+ "from qibo.models.iqae import IQAE"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "21cbb3f8",
+ "metadata": {},
+ "source": [
+ "### 1. Defining $f(x)$, $x_{min}$, $x_{max}$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "7db33743",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def f(x): \n",
+ " \"\"\"Target function f(x) = 1 + x ^ 2\"\"\"\n",
+ " \n",
+ " y = 1 + x ** 2\n",
+ " return y\n",
+ "\n",
+ "x_min_int = 0\n",
+ "x_max_int = 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3a248119",
+ "metadata": {},
+ "source": [
+ "### 2. Defining and training the QNN"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d3558111",
+ "metadata": {},
+ "source": [
+ "We import the QNN: `vqregressor_linear_ansatz`, which is a modified version of the vqregressor found at https://github.com/qiboteam/qibo/tree/master/examples/vqregressor. In our modified version, we replace the ansatz function used in the original implementation with the one we have previously defined."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "975d48a2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from vqregressor_linear_ansatz import VQRegressor_linear_ansatz"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a823c919",
+ "metadata": {},
+ "source": [
+ "We define the parameters of the QNN"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "cec09f1f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note that the chosen number of layers is intentionally larger than necessary to fit the function. \n",
+ "# This configuration ensures high accuracy while keeping a reasonable number of epochs.\n",
+ "nlayers = 10\n",
+ "nepochs = 100\n",
+ "loss_treshold = 10^-4\n",
+ "learning_rate = 0.05\n",
+ "nbatches = 1\n",
+ "ndata_points = 200\n",
+ "# Note they can be different than x_min,x_max, but [x_max,x_min] ⊆ [x_max_fit,x_min_fit] \n",
+ "x_max_fit = 1\n",
+ "x_min_fit = -1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "36f22f9c",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.1.16|INFO|2023-07-31 08:50:42]: Using qibojit (numba) backend on /CPU:0\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Iteration 1 epoch 1 | loss: 0.7513285431118756\n",
+ "Iteration 2 epoch 2 | loss: 0.49078195900193\n",
+ "Iteration 3 epoch 3 | loss: 0.25449817735514246\n",
+ "Iteration 4 epoch 4 | loss: 0.1246609331106105\n",
+ "Iteration 5 epoch 5 | loss: 0.0656780490580047\n",
+ "Iteration 6 epoch 6 | loss: 0.033157170041590786\n",
+ "Iteration 7 epoch 7 | loss: 0.021080634318318345\n",
+ "Iteration 8 epoch 8 | loss: 0.02302933284076279\n",
+ "Iteration 9 epoch 9 | loss: 0.02804566383739096\n",
+ "Iteration 10 epoch 10 | loss: 0.02993298809989462\n",
+ "Iteration 11 epoch 11 | loss: 0.02809272996633005\n",
+ "Iteration 12 epoch 12 | loss: 0.024260018563301875\n",
+ "Iteration 13 epoch 13 | loss: 0.020466737677239978\n",
+ "Iteration 14 epoch 14 | loss: 0.017961154451515637\n",
+ "Iteration 15 epoch 15 | loss: 0.01635525503721851\n",
+ "Iteration 16 epoch 16 | loss: 0.013824870329752614\n",
+ "Iteration 17 epoch 17 | loss: 0.009480363571353416\n",
+ "Iteration 18 epoch 18 | loss: 0.005428317914456559\n",
+ "Iteration 19 epoch 19 | loss: 0.004747379983842302\n",
+ "Iteration 20 epoch 20 | loss: 0.007261736229780217\n",
+ "Iteration 21 epoch 21 | loss: 0.008972805823541971\n",
+ "Iteration 22 epoch 22 | loss: 0.006995705828705413\n",
+ "Iteration 23 epoch 23 | loss: 0.0031234835775331904\n",
+ "Iteration 24 epoch 24 | loss: 0.0010645149525778657\n",
+ "Iteration 25 epoch 25 | loss: 0.001660608449799947\n",
+ "Iteration 26 epoch 26 | loss: 0.0024468184819136764\n",
+ "Iteration 27 epoch 27 | loss: 0.0019337945857293712\n",
+ "Iteration 28 epoch 28 | loss: 0.0012357754610404163\n",
+ "Iteration 29 epoch 29 | loss: 0.0013015455646036862\n",
+ "Iteration 30 epoch 30 | loss: 0.0016682446926897167\n",
+ "Iteration 31 epoch 31 | loss: 0.0016495496176469587\n",
+ "Iteration 32 epoch 32 | loss: 0.0012520977035885572\n",
+ "Iteration 33 epoch 33 | loss: 0.0009722650541584987\n",
+ "Iteration 34 epoch 34 | loss: 0.0010738639185073821\n",
+ "Iteration 35 epoch 35 | loss: 0.001237623835436679\n",
+ "Iteration 36 epoch 36 | loss: 0.001079914927835038\n",
+ "Iteration 37 epoch 37 | loss: 0.0008087925855009583\n",
+ "Iteration 38 epoch 38 | loss: 0.0008412140573143836\n",
+ "Iteration 39 epoch 39 | loss: 0.0010243360494146807\n",
+ "Iteration 40 epoch 40 | loss: 0.0009260517614442422\n",
+ "Iteration 41 epoch 41 | loss: 0.0006083674444202678\n",
+ "Iteration 42 epoch 42 | loss: 0.00048552087540479593\n",
+ "Iteration 43 epoch 43 | loss: 0.0006110987021632932\n",
+ "Iteration 44 epoch 44 | loss: 0.0006559683517174265\n",
+ "Iteration 45 epoch 45 | loss: 0.0005150053878726019\n",
+ "Iteration 46 epoch 46 | loss: 0.0004114721142009153\n",
+ "Iteration 47 epoch 47 | loss: 0.0004471177670137205\n",
+ "Iteration 48 epoch 48 | loss: 0.0004770075457458069\n",
+ "Iteration 49 epoch 49 | loss: 0.00040857499834427296\n",
+ "Iteration 50 epoch 50 | loss: 0.00033020904524591827\n",
+ "Iteration 51 epoch 51 | loss: 0.0003179087176282798\n",
+ "Iteration 52 epoch 52 | loss: 0.00032425348191094495\n",
+ "Iteration 53 epoch 53 | loss: 0.0003081269055732997\n",
+ "Iteration 54 epoch 54 | loss: 0.00029739364175727137\n",
+ "Iteration 55 epoch 55 | loss: 0.0002972067254557964\n",
+ "Iteration 56 epoch 56 | loss: 0.0002790839752707141\n",
+ "Iteration 57 epoch 57 | loss: 0.000250994009314296\n",
+ "Iteration 58 epoch 58 | loss: 0.00024068099758678817\n",
+ "Iteration 59 epoch 59 | loss: 0.00024012049242896222\n",
+ "Iteration 60 epoch 60 | loss: 0.00023072309010057932\n",
+ "Iteration 61 epoch 61 | loss: 0.0002204255806400206\n",
+ "Iteration 62 epoch 62 | loss: 0.00021693211012467254\n",
+ "Iteration 63 epoch 63 | loss: 0.00021032053408251615\n",
+ "Iteration 64 epoch 64 | loss: 0.00019814302410227347\n",
+ "Iteration 65 epoch 65 | loss: 0.00018970104609909337\n",
+ "Iteration 66 epoch 66 | loss: 0.0001849409887409433\n",
+ "Iteration 67 epoch 67 | loss: 0.000178175085771177\n",
+ "Iteration 68 epoch 68 | loss: 0.00017235266528996997\n",
+ "Iteration 69 epoch 69 | loss: 0.00016920670127216307\n",
+ "Iteration 70 epoch 70 | loss: 0.00016352665275946328\n",
+ "Iteration 71 epoch 71 | loss: 0.0001558330982688153\n",
+ "Iteration 72 epoch 72 | loss: 0.00015122871287318634\n",
+ "Iteration 73 epoch 73 | loss: 0.0001481650541488665\n",
+ "Iteration 74 epoch 74 | loss: 0.0001435783238101642\n",
+ "Iteration 75 epoch 75 | loss: 0.000139808928386417\n",
+ "Iteration 76 epoch 76 | loss: 0.00013763074842478883\n",
+ "Iteration 77 epoch 77 | loss: 0.00013405673992693997\n",
+ "Iteration 78 epoch 78 | loss: 0.00012925759824435786\n",
+ "Iteration 79 epoch 79 | loss: 0.00012568131156775818\n",
+ "Iteration 80 epoch 80 | loss: 0.000122960138626043\n",
+ "Iteration 81 epoch 81 | loss: 0.00012002635696736961\n",
+ "Iteration 82 epoch 82 | loss: 0.00011738809629738198\n",
+ "Iteration 83 epoch 83 | loss: 0.00011504380983272437\n",
+ "Iteration 84 epoch 84 | loss: 0.00011252787350936655\n",
+ "Iteration 85 epoch 85 | loss: 0.00011014441030141236\n",
+ "Iteration 86 epoch 86 | loss: 0.00010794102346639878\n",
+ "Iteration 87 epoch 87 | loss: 0.00010557841843900766\n",
+ "Iteration 88 epoch 88 | loss: 0.00010325318195926471\n",
+ "Iteration 89 epoch 89 | loss: 0.00010112572290540777\n",
+ "Iteration 90 epoch 90 | loss: 9.901680049760298e-05\n",
+ "Iteration 91 epoch 91 | loss: 9.702508371139033e-05\n",
+ "Iteration 92 epoch 92 | loss: 9.525568074239291e-05\n",
+ "Iteration 93 epoch 93 | loss: 9.354329673964097e-05\n",
+ "Iteration 94 epoch 94 | loss: 9.188064428852769e-05\n",
+ "Iteration 95 epoch 95 | loss: 9.032782168927708e-05\n",
+ "Iteration 96 epoch 96 | loss: 8.881823164035069e-05\n",
+ "Iteration 97 epoch 97 | loss: 8.739132343365943e-05\n",
+ "Iteration 98 epoch 98 | loss: 8.607781395924396e-05\n",
+ "Iteration 99 epoch 99 | loss: 8.477887801842459e-05\n",
+ "Iteration 100 epoch 100 | loss: 8.350302085692635e-05\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAKyCAYAAAA6kpdwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eXyddZ3//z/e11lzcrK1SdOFpWkIBVoCAhYECSgMBaRa0O+MM8NP6ooIrsAYtUrRjlTBXQR1Zqyj8nFBXMogoiMQF2hBoBmKhNKkFLukW7aTs5/r/fvjnZM0bdKm0DZt+rzfbjHkOtd1nes66Tnx9rxe1+tlrLUWERERERERERERERHZgzfeByAiIiIiIiIiIiIicrhSiC4iIiIiIiIiIiIiMgqF6CIiIiIiIiIiIiIio1CILiIiIiIiIiIiIiIyCoXoIiIiIiIiIiIiIiKjUIguIiIiIiIiIiIiIjIKhegiIiIiIiIiIiIiIqNQiC4iIiIiIiIiIiIiMgqF6CIiIiIiIiIiIiIio1CILiIiIiL7NHPmTBYtWjT48yOPPIIxhkceeeSAPYcxhiVLlhyw/Y2XH/zgB5x00kmEQiEqKyv3e/v169djjOGOO+448Ad3lHi1v4MjwaJFi5g5c+Yr2nbJkiUYYw7sAYmIiIhMYArRRURERA5zy5cvxxgz+BWNRjnxxBO54YYb6OzsHO/D2y8PPPDAhAjKR/P888+zaNEi6uvr+e53v8t3vvOdUdc9XF6L/v5+Pve5z9HY2EgsFqOiooLzzz+fH/zgB1hr91i/+O/wS1/60h6PFf+tPvnkk4PLioFtbW0tyWRyj21mzpzJFVdcccDOZ6TfQTKZZMmSJQf0os++bNq0iSVLlvDMM88csucUERERkYMjON4HICIiIiJj89nPfpa6ujrS6TR/+tOfuOuuu3jggQd49tlnicVih/RYmpqaSKVShMPh/drugQce4M477xwxPE6lUgSDR/b/PX3kkUfwfZ+vfe1rnHDCCXtdd2+vxaHS2dnJRRddxN/+9jfe/va3c8MNN5BOp/n5z3/OO97xDh588EF+8IMf4Hl71t7cfvvtXHfddWP+t7d161buuusubrzxxgN9GsOM9DvYvn07t956KwAXXnjhQX3+ok2bNnHrrbcyc+ZMTj/99AO+/+9+97v4vv+Ktl28eDHNzc0H+IhEREREJi5VoouIiIgcIS677DKuvvpq3vOe97B8+XI+8pGP0NHRwa9+9atRt+nv7z8ox+J5HtFodMRw9ZWKRqNHfIi+detWgCOmhcg111zD3/72N37xi1/wox/9iPe973186EMf4tFHH+Wmm27innvuGbHi/PTTT6ezs5O77757zM91+umnc/vtt5NKpQ7kKezhUP4ODuT7a6Qq/b0JhUJEIpFX9FzBYJBoNPqKthURERE5GilEFxERETlCvfGNbwSgo6MDcD2S4/E469at4/LLL6esrIx//dd/BcD3fb761a8yZ84cotEotbW1XHvttXR1dQ3bp7WWpUuXcswxxxCLxXjDG97AmjVr9nju0Xqir1y5kssvv5yqqipKS0tpbGzka1/72uDx3XnnnQDD2tMUjdQT/emnn+ayyy6jvLyceDzORRddxOOPPz5snWILkT//+c987GMfo6amhtLSUq688kq2bds2bN0nn3yS+fPnU11dTUlJCXV1dbzrXe8ay8vNt771LebMmUMkEmH69Olcf/31dHd3Dz4+c+ZMbrnlFgBqamr22uN9X69F0Xe+8x3q6+uJRCK89rWv5Yknnthjneeff563ve1tTJo0iWg0yllnncWvf/3rfZ7P448/zm9/+1sWLVrEm9/85j0ev+2222hoaGDZsmV7BN/nnXceb3zjG/niF7845lD8M5/5DJ2dndx1111jWn93v/rVr3jTm97E9OnTiUQi1NfX87nPfY5CoTC4zki/g0WLFlFTUwPArbfeOvha7/q7GctrWPx39uijj/KBD3yAKVOmcMwxx4x4rI888givfe1rAXjnO985+JzLly8HXDX83Llz+etf/0pTUxOxWIxPfvKTYz5P2LMn+q699Pf172aknujGGG644QZ++ctfMnfuXCKRCHPmzOHBBx8c8fzOOussotEo9fX1fPvb31afdREREZnQjuxSHxEREZGj2Lp16wCYPHny4LJ8Ps/8+fN5/etfzx133DHYauPaa69l+fLlvPOd7+RDH/oQHR0dfPOb3+Tpp5/mz3/+M6FQCHBB59KlS7n88su5/PLLeeqpp7jkkkvIZrP7PJ7f/e53XHHFFUybNo0Pf/jDTJ06lb/97W/cf//9fPjDH+baa69l06ZN/O53v+MHP/jBPve3Zs0azj//fMrLy/m3f/s3QqEQ3/72t7nwwgt59NFHOfvss4et/8EPfpCqqipuueUW1q9fz1e/+lVuuOEGfvKTnwCuQvmSSy6hpqaG5uZmKisrWb9+Pffdd98+j2XJkiXceuutXHzxxVx33XW0tbVx11138cQTTwy+fl/96lf57//+b37xi19w1113EY/HaWxsHHF/Y3kt7rnnHvr6+rj22msxxvDFL36Rq666ivb29sHf15o1azjvvPOYMWMGzc3NlJaW8tOf/pSFCxfy85//nCuvvHLUc1qxYgUA73jHO0Z8PBgM8i//8i/ceuut/OUvf+Giiy7a4zVpamrirrvu4mMf+9g+X8Pzzz9/MHi/7rrrKCkp2ec2u1q+fDnxeJyPfexjxONx/vCHP/CZz3yG3t5ebr/9doARfwennnoq55xzDtdddx1XXnklV111FcDg72Z/X8MPfOAD1NTU8JnPfGbUSvSTTz6Zz372s3zmM5/hfe97H+effz4A55577uA6O3bs4LLLLuPtb387V199NbW1tWM+z70Zy7+b0fzpT3/ivvvu4wMf+ABlZWV8/etf561vfSsbNmwY/Jx5+umnufTSS5k2bRq33norhUKBz372s4MXKkREREQmJCsiIiIih7Xvfe97FrC///3v7bZt2+zLL79sf/zjH9vJkyfbkpIS+/e//91aa+0111xjAdvc3Dxs+z/+8Y8WsD/60Y+GLX/wwQeHLd+6dasNh8P2TW96k/V9f3C9T37ykxaw11xzzeCyhx9+2AL24YcfttZam8/nbV1dnT3++ONtV1fXsOfZdV/XX3+9He3/ggL2lltuGfx54cKFNhwO23Xr1g0u27Rpky0rK7NNTU17vD4XX3zxsOf66Ec/agOBgO3u7rbWWvuLX/zCAvaJJ54Y8flHU3xdLrnkElsoFAaXf/Ob37SA/a//+q/BZbfccosF7LZt2/a539Fei46ODgvYyZMn2507dw4u/9WvfmUBu2LFisFlF110kT311FNtOp0eXOb7vj333HNtQ0PDXp9/4cKFFtjj97Wr++67zwL261//+uAywF5//fXWWmvf8IY32KlTp9pkMmmtHfpd7Poa7/qaPProoxawX/7ylwcfP/744+2b3vSmvR6rtXbwOXZ17bXX2lgsNuz8R/odbNu2bY9/X0VjfQ2L5/b617/e5vP5fR7vE088YQH7ve99b4/HLrjgAgvYu++++xWf5zXXXGOPP/74wZ/3599N8TXaFWDD4bB98cUXB5etXr3aAvYb3/jG4LIFCxbYWCxmN27cOLhs7dq1NhgMjvreFhERETnSqZ2LiIiIyBHi4osvpqamhmOPPZa3v/3txONxfvGLXzBjxoxh61133XXDfv7Zz35GRUUF//AP/8D27dsHv84880zi8TgPP/wwAL///e/JZrN88IMfHNaW4SMf+cg+j+3pp5+mo6ODj3zkI3v0on4lLR4KhQIPPfQQCxcuZNasWYPLp02bxr/8y7/wpz/9id7e3mHbvO997xv2XOeffz6FQoGXXnoJGOqRff/995PL5cZ8LMXX5SMf+ciwHvDvfe97KS8v53/+53/2+/zG4p/+6Z+oqqoa/LlYzdze3g7Azp07+cMf/sA//uM/0tfXN/h73bFjB/Pnz2ft2rVs3Lhx1P339fUBUFZWNuo6xceK6+5uyZIlbNmyZcy90ZuamnjDG96wX21ginatXC+e7/nnn08ymeT555/fr30VvZLX8L3vfS+BQOAVPd+uIpEI73znO/dY/mrPc1//bvbm4osvpr6+fvDnxsZGysvLB7ctFAr8/ve/Z+HChUyfPn1wvRNOOIHLLrtsn/sXEREROVIpRBcRERE5Qtx555387ne/4+GHH+a5556jvb2d+fPnD1snGAzu0ad57dq19PT0MGXKFGpqaoZ9JRKJwUGMxbC5oaFh2PY1NTXDQrmRFFvLzJ0791WdY9G2bdtIJpPMnj17j8dOPvlkfN/n5ZdfHrb8uOOOG/Zz8ZiLfd8vuOAC3vrWt3LrrbdSXV3NW97yFr73ve+RyWT2eizF12X3YwmHw8yaNWvw8QNtX+fz4osvYq3l05/+9B6/12Jf8OLvdiT7Csh3fWzKlCkjPv5KQvH9Dd6L1qxZw5VXXklFRQXl5eXU1NRw9dVXA9DT07Nf+yp6Ja9hXV3dK3qu3c2YMYNwOLzH8ld7nvv6d7M/2xa3L267detWUqkUJ5xwwh7rjbRMREREZKJQT3QRERGRI8S8efM466yz9rpOJBIZVi0NbqjolClT+NGPfjTiNhOll/Fo1cHWWsBVxN977708/vjjrFixgt/+9re8613v4ktf+hKPP/448Xj8UB7uPu3rfHzfB+Cmm27a42JK0d6CzVNOOYVf/vKXtLa20tTUNOI6ra2tAMPuBtjdLbfcwoUXXsi3v/3tPe5CGElTUxMXXnghX/ziF3n/+9+/z/UBuru7ueCCCygvL+ezn/0s9fX1RKNRnnrqKT7+8Y8Pvhb765W8hvvby300I+3nQJznvv7dHKxtRURERCYyhegiIiIiE1x9fT2///3vOe+88/YaAB5//PGAq1zfNTTdtm3bPqtYiy0gnn32WS6++OJR1xtra5eamhpisRhtbW17PPb888/jeR7HHnvsmPa1u3POOYdzzjmHf//3f+eee+7hX//1X/nxj3/Me97znhHXL74ubW1tw16XbDZLR0fHXs93b15Jm5tdFY8lFAq9omNYsGABn//85/nv//7vEUP0QqHAPffcQ21t7aghO7gK/wsvvJAvfOELfOYznxnTcy9ZsmQweB+LRx55hB07dnDfffcNO5aOjo4xbT/aa/1qX8NX8px782rP82CbMmUK0WiUF198cY/HRlomIiIiMlGonYuIiIjIBPeP//iPFAoFPve5z+3xWD6fp7u7G3D9kEOhEN/4xjeGVZ5+9atf3edznHHGGdTV1fHVr351cH9Fu+6rtLQUYI91dhcIBLjkkkv41a9+xfr16weXd3Z2cs899/D617+e8vLyfR7Xrrq6uvaoqD399NMB9trS5eKLLyYcDvP1r3992Pb/+Z//SU9PD29605v26ziKxvpajGbKlCmDQfTmzZv3eHzbtm173f6cc87hkksu4Xvf+x7333//Ho9/6lOf4oUXXuDf/u3fCAb3XntTbNHyne98Z0zHvmvwnk6n97l+sUJ619c/m83yrW99a0zPF4vFgD1f61f7Gu7NK/n9vtrzPNgCgQAXX3wxv/zlL9m0adPg8hdffJHf/OY343hkIiIiIgeXKtFFREREJrgLLriAa6+9lttuu41nnnmGSy65hFAoxNq1a/nZz37G1772Nd72trdRU1PDTTfdxG233cYVV1zB5ZdfztNPP81vfvMbqqur9/ocnudx1113sWDBAk4//XTe+c53Mm3aNJ5//nnWrFnDb3/7WwDOPPNMAD70oQ8xf/58AoEAb3/720fc59KlS/nd737H61//ej7wgQ8QDAb59re/TSaT4Ytf/OJ+vw7f//73+da3vsWVV15JfX09fX19fPe736W8vJzLL7981O1qamr4xCc+wa233sqll17Km9/8Ztra2vjWt77Fa1/72sF+1ftrf16L0dx55528/vWv59RTT+W9730vs2bNorOzk8cee4y///3vrF69eq/b//d//zdvfOMbectb3sK//Mu/cP7555PJZLjvvvt45JFHuPrqq/noRz+6z+O44IILuOCCC3j00UfHfOy33HILb3jDG8a07rnnnktVVRXXXHMNH/rQhzDG8IMf/GDMbUZKSko45ZRT+MlPfsKJJ57IpEmTmDt3LnPnzn3Vr+Fo6uvrqays5O6776asrIzS0lLOPvvsvfZUf7XneSgsWbKEhx56iPPOO4/rrruOQqHAN7/5TebOncszzzwz3ocnIiIiclAoRBcRERE5Ctx9992ceeaZfPvb3+aTn/wkwWCQmTNncvXVV3PeeecNrrd06VKi0Sh33303Dz/8MGeffTYPPfTQmKqt58+fz8MPP8ytt97Kl770JXzfp76+nve+972D61x11VV88IMf5Mc//jE//OEPsdaOGhzPmTOHP/7xj3ziE5/gtttuw/d9zj77bH74wx9y9tln7/drcMEFF7Bq1Sp+/OMf09nZSUVFBfPmzeNHP/rRPodFLlmyhJqaGr75zW/y0Y9+lEmTJvG+972Pz3/+84RCof0+Fti/12I0p5xyCk8++SS33nory5cvZ8eOHUyZMoXXvOY1Y2qtUltby8qVK/nyl7/MT3/6U+69997ByvBPf/rTfPaznx3zsSxZsmTMoTjAhRdeOObgffLkydx///3ceOONLF68mKqqKq6++mouuuiiUXuZ7+4//uM/+OAHP8hHP/pRstkst9xyC3Pnzn3Vr+FoQqEQ3//+9/nEJz7B+9//fvL5PN/73vf2+m/tQJznwXbmmWfym9/8hptuuolPf/rTHHvssXz2s5/lb3/7G88///x4H56IiIjIQWHs4VTWICIiIiIi42rjxo2ce+655PN5HnvsMY477rjxPiQ5AixcuJA1a9awdu3a8T4UERERkQNOPdFFRERERGTQjBkzePDBB0mn01x22WX7HCorR59UKjXs57Vr1/LAAw9w4YUXjs8BiYiIiBxkqkQXERERERGRMZs2bRqLFi1i1qxZvPTSS9x1111kMhmefvppGhoaxvvwRERERA449UQXERERERGRMbv00kv5f//v/7FlyxYikQive93r+PznP68AXURERCYsVaKLiIiIiIiIiIiIiIxCPdFFREREREREREREREahEF1EREREREREREREZBRHXU903/fZtGkTZWVlGGPG+3BEREREREREREREZBxYa+nr62P69Ol43uj15kddiL5p0yaOPfbY8T4MERERERERERERETkMvPzyyxxzzDGjPn7UhehlZWWAe2HKy8vH+WhEREREREREREREZDz09vZy7LHHDmbGoznqQvRiC5fy8nKF6CIiIiIiIiIiIiJHuX21/dZgURERERERERERERGRUShEFxEREREREREREREZhUJ0EREREREREREREZFRKEQXERERERERERERERmFQnQRERERERERERERkVEoRBcRERERERERERERGYVCdBERERERERERERGRUShEFxEREREREREREREZhUJ0EREREREREREREZFRKEQXERERERERERERERmFQnQRERERERERERERkVEoRBcRERERERERERERGYVCdBERERERERERERGRUShEFxEREREREREREREZxbiG6Lfddhuvfe1rKSsrY8qUKSxcuJC2trZ9bvezn/2Mk046iWg0yqmnnsoDDzxwCI5WRERERERERERERI424xqiP/roo1x//fU8/vjj/O53vyOXy3HJJZfQ398/6jZ/+ctf+Od//mfe/e538/TTT7Nw4UIWLlzIs88+ewiPXERERERERERERESOBsZaa8f7IIq2bdvGlClTePTRR2lqahpxnX/6p3+iv7+f+++/f3DZOeecw+mnn87dd9+9z+fo7e2loqKCnp4eysvLD9ixi4iIiIiIiIiIiMiRY6xZ8WHVE72npweASZMmjbrOY489xsUXXzxs2fz583nsscdGXD+TydDb2zvsS0REREREREREROSoZi309cG2be774VNrfdgJjvcBFPm+z0c+8hHOO+885s6dO+p6W7Zsoba2dtiy2tpatmzZMuL6t912G7feeusBPVYRERERERERERGRI1IyCS0tsGIFtLVBoQCBAMyeDQsWQFMTxGLjfZSHlcMmRL/++ut59tln+dOf/nRA9/uJT3yCj33sY4M/9/b2cuyxxx7Q5xARERERERERERE57LW2wtKl0N4OxkBVFYTDkM/DqlWwciXMmgWLF0Nj43gf7WHjsAjRb7jhBu6//35aWlo45phj9rru1KlT6ezsHLass7OTqVOnjrh+JBIhEokcsGMVEREREREREREROeK0tkJzM3R2Ql2dC893VV0N2SysW+fWW7ZMQfqAce2Jbq3l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y559tuufbq0Lysf2QAcXHIMLuKuqhqvZYf/K99J2SnUUI7c3sqVMQ4O7rBR4z549HHiX2tS0tbnrwd3eWWe5YL63d/jykdusr3fB+6ZNrqJ9xYpDrwIvtdS59Va480740Y/ggQdg507YswceecR939bmDlr8y78MB+giIiIiIicqheiHQy7nPrXt3u0+oZVKhsZKJNz1u3e75Q9ja5d9+/bxi1/8gmuvvXa/6vBp06bxpje9if/+7//GDn6Su/nmm1mwYAGPPPII//zP/7zf9tra2njta1/LFVdcwaOPPsq73vWuoRD+QFpbW7nzzju56667uOuuu7jvvvtYtWrV0PV9fX186EMf4sEHH+RXv/oVnudx5ZVXEpU+CYqIiIg8B2PD35//HB57DG6/HS65xFVu9/e78HnRIvdW7M473fV/9mcucI8iGBhw/woF9zWfd0F1ebk7ybCubnSP8ZHhdk+Pq9YuK4Nzz3VfS4H3yJYyZ53l1pso8B6vTQ2421+wYDi07+kZrrafOdMF21u2uAr0Vaue34mPY1vqlPrAz58P553n+spPNIxVREREROREo3Yuh0NzsyvFaWw8+KcJz3PLbd7s1lu69LDswqZNm7DWcsYZZ4x7/RlnnEFnZyd79+4F4BWveAUf/vCHh67fsmXLqOW/9rWvMW/ePG666SYA5s2bx+OPP85nP/vZA+5HFEWsXr2aisFzed/85jfzq1/9ami917zmNaOW/9a3vkV9fT1PPPHEIfVjFxERERlPKfzNZNz3l17q3m5NNKi0qcmF6b/8JXz9664DXyn4jsXcdl72MnjXu1xwfMstozv3lfqf19S47Xue+/902oXZTz/t2r2A29bs2W75TZtcOD9nzvid/sZrU1O6vblzYds26OpylfannOLu07x5rhXM4sXqQy4iIiIicjgpRH++rHVNI42ZuAJ9rETCLb9mDSxZMrlpT5PenXHOQR7Heeedd8DrN27cyMte9rJRly1atOig2501a9ZQgA4wffp09uzZM/T9pk2b+OQnP8natWtpb28fqkDfunWrQnQRERE5IsYG62Ol03DFFXD55a7Ce+9ed8JgebmrPB853HRsz/D+flcJ/vKXu4p3gHvucdfFYnDmmcO3m826r6Vq+IMF3uP1KC/d3iWXwKtfDS99qdu3sQcHRERERETk8FGI/nxls+4TTU3Noa1XU+PW6+ub+BPdITjttNMwxvDkk09y5ZVX7nf9k08+SU1NDfX19QCUjzwP+TCKx+OjvjfGjGrVsmzZMk499VT+/d//nRkzZhBFEfPnz6dQOk9ZRERE5Cgxxg3GPNBwzFLbmCVLJq5uv/zy/a+DiZc/kMncnoiIiIiIHFnqZPh85fPunNzYIR6PKJ37299/WHajtraWSy65hK985Sv0j9nmrl27+K//+i9e//rXYyb5aWvevHk8+OCDoy774x//+Lz2saOjg40bN7JixQr+6q/+aqjFjIiIiMjxZmzP8JFvsca77kDLP9/bExERERGRI0sh+vOVSrlzaoPg0NYLArfemCGgz8ett97KwMAAS5Ysobm5mW3btnH33XdzySWXMHPmzIP2Mx/pXe96F0899RQf+9jHePrpp7njjjtYvXo1wKSD+LFqamqora3l61//Os888wy//vWv+dCHPvSctiUiIiIiIiIiIiLyQlCI/nxlMm6K06FWVHd2uvUOY1uVuXPn8uCDDzJ79mxe97rXMWfOHN75zndy8cUXc//99zNlypRJb6uxsZEf/vCH/PjHP6apqYmvfvWrfOITnwAgmUw+p/3zPI/vf//7PPTQQ8yfP58PfvCDQ4NLRURERERERERERI5Fxk52EuUJoqenh6qqKrq7u6msrBx1XT6fp62tjcbGRlKp1OQ3evfdsGIFzJo1ueGihQJs2QIrV7oml8eJz372s9x2221s27btaO/KpDznn6eIiIiIiIiIiIic8A6UFY+kwaKHw+LFMHs2tLbC3LngHaDAP4qgrQ3mzHHrHcO+8pWv8LKXvYza2lp+//vfc9NNN/G+973vaO+WiIiIiIiIiIiIyAtG7VwOh3TaVaJPnQqbNrlK8/EUCu76qVPd8un0C7ufh2jTpk1cfvnlnHnmmXzmM5/hwx/+MDfccMPR3i0RERERERERERGRF4zauYzwvNt/tLS4Fi2bN4MxUFMDsZgbItrZCda6ivUVK6Cp6TDdI5mI2rmIiIiIiIiIiIjIRNTO5WhoaoLVq6G5GdasgY0bob8ffB8WLYJly1wLl2O8Al1EREREREREREREHIXoh1s67YaFLlkCfX0uRC8rg/JyV50uIiIiIiIiIiIiIscNhejjOCwdboyBTMb9k6PiRdapSERERERERERERI4ADRYdIR6PA5DL5Y7ynsjhUPo5ln6uIiIiIiIiIiIiIodKlegj+L5PdXU1e/bsASCdTmPUguW4Y60ll8uxZ88eqqur8X3/aO+SiIiIiIiIiIiIHKcUoo8xbdo0gKEgXY5f1dXVQz9PERERERERERERkedCIfoYxhimT59OQ0MDxWLxaO+OPEfxeFwV6CIiIiIiIiIiIvK8KUSfgO/7CmFFREREREREREREXuQ0WFREREREREREREREZAIK0UVEREREREREREREJqAQXURERERERERERERkAi+6nujWWgB6enqO8p6IiIiIiIiIiIiIyNFSyohLmfFEXnQhem9vLwAnn3zyUd4TERERERERERERETnaent7qaqqmvB6Yw8Ws59goihix44dVFRUYIw52rvznPT09HDyySezbds2Kisrj/buyIucno9yLNHzUY4Vei7KsUTPRzmW6Pkoxwo9F+VYouejHCtejM9Fay29vb3MmDEDz5u48/mLrhLd8zxOOumko70bh0VlZeWL5gktxz49H+VYouejHCv0XJRjiZ6PcizR81GOFXouyrFEz0c5VrzYnosHqkAv0WBREREREREREREREZEJKEQXEREREREREREREZmAQvTjUDKZ5FOf+hTJZPJo74qIno9yTNHzUY4Vei7KsUTPRzmW6Pkoxwo9F+VYouejHCv0XJzYi26wqIiIiIiIiIiIiIjIZKkSXURERERERERERERkAgrRRUREREREREREREQmoBBdRERERERERERERGQCCtGPUZ/97Gf5sz/7M9LpNNXV1ZNax1rLJz/5SaZPn05ZWRmvfOUr2bRp06hl9u3bx5ve9CYqKyuprq7m7W9/O9ls9gjcAzlRHOpzZsuWLRhjxv33gx/8YGi58a7//ve//0LcJTmOPZfXsJe//OX7Pdfe/e53j1pm69atXHbZZaTTaRoaGvjIRz5CEARH8q7ICeBQn4/79u3jH//xH5k3bx5lZWWccsop/NM//RPd3d2jltProxzMl7/8ZWbNmkUqleL8889n3bp1B1z+Bz/4AaeffjqpVIqzzz6bn//856Oun8x7SJGJHMrz8d///d/5y7/8S2pqaqipqeGVr3zlfstfc801+70GLl269EjfDTlBHMrzcfXq1fs911Kp1Khl9Pooz9WhPBfH+7xijOGyyy4bWkavjfJcNTc3s2zZMmbMmIExhjvvvPOg69x7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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "np.random.seed(27)\n",
+ "vqr = VQRegressor_linear_ansatz(layers=nlayers, \n",
+ " ndata=ndata_points,\n",
+ " function=f, xmax=x_max_fit, \n",
+ " xmin=x_min_fit)\n",
+ "# With the selected configuration, the training usually takes around 30'\n",
+ "vqr.train_with_psr(\n",
+ " epochs=nepochs,\n",
+ " learning_rate=learning_rate,\n",
+ " batches=nbatches,\n",
+ " J_treshold=loss_treshold,\n",
+ ")\n",
+ "vqr.show_predictions(\"Predictions of the QNN after training\", False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9cf7754b",
+ "metadata": {},
+ "source": [
+ "### 3. Obtaining the Fourier series from the QNN"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "08459223",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# We import the qibo function that obtains the Fourier series out of a \n",
+ "# quantum circuit that returns an expectation value\n",
+ "from qibo.models.utils import fourier_coefficients\n",
+ "# We get the optimized quantum circuit and the normalized Fourier coefficients\n",
+ "quantum_circuit_optimized = vqr.one_prediction\n",
+ "normalized_coeffs = fourier_coefficients(quantum_circuit_optimized,\n",
+ " n_inputs=1, degree=nlayers, lowpass_filter=True)\n",
+ "# We obtain the non-normalized coeffs\n",
+ "coeffs = normalized_coeffs * vqr.norm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "283f5918",
+ "metadata": {},
+ "source": [
+ "Let's see how they look like"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "9b2e692f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[ 9.91228857e-01+0.j -9.14152432e-04-0.04336848j\n",
+ " 3.40828521e-01-0.02305663j -1.77033953e-01+0.00110917j\n",
+ " -3.63049787e-01+0.12160816j 7.33018369e-02-0.05831041j\n",
+ " 2.28576955e-01-0.1387953j -2.13031210e-03+0.16034618j\n",
+ " -1.57394048e-01-0.03133756j 5.23539080e-02-0.02270211j\n",
+ " 5.30676569e-03-0.00145169j 5.30676569e-03+0.00145169j\n",
+ " 5.23539080e-02+0.02270211j -1.57394048e-01+0.03133756j\n",
+ " -2.13031210e-03-0.16034618j 2.28576955e-01+0.1387953j\n",
+ " 7.33018369e-02+0.05831041j -3.63049787e-01-0.12160816j\n",
+ " -1.77033953e-01-0.00110917j 3.40828521e-01+0.02305663j\n",
+ " -9.14152432e-04+0.04336848j]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(coeffs)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eb17b487",
+ "metadata": {},
+ "source": [
+ "Now we check that Fourier series obtained it's correct and also fits the function "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "901478f7",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB9+klEQVR4nO3dd3hTZRvH8W+S7k1LSymUMsqS1bKHbJCtDAVFBRwoihMnrwpuxYETQZHhYCtL9t67UPYsLbTQsgrdM3nePw4NVlYLbU+b3p/rykVyzpPkd0hLbs55hkEppRBCCCGEsBFGvQMIIYQQQhQkKW6EEEIIYVOkuBFCCCGETZHiRgghhBA2RYobIYQQQtgUKW6EEEIIYVOkuBFCCCGETbHTO0BRs1gsnD17Fnd3dwwGg95xhBBCCJEHSimSkpIICAjAaLz1uZlSV9ycPXuWwMBAvWMIIYQQ4g5ER0dTsWLFW7YpdcWNu7s7oP3leHh46JxGCCGEEHmRmJhIYGCg9Xv8VkpdcZNzKcrDw0OKGyGEEKKEyUuXEulQLIQQQgibIsWNEEIIIWyKFDdCCCGEsCmlrs+NEMWN2WwmKytL7xhC3JSDg8Nth94KUZxIcSOETpRSxMXFceXKFb2jCHFLRqORKlWq4ODgoHcUIfJEihshdJJT2Pj5+eHi4iKTSopiKWfi09jYWCpVqiQ/p6JE0LW4+eyzz5g7dy5HjhzB2dmZli1bMmbMGGrWrHnL582ZM4f33nuPqKgoqlevzpgxY+jevXsRpRbi7pnNZmth4+Pjo3ccIW7J19eXs2fPkp2djb29vd5xhLgtXS+irl+/nuHDh7Nt2zZWrlxJVlYW9913HykpKTd9zpYtW3jkkUd46qmn2LNnD71796Z3794cOHCgCJMLcXdy+ti4uLjonESI28u5HGU2m3VOIkTeGJRSSu8QOS5cuICfnx/r16+nTZs2N2wzYMAAUlJSWLRokXVb8+bNCQkJYcKECbd9j8TERDw9PUlISJBJ/IRu0tPTiYyMpEqVKjg5OekdR4hbkp9XURzk5/u7WHV/T0hIAMDb2/umbbZu3UqnTp1ybevSpQtbt269YfuMjAwSExNz3YQQQghhu4pNcWOxWHjllVdo1aoVdevWvWm7uLg4ypUrl2tbuXLliIuLu2H7zz77DE9PT+tNFs0UQgghbFuxKW6GDx/OgQMHmDlzZoG+7siRI0lISLDeoqOjC/T1hRBCCFG8FIvi5oUXXmDRokWsXbv2tsuY+/v7c+7cuVzbzp07h7+//w3bOzo6WhfJlMUyhbg7BoPhlrf3339f12zz58/X7f2FEJrtJy+Rka1v53NdixulFC+88ALz5s1jzZo1VKlS5bbPadGiBatXr861beXKlbRo0aKwYgohroqNjbXevv32Wzw8PHJte/311/P1epmZmYWUVAihh9iENAb8so1mn64mKV2/mdd1LW6GDx/On3/+yfTp03F3dycuLo64uDjS0tKsbQYNGsTIkSOtj19++WWWLVvG119/zZEjR3j//ffZtWsXL7zwgh6HIESBUUqRmpmtyy2vgyb9/f2tN09PTwwGg/VxSkoKjz76KOXKlcPNzY0mTZqwatWqXM+vXLkyH330EYMGDcLDw4NnnnkGgIkTJxIYGIiLiwt9+vRh7NixeHl55XruggULaNiwIU5OTlStWpUPPviA7Oxs6+sC9OnTB4PBYH0shChai/fFAlDDzx13J/3mRNJ1Er/x48cD0K5du1zbp0yZwpAhQwA4ffp0rjVNWrZsyfTp03n33Xf53//+R/Xq1Zk/f/4tOyELURKkZZm5Z9RyXd770IddcHG4u38OkpOT6d69O5988gmOjo78/vvv9OrVi6NHj1KpUiVru6+++opRo0YxevRoADZv3sywYcMYM2YM999/P6tWreK9997L9dobN25k0KBBfP/997Ru3ZqIiAhrYTR69Gh27tyJn58fU6ZMoWvXrphMprs6FiHEnfln71kAejUor2sOXYubvPxvcd26dddte+ihh3jooYcKIZEQ4k41aNCABg0aWB9/9NFHzJs3j4ULF+Y6s9qhQwdee+016+N33nmHbt26WS9p1ahRgy1btuSay+qDDz7g7bffZvDgwQBUrVqVjz76iDfffJPRo0fj6+sLgJeX10373wkhClfUxRT2xiRgNEC3eqW4uBFCXONsb+LQh110e++7lZyczPvvv8/ixYuJjY0lOzubtLQ0Tp8+natd48aNcz0+evQoffr0ybWtadOmuYqbvXv3snnzZj755BPrNrPZTHp6OqmpqTLTsxDFwKJ92lmbVsFlKevmqGsWKW6EKCYMBsNdXxrS0+uvv87KlSv56quvCA4OxtnZmQcffPC6TsOurq75fu3k5GQ++OAD+vbte90+mTFXCP0ppVgQfvWSVP0AndNIcSOEKCCbN29myJAh1rMwycnJREVF3fZ5NWvWZOfOnbm2/fdxw4YNOXr0KMHBwTd9HXt7e1n7SAidHIlL4vj5ZBxMRrrU1f/SsBQ3QogCUb16debOnUuvXr0wGAy89957WCyW2z7vxRdfpE2bNowdO5ZevXqxZs0ali5disFgsLYZNWoUPXv2pFKlSjz44IMYjUb27t3LgQMH+PjjjwFtxNTq1atp1aoVjo6OlClTptCOVQiRW85Zm/a1fPF01n/l+GIxiZ9NUArWfQ6bvtU7iRC6GDt2LGXKlKFly5b06tWLLl260LBhw9s+r1WrVkyYMIGxY8fSoEEDli1bxquvvprrclOXLl1YtGgRK1asoEmTJjRv3pxvvvmGoKAga5uvv/6alStXEhgYSGhoaKEcoxDiehaLso6SeiCkgs5pNMVqVfCiUGirgp9YDX9e7Q/w0G9Qp3fBvbawObLK8q0NHTqUI0eOsHHjRr2jCOTnVdzazqh4HpqwFTdHO3a92wmnAhigcCMldlXwEi24IzQbpt2f9yycCdM3jxAlyFdffcXevXs5ceIEP/zwA7/99pt12LcQonhbEH4GgC51/AutsMkvKW4KSJbZwpqgl4nxbQPZ6TDvOcjO0DuWECXCjh076Ny5M/Xq1WPChAl8//33PP3003rHEkLcRpbZYp2V+IEQ/UdJ5ZAOxQXkwJkEnvx9D+UdBrPF7TCGi0dhw1fQ4R29owlR7M2ePVvvCEKIO7Dp+EUup2ZR1s2BltV89I5jJWduCkhIoBeVvF2IzXRmZ53/aRs3jYW4A/oGE0IIIQpJziWpnvUDsDMVn5Ki+CQp4QwGg/WU3ITzdaFWT7Bkw4LhYM7WOZ0QQghRsFIzs1lx6BwA9xejS1IgxU2ByiluNhy/yOX2n4GjJ8SGw7Zx+gYTQgghCtiqw+dJzTQT6O1MaKCX3nFykeKmAAX7uVMnwINsi2JRpIIuV9fBWfspXIrQN5wQQghRgBZevST1QIMKuSbdLA6kuClgva9OYLQw/AyEPgZV22mjp1a9r2suIYQQoqBcTslk3dELQPEaJZVDipsC1qtBAAYD7Iy6TMyVNOj6OWCAwwvhbLje8YQQQoi7tvRAHNkWRe3yHlQv5653nOtIcVPA/D2daF5FGw63cO9Z8KsN9R7Sdq75WMdkQghbExUVhcFgIDw8XO8oopTJGSV1f4Pid9YGpLgpFDmn6BZeXUiMdm+DwQQnVsLpbTomE6JgREdH8+STTxIQEICDgwNBQUG8/PLLXLp0SZc87dq145VXXtHlvQHef/99DAbDdbdVq1YV6vsGBgYSGxtL3bp1C/V9hPi3s1fS2BEVD0CvBuWvb5CHBXMLmxQ3haBb3fI4mIwciUviSFwi+FSDho9rO9d+qm84Ie7SyZMnady4McePH2fGjBmcOHGCCRMmsHr1alq0aEF8fLzeEXVRp04dYmNjc93atGlTaO+XmZmJyWTC398fO7s7n481MzOzAFOJ0mDRvrMoBU0ql6FiGZfcO7MzYXp/2DZen3BXSXFTCDxd7GlX0xe4tgw8rV/Xzt5ErofYvTqmE+LuDB8+HAcHB1asWEHbtm2pVKkS3bp1Y9WqVZw5c4Z33rk2K7fBYGD+/Pm5nu/l5cXUqVOtj9966y1q1KiBi4sLVatW5b333iMrK8u6//333yckJIQ//viDypUr4+npycMPP0xSUhIAQ4YMYf369Xz33XfWMyZRUVFMnToVLy+vXO89f/78XKM6cl578uTJVKpUCTc3N55//nnMZjNffPEF/v7++Pn58cknn9z278XOzg5/f/9cNwcHBwD2799Phw4dcHZ2xsfHh2eeeYbk5GTrc2905ql3794MGTLE+rhy5cp89NFHDBo0CA8PD5555pkbXpY6cOAA3bp1w83NjXLlyvH4449z8eLFXO/1wgsv8Morr1C2bFm6dOly22MT4t9yvtfu/+8K4BYzzHtGu0qx+kNIOKNDOo0UN4Wkd2jOqKmzWCwKvAKh7tVVw7f8qGMyIe5cfHw8y5cv5/nnn8fZ2TnXPn9/fx599FFmzZqFUirPr+nu7s7UqVM5dOgQ3333HRMnTuSbb77J1SYiIoL58+ezaNEiFi1axPr16/n8888B+O6772jRogVDhw61njEJDAzM8/tHRESwdOlSli1bxowZM5g0aRI9evQgJiaG9evXM2bMGN599122b9+e59f8t5SUFLp06UKZMmXYuXMnc+bMYdWqVbzwwgv5fq2vvvqKBg0asGfPHt57773r9l+5coUOHToQGhrKrl27WLZsGefOnaN///652v322284ODiwefNmJkyYcEfHJUqnE+eTOHg2ETujgR71/nNJasW7cHAe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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# We import some functions to obtain the Fourier series from the coefficients and to plot these results\n",
+ "from utils_qfiae import fourier_series, plot_data_fourier\n",
+ "# We plot the original function and the Fourier approximation\n",
+ "y_fourier = fourier_series(coeffs, vqr.features)\n",
+ "plot_fourier, accuracy_qnn = plot_data_fourier(vqr.labels * vqr.norm, vqr.features, y_fourier)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "98ffb093",
+ "metadata": {},
+ "source": [
+ "### 4. Integrating every trigonometric piece"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ec763dbb",
+ "metadata": {},
+ "source": [
+ "First we should rewrite the Fourier coefficients in a way that will be easy to integrate the trigonometric functions Sine and Cosine"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "a4a5552e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# We import some functions to convert exponential-like Fourier coefficients into trigonometric-like and to\n",
+ "# convert this trigonometric coeffs into an array that provides insight about if we get a Sine or a Cosine\n",
+ "from utils_qfiae import exp_fourier_to_trig, trig_to_final_array\n",
+ "trig_coeffs = exp_fourier_to_trig(coeffs)\n",
+ "fourier_array = trig_to_final_array(trig_coeffs)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ba1288a5",
+ "metadata": {},
+ "source": [
+ "Let's visualize what we have and note that in the first columnn 0 stands for Cosine and 1 stands for Sine, in the second column we have the frequencies and in the third one the coefficients"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "39669ec5",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[ 0.00000000e+00 0.00000000e+00 9.91228857e-01]\n",
+ " [ 0.00000000e+00 1.00000000e+00 -1.82830486e-03]\n",
+ " [ 0.00000000e+00 2.00000000e+00 6.81657041e-01]\n",
+ " [ 0.00000000e+00 3.00000000e+00 -3.54067907e-01]\n",
+ " [ 0.00000000e+00 4.00000000e+00 -7.26099574e-01]\n",
+ " [ 0.00000000e+00 5.00000000e+00 1.46603674e-01]\n",
+ " [ 0.00000000e+00 6.00000000e+00 4.57153911e-01]\n",
+ " [ 0.00000000e+00 7.00000000e+00 -4.26062419e-03]\n",
+ " [ 0.00000000e+00 8.00000000e+00 -3.14788097e-01]\n",
+ " [ 0.00000000e+00 9.00000000e+00 1.04707816e-01]\n",
+ " [ 0.00000000e+00 1.00000000e+01 1.06135314e-02]\n",
+ " [ 1.00000000e+00 1.00000000e+00 8.67369616e-02]\n",
+ " [ 1.00000000e+00 2.00000000e+00 4.61132575e-02]\n",
+ " [ 1.00000000e+00 3.00000000e+00 -2.21834947e-03]\n",
+ " [ 1.00000000e+00 4.00000000e+00 -2.43216317e-01]\n",
+ " [ 1.00000000e+00 5.00000000e+00 1.16620823e-01]\n",
+ " [ 1.00000000e+00 6.00000000e+00 2.77590597e-01]\n",
+ " [ 1.00000000e+00 7.00000000e+00 -3.20692363e-01]\n",
+ " [ 1.00000000e+00 8.00000000e+00 6.26751169e-02]\n",
+ " [ 1.00000000e+00 9.00000000e+00 4.54042108e-02]\n",
+ " [ 1.00000000e+00 1.00000000e+01 2.90337531e-03]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(fourier_array)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a33e89dc",
+ "metadata": {},
+ "source": [
+ "Before implementeing the IQAE model, we should define the quantum circuits $\\mathcal{A}$ and $\\mathcal{Q}$.\n",
+ "\n",
+ "The circuit $\\mathcal{A}$ is defined as:\n",
+ "$$|\\psi\\rangle=\\mathcal{A}|0\\rangle_{n+1}=\\sqrt{a}|\\tilde{\\psi}_1\\rangle|1\\rangle +\\sqrt{1-a}|\\tilde{\\psi}_0\\rangle|0\\rangle,$$\n",
+ "where $a\\in[0,1]$ is the parameter to be estimated, while $|\\tilde{\\psi}_1\\rangle$ and $|\\tilde{\\psi}_0\\rangle$ are the $n$-qubit good and bad states, respectively.\n",
+ "\n",
+ "From a more practical point of view, this circuit can be decomposed in two pieces:\n",
+ "$$\\mathcal{A}=\\left(\\mathcal{P}\\otimes \\mathcal{I}_1\\right)\\mathcal{R},$$\n",
+ "where $\\mathcal{I}_1$ is the identity matrix acting over the last qubit.\n",
+ "\n",
+ "So, to get $\\mathcal{A}$ we have to define the operator $\\mathcal{P}$, which encodes the probability distribution $p(x)$, and the operator $\\mathcal{R}$, which encodes the function we are going to integrate, $\\sin(x)^2$, into the interval $[b_{min},b_{max}]$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "d2c41980",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def operator_p(circ, list_of_qubits):\n",
+ " \"\"\"Generating the uniform probability distribution p(x)=1/2^n\"\"\"\n",
+ " \n",
+ " for qubit in list_of_qubits:\n",
+ " circ.add(gates.H(q=qubit))\n",
+ "\n",
+ "def operator_r(circ, list_of_qubits, qubit_to_measure, b_max, b_min):\n",
+ " \"\"\"Encoding the function Sin(x)^2 to be integrated in the interval [b_min,b_max]\"\"\"\n",
+ " \n",
+ " number_of_qubits = qubit_to_measure\n",
+ " circ.add(\n",
+ " gates.RY(q=qubit_to_measure, theta=(b_max - b_min) / 2 ** number_of_qubits + 2 * b_min)\n",
+ " )\n",
+ " for qubit in list_of_qubits:\n",
+ " circ.add(\n",
+ " gates.CRY(qubit, qubit_to_measure, \n",
+ " (b_max - b_min) / 2 ** (number_of_qubits-qubit-1))\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "751f9ece",
+ "metadata": {},
+ "source": [
+ "Then, the circuit $\\mathcal{A}$ will be:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "54c7d640",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def create_circuit_a(n_qubits, b_max, b_min):\n",
+ " \"\"\"Creation of circuit A\"\"\"\n",
+ " \n",
+ " circuit_a = Circuit(n_qubits + 1)\n",
+ " list_of_qubits = list(range(n_qubits))\n",
+ " operator_p(circuit_a, list_of_qubits)\n",
+ " operator_r(circuit_a, list_of_qubits, n_qubits, b_max, b_min)\n",
+ " return circuit_a"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "09adce54",
+ "metadata": {},
+ "source": [
+ "Let's see how the circuit A looks like for $n_{qubits}$=4"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "44e529ee",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "q0: ─H──o───────────\n",
+ "q1: ─H──|──o────────\n",
+ "q2: ─H──|──|──o─────\n",
+ "q3: ─H──|──|──|──o──\n",
+ "q4: ─RY─RY─RY─RY─RY─\n"
+ ]
+ }
+ ],
+ "source": [
+ "circuit_a_prob = create_circuit_a(n_qubits = 4,b_max = x_max_int, b_min = x_min_int)\n",
+ "print(circuit_a_prob.draw())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "217d3238",
+ "metadata": {},
+ "source": [
+ "On the other hand, the circuit $\\mathcal{Q}$ is defined as:\n",
+ "$$ \\mathcal{Q}=-\\mathcal{A} S_0 \\mathcal{A}^{-1} S_\\chi,$$\n",
+ "where the operator $S_0$ tags with a negative sign the $|0\\rangle_{n+1}$ state and does nothing to the other states, and the operator $S_\\chi$ changes the sign to the good state, i.e. $S_\\chi |\\tilde{\\psi}_1\\rangle|1\\rangle=-|\\tilde{\\psi}_1\\rangle|1\\rangle$.\n",
+ "\n",
+ "So, let's define $S_0$ and $S_\\chi$ to get $\\mathcal{Q}$:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "bdaec568",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def operator_s_0(circ, qubit_to_measure):\n",
+ " \"\"\"Tagging with a negative sign any state different to |0>_{n+1}. Note that this is \n",
+ " the opposite of what S_0 is supposed to do. The explanation is that the operator\n",
+ " here defined has absorbed the minus sign in \\mathcal{Q}.\"\"\"\n",
+ " \n",
+ " circ.add(gates.Z(q = qubit_to_measure))\n",
+ " \n",
+ "def operator_s_chi(circ, qubit_to_measure):\n",
+ " \"\"\"Computing reflection operator (I - 2|0><0|)\"\"\"\n",
+ " \n",
+ " number_of_qubits = qubit_to_measure\n",
+ " list_of_qubits = list(range(number_of_qubits))\n",
+ " for qubit in list_of_qubits:\n",
+ " circ.add(gates.X(q = qubit))\n",
+ " circ.add(gates.X(q=qubit_to_measure))\n",
+ " circ.add(gates.Z(qubit_to_measure).controlled_by(*range(0,qubit_to_measure)))\n",
+ " circ.add(gates.X(q=qubit_to_measure))\n",
+ " for qubit in list_of_qubits:\n",
+ " circ.add(gates.X(q = qubit))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "060f4a2d",
+ "metadata": {},
+ "source": [
+ "Then the circuit $\\mathcal{Q}$ will be:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "4367a593",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def create_circuit_q(n_qubits, circuit_a):\n",
+ " \"\"\"Creation of circuit Q\"\"\"\n",
+ " \n",
+ " circuit_q = Circuit(n_qubits + 1)\n",
+ " operator_s_0(circuit_q, n_qubits)\n",
+ " circuit_q.add(circuit_a.invert().on_qubits(\n",
+ " *range(0, n_qubits + 1, 1)))\n",
+ " operator_s_chi(circuit_q, n_qubits)\n",
+ " circuit_q.add(circuit_a.on_qubits(\n",
+ " *range(0, n_qubits + 1, 1)))\n",
+ " return circuit_q"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9c426c79",
+ "metadata": {},
+ "source": [
+ "Let's see how the circuit Q looks like for $n_{qubits}$=4\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "3d35db51",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "q0: ────────────o──H──X─o─X─H──o───────────\n",
+ "q1: ─────────o──|──H──X─o─X─H──|──o────────\n",
+ "q2: ──────o──|──|──H──X─o─X─H──|──|──o─────\n",
+ "q3: ───o──|──|──|──H──X─o─X─H──|──|──|──o──\n",
+ "q4: ─Z─RY─RY─RY─RY─RY─X─Z─X─RY─RY─RY─RY─RY─\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(create_circuit_q(n_qubits=4,circuit_a=circuit_a_prob).draw())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "476038e0",
+ "metadata": {},
+ "source": [
+ "Since we already have the circuits $\\mathcal{A}$ and $\\mathcal{Q}$, we can proceed to execute the `IQAE` model.\n",
+ "\n",
+ "But first let's do the last transformation of the Fourier coefficients"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "231c46c0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from utils_qfiae import coeffs_array_to_class\n",
+ "coeffs_class = coeffs_array_to_class(fourier_array)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d0055524",
+ "metadata": {},
+ "source": [
+ "Then, we will create the following function that will integrate each sine and cosine and then sum every term to obtain the final Monte Carlo integral.\n",
+ "\n",
+ "To create this integrator we will use the following mathematical properties:\n",
+ "\n",
+ "\\begin{equation}\n",
+ "\\frac{1}{x_{min}-x_{min}}\\int_{x_{min}}^{x_{max}}dx\\sin(x \\omega/2)^2=\\frac{1}{b_{max}-b_{min}} \\int_{b_{min}}^{b_{max}}dx\\sin(x)^2,\n",
+ "\\tag{1}\n",
+ "\\end{equation}\n",
+ "\n",
+ "if $b_{max}=x_{max}\\omega / 2$ and $b_{min}=x_{min} \\omega / 2$.\n",
+ "\n",
+ "\\begin{equation}\n",
+ "\\sin(x) = \\cos (x - \\pi/2)\n",
+ "\\tag{2}\n",
+ "\\end{equation}\n",
+ "\n",
+ "\\begin{equation}\n",
+ "\\cos(x) = 1 - 2 \\sin(x/2) ^ 2\n",
+ "\\tag{3}\n",
+ "\\end{equation}\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "0086cdde",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def qfiae_integrator(coeffs_f, x_max, x_min, nbit, shots, alpha, epsilon, method):\n",
+ " \"\"\"Takes the Fourier coefficients of the desired function and calculates the integral of the function\n",
+ " in the specified interval.\"\"\"\n",
+ " \n",
+ " mc_integral = 0\n",
+ " error_iqae = 0\n",
+ " if coeffs_f[0].angle == 0:\n",
+ " mc_integral += coeffs_f[0].coeff * (x_max - x_min)\n",
+ " for n in range(1,len(coeffs_f)):\n",
+ " angle = coeffs_f[n].angle\n",
+ " if coeffs_f[n].function == 0: # cosine\n",
+ " # Here we use Eq.(1)\n",
+ " a_circuit = create_circuit_a(n_qubits=nbit,\n",
+ " b_max=x_max * angle / 2,\n",
+ " b_min=x_min * angle / 2)\n",
+ " \n",
+ " if coeffs_f[n].function == 1: # sine\n",
+ " # Here we use Eq.(2)\n",
+ " a_circuit = create_circuit_a(n_qubits=nbit,\n",
+ " b_max=x_max * angle / 2 - np.pi / 4,\n",
+ " b_min=x_min * angle / 2 - np.pi / 4)\n",
+ " \n",
+ " q_circuit = create_circuit_q(n_qubits=nbit, circuit_a=a_circuit)\n",
+ " result = IQAE(a_circuit, q_circuit, alpha, epsilon, n_shots=shots, method=method).execute()\n",
+ " a_estimated = result.estimation\n",
+ " # This `a_estimated` corresponds to the integral of the right part of Eq.(1)\n",
+ " error = result.epsilon_estimated\n",
+ " # Here we use Eq.(3) and multiply the result by (x_max - x_min) to get rid of the normalization\n",
+ " mc_integral += (1 - 2 * a_estimated) * coeffs_f[n].coeff * (x_max - x_min)\n",
+ " error_iqae += (2 * abs(coeffs_f[n].coeff) * error * (x_max - x_min))**2\n",
+ "\n",
+ " return mc_integral , np.sqrt(error_iqae)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b1e07b95",
+ "metadata": {},
+ "source": [
+ "Finally, we define the parameters for the IQAE model and execute the qfiae_integrator function"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "05191653",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "nqubits = 4\n",
+ "nshots = 1024\n",
+ "alpha = 0.05\n",
+ "epsilon = 0.005\n",
+ "method_iqae = \"chernoff\"\n",
+ "qfiae_integral, iae_error = qfiae_integrator(coeffs_class, x_max=x_max_int, x_min=x_min_int, \n",
+ " nbit=nqubits, shots=nshots, alpha=alpha, \n",
+ " epsilon=epsilon, method=method_iqae)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1ed6f9e5",
+ "metadata": {},
+ "source": [
+ "Once the QFIAE algorithm has been performed we can visualize and compare the results with the ones obtained by classical calculation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "e65a5701",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The result obtained with QFIAE is 1.3368772283309165 ± 0.00781780414187758\n",
+ "The result obtained with classical numerical integration is 1.3333333333333333 ± 1.4802973661668752e-14\n"
+ ]
+ }
+ ],
+ "source": [
+ "from scipy.integrate import quad\n",
+ "classical_integral, classical_error = quad(f, x_min_int, x_max_int)\n",
+ "print(\"The result obtained with QFIAE is\", qfiae_integral, \n",
+ " \" ± \", iae_error)\n",
+ "print(\"The result obtained with classical numerical integration is\", \n",
+ " classical_integral, \" ± \", classical_error)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "tf2",
+ "language": "python",
+ "name": "tf2"
+ },
+ "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.16"
+ },
+ "widgets": {
+ "application/vnd.jupyter.widget-state+json": {
+ "state": {},
+ "version_major": 2,
+ "version_minor": 0
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/qfiae/utils_qfiae.py b/examples/qfiae/utils_qfiae.py
new file mode 100644
index 000000000..717ed0a13
--- /dev/null
+++ b/examples/qfiae/utils_qfiae.py
@@ -0,0 +1,103 @@
+import matplotlib.pyplot as plt
+import numpy as np
+
+from qibo import Circuit, gates
+from qibo.models.iqae import IQAE
+
+
+def fourier_series(coeffs, x_val, period=2 * np.pi):
+ """Compute the Fourier series for a given set of coefficients in exponencial form."""
+
+ xval_len = len(x_val)
+ x_vals = x_val.reshape(len(x_val))
+ n = 0
+ series = coeffs[0] * np.exp(1j * n * x_vals * (2 * np.pi / period))
+ for i in range(1, int((len(coeffs)) / 2) + 1):
+ n += 1
+ serie_i = coeffs[i] * np.exp(1j * n * x_vals * (2 * np.pi / period))
+ series += serie_i + np.conjugate(serie_i)
+ y_vals = np.real(series)
+ return y_vals
+
+
+def plot_data_fourier(function_test, xtest, y_fourier):
+ """Plot the Fourier representation alongside the function test and provides the efficiency of the fit."""
+
+ SSE = 0
+ SST = 0
+ average = np.sum(function_test) / function_test.size
+ N = len(function_test)
+
+ for i in range(N):
+ SSE += (function_test[i] - y_fourier[i]) ** 2
+ SST += (function_test[i] - average) ** 2
+
+ R = 1 - SSE / SST
+ R = round(float(R) * 100, 1)
+
+ sort_indices = np.argsort(xtest)
+ plt.plot(xtest[sort_indices], function_test[sort_indices], label="Target")
+ plt.plot(
+ xtest[sort_indices],
+ y_fourier[sort_indices],
+ label=f"Quantum Fourier \nAccuracy: {R}%",
+ )
+
+ plt.xlabel("x")
+ plt.ylabel("f(x)")
+ plt.legend(loc="upper center")
+
+ return plt, R
+
+
+def exp_fourier_to_trig(fourier_coeffs):
+ """Convert the Fourier coefficients from exponential form to trigonometric form."""
+
+ n = len(fourier_coeffs)
+ c0 = fourier_coeffs[0]
+ b_coeffs = [2 * np.real(fourier_coeffs[i]) for i in range(1, int(n / 2) + 1)]
+ a_coeffs = [-2 * np.imag(fourier_coeffs[i]) for i in range(1, int(n / 2) + 1)]
+ return [c0] + b_coeffs + a_coeffs
+
+
+def coeffs_array_to_class(fourier_coeffs):
+ """Convert the Fourier coefficients from array to a Class to be dealt in an easier way."""
+
+ fourier_class = []
+
+ for i in range(len(fourier_coeffs)):
+ fourier_class_term = Fourier_terms(
+ fourier_coeffs[i, 2], int(fourier_coeffs[i, 1]), int(fourier_coeffs[i, 0])
+ )
+ fourier_class.append(fourier_class_term)
+ return fourier_class
+
+
+def trig_to_final_array(trig_coeffs):
+ """Convert the array with the trigonometric coefficients into an array that tracks if a
+ coefficient corresponds to a Cosine (0) or to a Sine (1)."""
+
+ array_list = np.zeros(shape=(len(trig_coeffs), 3))
+ array_list[0, 0] = 0 # 0 means Cos
+ array_list[0, 1] = 0
+ array_list[0, 2] = np.real(trig_coeffs[0])
+ for i in range(1, int((len(trig_coeffs)) / 2) + 1):
+ # cosine
+ array_list[i, 0] = 0 # 0 means Cos
+ array_list[i, 1] = i
+ array_list[i, 2] = trig_coeffs[i]
+ for i in range(int((len(trig_coeffs)) / 2) + 1, len(trig_coeffs)):
+ # sine
+ array_list[i, 0] = 1 # 1 means Sin
+ array_list[i, 1] = i - (len(trig_coeffs) - 1) / 2
+ array_list[i, 2] = trig_coeffs[i]
+ return array_list
+
+
+class Fourier_terms:
+ """Class that contains all the information about a Fourier term"""
+
+ def __init__(self, coefficient, angle, function):
+ self.coeff = coefficient
+ self.angle = angle
+ self.function = function # 0 cosine, 1 sine
diff --git a/examples/qfiae/vqregressor_linear_ansatz.py b/examples/qfiae/vqregressor_linear_ansatz.py
new file mode 100644
index 000000000..391c9f5ae
--- /dev/null
+++ b/examples/qfiae/vqregressor_linear_ansatz.py
@@ -0,0 +1,362 @@
+import matplotlib.pyplot as plt
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+class VQRegressor_linear_ansatz:
+ def __init__(self, layers, ndata, function, xmax=np.pi, xmin=-np.pi):
+ """
+ This class implements an Adam Descent optimization performed on a
+ 1-qubit Variational Quantum Circuit defined by the linear_ansatz function.
+ Args:
+ layers: integer value representing the number of layers
+ ndata: integer value representing the training set's cardinality
+ function: target function f(x)
+ xmax: upper limit in the x variable
+ xmin: lower limit in the x variable
+ """
+ self.xmin = xmin
+ self.xmax = xmax
+ self.nqubits = 1
+ self.layers = layers
+ self.f = function
+ self.params = np.random.randn(3 * layers + 3).astype("float64")
+ self.nparams = (layers + 1) * 3
+ self.features, self.labels, self.norm = self.prepare_training_set(ndata)
+ self.nsample = len(self.labels)
+
+ def S(self, circuit, x):
+ """Data encoding circuit block."""
+
+ circuit.add(gates.RZ(q=0, theta=x))
+
+ def A(self, circuit, theta_vec):
+ """Trainable circuit block."""
+
+ circuit.add(gates.RZ(q=0, theta=theta_vec[0]))
+ circuit.add(gates.RY(q=0, theta=theta_vec[1]))
+ circuit.add(gates.RZ(q=0, theta=theta_vec[2]))
+
+ def linear_ansatz(self, weights, x=None):
+ """Variational Quantum Circuit of the Quantum Neural Network that performs the fit of f(x)."""
+
+ c = Circuit(self.nqubits)
+ # First unitary, Layer 0
+ self.A(c, weights[0:3])
+ # Adding different layers
+ for i in range(3, len(weights), 3):
+ self.S(c, x)
+ self.A(c, weights[i : i + 3])
+ c.add(gates.M(0))
+ return c
+
+ def label_points(self, x):
+ """
+ Function which implement the target function
+ Args:
+ x: np.float64 array of input variables
+ Returns: np.float64 array of output variables
+ """
+
+ y = self.f(x)
+ # We normalize the labels
+ ymax = np.max(np.abs(y))
+ y = y / ymax
+ return y, ymax
+
+ def prepare_training_set(self, n_sample):
+ """
+ This function picks a random sample from the uniform U[xmin,xmax]
+ Args:
+ n_sample: integer desired dataset cardinality
+ Returns: the features, the labels and the normalization constant
+ """
+
+ x = np.random.uniform(self.xmin, self.xmax, n_sample)
+ labels, norm = self.label_points(x)
+ return x, labels, norm
+
+ def show_predictions(self, title, save, features=None):
+ """
+ This function shows the VQR predictions on the training dataset
+ Args:
+ title: string title of the plot
+ save: boolean variable; pick True if you want to save the image as title.png
+ pick False if you don't want to save it
+ features: np.matrix in the form (2**nqubits, n_sample) on which you desire to perform
+ the predictions. Default = None and it takes the training dataset
+ """
+
+ if features is None:
+ features = self.features
+
+ labels, norm = self.label_points(features)
+
+ predictions = self.predict_sample(features)
+
+ plt.figure(figsize=(15, 7))
+ plt.title(title)
+ plt.scatter(
+ features,
+ predictions * norm,
+ label="Predicted",
+ s=100,
+ color="blue",
+ alpha=0.65,
+ )
+ plt.scatter(
+ features,
+ labels * norm,
+ label="Original",
+ s=100,
+ color="red",
+ alpha=0.65,
+ )
+ plt.xlabel("x")
+ plt.ylabel("f(x)")
+ plt.legend()
+ plt.tight_layout()
+ if save is True:
+ plt.savefig(title + ".pdf")
+ plt.close()
+ plt.show()
+
+ def set_parameters(self, new_params):
+ """
+ Function which set the new parameters into the circuit
+ Args:
+ new_params: np.array of the new parameters; it has to be (3 * nlayers) long
+ """
+
+ self.params = new_params
+
+ def one_prediction(self, this_feature):
+ """
+ This function executes one prediction
+ Args:
+ this_feature: np.float64 array 2**nqubits long, containing a specific feature
+ Returns: circuit's prediction of the output variable, evaluated as difference of probabilities
+ """
+
+ c = self.linear_ansatz(self.params, this_feature)
+ results = c().probabilities(qubits=[0])
+ res = results[0] - results[1]
+ return res
+
+ def predict_sample(self, features=None):
+ """
+ This function calculates the predictions related to a specific sample
+ Args:
+ features: np.matrix containing the N states; each state must be prepared (with
+ the opportune self.prepare_states function as an array with dim 2**nqubits)
+ Returns: np.array of the predictions
+ """
+
+ if features is None:
+ features = self.features
+
+ predictions = np.zeros(len(features))
+ for i in range(len(features)):
+ predictions[i] = self.one_prediction(features[i])
+
+ return predictions
+
+ def one_target_loss(self, this_feature, this_label):
+ """
+ Evaluation of the loss function for a single feature knowing its label
+ Args:
+ this_feature: the feature in form of an 2**nqubits-dim np.array
+ this_label: the associated label
+ Returns: one target loss function's value
+ """
+
+ this_prediction = self.one_prediction(this_feature)
+ cf = (this_prediction - this_label) ** 2
+ return cf
+
+ def loss(self, params, features=None, labels=None):
+ """
+ Evaluation of the total loss function
+ Args:
+ params: np.array of the params which define the circuit
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ Returns: loss function evaluated by summing contributes of each data
+ """
+
+ if params is None:
+ params = self.params
+
+ if features is None:
+ features = self.features
+
+ if labels is None:
+ labels = self.labels
+
+ self.set_parameters(params)
+ cf = 0
+ for feat, lab in zip(features, labels):
+ cf = cf + self.one_target_loss(feat, lab)
+ cf = cf / len(labels)
+ return cf
+
+ def dloss(self, features, labels):
+ """
+ This function calculates the loss function's gradients with respect to self.params
+ Args:
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ Returns: np.array of length self.nparams containing the loss function's gradients
+ """
+
+ loss_gradients = np.zeros(self.nparams)
+ loss = 0
+
+ for feat, label in zip(features, labels):
+ prediction_evaluation = self.one_prediction(feat)
+ loss += (label - prediction_evaluation) ** 2
+ obs_gradients = self.parameter_shift(feat)
+ for i in range(self.nparams):
+ loss_gradients[i] += (2 * prediction_evaluation * obs_gradients[i]) - (
+ 2 * label * obs_gradients[i]
+ )
+
+ return loss_gradients, (loss / len(features))
+
+ def shift_a_parameter(self, i, this_feature):
+ """
+ Parameter shift's execution on a single parameter
+ Args:
+ i: integer index which identify the parameter into self.params
+ this_feature: np.array 2**nqubits-long containing the state vector assciated to a data
+ Returns: derivative of the observable (here the prediction) with respect to self.params[i]
+ """
+
+ original = self.params.copy()
+ shifted = self.params.copy()
+
+ shifted[i] += np.pi / 2
+ self.set_parameters(shifted)
+ forward = self.one_prediction(this_feature)
+
+ shifted[i] -= np.pi
+ self.set_parameters(shifted)
+ backward = self.one_prediction(this_feature)
+
+ self.set_parameters(original)
+ result = 0.5 * (forward - backward)
+
+ return result
+
+ def parameter_shift(self, this_feature):
+ """
+ Full parameter-shift rule's implementation
+ Args:
+ this_feature: np.array 2**nqubits-long containing the state vector assciated to a data
+ Returns: np.array of the observable's gradients with respect to the variational parameters
+ """
+
+ obs_gradients = np.zeros(self.nparams, dtype=np.float64)
+ for ipar in range(self.nparams - 1):
+ obs_gradients[ipar] = self.shift_a_parameter(ipar, this_feature)
+ return obs_gradients
+
+ def AdamDescent(
+ self,
+ learning_rate,
+ m,
+ v,
+ features,
+ labels,
+ iteration,
+ beta_1=0.85,
+ beta_2=0.99,
+ epsilon=1e-8,
+ ):
+ """
+ Implementation of the Adam optimizer: during a run of this function parameters are updated.
+ Furthermore, new values of m and v are calculated.
+ Args:
+ learning_rate: np.float value of the learning rate
+ m: momentum's value before the execution of the Adam descent
+ v: velocity's value before the execution of the Adam descent
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ iteration: np.integer value corresponding to the current training iteration
+ beta_1: np.float value of the Adam's beta_1 parameter; default 0.85
+ beta_2: np.float value of the Adam's beta_2 parameter; default 0.99
+ epsilon: np.float value of the Adam's epsilon parameter; default 1e-8
+ Returns: np.float new values of momentum and velocity
+ """
+
+ grads, loss = self.dloss(features, labels)
+
+ for i in range(self.nparams):
+ m[i] = beta_1 * m[i] + (1 - beta_1) * grads[i]
+ v[i] = beta_2 * v[i] + (1 - beta_2) * grads[i] * grads[i]
+ mhat = m[i] / (1.0 - beta_1 ** (iteration + 1))
+ vhat = v[i] / (1.0 - beta_2 ** (iteration + 1))
+ self.params[i] -= learning_rate * mhat / (np.sqrt(vhat) + epsilon)
+
+ return m, v, loss
+
+ def train_with_psr(self, epochs, learning_rate, batches, J_treshold):
+ """
+ This function performs the full Adam descent's procedure
+ Args:
+ epochs: np.integer value corresponding to the epochs of training
+ learning_rate: np.float value of the learning rate
+ batches: np.integer value of the number of batches which divide the dataset
+ J_treshold: np.float value of the desired loss function's treshold
+ Returns: list of loss values, one for each epoch
+ """
+
+ losses = []
+ indices = []
+
+ # create index list
+ idx = np.arange(0, self.nsample)
+
+ m = np.zeros(self.nparams)
+ v = np.zeros(self.nparams)
+
+ # create index blocks on which we run
+ for ib in range(batches):
+ indices.append(np.arange(ib, self.nsample, batches))
+
+ iteration = 0
+
+ for epoch in range(epochs):
+ if epoch != 0 and losses[-1] < J_treshold:
+ print(
+ "Desired sensibility is reached, here we stop: ",
+ iteration,
+ " iteration",
+ )
+ break
+ # shuffle index list
+ np.random.shuffle(idx)
+ # run over the batches
+ for ib in range(batches):
+ iteration += 1
+
+ features = self.features[idx[indices[ib]]]
+ labels = self.labels[idx[indices[ib]]]
+ # update parameters
+ m, v, this_loss = self.AdamDescent(
+ learning_rate, m, v, features, labels, iteration
+ )
+ # track the training
+ print(
+ "Iteration ",
+ iteration,
+ " epoch ",
+ epoch + 1,
+ " | loss: ",
+ this_loss,
+ )
+ # in case one wants to plot J in function of the iterations
+ losses.append(this_loss)
+
+ return
diff --git a/examples/qsvd/README.md b/examples/qsvd/README.md
new file mode 100644
index 000000000..23282d48a
--- /dev/null
+++ b/examples/qsvd/README.md
@@ -0,0 +1,52 @@
+# Quantum Singular Value Decomposer
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/qsvd](https://github.com/qiboteam/qibo/tree/master/examples/qsvd)
+
+## Problem overview
+Much progress has been made towards a better understanding of bipartite and multipartite entanglement of quantum systems in the last decades. Among the many figures of merit that have been put forward to quantify entanglement, the von Neumann entropy stands out as it finely reveals the quantum correlations between subparts of the system. Yet, the explicit computation of this entropy, as well as many other bipartite measures of entanglement, relies on a clever decomposition of the tensor that describes a two-party system, and in general, it demands a large investment of computational resources.
+
+## Implementing the solution
+The code herein aims at reproducing the results of the manuscript ["Quantum Singular Value Decomposer"](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.101.062310). We are going to implement a quantum circuit that produces the Schmidt coefficients of the singular value decomposition of a pure bipartite state. In turn, these coefficients will be used to compute the von Neumann entropy. We call this circuit Quantum Singular Value Decomposer (QSVD). The QSVD is made of two unitaries, each acting on a separate subpart of the system, that are determined in a variational way. The frequencies of the outputs in the computational basis for the final state in the circuit deliver the eigenvalues of the decomposition (i.e. the Schmidt coefficients) without further treatment. From them, the von Neumann entropy readily follows. Moreover, the eigenvectors of the decomposition can be recreated from the direct action of the adjoint of the unitaries that conform the system on computational-basis states.
+
+The key ingredient of the algorithm is to train the circuit on exact coincidence of outputs for both subsystems. This is a subtle way to force a diagonal form onto the state. It also provides an example of a quantum circuit which is not trained to minimize some energy, but rather to achieve a precise relation between the superposition terms in the state.
+
+![qsvd](images/QSVD.png)
+
+## How to run an example
+
+To run a particular instance of the problem we have to set up the initial
+arguments:
+- `nqubits` (int): number of quantum bits. (default=6)
+- `subsize` (int): size of the bipartition with qubits 0,1,...,subsize-1. (default=3)
+- `nlayers` (int): number of ansatz layers. (default=5)
+- `nshots` (int): number of shots used when sampling the circuit. (default= 100000)
+- `RY` (bool): if True, Ry rotations are used in the ansatz. If False, RxRzRx are employed instead. (default=False)
+- `method` (string): classical optimization method, supported by scipy.optimize.minimize. (default='Powell')
+
+
+To run an example with default values, you should execute the following command:
+
+```bash
+python main.py
+```
+
+To run an example with different values, type for example:
+
+```bash
+python main.py --nqubits 5 --subsize 2 --nlayers 4 --nshots 10000
+```
+
+## Results
+The variational approach to the QSVD can be verified on simulations. We can consider random states such that the amplitudes are *c* = *a* + i*b* where *a* and *b* are random real numbers between -0.5 and 0.5, further restricted by a global normalization. We can start, for instance, with 6 qubit states and natural bipartition, i.e. 3 qubits in each subsystem, disregarding the presence of experimental noise. We consider results for a different number of layers in our variational circuit. The structure of the quantum circuit is the following:
+
+![ansatz](images/ansatz.png)
+
+where R stands for RxRzRz rotations (if `RY==False`) or Ry rotations. The figure below shows the entanglement entropy computed from the trained QSVD circuit vs. the exact entropy:
+
+![entropy](images/Entropy_6qubits.png)
+
+We have analyzed 500 random states for the 1 and 2 layers case, and 200 random states for the 3, 4 and 5 layers case. The mean number of optimization steps is of the order of a few hundred. We can also plot the mean error and standard deviation for the different number of layers:
+
+![error](images/error.png)
+
+As suggested by the [Solovay-Kitaev theorem](https://arxiv.org/abs/quant-ph/0505030), we observe fast convergence of results for every instance we analyze. The variational circuit approaches the exact result as we increase the number of layers, whatever the entanglement is. In this respect, it is worth mentioning that we can also analyze [Absolute Maximally Entangled states](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.100.022342), for which the convergence of the variational QSVD is fast and faithful.
diff --git a/examples/qsvd/images/Entropy_6qubits.png b/examples/qsvd/images/Entropy_6qubits.png
new file mode 100644
index 000000000..449de3df0
Binary files /dev/null and b/examples/qsvd/images/Entropy_6qubits.png differ
diff --git a/examples/qsvd/images/QSVD.png b/examples/qsvd/images/QSVD.png
new file mode 100644
index 000000000..e8afbd2a3
Binary files /dev/null and b/examples/qsvd/images/QSVD.png differ
diff --git a/examples/qsvd/images/ansatz.png b/examples/qsvd/images/ansatz.png
new file mode 100755
index 000000000..aee7e9ea1
Binary files /dev/null and b/examples/qsvd/images/ansatz.png differ
diff --git a/examples/qsvd/images/error.png b/examples/qsvd/images/error.png
new file mode 100644
index 000000000..8ada4b152
Binary files /dev/null and b/examples/qsvd/images/error.png differ
diff --git a/examples/qsvd/main.py b/examples/qsvd/main.py
new file mode 100644
index 000000000..88b48f0f8
--- /dev/null
+++ b/examples/qsvd/main.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+import argparse
+
+import numpy as np
+from qsvd import QSVD
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--nqubits", default=6, help="Number of qubits", type=int)
+parser.add_argument(
+ "--subsize",
+ default=3,
+ help="Subsize of the bipartition with qubits 0,1,...,subzize-1",
+ type=int,
+)
+parser.add_argument(
+ "--nlayers", default=5, help="Number of layers of the variational circuit", type=int
+)
+parser.add_argument(
+ "--nshots",
+ default=int(1e5),
+ help="Number of shots used when sampling the circuit",
+ type=int,
+)
+parser.add_argument(
+ "--RY",
+ action="store_true",
+ help="Use Ry rotations or RxRzRx rotations in the ansatz",
+)
+parser.add_argument(
+ "--method", default="Powell", help="Classical otimizer employed", type=str
+)
+parser.add_argument(
+ "--maxiter", default=None, help="Maximum number of iterations.", type=int
+)
+
+
+def main(nqubits, subsize, nlayers, nshots, RY, method, maxiter):
+ # We initialize the QSVD
+ Qsvd = QSVD(nqubits, subsize, nlayers, RY=RY)
+
+ # We choose an initial random state
+ initial_state = np.random.uniform(-0.5, 0.5, 2**nqubits) + 1j * np.random.uniform(
+ -0.5, 0.5, 2**nqubits
+ )
+ initial_state = initial_state / np.linalg.norm(initial_state)
+ if nqubits <= 6:
+ print("Initial random state: ", initial_state)
+
+ # We compute the exact Schmidt coefficients and von Neuman entropy
+ densmatrix = np.outer(initial_state, np.conjugate(initial_state))
+ n = nqubits - subsize
+ m = subsize
+ if subsize <= nqubits / 2:
+ reduced_dens = np.trace(
+ densmatrix.reshape(2**n, 2**m, 2**n, 2**m), axis1=1, axis2=3
+ )
+ else:
+ reduced_dens = np.trace(
+ densmatrix.reshape(2**n, 2**m, 2**n, 2**m), axis1=0, axis2=2
+ )
+
+ schmidt = np.linalg.eigvalsh(reduced_dens)
+ vneumann = -np.sum(schmidt[schmidt > 0] * np.log2(schmidt[schmidt > 0]))
+ schmidt = -np.sort(-np.sqrt(np.abs(schmidt)))
+ print("Exact Schmidt coefficients: ", schmidt)
+ print("Exact von Neumann entropy: ", vneumann)
+
+ # We choose initial random parameters
+ if not RY: # if Rx,Rz,Rx rotations are employed in the anstaz
+ initial_parameters = (
+ 2 * np.pi * np.random.rand(6 * nqubits * nlayers + 3 * nqubits)
+ )
+
+ else: # if Ry rotations are employed in the anstaz
+ initial_parameters = 2 * np.pi * np.random.rand(2 * nqubits * nlayers + nqubits)
+
+ # We train the QSVD
+ print("Training QSVD...")
+ cost_function, optimal_angles = Qsvd.minimize(
+ initial_parameters,
+ init_state=initial_state,
+ nshots=nshots,
+ method=method,
+ maxiter=maxiter,
+ )
+
+ # We use the optimal angles to compute the Schmidt coefficients of the bipartion
+ Schmidt_coefficients = Qsvd.Schmidt_coeff(optimal_angles, initial_state)
+ print("QSVD Schmidt coefficients: ", Schmidt_coefficients)
+
+ # We compute the von Neumann entropy using the Schmidt coefficients
+ VonNeumann_entropy = Qsvd.VonNeumann_entropy(optimal_angles, initial_state)
+ print("QSVD von Neumann entropy: ", VonNeumann_entropy)
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/qsvd/qsvd.py b/examples/qsvd/qsvd.py
new file mode 100644
index 000000000..3c33942a8
--- /dev/null
+++ b/examples/qsvd/qsvd.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+class QSVD:
+ def __init__(self, nqubits, subsize, nlayers, RY=False):
+ """
+ Class for the Quantum Singular Value Decomposer variational algorithm
+
+ Args:
+ nqubits: number of qubits
+ subsize: size of the subsystem with qubits 0,1,...,sub_size-1
+ nlayers: number of layers of the varitional ansatz
+ RY: if True, parameterized Ry gates are used in the circuit
+ if False, parameterized Rx,Rz,Rx gates are used in the circuit (default=False)
+ """
+ self.nqubits = nqubits
+ self.subsize = subsize
+ self.subsize2 = nqubits - subsize
+
+ if RY:
+
+ def rotations():
+ for q in range(self.nqubits):
+ yield gates.RY(q, theta=0)
+
+ else:
+
+ def rotations():
+ for q in range(self.nqubits):
+ yield gates.RX(q, theta=0)
+ yield gates.RZ(q, theta=0)
+ yield gates.RX(q, theta=0)
+
+ self._circuit = self.ansatz(nlayers, rotations)
+
+ def _CZ_gates(self):
+ """Yields CZ gates used in the variational circuit."""
+ # U
+ for q in range(0, self.subsize - 1, 2):
+ yield gates.CZ(q, q + 1)
+ # V
+ for q in range(self.subsize, self.nqubits - 1, 2):
+ yield gates.CZ(q, q + 1)
+
+ def ansatz(self, nlayers, rotations):
+ """
+ Args:
+ nlayers: number of layers of the varitional ansatz
+ rotations: Function that generates rotation gates (defined in __init__)
+
+ Returns:
+ Circuit model implementing the variational ansatz
+ """
+ c = Circuit(self.nqubits)
+ for _ in range(nlayers):
+ c.add(rotations())
+ c.add(self._CZ_gates())
+ c.add(rotations())
+ c.add(self._CZ_gates())
+ # Final rotations
+ c.add(rotations())
+ # Measurements
+ small = min(self.subsize, self.subsize2)
+ for q in range(small):
+ c.add(gates.M(q))
+ c.add(gates.M(q + self.subsize))
+ return c
+
+ def QSVD_circuit(self, theta):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+
+ Returns:
+ Circuit model implementing the variational ansatz for QSVD
+ """
+ self._circuit.set_parameters(theta)
+ return self._circuit
+
+ def QSVD_cost(self, theta, init_state=None, nshots=100000):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ init_state: numpy.array with the quantum state to be Schmidt-decomposed
+ nshots: int number of runs of the circuit during the sampling process (default=100000)
+
+ Returns:
+ numpy.float32 with the value of the cost function for the QSVD with angles theta
+ """
+
+ def Hamming(string1, string2):
+ """
+ Args:
+ two strings to be compared
+
+ Returns:
+ Hamming distance of the strings
+ """
+ return sum(q1 != q2 for q1, q2 in zip(string1, string2))
+
+ Circuit_ansatz = self.QSVD_circuit(theta)
+ result = Circuit_ansatz(init_state, nshots)
+ result = result.frequencies(binary=True)
+
+ loss = 0
+ for bit_string in result:
+ a = bit_string[: self.subsize2]
+ b = bit_string[self.subsize2 :]
+ loss += Hamming(a, b) * result[bit_string]
+ return loss / nshots
+
+ def minimize(
+ self, init_theta, init_state=None, nshots=100000, method="Powell", maxiter=None
+ ):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ init_state: numpy.array with the quantum state to be Schmidt-decomposed
+ nshots: int number of runs of the circuit during the sampling process (default=100000)
+ method: 'classical optimizer for the minimization. All methods from scipy.optimize.minmize are suported (default='Powell')
+
+ Returns:
+ numpy.float64 with value of the minimum found, numpy.ndarray with the optimal angles
+ """
+ from scipy.optimize import minimize
+
+ result = minimize(
+ self.QSVD_cost,
+ init_theta,
+ args=(init_state, nshots),
+ method=method,
+ options={"disp": True, "maxiter": maxiter},
+ )
+ loss = result.fun
+ optimal_angles = result.x
+
+ return loss, optimal_angles
+
+ def Schmidt_coeff(self, theta, init_state, nshots=100000):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ init_state: numpy.array with the quantum state to be Schmidt-decomposed
+ nshots: int number of runs of the circuit during the sampling process (default=100000)
+
+ Returns:
+ numpy.array with the Schmidt coefficients given by the QSVD, in decreasing order
+ """
+ Qsvd = self.QSVD_circuit(theta)
+ result = Qsvd(init_state, nshots)
+
+ result = result.frequencies(binary=True)
+ small = min(self.subsize, self.subsize2)
+
+ Schmidt = []
+ for i in range(2**small):
+ bit_string = bin(i)[2:].zfill(small)
+ Schmidt.append(result[2 * bit_string])
+
+ Schmidt = np.array(sorted(Schmidt, reverse=True))
+ Schmidt = np.sqrt(Schmidt / nshots)
+
+ return Schmidt / np.linalg.norm(Schmidt)
+
+ def VonNeumann_entropy(self, theta, init_state, tol=1e-14, nshots=100000):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ init_state: numpy.array with the quantum state to be Schmidt-decomposed
+ nshots: int number of runs of the circuit during the sampling process (default=10000)
+
+ Returns:
+ numpy.float64 with the value of the Von Neumann entropy for the given bipartition
+ """
+ Schmidt = self.Schmidt_coeff(theta, init_state, nshots=nshots)
+ Schmidt = Schmidt**2
+
+ non_zero_coeff = np.array([coeff for coeff in Schmidt if coeff > tol])
+
+ return -np.sum(non_zero_coeff * np.log2(non_zero_coeff))
diff --git a/examples/reuploading_classifier/README.md b/examples/reuploading_classifier/README.md
new file mode 100644
index 000000000..5e27ea72c
--- /dev/null
+++ b/examples/reuploading_classifier/README.md
@@ -0,0 +1,66 @@
+# Data reuploading for a universal quantum classifier
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/reuploading_classifier](https://github.com/qiboteam/qibo/tree/master/examples/reuploading_classifier).
+
+
+Based in the paper [Quantum 4, 226 (2020).](https://quantum-journal.org/papers/q-2020-02-06-226/). In this `README.md` file you can see a short
+set of instructions for the usage of the example.
+
+#### What this example does?
+
+This example provides a variational algorithm for classifying classical data using only one qubit. There are two main ideas
+in this work. The general idea is
+to create a single-qubit circuit where several different unitary gates are aplied
+- Re-uploading: create a single-qubit circuit where several different unitary gates are applied. The exact gate
+ depends on the point to classify and on a set of parameters found by learning the classes of a training set.
+- Separating classes: the Bloch sphere has plenty of room on its surface, thus it is possible to define a set of
+target states representing different labels. These states are chosen in such a way that they are maximally orthogonal
+among them.
+
+The re-uploading works in circuits like the one below. For this example, we will classify datasets with dimension 2.
+The exact dependency of the unitary gates was chosen to be Rz (w x1 + b)Ry (v x0 + a),
+although other Ansätze are also useful. Notice that the Ry gates are connected to the x0 coordinate,
+while Rz gates are related to x1 . Rotations around two different axis suffice to generate enough
+representativity to solve the problem.
+
+![circuit](images/circuit.png)
+
+The goal is to find a set of parameters such that all points belonging to the same class are driven to be as close as
+possible to a target state that depends on the class. These target states are chosen to be as orthogonal as possible.
+For binary classification, the states |0> and |1> are enough. Three classes require an equilateral triangle circumscribed
+to an equator. More classes require polyhedra. For 4, 6, 8, 12 and 20 labels, platonic polyhedra may be used.
+
+![block](images/bloch_states.png)
+
+#### Usage
+In this example there are only three files
+- `datasets.py` contains the classical functions that create the datasets of the experiment and their representations.
+- `qlassifier.py` encodes all quantum circuits and procedures needed to run the circuits.
+- `main.py` is the file calling all other functions. The action of every line of code is commented in the source code.
+
+The parameters to be fixed for a run of the experiment are
+- dataset: problem to solve, to choose between `['circle', '3_circles', 'square', '4_squares', 'crown', 'tricrown', 'wavy_lines']`
+- layers: layers of the classifier
+
+Every time the `main.py` is run, it checks whether there is a solution to that particular problem. If it has been
+already computed, then no minimization is done, only the results are saved. Otherwise, minimization is performed.
+A collection of solved examples is saved in `saved_parameters.pkl`. It is not mandatory, but it may save some time.
+
+#### Results
+
+Two different results are presented in this example. First, it is shown the labels that the algorithm guesses for every
+point (left), together with which ones are right and wrong (right). This result is useful for quantifying the accuracy
+of the classifier. The colors represent the **guessed** labels, while the area corresponding to each class is delimited
+with a black line.
+
+![test_set](images/test_set.png)
+
+The second result represents the distribution of the test points on the Bloch sphere. In this case, the colors represent
+the **actual** labels of the points. This plot is useful to see how the points in the same class get close to the target
+state.
+
+![world_map](images/world_map.png)
+
+Every time the `main.py` file is run, both results are saved in the folder
+`results/[name of the problem]/[number of layers]_layers`. The repository includes the solution
+for all problems with 10 layers.
diff --git a/examples/reuploading_classifier/datasets.py b/examples/reuploading_classifier/datasets.py
new file mode 100644
index 000000000..4a8b45291
--- /dev/null
+++ b/examples/reuploading_classifier/datasets.py
@@ -0,0 +1,359 @@
+from itertools import product
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def create_dataset(name, grid=None, samples=1000, seed=0):
+ """Function to create training and test sets for classifying.
+
+ Args:
+ name (str): Name of the problem to create the dataset, to choose between
+ ['circle', '3 circles', 'square', '4 squares', 'crown', 'tricrown', 'wavy lines'].
+ grid (int): Number of points in one direction defining the grid of points.
+ If not specified, the dataset does not follow a regular grid.
+ samples (int): Number of points in the set, randomly located.
+ This argument is ignored if grid is specified.
+ seed (int): Random seed
+
+ Returns:
+ Dataset for the given problem (x, y)
+ """
+ if grid is None:
+ np.random.seed(seed)
+ points = 1 - 2 * np.random.rand(samples, 2)
+ else:
+ x = np.linspace(-1, 1, grid)
+ points = np.array(list(product(x, x)))
+ creator = globals()[f"_{name}"]
+
+ x, y = creator(points)
+ return x, y
+
+
+def create_target(name):
+ """Function to create target states for classification.
+
+ Args:
+ name (str): Name of the problem to create the target states, to choose between
+ ['circle', '3 circles', 'square', '4 squares', 'crown', 'tricrown', 'wavy lines']
+
+ Returns:
+ List of numpy arrays encoding target states that depend only on the number of classes of the given problem
+ """
+ if name in ["circle", "square", "crown"]:
+ targets = [np.array([1, 0], dtype="complex"), np.array([0, 1], dtype="complex")]
+ elif name in ["tricrown"]:
+ targets = [
+ np.array([1, 0], dtype="complex"),
+ np.array([np.cos(np.pi / 3), np.sin(np.pi / 3)], dtype="complex"),
+ np.array([np.cos(np.pi / 3), -np.sin(np.pi / 3)], dtype="complex"),
+ ]
+ elif name in ["4_squares", "wavy_lines", "3_circles"]:
+ targets = [
+ np.array([1, 0], dtype=complex),
+ np.array([1 / np.sqrt(3), np.sqrt(2 / 3)], dtype=complex),
+ np.array(
+ [1 / np.sqrt(3), np.exp(1j * 2 * np.pi / 3) * np.sqrt(2 / 3)],
+ dtype=complex,
+ ),
+ np.array(
+ [1 / np.sqrt(3), np.exp(-1j * 2 * np.pi / 3) * np.sqrt(2 / 3)],
+ dtype=complex,
+ ),
+ ]
+
+ else:
+ raise NotImplementedError("This dataset is not implemented")
+
+ return targets
+
+
+def fig_template(name):
+ """Function to create templates for plotting results of classification.
+
+ Args:
+ name (str): Name of the problem to create the figure template, to choose between
+ ['circle', '3 circles', 'square', '4 squares', 'crown', 'tricrown', 'wavy lines']
+
+ Returns:
+ matplotlib.figure, matplotlib.axis with the templates for plotting results.
+
+ """
+ fig, axs = plt.subplots(ncols=2, figsize=(9, 4))
+ if name == "circle":
+ for ax in axs:
+ circle = plt.Circle(
+ (0, 0), np.sqrt(2 / np.pi), color="black", fill=False, zorder=10
+ )
+ ax.add_artist(circle)
+
+ elif name == "3_circles":
+ centers = np.array([[-1, 1], [1, 0], [-0.5, -0.5]])
+ radii = np.array([1, np.sqrt(6 / np.pi - 1), 1 / 2])
+ for c, r in zip(centers, radii):
+ for ax in axs:
+ circle = plt.Circle(c, r, color="black", fill=False, zorder=10)
+ ax.add_artist(circle)
+
+ elif name == "square":
+ p = 0.5 * np.sqrt(2)
+ for ax in axs:
+ ax.plot([-p, p, p, -p, -p], [-p, -p, p, p, -p], color="black", zorder=10)
+
+ elif name == "4_squares":
+ for ax in axs:
+ ax.plot([0, 0], [-1, 1], color="black", zorder=10)
+ ax.plot([-1, 1], [0, 0], color="black", zorder=10)
+
+ elif name == "crown" or name == "tricrown":
+ centers = [[0, 0], [0, 0]]
+ radii = [np.sqrt(0.8), np.sqrt(0.8 - 2 / np.pi)]
+ for c, r in zip(centers, radii):
+ for ax in axs:
+ circle = plt.Circle(c, r, color="black", fill=False, zorder=10)
+ ax.add_artist(circle)
+
+ elif name == "wavy_lines":
+ freq = 1
+
+ def fun1(s):
+ return s + np.sin(freq * np.pi * s)
+
+ def fun2(s):
+ return -s + np.sin(freq * np.pi * s)
+
+ x = np.linspace(-1, 1)
+ for ax in axs:
+ ax.plot(x, np.clip(fun1(x), -1, 1), color="black", zorder=10)
+ ax.plot(x, np.clip(fun2(x), -1, 1), color="black", zorder=10)
+
+ axs[0].set(xlabel=r"$x_0$", ylabel=r"$x_1$", xlim=[-1, 1], ylim=[-1, 1])
+ axs[0].axis("equal")
+ axs[1].set(xlabel=r"$x_0$", xlim=[-1, 1], ylim=[-1, 1])
+ axs[1].axis("equal")
+
+ return fig, axs
+
+
+def world_map_template():
+ """Function to create templates for plotting the Bloch Sphere after classification.
+
+ Returns:
+ matplotlib.figure, matplotlib.axis with the templates for plotting results
+ """
+ fig, ax = plt.subplots(figsize=(20, 10))
+ ax.plot(
+ laea_x(np.pi, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(np.pi, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(-np.pi, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(-np.pi, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(-np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(-np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(2 * np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(2 * np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(-2 * np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(-2 * np.pi / 3, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(0, np.linspace(-np.pi / 2, np.pi / 2)),
+ laea_y(0, np.linspace(-np.pi / 2, np.pi / 2)),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.linspace(-np.pi, np.pi), 0),
+ laea_y(np.linspace(-np.pi, np.pi), 0),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.linspace(-np.pi, np.pi), np.pi / 6),
+ laea_y(np.linspace(-np.pi, np.pi), np.pi / 6),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.linspace(-np.pi, np.pi), -np.pi / 6),
+ laea_y(np.linspace(-np.pi, np.pi), -np.pi / 6),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.linspace(-np.pi, np.pi), np.pi / 3),
+ laea_y(np.linspace(-np.pi, np.pi), np.pi / 3),
+ color="k",
+ zorder=10,
+ )
+ ax.plot(
+ laea_x(np.linspace(-np.pi, np.pi), -np.pi / 3),
+ laea_y(np.linspace(-np.pi, np.pi), -np.pi / 3),
+ color="k",
+ zorder=10,
+ )
+ ax.text(0, 1.47, r"$|0\rangle$", fontsize=20)
+ ax.text(0, -1.53, r"$|1\rangle$", fontsize=20)
+ ax.text(0.05, 0.05, r"$|+\rangle$", fontsize=20)
+ ax.text(2.9, 0, r"$|-\rangle$", fontsize=20)
+ ax.text(-3.2, 0, r"$|-\rangle$", fontsize=20)
+
+ return fig, ax
+
+
+def laea_x(lamb, phi):
+ """Auxiliary function to represent spheres in a 2D map - x axis. Inspired in the Hammer projection.
+
+ Args:
+ lamb (float): longitude.
+ phi (float): latitude.
+
+ Returns:
+ x-axis in Hammer projection.
+ """
+ return (
+ 2
+ * np.sqrt(2)
+ * np.cos(phi)
+ * np.sin(lamb / 2)
+ / np.sqrt(1 + np.cos(phi) * np.cos(lamb / 2))
+ )
+
+
+def laea_y(lamb, phi):
+ """Auxiliary function to represent spheres in a 2D map - y axis. Inspired in the Hammer projection.
+
+ Args:
+ lamb (float): longitude.
+ phi (float): latitude.
+
+ Returns:
+ y-axis in Hammer projection.
+ """
+ return np.sqrt(2) * np.sin(phi) / np.sqrt(1 + np.cos(phi) * np.cos(lamb / 2))
+
+
+def _circle(points):
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(np.linalg.norm(points, axis=1) > np.sqrt(2 / np.pi))
+ labels[ids] = 1
+
+ return points, labels
+
+
+def _3_circles(points):
+ centers = np.array([[-1, 1], [1, 0], [-0.5, -0.5]])
+ radii = np.array([1, np.sqrt(6 / np.pi - 1), 1 / 2])
+ labels = np.zeros(len(points), dtype=np.int32)
+ for j, (c, r) in enumerate(zip(centers, radii)):
+ ids = np.where(np.linalg.norm(points - c, axis=1) < r)
+ labels[ids] = 1 + j
+
+ return points, labels
+
+
+def _square(points):
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(np.max(np.abs(points), axis=1) > 0.5 * np.sqrt(2))
+ labels[ids] = 1
+
+ return points, labels
+
+
+def _4_squares(points):
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(np.logical_and(points[:, 0] < 0, points[:, 1] > 0))
+ labels[ids] = 1
+ ids = np.where(np.logical_and(points[:, 0] > 0, points[:, 1] < 0))
+ labels[ids] = 2
+ ids = np.where(np.logical_and(points[:, 0] > 0, points[:, 1] > 0))
+ labels[ids] = 3
+
+ return points, labels
+
+
+def _crown(points):
+ c = [[0, 0], [0, 0]]
+ r = [np.sqrt(0.8), np.sqrt(0.8 - 2 / np.pi)]
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(
+ np.logical_and(
+ np.linalg.norm(points - [c[0]], axis=1) < r[0],
+ np.linalg.norm(points - [c[1]], axis=1) > r[1],
+ )
+ )
+ labels[ids] = 1
+
+ return points, labels
+
+
+def _tricrown(points):
+ c = [[0, 0], [0, 0]]
+ r = [np.sqrt(0.8), np.sqrt(0.8 - 2 / np.pi)]
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(np.linalg.norm(points - [c[0]], axis=1) > r[0])
+ labels[ids] = 2
+ ids = np.where(
+ np.logical_and(
+ np.linalg.norm(points - [c[0]], axis=1) < r[0],
+ np.linalg.norm(points - [c[1]], axis=1) > r[1],
+ )
+ )
+ labels[ids] = 1
+
+ return points, labels
+
+
+def _wavy_lines(points):
+ freq = 1
+
+ def fun1(s):
+ return s + np.sin(freq * np.pi * s)
+
+ def fun2(s):
+ return -s + np.sin(freq * np.pi * s)
+
+ labels = np.zeros(len(points), dtype=np.int32)
+ ids = np.where(
+ np.logical_and(
+ points[:, 1] < fun1(points[:, 0]), points[:, 1] > fun2(points[:, 0])
+ )
+ )
+ labels[ids] = 1
+ ids = np.where(
+ np.logical_and(
+ points[:, 1] > fun1(points[:, 0]), points[:, 1] < fun2(points[:, 0])
+ )
+ )
+ labels[ids] = 2
+ ids = np.where(
+ np.logical_and(
+ points[:, 1] > fun1(points[:, 0]), points[:, 1] > fun2(points[:, 0])
+ )
+ )
+ labels[ids] = 3
+
+ return points, labels
diff --git a/examples/reuploading_classifier/images/bloch_states.png b/examples/reuploading_classifier/images/bloch_states.png
new file mode 100644
index 000000000..108ac9352
Binary files /dev/null and b/examples/reuploading_classifier/images/bloch_states.png differ
diff --git a/examples/reuploading_classifier/images/circuit.png b/examples/reuploading_classifier/images/circuit.png
new file mode 100644
index 000000000..4f33fca32
Binary files /dev/null and b/examples/reuploading_classifier/images/circuit.png differ
diff --git a/examples/reuploading_classifier/images/test_set.png b/examples/reuploading_classifier/images/test_set.png
new file mode 100644
index 000000000..172c34257
Binary files /dev/null and b/examples/reuploading_classifier/images/test_set.png differ
diff --git a/examples/reuploading_classifier/images/world_map.png b/examples/reuploading_classifier/images/world_map.png
new file mode 100644
index 000000000..fcd17d617
Binary files /dev/null and b/examples/reuploading_classifier/images/world_map.png differ
diff --git a/examples/reuploading_classifier/main.py b/examples/reuploading_classifier/main.py
new file mode 100644
index 000000000..2a4d8c35e
--- /dev/null
+++ b/examples/reuploading_classifier/main.py
@@ -0,0 +1,53 @@
+# /usr/bin/env python
+import argparse
+import pickle
+
+from qlassifier import single_qubit_classifier
+
+# TODO: fix issue with .pkl
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+ "--dataset", default="tricrown", help="Name of the example", type=str
+)
+parser.add_argument("--layers", default=10, help="Number of layers.", type=int)
+
+
+def main(dataset, layers):
+ """Perform classification for a given problem and number of layers.
+
+ Args:
+ dataset (str): Problem to create the dataset, to choose between
+ ['circle', '3_circles', 'square', '4_squares', 'crown', 'tricrown', 'wavy_lines']
+ layers (int): Number of layers to use in the classifier
+ """
+ ql = single_qubit_classifier(dataset, layers) # Define classifier
+ try:
+ with open("saved_parameters.pkl", "rb") as f:
+ # Load previous results. Have we ever run these problem?
+ data = pickle.load(f)
+ except:
+ data = {dataset: {}}
+
+ try:
+ parameters = data[dataset][layers]
+ print("Problem solved before, obtaining parameters from file...")
+ print("-" * 60)
+ except:
+ print("Problem never solved, finding optimal parameters...")
+ result, parameters = ql.minimize(method="l-bfgs-b", options={"disp": True})
+
+ data[dataset][layers] = parameters
+ with open("saved_parameters.pkl", "wb") as f:
+ pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
+
+ ql.set_parameters(parameters)
+ value_loss = ql.cost_function_fidelity()
+ print("The value of the cost function achieved is %.6f" % value_loss)
+ ql.paint_results()
+ ql.paint_world_map()
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/reuploading_classifier/qlassifier.py b/examples/reuploading_classifier/qlassifier.py
new file mode 100644
index 000000000..bc20cbc25
--- /dev/null
+++ b/examples/reuploading_classifier/qlassifier.py
@@ -0,0 +1,291 @@
+import os
+
+import numpy as np
+from datasets import create_dataset, create_target, fig_template, world_map_template
+from matplotlib.cm import get_cmap
+from matplotlib.colors import Normalize
+
+from qibo import Circuit, gates
+
+
+class single_qubit_classifier:
+ def __init__(self, name, layers, grid=11, test_samples=1000, seed=0):
+ """Class with all computations needed for classification.
+
+ Args:
+ name (str): Name of the problem to create the dataset, to choose between
+ ['circle', '3 circles', 'square', '4 squares', 'crown', 'tricrown', 'wavy lines'].
+ layers (int): Number of layers to use in the classifier.
+ grid (int): Number of points in one direction defining the grid of points.
+ If not specified, the dataset does not follow a regular grid.
+ samples (int): Number of points in the set, randomly located.
+ This argument is ignored if grid is specified.
+ seed (int): Random seed.
+
+ Returns:
+ Dataset for the given problem (x, y).
+ """
+ np.random.seed(seed)
+ self.name = name
+ self.layers = layers
+ self.training_set = create_dataset(name, grid=grid)
+ self.test_set = create_dataset(name, samples=test_samples)
+ self.target = create_target(name)
+ self.params = np.random.randn(layers * 4)
+ self._circuit = self._initialize_circuit()
+ try:
+ os.makedirs("results/" + self.name + "/%s_layers" % self.layers)
+ except:
+ pass
+
+ def set_parameters(self, new_params):
+ """Method for updating parameters of the class.
+
+ Args:
+ new_params (array): New parameters to update
+ """
+ self.params = new_params
+
+ def _initialize_circuit(self):
+ """Creates variational circuit."""
+ C = Circuit(1)
+ for l in range(self.layers):
+ C.add(gates.RY(0, theta=0))
+ C.add(gates.RZ(0, theta=0))
+ return C
+
+ def circuit(self, x):
+ """Method creating the circuit for a point (in the datasets).
+
+ Args:
+ x (array): Point to create the circuit.
+
+ Returns:
+ Qibo circuit.
+ """
+ params = []
+ for i in range(0, 4 * self.layers, 4):
+ params.append(self.params[i] * x[0] + self.params[i + 1])
+ params.append(self.params[i + 2] * x[1] + self.params[i + 3])
+ self._circuit.set_parameters(params)
+ return self._circuit
+
+ def cost_function_one_point_fidelity(self, x, y):
+ """Method for computing the cost function for
+ a given sample (in the datasets), using fidelity.
+
+ Args:
+ x (array): Point to create the circuit.
+ y (int): label of x.
+
+ Returns:
+ float with the cost function.
+ """
+ C = self.circuit(x)
+ state = C.execute().state()
+ cf = 0.5 * (1 - fidelity(state, self.target[y])) ** 2
+ return cf
+
+ def cost_function_fidelity(self, params=None):
+ """Method for computing the cost function for the training set, using fidelity.
+
+ Args:
+ params(array): new parameters to update before computing
+
+ Returns:
+ float with the cost function.
+ """
+ if params is None:
+ params = self.params
+
+ self.set_parameters(params)
+ cf = 0
+ for x, y in zip(self.training_set[0], self.training_set[1]):
+ cf += self.cost_function_one_point_fidelity(x, y)
+ cf /= len(self.training_set[0])
+ return cf
+
+ def minimize(self, method="BFGS", options=None, compile=True):
+ loss = self.cost_function_fidelity
+
+ if method == "cma":
+ # Genetic optimizer
+ import cma
+
+ r = cma.fmin2(lambda p: loss(p), self.params, 2)
+ result = r[1].result.fbest
+ parameters = r[1].result.xbest
+
+ elif method == "sgd":
+ import tensorflow as tf
+
+ circuit = self.circuit(self.training_set[0])
+
+ sgd_options = {
+ "nepochs": 5001,
+ "nmessage": 1000,
+ "optimizer": "Adamax",
+ "learning_rate": 0.5,
+ }
+ if options is not None:
+ sgd_options.update(options)
+
+ # proceed with the training
+ vparams = tf.Variable(self.params)
+ optimizer = getattr(tf.optimizers, sgd_options["optimizer"])(
+ learning_rate=sgd_options["learning_rate"]
+ )
+
+ def opt_step():
+ with tf.GradientTape() as tape:
+ l = loss(vparams)
+ grads = tape.gradient(l, [vparams])
+ optimizer.apply_gradients(zip(grads, [vparams]))
+ return l, vparams
+
+ if compile:
+ opt_step = tf.function(opt_step)
+
+ l_optimal, params_optimal = 10, self.params
+ for e in range(sgd_options["nepochs"]):
+ l, vparams = opt_step()
+ if l < l_optimal:
+ l_optimal, params_optimal = l, vparams
+ if e % sgd_options["nmessage"] == 0:
+ print("ite %d : loss %f" % (e, l))
+
+ result = np.array(self.cost_function(params_optimal))
+ parameters = np.array(params_optimal)
+
+ else:
+ import numpy as np
+ from scipy.optimize import minimize
+
+ m = minimize(lambda p: loss(p), self.params, method=method, options=options)
+ result = m.fun
+ parameters = m.x
+
+ return result, parameters
+
+ def eval_test_set_fidelity(self):
+ """Method for evaluating points in the training set, using fidelity.
+
+ Returns:
+ list of guesses.
+ """
+ labels = [[0]] * len(self.test_set[0])
+ for j, x in enumerate(self.test_set[0]):
+ C = self.circuit(x)
+ state = C.execute().state()
+ fids = np.empty(len(self.target))
+ for i, t in enumerate(self.target):
+ fids[i] = fidelity(state, t)
+ labels[j] = np.argmax(fids)
+
+ return labels
+
+ def paint_results(self):
+ """Method for plotting the guessed labels and the right guesses.
+
+ Returns:
+ plot with results.
+ """
+ fig, axs = fig_template(self.name)
+ guess_labels = self.eval_test_set_fidelity()
+ colors_classes = get_cmap("tab10")
+ norm_class = Normalize(vmin=0, vmax=10)
+ x = self.test_set[0]
+ x_0, x_1 = x[:, 0], x[:, 1]
+ axs[0].scatter(
+ x_0, x_1, c=guess_labels, s=2, cmap=colors_classes, norm=norm_class
+ )
+ colors_rightwrong = get_cmap("RdYlGn")
+ norm_rightwrong = Normalize(vmin=-0.1, vmax=1.1)
+
+ checks = [int(g == l) for g, l in zip(guess_labels, self.test_set[1])]
+ axs[1].scatter(
+ x_0, x_1, c=checks, s=2, cmap=colors_rightwrong, norm=norm_rightwrong
+ )
+ print(
+ "The accuracy for this classification is %.2f"
+ % (100 * np.sum(checks) / len(checks)),
+ "%",
+ )
+
+ fig.savefig("results/" + self.name + "/%s_layers/test_set.pdf" % self.layers)
+
+ def paint_world_map(self):
+ """Method for plotting the proper labels on the Bloch sphere.
+
+ Returns:
+ plot with 2D representation of Bloch sphere.
+ """
+ angles = np.zeros((len(self.test_set[0]), 2))
+ from datasets import laea_x, laea_y
+
+ fig, ax = world_map_template()
+ colors_classes = get_cmap("tab10")
+ norm_class = Normalize(vmin=0, vmax=10)
+ for i, x in enumerate(self.test_set[0]):
+ C = self.circuit(x)
+ state = C.execute().state()
+ angles[i, 0] = np.pi / 2 - np.arccos(
+ np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2
+ )
+ angles[i, 1] = np.angle(state[1] / state[0])
+
+ ax.scatter(
+ laea_x(angles[:, 1], angles[:, 0]),
+ laea_y(angles[:, 1], angles[:, 0]),
+ c=self.test_set[1],
+ cmap=colors_classes,
+ s=15,
+ norm=norm_class,
+ )
+
+ if len(self.target) == 2:
+ angles_0 = np.zeros(len(self.target))
+ angles_1 = np.zeros(len(self.target))
+ angles_0[0] = np.pi / 2
+ angles_0[1] = -np.pi / 2
+ col = list(range(2))
+
+ elif len(self.target) == 3:
+ angles_0 = np.zeros(len(self.target) + 1)
+ angles_1 = np.zeros(len(self.target) + 1)
+ angles_0[0] = np.pi / 2
+ angles_0[1] = -np.pi / 6
+ angles_0[2] = -np.pi / 6
+ angles_0[3] = -np.pi / 6
+ angles_1[2] = np.pi
+ angles_1[3] = -np.pi
+ col = list(range(3)) + [2]
+
+ else:
+ angles_0 = np.zeros(len(self.target))
+ angles_1 = np.zeros(len(self.target))
+ for i, state in enumerate(self.target):
+ angles_0[i] = np.pi / 2 - np.arccos(
+ np.abs(state[0]) ** 2 - np.abs(state[1]) ** 2
+ )
+ angles_1[i] = np.angle(state[1] / state[0])
+ col = list(range(len(self.target)))
+
+ ax.scatter(
+ laea_x(angles_1, angles_0),
+ laea_y(angles_1, angles_0),
+ c=col,
+ cmap=colors_classes,
+ s=500,
+ norm=norm_class,
+ marker="P",
+ zorder=11,
+ )
+
+ ax.axis("off")
+
+ fig.savefig("results/" + self.name + "/%s_layers/world_map.pdf" % self.layers)
+
+
+def fidelity(state1, state2):
+ return np.abs(np.sum(np.conj(state2) * state1)) ** 2
diff --git a/examples/reuploading_classifier/saved_parameters.pkl b/examples/reuploading_classifier/saved_parameters.pkl
new file mode 100644
index 000000000..48bb6da9e
Binary files /dev/null and b/examples/reuploading_classifier/saved_parameters.pkl differ
diff --git a/examples/shor/README.md b/examples/shor/README.md
new file mode 100644
index 000000000..3dfb9dae3
--- /dev/null
+++ b/examples/shor/README.md
@@ -0,0 +1,77 @@
+# Shor's factorization algorithm
+
+Code at: [https://github.com/qiboteam/qibo/tree/shor/examples/shor](https://github.com/qiboteam/qibo/tree/shor/examples/shor)
+
+The original paper by Shor [arXiv:9508027](https://arxiv.org/abs/quant-ph/9508027) stated that the discrete logarithm problem and factorization could be reduced to order finding. Then, Shor proposed an order finding algorithm using a quantum computer that would run in polynomial time.
+
+A circuit proposal for Shor's algorithm, mainly on the construction of a quantum modular exponentiation, followed shortly [arXiv:9511018](https://arxiv.org/abs/quant-ph/9511018) by Vedral, Barenco and Ekert. This implementation required 7n+2 qubits, where n refers to the number of bits needed to encode the number to factorize.
+
+The following years, more proposals were introduced that decreased the number of qubits needed for the algorithm. This example is based on the Shor's algorithm implementation put forward by Beauregard [arXiv:0205095](https://arxiv.org/abs/quant-ph/0205095) in which the number of qubits needed is reduced to only 2n+3.
+
+All images taken from [arXiv:0205095](https://arxiv.org/abs/quant-ph/0205095).
+
+#### Quantum order finding algorithm
+
+The aim of this algorithm is to find the least integer r>0 such that x^r = 1 mod N.
+
+- Start from state |00...00>|00...01>.
+- Create full superposition on the first register.
+- Apply modular exponentiation to second register such that ~|j>|x^j mod N>.
+- Apply inverse Quantum Fourier tranform on the first register.
+- Measure the first quantum register. The result, divided by the maximum possible value, can be interpreted as ~(s/r).
+- Use the continued fractions algorithm to estimate the value of r.
+
+The order finding algorithm on a quantum circuit looks like the following.
+
+![order-finding](images/order-finding.png)
+
+#### Quantum factorization of number N
+
+The order finding algorithm can be used as a subroutine to factorize a number N.
+
+To find a non-trivial factor of N:
+
+- If N is even, return the factor 2.
+- Check if N = a^b for integers a > 1 and b > 2.
+- Choose x randomly in range(1, N-1). If gcd(x, N) > 1 return it as a factor.
+- Use the quantum order finding algorithm to find order r of x modulo N.
+- If r is even and x^(r/2) is not equal to -1 mod N. Compute gcd(x^(r/2) - 1, N) and gcd(x^(r/2) + 1, N) and check if they are non-trivial factors of N.
+
+This algorithm has a chance to fail, as not all random values of x will output an even value for r. In that case, the algorithm has to be re-run. It also might be the case that the output of the quantum computer is close, but not exactly the peak we are looking for, in that case, the algorithm could also fail. Regardless, Shor argues that repeating the experiment O(log(log(N))) times is enough to find the solution with high probability.
+
+This can be increased to O(1) if the result of the quantum computer is enhanced by classicaly trying variations on the output of the quantum computer if quantum resources are scarce.
+
+#### Qubit reduction techniques
+
+Beauregard achieves a qubit reduction to 2n+3 due to two main modifications.
+
+##### Fixing x and N
+
+Since the values for x and N are fixed before entering the quantum computer, they do not need to be stored in a quantum register. This benefits some reversible quantum addition algorithms more than others. In particular, performing the addition in Fourier space only requires n+1 qubits for adding two n-bit numbers when one of them is fixed.
+
+![fourier-add](images/determined-fourier-addition.png)
+
+Where the repeated gate can be computed classically ahead of time and just added to the system as a RZ (U1) gate.
+
+##### One-qubit controlling trick
+
+Since the controlled gates that apply the modular exponentiation commute, instead of using all 2n qubits to perform the inverse Quantum Fourier tranform, a single qubit can act as a control by performing multiple measurements on it.
+
+![semiclassical](images/semiclassical.png)
+
+The single qubit at the top register will be measured 2n times, and will be reset to |0> accordingly. Additionally, the Rotation gates applied to the qubit will depend on all the previous measurements recorded. This can be interpreted as a semiclassical implementation of the inverse Quantum Fourier transform.
+
+### Running the code
+
+This example contains two files
+- `functions.py` contains all functions necessary to run Shor's factorization algorithm.
+- `main.py` is the file where the functions are assembled so that a given number N can be factorized.
+
+Certain parameters can be given to the main file in order to tune the example.
+- `N`: number to factorize. Recomended to be the multiplication of two prime numbers. Minimum number possible is 15. `default = 15`
+- `times`: maximum number of tries for the algorithm to find the prime numbers. `default = 10`
+- `A`: fix the value used for order finding. If `None` the value is chosen randomly. `default = None`
+- `semiclassical`: flag to perform the iQFT in a semiclassical way. This achieves the final scaling of 2n+3 qubits (4n+2 qubits instead).
+- `enhance`: flag to enhance the quantum result classically in order to find the solution in less iterations.
+
+This example returns a comprehensible step by step analysis of Shor's factorization algorithm, culmination with two non-trivial factors of given number N.
diff --git a/examples/shor/functions.py b/examples/shor/functions.py
new file mode 100644
index 000000000..f4b2b3664
--- /dev/null
+++ b/examples/shor/functions.py
@@ -0,0 +1,423 @@
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+def adder_angles(a, n):
+ """Classical computation of the angles needed for adder in Fourier space.
+ Args:
+ a (int) = number to add.
+ n (int) = number of bits without overflow.
+
+ Returns:
+ angles (list) = list of angles in order of application.
+ """
+ A = "{0:0{bits}b}".format(a, bits=n + 1)
+ angles = []
+ for i in reversed(range(len(A))):
+ angle = 0
+ m = 1
+ for j in range(i, len(A)):
+ if int(A[j]) == 1:
+ angle += 2 * np.pi / (2**m)
+ m += 1
+ angles.append(angle)
+ return angles
+
+
+def phi_adder(b, angles):
+ """Quantum adder in Fourier space.
+ Args:
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in range(0, len(b)):
+ yield gates.U1(b[i], angles[i])
+
+
+def i_phi_adder(b, angles):
+ """(Inverse) Quantum adder in Fourier space.
+ Args:
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in reversed(range(0, len(b))):
+ yield gates.U1(b[i], -angles[i])
+
+
+def c_phi_adder(c, b, angles):
+ """(1 Control) Quantum adder in Fourier space.
+ Args:
+ c (int): qubit acting as control.
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in range(0, len(b)):
+ yield gates.U1(b[i], angles[i]).controlled_by(c)
+
+
+def i_c_phi_adder(c, b, angles):
+ """(Inverse) (1 Control) Quantum adder in Fourier space.
+ Args:
+ c (int): qubit acting as control.
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in reversed(range(0, len(b))):
+ yield gates.U1(b[i], -angles[i]).controlled_by(c)
+
+
+def cc_phi_adder(c1, c2, b, angles):
+ """(2 Controls) Quantum adder in Fourier space.
+ Args:
+ c1 (int): qubit acting as first control.
+ c2 (int): qubit acting as second control.
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in range(0, len(b)):
+ yield gates.U1(b[i], angles[i]).controlled_by(c1, c2)
+
+
+def i_cc_phi_adder(c1, c2, b, angles):
+ """(Inverse) (2 Controls) Quantum adder in Fourier space.
+ Args:
+ c1 (int): qubit acting as first control.
+ c2 (int): qubit acting as second control.
+ b (list): quantum register where the addition is implemented.
+ angles (list): list of angles that encode the number to be added.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in reversed(range(0, len(b))):
+ yield gates.U1(b[i], -angles[i]).controlled_by(c1, c2)
+
+
+def qft(q):
+ """Quantum Fourier Transform on a quantum register.
+ Args:
+ q (list): quantum register where the QFT is applied.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i1 in range(len(q)):
+ yield gates.H(q[i1])
+ for i2 in range(i1 + 1, len(q)):
+ theta = np.pi / 2 ** (i2 - i1)
+ yield gates.CU1(q[i2], q[i1], theta)
+ for i in range(len(q) // 2):
+ yield gates.SWAP(i, len(q) - i - 1)
+
+
+def i_qft(q):
+ """(Inverse) Quantum Fourier Transform on a quantum register.
+ Args:
+ q (list): quantum register where the QFT is applied.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ for i in range(len(q) // 2):
+ yield gates.SWAP(i, len(q) - i - 1)
+ for i1 in reversed(range(len(q))):
+ for i2 in reversed(range(i1 + 1, len(q))):
+ theta = np.pi / 2 ** (i2 - i1)
+ yield gates.CU1(q[i2], q[i1], -theta)
+ yield gates.H(q[i1])
+
+
+def cc_phi_mod_adder(c1, c2, b, ang_a, ang_N, ancilla):
+ """(2 Controls) Quantum modular addition with fixed a and N in Fourier space.
+ Args:
+ c1 (int): qubit acting as first control.
+ c2 (int): qubit acting as second control.
+ b (list): quantum register where the addition is implemented.
+ ang_a (list): list of angles that encode the number to be added.
+ ang_N (list): list of angles that encode the modulo number.
+ ancilla (int): extra qubit needed for the computation.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ yield cc_phi_adder(c1, c2, b, ang_a)
+ yield i_phi_adder(b, ang_N)
+ yield i_qft(b)
+ yield gates.CNOT(b[0], ancilla)
+ yield qft(b)
+ yield c_phi_adder(ancilla, b, ang_N)
+ yield i_cc_phi_adder(c1, c2, b, ang_a)
+ yield i_qft(b)
+ yield gates.X(b[0]), gates.CNOT(b[0], ancilla), gates.X(b[0])
+ yield qft(b)
+ yield cc_phi_adder(c1, c2, b, ang_a)
+
+
+def i_cc_phi_mod_adder(c1, c2, b, ang_a, ang_N, ancilla):
+ """(Inverse) (2 Controls) Quantum modular addition with fixed a and N in Fourier space.
+ Args:
+ c1 (int): qubit acting as first control.
+ c2 (int): qubit acting as second control.
+ b (list): quantum register where the addition is implemented.
+ ang_a (list): list of angles that encode the number to be added.
+ ang_N (list): list of angles that encode the modulo number.
+ ancilla (int): extra qubit needed for the computation.
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ yield i_cc_phi_adder(c1, c2, b, ang_a)
+ yield i_qft(b)
+ yield gates.X(b[0]), gates.CNOT(b[0], ancilla), gates.X(b[0])
+ yield qft(b)
+ yield cc_phi_adder(c1, c2, b, ang_a)
+ yield i_c_phi_adder(ancilla, b, ang_N)
+ yield i_qft(b)
+ yield gates.CNOT(b[0], ancilla)
+ yield qft(b)
+ yield phi_adder(b, ang_N)
+ yield i_cc_phi_adder(c1, c2, b, ang_a)
+
+
+def c_mult_mod(c, x, b, a, N, ancilla, n):
+ """(1 Control) Quantum modular multiplication. |1>|x>|b> --> |1>|x>|(b+a*x)%N>
+ Args:
+ c (int): qubit acting as control.
+ x (list): quantum register encoding the number of times a is multiplied.
+ b (list): quantum register where the final result is encoded.
+ a (int): number to multiply.
+ N (int): modulo.
+ ancilla (int): extra quantum register needed for the modular addition.
+ n (int): number of bits needed to encode N
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ ang_N = adder_angles(N, n)
+ yield qft(b)
+ for i in range(len(x)):
+ ang_a = adder_angles((a * 2 ** (n - 1 - i)) % N, n)
+ yield cc_phi_mod_adder(c, x[i], b, ang_a, ang_N, ancilla)
+ yield i_qft(b)
+
+
+def i_c_mult_mod(c, x, b, a, N, ancilla, n):
+ """(Inverse) (1 Control) Quantum modular multiplication. |1>|x>|(b+a*x)%N> --> |1>|x>|b>
+ Args:
+ c (int): qubit acting as control.
+ x (list): quantum register encoding the number of times a is multiplied.
+ b (list): quantum register where the final result is encoded.
+ a (int): number to multiply.
+ N (int): modulo.
+ ancilla (int): extra quantum register needed for the modular addition.
+ n (int): number of bits needed to encode N
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ ang_N = adder_angles(N, n)
+ yield qft(b)
+ for i in reversed(range(len(x))):
+ ang_a = adder_angles((a * 2 ** (n - 1 - i)) % N, n)
+ yield i_cc_phi_mod_adder(c, x[i], b, ang_a, ang_N, ancilla)
+ yield i_qft(b)
+
+
+def c_U(c, x, b, a, N, ancilla, n):
+ """(1 Control) Quantum circuit that applies the modular multiplication as a part of the
+ modular exponentiation. |1>|x>|0> ---> |1>|(a*x)%N>|0>
+ Args:
+ c (int): qubit acting as control.
+ x (list): quantum register encoding the number of times a is multiplied.
+ b (list): quantum register initialized at |0>
+ a (int): number to multiply.
+ N (int): modulo.
+ ancilla (int): extra quantum register needed for the modular addition.
+ n (int): number of bits needed to encode N
+
+ Returns:
+ generator with the required quantum gates applied on the quantum circuit.
+ """
+ yield c_mult_mod(c, x, b, a, N, ancilla, n)
+ for i in range(1, len(b)):
+ yield gates.SWAP(b[i], x[i - 1]).controlled_by(c)
+ a_inv = mod_inv(a, N)
+ yield i_c_mult_mod(c, x, b, a_inv, N, ancilla, n)
+
+
+def find_factor_of_prime_power(N):
+ """Classical algorithm to check if given number is a prime power.
+ Args:
+ N (int): number to factorize.
+
+ Returns:
+ If the number is not a prime power: None
+ If the number is a prime power: f1 (int) or f2 (int)
+ """
+ for i in range(2, int(np.floor(np.log2(N))) + 1):
+ f = N ** (1 / i)
+ f1 = np.floor(f)
+ if f1**i == N:
+ return f1
+ f2 = np.ceil(f)
+ if f2**i == N:
+ return f2
+ return None
+
+
+def egcd(a, b):
+ """Extended Euclid's algorithm for the modular inverse calculation."""
+ if a == 0:
+ return (b, 0, 1)
+ else:
+ g, y, x = egcd(b % a, a)
+ return (g, x - (b // a) * y, y)
+
+
+def mod_inv(a, N):
+ """Classical algorithm to calculate the inverse of a modulo N.
+ Args:
+ a (int): number to invert.
+ N (int): modulo.
+
+ Returns:
+ x%N (int): inverse of a modulo N.
+ """
+ g, x, y = egcd(a, N)
+ if g != 1:
+ raise ValueError("modular inverse does not exist")
+ else:
+ return x % N
+
+
+def quantum_order_finding_full(N, a):
+ """Quantum circuit that performs the order finding algorithm using a fully quantum iQFT.
+ Args:
+ N (int): number to factorize.
+ a (int): chosen number to use in the algorithm.
+
+ Returns:
+ s (float): value of the state measured by the quantum computer.
+ """
+ print(" - Performing algorithm using a fully quantum iQFT.\n")
+ # Creating the parts of the needed quantum circuit.
+ n = int(np.ceil(np.log2(N)))
+ b = [i for i in range(n + 1)]
+ x = [n + 1 + i for i in range(n)]
+ ancilla = 2 * n + 1
+ q_reg = [2 * n + 2 + i for i in range(2 * n)]
+ circuit = Circuit(4 * n + 2)
+ print(f" - Total number of qubits used: {4*n+2}.\n")
+
+ # Building the quantum circuit
+ for i in range(len(q_reg)):
+ circuit.add(gates.H(q_reg[i]))
+ circuit.add(gates.X(x[len(x) - 1]))
+ exponents = []
+ exp = a % N
+ for i in range(len(q_reg)):
+ exponents.append(exp)
+ exp = (exp**2) % N
+ # a**(2**i)
+ circuit.add(
+ (c_U(q, x, b, exponents[i], N, ancilla, n) for i, q in enumerate(q_reg))
+ )
+ circuit.add(i_qft(q_reg))
+
+ # Adding measurement gates
+ circuit.add(gates.M(*q_reg))
+ result = circuit(nshots=1)
+ s = result.frequencies(binary=False).most_common()[0][0]
+ print(f"The quantum circuit measures s = {s}.\n")
+ return s
+
+
+def quantum_order_finding_semiclassical(N, a):
+ """Quantum circuit that performs the order finding algorithm using a semiclassical iQFT.
+ Args:
+ N (int): number to factorize.
+ a (int): chosen number to use in the algorithm.
+
+ Returns:
+ s (float): value of the state measured by the quantum computer.
+ """
+ print(" - Performing algorithm using a semiclassical iQFT.\n")
+ # Creating the parts of the needed quantum circuit.
+ n = int(np.ceil(np.log2(N)))
+ b = [i for i in range(n + 1)]
+ x = [n + 1 + i for i in range(n)]
+ ancilla = 2 * n + 1
+ q_reg = 2 * n + 2
+ print(f" - Total number of qubits used: {2*n+3}.\n")
+ results = []
+ exponents = []
+ exp = a % N
+ for i in range(2 * n):
+ exponents.append(exp)
+ exp = (exp**2) % N
+
+ circuit = Circuit(2 * n + 3)
+ # Building the quantum circuit
+ circuit.add(gates.H(q_reg))
+ circuit.add(gates.X(x[len(x) - 1]))
+ # a_i = (a**(2**(2*n - 1)))
+ circuit.add(c_U(q_reg, x, b, exponents[-1], N, ancilla, n))
+ circuit.add(gates.H(q_reg))
+ results.append(circuit.add(gates.M(q_reg, collapse=True)))
+ # Using multiple measurements for the semiclassical QFT.
+ for i in range(1, 2 * n):
+ # reset measured qubit to |0>
+ circuit.add(gates.RX(q_reg, theta=np.pi * results[-1].symbols[0]))
+ circuit.add(gates.H(q_reg))
+ # a_i = (a**(2**(2*n - 1 - i)))
+ circuit.add(c_U(q_reg, x, b, exponents[-1 - i], N, ancilla, n))
+ angle = 0
+ for k in range(2, i + 2):
+ angle += 2 * np.pi * results[i + 1 - k].symbols[0] / (2**k)
+ circuit.add(gates.U1(q_reg, -angle))
+ circuit.add(gates.H(q_reg))
+ results.append(circuit.add(gates.M(q_reg, collapse=True)))
+
+ circuit() # execute
+ s = sum(int(r.symbols[0].outcome()) * (2**i) for i, r in enumerate(results))
+ print(f"The quantum circuit measures s = {s}.\n")
+ return s
+
+
+def find_factors(r, a, N):
+ if r % 2 != 0:
+ print("The value found for r is not even. Trying again.\n")
+ print("-" * 60 + "\n")
+ return None
+ if a ** (r // 2) == -1 % N:
+ print("Unusable value for r found. Trying again.\n")
+ print("-" * 60 + "\n")
+ return None
+ f1 = np.gcd((a ** (r // 2)) - 1, N)
+ f2 = np.gcd((a ** (r // 2)) + 1, N)
+ if (f1 == N or f1 == 1) and (f2 == N or f2 == 1):
+ print(f"Trivial factors 1 and {N} found. Trying again.\n")
+ print("-" * 60 + "\n")
+ return None
+ if f1 != 1 and f1 != N:
+ f2 = N // f1
+ elif f2 != 1 and f2 != N:
+ f1 = N // f2
+ print(f"Found as factors for {N}: {f1} and {f2}.\n")
+ return f1, f2
diff --git a/examples/shor/images/determined-fourier-addition.png b/examples/shor/images/determined-fourier-addition.png
new file mode 100644
index 000000000..53317792b
Binary files /dev/null and b/examples/shor/images/determined-fourier-addition.png differ
diff --git a/examples/shor/images/order-finding.png b/examples/shor/images/order-finding.png
new file mode 100644
index 000000000..a5e496df3
Binary files /dev/null and b/examples/shor/images/order-finding.png differ
diff --git a/examples/shor/images/semiclassical.png b/examples/shor/images/semiclassical.png
new file mode 100644
index 000000000..d03a81a58
Binary files /dev/null and b/examples/shor/images/semiclassical.png differ
diff --git a/examples/shor/main.py b/examples/shor/main.py
new file mode 100644
index 000000000..36c06e991
--- /dev/null
+++ b/examples/shor/main.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+import argparse
+import fractions
+
+import functions
+import numpy as np
+
+
+def main(N, times, A, semiclassical, enhance):
+ """Factorize a number using Shor's algorithm.
+ Args:
+ N (int): number to factorize.
+ times (int): maximum number of repetitions allowed to the algorithm.
+ A (int) (optional): fix the number a used in the quantum order finding algorithm.
+ semiclassical (bool): use the semiclassical iQFT method to reduce number of qubits.
+ enhance (bool): classically enhance the quantum output for higher probability of success.
+
+ Returns:
+ f1 (int), f2 (int): factors of the given number N.
+ """
+ if N % 2 == 0:
+ raise ValueError(
+ f"Input number {N} needs to be odd for it to be a hard problem."
+ )
+ f = functions.find_factor_of_prime_power(N)
+ if f is not None:
+ raise ValueError(f"Input number {N} is a prime power with factor {f}.")
+ n = int(np.ceil(np.log2(N)))
+ for i in range(times):
+ if A:
+ a = A
+ else:
+ a = np.random.randint(2, N - 1)
+ f = np.gcd(a, N)
+ if 1 < f < N:
+ print(
+ f"N = {N} and a = {a} share factor {f}. Trying again for quantum instance.\n"
+ )
+ print("-" * 60 + "\n")
+ continue
+ print(f"Using Shor's algorithm to factorize N = {N} with a = {a}.\n")
+ if semiclassical:
+ s = functions.quantum_order_finding_semiclassical(N, a)
+ else:
+ s = functions.quantum_order_finding_full(N, a)
+ sr = s / 2 ** (2 * n)
+ r = fractions.Fraction.from_float(sr).limit_denominator(N).denominator
+ print(f"Quantum circuit outputs r = {r}.\n")
+ factors = functions.find_factors(r, a, N)
+ if factors:
+ return factors
+ if enhance:
+ for c in range(-4, 5):
+ sr = (s + c) / 2 ** (2 * n)
+ rr = fractions.Fraction.from_float(sr).limit_denominator(N).denominator
+ if rr == r:
+ continue
+ print(f"Checking for near values outputs r = {rr}.\n")
+ factors = functions.find_factors(rr, a, N)
+ if factors:
+ return factors
+ print("Checking multiples.\n")
+ for c in range(2, n):
+ rr = c * r
+ factors = functions.find_factors(rr, a, N)
+ if factors:
+ return factors
+ continue
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--N", default=15, type=int)
+ parser.add_argument("--times", default=10, type=int)
+ parser.add_argument("--A", default=None, type=int)
+ parser.add_argument("--semiclassical", action="store_true")
+ parser.add_argument("--enhance", action="store_true")
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/test_examples.py b/examples/test_examples.py
new file mode 100644
index 000000000..72cdd6603
--- /dev/null
+++ b/examples/test_examples.py
@@ -0,0 +1,373 @@
+import os
+import signal
+import sys
+from contextlib import contextmanager
+
+import pytest
+
+base_dir = os.path.join(os.getcwd(), "examples")
+sys.path.append(base_dir)
+
+
+@pytest.fixture(autouse=True)
+def max_time_setter(request):
+ request.function.__globals__["max_time"] = request.config.getoption(
+ "--examples-timeout"
+ )
+
+
+@contextmanager
+def timeout(time):
+ """Auxiliary timeout method. Register an alarm for a given
+ input time. This function provides a silent timeout error.
+
+ Args:
+ time (int): timeout seconds.
+ """
+ # register signal
+ signal.signal(signal.SIGALRM, raise_timeout)
+ signal.alarm(time)
+ try:
+ yield
+ except TimeoutError:
+ pass
+ finally:
+ # unregister signal
+ signal.signal(signal.SIGALRM, signal.SIG_IGN)
+
+
+def raise_timeout(signum, frame):
+ raise TimeoutError
+
+
+def run_script(args, script_name="main.py"):
+ """Executes external Python script with given arguments.
+
+ Args:
+ args (dict): Dictionary with arguments required by the script's main
+ function.
+ script_name (str): Name of the script file.
+ max_time (float): Time-out time in seconds.
+ """
+ code = open(script_name).read()
+ end = code.find("\nif __name__ ==")
+ code = code[:end] + "\n\nmain(**args)"
+ with timeout(max_time):
+ exec(code, {"args": args})
+
+
+@pytest.mark.parametrize("N", [3])
+@pytest.mark.parametrize("p", [0, 0.001])
+def test_3_tangle(N, p, shots=100, post_selection=True, no_plot=True):
+ args = locals()
+ path = os.path.join(base_dir, "3_tangle")
+ sys.path.append(path)
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("layers", [1, 2])
+@pytest.mark.parametrize("maxsteps", [5])
+@pytest.mark.parametrize("T_max", [2])
+def test_aavqe(nqubits, layers, maxsteps, T_max):
+ args = locals()
+ os.chdir(os.path.join(base_dir, "aavqe"))
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("layers", [1, 2])
+@pytest.mark.parametrize("compress", [1])
+@pytest.mark.parametrize("lambdas", [[0.9, 0.95, 1.0, 1.05, 1.10]])
+@pytest.mark.parametrize("maxiter", [1])
+def test_autoencoder(nqubits, layers, compress, lambdas, maxiter):
+ args = locals()
+ os.chdir(os.path.join(base_dir, "autoencoder"))
+ run_script(args)
+
+
+@pytest.mark.parametrize(("h_value", "collisions", "b"), [(163, 2, 7)])
+def test_hash_grover(h_value, collisions, b):
+ # remove ``functions`` module from 3SAT because the same name is used
+ # for a different module in the Hash
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "hash-grover")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize(("nqubits", "subsize"), [(3, 1), (4, 2)])
+@pytest.mark.parametrize("nlayers", [1, 2])
+@pytest.mark.parametrize("nshots", [1000])
+@pytest.mark.parametrize("RY", [False, True])
+def test_qsvd(nqubits, subsize, nlayers, nshots, RY, method="Powell", maxiter=1):
+ args = locals()
+ path = os.path.join(base_dir, "qsvd")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("dataset", ["circle", "square"])
+@pytest.mark.parametrize("layers", [2, 3])
+def test_reuploading_classifier(dataset, layers):
+ args = locals()
+ path = os.path.join(base_dir, "reuploading_classifier")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("data", [(2, 0.4, 0.05, 0.1, 1.9)])
+@pytest.mark.parametrize("bins", [8, 10])
+def test_unary(data, bins, M=10, shots=1000):
+ args = locals()
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ path = os.path.join(base_dir, "unary")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [3, 6])
+@pytest.mark.parametrize("circuit_name", ["qft", "variational"])
+def test_benchmarks(nqubits, circuit_name):
+ path = os.path.join(base_dir, "benchmarks")
+ sys.path[-1] = path
+ os.chdir(path)
+ code = open("main.py").read()
+ start = code.find("def main")
+ end = code.find("\nif __name__ ==")
+ header = (
+ "import argparse\nimport os\nimport time\nimport numpy as np"
+ "\nimport qibo\nimport circuits\nfrom utils import "
+ "BenchmarkLogger, parse_accelerators\n\n"
+ )
+ args = {
+ "nqubits": nqubits,
+ "circuit_name": circuit_name,
+ "backend": "qibojit",
+ "precision": "double",
+ "device": None,
+ "accelerators": None,
+ "get_branch": False,
+ "nshots": None,
+ "fuse": False,
+ "compile": False,
+ "nlayers": None,
+ "gate_type": None,
+ "params": {},
+ "filename": None,
+ }
+ code = header + code[start:end] + "\n\nmain(**args)"
+ with timeout(max_time):
+ exec(code, {"args": args})
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("nlayers", [1, 2])
+@pytest.mark.parametrize("fuse", [False, True])
+def test_vqe_benchmarks(nqubits, nlayers, fuse, method="Powell"):
+ args = locals()
+ args["backend"] = "qibojit"
+ path = os.path.join(base_dir, "benchmarks")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="vqe.py")
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("nangles", [2])
+@pytest.mark.parametrize("dense", [False, True])
+@pytest.mark.parametrize("solver", ["exp", "rk4"])
+def test_qaoa_benchmarks(nqubits, nangles, dense, solver, method="Powell"):
+ args = locals()
+ path = os.path.join(base_dir, "benchmarks")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="qaoa.py")
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("dt", [0.1, 0.01])
+@pytest.mark.parametrize("dense", [False, True])
+@pytest.mark.parametrize("solver", ["exp", "rk4"])
+@pytest.mark.parametrize("backend", ["qibojit"])
+def test_evolution_benchmarks(nqubits, dt, dense, solver, backend):
+ args = locals()
+ path = os.path.join(base_dir, "benchmarks")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="evolution.py")
+
+
+@pytest.mark.parametrize("nclasses", [3])
+@pytest.mark.parametrize("nqubits", [4])
+@pytest.mark.parametrize("nlayers", [4, 5])
+@pytest.mark.parametrize("nshots", [int(1e5)])
+@pytest.mark.parametrize("training", [False])
+@pytest.mark.parametrize("RxRzRx", [False])
+def test_variational_classifier(
+ nclasses, nqubits, nlayers, nshots, training, RxRzRx, method="Powell"
+):
+ args = locals()
+ path = os.path.join(base_dir, "variational_classifier")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [4, 8])
+@pytest.mark.parametrize("instance", [1])
+def test_grover3sat(nqubits, instance):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "grover3sat")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits,hfield,T,dt", [(4, 1, 10, 1e-1)])
+@pytest.mark.parametrize("solver", ["exp", "rk4"])
+def test_adiabatic_linear(nqubits, hfield, T, dt, solver, save=False):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "adiabatic")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="linear.py")
+
+
+@pytest.mark.parametrize("nqubits,hfield,dt, params", [(4, 1, 1e-2, [1.0])])
+@pytest.mark.parametrize("solver", ["exp"])
+@pytest.mark.parametrize("method", [("Powell")])
+def test_adiabatic_optimize(
+ nqubits, hfield, params, dt, solver, method, maxiter=None, save=False
+):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "adiabatic")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="optimize.py")
+
+
+@pytest.mark.parametrize("nqubits,hfield,T", [(4, 1, 1)])
+def test_adiabatic_trotter(nqubits, hfield, T, save=False):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "adiabatic")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="trotter_error.py")
+
+
+@pytest.mark.parametrize("nqubits,instance,T,dt", [(4, 1, 10, 1e-1)])
+@pytest.mark.parametrize("solver", ["exp", "rk4"])
+@pytest.mark.parametrize("dense", [True, False])
+@pytest.mark.parametrize("params", [[0.5, 0.5]])
+@pytest.mark.parametrize("method,maxiter", [("BFGS", 1)])
+def test_adiabatic3sat(
+ nqubits, instance, T, dt, solver, dense, params, method, maxiter, plot=False
+):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "adiabatic3sat")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("layers", [3, 2])
+@pytest.mark.parametrize("autoencoder", [0, 1])
+@pytest.mark.parametrize("example", [0, 1])
+@pytest.mark.parametrize("maxiter", [1])
+def test_ef_qae(layers, autoencoder, example, maxiter):
+ args = locals()
+ os.chdir(os.path.join(base_dir, "EF_QAE"))
+ run_script(args)
+
+
+@pytest.mark.parametrize("N", [15, 21])
+@pytest.mark.parametrize("times", [2, 10])
+@pytest.mark.parametrize("A", [None])
+@pytest.mark.parametrize("semiclassical", [True, False])
+@pytest.mark.parametrize("enhance", [True, False])
+def test_shor(N, times, A, semiclassical, enhance):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "shor")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+@pytest.mark.parametrize("delta_t", [0.5, 0.1])
+@pytest.mark.parametrize("max_layers", [10, 100])
+def test_falqon(nqubits, delta_t, max_layers):
+ if "functions" in sys.modules:
+ del sys.modules["functions"]
+ args = locals()
+ path = os.path.join(base_dir, "falqon")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args)
+
+
+@pytest.mark.parametrize("nqubits", [5, 6, 7])
+def test_grover_example1(nqubits):
+ args = locals()
+ path = os.path.join(base_dir, "grover")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="example1.py")
+
+
+@pytest.mark.parametrize("nqubits", [5, 8, 10])
+@pytest.mark.parametrize("num_1", [1, 2])
+@pytest.mark.parametrize("iterative", [False, True])
+def test_grover_example2(nqubits, num_1, iterative):
+ args = locals()
+ path = os.path.join(base_dir, "grover")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="example2.py")
+
+
+@pytest.mark.parametrize("nqubits", [5, 8, 10])
+@pytest.mark.parametrize("num_1", [1, 2])
+def test_grover_example3(nqubits, num_1):
+ args = locals()
+ path = os.path.join(base_dir, "grover")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="example3.py")
+
+
+@pytest.mark.parametrize("n_layers", [6])
+@pytest.mark.parametrize("batch_size", [20])
+@pytest.mark.parametrize("nepochs", [7])
+@pytest.mark.parametrize("train_size", [100])
+@pytest.mark.parametrize("filename", ["parameters/test_params"])
+@pytest.mark.parametrize("lr_boundaries", [[1, 2, 3, 4, 5, 6]])
+def test_anomalydetection_train(
+ n_layers, batch_size, nepochs, train_size, filename, lr_boundaries
+):
+ args = locals()
+ path = os.path.join(base_dir, "anomaly_detection")
+ sys.path[-1] = path
+ os.chdir(path)
+ run_script(args, script_name="train.py")
diff --git a/examples/transpiler/Drift speed limit.ipynb b/examples/transpiler/Drift speed limit.ipynb
new file mode 100644
index 000000000..9b51dcc0c
--- /dev/null
+++ b/examples/transpiler/Drift speed limit.ipynb
@@ -0,0 +1,600 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d5ec5eda",
+ "metadata": {},
+ "source": [
+ "# Theory behind this transformation\n",
+ "### See pp.186-187 of D'Alessandro"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a5cc7001",
+ "metadata": {},
+ "source": [
+ "We are interested in 2-qubit unitaries. Suppose that we ignore the global phase. Then w.l.o.g. our target unitary is a special unitary in $\\mathsf{SU}(4)$. The corresponding Lie algebra is $\\mathsf{su}(4)$.\n",
+ "\n",
+ "We have a constand drift Hailtonian $H_D = \\sigma_{z}\\otimes\\sigma_{z}$, whereas we can arbitrarily control the local Hamiltonians $H_C = \\sum_{i=x,y,z}(a_{i}(t)\\sigma_{i}\\otimes I + b_i(t)I\\otimes\\sigma_{i})$ and coefficients $a_{i}(t),b_{i}(t)$ can be arbitrarily strong.\n",
+ "\n",
+ "Then we naturally obtain the Cartan decomposition \n",
+ "\\begin{equation}\n",
+ " \\mathsf{su}(4) = \\mathfrak{k} \\oplus \\mathfrak{p},\n",
+ "\\end{equation}\n",
+ "where the subalgebra \n",
+ "\\begin{equation}\n",
+ " \\mathfrak{k} = \\mathrm{span}\\{i\\sigma_{j}\\otimes I, iI\\otimes\\sigma_{j} \\vert j = x,y,z\\}, \n",
+ "\\end{equation}\n",
+ "and\n",
+ "\\begin{equation}\n",
+ " \\mathfrak{p} = \\mathrm{span}\\{i\\sigma_{j}\\otimes \\sigma{k} \\vert j,k = x,y,z\\}.\n",
+ "\\end{equation}\n",
+ "Note that \n",
+ "\\begin{equation}\n",
+ " [ \\mathfrak{k},\\mathfrak{k} ] \\subseteq \\mathfrak{k},\\quad [ \\mathfrak{k},\\mathfrak{p} ] \\subseteq \\mathfrak{p},\\quad [ \\mathfrak{p},\\mathfrak{p} ] \\subseteq \\mathfrak{k}.\n",
+ "\\end{equation}\n",
+ "The Lie group corresponding to the Lie algebra $\\mathfrak{k}$ is $\\mathcal{K} = \\exp(\\mathfrak{k})$.\n",
+ "\n",
+ "Now we can define a Cartan subalgebra (included in $\\mathfrak{p}$) w.r.t. the decomposition\n",
+ "\\begin{equation}\n",
+ " \\mathfrak{a} = \\mathrm{span}\\{i\\sigma_j\\otimes\\sigma_j \\vert j = x,y,z\\}.\n",
+ "\\end{equation}\n",
+ "Then a decomposition \n",
+ "\\begin{equation}\n",
+ " U = K_{1}AK_{2},\n",
+ "\\end{equation}\n",
+ "where $K_1,K_2\\in\\mathcal{K}$ and $A \\in \\exp(\\mathfrak{a})$ always exists. \n",
+ "By finding the exponent of $A$, one can find the minimal time it takes to generate the target unitary $U$. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "84d03f0f",
+ "metadata": {},
+ "source": [
+ "It is known that $\\mathfrak{k} = T \\mathsf{so}(4) T^{-1}$, where $T$ is defined in the code below.\n",
+ "Hence, we first transform our unitary to be \n",
+ "\\begin{equation}\n",
+ " \\tilde{U} = T^{-1}UT = (T^{-1}K_1 T) (T^{-1} A T) (T^{-1} K_2 T) = O_1 D O_2,\n",
+ "\\end{equation}\n",
+ "where $O_1, O_2$ are real orthogonal matrices with determinant 1 and $D$ is a diagonal matrix. \n",
+ "\n",
+ "Hence, if we find $D$, we can apply the inverse tranformation to get $A = TDT^{-1}$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "4490e0c5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from scipy.linalg import eig, det, sqrtm, inv, logm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "efd43c1b",
+ "metadata": {},
+ "source": [
+ "# Groundworks"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d644f93a",
+ "metadata": {},
+ "source": [
+ "## Define $T$, $T^{-1}$, and a function to output the adjoint $T^{-1}UT$ (forward) or $TUT^{-1}$ (backward)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "5a6b3523",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "T = np.sqrt(1/2)*np.array([[0,1j,1,0],[1j,0,0,-1],[1j,0,0,1],[0,-1j,1,0]])\n",
+ "Tinv = np.transpose(np.conjugate(T))\n",
+ "\n",
+ "\n",
+ "def Tconj(mat,d = 'f'):\n",
+ " if d=='f':\n",
+ " return Tinv@mat@T\n",
+ " else:\n",
+ " return T@mat@Tinv"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f6f9394a",
+ "metadata": {},
+ "source": [
+ "## Convert a general (4-dimensional) unitary into a special unitary \n",
+ "### It only changes the global phase of a state which we do not care. If we need to care, use factor output"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "7865e9eb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def UtoSU(mat):\n",
+ " factor = np.log(det(mat),dtype = complex)/4\n",
+ " newmat = mat/np.power(det(mat),1/4,dtype = complex)\n",
+ " return newmat, factor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "00cedf0c",
+ "metadata": {},
+ "source": [
+ "## Finding $O_1$ and $D$ "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae693088",
+ "metadata": {},
+ "source": [
+ "We have $\\tilde{U} = O_1DO_2$. Then $\\tilde{U}\\tilde{U}^{\\top} = O_1D^2O_1^{\\top}$.\n",
+ "Equivalently,\n",
+ "\\begin{equation}\n",
+ " \\tilde{U}\\tilde{U}^{\\top} O_1 = O_1 D^2.\n",
+ "\\end{equation}\n",
+ "\n",
+ "Then $O_1$ is a matrix that has real orthonormal eigenvectors of $\\tilde{U}\\tilde{U}^{\\top}$ as its columns and $D^2$ is a diagonal matrix whose diagonal entries are eigenvalues of $\\tilde{U}\\tilde{U}^{\\top}$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ab6511d9",
+ "metadata": {},
+ "source": [
+ "## This part is problematic!!! We need a better algorithm for $U = O_1DO_2$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ed6654f9",
+ "metadata": {},
+ "source": [
+ "We cannot really recover $D$ that makes $O_2$ real and orthogonal.\n",
+ "See\n",
+ "\\begin{equation}\n",
+ "D = \\begin{pmatrix}\n",
+ " 1 & 0 & 0 & 0\\\\\n",
+ " 0 & 1 & 0 & 0\\\\\n",
+ " 0 & 0 & 1 & 0\\\\\n",
+ " 0 & 0 & 0 & -1\n",
+ "\\end{pmatrix}\n",
+ "\\end{equation}\n",
+ "which gives $D^2 = I$. Then the sqrtm function will give $D = I$ wrongly."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e1a613d",
+ "metadata": {},
+ "source": [
+ "This wrong evaluation makes $\\mathrm{det}(O_2) = -1$, and we don't want that. In such cases, we simply multiply -1 to the first diagonal element of $D$ and the first row of $O_2$. This makes $\\mathrm{det}(O_2) = 1$. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "4d5368d0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# from U, compute UU^{T}\n",
+ "def UUT(mat):\n",
+ " return mat@np.transpose(mat)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "c27a7e60",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# finding O_1 and D^2\n",
+ "def O1D2(UUTmat):\n",
+ " w, v = eig(UUTmat)\n",
+ " Diagmat = np.diag(w)\n",
+ " return v, Diagmat"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "5254fd6b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# output a candidate for O_1, D, O_2\n",
+ "def ODO_decomp(U_target):\n",
+ " Utilde, factor = UtoSU(Tconj(U_target))\n",
+ " O1, D2 = O1D2(UUT(Utilde))\n",
+ " D1 = sqrtm(D2)\n",
+ " O2 = inv(O1@D1)@Utilde\n",
+ " if det(O2) != 1:\n",
+ " D1 = np.diag([-1,1,1,1])@D1\n",
+ " O2 = np.diag([-1,1,1,1])@O2\n",
+ " return O1, D1, O2, factor\n",
+ "\n",
+ "def KAK_decomp(U_target):\n",
+ " O1, D, O2 , factor = ODO_decomp(U_target)\n",
+ " return Tconj(O1,d = 'r'), Tconj(D,d = 'r'), Tconj(O2,d = 'r'), factor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17555ade",
+ "metadata": {},
+ "source": [
+ "From $A\\in\\exp(\\mathfrak{a})$, \n",
+ "\\begin{equation}\n",
+ " A = e^{ia} = e^{i(c_x \\sigma_x\\otimes\\sigma_x +c_y \\sigma_y\\otimes\\sigma_y + c_z \\sigma_z\\otimes\\sigma_z)}.\n",
+ "\\end{equation}\n",
+ "The minimal time is $c_x+c_y+c_z$.\n",
+ "\n",
+ "To find $c_i$, use the inner product:\n",
+ "\\begin{equation}\n",
+ " c_i = \\mathrm{Tr}[a(\\sigma_i\\otimes\\sigma_i)]/4.\n",
+ "\\end{equation}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "6d00e5e6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sigx2 = np.kron(np.array([[0,1],[1,0]]),np.array([[0,1],[1,0]]))\n",
+ "sigy2 = np.kron(np.array([[0,-1j],[1j,0]]),np.array([[0,1],[1,0]]))\n",
+ "sigz2 = np.kron(np.array([[1,0],[0,-1]]),np.array([[1,0],[0,-1]]))\n",
+ "def optimal_drift(A):\n",
+ " a = logm(A)/1j\n",
+ " c_x = np.trace(a@sigx2)/4\n",
+ " c_y = np.trace(a@sigy2)/4\n",
+ " c_z = np.trace(a@sigz2)/4\n",
+ " return c_x,c_y,c_z,np.abs(c_x)+np.abs(c_y)+np.abs(c_z)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17ccc50f",
+ "metadata": {},
+ "source": [
+ "## Example "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "32b807ad",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "mintime: 1.5707963267948961\n",
+ "drift term: (-0.7853981633974481-2.1510571102112408e-16j) for XX (-5.704771009112362e-17-5.551115123125783e-17j) for YY (0.7853981633974481-6.938893903907228e-18j) for ZZ\n",
+ "control unitary before drift: \n",
+ " [[ 0.00000000e+00+5.00000000e-01j -2.39066301e-01-9.80444928e-18j\n",
+ " 2.39066301e-01+9.80444928e-18j 0.00000000e+00+5.00000000e-01j]\n",
+ " [ 2.56291923e-17+5.00000000e-01j 5.00000000e-01-1.18350172e-17j\n",
+ " 5.00000000e-01+1.18350172e-17j 2.98819589e-17-5.00000000e-01j]\n",
+ " [-2.98819589e-17-5.00000000e-01j 5.00000000e-01+1.18350172e-17j\n",
+ " 5.00000000e-01-1.18350172e-17j -2.56291923e-17+5.00000000e-01j]\n",
+ " [ 0.00000000e+00-5.00000000e-01j -6.65467733e-01+9.80444928e-18j\n",
+ " 6.65467733e-01-9.80444928e-18j 0.00000000e+00-5.00000000e-01j]]\n",
+ "control unitary after drift: \n",
+ " [[ 7.35702260e-01-2.77555756e-17j 5.55111512e-17-5.00000000e-01j\n",
+ " -5.55111512e-17+5.00000000e-01j 2.64297740e-01-2.77555756e-17j]\n",
+ " [-5.42591491e-50-5.52770798e-01j -5.00000000e-01+2.12638329e-18j\n",
+ " -5.00000000e-01-2.12638329e-18j -7.40335569e-50+5.52770798e-01j]\n",
+ " [-5.42591491e-50+5.52770798e-01j -5.00000000e-01+2.12638329e-18j\n",
+ " -5.00000000e-01-2.12638329e-18j -7.40335569e-50-5.52770798e-01j]\n",
+ " [ 7.35702260e-01+2.77555756e-17j -5.55111512e-17+5.00000000e-01j\n",
+ " 5.55111512e-17-5.00000000e-01j 2.64297740e-01+2.77555756e-17j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "CZ = np.diag([1,1,1,-1])\n",
+ "K1, A, K2, factor = KAK_decomp(CZ)\n",
+ "cx, cy, cz, tmin = optimal_drift(A)\n",
+ "print('mintime: ',tmin)\n",
+ "print('drift term:', cx, ' for XX', cy, ' for YY', cz,' for ZZ')\n",
+ "print('control unitary before drift: \\n', K1)\n",
+ "print('control unitary after drift: \\n', K2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "b5e0c182",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "mintime: 0.7853981633974483\n",
+ "drift term: (-0.7853981633974482+4.163336342344337e-17j) for XX (6.938893903907228e-17-1.1102230246251565e-16j) for YY -2.7755575615628907e-17j for ZZ\n",
+ "control unitary before drift: \n",
+ " [[ 1.66533454e-16+1.12589471e-16j 1.11022302e-16+1.83284050e-17j\n",
+ " 7.07106781e-01-2.41867861e-17j -7.07106781e-01-4.80856021e-17j]\n",
+ " [ 5.55111512e-17-2.38752869e-18j 0.00000000e+00+5.47247384e-18j\n",
+ " -7.07106781e-01-9.42717060e-18j -7.07106781e-01+1.56716807e-18j]\n",
+ " [ 7.07106781e-01+1.56716807e-18j -7.07106781e-01+9.42717060e-18j\n",
+ " 0.00000000e+00-5.47247384e-18j -5.55111512e-17-2.38752869e-18j]\n",
+ " [-7.07106781e-01-6.29367004e-17j -7.07106781e-01+3.13243651e-17j\n",
+ " -1.11022302e-16-3.71827462e-17j 1.66533454e-16+1.09455134e-16j]]\n",
+ "control unitary after drift: \n",
+ " [[ 5.34680621e-17-5.55111512e-17j 6.73458499e-17+0.00000000e+00j\n",
+ " 2.80935384e-17-7.07106781e-01j -9.45364669e-18+7.07106781e-01j]\n",
+ " [-7.07106781e-01-1.90980165e-17j 7.07106781e-01+2.24653325e-17j\n",
+ " -1.11022302e-16-1.41617055e-16j 0.00000000e+00-1.09685966e-17j]\n",
+ " [ 1.11022302e-16+1.55564857e-16j 0.00000000e+00-2.49163989e-17j\n",
+ " 7.07106781e-01+3.30458188e-17j 7.07106781e-01+3.64131347e-17j]\n",
+ " [-9.45364669e-18-7.07106781e-01j 2.74176128e-17-7.07106781e-01j\n",
+ " 9.91876038e-17-1.66533454e-16j -5.75542404e-17+5.55111512e-17j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "CNOT = np.array([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]])\n",
+ "K1, A, K2, factor = KAK_decomp(CNOT)\n",
+ "cx, cy, cz, tmin = optimal_drift(A)\n",
+ "print('mintime: ',tmin)\n",
+ "print('drift term:', cx, ' for XX', cy, ' for YY', cz,' for ZZ')\n",
+ "print('control unitary before drift: \\n', K1)\n",
+ "print('control unitary after drift: \\n', K2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "caabdb66",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "mintime: 1.570796326794897\n",
+ "drift term: (0.7853981633974485-1.3877787807814457e-17j) for XX (-9.71445146547012e-17+5.551115123125783e-17j) for YY (-0.7853981633974484-3.122502256758253e-17j) for ZZ\n",
+ "control unitary before drift: \n",
+ " [[-0.20412415+0.59229013j -0.63784254+0.01442622j 0.63784254-0.01442622j\n",
+ " 0.20412415+0.18404184j]\n",
+ " [-0.09158 +0.10668525j 0.55901586-0.03016342j 0.44098414+0.03016342j\n",
+ " -0.09158 -0.70981133j]\n",
+ " [ 0.09158 -0.10668525j 0.44098414+0.03016342j 0.55901586-0.03016342j\n",
+ " 0.09158 +0.70981133j]\n",
+ " [ 0.20412415-0.59229013j -0.28975034-0.01442622j 0.28975034+0.01442622j\n",
+ " -0.20412415-0.18404184j]]\n",
+ "control unitary after drift: \n",
+ " [[-0.15728869-4.49765972e-01j 0.24062072+1.49535502e-01j\n",
+ " -0.24062072-1.49535502e-01j 0.28714375+9.57149330e-01j]\n",
+ " [-0.53902958+6.63513899e-19j -0.5 +9.57738001e-20j\n",
+ " -0.5 -9.57738001e-20j -0.53902958-1.28101650e-18j]\n",
+ " [ 0.53902958+6.63513899e-19j -0.5 +9.57738001e-20j\n",
+ " -0.5 -9.57738001e-20j 0.53902958-1.28101650e-18j]\n",
+ " [ 0.07866646+1.18890313e-02j 0.09919936-6.38093565e-01j\n",
+ " -0.09919936+6.38093565e-01j -0.06829 +2.61730013e-01j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "SWAP = np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])\n",
+ "K1, A, K2, factor = KAK_decomp(SWAP)\n",
+ "cx, cy, cz, tmin = optimal_drift(A)\n",
+ "print('mintime: ',tmin)\n",
+ "print('drift term:', cx, ' for XX', cy, ' for YY', cz,' for ZZ')\n",
+ "print('control unitary before drift: \\n', K1)\n",
+ "print('control unitary after drift: \\n', K2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "ac2e443a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "[Qibo 0.2.9|INFO|2024-07-11 14:05:15]: Using qibojit (numba) backend on /CPU:0\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "mintime: 0.7853981633974483\n",
+ "drift term: (-0.7853981633974482+4.163336342344337e-17j) for XX (6.938893903907228e-17-1.1102230246251565e-16j) for YY -2.7755575615628907e-17j for ZZ\n",
+ "control unitary before drift: \n",
+ " [[ 1.66533454e-16+1.12589471e-16j 1.11022302e-16+1.83284050e-17j\n",
+ " 7.07106781e-01-2.41867861e-17j -7.07106781e-01-4.80856021e-17j]\n",
+ " [ 5.55111512e-17-2.38752869e-18j 0.00000000e+00+5.47247384e-18j\n",
+ " -7.07106781e-01-9.42717060e-18j -7.07106781e-01+1.56716807e-18j]\n",
+ " [ 7.07106781e-01+1.56716807e-18j -7.07106781e-01+9.42717060e-18j\n",
+ " 0.00000000e+00-5.47247384e-18j -5.55111512e-17-2.38752869e-18j]\n",
+ " [-7.07106781e-01-6.29367004e-17j -7.07106781e-01+3.13243651e-17j\n",
+ " -1.11022302e-16-3.71827462e-17j 1.66533454e-16+1.09455134e-16j]]\n",
+ "control unitary after drift: \n",
+ " [[ 5.34680621e-17-5.55111512e-17j 6.73458499e-17+0.00000000e+00j\n",
+ " 2.80935384e-17-7.07106781e-01j -9.45364669e-18+7.07106781e-01j]\n",
+ " [-7.07106781e-01-1.90980165e-17j 7.07106781e-01+2.24653325e-17j\n",
+ " -1.11022302e-16-1.41617055e-16j 0.00000000e+00-1.09685966e-17j]\n",
+ " [ 1.11022302e-16+1.55564857e-16j 0.00000000e+00-2.49163989e-17j\n",
+ " 7.07106781e-01+3.30458188e-17j 7.07106781e-01+3.64131347e-17j]\n",
+ " [-9.45364669e-18-7.07106781e-01j 2.74176128e-17-7.07106781e-01j\n",
+ " 9.91876038e-17-1.66533454e-16j -5.75542404e-17+5.55111512e-17j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "from qibo.hamiltonians import SymbolicHamiltonian\n",
+ "from qibo.symbols import *\n",
+ "H = SymbolicHamiltonian( X(1)*X(2) + Y(1)*Y(2)+Z(1)*Z(2)+Z(1))\n",
+ "K1, A, K2, factor = KAK_decomp(CNOT)\n",
+ "cx, cy, cz, tmin = optimal_drift(A)\n",
+ "print('mintime: ',tmin)\n",
+ "print('drift term:', cx, ' for XX', cy, ' for YY', cz,' for ZZ')\n",
+ "print('control unitary before drift: \\n', K1)\n",
+ "print('control unitary after drift: \\n', K2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 59,
+ "id": "9e57a3c2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[ 0.70710678 0. 0.70710678 0. ]\n",
+ " [ 0. -0.70710678 0. 0.70710678]\n",
+ " [ 0.70710678 0. -0.70710678 0. ]\n",
+ " [ 0. 0.70710678 0. 0.70710678]]\n",
+ "[[ 0.07059289+0.99500417j 0. +0.j 0.07059289+0.j\n",
+ " 0. +0.j ]\n",
+ " [ 0. +0.j -0.07059289+0.99500417j 0. +0.j\n",
+ " 0.07059289+0.j ]\n",
+ " [ 0.07059289+0.j 0. +0.j -0.07059289+0.99500417j\n",
+ " 0. +0.j ]\n",
+ " [ 0. +0.j 0.07059289+0.j 0. +0.j\n",
+ " 0.07059289+0.99500417j]]\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "execution_count": 59,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from qibo.transpiler.unitary_decompositions import two_qubit_decomposition,cnot_decomposition\n",
+ "\n",
+ "Z = np.array([[1,0],[0,-1]])\n",
+ "X = np.array([[0,1],[1,0]])\n",
+ "id = np.array([[1,0],[0,1]])\n",
+ "\n",
+ "operator = 1/np.sqrt(2) * (np.kron(Z,Z)+np.kron(X,id))\n",
+ "print(operator)\n",
+ "two_qubit_decomposition(0,1,operator)\n",
+ "import scipy\n",
+ "u = 1j* scipy.linalg.expm( -1j * 0.1 * operator)\n",
+ "print(u)\n",
+ "\n",
+ "two_qubit_decomposition(0,1,u)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "id": "24ff0a60",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[0 0 0 0]\n",
+ " [0 0 2 0]\n",
+ " [0 2 0 0]\n",
+ " [0 0 0 0]]\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "[,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ,\n",
+ " ]"
+ ]
+ },
+ "execution_count": 72,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#D = np.outer(rho_A,rho_A)\n",
+ "state_01 = np.array([0,1,0,0])\n",
+ "state_10 = np.array([0,0,1,0])\n",
+ "operator_0 = np.outer(state_10,state_01)\n",
+ "operator_1 = np.outer(state_01,state_10)\n",
+ "N = 2*operator_0 + 2*operator_1\n",
+ "print(N)\n",
+ "t = 0.1\n",
+ "N_unitary = scipy.linalg.expm(\n",
+ " -1j * N * t\n",
+ ")\n",
+ "two_qubit_decomposition(0,1,unitary=N_unitary)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d37beb12",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "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.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/transpiler/KAK_decomposition.py b/examples/transpiler/KAK_decomposition.py
new file mode 100644
index 000000000..d15bde8d7
--- /dev/null
+++ b/examples/transpiler/KAK_decomposition.py
@@ -0,0 +1,236 @@
+import numpy as np
+
+
+def orth_decomp_of_unitary(X):
+ """Decomposes unitary X = Ql @ exp(i Theta) @ Qr'
+ into orthogonal matrices Ql, Qr and angles Theta.
+
+ """
+
+ # one can also directly decompose XX^T and infer Ql from Qr. This might save about 3 eigh / qrs
+ # I will here try to implement something where I understand the proof, why it works.
+ # The benefit here is that we reliably work with real orthogonal matrices all the time.
+ Xr = X.real
+ Xi = X.imag
+ _, Ql = np.linalg.eigh(Xr @ Xi.transpose())
+ _, Qr = np.linalg.eigh(Xr.transpose() @ Xi)
+
+ Dr = Ql.transpose() @ Xr @ Qr
+ # if Xi injective this is diagonal, if not there is an arbitrary SO(dim ker(xi)) too much
+ # fixing the kernels, I don't know if there is a smarted way to do this!
+ if not np.allclose(Dr, np.diag(np.diag(Dr))):
+ Q, R = np.linalg.qr(Dr)
+ Dr = np.diag(R).copy()
+ Ql = Ql @ Q
+ else:
+ Dr = np.diag(Dr).copy()
+
+ Di = Ql.transpose() @ Xi @ Qr
+ # if Xr injective this is diagonal, if not there is an arbitrary SO(dim ker(Xr)) too much
+ if not np.allclose(Di, np.diag(np.diag(Di))):
+ Q, R = np.linalg.qr(Di.T)
+ Di = np.diag(R).copy()
+ Qr = Qr @ Q
+
+ else:
+ Di = np.diag(Di).copy()
+
+ # ensure Ql, Qr are in SO
+ if np.linalg.det(Ql) < 0:
+ Ql[:, 0] = -Ql[:, 0]
+ Dr[0] = -Dr[0]
+ Di[0] = -Di[0]
+
+ if np.linalg.det(Qr) < 0:
+ Qr[:, 0] = -Qr[:, 0]
+ Dr[0] = -Dr[0]
+ Di[0] = -Di[0]
+
+ Theta = np.angle(Dr + 1j * Di)
+
+ return Theta, Qr, Ql
+
+
+def unit_kronecker_rank_approx(X, verbose=True):
+ """Approximates a n^2 x m^2 matrix X as A otimes B"""
+ # better check for square dimensions
+
+ n, m = np.sqrt(X.shape).astype(int)
+
+ Y = X.reshape(n, n, m, m).transpose((0, 2, 1, 3)).reshape(n * m, n * m)
+ U, S, Vh = np.linalg.svd(Y)
+
+ if verbose:
+ print("Truncating the spectrum", S)
+
+ U = U @ np.sqrt(np.diag(S))
+ Vh = np.sqrt(np.diag(S)) @ Vh
+ A = U[:, 0].reshape(n, m)
+ B = Vh[0, :].reshape(n, m)
+ return A, B
+
+
+MAGIC_BASIS = np.array(
+ [[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]
+) / np.sqrt(2)
+
+HADAMARD = np.array([[1, 1, -1, 1], [1, 1, 1, -1], [1, -1, -1, -1], [1, -1, 1, 1]]) / 2
+
+
+def KAK_decomp(U):
+ """Calculates the KAK decomposition of U in U(4).
+ U = np.kron(A0, A1) @ heisenberg_unitary(K) @ np.kron(B0.conj().T, B1.conj().T)
+ """
+
+ Theta, Qr, Ql = orth_decomp_of_unitary(MAGIC_BASIS.conj().T @ U @ MAGIC_BASIS)
+ A0, A1 = unit_kronecker_rank_approx(MAGIC_BASIS @ Ql @ MAGIC_BASIS.conj().T)
+ B0, B1 = unit_kronecker_rank_approx(MAGIC_BASIS @ Qr @ MAGIC_BASIS.conj().T)
+ K = HADAMARD.T @ Theta / 2
+
+ print("Theta", Theta)
+ print("K", K)
+
+ # Make A0, A1, B0, B1 special
+ def make_special(X):
+ return np.sqrt(np.linalg.det(X).conj()) * X
+
+ A0, A1, B0, B1 = map(make_special, (A0, A1, B0, B1))
+
+ return A0, A1, K, B0, B1
+
+
+# ----
+# Tests
+# ----
+
+
+# some convenient functions and constants
+def haar_rand_unitary(dim, real=False):
+ """Returns a random unitary dim x dim matrix drawn from the Haar measure.
+
+ Args:
+ dim (int): the dimension of the unitary
+ real (boolean): If true returns an orthogonal matrix.
+
+ Returns:
+ numpy.array: random unitary matrix
+ """
+
+ # Draw a random Gaussian
+ gaussianMatrix = np.random.randn(dim, dim)
+ if not real: # Add a random imaginary part
+ gaussianMatrix = gaussianMatrix + 1j * np.random.randn(dim, dim)
+
+ # project to Haar random unitary
+ Q, R = np.linalg.qr(gaussianMatrix)
+ D = np.diag(R)
+ D = D / np.abs(D)
+ R = np.diag(D)
+ Q = Q.dot(R)
+
+ return Q
+
+
+def is_unitary(X, special=False, eps=1e-8):
+ is_special = np.abs(np.linalg.det(X) - 1) < 1e-8 if special else True
+ return is_special and np.linalg.norm(X @ X.conj().T - np.eye(X.shape[0])) < eps
+
+
+PAULI_X = np.array([[0, 1], [1, 0]])
+PAULI_Y = np.array([[0, -1j], [1j, 0]])
+PAULI_Z = np.array([[1, 0], [0, -1]])
+
+PXX = np.kron(PAULI_X, PAULI_X)
+PYY = np.kron(PAULI_Y, PAULI_Y)
+PZZ = np.kron(PAULI_Z, PAULI_Z)
+
+
+def heisenberg_unitary(k):
+ from scipy.linalg import expm
+
+ H = k[0] * np.eye(4) + k[1] * PXX + k[2] * PYY + k[3] * PZZ
+ return expm(1j * H)
+
+
+# test low_kronecker_rank_approx
+
+n, m = 2, 3
+
+A = np.random.randn(n, m)
+B = np.random.randn(n, m)
+
+C = np.kron(A, B)
+
+Arec, Brec = unit_kronecker_rank_approx(C)
+
+assert np.linalg.norm(C - np.kron(Arec, Brec)) < 1e-8
+
+
+# test orth_decomp_of_unitary
+dim = 10
+
+X = haar_rand_unitary(dim)
+
+
+def test_orth_decomp_of_unitary(X):
+
+ Theta, Qr, Ql = orth_decomp_of_unitary(X)
+ assert np.linalg.norm(Ql @ np.diag(np.exp(1j * Theta)) @ Qr.T - X) < 1e-8
+
+
+test_orth_decomp_of_unitary(X)
+
+# test KAK_decomp(U)
+
+
+def test_KAK_decomposition(U):
+ A0, A1, K, B0, B1 = KAK_decomp(U)
+
+ # Correctly decomposed
+ assert (
+ np.linalg.norm(
+ np.kron(A0, A1) @ heisenberg_unitary(K) @ np.kron(B0.conj().T, B1.conj().T)
+ - U
+ )
+ < 1e-8
+ )
+
+ # A0, A1, B0, B1 are in SU(2)
+ assert is_unitary(A0, special=True)
+ assert is_unitary(A1, special=True)
+ assert is_unitary(B0, special=True)
+ assert is_unitary(B1, special=True)
+
+
+# test KAK_decomp of random unitary
+
+test_KAK_decomposition(haar_rand_unitary(4))
+
+# test on Marek's example
+from scipy.linalg import expm
+
+H = 0.1 * (
+ PXX + PYY + 0.5 * (np.kron(PAULI_Z, np.eye(2)) + np.kron(np.eye(2), PAULI_Z))
+)
+U = expm(1j * H)
+UM = MAGIC_BASIS.conj().T @ U @ MAGIC_BASIS
+assert is_unitary(U)
+assert is_unitary(UM)
+
+test_orth_decomp_of_unitary(UM)
+
+test_KAK_decomposition(U)
+
+H2 = 0.1 * (
+ PXX + PZZ + PYY + (np.kron(PAULI_Z, np.eye(2)) + np.kron(np.eye(2), PAULI_Z))
+)
+U2 = expm(1j * H2)
+UM2 = MAGIC_BASIS.conj().T @ U2 @ MAGIC_BASIS
+test_orth_decomp_of_unitary(UM2)
+
+test_KAK_decomposition(expm(1j * H2))
+
+# test Heisenberg type Hamiltonian
+
+K = [0.1, 0.3, 0.6, 0.2]
+test_KAK_decomposition(heisenberg_unitary(K))
diff --git a/examples/transpiler/foo b/examples/transpiler/foo
new file mode 100644
index 000000000..e69de29bb
diff --git a/examples/unary/README.md b/examples/unary/README.md
new file mode 100644
index 000000000..5ad15c642
--- /dev/null
+++ b/examples/unary/README.md
@@ -0,0 +1,81 @@
+# Quantum unary approach to option pricing
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/unary](https://github.com/qiboteam/qibo/tree/master/examples/unary)
+
+Based in the paper [arXiv:1912.01618](https://arxiv.org/abs/1912.01618). In this `README.md` file you can see a short
+set of instructions for the usage of the example.
+
+#### What this example does?
+
+This example provides a new strategy to compute the expected payoff of a given (European) option using quantum computing.
+It is based on three previous results:
+- the Black-Scholes model, a classical model that models the behavior of an asset in the market
+- Amplitude Estimation, a quantum procedure to estimate the probability of obtaining some outcomes from a quantum circuit,
+providing quantum advantage
+- option pricing in quantum computers, a previous paper [arXiv:1905.02666](https://arxiv.org/abs/1905.02666)
+
+The main feature of this procedure is to use the unary encoding of prices, that is, we work in the subspace of the
+Hilbert space where only one qubit is in the |1> state. This allows for a simplification of the circuit and resilience
+against errors, what makes the algorithm more suitable for NISQ devices. In contradistinction, it has an exponential
+overhead in the number of qubits as compared to the standard approaches. That makes our algorithm more efficient only when the problem is not
+very large.
+
+The algorithm is divided in three steps. First, the probability distribution as computed in the Black-Scholes model is
+loaded into the quantum circuit by means of an exact circuit using *iSWAP* gates. Then, the payoff is computed by applying
+some controlled rotations on an ancilla qubit. Amplitude Estimation can be applied if the algorithm is applied and reversed
+together with some extra operations to achieve quantum advantage.
+
+#### Usage
+In this example there are only three files
+- `aux_functions.py` contains the classical functions that are needed to run the experiment.
+- `functions.py` encodes all quantum circuits and procedures needed to run the circuits.
+- `main.py` is the file calling all other functions. The action of every line of code is commented in the source code.
+
+The parameters to be fixed for a run of the experiment are
+- `S0`: initial price of the asset
+- `K`: strike of the European option
+- `sig`: volatility of the asset
+- `r`: interest rate of the asset
+- `T`: maturity date
+- `shots`: number of times the quantum experiments are ran
+- `bins`: number of bins (qubits) used for the experiment
+- `max_m`: number of applications of the Q operator in Amplitude Estimation for the last step, increasing linearly (see
+the original paper for more details)
+
+#### Results
+
+Three different results are presented in this example. First, it is shown how the amplitude distributor fits the predictions
+of the Black-Scholes model. As more qubits are added to the simulation, the approximation gets closer to the classical prediction.
+
+![prob](images/Probability_distribution.png)
+
+The second result is printed in the console after every run of the example. It returns the exact classical value of the
+expected payoff, the expected payoff returned in the quantum experiment and the percentage error.
+```bash
+Exact value of the expected payoff: 0.15950474339651186
+
+Expected payoff from quantum simulation: 0.16071698625159084
+
+Percentage error: 0.7600042664972508 %
+```
+
+Finally, results in amplitude estimation are provided.
+
+First, it is clear to see how the expected results converge to the
+optimal quantum result. Notice that the optimal quantum result is not exactly the exact classical value. This is due to
+discretization errors.
+
+![amplitude](images/Amplitude_Estimation_Results.png)
+
+Second, it is worthy to study how the uncertainties in the outcomes evolve with the number of Amplitude Estimation
+iterations. Here it is depicted how these uncertainties are always lower than the classical standard Monte Carlo ones, but
+larger than the optimal quantum uncertainties. The reason is that the depth of the circuit is controlled to increase
+linearly, in order to control de errors in the circuit.
+
+![amplitude2](images/Amplitude_Estimation_Uncertainties.png)
+
+The ultimate strength of this algorithm is its resilience to errors in the circuit. The figure below describes the errors
+obtained for our algorithm (left) as compared to the method proposed in [arXiv:1905.02666](https://arxiv.org/abs/1905.02666) (right)
+when noise is considered for 8 bins.
+
+![AE](images/AE.png)
diff --git a/examples/unary/aux_functions.py b/examples/unary/aux_functions.py
new file mode 100644
index 000000000..f424d30df
--- /dev/null
+++ b/examples/unary/aux_functions.py
@@ -0,0 +1,98 @@
+import numpy as np
+from scipy.special import erfinv
+
+
+def log_normal(x, mu, sig):
+ """Lognormal probability distribution function normalized for representation in finite intervals.
+ Args:
+ x (np.array): variable of the probability distribution.
+ mu (real): mean of the lognormal distribution.
+ sig (real): sigma of the lognormal distribution.
+
+ Returns:
+ f (np.array): normalized probability distribution for the intervals of x.
+ """
+ dx = x[1] - x[0]
+ log_norm = (
+ 1
+ / (x * sig * np.sqrt(2 * np.pi))
+ * np.exp(-np.power(np.log(x) - mu, 2.0) / (2 * np.power(sig, 2.0)))
+ )
+ f = log_norm * dx / (np.sum(log_norm * dx))
+ return f
+
+
+def classical_payoff(S0, sig, r, T, K, samples=1000000):
+ """Function computing the payoff classically given some data.
+ Args:
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+ K (real): strike price.
+ samples (int): total precision of the classical calculation.
+
+ Returns:
+ cl_payoff (real): classically computed payoff.
+ """
+ mu = (r - 0.5 * sig**2) * T + np.log(
+ S0
+ ) # Define all the parameters to be used in the computation
+ mean = np.exp(
+ mu + 0.5 * T * sig**2
+ ) # Set the relevant zone of study and create the mapping between qubit and option price, and
+ # generate the target lognormal distribution within the interval
+ variance = (np.exp(T * sig**2) - 1) * np.exp(2 * mu + T * sig**2)
+ Sp = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), samples
+ )
+ lnp = log_normal(Sp, mu, sig * np.sqrt(T))
+ cl_payoff = 0
+ for i in range(len(Sp)):
+ if K < Sp[i]:
+ cl_payoff += lnp[i] * (Sp[i] - K)
+ return cl_payoff
+
+
+def get_theta(m_s, ones_s, zeroes_s, alpha=0.05):
+ """Function to extract measurement values of a in an iterative AE algorithm.
+ Args:
+ m_s (list): set of m to be used.
+ ones_s (list): Number of outcomes with 1, as many as m_s.
+ zeroes_s (real): Number of outcomes with 0, as many as m_s.
+ alpha (real): Confidence level.
+
+ Returns:
+ theta_s (list): results for the angle estimation
+ err_theta_s (list): errors on the angle estimation
+ """
+ z = erfinv(1 - alpha / 2)
+ ones_s = np.array(ones_s)
+ zeroes_s = np.array(zeroes_s)
+ valid_s = ones_s + zeroes_s
+ a_s = ones_s / valid_s
+ theta_s = np.zeros(len(m_s))
+ err_theta_s = np.zeros(len(m_s))
+ if m_s[0] == 0:
+ theta_s[0] = np.arcsin(np.sqrt(a_s[0]))
+ err_theta_s[0] = z / 2 / np.sqrt(valid_s[0])
+ else:
+ raise ValueError("AE does not start with m=0")
+ for j, m in enumerate(m_s[1:]):
+ aux_theta = np.arcsin(np.sqrt(a_s[j + 1]))
+ theta = (
+ [aux_theta]
+ + [np.pi * k + aux_theta for k in range(1, m + 1, 1)]
+ + [np.pi * k - aux_theta for k in range(1, m + 1, 1)]
+ )
+ theta = np.array(theta) / (2 * m + 1)
+ arg = np.argmin(np.abs(theta - theta_s[j]))
+ new_theta = theta[arg]
+ new_error_theta = z / 2 / np.sqrt(valid_s[j + 1]) / (2 * m + 1)
+ theta_s[j + 1] = (
+ theta_s[j] / err_theta_s[j] ** 2 + new_theta / new_error_theta**2
+ ) / (1 / err_theta_s[j] ** 2 + 1 / new_error_theta**2)
+ err_theta_s[j + 1] = (1 / err_theta_s[j] ** 2 + 1 / new_error_theta**2) ** (
+ -1 / 2
+ )
+ return theta_s, err_theta_s
diff --git a/examples/unary/functions.py b/examples/unary/functions.py
new file mode 100644
index 000000000..81ce0289a
--- /dev/null
+++ b/examples/unary/functions.py
@@ -0,0 +1,586 @@
+import aux_functions as aux
+import matplotlib.pyplot as plt
+import numpy as np
+
+from qibo import Circuit, gates
+
+
+def rw_circuit(qubits, parameters, X=True):
+ """Circuit that implements the amplitude distributor part of the option pricing algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ paramters (list): values to be introduces into the fSim gates for amplitude distribution.
+ X (bool): whether or not the first X gate is executed.
+
+ Returns:
+ generator that yield the gates needed for the amplitude distributor circuit
+ """
+ if qubits % 2 == 0:
+ mid1 = int(qubits / 2)
+ mid0 = int(mid1 - 1)
+ if X:
+ yield gates.X(mid1)
+ yield gates.fSim(mid1, mid0, parameters[mid0] / 2, 0)
+ for i in range(mid0):
+ yield gates.fSim(mid0 - i, mid0 - i - 1, parameters[mid0 - i - 1] / 2, 0)
+ yield gates.fSim(mid1 + i, mid1 + i + 1, parameters[mid1 + i] / 2, 0)
+ else:
+ mid = int((qubits - 1) / 2)
+ if X:
+ yield gates.X(mid)
+ for i in range(mid):
+ yield gates.fSim(mid - i, mid - i - 1, parameters[mid - i - 1] / 2, 0)
+ yield gates.fSim(mid + i, mid + i + 1, parameters[mid + i] / 2, 0)
+
+
+def rw_circuit_inv(qubits, parameters, X=True):
+ """Circuit that implements the amplitude distributor part of the option pricing algorithm in reverse.
+ Used in the amplitude estimation part of the algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ paramters (list): values to be introduces into the fSim gates for amplitude distribution.
+ X (bool): whether or not the first X gate is executed.
+
+ Returns:
+ generator that yield the gates needed for the amplitude distributor circuit in reverse order.
+ """
+ if qubits % 2 == 0:
+ mid1 = int(qubits / 2)
+ mid0 = int(mid1 - 1)
+ for i in range(mid0 - 1, -1, -1):
+ yield gates.fSim(mid0 - i, mid0 - i - 1, -parameters[mid0 - i - 1] / 2, 0)
+ yield gates.fSim(mid1 + i, mid1 + i + 1, -parameters[mid1 + i] / 2, 0)
+ yield gates.fSim(mid1, mid0, -parameters[mid0] / 2, 0)
+ if X:
+ yield gates.X(mid1)
+ else:
+ mid = int((qubits - 1) / 2)
+ for i in range(mid - 1, -1, -1):
+ yield gates.fSim(mid + i, mid + i + 1, -parameters[mid + i] / 2, 0)
+ yield gates.fSim(mid - i, mid - i - 1, -parameters[mid - i - 1] / 2, 0)
+ if X:
+ yield gates.X(mid)
+
+
+def create_qc(qubits):
+ """Creation of the quantum circuit and registers where the circuit will be implemented.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+
+ Returns:
+ q (list): quantum register encoding the asset's price in the unary bases.
+ ancilla (int): qubit that encodes the payoff of the options.
+ circuit (Circuit): quantum circuit with enough allocated space for the algorithm to run.
+ """
+ q = [i for i in range(qubits)]
+ ancilla = qubits
+ circuit = Circuit(qubits + 1)
+ return q, ancilla, circuit
+
+
+def rw_parameters(qubits, pdf):
+ """Parameters that encode a target probability distribution into the unary basis
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ pdf (list): known probability distribution function that wants to be reproduced.
+
+ Returns:
+ paramters (list): values to be introduces into the fSim gates for amplitude distribution.
+ """
+ if qubits % 2 == 0:
+ mid = qubits // 2
+ else:
+ mid = (qubits - 1) // 2 # Important to keep track of the centre
+ last = 1
+ parameters = []
+ for i in range(mid - 1):
+ angle = 2 * np.arctan(np.sqrt(pdf[i] / (pdf[i + 1] * last)))
+ parameters.append(angle)
+ last = (
+ np.cos(angle / 2)
+ ) ** 2 # The last solution is needed to solve the next one
+ angle = 2 * np.arcsin(np.sqrt(pdf[mid - 1] / last))
+ parameters.append(angle)
+ last = (np.cos(angle / 2)) ** 2
+ for i in range(mid, qubits - 1):
+ angle = 2 * np.arccos(np.sqrt(pdf[i] / last))
+ parameters.append(angle)
+ last *= (np.sin(angle / 2)) ** 2
+ return parameters
+
+
+def measure_probability(q):
+ """Measuring gates on the unary basis qubits to check the validity of the amplitude distributor.
+ Args:
+ q (list): quantum register encoding the asset's price in the unary bases.
+
+ Returns:
+ generator that yels the measuring gates to check the probability distribution.
+ """
+ yield gates.M(
+ *q, register_name="prob"
+ ) # No measure on the ancilla qubit is necessary
+
+
+def extract_probability(qubits, counts, samples):
+ """Measuring gates on the unary basis qubits to check the validity of the amplitude distributor.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ counts (dict): times each output has been measured.
+ samples (int): number of samples for normalization.
+
+ Returns:
+ prob (list): normalized probabilities for the measured outcomes.
+ """
+ form = "{0:0%sb}" % str(qubits) # qubits?
+ prob = []
+ for i in reversed(range(qubits)):
+ prob.append(counts.get(form.format(2**i), 0) / samples)
+ return prob
+
+
+def get_pdf(qubits, S0, sig, r, T):
+ """Get a pdf to input into the quantum register from a target probability distribution.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+
+ Returns:
+ values (np.array): price values associated to the unary basis.
+ pdf (np.array): probability distribution for the asset's price evolution.
+ """
+ mu = (r - 0.5 * sig**2) * T + np.log(S0)
+ mean = np.exp(mu + 0.5 * T * sig**2)
+ variance = (np.exp(T * sig**2) - 1) * np.exp(2 * mu + T * sig**2)
+ values = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), qubits
+ )
+ pdf = aux.log_normal(values, mu, sig * np.sqrt(T))
+ return values, pdf
+
+
+def load_quantum_sim(qu, S0, sig, r, T):
+ """Get a pdf to input into the quantum register from a target probability distribution.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+
+ Returns:
+ circuit (Circuit): quantum circuit with the target probability encoded in the unary basis
+ values (np.array): price values associated to the unary basis.
+ pdf (np.array): probability distribution for the asset's price evolution.
+ """
+ (values, pdf) = get_pdf(qu, S0, sig, r, T)
+ q, ancilla, circuit = create_qc(qu)
+ lognormal_parameters = rw_parameters(
+ qu, pdf
+ ) # Solve for the parameters needed to create the target lognormal distribution
+ circuit.add(
+ rw_circuit(qu, lognormal_parameters)
+ ) # Build the probaility loading circuit with the adjusted parameters
+ circuit.add(
+ measure_probability(q)
+ ) # Circuit to test the precision of the probability loading algorithm
+ return circuit, (values, pdf)
+
+
+def run_quantum_sim(qubits, circuit, shots):
+ """Execute the quantum circuit and extract the probability of measuring each state of the unary basis
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ circuit (Circuit): quantum circuit with the target probability encoded in the unary basis.
+ shots (int): number of samples to extract from the circuit.
+
+ Returns:
+ prob_sim (list): normalized probability of each possible output in the unary basis.
+ """
+ result = circuit(nshots=shots)
+ frequencies = result.frequencies(binary=True, registers=False)
+
+ prob_sim = extract_probability(qubits, frequencies, shots)
+
+ return prob_sim
+
+
+def payoff_circuit(qubits, ancilla, K, S):
+ """Quantum circuit that encodes the expected payoff into the probability of measuring an acilla qubit.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ ancilla (int): qubit that encodes the payoff of the options.
+ K (real): strike price.
+ S (np.array): equivalent asset price for each element of the unary basis.
+
+ Returns:
+ generator that yields the gates required to encode the payoff into an ancillary qubit.
+ """
+ for i in range(qubits): # Determine the first qubit's price that
+ qK = i # surpasses the strike price
+ if K < S[i]:
+ break
+ for i in range(qK, qubits): # Control-RY rotations controled by states
+ angle = 2 * np.arcsin(
+ np.sqrt((S[i] - K) / (S[qubits - 1] - K))
+ ) # with higher value than the strike
+ yield gates.RY(ancilla, angle).controlled_by(i) # targeting the ancilla qubit
+
+
+def payoff_circuit_inv(qubits, ancilla, K, S):
+ """Quantum circuit that encodes the expected payoff into the probability of measuring an acilla qubit in reverse.
+ Circuit used in the amplitude estimation part of the algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ ancilla (int): qubit that encodes the payoff of the options.
+ K (real): strike price.
+ S (np.array): equivalent asset price for each element of the unary basis.
+
+ Returns:
+ generator that yields the gates required for the inverse of the circuit used to encode
+ the payoff into an ancillary qubit.
+ """
+ for i in range(qubits): # Determine the first qubit's price that
+ qK = i # surpasses the strike price
+ if K < S[i]:
+ break
+ for i in range(qK, qubits): # Control-RY rotations controled by states
+ angle = 2 * np.arcsin(
+ np.sqrt((S[i] - K) / (S[qubits - 1] - K))
+ ) # with higher value than the strike
+ yield gates.RY(ancilla, -angle).controlled_by(i) # targeting the ancilla qubit
+
+
+def measure_payoff(q, ancilla):
+ """Measurement gates needed to measure the expected payoff and perform post-selection
+ Args:
+ q (list): quantum register encoding the asset's price in the unary bases.
+ ancilla (int): qubit that encodes the payoff of the options.
+
+ Returns:
+ generator that yields the measurement gates to recover the expected payoff.
+ """
+ yield gates.M(*(q + [ancilla]), register_name="payoff")
+
+
+def load_payoff_quantum_sim(qubits, S0, sig, r, T, K):
+ """Measurement gates needed to measure the expected payoff and perform post-selection
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+ K (real): strike price.
+
+ Returns:
+ circuit (Circuit): full quantum circuit with the amplitude distributor and payoff estimator.
+ S (np.array): equivalent asset price for each element of the unary basis.
+ """
+ mu = (r - 0.5 * sig**2) * T + np.log(S0)
+ mean = np.exp(mu + 0.5 * T * sig**2)
+ variance = (np.exp(T * sig**2) - 1) * np.exp(2 * mu + T * sig**2)
+ S = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), qubits
+ )
+ ln = aux.log_normal(S, mu, sig * np.sqrt(T))
+ q, ancilla, circuit = create_qc(qubits)
+ lognormal_parameters = rw_parameters(qubits, ln)
+ circuit.add(rw_circuit(qubits, lognormal_parameters))
+ circuit.add(payoff_circuit(qubits, ancilla, K, S))
+ circuit.add(measure_payoff(q, ancilla))
+ return circuit, S
+
+
+def run_payoff_quantum_sim(qubits, circuit, shots, S, K):
+ """Exacute the circuit that estimates the payoff of the option in the unary representation. Includes
+ post-selection scheme.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ circuit (Circuit): full quantum circuit with the amplitude distributor and payoff estimator.
+ shots (int): number of shots to be performed
+ S (np.array): equivalent asset price for each element of the unary basis.
+ K (real): strike price.
+
+ Returns:
+ qu_payoff_sim (real): estimated payoff from the probability of the ancillary qubit.
+ """
+ job_payoff_sim = circuit(nshots=shots)
+ counts_payoff_sim = job_payoff_sim.frequencies(binary=True, registers=False)
+ ones = 0
+ zeroes = 0
+ for key in counts_payoff_sim.keys(): # Post-selection
+ unary = 0
+ for i in range(0, qubits):
+ unary += int(key[i])
+ if unary == 1:
+ if int(key[qubits]) == 0:
+ zeroes += counts_payoff_sim.get(key)
+ else:
+ ones += counts_payoff_sim.get(key)
+ qu_payoff_sim = ones * (S[qubits - 1] - K) / (ones + zeroes)
+ return qu_payoff_sim
+
+
+def diff_qu_cl(qu_payoff_sim, cl_payoff):
+ """Calculation of the error from the simulated results and the classical expeted value.
+ Args:
+ qu_payoff_sim (real): estimated payoff from the probability of the ancillary qubit.
+ cl_payoff (real): exact value computed classically.
+
+ Returns:
+ error (real): relative error between the simulated and exact result, in percentage.
+ """
+ error = 100 * np.abs(qu_payoff_sim - cl_payoff) / cl_payoff
+ return error
+
+
+def diffusion_operator(qubits):
+ """Quantum circuit that performs the diffusion operator, part of the amplitude estimation algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+
+ Returns:
+ generator that yield the necessary gates to perform the diffusion operator.
+ """
+ if qubits % 2 == 0:
+ mid = int(qubits / 2)
+ else:
+ mid = int((qubits - 1) / 2) # The random walk starts from the middle qubit
+ yield gates.X(qubits)
+ yield gates.H(qubits)
+ yield gates.CNOT(mid, qubits)
+ yield gates.H(qubits)
+ yield gates.X(qubits)
+
+
+def oracle_operator(qubits):
+ """Quantum circuit that performs the oracle operator, part of the amplitude estimation algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+
+ Returns:
+ generator that yield the necessary gates to perform the oracke operator.
+ """
+ yield gates.Z(qubits)
+
+
+def Q(qubits, ancilla, K, S, lognormal_parameters):
+ """Quantum circuit that performs the main operator for the amplitude estimation algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ ancilla (int): qubit that encodes the payoff of the options.
+ K (real): strike price.
+ S (np.array): equivalent asset price for each element of the unary basis.
+ lognormal_parameters (list): values to be introduces into the fSim gates for amplitude distribution.
+
+ Returns:
+ generator that yield the necessary gates to perform the main operator for AE.
+ """
+ yield oracle_operator(qubits)
+ yield payoff_circuit_inv(qubits, ancilla, K, S)
+ yield rw_circuit_inv(qubits, lognormal_parameters, X=False)
+ yield diffusion_operator(qubits)
+ yield rw_circuit(qubits, lognormal_parameters, X=False)
+ yield payoff_circuit(qubits, ancilla, K, S)
+
+
+def load_Q_operator(qubits, iterations, S0, sig, r, T, K):
+ """Quantum circuit that performs the main operator for the amplitude estimation algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ iterations (int): number of consecutive implementations of operator Q.
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+ K (real): strike price.
+
+ Returns:
+ circuit (Circuit): quantum circuit that performs the m=iterations step of the iterative
+ amplitude estimation algorithm.
+ """
+ iterations = int(iterations)
+ mu = (r - 0.5 * sig**2) * T + np.log(S0)
+ mean = np.exp(mu + 0.5 * T * sig**2)
+ variance = (np.exp(T * sig**2) - 1) * np.exp(2 * mu + T * sig**2)
+ S = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), qubits
+ )
+ ln = aux.log_normal(S, mu, sig * np.sqrt(T))
+ lognormal_parameters = rw_parameters(qubits, ln)
+ q, ancilla, circuit = create_qc(qubits)
+ circuit.add(rw_circuit(qubits, lognormal_parameters))
+ circuit.add(payoff_circuit(qubits, ancilla, K, S))
+ for i in range(iterations):
+ circuit.add(Q(qubits, ancilla, K, S, lognormal_parameters))
+ circuit.add(measure_payoff(q, ancilla))
+ return circuit
+
+
+def run_Q_operator(qubits, circuit, shots):
+ """Execution of the quantum circuit for a step in the used amplitude estimation algorithm.
+ Args:
+ qubits (int): number of qubits used for the unary basis.
+ circuit (Circuit): quantum circuit that performs the m=iterations step of the iterative
+ amplitude estimation algorithm.
+ shots (int): number of shots to be taken in intermediate steps of the AE algorithm.
+
+ Returns:
+ ones (int): number of measured ones after post-selection.
+ zeroes (int): number of measured zeroes after post-selection.
+ """
+ job_payoff_sim = circuit(nshots=shots)
+ counts_payoff_sim = job_payoff_sim.frequencies(binary=True, registers=False)
+ ones = 0
+ zeroes = 0
+ for key in counts_payoff_sim.keys():
+ unary = 0
+ for i in range(0, qubits):
+ unary += int(key[i])
+ if unary == 1:
+ if int(key[qubits]) == 0:
+ zeroes += counts_payoff_sim.get(key)
+ else:
+ ones += counts_payoff_sim.get(key)
+ return ones, zeroes
+
+
+def paint_prob_distribution(bins, prob_sim, S0, sig, r, T):
+ """Funtion that returns a histogram with the probabilities of the outcome measures and compares it
+ with the target probability distribution.
+ Args:
+ bins (int): number of bins of precision.
+ prob_sim (list): probabilities from measuring the quantum circuit.
+ S0 (real): initial asset price.
+ sig (real): market volatility.
+ r (real): market rate.
+ T (real): maturity time.
+
+ Returns:
+ image of the probability histogram in a .png file.
+ """
+ from scipy.integrate import trapz
+
+ mu = (r - 0.5 * sig**2) * T + np.log(S0)
+ mean = np.exp(mu + 0.5 * T * sig**2)
+ variance = (np.exp(T * sig**2) - 1) * np.exp(2 * mu + T * sig**2)
+ S = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), bins
+ )
+ width = (S[1] - S[0]) / 1.2
+ fig, ax = plt.subplots()
+ ax.bar(S, prob_sim, width, label="Quantum", alpha=0.8)
+ x = np.linspace(
+ max(mean - 3 * np.sqrt(variance), 0), mean + 3 * np.sqrt(variance), bins * 100
+ )
+ y = aux.log_normal(x, mu, sig * np.sqrt(T))
+ y = y * trapz(prob_sim, S) / trapz(y, x)
+ ax.plot(x, y, label="PDF", color="black")
+ plt.ylabel("Probability")
+ plt.xlabel("Option price")
+ plt.title(f"Option price distribution for {bins} qubits ")
+ ax.legend()
+ fig.tight_layout()
+ fig.savefig("Probability_distribution.png")
+
+
+def paint_AE(a, a_conf, bins, M, data, shots=10000, alpha=0.05):
+ """Visualization of the results of applying amplitude estimation to the option pricing algorithm.
+ Args:
+ a (np.array): estimated values for the probability of measuring the ancilla.
+ a_conf (np.array): errors on the estimation of the probability of measuring the ancilla.
+ bins (int): number of bins of precision.
+ M (int): total number of aplications of the Q operator.
+ data (tuple): data necessary to characterize the probability distribution.
+ shots (int): number of shots to be taken in intermediate steps of the AE algorithm.
+ alpha (real): confidence interval.
+
+ Returns:
+ images of the results and uncertainties of performing amplitude estimation up to M times in .png format.
+ """
+ S0, sig, r, T, K = data
+ values, pdf = get_pdf(bins, S0, sig, r, T)
+ a_un = np.sum(pdf[values >= K] * (values[values >= K] - K))
+ cl_payoff = aux.classical_payoff(S0, sig, r, T, K, samples=1000000)
+ fig, ax = plt.subplots()
+ un_data = a * (values[bins - 1] - K)
+ un_conf = a_conf * (values[bins - 1] - K)
+ ax.scatter(
+ np.arange(M + 1), un_data, c="C0", marker="x", zorder=10, label="Measurements"
+ )
+ ax.fill_between(
+ np.arange(M + 1), un_data - un_conf, un_data + un_conf, color="C0", alpha=0.3
+ )
+ ax.plot([0, M], [cl_payoff, cl_payoff], c="black", ls="--", label="Cl. payoff")
+ ax.plot([0, M], [a_un, a_un], c="blue", ls="--", label="Optimal approximation")
+ ax.set(ylim=[0.15, 0.17])
+ ax.legend()
+ fig.tight_layout()
+ fig.savefig("Amplitude_Estimation_Results.png")
+ from scipy.special import erfinv
+
+ z = erfinv(1 - alpha / 2)
+ fig, bx = plt.subplots()
+ bx.scatter(
+ np.arange(M + 1), un_conf, c="C0", marker="x", zorder=10, label="Measurements"
+ )
+ a_max = np.max(values) - K
+ bound_down = (
+ np.sqrt(un_data)
+ * np.sqrt(a_max - un_data)
+ * z
+ / np.sqrt(shots)
+ / np.cumsum(1 + 2 * (np.arange(M + 1)))
+ )
+ bound_up = (
+ np.sqrt(un_data)
+ * np.sqrt(a_max - un_data)
+ * z
+ / np.sqrt(shots)
+ / np.sqrt(np.cumsum(1 + 2 * (np.arange(M + 1))))
+ )
+ bx.plot(np.arange(M + 1), bound_up, ls=":", c="C0", label="Classical sampling")
+ bx.plot(
+ np.arange(M + 1), bound_down, ls="-.", c="C0", label="Optimal Quantum Sampling"
+ )
+ bx.legend()
+ bx.set(yscale="log")
+ fig.tight_layout()
+ fig.savefig("Amplitude_Estimation_Uncertainties.png")
+
+
+def amplitude_estimation(bins, M, data, shots=10000):
+ """Execution of the quantum circuit for a step in the used amplitude estimation algorithm.
+ Args:
+ bins (int): number of bins of precision.
+ M (int): total number of aplications of the Q operator.
+ data (tuple): data necessary to characterize the probability distribution.
+ shots (int): number of shots to be taken in intermediate steps of the AE algorithm.
+
+ Returns:
+ a_s (np.array): estimated values for the probability of measuring the ancilla.
+ error_s (np.array): errors on the estimation of the probability of measuring the ancilla.
+ """
+ S0, sig, r, T, K = data
+ circuit, S = load_payoff_quantum_sim(bins, S0, sig, r, T, K)
+ qu_payoff_sim = run_payoff_quantum_sim(bins, circuit, shots, S, K)
+ m_s = np.arange(0, M + 1, 1)
+ circuits = []
+ for j, m in enumerate(m_s):
+ qc = load_Q_operator(bins, m, S0, sig, r, T, K)
+ circuits.append(qc)
+ ones_s = []
+ zeroes_s = []
+ for j, m in enumerate(m_s):
+ ones, zeroes = run_Q_operator(bins, circuits[j], shots)
+ ones_s.append(ones)
+ zeroes_s.append(zeroes)
+ theta_max_s, error_theta_s = aux.get_theta(m_s, ones_s, zeroes_s)
+ a_s, error_s = np.sin(theta_max_s) ** 2, np.abs(
+ np.sin(2 * theta_max_s) * error_theta_s
+ )
+ return a_s, error_s
diff --git a/examples/unary/images/AE.png b/examples/unary/images/AE.png
new file mode 100644
index 000000000..7d0fe2b7b
Binary files /dev/null and b/examples/unary/images/AE.png differ
diff --git a/examples/unary/images/Amplitude_Estimation_Results.png b/examples/unary/images/Amplitude_Estimation_Results.png
new file mode 100644
index 000000000..b7b73f41b
Binary files /dev/null and b/examples/unary/images/Amplitude_Estimation_Results.png differ
diff --git a/examples/unary/images/Amplitude_Estimation_Uncertainties.png b/examples/unary/images/Amplitude_Estimation_Uncertainties.png
new file mode 100644
index 000000000..93891e3e9
Binary files /dev/null and b/examples/unary/images/Amplitude_Estimation_Uncertainties.png differ
diff --git a/examples/unary/images/Probability_distribution.png b/examples/unary/images/Probability_distribution.png
new file mode 100644
index 000000000..f591171be
Binary files /dev/null and b/examples/unary/images/Probability_distribution.png differ
diff --git a/examples/unary/main.py b/examples/unary/main.py
new file mode 100644
index 000000000..cdbf7a901
--- /dev/null
+++ b/examples/unary/main.py
@@ -0,0 +1,62 @@
+import argparse
+
+import aux_functions as aux
+import functions as fun
+
+
+def main(data, bins, M, shots):
+ S0, sig, r, T, K = data
+
+ # Create circuit to load the probability distribution
+ circuit, (values, pdf) = fun.load_quantum_sim(bins, S0, sig, r, T)
+
+ # Measure the probability distribution
+ prob_sim = fun.run_quantum_sim(bins, circuit, shots)
+
+ # Generate the probability distribution plots
+ fun.paint_prob_distribution(bins, prob_sim, S0, sig, r, T)
+ print(f"Histogram printed for unary simulation with {bins} qubits.\n")
+
+ # Create circuit to compute the expected payoff
+ circuit, S = fun.load_payoff_quantum_sim(bins, S0, sig, r, T, K)
+
+ # Run the circuit for expected payoff
+ qu_payoff_sim = fun.run_payoff_quantum_sim(bins, circuit, shots, S, K)
+
+ # Computing exact classical payoff
+ cl_payoff = aux.classical_payoff(S0, sig, r, T, K, samples=1000000)
+
+ # Finding differences between exact value and quantum approximation
+ error = fun.diff_qu_cl(qu_payoff_sim, cl_payoff)
+ print(f"Exact value of the expected payoff: {cl_payoff}\n")
+ print(f"Expected payoff from quantum simulation: {qu_payoff_sim}\n")
+ print(f"Percentage error: {error} %\n")
+ print("-" * 60 + "\n")
+
+ # Applying amplitude estimation
+ a_s, error_s = fun.amplitude_estimation(bins, M, data)
+ print(f"Amplitude estimation with a total of {M} runs.\n")
+ fun.paint_AE(a_s, error_s, bins, M, data)
+ print("Amplitude estimation result plots generated.")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--bins", default=16, type=int)
+ parser.add_argument("--M", default=10, type=int, help="Max AE runs.")
+ parser.add_argument("--S0", default=2, type=float, help="Initial value of asset.")
+ parser.add_argument("--K", default=1.9, type=float, help="Strike price.")
+ parser.add_argument("--sig", default=0.4, type=float, help="Volatility.")
+ parser.add_argument("--r", default=0.05, type=float, help="Market rate.")
+ parser.add_argument("--T", default=0.1, type=float, help="Maturity date.")
+ args = vars(parser.parse_args())
+ S0 = args.get("S0")
+ K = args.get("K")
+ sig = args.get("sig")
+ r = args.get("r")
+ T = args.get("T")
+ data = (S0, sig, r, T, K)
+ shots = 1000000
+ bins = args.get("bins")
+ max_m = args.get("M")
+ main(data, bins, max_m, shots)
diff --git a/examples/variational_classifier/README.md b/examples/variational_classifier/README.md
new file mode 100644
index 000000000..dd4532777
--- /dev/null
+++ b/examples/variational_classifier/README.md
@@ -0,0 +1,45 @@
+# Variational Quantum Classifier
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/variational_classifier](https://github.com/qiboteam/qibo/tree/master/examples/variational_classifier).
+
+## Problem overview
+
+We want to perform a supervised classification task with a [variational quantum classifer](https://arxiv.org/abs/1802.06002). The classifier is trained to minimize a local loss function given by the quadratic deviation of the classifier's predictions from the actual labels of the examples in the training set. A variational quantum circuit is employed to perform the classification.
+
+## Implementing the solution
+
+The standard [iris data set](https://archive.ics.uci.edu/ml/datasets/iris) is chosen for the classification task. It consists of 150 4-dimensional data vectors containing the length and widht of the sepals and petals of individuals from three different species of plants (*Iris setosa*, *Iris versicolor*, *Iris virginica*). We associate one computational-basis state to each of the classes (|00>, |01>, |10>) in the subspace of measured qubits, and we employ the following architecture for the ansatz:
+
+![ansatz](images/ansatz.png)
+
+where R stands for Ry rotations (if `RxRzRx=False`) or RxRzRx rotations.
+
+## How to run an example
+
+To run a particular instance of the problem, we have to set up the initial
+arguments:
+- `nclases` (int): number of classes of the training set (default=3)
+- `nqubits` (int): number of quantum bits. It must be larger than 1 (default=4)
+- `nlayers` (int): number of ansatz layers. (default=5)
+- `nshots` (int): number of shots used when sampling the circuit. (default=100000)
+- `training`(flag): if True, actual training occurs. If False, pre-computed optimal values are employed, with default nqubits and nlayers. (default=False)
+- `RxRzRx` (flag): if True, RxRzRx rotations are used in the ansatz. If False, Ry are employed instead. (default=False)
+- `method` (string): classical optimization method, supported by scipy.optimize.minimize. (default='Powell')
+
+To run an example with the optimal values obtained for 4 qubits and 5 layers, you should execute the following command:
+
+```bash
+python main.py
+```
+
+To run an example with different values, and actually train the classifier, type for example:
+
+```bash
+python main.py --nqubits 4 --nlayers 5 --nshots 100000 --training
+```
+
+Note that nclases must be 3 and cannot be changed in this example, because we are classifing the Iris data set.
+
+## Results
+
+The classification accuracy for the training and test sets is found to be around 70% and 73%, respectively.
diff --git a/examples/variational_classifier/data/iris.data b/examples/variational_classifier/data/iris.data
new file mode 100644
index 000000000..a3490e0e0
--- /dev/null
+++ b/examples/variational_classifier/data/iris.data
@@ -0,0 +1,150 @@
+5.1,3.5,1.4,0.2,Iris-setosa
+4.9,3.0,1.4,0.2,Iris-setosa
+4.7,3.2,1.3,0.2,Iris-setosa
+4.6,3.1,1.5,0.2,Iris-setosa
+5.0,3.6,1.4,0.2,Iris-setosa
+5.4,3.9,1.7,0.4,Iris-setosa
+4.6,3.4,1.4,0.3,Iris-setosa
+5.0,3.4,1.5,0.2,Iris-setosa
+4.4,2.9,1.4,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+5.4,3.7,1.5,0.2,Iris-setosa
+4.8,3.4,1.6,0.2,Iris-setosa
+4.8,3.0,1.4,0.1,Iris-setosa
+4.3,3.0,1.1,0.1,Iris-setosa
+5.8,4.0,1.2,0.2,Iris-setosa
+5.7,4.4,1.5,0.4,Iris-setosa
+5.4,3.9,1.3,0.4,Iris-setosa
+5.1,3.5,1.4,0.3,Iris-setosa
+5.7,3.8,1.7,0.3,Iris-setosa
+5.1,3.8,1.5,0.3,Iris-setosa
+5.4,3.4,1.7,0.2,Iris-setosa
+5.1,3.7,1.5,0.4,Iris-setosa
+4.6,3.6,1.0,0.2,Iris-setosa
+5.1,3.3,1.7,0.5,Iris-setosa
+4.8,3.4,1.9,0.2,Iris-setosa
+5.0,3.0,1.6,0.2,Iris-setosa
+5.0,3.4,1.6,0.4,Iris-setosa
+5.2,3.5,1.5,0.2,Iris-setosa
+5.2,3.4,1.4,0.2,Iris-setosa
+4.7,3.2,1.6,0.2,Iris-setosa
+4.8,3.1,1.6,0.2,Iris-setosa
+5.4,3.4,1.5,0.4,Iris-setosa
+5.2,4.1,1.5,0.1,Iris-setosa
+5.5,4.2,1.4,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+5.0,3.2,1.2,0.2,Iris-setosa
+5.5,3.5,1.3,0.2,Iris-setosa
+4.9,3.1,1.5,0.1,Iris-setosa
+4.4,3.0,1.3,0.2,Iris-setosa
+5.1,3.4,1.5,0.2,Iris-setosa
+5.0,3.5,1.3,0.3,Iris-setosa
+4.5,2.3,1.3,0.3,Iris-setosa
+4.4,3.2,1.3,0.2,Iris-setosa
+5.0,3.5,1.6,0.6,Iris-setosa
+5.1,3.8,1.9,0.4,Iris-setosa
+4.8,3.0,1.4,0.3,Iris-setosa
+5.1,3.8,1.6,0.2,Iris-setosa
+4.6,3.2,1.4,0.2,Iris-setosa
+5.3,3.7,1.5,0.2,Iris-setosa
+5.0,3.3,1.4,0.2,Iris-setosa
+7.0,3.2,4.7,1.4,Iris-versicolor
+6.4,3.2,4.5,1.5,Iris-versicolor
+6.9,3.1,4.9,1.5,Iris-versicolor
+5.5,2.3,4.0,1.3,Iris-versicolor
+6.5,2.8,4.6,1.5,Iris-versicolor
+5.7,2.8,4.5,1.3,Iris-versicolor
+6.3,3.3,4.7,1.6,Iris-versicolor
+4.9,2.4,3.3,1.0,Iris-versicolor
+6.6,2.9,4.6,1.3,Iris-versicolor
+5.2,2.7,3.9,1.4,Iris-versicolor
+5.0,2.0,3.5,1.0,Iris-versicolor
+5.9,3.0,4.2,1.5,Iris-versicolor
+6.0,2.2,4.0,1.0,Iris-versicolor
+6.1,2.9,4.7,1.4,Iris-versicolor
+5.6,2.9,3.6,1.3,Iris-versicolor
+6.7,3.1,4.4,1.4,Iris-versicolor
+5.6,3.0,4.5,1.5,Iris-versicolor
+5.8,2.7,4.1,1.0,Iris-versicolor
+6.2,2.2,4.5,1.5,Iris-versicolor
+5.6,2.5,3.9,1.1,Iris-versicolor
+5.9,3.2,4.8,1.8,Iris-versicolor
+6.1,2.8,4.0,1.3,Iris-versicolor
+6.3,2.5,4.9,1.5,Iris-versicolor
+6.1,2.8,4.7,1.2,Iris-versicolor
+6.4,2.9,4.3,1.3,Iris-versicolor
+6.6,3.0,4.4,1.4,Iris-versicolor
+6.8,2.8,4.8,1.4,Iris-versicolor
+6.7,3.0,5.0,1.7,Iris-versicolor
+6.0,2.9,4.5,1.5,Iris-versicolor
+5.7,2.6,3.5,1.0,Iris-versicolor
+5.5,2.4,3.8,1.1,Iris-versicolor
+5.5,2.4,3.7,1.0,Iris-versicolor
+5.8,2.7,3.9,1.2,Iris-versicolor
+6.0,2.7,5.1,1.6,Iris-versicolor
+5.4,3.0,4.5,1.5,Iris-versicolor
+6.0,3.4,4.5,1.6,Iris-versicolor
+6.7,3.1,4.7,1.5,Iris-versicolor
+6.3,2.3,4.4,1.3,Iris-versicolor
+5.6,3.0,4.1,1.3,Iris-versicolor
+5.5,2.5,4.0,1.3,Iris-versicolor
+5.5,2.6,4.4,1.2,Iris-versicolor
+6.1,3.0,4.6,1.4,Iris-versicolor
+5.8,2.6,4.0,1.2,Iris-versicolor
+5.0,2.3,3.3,1.0,Iris-versicolor
+5.6,2.7,4.2,1.3,Iris-versicolor
+5.7,3.0,4.2,1.2,Iris-versicolor
+5.7,2.9,4.2,1.3,Iris-versicolor
+6.2,2.9,4.3,1.3,Iris-versicolor
+5.1,2.5,3.0,1.1,Iris-versicolor
+5.7,2.8,4.1,1.3,Iris-versicolor
+6.3,3.3,6.0,2.5,Iris-virginica
+5.8,2.7,5.1,1.9,Iris-virginica
+7.1,3.0,5.9,2.1,Iris-virginica
+6.3,2.9,5.6,1.8,Iris-virginica
+6.5,3.0,5.8,2.2,Iris-virginica
+7.6,3.0,6.6,2.1,Iris-virginica
+4.9,2.5,4.5,1.7,Iris-virginica
+7.3,2.9,6.3,1.8,Iris-virginica
+6.7,2.5,5.8,1.8,Iris-virginica
+7.2,3.6,6.1,2.5,Iris-virginica
+6.5,3.2,5.1,2.0,Iris-virginica
+6.4,2.7,5.3,1.9,Iris-virginica
+6.8,3.0,5.5,2.1,Iris-virginica
+5.7,2.5,5.0,2.0,Iris-virginica
+5.8,2.8,5.1,2.4,Iris-virginica
+6.4,3.2,5.3,2.3,Iris-virginica
+6.5,3.0,5.5,1.8,Iris-virginica
+7.7,3.8,6.7,2.2,Iris-virginica
+7.7,2.6,6.9,2.3,Iris-virginica
+6.0,2.2,5.0,1.5,Iris-virginica
+6.9,3.2,5.7,2.3,Iris-virginica
+5.6,2.8,4.9,2.0,Iris-virginica
+7.7,2.8,6.7,2.0,Iris-virginica
+6.3,2.7,4.9,1.8,Iris-virginica
+6.7,3.3,5.7,2.1,Iris-virginica
+7.2,3.2,6.0,1.8,Iris-virginica
+6.2,2.8,4.8,1.8,Iris-virginica
+6.1,3.0,4.9,1.8,Iris-virginica
+6.4,2.8,5.6,2.1,Iris-virginica
+7.2,3.0,5.8,1.6,Iris-virginica
+7.4,2.8,6.1,1.9,Iris-virginica
+7.9,3.8,6.4,2.0,Iris-virginica
+6.4,2.8,5.6,2.2,Iris-virginica
+6.3,2.8,5.1,1.5,Iris-virginica
+6.1,2.6,5.6,1.4,Iris-virginica
+7.7,3.0,6.1,2.3,Iris-virginica
+6.3,3.4,5.6,2.4,Iris-virginica
+6.4,3.1,5.5,1.8,Iris-virginica
+6.0,3.0,4.8,1.8,Iris-virginica
+6.9,3.1,5.4,2.1,Iris-virginica
+6.7,3.1,5.6,2.4,Iris-virginica
+6.9,3.1,5.1,2.3,Iris-virginica
+5.8,2.7,5.1,1.9,Iris-virginica
+6.8,3.2,5.9,2.3,Iris-virginica
+6.7,3.3,5.7,2.5,Iris-virginica
+6.7,3.0,5.2,2.3,Iris-virginica
+6.3,2.5,5.0,1.9,Iris-virginica
+6.5,3.0,5.2,2.0,Iris-virginica
+6.2,3.4,5.4,2.3,Iris-virginica
+5.9,3.0,5.1,1.8,Iris-virginica
diff --git a/examples/variational_classifier/data/optimal_angles_ry_4q_4l.npy b/examples/variational_classifier/data/optimal_angles_ry_4q_4l.npy
new file mode 100644
index 000000000..589d17a05
Binary files /dev/null and b/examples/variational_classifier/data/optimal_angles_ry_4q_4l.npy differ
diff --git a/examples/variational_classifier/data/optimal_angles_ry_4q_5l.npy b/examples/variational_classifier/data/optimal_angles_ry_4q_5l.npy
new file mode 100644
index 000000000..386154feb
Binary files /dev/null and b/examples/variational_classifier/data/optimal_angles_ry_4q_5l.npy differ
diff --git a/examples/variational_classifier/images/ansatz.png b/examples/variational_classifier/images/ansatz.png
new file mode 100755
index 000000000..aee7e9ea1
Binary files /dev/null and b/examples/variational_classifier/images/ansatz.png differ
diff --git a/examples/variational_classifier/main.py b/examples/variational_classifier/main.py
new file mode 100644
index 000000000..20bcb5f43
--- /dev/null
+++ b/examples/variational_classifier/main.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+import argparse
+from pathlib import Path
+
+import numpy as np
+from qclassifier import QuantumClassifer
+
+LOCAL_FOLDER = Path(__file__).parent
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+ "--nclasses", default=3, help="Number of classes to be classified", type=int
+)
+parser.add_argument("--nqubits", default=4, help="Number of qubits", type=int)
+parser.add_argument(
+ "--nlayers", default=5, help="Number of layers of the variational circuit", type=int
+)
+parser.add_argument(
+ "--nshots",
+ default=int(1e5),
+ help="Number of shots used when sampling the circuit",
+ type=int,
+)
+parser.add_argument(
+ "--training",
+ action="store_true",
+ help="Train the quantum classifier or ortherwise use precomputed angles for the circuit",
+)
+parser.add_argument(
+ "--RxRzRx",
+ action="store_true",
+ help="Use Ry rotations or RxRzRx rotations in the ansatz",
+)
+parser.add_argument(
+ "--method", default="Powell", help="Classical optimizer employed", type=str
+)
+
+
+def main(nclasses, nqubits, nlayers, nshots, training, RxRzRx, method):
+ # We initialize the quantum classifier
+ RY = not RxRzRx
+ qc = QuantumClassifer(nclasses, nqubits, nlayers, RY=RY)
+
+ # We load the iris data set
+ path_data = LOCAL_FOLDER / "data" / "iris.data"
+ data = open(path_data)
+ data = data.readlines()
+ data = [i.split(",") for i in data]
+
+ for i in range(len(data)):
+ del data[i][-1] # We delete text labels from the kets
+ data[i] += [0] * (2**nqubits - 4) # We pad with zeros
+ data[i] = np.float32(data[i]) # We transform strings to floats
+ # We re-scale each feature of the data
+ data[i][0] /= 7.9
+ data[i][1] /= 4.4
+ data[i][2] /= 6.9
+ data[i][3] /= 2.5
+ data[i] = data[i] ** 2
+ data[i] /= np.linalg.norm(data[i]) # We normalize the ket
+
+ data = np.float32(np.array(data))
+
+ # We define our training set (both kets and labels)
+ data_train = np.concatenate((data[0:35], data[50:85], data[100:135]))
+ labels_train = np.array([[1, 1]] * 35 + [[1, -1]] * 35 + [[-1, 1]] * 35)
+
+ # We load pre-trained angles or actually train the Quantum_Classifer
+ if not training:
+ if RY:
+ try:
+ path_angles = (
+ LOCAL_FOLDER
+ / "data"
+ / f"optimal_angles_ry_{nqubits}q_{nlayers}l.npy"
+ )
+ optimal_angles = np.load(path_angles)
+ except:
+ raise FileNotFoundError(
+ "There are no pre-trained angles saved for this choice of nqubits, nlayers and type of ansatz."
+ )
+ else:
+ try:
+ path_angles = (
+ LOCAL_FOLDER
+ / "data"
+ / f"optimal_angles_rxrzrx_{nqubits}q_{nlayers}l.npy"
+ )
+ optimal_angles = np.load(path_angles)
+ except:
+ raise FileNotFoundError(
+ "There are no pre-trained angles saved for this choice of nqubits, nlayers and type of ansatz."
+ )
+ else:
+ # We choose initial random parameters (execpt for the biases, that we set to zero)
+ measured_qubits = int(np.ceil(np.log2(nclasses)))
+ if RY: # if Ry rotations are employed in the ansatz
+ initial_parameters = (
+ 2
+ * np.pi
+ * np.random.rand(2 * nqubits * nlayers + nqubits + measured_qubits)
+ )
+ for bias in range(measured_qubits):
+ initial_parameters[bias] = 0.0
+ print("Training classifier...")
+ cost_function, optimal_angles = qc.minimize(
+ initial_parameters,
+ data_train,
+ labels_train,
+ nshots=nshots,
+ method=method,
+ )
+ path_angles = (
+ LOCAL_FOLDER / "data" / f"optimal_angles_ry_{nqubits}q_{nlayers}l.npy"
+ )
+ np.save(
+ path_angles,
+ optimal_angles,
+ )
+ else: # if RxRzRx rotations are employed in the ansatz
+ initial_parameters = (
+ 2
+ * np.pi
+ * np.random.rand(6 * nqubits * nlayers + 3 * nqubits + measured_qubits)
+ )
+ for bias in range(measured_qubits):
+ initial_parameters[bias] = 0.0
+ print("Training classifier...")
+ cost_function, optimal_angles = qc.minimize(
+ initial_parameters,
+ data_train,
+ labels_train,
+ nshots=nshots,
+ method=method,
+ )
+ path_angles = (
+ LOCAL_FOLDER
+ / "data"
+ / f"optimal_angles_rxrzrx_{nqubits}q_{nlayers}l.npy"
+ )
+ np.save(
+ path_angles,
+ optimal_angles,
+ )
+
+ # We define our test set (both kets and labels)
+ data_test = np.concatenate((data[35:50], data[85:100], data[135:150]))
+ labels_test = [[1, 1]] * 15 + [[1, -1]] * 15 + [[-1, 1]] * 15
+
+ # We run an accuracy check for the training and the test sets
+ predictions_train = [
+ qc.Predictions(
+ qc.Classifier_circuit(optimal_angles),
+ optimal_angles,
+ init_state=ket,
+ nshots=nshots,
+ )
+ for ket in data_train
+ ]
+ predictions_test = [
+ qc.Predictions(
+ qc.Classifier_circuit(optimal_angles),
+ optimal_angles,
+ init_state=ket,
+ nshots=nshots,
+ )
+ for ket in data_test
+ ]
+
+ print(
+ f"Train set | # Clases: {nclasses} | # Qubits: {nqubits} | # Layers: {nlayers} | "
+ + f"Accuracy: {qc.Accuracy(labels_train, predictions_train)}"
+ )
+ print(
+ f"Test set | # Clases: {nclasses} | # Qubits: {nqubits} | # Layers: {nlayers} | "
+ + f"Accuracy: {qc.Accuracy(labels_test, predictions_test)}"
+ )
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/variational_classifier/qclassifier.py b/examples/variational_classifier/qclassifier.py
new file mode 100644
index 000000000..5cea93221
--- /dev/null
+++ b/examples/variational_classifier/qclassifier.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+import numpy as np
+
+import qibo
+from qibo import Circuit, gates
+
+
+class QuantumClassifer:
+ def __init__(self, nclasses, nqubits, nlayers, RY=True):
+ """
+ Class for a multi-task variational quantum classifier
+
+ Args:
+ nclases: int number of classes to be classified
+ nqubits: int number of qubits employed in the quantum circuit
+ """
+ self.nclasses = nclasses
+ self.nqubits = nqubits
+ self.measured_qubits = int(np.ceil(np.log2(self.nclasses)))
+
+ if self.nqubits <= 1:
+ raise ValueError("nqubits must be larger than 1")
+
+ if RY:
+
+ def rotations():
+ for q in range(self.nqubits):
+ yield gates.RY(q, theta=0)
+
+ else:
+
+ def rotations():
+ for q in range(self.nqubits):
+ yield gates.RX(q, theta=0)
+ yield gates.RZ(q, theta=0)
+ yield gates.RX(q, theta=0)
+
+ self._circuit = self.ansatz(nlayers, rotations)
+
+ def _CZ_gates1(self):
+ """Yields CZ gates used in the variational circuit."""
+ for q in range(0, self.nqubits - 1, 2):
+ yield gates.CZ(q, q + 1)
+
+ def _CZ_gates2(self):
+ """Yields CZ gates used in the variational circuit."""
+ for q in range(1, self.nqubits - 1, 2):
+ yield gates.CZ(q, q + 1)
+
+ yield gates.CZ(0, self.nqubits - 1)
+
+ def ansatz(self, nlayers, rotations):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ nlayers: int number of layers of the varitional circuit ansatz
+
+ Returns:
+ Circuit implementing the variational ansatz
+ """
+ c = Circuit(self.nqubits)
+ for _ in range(nlayers):
+ c.add(rotations())
+ c.add(self._CZ_gates1())
+ c.add(rotations())
+ c.add(self._CZ_gates2())
+ # Final rotations
+ c.add(rotations())
+ # Measurements
+ c.add(gates.M(*range(self.measured_qubits)))
+
+ return c
+
+ def Classifier_circuit(self, theta):
+ """
+ Args:
+ theta: list or numpy.array with the biases and the angles to be used in the circuit
+ nlayers: int number of layers of the varitional circuit ansatz
+ RY: if True, parameterized Rx,Rz,Rx gates are used in the circuit
+ if False, parameterized Ry gates are used in the circuit (default=False)
+
+ Returns:
+ Circuit implementing the variational ansatz for angles "theta"
+ """
+ bias = np.array(theta[0 : self.measured_qubits])
+ angles = theta[self.measured_qubits :]
+
+ self._circuit.set_parameters(angles)
+
+ return self._circuit
+
+ def Predictions(self, circuit, theta, init_state, nshots=10000):
+ """
+ Args:
+ theta: list or numpy.array with the biases to be used in the circuit
+ init_state: numpy.array with the quantum state to be classified
+ nshots: int number of runs of the circuit during the sampling process (default=10000)
+
+ Returns:
+ numpy.array() with predictions for each qubit, for the initial state
+ """
+ bias = np.array(theta[0 : self.measured_qubits])
+ circuit = circuit(init_state, nshots)
+ result = circuit.frequencies(binary=False)
+ prediction = np.zeros(self.measured_qubits)
+
+ for qubit in range(self.measured_qubits):
+ for clase in range(self.nclasses):
+ binary = bin(clase)[2:].zfill(self.measured_qubits)
+ prediction[qubit] += result[clase] * (1 - 2 * int(binary[-qubit - 1]))
+
+ return prediction / nshots + bias
+
+ def square_loss(self, labels, predictions):
+ """
+ Args:
+ labels: list or numpy.array with the qubit labels of the quantum states to be classified
+ predictions: list or numpy.array with the qubit predictions for the quantum states to be classified
+
+ Returns:
+ numpy.float32 with the value of the square-loss function
+ """
+ loss = 0
+ for l, p in zip(labels, predictions):
+ for qubit in range(self.measured_qubits):
+ loss += (l[qubit] - p[qubit]) ** 2
+
+ return loss / len(labels)
+
+ def Cost_function(self, theta, data=None, labels=None, nshots=10000):
+ """
+ Args:
+ theta: list or numpy.array with the biases and the angles to be used in the circuit
+ nlayers: int number of layers of the varitional circuit ansatz
+ data: numpy.array data[page][word] (this is an array of kets)
+ labels: list or numpy.array with the labels of the quantum states to be classified
+ nshots: int number of runs of the circuit during the sampling process (default=10000)
+
+ Returns:
+ numpy.float32 with the value of the square-loss function
+ """
+ circ = self.Classifier_circuit(theta)
+
+ Bias = np.array(theta[0 : self.measured_qubits])
+ predictions = np.zeros(shape=(len(data), self.measured_qubits))
+
+ for i, text in enumerate(data):
+ predictions[i] = self.Predictions(circ, Bias, text, nshots)
+
+ s = self.square_loss(labels, predictions)
+
+ return s
+
+ def minimize(
+ self, init_theta, data=None, labels=None, nshots=10000, method="Powell"
+ ):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit
+ nlayers: int number of layers of the varitional ansatz
+ init_state: numpy.array with the quantum state to be Schmidt-decomposed
+ nshots: int number of runs of the circuit during the sampling process (default=10000)
+ RY: if True, parameterized Rx,Rz,Rx gates are used in the circuit
+ if False, parameterized Ry gates are used in the circuit (default=True)
+ method: str 'classical optimizer for the minimization'. All methods from scipy.optimize.minmize are suported (default='Powell')
+
+ Returns:
+ numpy.float64 with value of the minimum found, numpy.ndarray with the optimal angles
+ """
+ from scipy.optimize import minimize
+
+ result = minimize(
+ self.Cost_function, init_theta, args=(data, labels, nshots), method=method
+ )
+ loss = result.fun
+ optimal_angles = result.x
+
+ return loss, optimal_angles
+
+ def Accuracy(self, labels, predictions, sign=True, tolerance=1e-2):
+ """
+ Args:
+ labels: numpy.array with the labels of the quantum states to be classified
+ predictions: numpy.array with the predictions for the quantum states classified
+ sign: if True, labels = np.sign(labels) and predictions = np.sign(predictions) (default=True)
+ tolerance: float tolerance level to consider a prediction correct (default=1e-2)
+
+ Returns:
+ float with the proportion of states classified successfully
+ """
+ if sign:
+ labels = [np.sign(label) for label in labels]
+ predictions = [np.sign(prediction) for prediction in predictions]
+
+ accur = 0
+ for l, p in zip(labels, predictions):
+ if np.allclose(l, p, rtol=0.0, atol=tolerance):
+ accur += 1
+
+ accur = accur / len(labels)
+
+ return accur
diff --git a/examples/vqregressor/README.md b/examples/vqregressor/README.md
new file mode 100644
index 000000000..5aece2c2d
--- /dev/null
+++ b/examples/vqregressor/README.md
@@ -0,0 +1,67 @@
+# Parameter Shift Rule for an hardware-compatible Variational Quantum Regressor
+
+Code at: [https://github.com/qiboteam/qibo/tree/master/examples/vqregressor](https://github.com/qiboteam/qibo/tree/master/examples/vqregressor)
+
+### Problem overview
+
+
+We want to tackle a simple one dimensional regression problem using a single qubit Variational Quantum Circuit (VQC) as model,
+initialized using a [re-uploading strategy](https://arxiv.org/abs/1907.02085). In particular, in this example we
+fit the function y = sin(2x), picking the x points from the interval I=[-1,1].
+The optimization is performed using an [Adam](https://arxiv.org/abs/1412.6980) optimizer.
+It needs the circuit's gradients, which we evaluate through the [Parameter Shift Rule](https://arxiv.org/abs/1811.11184) (PSR).
+
+A method like this is
+needed because in quantum computation we can't perform the [Back-Propagation Algorithm](https://www.nature.com/articles/323533a0) on the hardware:
+in that case the values of the target function in the middle of the propagation are needed, but for evaluating them on the hardware we have to measure,
+and measuring we provoke the collapse of the system and the loss of all the information wealth. The PSR provide us with a numerical tool, with which we
+can perform a gradient descent even on the physical qubit.
+
+### The Parameter Shift Rule in a nutshell
+
+Let's consider a parametrized circuit U, in which we build up a gate of the form G = exp [-i a A]
+(which represents an unitary operator with at most two eigenvalues +r and -r), and an observable B.
+Finally, let |qf> be the state we obtain by applying U to |0>.
+
+We are interested in evaluating the gradients of the following expression:
+
+![equation_f](images/equation_f.png)
+
+where we specify that f depends directly on the parameter a. We are interested in this result because the expectation value of B is typically involved
+in computing predictions in quantum machine learning problems. The PSR allows us to calculate the derivative of f(a) with respect to a evaluating
+f twice more:
+
+![equation_psr](images/equation_psr.png)
+
+where the two new parameters involved in the evaluation of f are obtained by shifting a forward and backward by a shift parameter s and s = (pi/4)r. Finally, if we pick A from the rotations generators we can use s=pi/2 and r=1/2.
+
+In the end, we have to use PSR into a gradient descent strategy. We choose an [MSE loss function](https://en.wikipedia.org/wiki/Mean_squared_error), which leads to the following explicit formula:
+
+![equation_dJ](images/equation_dJ.png)
+
+where we indicate with the subscript j the dipendence of J on the j-th input variable and y is the correct label of that specific x under the true law.
+
+### This example
+
+As mentioned above, we use a Variational Quantum Circuit based on a re-uploading strategy. In particular, we use the following architecture:
+
+![ansatz](images/ansatz.png)
+
+At the end of the circuit execution we perform a measurement on the qubit. After Nshots measurements, we use the difference of the probabilities
+of occurrence of the two states |0> and |1> as estimator for y.
+
+### How to use it?
+
+In this example we use only two files:
+
+- `vqregressor.py` contains the variational quantum regressor's implementation, with all the methods required for the optimization;
+- `main.py` contains a commented example of usage of the regressor.
+
+The user can change the target function modifying the method `vqregressor.label_points`, in which the true law is written and normalized. Once in the folder, one have to run a command like the following:
+
+`python3 main.py --layers 1 --learning_rate 0.05 --epochs 200 --batches 1 --ndata 30 --J_treshold 1e-4`
+
+for performing an optimization. At the end of the process it shows a plot containing true labels of the training sample and the predictions purposed
+by the model in a form like the following:
+
+![results](images/results.png)
diff --git a/examples/vqregressor/images/ansatz.png b/examples/vqregressor/images/ansatz.png
new file mode 100644
index 000000000..622c951a4
Binary files /dev/null and b/examples/vqregressor/images/ansatz.png differ
diff --git a/examples/vqregressor/images/equation_dJ.png b/examples/vqregressor/images/equation_dJ.png
new file mode 100644
index 000000000..7ba9ef37d
Binary files /dev/null and b/examples/vqregressor/images/equation_dJ.png differ
diff --git a/examples/vqregressor/images/equation_f.png b/examples/vqregressor/images/equation_f.png
new file mode 100644
index 000000000..55ed87c42
Binary files /dev/null and b/examples/vqregressor/images/equation_f.png differ
diff --git a/examples/vqregressor/images/equation_psr.png b/examples/vqregressor/images/equation_psr.png
new file mode 100644
index 000000000..a821150d7
Binary files /dev/null and b/examples/vqregressor/images/equation_psr.png differ
diff --git a/examples/vqregressor/images/results.png b/examples/vqregressor/images/results.png
new file mode 100644
index 000000000..7aad0690a
Binary files /dev/null and b/examples/vqregressor/images/results.png differ
diff --git a/examples/vqregressor/main.py b/examples/vqregressor/main.py
new file mode 100644
index 000000000..aef17da53
--- /dev/null
+++ b/examples/vqregressor/main.py
@@ -0,0 +1,48 @@
+import argparse
+
+import numpy as np
+from vqregressor import VQRegressor
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+ "--layers", default=3, help="Number of layers you want to involve", type=int
+)
+parser.add_argument(
+ "--learning_rate",
+ default=0.045,
+ help="Learning rate for the Adam Descent",
+ type=float,
+)
+parser.add_argument("--epochs", default=150, help="Number of training epochs", type=int)
+parser.add_argument(
+ "--batches",
+ default=1,
+ help="Number of batches which divide the training sample",
+ type=int,
+)
+parser.add_argument(
+ "--ndata", default=100, help="Number of data in the training set", type=int
+)
+parser.add_argument(
+ "--J_treshold", default=1e-4, help="Number of data in the training set", type=float
+)
+
+
+def main(layers, learning_rate, epochs, batches, ndata, J_treshold):
+ # We initialize the quantum regressor
+ vqr = VQRegressor(layers=layers, ndata=ndata)
+ # and the initial parameters
+ initial_params = np.random.randn(3 * layers)
+ # Let's go with the training
+ vqr.train_with_psr(
+ epochs=epochs,
+ learning_rate=learning_rate,
+ batches=batches,
+ J_treshold=J_treshold,
+ )
+ vqr.show_predictions("Predictions of the VQR after training", False)
+
+
+if __name__ == "__main__":
+ args = vars(parser.parse_args())
+ main(**args)
diff --git a/examples/vqregressor/vqregressor.py b/examples/vqregressor/vqregressor.py
new file mode 100644
index 000000000..9488271d4
--- /dev/null
+++ b/examples/vqregressor/vqregressor.py
@@ -0,0 +1,404 @@
+import matplotlib.pyplot as plt
+import numpy as np
+
+from qibo import Circuit, gates
+
+# Here we use the default numpy backend
+
+
+class VQRegressor:
+ def __init__(self, layers, ndata, states=None):
+ """
+ This class implement an Adam Descent optimization performed on a
+ 1-qubit Variational Quantum Circuit like this:
+ H --> [RY, H, RZ] --> ... --> [RY, H, RZ]
+ where each couple of squared parenthesis represents a layer.
+ Args:
+ layers: integer value representing the number of layers
+ ndata: integer value representing the training set's cardinality
+ states: (2, N)-dim np.matrix containing the states on which we perform the training.
+ You can prepare a state sample generating some point in [-1,1] and submitting them to
+ VQRegressor.prepare_states()
+ """
+
+ self.nqubits = 1
+ self.layers = layers
+ self.params = np.random.randn(3 * layers).astype("float64")
+ self.nparams = len(self.params)
+ self.features, self.labels = self.prepare_training_set(ndata, states)
+ self.nsample = len(self.labels)
+ self._circuit = self.ansatz(layers)
+
+ def ansatz(self, layers):
+ """
+ The circuit's ansatz: a sequence of RZ and RY with a beginning H gate
+ Args:
+ layers: integer, number of layers which compose the circuit
+ Returns: abstract qibo circuit
+ """
+ c = Circuit(self.nqubits)
+
+ c.add(gates.H(q=0))
+ for l in range(layers):
+ c.add(gates.RY(q=0, theta=0))
+ c.add(gates.H(0))
+ c.add(gates.RZ(q=0, theta=0))
+ c.add(gates.M(0))
+ return c
+
+ def label_points(self, x):
+ """
+ Function which implement the target function
+ Args:
+ x: np.float64 array of input variables
+ Returns: np.float64 array of output variables
+ """
+ # here you can define the function you want to fit
+ y = np.sin(2 * x)
+
+ # that is normalized here
+ ymax = np.max(np.abs(y))
+ y = y / ymax
+
+ return y
+
+ def prepare_states(self, x):
+ """
+ State preparation: each data have to be embedded into a 2**nqubits vector state
+ Args:
+ x: np.array of input data with length N
+ Returns: np.matrix (2**nqubits, N) of the states
+ """
+ N = len(x)
+ states = np.zeros((N, 2))
+ for i in range(N):
+ states[i][0] = x[i]
+ states[i][1] = 0.33
+ return states
+
+ def prepare_training_set(self, n_sample, states=None):
+ """
+ This function picks a random sample from the uniform U[0,1]
+ Args:
+ n_sample: integer desired dataset cardinality
+ Returns: np.matrix containing the states and np.array containing the labels
+ """
+ if states is None:
+ x = np.random.uniform(-1, 1, n_sample)
+ labels = self.label_points(x)
+ states = self.prepare_states(x)
+ else:
+ states = states
+ labels = self.label_points(states.T[0])
+ return states, labels
+
+ def show_predictions(self, title, save, features=None):
+ """
+ This function shows the VQR predictions on the training dataset
+ Args:
+ title: string title of the plot
+ save: boolean variable; pick True if you want to save the image as title.png
+ pick False if you don't want to save it
+ features: np.matrix in the form (2**nqubits, n_sample) on which you desire to perform
+ the predictions. Default = None and it takes the training dataset
+
+ """
+ if features is None:
+ features = self.features
+
+ labels = self.label_points(features.T[0])
+
+ predictions = self.predict_sample(features)
+
+ plt.figure(figsize=(15, 7))
+ plt.title(title)
+ plt.scatter(
+ features.T[0],
+ predictions,
+ label="Predicted",
+ s=100,
+ color="blue",
+ alpha=0.65,
+ )
+ plt.scatter(
+ features.T[0], labels, label="Original", s=100, color="red", alpha=0.65
+ )
+ plt.xlabel("x")
+ plt.ylabel("y")
+ plt.legend()
+ plt.tight_layout()
+ if save is True:
+ plt.savefig(title + ".pdf")
+ plt.close()
+ plt.show()
+
+ def set_parameters(self, new_params):
+ """
+ Function which set the new parameters into the circuit
+ Args:
+ new_params: np.array of the new parameters; it has to be (3 * nlayers) long
+ """
+ self.params = new_params
+
+ def get_parameters(self):
+ return self.params
+
+ def circuit(self, feature):
+ """
+ This function prepare the circuit implementing the data re-uploading strategy. In this case
+ we choose to implement RY-RZ layers, depending on 3 parameters. The data affect the first
+ parameter in each layer.
+ Args:
+ feature: 2**nqubits dimensional np.array containing a specific feature
+ Returns: modified qibo circuit according to re-uploading strategy
+ """
+ params = []
+ for i in range(0, 3 * self.layers, 3):
+ params.append(self.params[i] * feature[0] + self.params[i + 1])
+ params.append(self.params[i + 2])
+ self._circuit.set_parameters(params)
+ return self._circuit
+
+ def one_prediction(self, this_feature):
+ """
+ This function execute one prediction
+ Args:
+ this_feature: np.float64 array 2**nqubits long, containing a specific feature
+ Returns: circuit's prediction of the output variable, evaluated as difference of probabilities
+ """
+ nshots = 1024
+ c = self.circuit(this_feature)
+ results = c(nshots=nshots).probabilities(qubits=[0])
+ res = results[0] - results[1]
+ return res
+
+ def predict_sample(self, features=None):
+ """
+ This function calculates the predictions related to a specific sample
+ Args:
+ features: np.matrix containing the N states; each state must be prepared (with
+ the opportune self.prepare_states function as an array with dim 2**nqubits)
+ Returns: np.array of the predictions
+ """
+ if features is None:
+ features = self.features
+
+ predictions = np.zeros(len(features))
+ for i in range(len(features)):
+ predictions[i] = self.one_prediction(features[i])
+
+ return predictions
+
+ def one_target_loss(self, this_feature, this_label):
+ """
+ Evaluation of the loss function for a single feature knowing its label
+ Args:
+ this_feature: the feature in form of an 2**nqubits-dim np.array
+ this_label: the associated label
+ Returns: one target loss function's value
+ """
+ this_prediction = self.one_prediction(this_feature)
+ cf = (this_prediction - this_label) ** 2
+ return cf
+
+ def loss(self, params, features=None, labels=None):
+ """
+ Evaluation of the total loss function
+ Args:
+ params: np.array of the params which define the circuit
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ Returns: loss function evaluated by summing contributes of each data
+ """
+ if params is None:
+ params = self.params
+
+ if features is None:
+ features = self.features
+
+ if labels is None:
+ labels = self.labels
+
+ self.set_parameters(params)
+ cf = 0
+ for feat, lab in zip(features, labels):
+ cf = cf + self.one_target_loss(feat, lab)
+ cf = cf / len(labels)
+ return cf
+
+ def dloss(self, features, labels):
+ """
+ This function calculates the loss function's gradients with respect to self.params
+ Args:
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ Returns: np.array of length self.nparams containing the loss function's gradients
+ """
+ loss_gradients = np.zeros(self.nparams)
+ loss = 0
+
+ for feat, label in zip(features, labels):
+ prediction_evaluation = self.one_prediction(feat) #
+ loss += (label - prediction_evaluation) ** 2
+ obs_gradients = self.parameter_shift(feat) # d
+ for i in range(self.nparams):
+ loss_gradients[i] += (2 * prediction_evaluation * obs_gradients[i]) - (
+ 2 * label * obs_gradients[i]
+ )
+
+ return loss_gradients, (loss / len(features))
+
+ def shift_a_parameter(self, i, this_feature):
+ """
+ Parameter shift's execution on a single parameter
+ Args:
+ i: integer index which identify the parameter into self.params
+ this_feature: np.array 2**nqubits-long containing the state vector assciated to a data
+ Returns: derivative of the observable (here the prediction) with respect to self.params[i]
+ """
+ original = self.params.copy()
+ shifted = self.params.copy()
+
+ # customized parameter shift rule when x contributes to param's definition
+ if i % 3 == 0:
+ shifted[i] += np.pi / 2 / this_feature[0]
+
+ self.set_parameters(shifted)
+ forward = self.one_prediction(this_feature)
+
+ shifted[i] -= np.pi / this_feature[0]
+
+ self.set_parameters(shifted)
+ backward = self.one_prediction(this_feature)
+
+ self.set_parameters(original)
+
+ result = 0.5 * (forward - backward) * this_feature[0]
+
+ else:
+ shifted[i] += np.pi / 2
+
+ self.set_parameters(shifted)
+ forward = self.one_prediction(this_feature)
+
+ shifted[i] -= np.pi
+
+ self.set_parameters(shifted)
+ backward = self.one_prediction(this_feature)
+
+ self.set_parameters(original)
+
+ result = 0.5 * (forward - backward)
+
+ return result
+
+ def parameter_shift(self, this_feature):
+ """
+ Full parameter-shift rule's implementation
+ Args:
+ this_feature: np.array 2**nqubits-long containing the state vector assciated to a data
+ Returns: np.array of the observable's gradients with respect to the variational parameters
+ """
+ obs_gradients = np.zeros(self.nparams, dtype=np.float64)
+ for ipar in range(self.nparams):
+ obs_gradients[ipar] = self.shift_a_parameter(ipar, this_feature)
+ return obs_gradients
+
+ def AdamDescent(
+ self,
+ learning_rate,
+ m,
+ v,
+ features,
+ labels,
+ iteration,
+ beta_1=0.85,
+ beta_2=0.99,
+ epsilon=1e-8,
+ ):
+ """
+ Implementation of the Adam optimizer: during a run of this function parameters are updated.
+ Furthermore, new values of m and v are calculated.
+ Args:
+ learning_rate: np.float value of the learning rate
+ m: momentum's value before the execution of the Adam descent
+ v: velocity's value before the execution of the Adam descent
+ features: np.matrix containig the n_sample-long vector of states
+ labels: np.array of the labels related to features
+ iteration: np.integer value corresponding to the current training iteration
+ beta_1: np.float value of the Adam's beta_1 parameter; default 0.85
+ beta_2: np.float value of the Adam's beta_2 parameter; default 0.99
+ epsilon: np.float value of the Adam's epsilon parameter; default 1e-8
+ Returns: np.float new values of momentum and velocity
+ """
+
+ grads, loss = self.dloss(features, labels)
+
+ for i in range(self.nparams):
+ m[i] = beta_1 * m[i] + (1 - beta_1) * grads[i]
+ v[i] = beta_2 * v[i] + (1 - beta_2) * grads[i] * grads[i]
+ mhat = m[i] / (1.0 - beta_1 ** (iteration + 1))
+ vhat = v[i] / (1.0 - beta_2 ** (iteration + 1))
+ self.params[i] -= learning_rate * mhat / (np.sqrt(vhat) + epsilon)
+
+ return m, v, loss
+
+ def train_with_psr(self, epochs, learning_rate, batches, J_treshold):
+ """
+ This function performs the full Adam descent's procedure
+ Args:
+ epochs: np.integer value corresponding to the epochs of training
+ learning_rate: np.float value of the learning rate
+ batches: np.integer value of the number of batches which divide the dataset
+ J_treshold: np.float value of the desired loss function's treshold
+ Returns: list of loss values, one for each epoch
+ """
+
+ losses = []
+ indices = []
+
+ # create index list
+ idx = np.arange(0, self.nsample)
+
+ m = np.zeros(self.nparams)
+ v = np.zeros(self.nparams)
+
+ # create index blocks on which we run
+ for ib in range(batches):
+ indices.append(np.arange(ib, self.nsample, batches))
+
+ iteration = 0
+
+ for epoch in range(epochs):
+ if epoch != 0 and losses[-1] < J_treshold:
+ print(
+ "Desired sensibility is reached, here we stop: ",
+ iteration,
+ " iteration",
+ )
+ break
+ # shuffle index list
+ np.random.shuffle(idx)
+ # run over the batches
+ for ib in range(batches):
+ iteration += 1
+
+ features = self.features[idx[indices[ib]]]
+ labels = self.labels[idx[indices[ib]]]
+ # update parameters
+ m, v, this_loss = self.AdamDescent(
+ learning_rate, m, v, features, labels, iteration
+ )
+ # track the training
+ print(
+ "Iteration ",
+ iteration,
+ " epoch ",
+ epoch + 1,
+ " | loss: ",
+ this_loss,
+ )
+ # in case one wants to plot J in function of the iterations
+ losses.append(this_loss)
+
+ return losses
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..3912e526a
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,252 @@
+{
+ "nodes": {
+ "devenv": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "nix": "nix",
+ "nixpkgs": "nixpkgs",
+ "pre-commit-hooks": "pre-commit-hooks"
+ },
+ "locked": {
+ "lastModified": 1710144971,
+ "narHash": "sha256-CjTOdoBvT/4AQncTL20SDHyJNgsXZjtGbz62yDIUYnM=",
+ "owner": "cachix",
+ "repo": "devenv",
+ "rev": "6c0bad0045f1e1802f769f7890f6a59504825f4d",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "devenv",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1673956053,
+ "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1685518550,
+ "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "devenv",
+ "pre-commit-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1660459072,
+ "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "lowdown-src": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1633514407,
+ "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
+ "owner": "kristapsdz",
+ "repo": "lowdown",
+ "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "kristapsdz",
+ "repo": "lowdown",
+ "type": "github"
+ }
+ },
+ "nix": {
+ "inputs": {
+ "lowdown-src": "lowdown-src",
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ],
+ "nixpkgs-regression": "nixpkgs-regression"
+ },
+ "locked": {
+ "lastModified": 1676545802,
+ "narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
+ "owner": "domenkozar",
+ "repo": "nix",
+ "rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "domenkozar",
+ "ref": "relaxed-flakes",
+ "repo": "nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1678875422,
+ "narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs-regression": {
+ "locked": {
+ "lastModified": 1643052045,
+ "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ }
+ },
+ "nixpkgs-stable": {
+ "locked": {
+ "lastModified": 1685801374,
+ "narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "c37ca420157f4abc31e26f436c1145f8951ff373",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-23.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1710272261,
+ "narHash": "sha256-g0bDwXFmTE7uGDOs9HcJsfLFhH7fOsASbAuOzDC+fhQ=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "0ad13a6833440b8e238947e47bea7f11071dc2b2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pre-commit-hooks": {
+ "inputs": {
+ "flake-compat": [
+ "devenv",
+ "flake-compat"
+ ],
+ "flake-utils": "flake-utils",
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ],
+ "nixpkgs-stable": "nixpkgs-stable"
+ },
+ "locked": {
+ "lastModified": 1704725188,
+ "narHash": "sha256-qq8NbkhRZF1vVYQFt1s8Mbgo8knj+83+QlL5LBnYGpI=",
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "rev": "ea96f0c05924341c551a797aaba8126334c505d2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "pre-commit-hooks.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "devenv": "devenv",
+ "nixpkgs": "nixpkgs_2",
+ "systems": "systems_2"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_2": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..4d4ccaaac
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,59 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ systems.url = "github:nix-systems/default";
+ devenv.url = "github:cachix/devenv";
+ };
+
+ outputs = {
+ self,
+ nixpkgs,
+ devenv,
+ systems,
+ ...
+ } @ inputs: let
+ forEachSystem = nixpkgs.lib.genAttrs (import systems);
+ in {
+ packages = forEachSystem (system: {
+ default =
+ nixpkgs.legacyPackages.${system}.poetry2nix.mkPoetryApplication
+ {
+ projectDir = self;
+ preferWheels = true;
+ };
+ });
+
+ devShells =
+ forEachSystem
+ (system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ default = devenv.lib.mkShell {
+ inherit inputs pkgs;
+ modules = [
+ {
+ packages = with pkgs; [
+ pre-commit
+ poethepoet
+ stdenv.cc.cc.lib
+ ];
+
+ languages.python = {
+ enable = true;
+ poetry = {
+ enable = true;
+ install.enable = true;
+ install.groups = ["dev"];
+ };
+ };
+ }
+ ];
+ };
+ });
+ };
+
+ nixConfig = {
+ extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
+ extra-substituters = "https://devenv.cachix.org";
+ };
+}
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 000000000..03761577d
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,5621 @@
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+
+[[package]]
+name = "absl-py"
+version = "2.1.0"
+description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff"},
+ {file = "absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308"},
+]
+
+[[package]]
+name = "alabaster"
+version = "0.7.16"
+description = "A light, configurable Sphinx theme"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"},
+ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"},
+]
+
+[[package]]
+name = "antlr4-python3-runtime"
+version = "4.13.1"
+description = "ANTLR 4.13.1 runtime for Python 3"
+optional = false
+python-versions = "*"
+files = [
+ {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"},
+ {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"},
+]
+
+[[package]]
+name = "anyio"
+version = "4.4.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
+ {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+]
+
+[package.dependencies]
+exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
+idna = ">=2.8"
+sniffio = ">=1.1"
+typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
+
+[[package]]
+name = "appnope"
+version = "0.1.4"
+description = "Disable App Nap on macOS >= 10.9"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"},
+ {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"},
+]
+
+[[package]]
+name = "astroid"
+version = "3.1.0"
+description = "An abstract syntax tree for Python with inference support."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"},
+ {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "asttokens"
+version = "2.4.1"
+description = "Annotate AST trees with source code positions"
+optional = false
+python-versions = "*"
+files = [
+ {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"},
+ {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"},
+]
+
+[package.dependencies]
+six = ">=1.12.0"
+
+[package.extras]
+astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
+test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
+
+[[package]]
+name = "astunparse"
+version = "1.6.3"
+description = "An AST unparser for Python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"},
+ {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"},
+]
+
+[package.dependencies]
+six = ">=1.6.1,<2.0"
+wheel = ">=0.23.0,<1.0"
+
+[[package]]
+name = "attrs"
+version = "21.4.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+]
+
+[package.extras]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"]
+tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"]
+
+[[package]]
+name = "autoray"
+version = "0.6.12"
+description = "Abstract your array operations."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "autoray-0.6.12-py3-none-any.whl", hash = "sha256:3ed7a4abcec052bcbb4f0447c426d0a0b9b9fa03ab71e76eaa77747ca43ac3e2"},
+ {file = "autoray-0.6.12.tar.gz", hash = "sha256:721328aa06fc3577155d988052614a7b4bd6e4d01b340695344031ee4abd2a1e"},
+]
+
+[package.extras]
+docs = ["astroid (<3)", "furo", "ipython (!=8.7.0)", "myst-nb", "setuptools-scm", "sphinx (>=2.0)", "sphinx-autoapi", "sphinx-copybutton"]
+tests = ["coverage", "numpy", "pytest", "pytest-cov"]
+
+[[package]]
+name = "babel"
+version = "2.15.0"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"},
+ {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"},
+]
+
+[package.extras]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.12.3"
+description = "Screen-scraping library"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
+ {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+cchardet = ["cchardet"]
+chardet = ["chardet"]
+charset-normalizer = ["charset-normalizer"]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "bleach"
+version = "6.1.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"},
+ {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"},
+]
+
+[package.dependencies]
+six = ">=1.9.0"
+webencodings = "*"
+
+[package.extras]
+css = ["tinycss2 (>=1.1.0,<1.3)"]
+
+[[package]]
+name = "cachetools"
+version = "5.4.0"
+description = "Extensible memoizing collections and decorators"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
+ {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"},
+]
+
+[[package]]
+name = "certifi"
+version = "2024.7.4"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+]
+
+[[package]]
+name = "cffi"
+version = "1.16.0"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
+ {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
+ {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
+ {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
+ {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
+ {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
+ {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
+ {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
+ {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
+ {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
+ {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
+ {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
+ {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
+ {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
+ {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
+ {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
+ {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
+ {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
+ {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
+ {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
+ {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
+ {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
+ {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
+ {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
+ {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
+ {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
+ {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "cirq"
+version = "1.3.0"
+description = "A framework for creating, editing, and invoking Noisy Intermediate Scale Quantum (NISQ) circuits."
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq-1.3.0-py3-none-any.whl", hash = "sha256:9e39f2c919864ee3f6a4b4b1dcfdaacb4d5e753e2c7fc7ebb0a93947d6cefcab"},
+]
+
+[package.dependencies]
+cirq-aqt = "1.3.0"
+cirq-core = "1.3.0"
+cirq-ft = "1.3.0"
+cirq-google = "1.3.0"
+cirq-ionq = "1.3.0"
+cirq-pasqal = "1.3.0"
+cirq-rigetti = "1.3.0"
+cirq-web = "1.3.0"
+
+[package.extras]
+dev-env = ["asv", "black (==23.3.0)", "codeowners", "coverage (<=6.2)", "filelock (>=3.0.12,<3.1.0)", "freezegun (>=0.3.15,<0.4.0)", "grpcio-tools (>=1.56.0,<1.57.0)", "importlib-metadata", "ipykernel (==5.3.4)", "ipython (>=7.34.0)", "mypy (==1.2.0)", "mypy-protobuf (==3.4)", "notebook (>=6.4.1,<=6.4.7)", "papermill (>=2.3.2,<2.4.0)", "pylint (>=2.13.0,<2.14.0)", "pytest", "pytest-asyncio", "pytest-cov (>=3.0,<4.0)", "pytest-randomly", "pytest-xdist (>=2.2.0,<2.3.0)", "qiskit-aer (>=0.12.2,<0.13.0)", "rstcheck (>=3.3.1,<3.4.0)", "seaborn (>=0.11.1,<0.12.0)", "setuptools", "twine", "types-backports (==0.1.3)", "types-cachetools", "types-protobuf (>=3.20,<4.0)", "types-requests (==2.28.1)", "types-setuptools (==62.6.1)", "virtualenv", "virtualenv-clone", "wheel"]
+
+[[package]]
+name = "cirq-aqt"
+version = "1.3.0"
+description = "A Cirq package to simulate and connect to Alpine Quantum Technologies quantum computers"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_aqt-1.3.0-py3-none-any.whl", hash = "sha256:e70324b80ac0c752447c67ee49d84129fb21b8ce63737c15cdd155cb9fb1f405"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+requests = ">=2.18,<3.0"
+
+[[package]]
+name = "cirq-core"
+version = "1.3.0"
+description = "A framework for creating, editing, and invoking Noisy Intermediate Scale Quantum (NISQ) circuits."
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_core-1.3.0-py3-none-any.whl", hash = "sha256:d46771ddf4adb0867d3fb6da8c4484b73bada5bfaa58c19e2444aa8838270fd9"},
+]
+
+[package.dependencies]
+duet = ">=0.2.8,<0.3.0"
+matplotlib = ">=3.0,<4.0"
+networkx = ">=2.4"
+numpy = ">=1.16,<2.0"
+pandas = "*"
+scipy = "*"
+sortedcontainers = ">=2.0,<3.0"
+sympy = "*"
+tqdm = "*"
+typing-extensions = ">=4.2"
+
+[package.extras]
+contrib = ["autoray", "numba (>=0.53.0)", "opt-einsum", "ply (>=3.6)", "pylatex (>=1.3.0,<1.4.0)", "quimb"]
+
+[[package]]
+name = "cirq-ft"
+version = "1.3.0"
+description = "A Cirq package for fault-tolerant algorithms"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_ft-1.3.0-py3-none-any.whl", hash = "sha256:ff45a911b74abece20e97856bad6ff388efdd79068f654a8cee7925d2fe54bf5"},
+]
+
+[package.dependencies]
+attrs = "*"
+cachetools = ">=5.3"
+cirq-core = "1.3.0"
+ipywidgets = "*"
+nbconvert = "*"
+nbformat = "*"
+
+[[package]]
+name = "cirq-google"
+version = "1.3.0"
+description = "The Cirq module that provides tools and access to the Google Quantum Computing Service"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_google-1.3.0-py3-none-any.whl", hash = "sha256:aa802887679f64ef8359f77d5bbd5bb98dc076c35c152c3b7f172c6e09d4e667"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+google-api-core = {version = ">=1.14.0", extras = ["grpc"]}
+proto-plus = ">=1.20.0"
+protobuf = ">=3.15.0"
+
+[[package]]
+name = "cirq-ionq"
+version = "1.3.0"
+description = "A Cirq package to simulate and connect to IonQ quantum computers"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_ionq-1.3.0-py3-none-any.whl", hash = "sha256:7368da1012a5aafa6ae9125bfafc3beaf1ed799c0f9680947cf8709652f1f640"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+requests = ">=2.18,<3.0"
+
+[[package]]
+name = "cirq-pasqal"
+version = "1.3.0"
+description = "A Cirq package to simulate and connect to Pasqal quantum computers"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_pasqal-1.3.0-py3-none-any.whl", hash = "sha256:953c04f1ba5efa94a26cae88e0d2ceeabde3c664beef43800b4be728c7f6f6eb"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+requests = ">=2.18,<3.0"
+
+[[package]]
+name = "cirq-rigetti"
+version = "1.3.0"
+description = "A Cirq package to simulate and connect to Rigetti quantum computers and Quil QVM"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_rigetti-1.3.0-py3-none-any.whl", hash = "sha256:b771e523a8f2bd83ac3b22a79ae8ce03467c75c083ff44bcb04022c12c6dace3"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+pyquil = ">=3.2.0,<4.0.0"
+
+[[package]]
+name = "cirq-web"
+version = "1.3.0"
+description = "Web-based 3D visualization tools for Cirq."
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "cirq_web-1.3.0-py3-none-any.whl", hash = "sha256:5206d96e378f8f6fe7e0067cb84db9f48eff372bbb565f0056e1e95d7952b458"},
+]
+
+[package.dependencies]
+cirq-core = "1.3.0"
+
+[[package]]
+name = "cloudpickle"
+version = "3.0.0"
+description = "Pickler class to extend the standard pickle.Pickler functionality"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"},
+ {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"},
+]
+
+[[package]]
+name = "cma"
+version = "3.4.0"
+description = "CMA-ES, Covariance Matrix Adaptation Evolution Strategy for non-linear numerical optimization in Python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "cma-3.4.0-py3-none-any.whl", hash = "sha256:4140e490cc4e68cf8c7b1114e079c0561c9b78b1bf9ec69362c20865636ae5ca"},
+ {file = "cma-3.4.0.tar.gz", hash = "sha256:a1ebd969b99871be3715d5a24b7bf54cf04ea94e80d6b8536d7147620dd10f6c"},
+]
+
+[package.dependencies]
+numpy = "*"
+
+[package.extras]
+constrained-solution-tracking = ["moarchiving"]
+plotting = ["matplotlib"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "comm"
+version = "0.2.2"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"},
+ {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"},
+]
+
+[package.dependencies]
+traitlets = ">=4"
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+optional = false
+python-versions = "*"
+files = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "contourpy"
+version = "1.2.1"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"},
+ {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"},
+ {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"},
+ {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"},
+ {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"},
+ {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"},
+ {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"},
+ {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"},
+ {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"},
+ {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"},
+ {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"},
+ {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"},
+ {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"},
+ {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"},
+ {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"},
+ {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"},
+ {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"},
+ {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"},
+ {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"},
+ {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"},
+ {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"},
+ {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"},
+ {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"},
+ {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"},
+ {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"},
+ {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"},
+ {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"},
+ {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"},
+ {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"},
+ {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"},
+]
+
+[package.dependencies]
+numpy = ">=1.20"
+
+[package.extras]
+bokeh = ["bokeh", "selenium"]
+docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
+mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"]
+test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
+test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
+
+[[package]]
+name = "cotengra"
+version = "0.6.2"
+description = "Hyper optimized contraction trees for large tensor networks and einsums."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cotengra-0.6.2-py3-none-any.whl", hash = "sha256:524711515cfbaae22ae56a8c6bf3339dfe50485b9b615de52f8f7c330847b196"},
+ {file = "cotengra-0.6.2.tar.gz", hash = "sha256:a56b921d0339d1397e925a7b69029d3301636d09b99b509368751753bda39d24"},
+]
+
+[package.dependencies]
+autoray = "*"
+
+[package.extras]
+docs = ["astroid (<3.0.0)", "furo", "ipython (!=8.7.0)", "myst-nb", "setuptools-scm", "sphinx (>=2.0)", "sphinx-autoapi", "sphinx-copybutton", "sphinx-design"]
+recommended = ["cotengrust (>=0.1.3)", "cytoolz", "kahypar", "networkx", "numpy", "opt-einsum", "optuna", "ray", "tqdm"]
+test = ["altair", "baytune", "chocolate", "dask", "distributed", "kahypar", "matplotlib", "networkx", "nevergrad", "numpy", "opt-einsum", "pytest", "seaborn", "skopt"]
+
+[[package]]
+name = "coverage"
+version = "7.6.0"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"},
+ {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"},
+ {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"},
+ {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"},
+ {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"},
+ {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"},
+ {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"},
+ {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"},
+ {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"},
+ {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"},
+ {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"},
+ {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"},
+ {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"},
+ {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"},
+ {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"},
+ {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"},
+ {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"},
+ {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"},
+ {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"},
+ {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"},
+ {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"},
+ {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"},
+ {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"},
+ {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"},
+ {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"},
+ {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"},
+ {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"},
+ {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"},
+ {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"},
+ {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"},
+ {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"},
+ {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"},
+ {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"},
+ {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"},
+ {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"},
+ {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"},
+ {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"},
+ {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"},
+ {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"},
+ {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"},
+ {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"},
+ {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"},
+ {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"},
+ {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"},
+ {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"},
+ {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"},
+ {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"},
+ {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"},
+ {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"},
+ {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"},
+ {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"},
+ {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "cupy-cuda11x"
+version = "12.3.0"
+description = "CuPy: NumPy & SciPy for GPU"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cupy_cuda11x-12.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:be948cc7625c7ebeddaa42f39d0222e061b18911b56b2766efd638aa1a88c53c"},
+ {file = "cupy_cuda11x-12.3.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1a8d5f867cbe65966d4ca9cb56bc19d316d3947149d07d5e779b90913587ffd3"},
+ {file = "cupy_cuda11x-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:04d3ee82364d26dc3209e35864e1fbacb10d11294296752f6047ad009bee7bb1"},
+ {file = "cupy_cuda11x-12.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:804d49f53a6c817c88fd289de753018ca22c127fac51ec8acda5ef70532a8ae2"},
+ {file = "cupy_cuda11x-12.3.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:4173486906388c91a70f639003313b96e9449cb8a008137e8ac4bcd872bcf34c"},
+ {file = "cupy_cuda11x-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:191a0cb9d7620fdb8baf1dc9f8e60ebef0562d7102990244f94b8ba7ab2a48df"},
+ {file = "cupy_cuda11x-12.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3dd934a2e3c88798a342be8556fef9f608282adb0bce979b957400a235f492c4"},
+ {file = "cupy_cuda11x-12.3.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:09accf46c9352ae56422a7f37c03413c943f4b254cfaad4cc824c947368b96ce"},
+ {file = "cupy_cuda11x-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:6eb6874d756f936c4aecd053b81376ec132079c6a9f8e97e210261816b81b2e4"},
+ {file = "cupy_cuda11x-12.3.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:26139bb797017296e2f2ed7cbf6aabd31a6bf914955fa6e6193f3ea93a23ac8e"},
+ {file = "cupy_cuda11x-12.3.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:0d53e6f08979e67142cfc4e4d734cc9c6275f8be88f149849ad41d42f57a2d6e"},
+ {file = "cupy_cuda11x-12.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:6a65056e317a0d8ab88477c3fcbc30f116a540f35856b81c98bbbde207fa25ad"},
+ {file = "cupy_cuda11x-12.3.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:fa2abd3b166e1cd6f2499604814b03d9220a4412dcb1a1f383fc4a6d571ea5fb"},
+ {file = "cupy_cuda11x-12.3.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ec83f9f39b9703d8fdc703c04f0f126be1355876c6bdf5d261488bfddccdfb74"},
+ {file = "cupy_cuda11x-12.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1ec71333cb8871826cbc8573d506da65e9dc53236bd61dfce7ea73f9c3f174e"},
+]
+
+[package.dependencies]
+fastrlock = ">=0.5"
+numpy = ">=1.20,<1.29"
+
+[package.extras]
+all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.14)"]
+stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"]
+test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"]
+
+[[package]]
+name = "cupy-cuda12x"
+version = "12.3.0"
+description = "CuPy: NumPy & SciPy for GPU"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cupy_cuda12x-12.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:77687b019383eb89da981638cfec6d5ba556a15f1be0945e8f7f80030b851d04"},
+ {file = "cupy_cuda12x-12.3.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:32d0e03789ef3f02f0c098818e957c235b75c1636e9e0036299480db0c423dcd"},
+ {file = "cupy_cuda12x-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:6ab0d67db0ae74118b3b29c96cd79428593ee646c1e7b4f92ad76028033d3646"},
+ {file = "cupy_cuda12x-12.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e8a4906da820f6ce39a3a1d18c4504da4e0faad87598761ea9d6bf8288423d69"},
+ {file = "cupy_cuda12x-12.3.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:49626a975c87008a11b67a43ca4b5bd00c3029e093430511c83cbda422b6a89f"},
+ {file = "cupy_cuda12x-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:182fed8568f60208bec43b8ad011f4ab2a80f5bc428bd34672b9ef130a93f772"},
+ {file = "cupy_cuda12x-12.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:43c54e121d9fdc92b246df438bc3f49d47a85c1562deefc9f5e16e55c27cec6a"},
+ {file = "cupy_cuda12x-12.3.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:3e65f1cfd32fa5d9894681de35e57a79a8bf74e8a84e6ee3d24542ba71aaca34"},
+ {file = "cupy_cuda12x-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c7282a51a6e74a2221285b44eb6deefac96965016b7eb4387903e5cee059bcb7"},
+ {file = "cupy_cuda12x-12.3.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5dea10a66a0e208f83ef80fca0e57f6ed227b93c138a65c6d23ff2d8350a3a33"},
+ {file = "cupy_cuda12x-12.3.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:27993b46a1dd5a6b2c269ab436e0927b5eb66adb03d24b9b321850a1b4140d7a"},
+ {file = "cupy_cuda12x-12.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:8fe596e792ef47bab71cc85fe0982d752357276d6eb701cfc823697d29e83257"},
+ {file = "cupy_cuda12x-12.3.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:716db7567453a88795c1e157f4e9059d291f759e01d7c18df65feafb02209b31"},
+ {file = "cupy_cuda12x-12.3.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:1ac1312fa4f24598bced9ddd6b2cd66236c53171e9941c5ee170c5b14c7f0e3a"},
+ {file = "cupy_cuda12x-12.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:3c7ba97dcb78f8b65ff7c179eacde3e3f2cd895d5fd5041c5617e717d15b2e76"},
+]
+
+[package.dependencies]
+fastrlock = ">=0.5"
+numpy = ">=1.20,<1.29"
+
+[package.extras]
+all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.14)"]
+stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"]
+test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"]
+
+[[package]]
+name = "cuquantum-python-cu11"
+version = "23.10.0"
+description = "NVIDIA cuQuantum Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "cuquantum_python_cu11-23.10.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a0f0dfcb6239ec5fce836fa2f641820d3235ac7d83f391eac90952cf481da03f"},
+ {file = "cuquantum_python_cu11-23.10.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:ad5e38501cb53d50ba19fc48790f2c79fbc14c22e101d51a0b338f6c6971e6a0"},
+ {file = "cuquantum_python_cu11-23.10.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:f5bd44f0a50b38fa778836577e11515fb820c98217d2958fdedfc861a701f604"},
+ {file = "cuquantum_python_cu11-23.10.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:01e6b210ed66a1fda172884f1eca68b4763f676f81949af8f0d6b16d798f1881"},
+ {file = "cuquantum_python_cu11-23.10.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0f658d3c83a8f05b81749a1fecc232ca23650147f53d82fe61dae987e544fb9c"},
+ {file = "cuquantum_python_cu11-23.10.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:997e47861bab2c5e183a3b7439ba7fe4dd777cbf4d24f2234ac4ad7936cbc699"},
+]
+
+[package.dependencies]
+custatevec-cu11 = ">=1.5,<2.0"
+cutensornet-cu11 = ">=2.3,<3.0"
+numpy = ">=1.21,<2.0"
+
+[[package]]
+name = "cuquantum-python-cu12"
+version = "23.10.0"
+description = "NVIDIA cuQuantum Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "cuquantum_python_cu12-23.10.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:56a043951348a868957405103d8a0fb24da496c330876480958d371c040c371a"},
+ {file = "cuquantum_python_cu12-23.10.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:171f5cb136c9db0e3520c99a9c4a55c8f8c84440b725e3e5511e7d9b6373ba1e"},
+ {file = "cuquantum_python_cu12-23.10.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a5a747885a164b4ccc41d3a5d225e4a8cc3076595b1b6169a82c8bf63ff1a801"},
+ {file = "cuquantum_python_cu12-23.10.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:719cfa4c53ec40465c425ae8482cc8272686a29026c0bcbff23248c0418f8c95"},
+ {file = "cuquantum_python_cu12-23.10.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f97601858323f85caedbf4192fafc406512a56c07ebfcf122503012978a21929"},
+ {file = "cuquantum_python_cu12-23.10.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:24a48bd0763362d709afadbc7f09e4977d0f2dff6a7200c42415f7fc39216652"},
+]
+
+[package.dependencies]
+cupy-cuda12x = ">=10.0"
+custatevec-cu12 = ">=1.5,<2.0"
+cutensornet-cu12 = ">=2.3,<3.0"
+numpy = ">=1.21,<2.0"
+
+[[package]]
+name = "custatevec-cu11"
+version = "1.6.0.post1"
+description = "cuStateVec - a component of NVIDIA cuQuantum SDK"
+optional = false
+python-versions = "*"
+files = [
+ {file = "custatevec_cu11-1.6.0.post1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6840bd957455d33f9df1f2e266fe33079e429b2442f5ff6ef5eae6c1b618108e"},
+ {file = "custatevec_cu11-1.6.0.post1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8d57ada4d3cc77b24b3be83b55929942186698e71edadaa952448c1b0af43539"},
+]
+
+[[package]]
+name = "custatevec-cu12"
+version = "1.6.0.post1"
+description = "cuStateVec - a component of NVIDIA cuQuantum SDK"
+optional = false
+python-versions = "*"
+files = [
+ {file = "custatevec_cu12-1.6.0.post1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4f38175cb6cb9dfa0008e5109e22bf92eeedd3aad843be3ce27ad41b53318f95"},
+ {file = "custatevec_cu12-1.6.0.post1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0c875981de852091f5f3c040d705b284424171aa5df07f843728ff2fae450016"},
+]
+
+[[package]]
+name = "cutensor-cu11"
+version = "2.0.2"
+description = "NVIDIA cuTENSOR"
+optional = false
+python-versions = "*"
+files = [
+ {file = "cutensor_cu11-2.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e17003e5f5cf0e83292e9e7e380b64c87a311f8096b3a287a630cbab743ef52f"},
+ {file = "cutensor_cu11-2.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6d37a1164cb02d74322b35b09f018ce51aff078dedee10823820b9d878ebb8c3"},
+ {file = "cutensor_cu11-2.0.2-py3-none-win_amd64.whl", hash = "sha256:4576723d94b81bdc733e1cdb30808551ed1ddeb7d0440df58f56b2555d639f02"},
+]
+
+[[package]]
+name = "cutensor-cu12"
+version = "2.0.2"
+description = "NVIDIA cuTENSOR"
+optional = false
+python-versions = "*"
+files = [
+ {file = "cutensor_cu12-2.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1db559bdfe4345ac19ee66ab7ee49a54e98b1529fc96de812ade3dbc0a90ef47"},
+ {file = "cutensor_cu12-2.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:18c96a4f1e8a559eec626527f5928d5f5b575f6c2b9c45e87309a025ae682334"},
+ {file = "cutensor_cu12-2.0.2-py3-none-win_amd64.whl", hash = "sha256:e2ae37dc9e4a1643dee9318ffdbd212097660e69826328953830cead567fd543"},
+]
+
+[[package]]
+name = "cutensornet-cu11"
+version = "2.4.0.post1"
+description = "cuTensorNet - a component of NVIDIA cuQuantum SDK"
+optional = false
+python-versions = "*"
+files = [
+ {file = "cutensornet_cu11-2.4.0.post1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:110b64a69f245666f5895ae621c9205d5aabb008e3e845f716620d3fd3846633"},
+ {file = "cutensornet_cu11-2.4.0.post1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8a4ef6517086623c702138a8f5f0e8c69a9e71ba6e729b05889778058449ba42"},
+]
+
+[package.dependencies]
+cutensor-cu11 = ">=2.0.1,<3"
+
+[[package]]
+name = "cutensornet-cu12"
+version = "2.4.0.post1"
+description = "cuTensorNet - a component of NVIDIA cuQuantum SDK"
+optional = false
+python-versions = "*"
+files = [
+ {file = "cutensornet_cu12-2.4.0.post1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0356564387165914ea3c07047a1ffef2d92dac74a97b544e63664c6cef0af599"},
+ {file = "cutensornet_cu12-2.4.0.post1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2301e1f6e24fd002b69a89358592898a949a319ba6497bd3d5ff569ec5841d45"},
+]
+
+[package.dependencies]
+cutensor-cu12 = ">=2.0.1,<3"
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+description = "Composable style cycles"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
+ {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
+]
+
+[package.extras]
+docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
+tests = ["pytest", "pytest-cov", "pytest-xdist"]
+
+[[package]]
+name = "cytoolz"
+version = "0.12.3"
+description = "Cython implementation of Toolz: High performance functional utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346"},
+ {file = "cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201"},
+ {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd"},
+ {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f"},
+ {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f"},
+ {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98"},
+ {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808"},
+ {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd"},
+ {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164"},
+ {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf"},
+ {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3"},
+ {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5"},
+ {file = "cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd"},
+ {file = "cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239"},
+ {file = "cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d"},
+ {file = "cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5"},
+ {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d"},
+ {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821"},
+ {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a"},
+ {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc"},
+ {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a"},
+ {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0"},
+ {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4"},
+ {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406"},
+ {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389"},
+ {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2"},
+ {file = "cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605"},
+ {file = "cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6"},
+ {file = "cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716"},
+ {file = "cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293"},
+ {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37"},
+ {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673"},
+ {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e"},
+ {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6"},
+ {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2"},
+ {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2"},
+ {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf"},
+ {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528"},
+ {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2"},
+ {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887"},
+ {file = "cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f"},
+ {file = "cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9a38332cfad2a91e89405b7c18b3f00e2edc951c225accbc217597d3e4e9fde"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f501ae1353071fa5d6677437bbeb1aeb5622067dce0977cedc2c5ec5843b202"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56f899758146a52e2f8cfb3fb6f4ca19c1e5814178c3d584de35f9e4d7166d91"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800f0526adf9e53d3c6acda748f4def1f048adaa780752f154da5cf22aa488a2"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0976a3fcb81d065473173e9005848218ce03ddb2ec7d40dd6a8d2dba7f1c3ae"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c835eab01466cb67d0ce6290601ebef2d82d8d0d0a285ed0d6e46989e4a7a71a"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fba0616fcd487e34b8beec1ad9911d192c62e758baa12fcb44448b9b6feae22"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f6e8207d732651e0204779e1ba5a4925c93081834570411f959b80681f8d333"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8119bf5961091cfe644784d0bae214e273b3b3a479f93ee3baab97bbd995ccfe"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7ad1331cb68afeec58469c31d944a2100cee14eac221553f0d5218ace1a0b25d"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:92c53d508fb8a4463acc85b322fa24734efdc66933a5c8661bdc862103a3373d"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:2c6dd75dae3d84fa8988861ab8b1189d2488cb8a9b8653828f9cd6126b5e7abd"},
+ {file = "cytoolz-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:caf07a97b5220e6334dd32c8b6d8b2bd255ca694eca5dfe914bb5b880ee66cdb"},
+ {file = "cytoolz-0.12.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed0cfb9326747759e2ad81cb6e45f20086a273b67ac3a4c00b19efcbab007c60"},
+ {file = "cytoolz-0.12.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:96a5a0292575c3697121f97cc605baf2fd125120c7dcdf39edd1a135798482ca"},
+ {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76f2f50a789c44d6fd7f773ec43d2a8686781cd52236da03f7f7d7998989bee"},
+ {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905fdccacc64b4beba37f95cab9d792289c80f4d70830b70de2fc66c007ec01"},
+ {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ebe23028eac51251f22ba01dba6587d30aa9c320372ca0c14eeab67118ec3f"},
+ {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c715404a3825e37fe3966fe84c5f8a1f036e7640b2a02dbed96cac0c933451"},
+ {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac0adffc1b6b6a4c5f1fd1dd2161afb720bcc771a91016dc6bdba59af0a5d3"},
+ {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:37441bf4a2a4e2e0fe9c3b0ea5e72db352f5cca03903977ffc42f6f6c5467be9"},
+ {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f04037302049cb30033f7fa4e1d0e44afe35ed6bfcf9b380fc11f2a27d3ed697"},
+ {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f37b60e66378e7a116931d7220f5352186abfcc950d64856038aa2c01944929c"},
+ {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ec9be3e4b6f86ea8b294d34c990c99d2ba6c526ef1e8f46f1d52c263d4f32cd7"},
+ {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e9199c9e3fbf380a92b8042c677eb9e7ed4bccb126de5e9c0d26f5888d96788"},
+ {file = "cytoolz-0.12.3-cp38-cp38-win32.whl", hash = "sha256:18cd61e078bd6bffe088e40f1ed02001387c29174750abce79499d26fa57f5eb"},
+ {file = "cytoolz-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:765b8381d4003ceb1a07896a854eee2c31ebc950a4ae17d1e7a17c2a8feb2a68"},
+ {file = "cytoolz-0.12.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4a52dd2a36b0a91f7aa50ca6c8509057acc481a24255f6cb07b15d339a34e0f"},
+ {file = "cytoolz-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:581f1ce479769fe7eeb9ae6d87eadb230df8c7c5fff32138162cdd99d7fb8fc3"},
+ {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f505d4c6eb79585c8ad0b9dc140ef30a138c880e4e3b40230d642690e36366"},
+ {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59276021619b432a5c21c01cda8320b9cc7dbc40351ffc478b440bfccd5bbdd3"},
+ {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e44f4c25e1e7cf6149b499c74945a14649c8866d36371a2c2d2164e4649e7755"},
+ {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64f8e60c1dd69e4d5e615481f2d57937746f4a6be2d0f86e9e7e3b9e2243b5e"},
+ {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c63186f3bf9d7ef1347bc0537bb9a0b4111a0d7d6e619623cabc18fef0dc3b"},
+ {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fdddb9d988405f24035234f1e8d1653ab2e48cc2404226d21b49a129aefd1d25"},
+ {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6986632d8a969ea1e720990c818dace1a24c11015fd7c59b9fea0b65ef71f726"},
+ {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0ba1cbc4d9cd7571c917f88f4a069568e5121646eb5d82b2393b2cf84712cf2a"},
+ {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7d267ffc9a36c0a9a58c7e0adc9fa82620f22e4a72533e15dd1361f57fc9accf"},
+ {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95e878868a172a41fbf6c505a4b967309e6870e22adc7b1c3b19653d062711fa"},
+ {file = "cytoolz-0.12.3-cp39-cp39-win32.whl", hash = "sha256:8e21932d6d260996f7109f2a40b2586070cb0a0cf1d65781e156326d5ebcc329"},
+ {file = "cytoolz-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:0d8edfbc694af6c9bda4db56643fb8ed3d14e47bec358c2f1417de9a12d6d1fb"},
+ {file = "cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905"},
+ {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d"},
+ {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60"},
+ {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36"},
+ {file = "cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb"},
+ {file = "cytoolz-0.12.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cd88028bb897fba99ddd84f253ca6bef73ecb7bdf3f3cf25bc493f8f97d3c7c5"},
+ {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b19223e7f7bd7a73ec3aa6fdfb73b579ff09c2bc0b7d26857eec2d01a58c76"},
+ {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a79d72b08048a0980a59457c239555f111ac0c8bdc140c91a025f124104dbb4"},
+ {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd70141b32b717696a72b8876e86bc9c6f8eff995c1808e299db3541213ff82"},
+ {file = "cytoolz-0.12.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a1445c91009eb775d479e88954c51d0b4cf9a1e8ce3c503c2672d17252882647"},
+ {file = "cytoolz-0.12.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ca6a9a9300d5bda417d9090107c6d2b007683efc59d63cc09aca0e7930a08a85"},
+ {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6feb903d2a08a4ba2e70e950e862fd3be9be9a588b7c38cee4728150a52918"},
+ {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b6f43f086e5a965d33d62a145ae121b4ccb6e0789ac0acc895ce084fec8c65"},
+ {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:534fa66db8564d9b13872d81d54b6b09ae592c585eb826aac235bd6f1830f8ad"},
+ {file = "cytoolz-0.12.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fea649f979def23150680de1bd1d09682da3b54932800a0f90f29fc2a6c98ba8"},
+ {file = "cytoolz-0.12.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a447247ed312dd64e3a8d9483841ecc5338ee26d6e6fbd29cd373ed030db0240"},
+ {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba3f843aa89f35467b38c398ae5b980a824fdbdb94065adc6ec7c47a0a22f4c7"},
+ {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582c22f97a380211fb36a7b65b1beeb84ea11d82015fa84b054be78580390082"},
+ {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47feb089506fc66e1593cd9ade3945693a9d089a445fbe9a11385cab200b9f22"},
+ {file = "cytoolz-0.12.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba9002d2f043943744a9dc8e50a47362bcb6e6f360dc0a1abcb19642584d87bb"},
+ {file = "cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282"},
+]
+
+[package.dependencies]
+toolz = ">=0.8.0"
+
+[package.extras]
+cython = ["cython"]
+
+[[package]]
+name = "debugpy"
+version = "1.8.2"
+description = "An implementation of the Debug Adapter Protocol for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"},
+ {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"},
+ {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"},
+ {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"},
+ {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"},
+ {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"},
+ {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"},
+ {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"},
+ {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"},
+ {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"},
+ {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"},
+ {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"},
+ {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"},
+ {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"},
+ {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"},
+ {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"},
+ {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"},
+ {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"},
+ {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"},
+ {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"},
+ {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"},
+ {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
+[[package]]
+name = "deprecated"
+version = "1.2.14"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
+ {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
+]
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
+
+[[package]]
+name = "dill"
+version = "0.3.8"
+description = "serialize all of Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
+ {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
+]
+
+[package.extras]
+graph = ["objgraph (>=1.7.2)"]
+profile = ["gprof2dot (>=2022.7.29)"]
+
+[[package]]
+name = "docutils"
+version = "0.19"
+description = "Docutils -- Python Documentation Utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"},
+ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"},
+]
+
+[[package]]
+name = "duet"
+version = "0.2.9"
+description = "A simple future-based async library for python."
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "duet-0.2.9-py3-none-any.whl", hash = "sha256:a16088b68b0faee8aee12cdf4d0a8af060ed958badb44f3e32f123f13f64119a"},
+ {file = "duet-0.2.9.tar.gz", hash = "sha256:d6fa39582e6a3dce1096c47e5fbcbda648a633eed94a38943e68662afa2587f3"},
+]
+
+[package.extras]
+dev-env = ["black (==22.3.0)", "isort (==5.7.*)", "mypy (==0.931.*)", "pylint (==2.10.*)", "pytest (==6.2.*)", "twine (==3.3.*)", "wheel"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "executing"
+version = "2.0.1"
+description = "Get the currently executing AST node of a frame, and other information"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"},
+ {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"},
+]
+
+[package.extras]
+tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
+
+[[package]]
+name = "fancycompleter"
+version = "0.9.1"
+description = "colorful TAB completion for Python prompt"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"},
+ {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"},
+]
+
+[package.dependencies]
+pyreadline = {version = "*", markers = "platform_system == \"Windows\""}
+pyrepl = ">=0.8.2"
+
+[[package]]
+name = "fastjsonschema"
+version = "2.20.0"
+description = "Fastest Python implementation of JSON schema"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"},
+ {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"},
+]
+
+[package.extras]
+devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
+
+[[package]]
+name = "fastrlock"
+version = "0.8.2"
+description = "Fast, re-entrant optimistic lock implemented in Cython"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fastrlock-0.8.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:94e348c72a1fd1f8191f25ea056448e4f5a87b8fbf005b39d290dcb0581a48cd"},
+ {file = "fastrlock-0.8.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5595903444c854b99c42122b87edfe8a37cd698a4eae32f4fd1d2a7b6c115d"},
+ {file = "fastrlock-0.8.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4bbde174a0aff5f6eeba75cf8c4c5d2a316316bc21f03a0bddca0fc3659a6f3"},
+ {file = "fastrlock-0.8.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7a2ccaf88ac0db153e84305d1ef0aa138cea82c6a88309066f6eaa3bc98636cd"},
+ {file = "fastrlock-0.8.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:31a27a2edf482df72b91fe6c6438314d2c65290aa7becc55589d156c9b91f0da"},
+ {file = "fastrlock-0.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:e9904b5b37c3e5bb4a245c56bc4b7e497da57ffb8528f4fc39af9dcb168ee2e1"},
+ {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:43a241655e83e4603a152192cf022d5ca348c2f4e56dfb02e5c9c4c1a32f9cdb"},
+ {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9121a894d74e65557e47e777060a495ab85f4b903e80dd73a3c940ba042920d7"},
+ {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:11bbbbc526363955aeddb9eec4cee2a0012322b7b2f15b54f44454fcf4fd398a"},
+ {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:27786c62a400e282756ae1b090bcd7cfa35f28270cff65a9e7b27a5327a32561"},
+ {file = "fastrlock-0.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08315bde19d0c2e6b06593d5a418be3dc8f9b1ee721afa96867b9853fceb45cf"},
+ {file = "fastrlock-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8b49b5743ede51e0bcf6805741f39f5e0e0fd6a172ba460cb39e3097ba803bb"},
+ {file = "fastrlock-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b443e73a4dfc7b6e0800ea4c13567b9694358e86f53bb2612a51c9e727cac67b"},
+ {file = "fastrlock-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:b3853ed4ce522598dc886160a7bab432a093051af85891fa2f5577c1dcac8ed6"},
+ {file = "fastrlock-0.8.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:790fc19bccbd39426060047e53629f171a44745613bf360a045e9f9c8c4a2cea"},
+ {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:dbdce852e6bb66e1b8c36679d482971d69d93acf1785657522e51b7de30c3356"},
+ {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47713ffe6d4a627fbf078be9836a95ac106b4a0543e3841572c91e292a5d885"},
+ {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:ea96503b918fceaf40443182742b8964d47b65c5ebdea532893cb9479620000c"},
+ {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c6bffa978793bea5e1b00e677062e53a62255439339591b70e209fa1552d5ee0"},
+ {file = "fastrlock-0.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75c07726c8b1a52147fd7987d6baaa318c5dced1416c3f25593e40f56e10755b"},
+ {file = "fastrlock-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88f079335e9da631efa64486c8207564a7bcd0c00526bb9e842e9d5b7e50a6cc"},
+ {file = "fastrlock-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4fb2e77ff04bc4beb71d63c8e064f052ce5a6ea1e001d528d4d7f4b37d736f2e"},
+ {file = "fastrlock-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4c9083ea89ab236b06e9ef2263971db3b4b507195fc7d5eecab95828dcae325"},
+ {file = "fastrlock-0.8.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:98195866d3a9949915935d40a88e4f1c166e82e378f622c88025f2938624a90a"},
+ {file = "fastrlock-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ea9bf5f9fad2b0077e944a7813f91593a4f61adf8faf734a70aed3f2b3a40"},
+ {file = "fastrlock-0.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc1bf0ac8a194313cf6e645e300a8a379674ceed8e0b1e910a2de3e3c28989e"},
+ {file = "fastrlock-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a3dcc876050b8f5cbc0ee84ef1e7f0c1dfe7c148f10098828bc4403683c33f10"},
+ {file = "fastrlock-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:685e656048b59d8dfde8c601f188ad53a4d719eb97080cafc8696cda6d75865e"},
+ {file = "fastrlock-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:fb5363cf0fddd9b50525ddbf64a1e1b28ec4c6dfb28670a940cb1cf988a6786b"},
+ {file = "fastrlock-0.8.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:a74f5a92fa6e51c4f3c69b29c4662088b97be12f40652a21109605a175c81824"},
+ {file = "fastrlock-0.8.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccf39ad5702e33e4d335b48ef9d56e21619b529b7f7471b5211419f380329b62"},
+ {file = "fastrlock-0.8.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:66f2662c640bb71a1016a031eea6eef9d25c2bcdf7ffd1d1ddc5a58f9a1ced04"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:17734e2e5af4c07ddb0fb10bd484e062c22de3be6b67940b9cc6ec2f18fa61ba"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ab91b0c36e95d42e1041a4907e3eefd06c482d53af3c7a77be7e214cc7cd4a63"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b32fdf874868326351a75b1e4c02f97e802147119ae44c52d3d9da193ec34f5b"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2074548a335fcf7d19ebb18d9208da9e33b06f745754466a7e001d2b1c58dd19"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fb04442b6d1e2b36c774919c6bcbe3339c61b337261d4bd57e27932589095af"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1fed2f4797ad68e9982038423018cf08bec5f4ce9fed63a94a790773ed6a795c"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e380ec4e6d8b26e389713995a43cb7fe56baea2d25fe073d4998c4821a026211"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:25945f962c7bd808415cfde3da624d4399d4ea71ed8918538375f16bceb79e1c"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c1719ddc8218b01e82fb2e82e8451bd65076cb96d7bef4477194bbb4305a968"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5460c5ee6ced6d61ec8cd2324ebbe793a4960c4ffa2131ffff480e3b61c99ec5"},
+ {file = "fastrlock-0.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:33145acbad8317584cd64588131c7e1e286beef6280c0009b4544c91fce171d2"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:59344c1d46b7dec97d3f22f1cc930fafe8980b3c5bc9c9765c56738a5f1559e4"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2a1c354f13f22b737621d914f3b4a8434ae69d3027a775e94b3e671756112f9"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:cf81e0278b645004388873e0a1f9e3bc4c9ab8c18e377b14ed1a544be4b18c9a"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b15430b93d7eb3d56f6ff690d2ebecb79ed0e58248427717eba150a508d1cd7"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:067edb0a0805bf61e17a251d5046af59f6e9d2b8ad01222e0ef7a0b7937d5548"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb31fe390f03f7ae886dcc374f1099ec88526631a4cb891d399b68181f154ff0"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:643e1e65b4f5b284427e61a894d876d10459820e93aa1e724dfb415117be24e0"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5dfb78dd600a12f23fc0c3ec58f81336229fdc74501ecf378d1ce5b3f2f313ea"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8ca0fe21458457077e4cb2d81e1ebdb146a00b3e9e2db6180a773f7ea905032"},
+ {file = "fastrlock-0.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d918dfe473291e8bfd8e13223ea5cb9b317bd9f50c280923776c377f7c64b428"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c393af77c659a38bffbca215c0bcc8629ba4299568308dd7e4ff65d62cabed39"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73426f5eb2ecc10626c67cf86bd0af9e00d53e80e5c67d5ce8e18376d6abfa09"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:320fd55bafee3eb069cfb5d6491f811a912758387ef2193840e2663e80e16f48"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c1c91a68926421f5ccbc82c85f83bd3ba593b121a46a1b9a554b3f0dd67a4bf"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ad1bc61c7f6b0e58106aaab034916b6cb041757f708b07fbcdd9d6e1ac629225"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87f4e01b042c84e6090dbc4fbe3415ddd69f6bc0130382323f9d3f1b8dd71b46"},
+ {file = "fastrlock-0.8.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d34546ad2e4a480b94b6797bcc5a322b3c705c4c74c3e4e545c4a3841c1b2d59"},
+ {file = "fastrlock-0.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ebb32d776b61acd49f859a1d16b9e3d84e7b46d0d92aebd58acd54dc38e96664"},
+ {file = "fastrlock-0.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30bdbe4662992348132d03996700e1cf910d141d629179b967b146a22942264e"},
+ {file = "fastrlock-0.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:07ed3c7b3867c05a3d6be4ced200c7767000f3431b9be6da66972822dd86e8be"},
+ {file = "fastrlock-0.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ddf5d247f686aec853ddcc9a1234bfcc6f57b0a0670d2ad82fc25d8ae7e6a15f"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7269bb3fc15587b0c191eecd95831d771a7d80f0c48929e560806b038ff3066c"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adcb9e77aa132cc6c9de2ffe7cf880a20aa8cdba21d367d1da1a412f57bddd5d"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a3b8b5d2935403f1b4b25ae324560e94b59593a38c0d2e7b6c9872126a9622ed"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2587cedbb36c7988e707d83f0f1175c1f882f362b5ebbee25d70218ea33d220d"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9af691a9861027181d4de07ed74f0aee12a9650ac60d0a07f4320bff84b5d95f"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99dd6652bd6f730beadf74ef769d38c6bbd8ee6d1c15c8d138ea680b0594387f"},
+ {file = "fastrlock-0.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d63b6596368dab9e0cc66bf047e7182a56f33b34db141816a4f21f5bf958228"},
+ {file = "fastrlock-0.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ff75c90663d6e8996610d435e71487daa853871ad1770dd83dc0f2fc4997241e"},
+ {file = "fastrlock-0.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e27c3cd27fbd25e5223c5c992b300cd4ee8f0a75c6f222ce65838138d853712c"},
+ {file = "fastrlock-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:dd961a32a7182c3891cdebca417fda67496d5d5de6ae636962254d22723bdf52"},
+ {file = "fastrlock-0.8.2.tar.gz", hash = "sha256:644ec9215cf9c4df8028d8511379a15d9c1af3e16d80e47f1b6fdc6ba118356a"},
+]
+
+[[package]]
+name = "filelock"
+version = "3.15.4"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
+ {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
+[[package]]
+name = "flatbuffers"
+version = "24.3.25"
+description = "The FlatBuffers serialization format for Python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812"},
+ {file = "flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4"},
+]
+
+[[package]]
+name = "fonttools"
+version = "4.53.1"
+description = "Tools to manipulate font files"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"},
+ {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"},
+ {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"},
+ {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"},
+ {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"},
+ {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"},
+ {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"},
+ {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"},
+ {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"},
+ {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"},
+ {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"},
+ {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"},
+ {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"},
+ {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"},
+ {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"},
+ {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"},
+ {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"},
+ {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"},
+ {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"},
+ {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"},
+ {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"},
+ {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"},
+ {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"},
+ {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"},
+ {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"},
+ {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"},
+ {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"},
+ {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"},
+ {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"},
+ {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"},
+ {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"},
+ {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"},
+ {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"},
+ {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"},
+ {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"},
+ {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"},
+ {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"},
+ {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"},
+ {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"},
+ {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"},
+ {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"},
+ {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"},
+]
+
+[package.extras]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["munkres", "pycairo", "scipy"]
+lxml = ["lxml (>=4.0)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=15.1.0)"]
+woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
+
+[[package]]
+name = "fsspec"
+version = "2024.6.1"
+description = "File-system specification"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"},
+ {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"},
+]
+
+[package.extras]
+abfs = ["adlfs"]
+adl = ["adlfs"]
+arrow = ["pyarrow (>=1)"]
+dask = ["dask", "distributed"]
+dev = ["pre-commit", "ruff"]
+doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"]
+dropbox = ["dropbox", "dropboxdrivefs", "requests"]
+full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"]
+fuse = ["fusepy"]
+gcs = ["gcsfs"]
+git = ["pygit2"]
+github = ["requests"]
+gs = ["gcsfs"]
+gui = ["panel"]
+hdfs = ["pyarrow (>=1)"]
+http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"]
+libarchive = ["libarchive-c"]
+oci = ["ocifs"]
+s3 = ["s3fs"]
+sftp = ["paramiko"]
+smb = ["smbprotocol"]
+ssh = ["paramiko"]
+test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"]
+test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"]
+test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"]
+tqdm = ["tqdm"]
+
+[[package]]
+name = "furo"
+version = "2022.12.7"
+description = "A clean customisable Sphinx documentation theme."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "furo-2022.12.7-py3-none-any.whl", hash = "sha256:7cb76c12a25ef65db85ab0743df907573d03027a33631f17d267e598ebb191f7"},
+ {file = "furo-2022.12.7.tar.gz", hash = "sha256:d8008f8efbe7587a97ba533c8b2df1f9c21ee9b3e5cad0d27f61193d38b1a986"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+pygments = ">=2.7"
+sphinx = ">=5.0,<7.0"
+sphinx-basic-ng = "*"
+
+[[package]]
+name = "future"
+version = "1.0.0"
+description = "Clean single-source support for Python 3 and 2"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"},
+ {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"},
+]
+
+[[package]]
+name = "gast"
+version = "0.6.0"
+description = "Python AST that abstracts the underlying Python version"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+files = [
+ {file = "gast-0.6.0-py3-none-any.whl", hash = "sha256:52b182313f7330389f72b069ba00f174cfe2a06411099547288839c6cbafbd54"},
+ {file = "gast-0.6.0.tar.gz", hash = "sha256:88fc5300d32c7ac6ca7b515310862f71e6fdf2c029bbec7c66c0f5dd47b6b1fb"},
+]
+
+[[package]]
+name = "google-api-core"
+version = "2.19.1"
+description = "Google API client core library"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"},
+ {file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"},
+]
+
+[package.dependencies]
+google-auth = ">=2.14.1,<3.0.dev0"
+googleapis-common-protos = ">=1.56.2,<2.0.dev0"
+grpcio = [
+ {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
+ {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
+]
+grpcio-status = [
+ {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
+ {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
+]
+proto-plus = ">=1.22.3,<2.0.0dev"
+protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
+requests = ">=2.18.0,<3.0.0.dev0"
+
+[package.extras]
+grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"]
+grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
+grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
+
+[[package]]
+name = "google-auth"
+version = "2.32.0"
+description = "Google Authentication Library"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "google_auth-2.32.0-py2.py3-none-any.whl", hash = "sha256:53326ea2ebec768070a94bee4e1b9194c9646ea0c2bd72422785bd0f9abfad7b"},
+ {file = "google_auth-2.32.0.tar.gz", hash = "sha256:49315be72c55a6a37d62819e3573f6b416aca00721f7e3e31a008d928bf64022"},
+]
+
+[package.dependencies]
+cachetools = ">=2.0.0,<6.0"
+pyasn1-modules = ">=0.2.1"
+rsa = ">=3.1.4,<5"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"]
+enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"]
+pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
+reauth = ["pyu2f (>=0.1.5)"]
+requests = ["requests (>=2.20.0,<3.0.0.dev0)"]
+
+[[package]]
+name = "google-pasta"
+version = "0.2.0"
+description = "pasta is an AST-based Python refactoring library"
+optional = false
+python-versions = "*"
+files = [
+ {file = "google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e"},
+ {file = "google_pasta-0.2.0-py2-none-any.whl", hash = "sha256:4612951da876b1a10fe3960d7226f0c7682cf901e16ac06e473b267a5afa8954"},
+ {file = "google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed"},
+]
+
+[package.dependencies]
+six = "*"
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.63.2"
+description = "Common protobufs used in Google APIs"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"},
+ {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"},
+]
+
+[package.dependencies]
+protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
+
+[package.extras]
+grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
+
+[[package]]
+name = "grpcio"
+version = "1.65.1"
+description = "HTTP/2-based RPC framework"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "grpcio-1.65.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:3dc5f928815b8972fb83b78d8db5039559f39e004ec93ebac316403fe031a062"},
+ {file = "grpcio-1.65.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8333ca46053c35484c9f2f7e8d8ec98c1383a8675a449163cea31a2076d93de8"},
+ {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:7af64838b6e615fff0ec711960ed9b6ee83086edfa8c32670eafb736f169d719"},
+ {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb64b4166362d9326f7efbf75b1c72106c1aa87f13a8c8b56a1224fac152f5c"},
+ {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8422dc13ad93ec8caa2612b5032a2b9cd6421c13ed87f54db4a3a2c93afaf77"},
+ {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4effc0562b6c65d4add6a873ca132e46ba5e5a46f07c93502c37a9ae7f043857"},
+ {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a6c71575a2fedf259724981fd73a18906513d2f306169c46262a5bae956e6364"},
+ {file = "grpcio-1.65.1-cp310-cp310-win32.whl", hash = "sha256:34966cf526ef0ea616e008d40d989463e3db157abb213b2f20c6ce0ae7928875"},
+ {file = "grpcio-1.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:ca931de5dd6d9eb94ff19a2c9434b23923bce6f767179fef04dfa991f282eaad"},
+ {file = "grpcio-1.65.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bbb46330cc643ecf10bd9bd4ca8e7419a14b6b9dedd05f671c90fb2c813c6037"},
+ {file = "grpcio-1.65.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d827a6fb9215b961eb73459ad7977edb9e748b23e3407d21c845d1d8ef6597e5"},
+ {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:6e71aed8835f8d9fbcb84babc93a9da95955d1685021cceb7089f4f1e717d719"},
+ {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1c84560b3b2d34695c9ba53ab0264e2802721c530678a8f0a227951f453462"},
+ {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27adee2338d697e71143ed147fe286c05810965d5d30ec14dd09c22479bfe48a"},
+ {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f62652ddcadc75d0e7aa629e96bb61658f85a993e748333715b4ab667192e4e8"},
+ {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:71a05fd814700dd9cb7d9a507f2f6a1ef85866733ccaf557eedacec32d65e4c2"},
+ {file = "grpcio-1.65.1-cp311-cp311-win32.whl", hash = "sha256:b590f1ad056294dfaeac0b7e1b71d3d5ace638d8dd1f1147ce4bd13458783ba8"},
+ {file = "grpcio-1.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:12e9bdf3b5fd48e5fbe5b3da382ad8f97c08b47969f3cca81dd9b36b86ed39e2"},
+ {file = "grpcio-1.65.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:54cb822e177374b318b233e54b6856c692c24cdbd5a3ba5335f18a47396bac8f"},
+ {file = "grpcio-1.65.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aaf3c54419a28d45bd1681372029f40e5bfb58e5265e3882eaf21e4a5f81a119"},
+ {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:557de35bdfbe8bafea0a003dbd0f4da6d89223ac6c4c7549d78e20f92ead95d9"},
+ {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bfd95ef3b097f0cc86ade54eafefa1c8ed623aa01a26fbbdcd1a3650494dd11"},
+ {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e6a8f3d6c41e6b642870afe6cafbaf7b61c57317f9ec66d0efdaf19db992b90"},
+ {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1faaf7355ceed07ceaef0b9dcefa4c98daf1dd8840ed75c2de128c3f4a4d859d"},
+ {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:60f1f38eed830488ad2a1b11579ef0f345ff16fffdad1d24d9fbc97ba31804ff"},
+ {file = "grpcio-1.65.1-cp312-cp312-win32.whl", hash = "sha256:e75acfa52daf5ea0712e8aa82f0003bba964de7ae22c26d208cbd7bc08500177"},
+ {file = "grpcio-1.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff5a84907e51924973aa05ed8759210d8cdae7ffcf9e44fd17646cf4a902df59"},
+ {file = "grpcio-1.65.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:1fbd6331f18c3acd7e09d17fd840c096f56eaf0ef830fbd50af45ae9dc8dfd83"},
+ {file = "grpcio-1.65.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de5b6be29116e094c5ef9d9e4252e7eb143e3d5f6bd6d50a78075553ab4930b0"},
+ {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e4a3cdba62b2d6aeae6027ae65f350de6dc082b72e6215eccf82628e79efe9ba"},
+ {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941c4869aa229d88706b78187d60d66aca77fe5c32518b79e3c3e03fc26109a2"},
+ {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f40cebe5edb518d78b8131e87cb83b3ee688984de38a232024b9b44e74ee53d3"},
+ {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2ca684ba331fb249d8a1ce88db5394e70dbcd96e58d8c4b7e0d7b141a453dce9"},
+ {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8558f0083ddaf5de64a59c790bffd7568e353914c0c551eae2955f54ee4b857f"},
+ {file = "grpcio-1.65.1-cp38-cp38-win32.whl", hash = "sha256:8d8143a3e3966f85dce6c5cc45387ec36552174ba5712c5dc6fcc0898fb324c0"},
+ {file = "grpcio-1.65.1-cp38-cp38-win_amd64.whl", hash = "sha256:76e81a86424d6ca1ce7c16b15bdd6a964a42b40544bf796a48da241fdaf61153"},
+ {file = "grpcio-1.65.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb5175f45c980ff418998723ea1b3869cce3766d2ab4e4916fbd3cedbc9d0ed3"},
+ {file = "grpcio-1.65.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b12c1aa7b95abe73b3e04e052c8b362655b41c7798da69f1eaf8d186c7d204df"},
+ {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3019fb50128b21a5e018d89569ffaaaa361680e1346c2f261bb84a91082eb3d3"},
+ {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ae15275ed98ea267f64ee9ddedf8ecd5306a5b5bb87972a48bfe24af24153e8"},
+ {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f096ffb881f37e8d4f958b63c74bfc400c7cebd7a944b027357cd2fb8d91a57"},
+ {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2f56b5a68fdcf17a0a1d524bf177218c3c69b3947cb239ea222c6f1867c3ab68"},
+ {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:941596d419b9736ab548aa0feb5bbba922f98872668847bf0720b42d1d227b9e"},
+ {file = "grpcio-1.65.1-cp39-cp39-win32.whl", hash = "sha256:5fd7337a823b890215f07d429f4f193d24b80d62a5485cf88ee06648591a0c57"},
+ {file = "grpcio-1.65.1-cp39-cp39-win_amd64.whl", hash = "sha256:1bceeec568372cbebf554eae1b436b06c2ff24cfaf04afade729fb9035408c6c"},
+ {file = "grpcio-1.65.1.tar.gz", hash = "sha256:3c492301988cd720cd145d84e17318d45af342e29ef93141228f9cd73222368b"},
+]
+
+[package.extras]
+protobuf = ["grpcio-tools (>=1.65.1)"]
+
+[[package]]
+name = "grpcio-status"
+version = "1.62.2"
+description = "Status proto mapping for gRPC"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "grpcio-status-1.62.2.tar.gz", hash = "sha256:62e1bfcb02025a1cd73732a2d33672d3e9d0df4d21c12c51e0bbcaf09bab742a"},
+ {file = "grpcio_status-1.62.2-py3-none-any.whl", hash = "sha256:206ddf0eb36bc99b033f03b2c8e95d319f0044defae9b41ae21408e7e0cda48f"},
+]
+
+[package.dependencies]
+googleapis-common-protos = ">=1.5.5"
+grpcio = ">=1.62.2"
+protobuf = ">=4.21.6"
+
+[[package]]
+name = "grpcio-status"
+version = "1.65.1"
+description = "Status proto mapping for gRPC"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "grpcio_status-1.65.1-py3-none-any.whl", hash = "sha256:0ec2070f7dbcc2fe78a7b34233a2a00f8ced727d2f1dec1af422d628cf86b92c"},
+ {file = "grpcio_status-1.65.1.tar.gz", hash = "sha256:740d68d4a1824e59063f394df05171886262d5367b82256d54aac8aa7c5c79bf"},
+]
+
+[package.dependencies]
+googleapis-common-protos = ">=1.5.5"
+grpcio = ">=1.65.1"
+protobuf = ">=5.26.1,<6.0dev"
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+
+[[package]]
+name = "h5py"
+version = "3.11.0"
+description = "Read and write HDF5 files from Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"},
+ {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"},
+ {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"},
+ {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"},
+ {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"},
+ {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"},
+ {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"},
+ {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"},
+ {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"},
+ {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"},
+ {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"},
+ {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"},
+ {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"},
+ {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"},
+ {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"},
+ {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"},
+ {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"},
+ {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"},
+ {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"},
+ {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"},
+ {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"},
+]
+
+[package.dependencies]
+numpy = ">=1.17.3"
+
+[[package]]
+name = "httpcore"
+version = "0.16.3"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
+ {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+]
+
+[package.dependencies]
+anyio = ">=3.0,<5.0"
+certifi = "*"
+h11 = ">=0.13,<0.15"
+sniffio = "==1.*"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+
+[[package]]
+name = "httpx"
+version = "0.23.3"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
+ {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
+]
+
+[package.dependencies]
+certifi = "*"
+httpcore = ">=0.15.0,<0.17.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+
+[[package]]
+name = "hyperopt"
+version = "0.2.7"
+description = "Distributed Asynchronous Hyperparameter Optimization"
+optional = false
+python-versions = "*"
+files = [
+ {file = "hyperopt-0.2.7-py2.py3-none-any.whl", hash = "sha256:f3046d91fe4167dbf104365016596856b2524a609d22f047a066fc1ac796427c"},
+ {file = "hyperopt-0.2.7.tar.gz", hash = "sha256:1bf89ae58050bbd32c7307199046117feee245c2fd9ab6255c7308522b7ca149"},
+]
+
+[package.dependencies]
+cloudpickle = "*"
+future = "*"
+networkx = ">=2.2"
+numpy = "*"
+py4j = "*"
+scipy = "*"
+six = "*"
+tqdm = "*"
+
+[package.extras]
+atpe = ["lightgbm", "scikit-learn"]
+dev = ["black", "nose", "pre-commit", "pytest"]
+mongotrials = ["pymongo"]
+sparktrials = ["pyspark"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
+ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.0.0"
+description = "Read metadata from Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
+ {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
+]
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+perf = ["ipython"]
+test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
+
+[[package]]
+name = "importlib-resources"
+version = "6.4.0"
+description = "Read resources from Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
+ {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
+]
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "intel-openmp"
+version = "2021.4.0"
+description = "Intel OpenMP* Runtime Library"
+optional = false
+python-versions = "*"
+files = [
+ {file = "intel_openmp-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:41c01e266a7fdb631a7609191709322da2bbf24b252ba763f125dd651bcc7675"},
+ {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:3b921236a38384e2016f0f3d65af6732cf2c12918087128a9163225451e776f2"},
+ {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:e2240ab8d01472fed04f3544a878cda5da16c26232b7ea1b59132dbfb48b186e"},
+ {file = "intel_openmp-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:6e863d8fd3d7e8ef389d52cf97a50fe2afe1a19247e8c0d168ce021546f96fc9"},
+ {file = "intel_openmp-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:eef4c8bcc8acefd7f5cd3b9384dbf73d59e2c99fc56545712ded913f43c4a94f"},
+]
+
+[[package]]
+name = "ipykernel"
+version = "6.29.5"
+description = "IPython Kernel for Jupyter"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"},
+ {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+comm = ">=0.1.1"
+debugpy = ">=1.6.5"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=24"
+tornado = ">=6.1"
+traitlets = ">=5.4.0"
+
+[package.extras]
+cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
+pyqt5 = ["pyqt5"]
+pyside6 = ["pyside6"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "ipython"
+version = "8.18.1"
+description = "IPython: Productive Interactive Computing"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"},
+ {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+prompt-toolkit = ">=3.0.41,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
+
+[package.extras]
+all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.3"
+description = "Jupyter interactive widgets"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"},
+ {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"},
+]
+
+[package.dependencies]
+comm = ">=0.1.3"
+ipython = ">=6.1.0"
+jupyterlab-widgets = ">=3.0.11,<3.1.0"
+traitlets = ">=4.3.1"
+widgetsnbextension = ">=4.0.11,<4.1.0"
+
+[package.extras]
+test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
+
+[[package]]
+name = "iso8601"
+version = "1.1.0"
+description = "Simple module to parse ISO 8601 dates"
+optional = false
+python-versions = ">=3.6.2,<4.0"
+files = [
+ {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"},
+ {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"},
+]
+
+[[package]]
+name = "isort"
+version = "5.13.2"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "jedi"
+version = "0.19.1"
+description = "An autocompletion tool for Python that can be used for text editors."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
+ {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
+]
+
+[package.dependencies]
+parso = ">=0.8.3,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
+[[package]]
+name = "jinja2"
+version = "3.1.4"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "joblib"
+version = "1.4.2"
+description = "Lightweight pipelining with Python functions"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"},
+ {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"},
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.17.3"
+description = "An implementation of JSON Schema validation for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"},
+ {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"},
+]
+
+[package.dependencies]
+attrs = ">=17.4.0"
+pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2"
+
+[package.extras]
+format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
+format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
+
+[[package]]
+name = "jupyter-client"
+version = "8.6.2"
+description = "Jupyter protocol implementation and client libraries"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"},
+ {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-core"
+version = "5.7.2"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"},
+ {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"},
+]
+
+[package.dependencies]
+platformdirs = ">=2.5"
+pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = ">=5.3"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
+test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.3.0"
+description = "Pygments theme using JupyterLab CSS variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"},
+ {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"},
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.11"
+description = "Jupyter interactive widgets for JupyterLab"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"},
+ {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"},
+]
+
+[[package]]
+name = "keras"
+version = "3.4.1"
+description = "Multi-backend Keras."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "keras-3.4.1-py3-none-any.whl", hash = "sha256:15599c51e2090c12f39de6db6489a0cf265ddf6653f0731b82db5af2bfa19105"},
+ {file = "keras-3.4.1.tar.gz", hash = "sha256:34cd9aeaa008914715149234c215657ca758e1b473bd2aab2e211ac967d1f8fe"},
+]
+
+[package.dependencies]
+absl-py = "*"
+h5py = "*"
+ml-dtypes = "*"
+namex = "*"
+numpy = "*"
+optree = "*"
+packaging = "*"
+rich = "*"
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.5"
+description = "A fast implementation of the Cassowary constraint solver"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"},
+ {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
+]
+
+[[package]]
+name = "lark"
+version = "0.11.3"
+description = "a modern parsing library"
+optional = false
+python-versions = "*"
+files = [
+ {file = "lark-0.11.3.tar.gz", hash = "sha256:3100d9749b5a85735ec428b83100876a5da664804579e729c23a36341f961e7e"},
+]
+
+[package.extras]
+atomic-cache = ["atomicwrites"]
+nearley = ["js2py"]
+regex = ["regex"]
+
+[[package]]
+name = "latexcodec"
+version = "3.0.0"
+description = "A lexer and codec to work with LaTeX code in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "latexcodec-3.0.0-py3-none-any.whl", hash = "sha256:6f3477ad5e61a0a99bd31a6a370c34e88733a6bad9c921a3ffcfacada12f41a7"},
+ {file = "latexcodec-3.0.0.tar.gz", hash = "sha256:917dc5fe242762cc19d963e6548b42d63a118028cdd3361d62397e3b638b6bc5"},
+]
+
+[[package]]
+name = "libclang"
+version = "18.1.1"
+description = "Clang Python Bindings, mirrored from the official LLVM repo: https://github.com/llvm/llvm-project/tree/main/clang/bindings/python, to make the installation process easier."
+optional = false
+python-versions = "*"
+files = [
+ {file = "libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a"},
+ {file = "libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5"},
+ {file = "libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8"},
+ {file = "libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b"},
+ {file = "libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592"},
+ {file = "libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe"},
+ {file = "libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f"},
+ {file = "libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb"},
+ {file = "libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8"},
+ {file = "libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250"},
+]
+
+[[package]]
+name = "llvmlite"
+version = "0.43.0"
+description = "lightweight wrapper around basic LLVM functionality"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"},
+ {file = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"},
+ {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead"},
+ {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a"},
+ {file = "llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed"},
+ {file = "llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98"},
+ {file = "llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57"},
+ {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2"},
+ {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749"},
+ {file = "llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91"},
+ {file = "llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7"},
+ {file = "llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7"},
+ {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f"},
+ {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844"},
+ {file = "llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9"},
+ {file = "llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c"},
+ {file = "llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8"},
+ {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a"},
+ {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867"},
+ {file = "llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4"},
+ {file = "llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5"},
+]
+
+[[package]]
+name = "markdown"
+version = "3.6"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"},
+ {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+ {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.9.1"
+description = "Python plotting package"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"},
+ {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"},
+ {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"},
+ {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"},
+ {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"},
+ {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"},
+ {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"},
+ {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"},
+ {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"},
+ {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"},
+ {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"},
+ {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"},
+ {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"},
+ {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"},
+ {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"},
+ {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"},
+ {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"},
+ {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"},
+ {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"},
+ {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"},
+ {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"},
+ {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"},
+ {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"},
+ {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"},
+ {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"},
+ {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"},
+ {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"},
+ {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"},
+ {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"},
+]
+
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""}
+kiwisolver = ">=1.3.1"
+numpy = ">=1.23"
+packaging = ">=20.0"
+pillow = ">=8"
+pyparsing = ">=2.3.1"
+python-dateutil = ">=2.7"
+
+[package.extras]
+dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.7"
+description = "Inline Matplotlib backend for Jupyter"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
+ {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
+]
+
+[package.dependencies]
+traitlets = "*"
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "mistune"
+version = "3.0.2"
+description = "A sane and fast Markdown parser with useful plugins and renderers"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"},
+ {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"},
+]
+
+[[package]]
+name = "mkl"
+version = "2021.4.0"
+description = "Intel® oneAPI Math Kernel Library"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mkl-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:67460f5cd7e30e405b54d70d1ed3ca78118370b65f7327d495e9c8847705e2fb"},
+ {file = "mkl-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:636d07d90e68ccc9630c654d47ce9fdeb036bb46e2b193b3a9ac8cfea683cce5"},
+ {file = "mkl-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:398dbf2b0d12acaf54117a5210e8f191827f373d362d796091d161f610c1ebfb"},
+ {file = "mkl-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:439c640b269a5668134e3dcbcea4350459c4a8bc46469669b2d67e07e3d330e8"},
+ {file = "mkl-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:ceef3cafce4c009dd25f65d7ad0d833a0fbadc3d8903991ec92351fe5de1e718"},
+]
+
+[package.dependencies]
+intel-openmp = "==2021.*"
+tbb = "==2021.*"
+
+[[package]]
+name = "ml-dtypes"
+version = "0.4.0"
+description = ""
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "ml_dtypes-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93afe37f3a879d652ec9ef1fc47612388890660a2657fbb5747256c3b818fd81"},
+ {file = "ml_dtypes-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb83fd064db43e67e67d021e547698af4c8d5c6190f2e9b1c53c09f6ff5531d"},
+ {file = "ml_dtypes-0.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03e7cda6ef164eed0abb31df69d2c00c3a5ab3e2610b6d4c42183a43329c72a5"},
+ {file = "ml_dtypes-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a15d96d090aebb55ee85173d1775ae325a001aab607a76c8ea0b964ccd6b5364"},
+ {file = "ml_dtypes-0.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdf689be7351cc3c95110c910c1b864002f113e682e44508910c849e144f3df1"},
+ {file = "ml_dtypes-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c83e4d443962d891d51669ff241d5aaad10a8d3d37a81c5532a45419885d591c"},
+ {file = "ml_dtypes-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e2f4237b459a63c97c2c9f449baa637d7e4c20addff6a9bac486f22432f3b6"},
+ {file = "ml_dtypes-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:75b4faf99d0711b81f393db36d210b4255fd419f6f790bc6c1b461f95ffb7a9e"},
+ {file = "ml_dtypes-0.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ee9f91d4c4f9959a7e1051c141dc565f39e54435618152219769e24f5e9a4d06"},
+ {file = "ml_dtypes-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad6849a2db386b38e4d54fe13eb3293464561780531a918f8ef4c8169170dd49"},
+ {file = "ml_dtypes-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa32979ebfde3a0d7c947cafbf79edc1ec77ac05ad0780ee86c1d8df70f2259"},
+ {file = "ml_dtypes-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b67ec73a697c88c1122038e0de46520e48dc2ec876d42cf61bc5efe3c0b7675"},
+ {file = "ml_dtypes-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:41affb38fdfe146e3db226cf2953021184d6f0c4ffab52136613e9601706e368"},
+ {file = "ml_dtypes-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43cf4356a0fe2eeac6d289018d0734e17a403bdf1fd911953c125dd0358edcc0"},
+ {file = "ml_dtypes-0.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1724ddcdf5edbaf615a62110af47407f1719b8d02e68ccee60683acb5f74da1"},
+ {file = "ml_dtypes-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:723af6346447268a3cf0b7356e963d80ecb5732b5279b2aa3fa4b9fc8297c85e"},
+ {file = "ml_dtypes-0.4.0.tar.gz", hash = "sha256:eaf197e72f4f7176a19fe3cb8b61846b38c6757607e7bf9cd4b1d84cd3e74deb"},
+]
+
+[package.dependencies]
+numpy = [
+ {version = ">=1.21.2", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
+ {version = ">1.20", markers = "python_version < \"3.10\""},
+ {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
+ {version = ">=1.23.3", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
+]
+
+[package.extras]
+dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+description = "Python library for arbitrary-precision floating-point arithmetic"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
+ {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
+]
+
+[package.extras]
+develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"]
+docs = ["sphinx"]
+gmpy = ["gmpy2 (>=2.1.0a4)"]
+tests = ["pytest (>=4.6)"]
+
+[[package]]
+name = "msgpack"
+version = "1.0.8"
+description = "MessagePack serializer"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"},
+ {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"},
+ {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"},
+ {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"},
+ {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"},
+ {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"},
+ {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"},
+ {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"},
+ {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"},
+ {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"},
+ {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"},
+ {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"},
+ {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"},
+ {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"},
+ {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"},
+ {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"},
+ {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"},
+ {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"},
+ {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"},
+ {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"},
+ {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"},
+ {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"},
+ {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"},
+ {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"},
+ {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"},
+ {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"},
+ {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"},
+ {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"},
+ {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"},
+ {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"},
+ {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"},
+ {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"},
+ {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"},
+ {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"},
+ {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"},
+ {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"},
+ {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"},
+ {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"},
+ {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"},
+ {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"},
+ {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"},
+ {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"},
+ {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"},
+ {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"},
+ {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"},
+ {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"},
+ {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"},
+ {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"},
+ {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"},
+ {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"},
+ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"},
+ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"},
+ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
+ {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
+ {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
+ {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"},
+]
+
+[[package]]
+name = "namex"
+version = "0.0.8"
+description = "A simple utility to separate the implementation of your Python package and its public API surface."
+optional = false
+python-versions = "*"
+files = [
+ {file = "namex-0.0.8-py3-none-any.whl", hash = "sha256:7ddb6c2bb0e753a311b7590f84f6da659dd0c05e65cb89d519d54c0a250c0487"},
+ {file = "namex-0.0.8.tar.gz", hash = "sha256:32a50f6c565c0bb10aa76298c959507abdc0e850efe085dc38f3440fcb3aa90b"},
+]
+
+[[package]]
+name = "nbclient"
+version = "0.10.0"
+description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"},
+ {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"},
+]
+
+[package.dependencies]
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+nbformat = ">=5.1"
+traitlets = ">=5.4"
+
+[package.extras]
+dev = ["pre-commit"]
+docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"]
+test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"]
+
+[[package]]
+name = "nbconvert"
+version = "7.16.4"
+description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"},
+ {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+bleach = "!=5.0.0"
+defusedxml = "*"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
+jinja2 = ">=3.0"
+jupyter-core = ">=4.7"
+jupyterlab-pygments = "*"
+markupsafe = ">=2.0"
+mistune = ">=2.0.3,<4"
+nbclient = ">=0.5.0"
+nbformat = ">=5.7"
+packaging = "*"
+pandocfilters = ">=1.4.1"
+pygments = ">=2.4.1"
+tinycss2 = "*"
+traitlets = ">=5.1"
+
+[package.extras]
+all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
+docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
+qtpdf = ["pyqtwebengine (>=5.15)"]
+qtpng = ["pyqtwebengine (>=5.15)"]
+serve = ["tornado (>=6.1)"]
+test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"]
+webpdf = ["playwright"]
+
+[[package]]
+name = "nbformat"
+version = "5.10.4"
+description = "The Jupyter Notebook format"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"},
+ {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"},
+]
+
+[package.dependencies]
+fastjsonschema = ">=2.15"
+jsonschema = ">=2.6"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+traitlets = ">=5.1"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["pep440", "pre-commit", "pytest", "testpath"]
+
+[[package]]
+name = "nbsphinx"
+version = "0.8.12"
+description = "Jupyter Notebook Tools for Sphinx"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "nbsphinx-0.8.12-py3-none-any.whl", hash = "sha256:c15b681c7fce287000856f91fe1edac50d29f7b0c15bbc746fbe55c8eb84750b"},
+ {file = "nbsphinx-0.8.12.tar.gz", hash = "sha256:76570416cdecbeb21dbf5c3d6aa204ced6c1dd7ebef4077b5c21b8c6ece9533f"},
+]
+
+[package.dependencies]
+docutils = "*"
+jinja2 = "*"
+nbconvert = "!=5.4"
+nbformat = "*"
+sphinx = ">=1.8"
+traitlets = ">=5"
+
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+description = "Patch asyncio to allow nested event loops"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
+ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
+]
+
+[[package]]
+name = "networkx"
+version = "3.2.1"
+description = "Python package for creating and manipulating graphs and networks"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"},
+ {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"]
+developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"]
+doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"]
+extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"]
+test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
+[[package]]
+name = "numba"
+version = "0.60.0"
+description = "compiling Python code using LLVM"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"},
+ {file = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"},
+ {file = "numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781"},
+ {file = "numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e"},
+ {file = "numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198"},
+ {file = "numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8"},
+ {file = "numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b"},
+ {file = "numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703"},
+ {file = "numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8"},
+ {file = "numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2"},
+ {file = "numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404"},
+ {file = "numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c"},
+ {file = "numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e"},
+ {file = "numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d"},
+ {file = "numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347"},
+ {file = "numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74"},
+ {file = "numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449"},
+ {file = "numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b"},
+ {file = "numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25"},
+ {file = "numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab"},
+ {file = "numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16"},
+]
+
+[package.dependencies]
+llvmlite = "==0.43.*"
+numpy = ">=1.22,<2.1"
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
+ {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
+ {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
+ {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
+ {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
+ {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
+ {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
+ {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
+ {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
+ {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
+ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.1.3.1"
+description = "CUBLAS native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"},
+ {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"},
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.1.105"
+description = "CUDA profiling tools runtime libs."
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"},
+ {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"},
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.1.105"
+description = "NVRTC native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"},
+ {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"},
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.1.105"
+description = "CUDA Runtime native Libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"},
+ {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"},
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "8.9.2.26"
+description = "cuDNN runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"},
+]
+
+[package.dependencies]
+nvidia-cublas-cu12 = "*"
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.0.2.54"
+description = "CUFFT native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"},
+ {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"},
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.2.106"
+description = "CURAND native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"},
+ {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"},
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.4.5.107"
+description = "CUDA solver native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"},
+ {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"},
+]
+
+[package.dependencies]
+nvidia-cublas-cu12 = "*"
+nvidia-cusparse-cu12 = "*"
+nvidia-nvjitlink-cu12 = "*"
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.1.0.106"
+description = "CUSPARSE native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"},
+ {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"},
+]
+
+[package.dependencies]
+nvidia-nvjitlink-cu12 = "*"
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.20.5"
+description = "NVIDIA Collective Communication Library (NCCL) Runtime"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1fc150d5c3250b170b29410ba682384b14581db722b2531b0d8d33c595f33d01"},
+ {file = "nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:057f6bf9685f75215d0c53bf3ac4a10b3e6578351de307abad9e18a99182af56"},
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.5.82"
+description = "Nvidia JIT LTO Library"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_aarch64.whl", hash = "sha256:98103729cc5226e13ca319a10bbf9433bbbd44ef64fe72f45f067cacc14b8d27"},
+ {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f9b37bc5c8cf7509665cb6ada5aaa0ce65618f2332b7d3e78e9790511f111212"},
+ {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-win_amd64.whl", hash = "sha256:e782564d705ff0bf61ac3e1bf730166da66dd2fe9012f111ede5fc49b64ae697"},
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.1.105"
+description = "NVIDIA Tools Extension"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"},
+ {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"},
+]
+
+[[package]]
+name = "openqasm3"
+version = "1.0.0"
+description = "Reference OpenQASM AST in Python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "openqasm3-1.0.0-py3-none-any.whl", hash = "sha256:d4371737b4a49b0d56248ed3d30766a94000bccfb43303ec9c7ead351a1b6cc3"},
+ {file = "openqasm3-1.0.0.tar.gz", hash = "sha256:3f2bb1cca855cff114e046bac22d59adbf9b754cac6398961aa6d22588fb688e"},
+]
+
+[package.dependencies]
+antlr4-python3-runtime = {version = ">=4.7,<4.14", optional = true, markers = "extra == \"parser\""}
+importlib-metadata = {version = "*", optional = true, markers = "python_version < \"3.10\" and extra == \"parser\""}
+
+[package.extras]
+all = ["antlr4-python3-runtime (>=4.7,<4.14)", "importlib-metadata", "pytest (>=6.0)", "pyyaml"]
+parser = ["antlr4-python3-runtime (>=4.7,<4.14)", "importlib-metadata"]
+tests = ["pytest (>=6.0)", "pyyaml"]
+
+[[package]]
+name = "opt-einsum"
+version = "3.3.0"
+description = "Optimizing numpys einsum function"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"},
+ {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"},
+]
+
+[package.dependencies]
+numpy = ">=1.7"
+
+[package.extras]
+docs = ["numpydoc", "sphinx (==1.2.3)", "sphinx-rtd-theme", "sphinxcontrib-napoleon"]
+tests = ["pytest", "pytest-cov", "pytest-pep8"]
+
+[[package]]
+name = "optree"
+version = "0.12.1"
+description = "Optimized PyTree Utilities."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "optree-0.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:349aafac463642979f7fe7ca3aa9e2fa8a5a0f81ef7af6946a075b797673e600"},
+ {file = "optree-0.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8046cbbcd5f7494ba7c6811e44a6d2867216f2bdb7cef980a9a62e31d39270c"},
+ {file = "optree-0.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b43c09cf9dd28aed2efc163f4bb4808d7fad54250812960bf349399ba6972e16"},
+ {file = "optree-0.12.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c2f2e0e3978558bc8f7df8c5a999674097dd0dc71363210783eb8d7a6da8ef9"},
+ {file = "optree-0.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e323744d083bd8b4648c9ff2383f01bfbc33098656d56fdd984b2263ef905f3"},
+ {file = "optree-0.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80e0d4eba4a65d4c6f2002ed949142a40933b8185523894659c26c34693c4086"},
+ {file = "optree-0.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efffa3814ab8e3aaf7bf88495e4b6d263de9689d6f02dfa4490f8f64736806ac"},
+ {file = "optree-0.12.1-cp310-cp310-win32.whl", hash = "sha256:4ee926120887404e92877c99714b960bc29f572e8db69fd2e934022d80452f91"},
+ {file = "optree-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:a11e58d7c0a71a48d74ca0a6715f4c0932c6f9409ba93d600e3326df4cf778ae"},
+ {file = "optree-0.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:509bddd38dae8c4e8d6b988f514b7a9fe803ca916b11af67b40520f0b1eeeaef"},
+ {file = "optree-0.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06d6ef39b3ef9920d6cdb6d3d1d2804a37092d24dc406c4cb9b46cd6c9a44e89"},
+ {file = "optree-0.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce7cb233e87a2dc127b8ec82bd61f098e6ff1e57d0a09dc110a17b38bfd73034"},
+ {file = "optree-0.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ca77b810cf5959e6930d56534ecbecc4300f5e5fa14b977030265c1c8eab6c"},
+ {file = "optree-0.12.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2de1297b2bf019379ab86103e31caa97c8a08628f0c8b58cd7709f9048c589eb"},
+ {file = "optree-0.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:404cf2decd8fb6a1a8f6fef623c98873cdf7ae086aeb8909d104cd321d829ba0"},
+ {file = "optree-0.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c987931bd31d0f28cbff28925a875639170534a36ce178a40020aca0769d9549"},
+ {file = "optree-0.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124f30daf79d51b1bbbda7e74d01e637fa47aff4aa64cb082b88057535daa64"},
+ {file = "optree-0.12.1-cp311-cp311-win32.whl", hash = "sha256:d913122454d0e3f10dc25a1b598eaf588d225372f41ece3ad4d508bddd363e4d"},
+ {file = "optree-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d4d8e024b841f99907b2340fee7ac9994fbe300383a9af6c93578d12861a969"},
+ {file = "optree-0.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:e20b5569369a5f1e8faa2604799b91a1941fe17b5de8afc84c8c23ff66d8e585"},
+ {file = "optree-0.12.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:411a21eca034ddb98eb80e6c4bf552fc46b8d8ab7c4d250446d74d31a251a684"},
+ {file = "optree-0.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a67842cd1c5c83d74863872f06fe6ed64e44279c0378267a9805567fe3c38591"},
+ {file = "optree-0.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9280452c11da0872ec57be5d8f153207d6303b3cbf26115b2bf6d2b8157a5343"},
+ {file = "optree-0.12.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2027217c3acaf44e5f5aabe01ba0cbf33066f3f6df870881ddf597965f80db0"},
+ {file = "optree-0.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f65a31d7cfab2fed2bc29ab6eabcf4205dec6e0ee3cfb7006336c4f76d78fb0e"},
+ {file = "optree-0.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc1ec38d1ec43bb8358ab058c3220a70b7bfb56f2bb625f41cb09d117a0d6150"},
+ {file = "optree-0.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24d74a9d97d7bdbdbb30356850f204950c39ab8fad7f273ed29d1feda19060b2"},
+ {file = "optree-0.12.1-cp312-cp312-win32.whl", hash = "sha256:154738def491199d3fbcd919437315728e0a1caeaf4ec06688c76ef9d56e5ed6"},
+ {file = "optree-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d76905bced5cf569d23dc4890341fae2fa257cce58a492a1603afcdc5969ae7"},
+ {file = "optree-0.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:42025da0bac19cc6de756fe64511f15baffb3fa7d8402e54aab035c02903eb5c"},
+ {file = "optree-0.12.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:afa0051335c6032ee4dfc212952dcfb3b23fe59bcd70f56d25a214e7585cd62c"},
+ {file = "optree-0.12.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0460f025bf1c08f2c008b5e3628d849fcb5810345222e57879cd248fec7f9f7"},
+ {file = "optree-0.12.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6b98b80b1259e9817aca701beba616ce33e43e856e7d644f7e0f582b8e45565"},
+ {file = "optree-0.12.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e79eedd9406c59d542482768e490795dc6b6f1a014c7852d29d9fd61749bf94"},
+ {file = "optree-0.12.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562036d3de15204ed1a88d9fc262a7e1c20964d22ef132069e20dbd88215f983"},
+ {file = "optree-0.12.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aadb26d68f1d7871507f84846d8844aa94f47402d5277ce19cfe5102bb5df9e9"},
+ {file = "optree-0.12.1-cp37-cp37m-win32.whl", hash = "sha256:a55a79c1c72f73259532e4cbe9ff65bed9663064747db02591fb4714fe742d2e"},
+ {file = "optree-0.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1f8baf0ad6b58843d24fa8caf079cf1f0c33cc3658263cff960b5c1d0cc53bc8"},
+ {file = "optree-0.12.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7a71dd58522cd6258b61b639092ac7a2631d881f039ef968b31dfd555e513591"},
+ {file = "optree-0.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da37e6cc669a9840844722edb3f8dd5b4f07e99b0e8c9196089cb49af70c7b75"},
+ {file = "optree-0.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb968d3cc1db8944f220f1a67c9db043b86b47ace90ce3cfd23f3e6500baeb65"},
+ {file = "optree-0.12.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50893bd088bdb3e2f07ee481dafd848b483bea1a19cc978f2309139314e5bc7d"},
+ {file = "optree-0.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba6aed8b9684c5804a5e2d6b246c3b4a68bab793b6829d369ba1c53734852a0c"},
+ {file = "optree-0.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:646842f8a2de2caaacc32a8c91f8031a93eda145ac9c915bb0fd2ad5249c14b7"},
+ {file = "optree-0.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606983f4696d81128e205a1c34d0c9f3fe6ae12f6c26ed5e8ab3722d6f378ec2"},
+ {file = "optree-0.12.1-cp38-cp38-win32.whl", hash = "sha256:fd3ead0c64d22d692284d96c27d5091e682b002ffe5a52afacc9f1fcc8ae3180"},
+ {file = "optree-0.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:bd207b43e71fb3f8c315e2e4a5444f48317b2108889e96279d5426bca730a47e"},
+ {file = "optree-0.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9c473988b2d8fd7edc3958e6c7cb1d3f92afb7bcaff53b76a8f41cf4f3a24709"},
+ {file = "optree-0.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5f24b0a8b181a90a778cadc942a79336d29f0c164704d58cd20989bf7d0bea1c"},
+ {file = "optree-0.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49d3cfec1a51463b63e11c889bb00207c4e040016833cd202871ad946116925"},
+ {file = "optree-0.12.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1ca00bdfe4da8068c2773b7ac4c8c96d3f61b8d21eba6a8642dab23ee631b0d"},
+ {file = "optree-0.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bfe3d3e47e10b528f9324d446c871bfad7d0be8c2bd2a2fbc3ddf1600ae8558"},
+ {file = "optree-0.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a1a9905d2d917d5aff775283e0a59be2c6b529a219241c248d50b3ad51c6cce"},
+ {file = "optree-0.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ae426745931ae1c2ccd7a78b27f9b7402167e0600fa62e2ef1cd58727e7b94"},
+ {file = "optree-0.12.1-cp39-cp39-win32.whl", hash = "sha256:4b32f39988bfe6e76eeefb335da529e614145f7f1dfa8583fbc4aca8a72f504b"},
+ {file = "optree-0.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d90fb28d52725352858013cafe34d98d90ab1bb86b5d8dc29d420e9bbc5706b"},
+ {file = "optree-0.12.1-cp39-cp39-win_arm64.whl", hash = "sha256:d313303a1ce36ea55c3a96fc375c5cc64a9ab814ab2677ce64e4a7d755a9b1d0"},
+ {file = "optree-0.12.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:62d232a344c14b8e94fdd6de1acf2c0b05954b05d6bb346bddb13c38be37dc09"},
+ {file = "optree-0.12.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88d01ce6f78f209cad8dc4cf2d3222d7056cac93612abfd6beb40ab43a131769"},
+ {file = "optree-0.12.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b890ba0a21049addf589c314c85e98a68d3dfc84e3954491e9ce60f60cb7b0e7"},
+ {file = "optree-0.12.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47db001a224382493ae7a8df16e7a9668e971fc129970d137995421aa6b06f8f"},
+ {file = "optree-0.12.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:409ef6f3656299923d722509849d83607bb3e5c621dcfe6aa90ace85665e9b54"},
+ {file = "optree-0.12.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8513d6dd71807abb1037a5b5bc66b45c21afb42e9c90961fa5e762cea3943ab2"},
+ {file = "optree-0.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0950ee245db2c40824362def1efc15621a6492419628cec1fac0061818420f7"},
+ {file = "optree-0.12.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cefd4f4c7596cdd4c95dca431bc41284a43ebd7056e739480f157789aa34579d"},
+ {file = "optree-0.12.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23afe4aae42336bdf8cf4fba35c56593405bf8f8e163627f722205b3bf0d9310"},
+ {file = "optree-0.12.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b2fe5c04c218698a53ed2d4b7372f1989df8cf0a61d616e6f384770d8a5fb1c"},
+ {file = "optree-0.12.1.tar.gz", hash = "sha256:76a2240e7482355966a73c6c701e3d1f148420a77849c78d175d3b08bf06ff36"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.5.0"
+
+[package.extras]
+benchmark = ["dm-tree (>=0.1,<0.2.0a0)", "jax[cpu] (>=0.4.6,<0.5.0a0)", "pandas", "tabulate", "termcolor", "torch (>=2.0,<2.4.0a0)", "torchvision"]
+docs = ["docutils", "jax[cpu]", "numpy", "sphinx (>=5.2.1)", "sphinx-autoapi", "sphinx-autobuild", "sphinx-autodoc-typehints (>=1.19.2)", "sphinx-copybutton", "sphinx-rtd-theme", "sphinxcontrib-bibtex", "torch"]
+jax = ["jax"]
+lint = ["black", "cpplint", "doc8", "flake8", "flake8-bugbear", "flake8-comprehensions", "flake8-docstrings", "flake8-pyi", "flake8-simplify", "isort", "mypy", "pre-commit", "pydocstyle", "pyenchant", "pylint[spelling]", "ruff", "xdoctest"]
+numpy = ["numpy"]
+test = ["pytest", "pytest-cov", "pytest-xdist"]
+torch = ["torch"]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+]
+
+[[package]]
+name = "pandas"
+version = "2.2.2"
+description = "Powerful data structures for data analysis, time series, and statistics"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"},
+ {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"},
+ {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"},
+ {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"},
+ {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"},
+ {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"},
+ {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"},
+ {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"},
+ {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"},
+ {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"},
+ {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"},
+ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"},
+ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"},
+ {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"},
+ {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"},
+ {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"},
+ {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"},
+ {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"},
+ {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"},
+ {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"},
+ {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"},
+ {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"},
+ {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"},
+ {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"},
+ {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"},
+ {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"},
+ {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"},
+ {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"},
+ {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"},
+]
+
+[package.dependencies]
+numpy = [
+ {version = ">=1.22.4", markers = "python_version < \"3.11\""},
+ {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
+ {version = ">=1.23.2", markers = "python_version == \"3.11\""},
+]
+python-dateutil = ">=2.8.2"
+pytz = ">=2020.1"
+tzdata = ">=2022.7"
+
+[package.extras]
+all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"]
+aws = ["s3fs (>=2022.11.0)"]
+clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"]
+compression = ["zstandard (>=0.19.0)"]
+computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"]
+consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"]
+feather = ["pyarrow (>=10.0.1)"]
+fss = ["fsspec (>=2022.11.0)"]
+gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"]
+hdf5 = ["tables (>=3.8.0)"]
+html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"]
+mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"]
+parquet = ["pyarrow (>=10.0.1)"]
+performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
+plot = ["matplotlib (>=3.6.3)"]
+postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
+pyarrow = ["pyarrow (>=10.0.1)"]
+spss = ["pyreadstat (>=1.2.0)"]
+sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
+test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.9.2)"]
+
+[[package]]
+name = "pandocfilters"
+version = "1.5.1"
+description = "Utilities for writing pandoc filters in python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"},
+ {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"},
+]
+
+[[package]]
+name = "parso"
+version = "0.8.4"
+description = "A Python Parser"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
+ {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
+]
+
+[package.extras]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["docopt", "pytest"]
+
+[[package]]
+name = "pdbpp"
+version = "0.10.3"
+description = "pdb++, a drop-in replacement for pdb"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"},
+ {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"},
+]
+
+[package.dependencies]
+fancycompleter = ">=0.8"
+pygments = "*"
+wmctrl = "*"
+
+[package.extras]
+funcsigs = ["funcsigs"]
+testing = ["funcsigs", "pytest"]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+description = "Pexpect allows easy control of interactive console applications."
+optional = false
+python-versions = "*"
+files = [
+ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
+ {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
+]
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pillow"
+version = "10.4.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
+ {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
+ {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
+ {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
+ {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
+ {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
+ {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
+ {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
+ {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
+ {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
+ {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
+ {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
+ {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
+ {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
+ {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
+ {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
+ {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
+ {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
+ {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
+ {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
+ {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
+ {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
+ {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
+ {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
+ {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
+ {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
+ {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
+ {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
+ {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
+ {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
+ {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
+ {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
+ {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
+ {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
+ {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
+ {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
+ {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
+ {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
+ {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+typing = ["typing-extensions"]
+xmp = ["defusedxml"]
+
+[[package]]
+name = "platformdirs"
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+ {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "ply"
+version = "3.11"
+description = "Python Lex & Yacc"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
+ {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.47"
+description = "Library for building powerful interactive command lines in Python"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"},
+ {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "proto-plus"
+version = "1.24.0"
+description = "Beautiful, Pythonic protocol buffers."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"},
+ {file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"},
+]
+
+[package.dependencies]
+protobuf = ">=3.19.0,<6.0.0dev"
+
+[package.extras]
+testing = ["google-api-core (>=1.31.5)"]
+
+[[package]]
+name = "protobuf"
+version = "4.25.3"
+description = ""
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"},
+ {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"},
+ {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"},
+ {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"},
+ {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"},
+ {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"},
+ {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"},
+ {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"},
+ {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"},
+ {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"},
+ {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
+]
+
+[[package]]
+name = "protobuf"
+version = "5.27.2"
+description = ""
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "protobuf-5.27.2-cp310-abi3-win32.whl", hash = "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38"},
+ {file = "protobuf-5.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505"},
+ {file = "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5"},
+ {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b"},
+ {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e"},
+ {file = "protobuf-5.27.2-cp38-cp38-win32.whl", hash = "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863"},
+ {file = "protobuf-5.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6"},
+ {file = "protobuf-5.27.2-cp39-cp39-win32.whl", hash = "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca"},
+ {file = "protobuf-5.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce"},
+ {file = "protobuf-5.27.2-py3-none-any.whl", hash = "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470"},
+ {file = "protobuf-5.27.2.tar.gz", hash = "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714"},
+]
+
+[[package]]
+name = "psutil"
+version = "5.9.8"
+description = "Cross-platform lib for process and system monitoring in Python."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+ {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"},
+ {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"},
+ {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"},
+ {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"},
+ {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"},
+ {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"},
+ {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"},
+ {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"},
+ {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"},
+ {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"},
+ {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"},
+ {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"},
+ {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"},
+ {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"},
+ {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"},
+ {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"},
+]
+
+[package.extras]
+test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.2"
+description = "Safely evaluate AST nodes without side effects"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
+ {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
+[[package]]
+name = "py4j"
+version = "0.10.9.7"
+description = "Enables Python programs to dynamically access arbitrary Java objects"
+optional = false
+python-versions = "*"
+files = [
+ {file = "py4j-0.10.9.7-py2.py3-none-any.whl", hash = "sha256:85defdfd2b2376eb3abf5ca6474b51ab7e0de341c75a02f46dc9b5976f5a5c1b"},
+ {file = "py4j-0.10.9.7.tar.gz", hash = "sha256:0b6e5315bb3ada5cf62ac651d107bb2ebc02def3dee9d9548e3baac644ea8dbb"},
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.0"
+description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
+ {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.0"
+description = "A collection of ASN.1-based protocols modules"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"},
+ {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"},
+]
+
+[package.dependencies]
+pyasn1 = ">=0.4.6,<0.7.0"
+
+[[package]]
+name = "pybtex"
+version = "0.24.0"
+description = "A BibTeX-compatible bibliography processor in Python"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
+files = [
+ {file = "pybtex-0.24.0-py2.py3-none-any.whl", hash = "sha256:e1e0c8c69998452fea90e9179aa2a98ab103f3eed894405b7264e517cc2fcc0f"},
+ {file = "pybtex-0.24.0.tar.gz", hash = "sha256:818eae35b61733e5c007c3fcd2cfb75ed1bc8b4173c1f70b56cc4c0802d34755"},
+]
+
+[package.dependencies]
+latexcodec = ">=1.0.4"
+PyYAML = ">=3.01"
+six = "*"
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "pybtex-docutils"
+version = "1.0.3"
+description = "A docutils backend for pybtex."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b"},
+ {file = "pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9"},
+]
+
+[package.dependencies]
+docutils = ">=0.14"
+pybtex = ">=0.16"
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+description = "C parser in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
+]
+
+[[package]]
+name = "pydantic"
+version = "1.10.17"
+description = "Data validation and settings management using python type hints"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
+ {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
+ {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
+ {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
+ {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
+ {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
+ {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
+ {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
+ {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
+ {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
+ {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
+ {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
+ {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
+ {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
+ {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
+ {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
+ {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
+ {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
+ {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
+ {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
+ {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
+ {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
+ {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
+ {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
+ {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
+ {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
+ {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
+ {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
+ {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
+ {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
+ {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
+ {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
+ {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
+ {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
+ {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
+ {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
+ {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
+ {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
+ {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
+ {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
+ {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
+ {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
+ {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.2.0"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+
+[[package]]
+name = "pygments"
+version = "2.18.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+ {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pyjwt"
+version = "2.8.0"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
+ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
+[[package]]
+name = "pylint"
+version = "3.1.0"
+description = "python code static checker"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"},
+ {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"},
+]
+
+[package.dependencies]
+astroid = ">=3.1.0,<=3.2.0-dev0"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+dill = [
+ {version = ">=0.2", markers = "python_version < \"3.11\""},
+ {version = ">=0.3.7", markers = "python_version >= \"3.12\""},
+ {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
+]
+isort = ">=4.2.5,<5.13.0 || >5.13.0,<6"
+mccabe = ">=0.6,<0.8"
+platformdirs = ">=2.2.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+tomlkit = ">=0.10.1"
+typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+spelling = ["pyenchant (>=3.2,<4.0)"]
+testutils = ["gitpython (>3)"]
+
+[[package]]
+name = "pyparsing"
+version = "3.1.2"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+optional = false
+python-versions = ">=3.6.8"
+files = [
+ {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
+ {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
+]
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pyquil"
+version = "3.5.4"
+description = "A Python library for creating Quantum Instruction Language (Quil) programs."
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "pyquil-3.5.4-py3-none-any.whl", hash = "sha256:ef36458b38b314f647f0e6cb067358e92033821f90fb7a533baa5ff59bfbaaf1"},
+ {file = "pyquil-3.5.4.tar.gz", hash = "sha256:4fe65c83a4cf9164d3c7ddb314029d71b7388fb22fb56a1ba8f4dd3c1bb1d9a6"},
+]
+
+[package.dependencies]
+deprecated = ">=1.2.13,<2.0.0"
+lark = ">=0.11.1,<0.12.0"
+networkx = ">=2.5"
+numpy = ">=1.21,<2.0"
+qcs-api-client = ">=0.21.0,<0.22.0"
+rpcq = ">=3.10.0,<4.0.0"
+scipy = ">=1.6.1,<2.0.0"
+tenacity = ">=8.2.2,<9.0.0"
+types-deprecated = ">=1.2.9.2,<2.0.0.0"
+types-python-dateutil = ">=2.8.19,<3.0.0"
+types-retry = ">=0.9.9,<0.10.0"
+
+[package.extras]
+docs = ["Sphinx (>=4.0.2,<5.0.0)", "nbsphinx (>=0.8.6,<0.9.0)", "recommonmark (>=0.7.1,<0.8.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
+latex = ["ipython (>=7.21.0,<8.0.0)"]
+
+[[package]]
+name = "pyreadline"
+version = "2.1"
+description = "A python implmementation of GNU readline."
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"},
+]
+
+[[package]]
+name = "pyrepl"
+version = "0.9.0"
+description = "A library for building flexible command line interfaces"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"},
+]
+
+[[package]]
+name = "pyrsistent"
+version = "0.20.0"
+description = "Persistent/Functional/Immutable data structures"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce"},
+ {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f"},
+ {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34"},
+ {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b"},
+ {file = "pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f"},
+ {file = "pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656"},
+ {file = "pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174"},
+ {file = "pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423"},
+ {file = "pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f"},
+ {file = "pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf"},
+ {file = "pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b"},
+ {file = "pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4"},
+]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+ {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-rapidjson"
+version = "1.18"
+description = "Python wrapper around rapidjson"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "python-rapidjson-1.18.tar.gz", hash = "sha256:09a5c362e2fec2a41b53e79e88bd8f0704447cb67c1c89a59e3092ccb4a30355"},
+ {file = "python_rapidjson-1.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f38c7ca5fee31423bb34f464c789f57954886dd00e1a8c8483fd13e0c0d2583"},
+ {file = "python_rapidjson-1.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1912224817f734ee9138e91d170b62818fd01caa731aa8668e8c9bce9017fe7e"},
+ {file = "python_rapidjson-1.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af6ca181e812f2306d4806beb974334ddd0774a8f62194ad1721277236f4ad1"},
+ {file = "python_rapidjson-1.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08f859f64470ecb307cdcd7a532bef9c9ab3c94d2005c5693a7e18b3a11d4b28"},
+ {file = "python_rapidjson-1.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:507595740300e95dded254536558cd56733cc3207e3c2457f19231ad00e78d85"},
+ {file = "python_rapidjson-1.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5815af2f69a11c114e5004a77b8b036b5abcd06202c8bc1525856f9d836254a3"},
+ {file = "python_rapidjson-1.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d680b8c8f4dbceb465544bbdd28463aa7e0b651343aa73c2476533bf300e0266"},
+ {file = "python_rapidjson-1.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ff22c4160227be38322a88856f011c95d199103c30993bf3ee64f4bce9221807"},
+ {file = "python_rapidjson-1.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:9deb8a8a2df2982b446f2a19264a5da2780ddb415caf9e11d48e74701053f02e"},
+ {file = "python_rapidjson-1.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6ecd86abf689538fdab5a55483c38bf10bdd9a8ed204ae10fa5a1bac7222d88"},
+ {file = "python_rapidjson-1.18-cp310-cp310-win32.whl", hash = "sha256:a9d4cd0be643b8310c1c92987961c06b68429527154e9bea75118802cd179178"},
+ {file = "python_rapidjson-1.18-cp310-cp310-win_amd64.whl", hash = "sha256:52f1d509ec20ab5d26f6dbc5d56821e0b2b1a5a878439eb0b3a33137b59029f5"},
+ {file = "python_rapidjson-1.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83912aae7c508204c263818befa24cf3223ecf0175e70d0412169e1302f1b4f2"},
+ {file = "python_rapidjson-1.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f45a02e4593879772099cf88d18dbde3376334684a809feb9228b8745c0c08c"},
+ {file = "python_rapidjson-1.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f201e0c1e41c0e491cf2eca121d51f30c666f35ce33a6d14ba8fc5b76e4a2fa7"},
+ {file = "python_rapidjson-1.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:697d06a66a1ba267f5abbb04971e461df1d4528ba341af6848a1ef01ae224e90"},
+ {file = "python_rapidjson-1.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7e22b841fda1ec8c9e0a49069fbc6579363ba79fa5398fc7d37666357068cf"},
+ {file = "python_rapidjson-1.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:356b2f81e6cdb4c1bb9122b635c8bd827f845da7c0de8618874c933fb88de573"},
+ {file = "python_rapidjson-1.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acd2430dd7a8f66618247635c51a9413679e9a5279aaea708f854ef03cc933e1"},
+ {file = "python_rapidjson-1.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a587b3ec2b76480dfb57409654a9344ab47910e1b9d09e1c8eefe2db6c8c7364"},
+ {file = "python_rapidjson-1.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2cf502e6c01d0933dc65888ab62b86d67967903c9a66158c2e458b312e671345"},
+ {file = "python_rapidjson-1.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43e622aa170f0b1e04f5b5ac8c7bf94b99f79efceb3608d8f1456f617cd79cdb"},
+ {file = "python_rapidjson-1.18-cp311-cp311-win32.whl", hash = "sha256:f9c9faa7c1df63e2b238fcbdb915d52eba9ba42ee6e2a502f81e8aac07938783"},
+ {file = "python_rapidjson-1.18-cp311-cp311-win_amd64.whl", hash = "sha256:e7b1cadf5c8852ae6e0a19fcf5b734eef4f92170292686cfdcced1302ea0aa20"},
+ {file = "python_rapidjson-1.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52912323a2ac460ea605ab55f437196f662ec9db82669367dab4cda8f4c05b13"},
+ {file = "python_rapidjson-1.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebbd471d63bfa3c09180fd44eefec7b0f46ca41ee4552559c3a027799c67d781"},
+ {file = "python_rapidjson-1.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb89a794242a692ef5d15ec9ad14c21fd17abc4671af62eadc8e6a1861a0319"},
+ {file = "python_rapidjson-1.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae2fdd5a2520dc85f98224ba1fc96badd0b68d3a8ee41485b3e37be67b7bef"},
+ {file = "python_rapidjson-1.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f46face2b3e6891dd646dc1062c1133323ce4dc26409a084535f2af9e2bb4e3"},
+ {file = "python_rapidjson-1.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67a3f71225200344ffaab3d28add533398b92f65d9166e649222a50677370fd2"},
+ {file = "python_rapidjson-1.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7851252083aba29668cf1f02dc1c1e5e5a9113bf4f1dedc2f509c00e43f0c884"},
+ {file = "python_rapidjson-1.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:32c32256edb35a234b16dfa6452bdf066cc272675cf9b3eb980e853505202766"},
+ {file = "python_rapidjson-1.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f9d27c090782f83de06dd51b9a7143b04c32314e53ed531a2d8f170f9f255e9"},
+ {file = "python_rapidjson-1.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3e0b8863cc0e78e36d41aae856101291c0bea9215690decafa6bae5f413e1f3"},
+ {file = "python_rapidjson-1.18-cp312-cp312-win32.whl", hash = "sha256:123e7bf9726c09055d97ba0c4fc8cdb9deda80c2a9d5409bfd49935a0f38d0b2"},
+ {file = "python_rapidjson-1.18-cp312-cp312-win_amd64.whl", hash = "sha256:03d14892a1cdc24e5b200ca619fda397e0f36a3d1420edcb7212ae47d4d9fd3e"},
+ {file = "python_rapidjson-1.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d4861adede630a5eee77c46f9c901da2ac15bc3c0296ad851d69036db3a0374"},
+ {file = "python_rapidjson-1.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:35d0e9c8dd61670b5833546b3ded057b68e696ab530d3c14603e718a4bc3db00"},
+ {file = "python_rapidjson-1.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d229114f738ee0d9ff1b727aaf7bfe6a90d6f77e0449b33f87ad7814c493c921"},
+ {file = "python_rapidjson-1.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb0a8361b668e920d7fa78f725f59d224adedb3620f526509cef4416778e3393"},
+ {file = "python_rapidjson-1.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20256271a00f758a96ccfdd61434c11a1fc6b5e3fd4e7324dd832e576c9f720b"},
+ {file = "python_rapidjson-1.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad82fa706d7437ceb0d8e36870715e8318359bc604016fc505c14ccc109322e9"},
+ {file = "python_rapidjson-1.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f13a8be00c0fd31c75304f03df1240d16268720b9d12eca3d055f702dd607427"},
+ {file = "python_rapidjson-1.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9712964a864c7604319bebbdd4ab5de9a42698d3c9a6c15c964a06d586a2c66"},
+ {file = "python_rapidjson-1.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0f36f9c194d8c893463128a57bd7cde3bb28151eaf5bb5db5f552de0eb0eb93"},
+ {file = "python_rapidjson-1.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4704f9f987a62c4b7e419843bb3c5daea81271dba95cae47e92b2475978ae66b"},
+ {file = "python_rapidjson-1.18-cp313-cp313-win32.whl", hash = "sha256:2d197c686a4eacc2defe9bd31bf73b23877ad4974857b72b65e126cef7a50fa5"},
+ {file = "python_rapidjson-1.18-cp313-cp313-win_amd64.whl", hash = "sha256:30f4a317af410d3977cf405737a2d6e81c6695d24df33113523023f665bb5e75"},
+ {file = "python_rapidjson-1.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:300b8d84d5bebea7988312950fc949c1701055086b2790afaaad68e8f1cf389d"},
+ {file = "python_rapidjson-1.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:981dd50522999a4fe519ca14135e20b3acc4928df4d4421d96792913d2fb359d"},
+ {file = "python_rapidjson-1.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d03390ac612090c58553e1d8454faff6099a2b2ee0c44ebd19546d5a73b30689"},
+ {file = "python_rapidjson-1.18-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0550caca5227e3f929b63b758c19c584f39c10d4e1c4ad9b7e322f19030db3b8"},
+ {file = "python_rapidjson-1.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37295c26b6270515666243d499c060006471b0517dbdf7690b5f855b9531f9b8"},
+ {file = "python_rapidjson-1.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d058b9c740c55fe3ffab826742773f995620992eda6a31d794727526d0ea1610"},
+ {file = "python_rapidjson-1.18-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0c4697e3fa587c7f3938d2394ff6563085bbf346e4cab29fb425595d267a59d1"},
+ {file = "python_rapidjson-1.18-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aa8fbc9c31d9320e80a290d3cf847756d37290628ccaad3719de6fa51ab43597"},
+ {file = "python_rapidjson-1.18-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:191e051b7b384474b6558902b8c33f82474492e3d19cc188224cd1a5584ca4bf"},
+ {file = "python_rapidjson-1.18-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dd0bc1b3d4d72bd3eb9f60f84473fcefb316912422267bf06d8c2290ef33e02"},
+ {file = "python_rapidjson-1.18-cp38-cp38-win32.whl", hash = "sha256:1925a3ed72504812ab1d8edd59ad83bd4b96b5a3e149ee927f3cdb98b803ac22"},
+ {file = "python_rapidjson-1.18-cp38-cp38-win_amd64.whl", hash = "sha256:4e21cbd8585598ce091990196fe6fe354c607e13e2b17794f3711a8f2b2b8b11"},
+ {file = "python_rapidjson-1.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:68230f34a076a54298d5c860ae8aa08e3de5ab5a289b23b96a0a6039861f911b"},
+ {file = "python_rapidjson-1.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b1ec8b167484523bc0d753998594cb2614061755191946b73c7e88e124287595"},
+ {file = "python_rapidjson-1.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bad5d1a46b2d07f1d9b4ad1c316a36e024da451ff876d1572cb345c6bb50a42"},
+ {file = "python_rapidjson-1.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daf270f1d2feddf7680ddc2faf2778e814caf569095cc60c2079e856af3d2bc3"},
+ {file = "python_rapidjson-1.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72948a56b9d4964d72f2f3862d5d168b247457f9d1e70cee750a0cd660f67555"},
+ {file = "python_rapidjson-1.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0624eebe2ceba44dd84a3d3409fab1e7e1a021c3701b5ad5bd8a0fba47898d20"},
+ {file = "python_rapidjson-1.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b975fcecbf5f3845ce72040be4630ece4c5b467c24c749be2a81827918a2e530"},
+ {file = "python_rapidjson-1.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f725f560f4865fb5b684a26935f78690e44aa475c8b41a793d096a122115c9b3"},
+ {file = "python_rapidjson-1.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0a31ea1a7a11a6e60fed42364e6726d29346f6ba1a9212ea1b6753731f600909"},
+ {file = "python_rapidjson-1.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:477aff79a2d87daee45c49e917097426fe5495f99fb935a5adb20716cb52c86a"},
+ {file = "python_rapidjson-1.18-cp39-cp39-win32.whl", hash = "sha256:d13a0e3f647726f653cd3d6bfc770d595f51d75212b38df82d2a465bc0df5dd8"},
+ {file = "python_rapidjson-1.18-cp39-cp39-win_amd64.whl", hash = "sha256:412c716cbf41ecfb99879443fc11288513053e63302232df0ed99d629fd220da"},
+]
+
+[[package]]
+name = "pytz"
+version = "2024.1"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
+ {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+]
+
+[[package]]
+name = "pywin32"
+version = "306"
+description = "Python for Window Extensions"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
+ {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
+ {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
+ {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
+ {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
+ {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
+ {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
+ {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
+ {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
+ {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
+ {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
+ {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
+ {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
+ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "pyzmq"
+version = "26.0.3"
+description = "Python bindings for 0MQ"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"},
+ {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"},
+ {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"},
+ {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"},
+ {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"},
+ {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"},
+ {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"},
+ {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"},
+ {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"},
+ {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"},
+ {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"},
+ {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"},
+ {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"},
+ {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"},
+ {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"},
+ {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"},
+ {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"},
+ {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"},
+ {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"},
+ {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"},
+ {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"},
+ {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"},
+ {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"},
+ {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"},
+ {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"},
+ {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"},
+ {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"},
+ {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"},
+ {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"},
+ {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"},
+ {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"},
+ {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
+[[package]]
+name = "qcs-api-client"
+version = "0.21.6"
+description = "A client library for accessing the Rigetti QCS API"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "qcs_api_client-0.21.6-py3-none-any.whl", hash = "sha256:0231c9a741c137adc78539068b7fe1d4e1854bee4e3e868017abc4eb3876c3a0"},
+ {file = "qcs_api_client-0.21.6.tar.gz", hash = "sha256:1da391ff03715c9d50e14aa6006b5f0c3cffde9018fb968ff27545cc90c87a9c"},
+]
+
+[package.dependencies]
+attrs = ">=21.3.0,<22.0.0"
+httpx = ">=0.23.0,<0.24.0"
+iso8601 = ">=1.0.2,<2.0.0"
+pydantic = ">=1.7.2,<2.0.0"
+PyJWT = ">=2.4.0,<3.0.0"
+python-dateutil = ">=2.8.1,<3.0.0"
+retrying = ">=1.3.3,<2.0.0"
+rfc3339 = ">=6.2,<7.0"
+toml = ">=0.10.2,<0.11.0"
+
+[[package]]
+name = "qibojit"
+version = "0.1.5"
+description = "Simulation tools based on numba and cupy."
+optional = false
+python-versions = "^3.9,<3.13"
+files = []
+develop = false
+
+[package.dependencies]
+numba = ">=0.59.0"
+psutil = "^5.9.5"
+qibo = "^0.2.8"
+scipy = "^1.10.1"
+
+[package.extras]
+cupy = ["cupy-cuda12x (>=13.1.0,<14.0.0)"]
+cuquantum = ["cuquantum-python-cu12 (>=23.10.0,<24.0.0)"]
+
+[package.source]
+type = "git"
+url = "https://github.com/qiboteam/qibojit.git"
+reference = "HEAD"
+resolved_reference = "a92806f8335b16350d38a09a71ada37c5ae3c132"
+
+[[package]]
+name = "qibotn"
+version = "0.0.2"
+description = "A tensor-network translation module for Qibo"
+optional = false
+python-versions = "^3.9,<3.13"
+files = []
+develop = false
+
+[package.dependencies]
+qibo = "^0.2.8"
+quimb = {version = "^1.6.0", extras = ["tensor"]}
+
+[package.extras]
+cuda = ["cupy-cuda11x (>=11.6.0,<12.0.0)", "cuquantum-python-cu11 (>=23.3.0,<24.0.0)", "mpi4py (>=3.1.5,<4.0.0)"]
+
+[package.source]
+type = "git"
+url = "https://github.com/qiboteam/qibotn.git"
+reference = "HEAD"
+resolved_reference = "13a2db80036ffa792e98e90046ad50d321b9b9de"
+
+[[package]]
+name = "quimb"
+version = "1.8.3"
+description = "Quantum information and many-body library."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "quimb-1.8.3-py3-none-any.whl", hash = "sha256:b00114767280925dc9799817b231573adef5c089b5b01fb0d7b1c4abcc644fe3"},
+ {file = "quimb-1.8.3.tar.gz", hash = "sha256:13ad9b3eea7ac9504f1927949a97c2f2f1c0d0f2d90dcd27d7024631f85d865c"},
+]
+
+[package.dependencies]
+autoray = ">=0.6.12"
+cotengra = ">=0.6.1"
+cytoolz = ">=0.8.0"
+matplotlib = {version = ">=2.0", optional = true, markers = "extra == \"tensor\""}
+networkx = {version = ">=2.3", optional = true, markers = "extra == \"tensor\""}
+numba = ">=0.39"
+numpy = ">=1.17"
+psutil = ">=4.3.1"
+scipy = ">=1.0.0"
+tqdm = ">=4"
+
+[package.extras]
+advanced-solvers = ["mpi4py", "petsc4py", "slepc4py"]
+docs = ["astroid (<3.0.0)", "autoray (>=0.6.12)", "cotengra (>=0.6.1)", "doc2dash (>=2.4.1)", "furo", "ipython (!=8.7.0)", "myst-nb", "setuptools-scm", "sphinx (>=2.0)", "sphinx-autoapi", "sphinx-copybutton", "sphinx-design"]
+tensor = ["matplotlib (>=2.0)", "networkx (>=2.3)"]
+tests = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "qulacs"
+version = "0.6.4.1"
+description = "Quantum circuit simulator for research"
+optional = false
+python-versions = "*"
+files = [
+ {file = "qulacs-0.6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6f582debb716ea4bb9493b3250fb0d58a51acf534b4db9d16c06d87b01f28a5"},
+ {file = "qulacs-0.6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9d66c7061075157f76b11639914b4f71db1805323d4ccbbc9c9671c24278d98"},
+ {file = "qulacs-0.6.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61e6d1fc8f00185a7498b8b37756f175623a429d00147c86c4337bd45ebbc387"},
+ {file = "qulacs-0.6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:cad1c1a018b944be5a1459ba5a01e0a050a0ad767bbe2c988eac96559e625920"},
+ {file = "qulacs-0.6.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:50764d16dac2b9b2b5befb34859aec1fe96144149f2637afacd358d2b831bef4"},
+ {file = "qulacs-0.6.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:587e6540f490d0a2f075a8b88f971864802f864835eb6b54168df2f66eaf77df"},
+ {file = "qulacs-0.6.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bee026f96700ecc8ac35a93407fe15453888febc5ac286fb978ab8a165d565"},
+ {file = "qulacs-0.6.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:89be135541e85cf0fffa550ebed63441800fb1387f10d2aa48a2315c40477e86"},
+ {file = "qulacs-0.6.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2c9297bb3f9e213e5332f197f395843b49f847639302dd97a496852d04b19f06"},
+ {file = "qulacs-0.6.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36bd983a8ce670813fe657a943e88d9c1b12faf495bd257778dc5dfec7b0c142"},
+ {file = "qulacs-0.6.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6fbd1010ba09e4b3d2313378d5857fc8477487857c401631fa7c646b82eb005"},
+ {file = "qulacs-0.6.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:23febdcc489c2f0f30114a20e81492b4221ca8dbbfd1f0239ba01220b898caa7"},
+ {file = "qulacs-0.6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fe18ea1b1080895a24e8861e18be432665376f5606f884822295f2293b15e7f1"},
+ {file = "qulacs-0.6.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b79466aa8dbee245a2693020994e122310d332fd5d793f154eb79eea2fa06f80"},
+ {file = "qulacs-0.6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:c5c1eacf6f9572ce6dfc12e1abec10c00e4f71a90c27f1babbcd9b844880c912"},
+ {file = "qulacs-0.6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5e8c8990b8b55dd3a0bf4d02033d7f6c9183f8bb2f015b6a6105a79405036b"},
+ {file = "qulacs-0.6.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a03ef08a8cf43269a0277b0e7a917b56cec0374677d15249553ee1a5f4c73"},
+ {file = "qulacs-0.6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:1e7b6d8dc79275233c0dc9d5a8cefac3f55b625be6f8855e99f3769f408f4052"},
+ {file = "qulacs-0.6.4.1.tar.gz", hash = "sha256:be90b8586b6f1b2f2f806fca762f37116d02dc4fbe74d75ebfb4c6238c3bcde2"},
+]
+
+[package.dependencies]
+numpy = "*"
+scipy = "*"
+
+[package.extras]
+ci = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"]
+dev = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"]
+doc = ["breathe", "exhale", "ipykernel", "mypy", "myst-parser", "nbsphinx", "pybind11-stubgen", "sphinx (==7.*)", "sphinx-autoapi (==3.*)", "sphinx-copybutton", "sphinx-rtd-theme"]
+test = ["openfermion"]
+
+[[package]]
+name = "recommonmark"
+version = "0.7.1"
+description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects."
+optional = false
+python-versions = "*"
+files = [
+ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"},
+ {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"},
+]
+
+[package.dependencies]
+commonmark = ">=0.8.1"
+docutils = ">=0.11"
+sphinx = ">=1.3.1"
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "retrying"
+version = "1.3.4"
+description = "Retrying"
+optional = false
+python-versions = "*"
+files = [
+ {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"},
+ {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"},
+]
+
+[package.dependencies]
+six = ">=1.7.0"
+
+[[package]]
+name = "rfc3339"
+version = "6.2"
+description = "Format dates according to the RFC 3339."
+optional = false
+python-versions = "*"
+files = [
+ {file = "rfc3339-6.2-py3-none-any.whl", hash = "sha256:f44316b21b21db90a625cde04ebb0d46268f153e6093021fa5893e92a96f58a3"},
+ {file = "rfc3339-6.2.tar.gz", hash = "sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0"},
+]
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+optional = false
+python-versions = "*"
+files = [
+ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
+ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+]
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "rich"
+version = "13.7.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
+ {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "rpcq"
+version = "3.11.0"
+description = "The RPC framework and message specification for Rigetti QCS."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "rpcq-3.11.0.tar.gz", hash = "sha256:4361e759782f58dd0b8aa3a6d901e3ea5709f91c6a060bd444081fbb007b05a9"},
+]
+
+[package.dependencies]
+msgpack = ">=0.6,<2.0"
+python-rapidjson = "*"
+pyzmq = ">=17"
+"ruamel.yaml" = "*"
+
+[[package]]
+name = "rsa"
+version = "4.9"
+description = "Pure-Python RSA implementation"
+optional = false
+python-versions = ">=3.6,<4"
+files = [
+ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
+ {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
+]
+
+[package.dependencies]
+pyasn1 = ">=0.1.3"
+
+[[package]]
+name = "ruamel-yaml"
+version = "0.18.6"
+description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"},
+ {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"},
+]
+
+[package.dependencies]
+"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""}
+
+[package.extras]
+docs = ["mercurial (>5.7)", "ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
+
+[[package]]
+name = "ruamel-yaml-clib"
+version = "0.2.8"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"},
+ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"},
+ {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"},
+ {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"},
+ {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"},
+ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"},
+ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"},
+ {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"},
+ {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"},
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.5.1"
+description = "A set of python modules for machine learning and data mining"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "scikit_learn-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:781586c414f8cc58e71da4f3d7af311e0505a683e112f2f62919e3019abd3745"},
+ {file = "scikit_learn-1.5.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5b213bc29cc30a89a3130393b0e39c847a15d769d6e59539cd86b75d276b1a7"},
+ {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff4ba34c2abff5ec59c803ed1d97d61b036f659a17f55be102679e88f926fac"},
+ {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:161808750c267b77b4a9603cf9c93579c7a74ba8486b1336034c2f1579546d21"},
+ {file = "scikit_learn-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:10e49170691514a94bb2e03787aa921b82dbc507a4ea1f20fd95557862c98dc1"},
+ {file = "scikit_learn-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:154297ee43c0b83af12464adeab378dee2d0a700ccd03979e2b821e7dd7cc1c2"},
+ {file = "scikit_learn-1.5.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b5e865e9bd59396220de49cb4a57b17016256637c61b4c5cc81aaf16bc123bbe"},
+ {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909144d50f367a513cee6090873ae582dba019cb3fca063b38054fa42704c3a4"},
+ {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689b6f74b2c880276e365fe84fe4f1befd6a774f016339c65655eaff12e10cbf"},
+ {file = "scikit_learn-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a07f90846313a7639af6a019d849ff72baadfa4c74c778821ae0fad07b7275b"},
+ {file = "scikit_learn-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5944ce1faada31c55fb2ba20a5346b88e36811aab504ccafb9f0339e9f780395"},
+ {file = "scikit_learn-1.5.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0828673c5b520e879f2af6a9e99eee0eefea69a2188be1ca68a6121b809055c1"},
+ {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508907e5f81390e16d754e8815f7497e52139162fd69c4fdbd2dfa5d6cc88915"},
+ {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97625f217c5c0c5d0505fa2af28ae424bd37949bb2f16ace3ff5f2f81fb4498b"},
+ {file = "scikit_learn-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:da3f404e9e284d2b0a157e1b56b6566a34eb2798205cba35a211df3296ab7a74"},
+ {file = "scikit_learn-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88e0672c7ac21eb149d409c74cc29f1d611d5158175846e7a9c2427bd12b3956"},
+ {file = "scikit_learn-1.5.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:7b073a27797a283187a4ef4ee149959defc350b46cbf63a84d8514fe16b69855"},
+ {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b59e3e62d2be870e5c74af4e793293753565c7383ae82943b83383fdcf5cc5c1"},
+ {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd8d3a19d4bd6dc5a7d4f358c8c3a60934dc058f363c34c0ac1e9e12a31421d"},
+ {file = "scikit_learn-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:5f57428de0c900a98389c4a433d4a3cf89de979b3aa24d1c1d251802aa15e44d"},
+ {file = "scikit_learn-1.5.1.tar.gz", hash = "sha256:0ea5d40c0e3951df445721927448755d3fe1d80833b0b7308ebff5d2a45e6414"},
+]
+
+[package.dependencies]
+joblib = ">=1.2.0"
+numpy = ">=1.19.5"
+scipy = ">=1.6.0"
+threadpoolctl = ">=3.1.0"
+
+[package.extras]
+benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"]
+build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"]
+docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"]
+examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"]
+install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"]
+maintenance = ["conda-lock (==2.5.6)"]
+tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"]
+
+[[package]]
+name = "scipy"
+version = "1.13.1"
+description = "Fundamental algorithms for scientific computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"},
+ {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"},
+ {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"},
+ {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"},
+ {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"},
+ {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"},
+ {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"},
+ {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"},
+ {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"},
+ {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"},
+ {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"},
+ {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"},
+ {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"},
+ {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"},
+ {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"},
+ {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"},
+ {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"},
+ {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"},
+ {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"},
+ {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"},
+ {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"},
+ {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"},
+ {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"},
+ {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"},
+ {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"},
+]
+
+[package.dependencies]
+numpy = ">=1.22.4,<2.3"
+
+[package.extras]
+dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
+doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"]
+test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
+
+[[package]]
+name = "seaborn"
+version = "0.13.2"
+description = "Statistical data visualization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"},
+ {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"},
+]
+
+[package.dependencies]
+matplotlib = ">=3.4,<3.6.1 || >3.6.1"
+numpy = ">=1.20,<1.24.0 || >1.24.0"
+pandas = ">=1.2"
+
+[package.extras]
+dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"]
+docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
+stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
+
+[[package]]
+name = "setuptools"
+version = "70.3.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
+ {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
+]
+
+[package.extras]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
+]
+
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+optional = false
+python-versions = "*"
+files = [
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+optional = false
+python-versions = "*"
+files = [
+ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
+ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.5"
+description = "A modern CSS selector implementation for Beautiful Soup."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
+ {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
+]
+
+[[package]]
+name = "sphinx"
+version = "6.2.1"
+description = "Python documentation generator"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"},
+ {file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"},
+]
+
+[package.dependencies]
+alabaster = ">=0.7,<0.8"
+babel = ">=2.9"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.18.1,<0.20"
+imagesize = ">=1.3"
+importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
+Jinja2 = ">=3.0"
+packaging = ">=21.0"
+Pygments = ">=2.13"
+requests = ">=2.25.0"
+snowballstemmer = ">=2.0"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
+test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
+
+[[package]]
+name = "sphinx-basic-ng"
+version = "1.0.0b2"
+description = "A modern skeleton for Sphinx themes."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"},
+ {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"},
+]
+
+[package.dependencies]
+sphinx = ">=4.0"
+
+[package.extras]
+docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"]
+
+[[package]]
+name = "sphinx-copybutton"
+version = "0.5.2"
+description = "Add a copy button to each of your code cells."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"},
+ {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"},
+]
+
+[package.dependencies]
+sphinx = ">=1.8"
+
+[package.extras]
+code-style = ["pre-commit (==2.12.1)"]
+rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"]
+
+[[package]]
+name = "sphinx-markdown-tables"
+version = "0.0.17"
+description = "A Sphinx extension for rendering tables written in markdown"
+optional = false
+python-versions = "*"
+files = [
+ {file = "sphinx-markdown-tables-0.0.17.tar.gz", hash = "sha256:6bc6d3d400eaccfeebd288446bc08dd83083367c58b85d40fe6c12d77ef592f1"},
+ {file = "sphinx_markdown_tables-0.0.17-py3-none-any.whl", hash = "sha256:2bd0c30779653e4dd120300cbd9ca412c480738cc2241f6dea477a883f299e04"},
+]
+
+[package.dependencies]
+markdown = ">=3.4"
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "1.0.8"
+description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"},
+ {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-bibtex"
+version = "2.5.0"
+description = "Sphinx extension for BibTeX style citations."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "sphinxcontrib-bibtex-2.5.0.tar.gz", hash = "sha256:71b42e5db0e2e284f243875326bf9936aa9a763282277d75048826fef5b00eaa"},
+ {file = "sphinxcontrib_bibtex-2.5.0-py3-none-any.whl", hash = "sha256:748f726eaca6efff7731012103417ef130ecdcc09501b4d0c54283bf5f059f76"},
+]
+
+[package.dependencies]
+docutils = ">=0.8"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
+pybtex = ">=0.24"
+pybtex-docutils = ">=1.0.0"
+Sphinx = ">=2.1"
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "1.0.6"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"},
+ {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.0.5"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"},
+ {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
+test = ["html5lib", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+ {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+
+[package.extras]
+test = ["flake8", "mypy", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "1.0.7"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"},
+ {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "1.1.10"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"},
+ {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+standalone = ["Sphinx (>=5)"]
+test = ["pytest"]
+
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+description = "Extract data from python stack frames and tracebacks for informative displays"
+optional = false
+python-versions = "*"
+files = [
+ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
+ {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
+]
+
+[package.dependencies]
+asttokens = ">=2.1.0"
+executing = ">=1.2.0"
+pure-eval = "*"
+
+[package.extras]
+tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
+
+[[package]]
+name = "stim"
+version = "1.13.0"
+description = "A fast library for analyzing with quantum stabilizer circuits."
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "stim-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4cf48c0354ffff024009616dd659a872a39045ce481d360d90b3cecacf1e0e8f"},
+ {file = "stim-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65d890bd26e8b6b69754c67a33f87f620112f533ed939bdc42eae260f78443d7"},
+ {file = "stim-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710995a99162bb79d40f155a5c57d0a2576c4463d2ce52c62134eea450492d08"},
+ {file = "stim-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:8055ae3acef59f482e2aa57894151c9f66926842373628a356d3492cca6df8d1"},
+ {file = "stim-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:880c8e4afbf332d878b0e2270c26151f22168cc1241a12f03676ccd87b93b2bf"},
+ {file = "stim-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f6f7f444aea04d98a63e14403c25b853766ad205aecb712148167c597d1cfc35"},
+ {file = "stim-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05572d3c4d594811b9c2ce3c3d595ad29834625cf674ad85682a7839af055996"},
+ {file = "stim-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:3375e247c618fd2cabb0717911d0aea691d6dcbce59daab3ad7b451051943daa"},
+ {file = "stim-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2ec3a91c4cbe9cdb4b3c8cf621c1d45ad2d784b4fbdb702ed507e482bd4261b"},
+ {file = "stim-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:45e5cc5fdc44fcd081c5b2357a5fc2b662fabea05c4c9bfffbfec97ddb8b4b85"},
+ {file = "stim-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb9d6465ecfcc869e76353e1e81d574854f17ab901bbbb844d99c1290cb6edb"},
+ {file = "stim-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:ab5b39e305c7bbe05b0595345823f188bac5137c6ec73efc95cc98cbdcbe0c7f"},
+ {file = "stim-1.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8224092c38a7f401d935194c7dee64d34c55b9073ecf2f39d5d4c1cc7775d101"},
+ {file = "stim-1.13.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f032bf2a3b3b1bd6a3a0a6b10ad623bf91b7fb40712f8777c49fc61ae1bc0c4f"},
+ {file = "stim-1.13.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad10a8cce01def490116260fc4ef8c82b87a428fdc557187d88d282bdf1ee846"},
+ {file = "stim-1.13.0-cp36-cp36m-win32.whl", hash = "sha256:4127e32cb6603496eab703047da2b05aa42a9fd57a925e45c0aaa38ef840ceb7"},
+ {file = "stim-1.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2fa2448d8801125f1d9d01ace2a911affd95db7d8ac912062403250e9a5d6998"},
+ {file = "stim-1.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:927f32f64e78f1dc21e3f8d46cba05b2ee6f9cb74438898a18ae6212ca47549b"},
+ {file = "stim-1.13.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a3927cc72d39b3115dfbea7f5b29f12f1baeb85236e9655c7d1cac1f98e3779"},
+ {file = "stim-1.13.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f586f0563f151942b895038e561fab84bbc074ebbfa4965ee6fb3d1297d1f6a2"},
+ {file = "stim-1.13.0-cp37-cp37m-win32.whl", hash = "sha256:2afc76a5dc5a81fa4cd8b916ff78a5cb042d8ecfb147b2095f474f5cfc427a9d"},
+ {file = "stim-1.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05670eed32671a96e5f688d33a0269f6928a489c8a3135515f70e22e25692317"},
+ {file = "stim-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0dffd23dee4d9c77a357eb606de77f9a9072465cad73509240da22417eb6b56"},
+ {file = "stim-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecedfd5755324b7a44e8309a1c0ba6a4ad2448fcb069e3e703826fb9bed88709"},
+ {file = "stim-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ca7261e0825f053c49561e70ae35687983575ad50aee2af16cd25dcb40bae6"},
+ {file = "stim-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6c1917650706cfaf97524d7cd06da2d411a1ac72f989771fa25e3264eb6ac43"},
+ {file = "stim-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:29832a0429148447c8557fedfa7bd68a393244542b4d7c4a6636654e0050e824"},
+ {file = "stim-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7a70f18b7bbe36be5843bb96034a8604d2fbad006a60657a4f73d209b06210d9"},
+ {file = "stim-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a6ec4469d31873c9b790944e742bf87fbabb716b660983e92845c4f378846cd"},
+ {file = "stim-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:71d43995030f53d358f911247815e8de30d6f1ba504cdf2c3577cab64f9e42b8"},
+ {file = "stim-1.13.0.tar.gz", hash = "sha256:f9ddbb4f04df5746a8baf705cfbc41a3efc93eceb8b1010f9fb3de425a4b3e06"},
+]
+
+[package.dependencies]
+numpy = "*"
+
+[[package]]
+name = "sympy"
+version = "1.13.1"
+description = "Computer algebra system (CAS) in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"},
+ {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"},
+]
+
+[package.dependencies]
+mpmath = ">=1.1.0,<1.4"
+
+[package.extras]
+dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"]
+
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+description = "Pretty-print tabular data"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
+ {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
+]
+
+[package.extras]
+widechars = ["wcwidth"]
+
+[[package]]
+name = "tbb"
+version = "2021.13.0"
+description = "Intel® oneAPI Threading Building Blocks (oneTBB)"
+optional = false
+python-versions = "*"
+files = [
+ {file = "tbb-2021.13.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:a2567725329639519d46d92a2634cf61e76601dac2f777a05686fea546c4fe4f"},
+ {file = "tbb-2021.13.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:aaf667e92849adb012b8874d6393282afc318aca4407fc62f912ee30a22da46a"},
+ {file = "tbb-2021.13.0-py3-none-win32.whl", hash = "sha256:6669d26703e9943f6164c6407bd4a237a45007e79b8d3832fe6999576eaaa9ef"},
+ {file = "tbb-2021.13.0-py3-none-win_amd64.whl", hash = "sha256:3528a53e4bbe64b07a6112b4c5a00ff3c61924ee46c9c68e004a1ac7ad1f09c3"},
+]
+
+[[package]]
+name = "tenacity"
+version = "8.5.0"
+description = "Retry code until it succeeds"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
+ {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
+]
+
+[package.extras]
+doc = ["reno", "sphinx"]
+test = ["pytest", "tornado (>=4.5)", "typeguard"]
+
+[[package]]
+name = "tensorboard"
+version = "2.17.0"
+description = "TensorBoard lets you watch Tensors Flow"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "tensorboard-2.17.0-py3-none-any.whl", hash = "sha256:859a499a9b1fb68a058858964486627100b71fcb21646861c61d31846a6478fb"},
+]
+
+[package.dependencies]
+absl-py = ">=0.4"
+grpcio = ">=1.48.2"
+markdown = ">=2.6.8"
+numpy = ">=1.12.0"
+protobuf = ">=3.19.6,<4.24.0 || >4.24.0,<5.0.0"
+setuptools = ">=41.0.0"
+six = ">1.9"
+tensorboard-data-server = ">=0.7.0,<0.8.0"
+werkzeug = ">=1.0.1"
+
+[[package]]
+name = "tensorboard-data-server"
+version = "0.7.2"
+description = "Fast data loading for TensorBoard"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb"},
+ {file = "tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60"},
+ {file = "tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530"},
+]
+
+[[package]]
+name = "tensorflow"
+version = "2.17.0"
+description = "TensorFlow is an open source machine learning framework for everyone."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "tensorflow-2.17.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:515fe5ae8a9bc50312575412b08515f3ca66514c155078e0707bdffbea75d783"},
+ {file = "tensorflow-2.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b36683ac28af20abc3a548c72bf4537b00df1b1f3dd39d59df3873fefaf26f15"},
+ {file = "tensorflow-2.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147c93ded4cb7e500a65d3c26d74744ff41660db7a8afe2b00d1d08bf329b4ec"},
+ {file = "tensorflow-2.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46090587f69e33637d17d7c3d94a790cac7d4bc5ff5ecbf3e71fdc6982fe96e"},
+ {file = "tensorflow-2.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e8d26d6c24ccfb139db1306599257ca8f5cfe254ef2d023bfb667f374a17a64d"},
+ {file = "tensorflow-2.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca82f98ea38fa6c9e08ccc69eb6c2fab5b35b30a8999115b8b63b6f02fc69d9d"},
+ {file = "tensorflow-2.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8339777b1b5ebd8ffadaa8196f786e65fbb081a371d8e87b52f24563392d8552"},
+ {file = "tensorflow-2.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:ef615c133cf4d592a073feda634ccbeb521a554be57de74f8c318d38febbeab5"},
+ {file = "tensorflow-2.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ee18b4fcd627c5e872eabb25092af6c808b6ec77948662c88fc5c89a60eb0211"},
+ {file = "tensorflow-2.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72adfef0ee39dd641627906fd7b244fcf21bdd8a87216a998ed74d9c74653aff"},
+ {file = "tensorflow-2.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad7bfea6afb4ded3928ca5b24df9fda876cea4904c103a5163fcc0c3483e7a4"},
+ {file = "tensorflow-2.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:278bc80642d799adf08dc4e04f291aab603bba7457d50c1f9bc191ebbca83f43"},
+ {file = "tensorflow-2.17.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:97f89e95d68b4b46e1072243b9f315c3b340e27cc07b1e1988e2ca97ad844305"},
+ {file = "tensorflow-2.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dde37cff74ed22b8fa2eea944805b001ae38e96adc989666422bdea34f4e2d47"},
+ {file = "tensorflow-2.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ae8e6746deb2ec807b902ba26d62fcffb6a6b53555a1a5906ec00416c5e4175"},
+ {file = "tensorflow-2.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f80d11ad3766570deb6ff47d2bed2d166f51399ca08205e38ef024345571d6f"},
+]
+
+[package.dependencies]
+absl-py = ">=1.0.0"
+astunparse = ">=1.6.0"
+flatbuffers = ">=24.3.25"
+gast = ">=0.2.1,<0.5.0 || >0.5.0,<0.5.1 || >0.5.1,<0.5.2 || >0.5.2"
+google-pasta = ">=0.1.1"
+grpcio = ">=1.24.3,<2.0"
+h5py = ">=3.10.0"
+keras = ">=3.2.0"
+libclang = ">=13.0.0"
+ml-dtypes = ">=0.3.1,<0.5.0"
+numpy = [
+ {version = ">=1.23.5,<2.0.0", markers = "python_version <= \"3.11\""},
+ {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""},
+]
+opt-einsum = ">=2.3.2"
+packaging = "*"
+protobuf = ">=3.20.3,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev"
+requests = ">=2.21.0,<3"
+setuptools = "*"
+six = ">=1.12.0"
+tensorboard = ">=2.17,<2.18"
+tensorflow-io-gcs-filesystem = {version = ">=0.23.1", markers = "python_version < \"3.12\""}
+termcolor = ">=1.1.0"
+typing-extensions = ">=3.6.6"
+wrapt = ">=1.11.0"
+
+[package.extras]
+and-cuda = ["nvidia-cublas-cu12 (==12.3.4.1)", "nvidia-cuda-cupti-cu12 (==12.3.101)", "nvidia-cuda-nvcc-cu12 (==12.3.107)", "nvidia-cuda-nvrtc-cu12 (==12.3.107)", "nvidia-cuda-runtime-cu12 (==12.3.101)", "nvidia-cudnn-cu12 (==8.9.7.29)", "nvidia-cufft-cu12 (==11.0.12.1)", "nvidia-curand-cu12 (==10.3.4.107)", "nvidia-cusolver-cu12 (==11.5.4.101)", "nvidia-cusparse-cu12 (==12.2.0.103)", "nvidia-nccl-cu12 (==2.19.3)", "nvidia-nvjitlink-cu12 (==12.3.101)"]
+
+[[package]]
+name = "tensorflow-io-gcs-filesystem"
+version = "0.37.1"
+description = "TensorFlow IO"
+optional = false
+python-versions = "<3.13,>=3.7"
+files = [
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:249c12b830165841411ba71e08215d0e94277a49c551e6dd5d72aab54fe5491b"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:257aab23470a0796978efc9c2bcf8b0bc80f22e6298612a4c0a50d3f4e88060c"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8febbfcc67c61e542a5ac1a98c7c20a91a5e1afc2e14b1ef0cb7c28bc3b6aa70"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9679b36e3a80921876f31685ab6f7270f3411a4cc51bc2847e80d0e4b5291e27"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:32c50ab4e29a23c1f91cd0f9ab8c381a0ab10f45ef5c5252e94965916041737c"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b02f9c5f94fd62773954a04f69b68c4d576d076fd0db4ca25d5479f0fbfcdbad"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e1f2796b57e799a8ca1b75bf47c2aaa437c968408cc1a402a9862929e104cda"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7c8ee5fe2fd8cb6392669ef16e71841133041fee8a330eff519ad9b36e4556"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ffebb6666a7bfc28005f4fbbb111a455b5e7d6cd3b12752b7050863ecb27d5cc"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fe8dcc6d222258a080ac3dfcaaaa347325ce36a7a046277f6b3e19abc1efb3c5"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbb33f1745f218464a59cecd9a18e32ca927b0f4d77abd8f8671b645cc1a182f"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:286389a203a5aee1a4fa2e53718c661091aa5fea797ff4fa6715ab8436b02e6c"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ee5da49019670ed364f3e5fb86b46420841a6c3cb52a300553c63841671b3e6d"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8943036bbf84e7a2be3705cb56f9c9df7c48c9e614bb941f0936c58e3ca89d6f"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:426de1173cb81fbd62becec2012fc00322a295326d90eb6c737fab636f182aed"},
+ {file = "tensorflow_io_gcs_filesystem-0.37.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df00891669390078a003cedbdd3b8e645c718b111917535fa1d7725e95cdb95"},
+]
+
+[package.extras]
+tensorflow = ["tensorflow (>=2.16.0,<2.17.0)"]
+tensorflow-aarch64 = ["tensorflow-aarch64 (>=2.16.0,<2.17.0)"]
+tensorflow-cpu = ["tensorflow-cpu (>=2.16.0,<2.17.0)"]
+tensorflow-gpu = ["tensorflow-gpu (>=2.16.0,<2.17.0)"]
+tensorflow-rocm = ["tensorflow-rocm (>=2.16.0,<2.17.0)"]
+
+[[package]]
+name = "termcolor"
+version = "2.4.0"
+description = "ANSI color formatting for output in terminal"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
+ {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
+]
+
+[package.extras]
+tests = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.5.0"
+description = "threadpoolctl"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"},
+ {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"},
+]
+
+[[package]]
+name = "tinycss2"
+version = "1.3.0"
+description = "A tiny CSS parser"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"},
+ {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"},
+]
+
+[package.dependencies]
+webencodings = ">=0.4"
+
+[package.extras]
+doc = ["sphinx", "sphinx_rtd_theme"]
+test = ["pytest", "ruff"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.13.0"
+description = "Style preserving TOML library"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"},
+ {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"},
+]
+
+[[package]]
+name = "toolz"
+version = "0.12.1"
+description = "List processing tools and functional utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"},
+ {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"},
+]
+
+[[package]]
+name = "torch"
+version = "2.3.1"
+description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "torch-2.3.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:605a25b23944be5ab7c3467e843580e1d888b8066e5aaf17ff7bf9cc30001cc3"},
+ {file = "torch-2.3.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f2357eb0965583a0954d6f9ad005bba0091f956aef879822274b1bcdb11bd308"},
+ {file = "torch-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:32b05fe0d1ada7f69c9f86c14ff69b0ef1957a5a54199bacba63d22d8fab720b"},
+ {file = "torch-2.3.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:7c09a94362778428484bcf995f6004b04952106aee0ef45ff0b4bab484f5498d"},
+ {file = "torch-2.3.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:b2ec81b61bb094ea4a9dee1cd3f7b76a44555375719ad29f05c0ca8ef596ad39"},
+ {file = "torch-2.3.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:490cc3d917d1fe0bd027057dfe9941dc1d6d8e3cae76140f5dd9a7e5bc7130ab"},
+ {file = "torch-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5802530783bd465fe66c2df99123c9a54be06da118fbd785a25ab0a88123758a"},
+ {file = "torch-2.3.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:a7dd4ed388ad1f3d502bf09453d5fe596c7b121de7e0cfaca1e2017782e9bbac"},
+ {file = "torch-2.3.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:a486c0b1976a118805fc7c9641d02df7afbb0c21e6b555d3bb985c9f9601b61a"},
+ {file = "torch-2.3.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:224259821fe3e4c6f7edf1528e4fe4ac779c77addaa74215eb0b63a5c474d66c"},
+ {file = "torch-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:e5fdccbf6f1334b2203a61a0e03821d5845f1421defe311dabeae2fc8fbeac2d"},
+ {file = "torch-2.3.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:3c333dc2ebc189561514eda06e81df22bf8fb64e2384746b2cb9f04f96d1d4c8"},
+ {file = "torch-2.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:07e9ba746832b8d069cacb45f312cadd8ad02b81ea527ec9766c0e7404bb3feb"},
+ {file = "torch-2.3.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:462d1c07dbf6bb5d9d2f3316fee73a24f3d12cd8dacf681ad46ef6418f7f6626"},
+ {file = "torch-2.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff60bf7ce3de1d43ad3f6969983f321a31f0a45df3690921720bcad6a8596cc4"},
+ {file = "torch-2.3.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:bee0bd33dc58aa8fc8a7527876e9b9a0e812ad08122054a5bff2ce5abf005b10"},
+ {file = "torch-2.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:aaa872abde9a3d4f91580f6396d54888620f4a0b92e3976a6034759df4b961ad"},
+ {file = "torch-2.3.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3d7a7f7ef21a7520510553dc3938b0c57c116a7daee20736a9e25cbc0e832bdc"},
+ {file = "torch-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:4777f6cefa0c2b5fa87223c213e7b6f417cf254a45e5829be4ccd1b2a4ee1011"},
+ {file = "torch-2.3.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:2bb5af780c55be68fe100feb0528d2edebace1d55cb2e351de735809ba7391eb"},
+]
+
+[package.dependencies]
+filelock = "*"
+fsspec = "*"
+jinja2 = "*"
+mkl = {version = ">=2021.1.1,<=2021.4.0", markers = "platform_system == \"Windows\""}
+networkx = "*"
+nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-nccl-cu12 = {version = "2.20.5", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+sympy = "*"
+triton = {version = "2.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""}
+typing-extensions = ">=4.8.0"
+
+[package.extras]
+opt-einsum = ["opt-einsum (>=3.3)"]
+optree = ["optree (>=0.9.1)"]
+
+[[package]]
+name = "tornado"
+version = "6.4.1"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"},
+ {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"},
+ {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"},
+ {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"},
+ {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"},
+ {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"},
+ {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"},
+ {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"},
+ {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"},
+ {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"},
+ {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"},
+]
+
+[[package]]
+name = "tqdm"
+version = "4.66.4"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
+ {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+description = "Traitlets Python configuration system"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
+ {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
+
+[[package]]
+name = "triton"
+version = "2.3.1"
+description = "A language and compiler for custom Deep Learning operations"
+optional = false
+python-versions = "*"
+files = [
+ {file = "triton-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c84595cbe5e546b1b290d2a58b1494df5a2ef066dd890655e5b8a8a92205c33"},
+ {file = "triton-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d64ae33bcb3a7a18081e3a746e8cf87ca8623ca13d2c362413ce7a486f893e"},
+ {file = "triton-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf80e8761a9e3498aa92e7bf83a085b31959c61f5e8ac14eedd018df6fccd10"},
+ {file = "triton-2.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b13bf35a2b659af7159bf78e92798dc62d877aa991de723937329e2d382f1991"},
+ {file = "triton-2.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63381e35ded3304704ea867ffde3b7cfc42c16a55b3062d41e017ef510433d66"},
+ {file = "triton-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d968264523c7a07911c8fb51b4e0d1b920204dae71491b1fe7b01b62a31e124"},
+]
+
+[package.dependencies]
+filelock = "*"
+
+[package.extras]
+build = ["cmake (>=3.20)", "lit"]
+tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"]
+tutorials = ["matplotlib", "pandas", "tabulate", "torch"]
+
+[[package]]
+name = "types-deprecated"
+version = "1.2.9.20240311"
+description = "Typing stubs for Deprecated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-Deprecated-1.2.9.20240311.tar.gz", hash = "sha256:0680e89989a8142707de8103f15d182445a533c1047fd9b7e8c5459101e9b90a"},
+ {file = "types_Deprecated-1.2.9.20240311-py3-none-any.whl", hash = "sha256:d7793aaf32ff8f7e49a8ac781de4872248e0694c4b75a7a8a186c51167463f9d"},
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20240316"
+description = "Typing stubs for python-dateutil"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"},
+ {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"},
+]
+
+[[package]]
+name = "types-retry"
+version = "0.9.9.4"
+description = "Typing stubs for retry"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-retry-0.9.9.4.tar.gz", hash = "sha256:e4731dc684b56b875d9746459ad665d3bc281a56b530acdf1c97730167799941"},
+ {file = "types_retry-0.9.9.4-py3-none-any.whl", hash = "sha256:f29760a9fe8b1fefe253e5fe6be7e4c0eba243932c600e0eccffb42a21d17765"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.2.2"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
+ {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+description = "Measures the displayed width of unicode strings in a terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
+ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
+]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+optional = false
+python-versions = "*"
+files = [
+ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.0.3"
+description = "The comprehensive WSGI web application library."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
+ {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog (>=2.3)"]
+
+[[package]]
+name = "wheel"
+version = "0.43.0"
+description = "A built-package format for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"},
+ {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"},
+]
+
+[package.extras]
+test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.11"
+description = "Jupyter interactive widgets for Jupyter Notebook"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"},
+ {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"},
+]
+
+[[package]]
+name = "wmctrl"
+version = "0.5"
+description = "A tool to programmatically control windows inside X"
+optional = false
+python-versions = ">=2.7"
+files = [
+ {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"},
+ {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"},
+]
+
+[package.dependencies]
+attrs = "*"
+
+[package.extras]
+test = ["pytest"]
+
+[[package]]
+name = "wrapt"
+version = "1.16.0"
+description = "Module for decorators, wrappers and monkey patching."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"},
+ {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"},
+ {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"},
+ {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"},
+ {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"},
+ {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"},
+ {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"},
+ {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"},
+ {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"},
+ {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"},
+ {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"},
+ {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"},
+ {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"},
+ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
+]
+
+[[package]]
+name = "zipp"
+version = "3.19.2"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
+ {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
+]
+
+[package.extras]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+
+[extras]
+qinfo = []
+qulacs = ["qulacs"]
+tensorflow = ["tensorflow"]
+torch = ["torch"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.9,<3.13"
+content-hash = "bb1ae3d1fd342754f07fd73c3068f2aeb5826e3bf2af2a4501c27fbcaea7bbbc"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..517764907
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,135 @@
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "qibo"
+version = "0.2.11"
+description = "A framework for quantum computing with hardware acceleration."
+authors = ["The Qibo team"]
+license = "Apache License 2.0"
+readme = "README.md"
+homepage = "https://qibo.science/"
+repository = "https://github.com/qiboteam/qibo/"
+documentation = "https://qibo.science/docs/qibo/stable"
+keywords = []
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "Topic :: Scientific/Engineering :: Physics",
+]
+packages = [{ include = "qibo", from = "src" }]
+include = ["*.out", "*.yml"]
+
+[tool.poetry.dependencies]
+python = ">=3.9,<3.13"
+scipy = "^1.10.1"
+sympy = "^1.11.1"
+cma = "^3.3.0"
+joblib = "^1.2.0"
+hyperopt = "^0.2.7"
+# `setuptools` is only required because undeclared by `hyperopt`
+setuptools = ">=69.1.1,<71.0.0"
+tabulate = "^0.9.0"
+openqasm3 = { version = ">=0.5.0", extras = ["parser"] }
+numpy = "^1.26.4"
+networkx = "^3.2.1"
+tensorflow = { version = "^2.16.1", markers = "sys_platform == 'linux' or sys_platform == 'darwin'", optional = true }
+torch = { version = "^2.1.1", optional = true }
+qulacs = { version = "^0.6.4", optional = true, markers="(sys_platform == 'darwin' and python_version > '3.9') or sys_platform != 'darwin'"}
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+pdbpp = "^0.10.3"
+ipython = "^8.12.0"
+
+[tool.poetry.group.docs]
+optional = true
+
+[tool.poetry.group.docs.dependencies]
+sphinx = "^6.1.3"
+furo = "^2022.12.7"
+recommonmark = "^0.7.1"
+sphinxcontrib-bibtex = "^2.5.0"
+sphinx-markdown-tables = "^0.0.17"
+sphinx-copybutton = "^0.5.2"
+nbsphinx = "^0.8.12"
+ipython = "^8.10.0"
+qulacs = { version = "^0.6.4", markers="(sys_platform == 'darwin' and python_version > '3.9') or sys_platform != 'darwin'"}
+seaborn = "^0.13.2"
+ipykernel = "^6.29.4"
+qibojit = { git = "https://github.com/qiboteam/qibojit.git" }
+
+[tool.poetry.group.tests]
+optional = true
+
+[tool.poetry.group.tests.dependencies]
+pytest = "^7.2.1"
+cirq = "^1.1.0"
+ply = "^3.11"
+scikit-learn = "^1.2.1"
+pytest-cov = "^4.0.0"
+pylint = "3.1.0"
+matplotlib = "^3.7.0"
+tensorflow = { version = "^2.16.1", markers = "sys_platform == 'linux'" }
+torch = "^2.1.1"
+qibojit = { git = "https://github.com/qiboteam/qibojit.git" }
+qibotn = { git = "https://github.com/qiboteam/qibotn.git" }
+stim = "^1.12.0"
+qulacs = { version = "^0.6.4", markers="(sys_platform == 'darwin' and python_version > '3.9') or sys_platform != 'darwin'" }
+
+[tool.poe.tasks]
+test = "pytest"
+lint = "pylint src --errors-only"
+lint-warnings = "pylint src --exit-zero"
+docs = "make -C doc html"
+docs-clean = "make -C doc clean"
+test-docs = "make -C doc doctest"
+
+
+
+[tool.poetry.group.cuda11]
+optional = true
+
+[tool.poetry.group.cuda11.dependencies]
+cupy-cuda11x = "^12.0.0"
+cuquantum-python-cu11 = "^23.3.0"
+qibojit = { git = "https://github.com/qiboteam/qibojit.git" }
+qibotn = { git = "https://github.com/qiboteam/qibotn.git" }
+
+
+[tool.poetry.group.cuda12]
+optional = true
+
+[tool.poetry.group.cuda12.dependencies]
+cupy-cuda12x = "^12.0.0"
+cuquantum-python-cu12 = "^23.3.0"
+qibojit = { git = "https://github.com/qiboteam/qibojit.git" }
+qibotn = { git = "https://github.com/qiboteam/qibotn.git" }
+
+[tool.poetry.extras]
+tensorflow = ["tensorflow"]
+torch = ["torch"]
+qinfo = ["stim"]
+qulacs = ["qulacs"]
+
+[tool.pylint.main]
+ignored-modules = ["cvxpy"]
+
+[tool.pylint.reports]
+output-format = "colorized"
+
+[tool.coverage.run]
+omit = ["src/qibo/noise_model.py"]
+
+[tool.pytest.ini_options]
+testpaths = ['tests/']
+filterwarnings = ['ignore::RuntimeWarning']
+addopts = [
+ '--cov=qibo',
+ '--cov-append',
+ '--cov-report=xml',
+ '--cov-report=html',
+ '--durations=60',
+]
diff --git a/selfhosted b/selfhosted
new file mode 100755
index 000000000..21afeef92
--- /dev/null
+++ b/selfhosted
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Script for running the selfhosted tests on QPUs directly from GitHub
+# Tests need to be copied to /tmp/ because coverage does not work with NFS
+
+cp -r tests /tmp/
+cp pyproject.toml /tmp/
+cd /tmp/tests
+source /nfs/users/github/actions-runner/_work/qibo/qibo/testenv/bin/activate
+pytest
+pytest_status=$?
+if [[ $pytest_status -ne 0 ]]
+ then
+ exit $pytest_status
+fi
+cd -
+mv /tmp/tests/coverage.xml .
+mv /tmp/tests/htmlcov .
+rm -r /tmp/tests
diff --git a/src/qibo/__init__.py b/src/qibo/__init__.py
new file mode 100644
index 000000000..bcd912f1c
--- /dev/null
+++ b/src/qibo/__init__.py
@@ -0,0 +1,35 @@
+import importlib.metadata as im
+
+__version__ = im.version(__package__)
+
+from qibo import (
+ callbacks,
+ gates,
+ hamiltonians,
+ models,
+ optimizers,
+ parallel,
+ parameter,
+ result,
+ solvers,
+)
+from qibo.backends import (
+ construct_backend,
+ get_backend,
+ get_device,
+ get_precision,
+ get_threads,
+ list_available_backends,
+ matrices,
+ set_backend,
+ set_device,
+ set_precision,
+ set_threads,
+)
+from qibo.config import (
+ get_batch_size,
+ get_metropolis_threshold,
+ set_batch_size,
+ set_metropolis_threshold,
+)
+from qibo.models.circuit import Circuit
diff --git a/src/qibo/backends/__init__.py b/src/qibo/backends/__init__.py
new file mode 100644
index 000000000..f9334d00c
--- /dev/null
+++ b/src/qibo/backends/__init__.py
@@ -0,0 +1,261 @@
+import os
+from importlib import import_module
+
+import numpy as np
+
+from qibo.backends.abstract import Backend
+from qibo.backends.clifford import CliffordBackend
+from qibo.backends.npmatrices import NumpyMatrices
+from qibo.backends.numpy import NumpyBackend
+from qibo.backends.pytorch import PyTorchBackend
+from qibo.backends.tensorflow import TensorflowBackend
+from qibo.config import log, raise_error
+
+QIBO_NATIVE_BACKENDS = ("numpy", "tensorflow", "pytorch", "qulacs")
+
+
+class MetaBackend:
+ """Meta-backend class which takes care of loading the qibo backends."""
+
+ @staticmethod
+ def load(backend: str, **kwargs) -> Backend:
+ """Loads the native qibo backend.
+
+ Args:
+ backend (str): Name of the backend to load.
+ kwargs (dict): Additional arguments for the qibo backend.
+ Returns:
+ qibo.backends.abstract.Backend: The loaded backend.
+ """
+
+ if backend == "numpy":
+ return NumpyBackend()
+ elif backend == "tensorflow":
+ return TensorflowBackend()
+ elif backend == "pytorch":
+ return PyTorchBackend()
+ elif backend == "clifford":
+ engine = kwargs.pop("platform", None)
+ kwargs["engine"] = engine
+ return CliffordBackend(**kwargs)
+ elif backend == "qulacs":
+ from qibo.backends.qulacs import QulacsBackend
+
+ return QulacsBackend()
+ else:
+ raise_error(
+ ValueError,
+ f"Backend {backend} is not available. The native qibo backends are {QIBO_NATIVE_BACKENDS}.",
+ )
+
+ def list_available(self) -> dict:
+ """Lists all the available native qibo backends."""
+ available_backends = {}
+ for backend in QIBO_NATIVE_BACKENDS:
+ try:
+ MetaBackend.load(backend)
+ available = True
+ except: # pragma: no cover
+ available = False
+ available_backends[backend] = available
+ return available_backends
+
+
+class GlobalBackend(NumpyBackend):
+ """The global backend will be used as default by ``circuit.execute()``."""
+
+ _instance = None
+ _dtypes = {"double": "complex128", "single": "complex64"}
+ _default_order = [
+ {"backend": "qibojit", "platform": "cupy"},
+ {"backend": "qibojit", "platform": "numba"},
+ {"backend": "tensorflow"},
+ {"backend": "numpy"},
+ {"backend": "pytorch"},
+ ]
+
+ def __new__(cls):
+ if cls._instance is not None:
+ return cls._instance
+
+ backend = os.environ.get("QIBO_BACKEND")
+ if backend: # pragma: no cover
+ # Create backend specified by user
+ platform = os.environ.get("QIBO_PLATFORM")
+ cls._instance = construct_backend(backend, platform=platform)
+ else:
+ # Create backend according to default order
+ for kwargs in cls._default_order:
+ try:
+ cls._instance = construct_backend(**kwargs)
+ break
+ except (ModuleNotFoundError, ImportError):
+ pass
+
+ if cls._instance is None: # pragma: no cover
+ raise_error(RuntimeError, "No backends available.")
+
+ log.info(f"Using {cls._instance} backend on {cls._instance.device}")
+ return cls._instance
+
+ @classmethod
+ def set_backend(cls, backend, **kwargs): # pragma: no cover
+ if (
+ cls._instance is None
+ or cls._instance.name != backend
+ or cls._instance.platform != kwargs.get("platform")
+ ):
+ cls._instance = construct_backend(backend, **kwargs)
+ log.info(f"Using {cls._instance} backend on {cls._instance.device}")
+
+
+class QiboMatrices:
+ def __init__(self, dtype="complex128"):
+ self.create(dtype)
+
+ def create(self, dtype):
+ self.matrices = NumpyMatrices(dtype)
+ self.I = self.matrices.I(2)
+ self.X = self.matrices.X
+ self.Y = self.matrices.Y
+ self.Z = self.matrices.Z
+ self.SX = self.matrices.SX
+ self.H = self.matrices.H
+ self.S = self.matrices.S
+ self.SDG = self.matrices.SDG
+ self.CNOT = self.matrices.CNOT
+ self.CY = self.matrices.CY
+ self.CZ = self.matrices.CZ
+ self.CSX = self.matrices.CSX
+ self.CSXDG = self.matrices.CSXDG
+ self.SWAP = self.matrices.SWAP
+ self.iSWAP = self.matrices.iSWAP
+ self.SiSWAP = self.matrices.SiSWAP
+ self.SiSWAPDG = self.matrices.SiSWAPDG
+ self.FSWAP = self.matrices.FSWAP
+ self.ECR = self.matrices.ECR
+ self.SYC = self.matrices.SYC
+ self.TOFFOLI = self.matrices.TOFFOLI
+ self.CCZ = self.matrices.CCZ
+
+
+matrices = QiboMatrices()
+
+
+def get_backend():
+ return str(GlobalBackend())
+
+
+def set_backend(backend, **kwargs):
+ GlobalBackend.set_backend(backend, **kwargs)
+
+
+def get_precision():
+ return GlobalBackend().precision
+
+
+def set_precision(precision):
+ GlobalBackend().set_precision(precision)
+ matrices.create(GlobalBackend().dtype)
+
+
+def get_device():
+ return GlobalBackend().device
+
+
+def set_device(device):
+ parts = device[1:].split(":")
+ if device[0] != "/" or len(parts) < 2 or len(parts) > 3:
+ raise_error(
+ ValueError,
+ "Device name should follow the pattern: /{device type}:{device number}.",
+ )
+ backend = GlobalBackend()
+ backend.set_device(device)
+ log.info(f"Using {backend} backend on {backend.device}")
+
+
+def get_threads():
+ return GlobalBackend().nthreads
+
+
+def set_threads(nthreads):
+ if not isinstance(nthreads, int):
+ raise_error(TypeError, "Number of threads must be integer.")
+ if nthreads < 1:
+ raise_error(ValueError, "Number of threads must be positive.")
+ GlobalBackend().set_threads(nthreads)
+
+
+def _check_backend(backend):
+ if backend is None:
+ return GlobalBackend()
+
+ return backend
+
+
+def list_available_backends(*providers: str) -> dict:
+ """Lists all the backends that are available."""
+ available_backends = MetaBackend().list_available()
+ for backend in providers:
+ try:
+ module = import_module(backend.replace("-", "_"))
+ available = getattr(module, "MetaBackend")().list_available()
+ except:
+ available = False
+ available_backends.update({backend: available})
+ return available_backends
+
+
+def construct_backend(backend, **kwargs) -> Backend:
+ """Construct a generic native or non-native qibo backend.
+
+ Args:
+ backend (str): Name of the backend to load.
+ kwargs (dict): Additional arguments for constructing the backend.
+ Returns:
+ qibo.backends.abstract.Backend: The loaded backend.
+ """
+ if backend in QIBO_NATIVE_BACKENDS + ("clifford",):
+ return MetaBackend.load(backend, **kwargs)
+
+ provider = backend.replace("-", "_")
+ try:
+ module = import_module(provider)
+ return getattr(module, "MetaBackend").load(**kwargs)
+ except ImportError as e:
+ # pylint: disable=unsupported-membership-test
+ if provider not in e.msg:
+ raise e
+ raise_error(
+ ValueError,
+ f"The '{backend}' backends' provider is not available. Check that a Python "
+ f"package named '{provider}' is installed, and it is exposing valid Qibo "
+ "backends.",
+ )
+
+
+def _check_backend_and_local_state(seed, backend):
+ if (
+ seed is not None
+ and not isinstance(seed, int)
+ and not isinstance(seed, np.random.Generator)
+ ):
+ raise_error(
+ TypeError, "seed must be either type int or numpy.random.Generator."
+ )
+
+ backend = _check_backend(backend)
+
+ if seed is None or isinstance(seed, int):
+ if backend.__class__.__name__ in [
+ "CupyBackend",
+ "CuQuantumBackend",
+ ]: # pragma: no cover
+ local_state = backend.np.random.default_rng(seed)
+ else:
+ local_state = np.random.default_rng(seed)
+ else:
+ local_state = seed
+
+ return backend, local_state
diff --git a/src/qibo/backends/_clifford_operations.py b/src/qibo/backends/_clifford_operations.py
new file mode 100644
index 000000000..9030123e5
--- /dev/null
+++ b/src/qibo/backends/_clifford_operations.py
@@ -0,0 +1,534 @@
+from functools import cache, reduce
+
+import numpy as np
+from scipy import sparse
+
+name = "numpy"
+
+
+def _get_rxz(symplectic_matrix, nqubits):
+ return (
+ symplectic_matrix[:, -1],
+ symplectic_matrix[:, :nqubits],
+ symplectic_matrix[:, nqubits:-1],
+ )
+
+
+def I(symplectic_matrix, q, nqubits):
+ return symplectic_matrix
+
+
+def H(symplectic_matrix, q, nqubits):
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (x[:, q] & z[:, q])
+ symplectic_matrix[:, [q, nqubits + q]] = symplectic_matrix[:, [nqubits + q, q]]
+ return symplectic_matrix
+
+
+def CNOT(symplectic_matrix, control_q, target_q, nqubits):
+ ind_zt = nqubits + target_q
+ ind_zc = nqubits + control_q
+ r = symplectic_matrix[:, -1]
+ xcq = symplectic_matrix[:, control_q]
+ xtq = symplectic_matrix[:, target_q]
+ ztq = symplectic_matrix[:, ind_zt]
+ zcq = symplectic_matrix[:, ind_zc]
+ symplectic_matrix[:, -1] = r ^ (xcq & ztq) & (xtq ^ ~zcq)
+ symplectic_matrix[:, target_q] = xtq ^ xcq
+ symplectic_matrix[:, ind_zc] = zcq ^ ztq
+ return symplectic_matrix
+
+
+def CZ(symplectic_matrix, control_q, target_q, nqubits):
+ """Decomposition --> H-CNOT-H"""
+ ind_zt = nqubits + target_q
+ ind_zc = nqubits + control_q
+ r = symplectic_matrix[:, -1]
+ xcq = symplectic_matrix[:, control_q]
+ xtq = symplectic_matrix[:, target_q]
+ ztq = symplectic_matrix[:, ind_zt]
+ zcq = symplectic_matrix[:, ind_zc]
+ ztq_xor_xcq = ztq ^ xcq
+ symplectic_matrix[:, -1] = (
+ r ^ (xtq & ztq) ^ (xcq & xtq & (ztq ^ ~zcq)) ^ (xtq & ztq_xor_xcq)
+ )
+ z_control_q = xtq ^ zcq
+ z_target_q = ztq_xor_xcq
+ symplectic_matrix[:, ind_zc] = z_control_q
+ symplectic_matrix[:, ind_zt] = z_target_q
+ return symplectic_matrix
+
+
+def S(symplectic_matrix, q, nqubits):
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ r[:] = r ^ (x[:, q] & z[:, q])
+ z[:, q] = z[:, q] ^ x[:, q]
+ return symplectic_matrix
+
+
+def Z(symplectic_matrix, q, nqubits):
+ """Decomposition --> S-S"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ ((x[:, q] & z[:, q]) ^ x[:, q] & (z[:, q] ^ x[:, q]))
+ return symplectic_matrix
+
+
+def X(symplectic_matrix, q, nqubits):
+ """Decomposition --> H-S-S-H"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (z[:, q] & (z[:, q] ^ x[:, q])) ^ (z[:, q] & x[:, q])
+ return symplectic_matrix
+
+
+def Y(symplectic_matrix, q, nqubits):
+ """Decomposition --> S-S-H-S-S-H"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = (
+ r ^ (z[:, q] & (z[:, q] ^ x[:, q])) ^ (x[:, q] & (z[:, q] ^ x[:, q]))
+ )
+ return symplectic_matrix
+
+
+def SX(symplectic_matrix, q, nqubits):
+ """Decomposition --> H-S-H"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (z[:, q] & (z[:, q] ^ x[:, q]))
+ symplectic_matrix[:, q] = z[:, q] ^ x[:, q]
+ return symplectic_matrix
+
+
+def SDG(symplectic_matrix, q, nqubits):
+ """Decomposition --> S-S-S"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (x[:, q] & (z[:, q] ^ x[:, q]))
+ symplectic_matrix[:, nqubits + q] = z[:, q] ^ x[:, q]
+ return symplectic_matrix
+
+
+def SXDG(symplectic_matrix, q, nqubits):
+ """Decomposition --> H-S-S-S-H"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (z[:, q] & x[:, q])
+ symplectic_matrix[:, q] = z[:, q] ^ x[:, q]
+ return symplectic_matrix
+
+
+def RX(symplectic_matrix, q, nqubits, theta):
+ if theta % (2 * np.pi) == 0:
+ return I(symplectic_matrix, q, nqubits)
+ elif (theta / np.pi - 1) % 2 == 0:
+ return X(symplectic_matrix, q, nqubits)
+ elif (theta / (np.pi / 2) - 1) % 4 == 0:
+ return SX(symplectic_matrix, q, nqubits)
+ else: # theta == 3*pi/2 + 2*n*pi
+ return SXDG(symplectic_matrix, q, nqubits)
+
+
+def RZ(symplectic_matrix, q, nqubits, theta):
+ if theta % (2 * np.pi) == 0:
+ return I(symplectic_matrix, q, nqubits)
+ elif (theta / np.pi - 1) % 2 == 0:
+ return Z(symplectic_matrix, q, nqubits)
+ elif (theta / (np.pi / 2) - 1) % 4 == 0:
+ return S(symplectic_matrix, q, nqubits)
+ else: # theta == 3*pi/2 + 2*n*pi
+ return SDG(symplectic_matrix, q, nqubits)
+
+
+def RY_pi(symplectic_matrix, q, nqubits):
+ """Decomposition --> H-S-S"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (x[:, q] & (z[:, q] ^ x[:, q]))
+ symplectic_matrix[:, [nqubits + q, q]] = symplectic_matrix[:, [q, nqubits + q]]
+ return symplectic_matrix
+
+
+def RY_3pi_2(symplectic_matrix, q, nqubits):
+ """Decomposition --> H-S-S-H-S-S-H-S-S"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = r ^ (z[:, q] & (z[:, q] ^ x[:, q]))
+ symplectic_matrix[:, [nqubits + q, q]] = symplectic_matrix[:, [q, nqubits + q]]
+ return symplectic_matrix
+
+
+def RY(symplectic_matrix, q, nqubits, theta):
+ if theta % (2 * np.pi) == 0:
+ return I(symplectic_matrix, q, nqubits)
+ elif (theta / np.pi - 1) % 2 == 0:
+ return Y(symplectic_matrix, q, nqubits)
+ elif (theta / (np.pi / 2) - 1) % 4 == 0:
+ """Decomposition --> H-S-S"""
+ return RY_pi(symplectic_matrix, q, nqubits)
+ else: # theta == 3*pi/2 + 2*n*pi
+ """Decomposition --> H-S-S-H-S-S-H-S-S"""
+ return RY_3pi_2(symplectic_matrix, q, nqubits)
+
+
+def SWAP(symplectic_matrix, control_q, target_q, nqubits):
+ """Decomposition --> CNOT-CNOT-CNOT"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = (
+ r
+ ^ (x[:, control_q] & z[:, target_q] & (x[:, target_q] ^ ~z[:, control_q]))
+ ^ (
+ (x[:, target_q] ^ x[:, control_q])
+ & (z[:, target_q] ^ z[:, control_q])
+ & (z[:, target_q] ^ ~x[:, control_q])
+ )
+ ^ (
+ x[:, target_q]
+ & z[:, control_q]
+ & (x[:, control_q] ^ x[:, target_q] ^ z[:, control_q] ^ ~z[:, target_q])
+ )
+ )
+ symplectic_matrix[
+ :, [control_q, target_q, nqubits + control_q, nqubits + target_q]
+ ] = symplectic_matrix[
+ :, [target_q, control_q, nqubits + target_q, nqubits + control_q]
+ ]
+ return symplectic_matrix
+
+
+def iSWAP(symplectic_matrix, control_q, target_q, nqubits):
+ """Decomposition --> H-CNOT-CNOT-H-S-S"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = (
+ r
+ ^ (x[:, target_q] & z[:, target_q])
+ ^ (x[:, control_q] & z[:, control_q])
+ ^ (x[:, control_q] & (z[:, control_q] ^ x[:, control_q]))
+ ^ (
+ (z[:, control_q] ^ x[:, control_q])
+ & (z[:, target_q] ^ x[:, target_q])
+ & (x[:, target_q] ^ ~x[:, control_q])
+ )
+ ^ (
+ (x[:, target_q] ^ z[:, control_q] ^ x[:, control_q])
+ & (x[:, target_q] ^ z[:, target_q] ^ x[:, control_q])
+ & (x[:, target_q] ^ z[:, target_q] ^ x[:, control_q] ^ ~z[:, control_q])
+ )
+ ^ (x[:, control_q] & (x[:, target_q] ^ x[:, control_q] ^ z[:, control_q]))
+ )
+ z_control_q = x[:, target_q] ^ z[:, target_q] ^ x[:, control_q]
+ z_target_q = x[:, target_q] ^ z[:, control_q] ^ x[:, control_q]
+ symplectic_matrix[:, nqubits + control_q] = z_control_q
+ symplectic_matrix[:, nqubits + target_q] = z_target_q
+ symplectic_matrix[:, [control_q, target_q]] = symplectic_matrix[
+ :, [target_q, control_q]
+ ]
+ return symplectic_matrix
+
+
+def FSWAP(symplectic_matrix, control_q, target_q, nqubits):
+ """Decomposition --> X-CNOT-RY-CNOT-RY-CNOT-CNOT-X"""
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = RY(symplectic_matrix, control_q, nqubits, np.pi / 2)
+ symplectic_matrix = CNOT(symplectic_matrix, target_q, control_q, nqubits)
+ symplectic_matrix = RY(symplectic_matrix, control_q, nqubits, -np.pi / 2)
+ symplectic_matrix = CNOT(symplectic_matrix, target_q, control_q, nqubits)
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ return X(symplectic_matrix, control_q, nqubits)
+
+
+def CY(symplectic_matrix, control_q, target_q, nqubits):
+ """Decomposition --> S-CNOT-SDG"""
+ r, x, z = _get_rxz(symplectic_matrix, nqubits)
+ symplectic_matrix[:, -1] = (
+ r
+ ^ (x[:, target_q] & (z[:, target_q] ^ x[:, target_q]))
+ ^ (
+ x[:, control_q]
+ & (x[:, target_q] ^ z[:, target_q])
+ & (z[:, control_q] ^ ~x[:, target_q])
+ )
+ ^ ((x[:, target_q] ^ x[:, control_q]) & (z[:, target_q] ^ x[:, target_q]))
+ )
+ x_target_q = x[:, control_q] ^ x[:, target_q]
+ z_control_q = z[:, control_q] ^ z[:, target_q] ^ x[:, target_q]
+ z_target_q = z[:, target_q] ^ x[:, control_q]
+ symplectic_matrix[:, target_q] = x_target_q
+ symplectic_matrix[:, nqubits + control_q] = z_control_q
+ symplectic_matrix[:, nqubits + target_q] = z_target_q
+ return symplectic_matrix
+
+
+def CRX(symplectic_matrix, control_q, target_q, nqubits, theta):
+ # theta = 4 * n * pi
+ if theta % (4 * np.pi) == 0:
+ return I(symplectic_matrix, target_q, nqubits)
+ # theta = pi + 4 * n * pi
+ elif (theta / np.pi - 1) % 4 == 0:
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ return CY(symplectic_matrix, control_q, target_q, nqubits)
+ # theta = 2 * pi + 4 * n * pi
+ elif (theta / (2 * np.pi) - 1) % 2 == 0:
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = Y(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ return Y(symplectic_matrix, target_q, nqubits)
+ # theta = 3 * pi + 4 * n * pi
+ elif (theta / np.pi - 3) % 4 == 0:
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CY(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ return CZ(symplectic_matrix, control_q, target_q, nqubits)
+
+
+def CRZ(symplectic_matrix, control_q, target_q, nqubits, theta):
+ # theta = 4 * n * pi
+ if theta % (4 * np.pi) == 0:
+ return I(symplectic_matrix, target_q, nqubits)
+ # theta = pi + 4 * n * pi
+ elif (theta / np.pi - 1) % 4 == 0:
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CY(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ return CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ # theta = 2 * pi + 4 * n * pi
+ elif (theta / (2 * np.pi) - 1) % 2 == 0:
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ return X(symplectic_matrix, target_q, nqubits)
+ # theta = 3 * pi + 4 * n * pi
+ elif (theta / np.pi - 3) % 4 == 0:
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = X(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CY(symplectic_matrix, control_q, target_q, nqubits)
+ return X(symplectic_matrix, target_q, nqubits)
+
+
+def CRY(symplectic_matrix, control_q, target_q, nqubits, theta):
+ # theta = 4 * n * pi
+ if theta % (4 * np.pi) == 0:
+ return I(symplectic_matrix, target_q, nqubits)
+ # theta = pi + 4 * n * pi
+ elif (theta / np.pi - 1) % 4 == 0:
+ symplectic_matrix = Z(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = Z(symplectic_matrix, target_q, nqubits)
+ return CZ(symplectic_matrix, control_q, target_q, nqubits)
+ # theta = 2 * pi + 4 * n * pi
+ elif (theta / (2 * np.pi) - 1) % 2 == 0:
+ return CRZ(symplectic_matrix, control_q, target_q, nqubits, theta)
+ # theta = 3 * pi + 4 * n * pi
+ elif (theta / np.pi - 3) % 4 == 0:
+ symplectic_matrix = CZ(symplectic_matrix, control_q, target_q, nqubits)
+ symplectic_matrix = Z(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ return Z(symplectic_matrix, target_q, nqubits)
+
+
+def ECR(symplectic_matrix, control_q, target_q, nqubits):
+ symplectic_matrix = S(symplectic_matrix, control_q, nqubits)
+ symplectic_matrix = SX(symplectic_matrix, target_q, nqubits)
+ symplectic_matrix = CNOT(symplectic_matrix, control_q, target_q, nqubits)
+ return X(symplectic_matrix, control_q, nqubits)
+
+
+def _exponent(
+ x1: np.ndarray, z1: np.ndarray, x2: np.ndarray, z2: np.ndarray
+) -> np.ndarray:
+ """Helper function that computes the exponent to which i is raised for the product of the x and z paulis encoded in the symplectic matrix. This is used in _rowsum. The computation is performed parallely over the separated paulis x1[i], z1[i], x2[i] and z2[i].
+
+ Args:
+ x1 (np.array): Bits of the first x paulis.
+ z1 (np.array): Bits of the first z paulis.
+ x2 (np.array): Bits of the second x paulis.
+ z2 (np.array): Bits of the second z paulis.
+
+ Returns:
+ (np.array): The calculated exponents.
+ """
+ return 2 * (x1 * x2 * (z2 - z1) + z1 * z2 * (x1 - x2)) - x1 * z2 + x2 * z1
+
+
+def _rowsum(symplectic_matrix, h, i, nqubits, determined=False):
+ """Helper function that updates the symplectic matrix by setting the h-th generator equal to the (i+h)-th one. This is done to keep track of the phase of the h-th row of the symplectic matrix (r[h]). The function is applied parallely over all the rows h and i passed.
+
+ Args:
+ symplectic_matrix (np.array): Input symplectic matrix.
+ h (np.array): Indices of the rows encoding the generators to update.
+ i (np.array): Indices of the rows encoding the generators to use.
+ nqubits (int): Total number of qubits.
+
+ Returns:
+ (np.array): The updated symplectic matrix.
+ """
+ xi, zi = symplectic_matrix[i, :nqubits], symplectic_matrix[i, nqubits:-1]
+ xh, zh = symplectic_matrix[h, :nqubits], symplectic_matrix[h, nqubits:-1]
+ exponents = _exponent(xi, zi, xh, zh)
+ ind = (
+ 2 * symplectic_matrix[h, -1]
+ + 2 * symplectic_matrix[i, -1]
+ + np.sum(exponents, axis=-1)
+ ) % 4 == 0
+ r = np.ones(h.shape[0], dtype=np.uint8)
+ r[ind] = 0
+
+ xi_xh = xi ^ xh
+ zi_zh = zi ^ zh
+ if determined:
+ r = reduce(np.logical_xor, r)
+ xi_xh = reduce(np.logical_xor, xi_xh)
+ zi_zh = reduce(np.logical_xor, zi_zh)
+ symplectic_matrix[h[0], -1] = r
+ symplectic_matrix[h[0], :nqubits] = xi_xh
+ symplectic_matrix[h[0], nqubits:-1] = zi_zh
+ else:
+ symplectic_matrix[h, -1] = r
+ symplectic_matrix[h, :nqubits] = xi_xh
+ symplectic_matrix[h, nqubits:-1] = zi_zh
+ return symplectic_matrix
+
+
+def _determined_outcome(state, q, nqubits):
+ state[-1, :] = 0
+ idx = (state[:nqubits, q].nonzero()[0] + nqubits).astype(np.uint)
+ state = _pack_for_measurements(state, nqubits)
+ state = _rowsum(
+ state,
+ 2 * nqubits * np.ones(idx.shape, dtype=np.uint),
+ idx,
+ _packed_size(nqubits),
+ True,
+ )
+ state = _unpack_for_measurements(state, nqubits)
+ return state, state[-1, -1]
+
+
+def _random_outcome(state, p, q, nqubits):
+ p = p[0] + nqubits
+ tmp = state[p, q].copy()
+ state[p, q] = 0
+ h = state[:-1, q].nonzero()[0]
+ state[p, q] = tmp
+ if h.shape[0] > 0:
+ state = _pack_for_measurements(state, nqubits)
+ state = _rowsum(
+ state,
+ h.astype(np.uint),
+ p * np.ones(h.shape[0], dtype=np.uint),
+ _packed_size(nqubits),
+ False,
+ )
+ state = _unpack_for_measurements(state, nqubits)
+ state[p - nqubits, :] = state[p, :]
+ outcome = np.random.randint(2, size=1).item()
+ state[p, :] = 0
+ state[p, -1] = outcome
+ state[p, nqubits + q] = 1
+ return state, outcome
+
+
+@cache
+def _dim(nqubits):
+ """Returns the dimension of the symplectic matrix for a given number of qubits."""
+ return 2 * nqubits + 1
+
+
+@cache
+def _packed_size(n):
+ """Returns the size of an array of `n` booleans after packing."""
+ return np.ceil(n / 8).astype(int)
+
+
+def _packbits(array, axis):
+ return np.packbits(array, axis=axis)
+
+
+def _unpackbits(array, axis):
+ return np.unpackbits(array, axis=axis)
+
+
+def _pack_for_measurements(state, nqubits):
+ """Prepares the state for measurements by packing the rows of the X and Z sections of the symplectic matrix."""
+ r, x, z = _get_rxz(state, nqubits)
+ x = _packbits(x, axis=1)
+ z = _packbits(z, axis=1)
+ return np.hstack((x, z, r[:, None]))
+
+
+@cache
+def _pad_size(n):
+ """Returns the size of the pad added to an array of original dimension `n` after unpacking."""
+ return 8 - (n % 8)
+
+
+def _unpack_for_measurements(state, nqubits):
+ """Unpacks the symplectc matrix that was packed for measurements."""
+ xz = _unpackbits(state[:, :-1], axis=1)
+ padding_size = _pad_size(nqubits)
+ x, z = xz[:, :nqubits], xz[:, nqubits + padding_size : -padding_size]
+ return np.hstack((x, z, state[:, -1][:, None]))
+
+
+def _init_state_for_measurements(state, nqubits, collapse):
+ if collapse:
+ return _unpackbits(state, axis=0)[: _dim(nqubits)]
+ else:
+ return state.copy()
+
+
+# valid for a standard basis measurement only
+def M(state, qubits, nqubits, collapse=False):
+ sample = []
+ state = _init_state_for_measurements(state, nqubits, collapse)
+ for q in qubits:
+ p = state[nqubits:-1, q].nonzero()[0]
+ # random outcome, affects the state
+ if len(p) > 0:
+ state, outcome = _random_outcome(state, p, q, nqubits)
+ # determined outcome, state unchanged
+ else:
+ state, outcome = _determined_outcome(state, q, nqubits)
+ sample.append(outcome)
+ if collapse:
+ state = _packbits(state, axis=0)
+ return sample
+
+
+def cast(x, dtype=None, copy=False):
+ if dtype is None:
+ dtype = "complex128"
+ if isinstance(x, np.ndarray):
+ return x.astype(dtype, copy=copy)
+ elif sparse.issparse(x): # pragma: no cover
+ return x.astype(dtype, copy=copy)
+ return np.array(x, dtype=dtype, copy=copy)
+
+
+def _clifford_pre_execution_reshape(state):
+ """Reshape and packing applied to the symplectic matrix before execution to prepare the state in the form needed by each engine.
+
+ Args:
+ state (np.array): Input state.
+
+ Returns:
+ (np.array) The packed and reshaped state.
+ """
+ return _packbits(state, axis=0)
+
+
+def _clifford_post_execution_reshape(state, nqubits: int):
+ """Reshape and unpacking applied to the state after execution to retrieve the standard symplectic matrix form.
+
+ Args:
+ state (np.array): Input state.
+ nqubits (int): Number of qubits.
+
+ Returns:
+ (np.array) The unpacked and reshaped state.
+ """
+ state = _unpackbits(state, axis=0)[: _dim(nqubits)]
+ return state
+
+
+def identity_density_matrix(nqubits, normalize: bool = True):
+ state = np.eye(2**nqubits, dtype="complex128")
+ if normalize is True: # pragma: no cover
+ state /= 2**nqubits
+ return state
diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py
new file mode 100644
index 000000000..5a14c2402
--- /dev/null
+++ b/src/qibo/backends/abstract.py
@@ -0,0 +1,381 @@
+import abc
+
+from qibo.config import raise_error
+
+
+class Backend(abc.ABC):
+ def __init__(self):
+ super().__init__()
+ self.name = "backend"
+ self.platform = None
+
+ self.precision = "double"
+ self.dtype = "complex128"
+ self.matrices = None
+
+ self.device = "/CPU:0"
+ self.nthreads = 1
+ self.supports_multigpu = False
+ self.oom_error = MemoryError
+
+ def __reduce__(self):
+ """Allow pickling backend objects that have references to modules."""
+ return self.__class__, tuple()
+
+ def __repr__(self):
+ if self.platform is None:
+ return self.name
+ else:
+ return f"{self.name} ({self.platform})"
+
+ @abc.abstractmethod
+ def set_precision(self, precision): # pragma: no cover
+ """Set complex number precision.
+
+ Args:
+ precision (str): 'single' or 'double'.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def set_device(self, device): # pragma: no cover
+ """Set simulation device.
+
+ Args:
+ device (str): Device such as '/CPU:0', '/GPU:0', etc.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def set_threads(self, nthreads): # pragma: no cover
+ """Set number of threads for CPU simulation.
+
+ Args:
+ nthreads (int): Number of threads.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def cast(self, x, copy=False): # pragma: no cover
+ """Cast an object as the array type of the current backend.
+
+ Args:
+ x: Object to cast to array.
+ copy (bool): If ``True`` a copy of the object is created in memory.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def is_sparse(self, x): # pragma: no cover
+ """Determine if a given array is a sparse tensor."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def to_numpy(self, x): # pragma: no cover
+ """Cast a given array to numpy."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def compile(self, func): # pragma: no cover
+ """Compile the given method.
+
+ Available only for the tensorflow backend.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def zero_state(self, nqubits): # pragma: no cover
+ """Generate :math:`|000 \\cdots 0 \\rangle` state vector as an array."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def zero_density_matrix(self, nqubits): # pragma: no cover
+ """Generate :math:`|000\\cdots0\\rangle\\langle000\\cdots0|` density matrix as an array."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def identity_density_matrix(
+ self, nqubits, normalize: bool = True
+ ): # pragma: no cover
+ """Generate density matrix
+
+ .. math::
+ \\rho = \\frac{1}{2^\\text{nqubits}} \\, \\sum_{k=0}^{2^\\text{nqubits} - 1} \\,
+ |k \\rangle \\langle k|
+
+ if ``normalize=True``. If ``normalize=False``, returns the unnormalized
+ Identity matrix, which is equivalent to :func:`numpy.eye`.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def plus_state(self, nqubits): # pragma: no cover
+ """Generate :math:`|+++\\cdots+\\rangle` state vector as an array."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def plus_density_matrix(self, nqubits): # pragma: no cover
+ """Generate :math:`|+++\\cdots+\\rangle\\langle+++\\cdots+|` density matrix as an array."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def matrix(self, gate): # pragma: no cover
+ """Convert a :class:`qibo.gates.Gate` to the corresponding matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def matrix_parametrized(self, gate): # pragma: no cover
+ """Equivalent to :meth:`qibo.backends.abstract.Backend.matrix` for parametrized gates."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def matrix_fused(self, gate): # pragma: no cover
+ """Fuse matrices of multiple gates."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def apply_gate(self, gate, state, nqubits): # pragma: no cover
+ """Apply a gate to state vector."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def apply_gate_density_matrix(self, gate, state, nqubits): # pragma: no cover
+ """Apply a gate to density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def apply_gate_half_density_matrix(self, gate, state, nqubits): # pragma: no cover
+ """Apply a gate to one side of the density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def apply_channel(self, channel, state, nqubits): # pragma: no cover
+ """Apply a channel to state vector."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def apply_channel_density_matrix(self, channel, state, nqubits): # pragma: no cover
+ """Apply a channel to density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def collapse_state(
+ self, state, qubits, shot, nqubits, normalize=True
+ ): # pragma: no cover
+ """Collapse state vector according to measurement shot."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def collapse_density_matrix(
+ self, state, qubits, shot, nqubits, normalize=True
+ ): # pragma: no cover
+ """Collapse density matrix according to measurement shot."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def reset_error_density_matrix(self, gate, state, nqubits): # pragma: no cover
+ """Apply reset error to density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def thermal_error_density_matrix(self, gate, state, nqubits): # pragma: no cover
+ """Apply thermal relaxation error to density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def execute_circuit(
+ self, circuit, initial_state=None, nshots=None
+ ): # pragma: no cover
+ """Execute a :class:`qibo.models.circuit.Circuit`."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def execute_circuits(
+ self, circuits, initial_states=None, nshots=None
+ ): # pragma: no cover
+ """Execute multiple :class:`qibo.models.circuit.Circuit`s in parallel."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def execute_circuit_repeated(
+ self, circuit, nshots, initial_state=None
+ ): # pragma: no cover
+ """Execute a :class:`qibo.models.circuit.Circuit` multiple times.
+
+ Useful for noise simulation using state vectors or for simulating gates
+ controlled by measurement outcomes.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def execute_distributed_circuit(
+ self, circuit, initial_state=None, nshots=None
+ ): # pragma: no cover
+ """Execute a :class:`qibo.models.circuit.Circuit` using multiple GPUs."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_symbolic(
+ self, state, nqubits, decimals=5, cutoff=1e-10, max_terms=20
+ ): # pragma: no cover
+ """Dirac representation of a state vector."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_symbolic_density_matrix(
+ self, state, nqubits, decimals=5, cutoff=1e-10, max_terms=20
+ ): # pragma: no cover
+ """Dirac representation of a density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_probabilities(self, state, qubits, nqubits): # pragma: no cover
+ """Calculate probabilities given a state vector."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_probabilities_density_matrix(
+ self, state, qubits, nqubits
+ ): # pragma: no cover
+ """Calculate probabilities given a density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def set_seed(self, seed): # pragma: no cover
+ """Set the seed of the random number generator."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def sample_shots(self, probabilities, nshots): # pragma: no cover
+ """Sample measurement shots according to a probability distribution."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def aggregate_shots(self, shots): # pragma: no cover
+ """Collect shots to a single array."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def samples_to_binary(self, samples, nqubits): # pragma: no cover
+ """Convert samples from decimal representation to binary."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def samples_to_decimal(self, samples, nqubits): # pragma: no cover
+ """Convert samples from binary representation to decimal."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_frequencies(self, samples): # pragma: no cover
+ """Calculate measurement frequencies from shots."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def update_frequencies(
+ self, frequencies, probabilities, nsamples
+ ): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def sample_frequencies(self, probabilities, nshots): # pragma: no cover
+ """Sample measurement frequencies according to a probability distribution."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_norm(self, state, order=2): # pragma: no cover
+ """Calculate norm of a state vector.
+
+ For specifications on possible values of the parameter ``order``
+ for the ``tensorflow`` backend, please refer to
+ `tensorflow.norm `_.
+ For all other backends, please refer to
+ `numpy.linalg.norm `_.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_norm_density_matrix(self, state, order="nuc"): # pragma: no cover
+ """Calculate norm of a density matrix. Default is the ``nuclear`` norm.
+
+ If ``order="nuc"``, it returns the nuclear norm of ``state``,
+ assuming ``state`` is Hermitian (also known as trace norm).
+ For specifications on the other possible values of the
+ parameter ``order`` for the ``tensorflow`` backend, please refer to
+ `tensorflow.norm `_.
+ For all other backends, please refer to
+ `numpy.linalg.norm `_.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_overlap(self, state1, state2): # pragma: no cover
+ """Calculate overlap of two state vectors."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_overlap_density_matrix(self, state1, state2): # pragma: no cover
+ """Calculate overlap of two density matrices."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_eigenvalues(self, matrix, k=6): # pragma: no cover
+ """Calculate eigenvalues of a matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_eigenvectors(self, matrix, k=6): # pragma: no cover
+ """Calculate eigenvectors of a matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_expectation_state(
+ self, hamiltonian, state, normalize
+ ): # pragma: no cover
+ """Calculate expectation value of a state vector given the observable matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_expectation_density_matrix(
+ self, hamiltonian, state, normalize
+ ): # pragma: no cover
+ """Calculate expectation value of a density matrix given the observable matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_matrix_exp(
+ self, a, matrix, eigenvectors=None, eigenvalues=None
+ ): # pragma: no cover
+ """Calculate matrix exponential of a matrix.
+ If the eigenvectors and eigenvalues are given the matrix diagonalization is
+ used for exponentiation.
+ """
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_hamiltonian_matrix_product(
+ self, matrix1, matrix2
+ ): # pragma: no cover
+ """Multiply two matrices."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def calculate_hamiltonian_state_product(self, matrix, state): # pragma: no cover
+ """Multiply a matrix to a state vector or density matrix."""
+ raise_error(NotImplementedError)
+
+ @abc.abstractmethod
+ def assert_allclose(self, value, target, rtol=1e-7, atol=0.0): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def assert_circuitclose(self, circuit, target_circuit, rtol=1e-7, atol=0.0):
+ value = self.execute_circuit(circuit)._state
+ target = self.execute_circuit(target_circuit)._state
+ self.assert_allclose(value, target, rtol=rtol, atol=atol)
+
+ @abc.abstractmethod
+ def _test_regressions(self, name): # pragma: no cover
+ """Correct outcomes for tests that involve random numbers.
+
+ The outcomes of such tests depend on the backend.
+ """
+ raise_error(NotImplementedError)
diff --git a/src/qibo/backends/clifford.py b/src/qibo/backends/clifford.py
new file mode 100644
index 000000000..5fd2d1cd6
--- /dev/null
+++ b/src/qibo/backends/clifford.py
@@ -0,0 +1,340 @@
+"""Module defining the Clifford backend."""
+
+import collections
+from functools import reduce
+from importlib.util import find_spec, module_from_spec
+from typing import Union
+
+import numpy as np
+
+from qibo import gates
+from qibo.backends.numpy import NumpyBackend
+from qibo.config import raise_error
+
+
+def _get_engine_name(backend):
+ return backend.platform if backend.platform is not None else backend.name
+
+
+class CliffordBackend(NumpyBackend):
+ """Backend for the simulation of Clifford circuits following
+ `Aaronson & Gottesman (2004) `_.
+
+ Args:
+ :class:`qibo.backends.abstract.Backend`: Backend used for the calculation.
+ """
+
+ def __init__(self, engine=None):
+ super().__init__()
+
+ if engine == "stim":
+ import stim # pylint: disable=C0415
+
+ engine = "numpy"
+ self.platform = "stim"
+ self._stim = stim
+ else:
+ if engine is None:
+ from qibo.backends import _check_backend # pylint: disable=C0415
+
+ engine = _get_engine_name(_check_backend(engine))
+
+ self.platform = engine
+
+ spec = find_spec("qibo.backends._clifford_operations")
+ self.engine = module_from_spec(spec)
+ spec.loader.exec_module(self.engine)
+
+ if engine == "numpy":
+ pass
+ elif engine == "numba":
+ from qibojit.backends import ( # pylint: disable=C0415
+ clifford_operations_cpu,
+ )
+
+ for method in dir(clifford_operations_cpu):
+ setattr(self.engine, method, getattr(clifford_operations_cpu, method))
+ elif engine == "cupy": # pragma: no cover
+ from qibojit.backends import ( # pylint: disable=C0415
+ clifford_operations_gpu,
+ )
+
+ for method in dir(clifford_operations_gpu):
+ setattr(self.engine, method, getattr(clifford_operations_gpu, method))
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Backend `{engine}` is not supported for Clifford Simulation.",
+ )
+
+ self.np = self.engine.np
+
+ self.name = "clifford"
+
+ def cast(self, x, dtype=None, copy: bool = False):
+ """Cast an object as the array type of the current backend.
+
+ Args:
+ x: Object to cast to array.
+ copy (bool, optional): If ``True`` a copy of the object is created in memory.
+ Defaults to ``False``.
+ """
+ return self.engine.cast(x, dtype=dtype, copy=copy)
+
+ def calculate_frequencies(self, samples):
+ res, counts = self.engine.np.unique(samples, return_counts=True)
+ # The next two lines are necessary for the GPU backends
+ res = [int(r) if not isinstance(r, str) else r for r in res]
+ counts = [int(v) for v in counts]
+
+ return collections.Counter(dict(zip(res, counts)))
+
+ def zero_state(self, nqubits: int):
+ """Construct the zero state |00...00>.
+
+ Args:
+ nqubits (int): Number of qubits.
+
+ Returns:
+ (ndarray): Symplectic matrix for the zero state.
+ """
+ identity = self.np.eye(nqubits)
+ symplectic_matrix = self.np.zeros(
+ (2 * nqubits + 1, 2 * nqubits + 1), dtype=bool
+ )
+ symplectic_matrix[:nqubits, :nqubits] = self.np.copy(identity)
+ symplectic_matrix[nqubits:-1, nqubits : 2 * nqubits] = self.np.copy(identity)
+ return symplectic_matrix
+
+ def _clifford_pre_execution_reshape(self, state):
+ """Reshape the symplectic matrix to the shape needed by the engine before circuit execution.
+
+ Args:
+ state (ndarray): Input state.
+
+ Returns:
+ ndarray: Reshaped state.
+ """
+ return self.engine._clifford_pre_execution_reshape( # pylint: disable=protected-access
+ state
+ )
+
+ def _clifford_post_execution_reshape(self, state, nqubits: int):
+ """Reshape the symplectic matrix to the shape needed by the engine after circuit execution.
+
+ Args:
+ state (ndarray): Input state.
+ nqubits (int): Number of qubits.
+
+ Returns:
+ ndarray: Reshaped state.
+ """
+ return self.engine._clifford_post_execution_reshape( # pylint: disable=protected-access
+ state, nqubits
+ )
+
+ def apply_gate_clifford(self, gate, symplectic_matrix, nqubits):
+ """Apply a gate to a symplectic matrix."""
+ operation = getattr(self.engine, gate.__class__.__name__)
+ kwargs = (
+ {"theta": gate.init_kwargs["theta"]} if "theta" in gate.init_kwargs else {}
+ )
+
+ return operation(symplectic_matrix, *gate.init_args, nqubits, **kwargs)
+
+ def apply_channel(self, channel, state, nqubits):
+ probabilities = channel.coefficients + (1 - np.sum(channel.coefficients),)
+ index = self.np.random.choice(
+ range(len(probabilities)), size=1, p=probabilities
+ )[0]
+ index = int(index)
+ if index != len(channel.gates):
+ gate = channel.gates[index]
+ state = gate.apply_clifford(self, state, nqubits)
+ return state
+
+ def execute_circuit( # pylint: disable=R1710
+ self, circuit, initial_state=None, nshots: int = 1000
+ ):
+ """Execute a Clifford circuits.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Input circuit.
+ initial_state (ndarray, optional): The ``symplectic_matrix`` of the initial state.
+ If ``None``, defaults to the zero state. Defaults to ``None``.
+ nshots (int, optional): Number of shots to perform if ``circuit`` has measurements.
+ Defaults to :math:`10^{3}`.
+
+ Returns:
+ :class:`qibo.quantum_info.clifford.Clifford`: Object storing to the final results.
+ """
+ from qibo.quantum_info.clifford import Clifford # pylint: disable=C0415
+
+ if self.platform == "stim":
+ circuit_stim = self._stim.Circuit() # pylint: disable=E1101
+ for gate in circuit.queue:
+ circuit_stim.append(gate.__class__.__name__, list(gate.qubits))
+
+ x_destab, z_destab, x_stab, z_stab, x_phases, z_phases = (
+ self._stim.Tableau.from_circuit( # pylint: disable=no-member
+ circuit_stim
+ ).to_numpy()
+ )
+ symplectic_matrix = np.block([[x_destab, z_destab], [x_stab, z_stab]])
+ symplectic_matrix = np.c_[symplectic_matrix, np.r_[x_phases, z_phases]]
+
+ return Clifford(
+ symplectic_matrix,
+ measurements=circuit.measurements,
+ nshots=nshots,
+ _backend=self,
+ )
+
+ for gate in circuit.queue:
+ if (
+ not gate.clifford
+ and not gate.__class__.__name__ == "M"
+ and not isinstance(gate, gates.PauliNoiseChannel)
+ ):
+ raise_error(RuntimeError, "Circuit contains non-Clifford gates.")
+
+ if circuit.repeated_execution and nshots != 1:
+ return self.execute_circuit_repeated(circuit, nshots, initial_state)
+
+ try:
+ nqubits = circuit.nqubits
+
+ state = self.zero_state(nqubits) if initial_state is None else initial_state
+
+ state = self._clifford_pre_execution_reshape(state)
+
+ for gate in circuit.queue:
+ gate.apply_clifford(self, state, nqubits)
+
+ state = self._clifford_post_execution_reshape(state, nqubits)
+
+ clifford = Clifford(
+ state,
+ measurements=circuit.measurements,
+ nshots=nshots,
+ _backend=self,
+ )
+
+ return clifford
+
+ except self.oom_error: # pragma: no cover
+ raise_error(
+ RuntimeError,
+ f"State does not fit in {self.device} memory."
+ "Please switch the execution device to a "
+ "different one using ``qibo.set_device``.",
+ )
+
+ def execute_circuit_repeated(self, circuit, nshots: int = 1000, initial_state=None):
+ """Execute a Clifford circuits ``nshots`` times.
+
+ This is used for all the simulations that involve repeated execution.
+ For instance when collapsing measurement or noise channels are present.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): input circuit.
+ initial_state (ndarray, optional): Symplectic_matrix of the initial state.
+ If ``None``, defaults to :meth:`qibo.backends.clifford.CliffordBackend.zero_state`.
+ Defaults to ``None``.
+ nshots (int, optional): Number of times to repeat the execution.
+ Defaults to :math:`1000`.
+
+ Returns:
+ :class:`qibo.quantum_info.clifford.Clifford`: Object storing to the final results.
+ """
+ from qibo.quantum_info.clifford import Clifford # pylint: disable=C0415
+
+ circuit_copy = circuit.copy()
+ samples = []
+ for _ in range(nshots):
+ res = self.execute_circuit(circuit_copy, initial_state, nshots=1)
+ for measurement in circuit_copy.measurements:
+ measurement.result.reset()
+ samples.append(res.samples())
+ samples = self.np.vstack(samples)
+
+ for meas in circuit.measurements:
+ meas.result.register_samples(samples[:, meas.target_qubits], self)
+
+ result = Clifford(
+ self.zero_state(circuit.nqubits),
+ measurements=circuit.measurements,
+ nshots=nshots,
+ _backend=self,
+ )
+ result.symplectic_matrix, result._samples = None, None
+
+ return result
+
+ def sample_shots(
+ self,
+ state,
+ qubits: Union[tuple, list],
+ nqubits: int,
+ nshots: int,
+ collapse: bool = False,
+ ): # pylint: disable=W0221
+ """Sample shots by measuring selected ``qubits`` in symplectic matrix of a ``state``.
+
+ Args:
+ state (ndarray): symplectic matrix from which to sample shots from.
+ qubits: (tuple or list): qubits to measure.
+ nqubits (int): total number of qubits of the state.
+ nshots (int): number of shots to sample.
+ collapse (bool, optional): If ``True`` the input state is going to be
+ collapsed with the last shot. Defaults to ``False``.
+
+ Returns:
+ (ndarray): Samples shots.
+ """
+ if isinstance(qubits, list):
+ qubits = tuple(qubits)
+
+ if collapse:
+ samples = [self.engine.M(state, qubits, nqubits) for _ in range(nshots - 1)]
+ samples.append(self.engine.M(state, qubits, nqubits, collapse))
+ else:
+ samples = [self.engine.M(state, qubits, nqubits) for _ in range(nshots)]
+
+ return self.engine.cast(samples, dtype=int)
+
+ def symplectic_matrix_to_generators(
+ self, symplectic_matrix, return_array: bool = False
+ ):
+ """Extract the stabilizers and destabilizers generators from symplectic matrix.
+
+ Args:
+ symplectic_matrix (ndarray): The input symplectic_matrix.
+ return_array (bool, optional): If ``True`` returns the generators as ``ndarrays``.
+ If ``False``, generators are returned as strings. Defaults to ``False``.
+
+ Returns:
+ (list, list): Extracted generators and their corresponding phases, respectively.
+ """
+ bits_to_gate = {"00": "I", "01": "X", "10": "Z", "11": "Y"}
+
+ nqubits = int((symplectic_matrix.shape[1] - 1) / 2)
+ phases = (-1) ** symplectic_matrix[:-1, -1]
+ tmp = 1 * symplectic_matrix[:-1, :-1]
+ X, Z = tmp[:, :nqubits], tmp[:, nqubits:]
+ generators = []
+ for x, z in zip(X, Z):
+ paulis = [bits_to_gate[f"{zz}{xx}"] for xx, zz in zip(x, z)]
+ if return_array:
+ from qibo import matrices # pylint: disable=C0415
+
+ paulis = [self.cast(getattr(matrices, p)) for p in paulis]
+ matrix = reduce(self.np.kron, paulis)
+ generators.append(matrix)
+ else:
+ generators.append("".join(paulis))
+
+ if return_array:
+ generators = self.cast(generators)
+
+ return generators, phases
diff --git a/src/qibo/backends/einsum_utils.py b/src/qibo/backends/einsum_utils.py
new file mode 100644
index 000000000..347eed876
--- /dev/null
+++ b/src/qibo/backends/einsum_utils.py
@@ -0,0 +1,88 @@
+"""
+Gates use ``einsum`` to apply gates to state vectors. The einsum string that
+specifies the contraction indices is created using the following methods and
+used by :meth:`qibo.backends.numpy.NumpyEngine.apply_gate`.
+"""
+
+from qibo.config import EINSUM_CHARS, raise_error
+
+
+def prepare_strings(qubits, nqubits):
+ if nqubits + len(qubits) > len(EINSUM_CHARS): # pragma: no cover
+ raise_error(NotImplementedError, "Not enough einsum characters.")
+
+ inp = list(EINSUM_CHARS[:nqubits])
+ out = inp[:]
+ trans = list(EINSUM_CHARS[nqubits : nqubits + len(qubits)])
+ for i, q in enumerate(qubits):
+ trans.append(inp[q])
+ out[q] = trans[i]
+
+ inp = "".join(inp)
+ out = "".join(out)
+ trans = "".join(trans)
+ rest = EINSUM_CHARS[nqubits + len(qubits) :]
+ return inp, out, trans, rest
+
+
+def apply_gate_string(qubits, nqubits):
+ inp, out, trans, _ = prepare_strings(qubits, nqubits)
+ return f"{inp},{trans}->{out}"
+
+
+def apply_gate_density_matrix_string(qubits, nqubits):
+ inp, out, trans, rest = prepare_strings(qubits, nqubits)
+ if nqubits > len(rest): # pragma: no cover
+ raise_error(NotImplementedError, "Not enough einsum characters.")
+
+ trest = rest[:nqubits]
+ left = f"{inp}{trest},{trans}->{out}{trest}"
+ right = f"{trest}{inp},{trans}->{trest}{out}"
+ return left, right
+
+
+def apply_gate_density_matrix_controlled_string(qubits, nqubits):
+ inp, out, trans, rest = prepare_strings(qubits, nqubits)
+ if nqubits > len(rest): # pragma: no cover
+ raise_error(NotImplementedError, "Not enough einsum characters.")
+
+ trest, c = rest[:nqubits], rest[nqubits]
+ left = f"{c}{inp}{trest},{trans}->{c}{out}{trest}"
+ right = f"{c}{trest}{inp},{trans}->{c}{trest}{out}"
+ return left, right
+
+
+def control_order(gate, nqubits):
+ loop_start = 0
+ order = list(gate.control_qubits)
+ targets = list(gate.target_qubits)
+ for control in gate.control_qubits:
+ for i in range(loop_start, control):
+ order.append(i)
+ loop_start = control + 1
+ for i, t in enumerate(gate.target_qubits):
+ if t > control:
+ targets[i] -= 1
+ for i in range(loop_start, nqubits):
+ order.append(i)
+ return order, targets
+
+
+def control_order_density_matrix(gate, nqubits):
+ ncontrol = len(gate.control_qubits)
+ order, targets = control_order(gate, nqubits)
+ additional_order = [x + len(order) for x in order]
+ order_dm = (
+ order[:ncontrol]
+ + list(additional_order[:ncontrol])
+ + order[ncontrol:]
+ + list(additional_order[ncontrol:])
+ )
+ return order_dm, targets
+
+
+def reverse_order(order):
+ rorder = len(order) * [0]
+ for i, r in enumerate(order):
+ rorder[r] = i
+ return rorder
diff --git a/src/qibo/backends/npmatrices.py b/src/qibo/backends/npmatrices.py
new file mode 100644
index 000000000..58e78f4ca
--- /dev/null
+++ b/src/qibo/backends/npmatrices.py
@@ -0,0 +1,556 @@
+import cmath
+import math
+from functools import cached_property
+
+from qibo.config import raise_error
+
+
+class NumpyMatrices:
+ """Matrix representation of every gate as a numpy array."""
+
+ def __init__(self, dtype):
+ import numpy as np
+
+ self.dtype = dtype
+ self.np = np
+
+ def _cast(self, x, dtype):
+ return self.np.array(x, dtype=dtype)
+
+ # This method is used to cast the parameters of the gates to the right type for other backends
+ def _cast_parameter(self, x):
+ return x
+
+ @cached_property
+ def H(self):
+ return self._cast([[1, 1], [1, -1]], dtype=self.dtype) / math.sqrt(2)
+
+ @cached_property
+ def X(self):
+ return self._cast([[0, 1], [1, 0]], dtype=self.dtype)
+
+ @cached_property
+ def Y(self):
+ return self._cast([[0j, -1j], [1j, 0j]], dtype=self.dtype)
+
+ @cached_property
+ def Z(self):
+ return self._cast([[1, 0], [0, -1]], dtype=self.dtype)
+
+ @cached_property
+ def SX(self):
+ return self._cast([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=self.dtype) / 2
+
+ @cached_property
+ def SXDG(self):
+ return self._cast([[1 - 1j, 1 + 1j], [1 + 1j, 1 - 1j]], dtype=self.dtype) / 2
+
+ @cached_property
+ def S(self):
+ return self._cast([[1 + 0j, 0j], [0j, 1j]], dtype=self.dtype)
+
+ @cached_property
+ def SDG(self):
+ return self._cast([[1 + 0j, 0j], [0j, -1j]], dtype=self.dtype)
+
+ @cached_property
+ def T(self):
+ return self._cast(
+ [[1 + 0j, 0], [0, cmath.exp(1j * math.pi / 4.0)]], dtype=self.dtype
+ )
+
+ @cached_property
+ def TDG(self):
+ return self._cast(
+ [[1 + 0j, 0], [0, cmath.exp(-1j * math.pi / 4.0)]], dtype=self.dtype
+ )
+
+ def I(self, n=2):
+ return self._cast(self.np.eye(n), dtype=self.dtype)
+
+ def Align(self, delay, n=2):
+ return self._cast(self.I(n), dtype=self.dtype)
+
+ def M(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def RX(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ isin = -1j * self.np.sin(theta / 2.0)
+ return self._cast([[cos, isin], [isin, cos]], dtype=self.dtype)
+
+ def RY(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ sin = self.np.sin(theta / 2.0) + 0j
+ return self._cast([[cos, -sin], [sin, cos]], dtype=self.dtype)
+
+ def RZ(self, theta):
+ theta = self._cast_parameter(theta)
+ phase = self.np.exp(0.5j * theta)
+ return self._cast([[self.np.conj(phase), 0], [0, phase]], dtype=self.dtype)
+
+ def PRX(self, theta, phi):
+ theta = self._cast_parameter(theta)
+ phi = self._cast_parameter(phi)
+ cos = self.np.cos(theta / 2)
+ sin = self.np.sin(theta / 2)
+ exponent1 = -1.0j * self.np.exp(-1.0j * phi)
+ exponent2 = -1.0j * self.np.exp(1.0j * phi)
+ # The +0j is needed because of tensorflow casting issues
+ return self._cast(
+ [[cos + 0j, exponent1 * sin], [exponent2 * sin, cos + 0j]], dtype=self.dtype
+ )
+
+ def GPI(self, phi):
+ phi = self._cast_parameter(phi)
+ phase = self.np.exp(1.0j * phi)
+ return self._cast([[0, self.np.conj(phase)], [phase, 0]], dtype=self.dtype)
+
+ def GPI2(self, phi):
+ phi = self._cast_parameter(phi)
+ phase = self.np.exp(1.0j * phi)
+ return self._cast(
+ [[1, -1.0j * self.np.conj(phase)], [-1.0j * phase, 1]], dtype=self.dtype
+ ) / math.sqrt(2)
+
+ def U1(self, theta):
+ theta = self._cast_parameter(theta)
+ phase = self.np.exp(1j * theta)
+ return self._cast([[1, 0], [0, phase]], dtype=self.dtype)
+
+ def U2(self, phi, lam):
+ phi = self._cast_parameter(phi)
+ lam = self._cast_parameter(lam)
+ eplus = self.np.exp(1j * (phi + lam) / 2.0)
+ eminus = self.np.exp(1j * (phi - lam) / 2.0)
+ return self._cast(
+ [[self.np.conj(eplus), -self.np.conj(eminus)], [eminus, eplus]],
+ dtype=self.dtype,
+ ) / math.sqrt(2)
+
+ def U3(self, theta, phi, lam):
+ theta = self._cast_parameter(theta)
+ phi = self._cast_parameter(phi)
+ lam = self._cast_parameter(lam)
+ cost = self.np.cos(theta / 2)
+ sint = self.np.sin(theta / 2)
+ eplus = self.np.exp(1j * (phi + lam) / 2.0)
+ eminus = self.np.exp(1j * (phi - lam) / 2.0)
+ return self._cast(
+ [
+ [self.np.conj(eplus) * cost, -self.np.conj(eminus) * sint],
+ [eminus * sint, eplus * cost],
+ ],
+ dtype=self.dtype,
+ )
+
+ def U1q(self, theta, phi):
+ theta = self._cast_parameter(theta)
+ phi = self._cast_parameter(phi)
+ return self._cast(
+ self.U3(theta, phi - math.pi / 2, math.pi / 2 - phi), dtype=self.dtype
+ )
+
+ @cached_property
+ def CNOT(self):
+ return self._cast(
+ [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dtype=self.dtype
+ )
+
+ @cached_property
+ def CY(self):
+ return self._cast(
+ [
+ [1 + 0j, 0j, 0j, 0j],
+ [0j, 1 + 0j, 0j, 0j],
+ [0j, 0j, 0j, -1j],
+ [0j, 0j, 1j, 0j],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def CZ(self):
+ return self._cast(
+ [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], dtype=self.dtype
+ )
+
+ @cached_property
+ def CSX(self):
+ a = self._cast_parameter((1 + 1j) / 2)
+ b = self.np.conj(a)
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, a, b],
+ [0, 0, b, a],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def CSXDG(self):
+ a = self._cast_parameter((1 - 1j) / 2)
+ b = self.np.conj(a)
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, a, b],
+ [0, 0, b, a],
+ ],
+ dtype=self.dtype,
+ )
+
+ def CRX(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ isin = -1j * self.np.sin(theta / 2.0)
+ matrix = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, cos, isin],
+ [0, 0, isin, cos],
+ ]
+ return self._cast(matrix, dtype=self.dtype)
+
+ def CRY(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ sin = self.np.sin(theta / 2.0) + 0j
+ matrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, cos, -sin], [0, 0, sin, cos]]
+ return self._cast(matrix, dtype=self.dtype)
+
+ def CRZ(self, theta):
+ theta = self._cast_parameter(theta)
+ phase = self.np.exp(0.5j * theta)
+ matrix = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, self.np.conj(phase), 0],
+ [0, 0, 0, phase],
+ ]
+ return self._cast(matrix, dtype=self.dtype)
+
+ def CU1(self, theta):
+ theta = self._cast_parameter(theta)
+ phase = self.np.exp(1j * theta)
+ matrix = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, phase],
+ ]
+ return self._cast(matrix, dtype=self.dtype)
+
+ def CU2(self, phi, lam):
+ phi = self._cast_parameter(phi)
+ lam = self._cast_parameter(lam)
+ eplus = self.np.exp(1j * (phi + lam) / 2.0) / math.sqrt(2)
+ eminus = self.np.exp(1j * (phi - lam) / 2.0) / math.sqrt(2)
+ matrix = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, self.np.conj(eplus), -self.np.conj(eminus)],
+ [0, 0, eminus, eplus],
+ ]
+ return self._cast(matrix, dtype=self.dtype)
+
+ def CU3(self, theta, phi, lam):
+ theta = self._cast_parameter(theta)
+ phi = self._cast_parameter(phi)
+ lam = self._cast_parameter(lam)
+ cost = self.np.cos(theta / 2)
+ sint = self.np.sin(theta / 2)
+ eplus = self.np.exp(1j * (phi + lam) / 2.0)
+ eminus = self.np.exp(1j * (phi - lam) / 2.0)
+ matrix = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, self.np.conj(eplus) * cost, -self.np.conj(eminus) * sint],
+ [0, 0, eminus * sint, eplus * cost],
+ ]
+ return self._cast(matrix, dtype=self.dtype)
+
+ @cached_property
+ def SWAP(self):
+ return self._cast(
+ [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=self.dtype
+ )
+
+ @cached_property
+ def iSWAP(self):
+ return self._cast(
+ [
+ [1 + 0j, 0j, 0j, 0j],
+ [0j, 0j, 1j, 0j],
+ [0j, 1j, 0j, 0j],
+ [0j, 0j, 0j, 1 + 0j],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def SiSWAP(self):
+ return self._cast(
+ [
+ [1 + 0j, 0j, 0j, 0j],
+ [0j, 1 / math.sqrt(2) + 0j, 1j / math.sqrt(2), 0j],
+ [0j, 1j / math.sqrt(2), 1 / math.sqrt(2) + 0j, 0j],
+ [0j, 0j, 0j, 1 + 0j],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def SiSWAPDG(self):
+ return self._cast(
+ [
+ [1 + 0j, 0j, 0j, 0j],
+ [0j, 1 / math.sqrt(2) + 0j, -1j / math.sqrt(2), 0j],
+ [0j, -1j / math.sqrt(2), 1 / math.sqrt(2) + 0j, 0j],
+ [0j, 0j, 0j, 1 + 0j],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def FSWAP(self):
+ return self._cast(
+ [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, -1]], dtype=self.dtype
+ )
+
+ def fSim(self, theta, phi):
+ theta = self._cast_parameter(theta)
+ phi = self._cast_parameter(phi)
+ cost = self.np.cos(theta) + 0j
+ isint = -1j * self.np.sin(theta)
+ phase = self.np.exp(-1j * phi)
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, cost, isint, 0],
+ [0, isint, cost, 0],
+ [0, 0, 0, phase],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def SYC(self):
+ cost = math.cos(math.pi / 2) + 0j
+ isint = -1j * math.sin(math.pi / 2)
+ phase = cmath.exp(-1j * math.pi / 6)
+ return self._cast(
+ [
+ [1 + 0j, 0, 0, 0],
+ [0, cost, isint, 0],
+ [0, isint, cost, 0],
+ [0, 0, 0, phase],
+ ],
+ dtype=self.dtype,
+ )
+
+ def GeneralizedfSim(self, u, phi):
+ phi = self._cast_parameter(phi)
+ phase = self.np.exp(-1j * phi)
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, complex(u[0, 0]), complex(u[0, 1]), 0],
+ [0, complex(u[1, 0]), complex(u[1, 1]), 0],
+ [0, 0, 0, phase],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RXX(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ isin = -1j * self.np.sin(theta / 2.0)
+ return self._cast(
+ [
+ [cos, 0, 0, isin],
+ [0, cos, isin, 0],
+ [0, isin, cos, 0],
+ [isin, 0, 0, cos],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RYY(self, theta):
+ theta = self._cast_parameter(theta)
+ cos = self.np.cos(theta / 2.0) + 0j
+ isin = -1j * self.np.sin(theta / 2.0)
+ return self._cast(
+ [
+ [cos, 0, 0, -isin],
+ [0, cos, isin, 0],
+ [0, isin, cos, 0],
+ [-isin, 0, 0, cos],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RZZ(self, theta):
+ theta = self._cast_parameter(theta)
+ phase = self.np.exp(0.5j * theta)
+ return self._cast(
+ [
+ [self.np.conj(phase), 0, 0, 0],
+ [0, phase, 0, 0],
+ [0, 0, phase, 0],
+ [0, 0, 0, self.np.conj(phase)],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RZX(self, theta):
+ theta = self._cast_parameter(theta)
+ cos, sin = self.np.cos(theta / 2) + 0j, self.np.sin(theta / 2) + 0j
+ return self._cast(
+ [
+ [cos, -1j * sin, 0, 0],
+ [-1j * sin, cos, 0, 0],
+ [0, 0, cos, 1j * sin],
+ [0, 0, 1j * sin, cos],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RXXYY(self, theta):
+ theta = self._cast_parameter(theta)
+ cos, sin = self.np.cos(theta / 2) + 0j, self.np.sin(theta / 2) + 0j
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, cos, -1j * sin, 0],
+ [0, -1j * sin, cos, 0],
+ [0, 0, 0, 1],
+ ],
+ dtype=self.dtype,
+ )
+
+ def MS(self, phi0, phi1, theta):
+ phi0, phi1, theta = (
+ self._cast_parameter(phi0),
+ self._cast_parameter(phi1),
+ self._cast_parameter(theta),
+ )
+ plus = self.np.exp(1.0j * (phi0 + phi1))
+ minus = self.np.exp(1.0j * (phi0 - phi1))
+ cos = self.np.cos(theta / 2) + 0j
+ sin = self.np.sin(theta / 2) + 0j
+ return self._cast(
+ [
+ [cos, 0, 0, -1.0j * self.np.conj(plus) * sin],
+ [0, cos, -1.0j * self.np.conj(minus) * sin, 0],
+ [0, -1.0j * minus * sin, cos, 0],
+ [-1.0j * plus * sin, 0, 0, cos],
+ ],
+ dtype=self.dtype,
+ )
+
+ def GIVENS(self, theta):
+ theta = self._cast_parameter(theta)
+ return self._cast(
+ [
+ [1, 0, 0, 0],
+ [0, self.np.cos(theta), -self.np.sin(theta), 0],
+ [0, self.np.sin(theta), self.np.cos(theta), 0],
+ [0, 0, 0, 1],
+ ],
+ dtype=self.dtype,
+ )
+
+ def RBS(self, theta):
+ return self.GIVENS(-theta)
+
+ @cached_property
+ def ECR(self):
+ return self._cast(
+ [
+ [0j, 0j, 1 + 0j, 1j],
+ [0j, 0j, 1j, 1 + 0j],
+ [1 + 0j, -1j, 0j, 0j],
+ [-1j, 1 + 0j, 0j, 0j],
+ ],
+ dtype=self.dtype,
+ ) / math.sqrt(2)
+
+ @cached_property
+ def TOFFOLI(self):
+ return self._cast(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 1],
+ [0, 0, 0, 0, 0, 0, 1, 0],
+ ],
+ dtype=self.dtype,
+ )
+
+ @cached_property
+ def CCZ(self):
+ return self._cast(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, 0, -1],
+ ],
+ dtype=self.dtype,
+ )
+
+ def DEUTSCH(self, theta):
+ theta = self._cast_parameter(theta)
+ sin = self.np.sin(theta) + 0j # 0j necessary for right tensorflow dtype
+ cos = self.np.cos(theta) + 0j
+ return self._cast(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 1j * cos, sin],
+ [0, 0, 0, 0, 0, 0, sin, 1j * cos],
+ ],
+ dtype=self.dtype,
+ )
+
+ def Unitary(self, u):
+ return self.np.array(u, dtype=self.dtype, copy=False)
+
+ def CallbackGate(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def PartialTrace(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def UnitaryChannel(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def PauliNoiseChannel(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def ResetChannel(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def ThermalRelaxationChannel(self): # pragma: no cover
+ raise_error(NotImplementedError)
+
+ def FusedGate(self): # pragma: no cover
+ raise_error(NotImplementedError)
diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py
new file mode 100644
index 000000000..d69765058
--- /dev/null
+++ b/src/qibo/backends/numpy.py
@@ -0,0 +1,803 @@
+import collections
+import math
+
+import numpy as np
+from scipy import sparse
+from scipy.linalg import block_diag
+
+from qibo import __version__
+from qibo.backends import einsum_utils
+from qibo.backends.abstract import Backend
+from qibo.backends.npmatrices import NumpyMatrices
+from qibo.config import log, raise_error
+from qibo.result import CircuitResult, MeasurementOutcomes, QuantumState
+
+
+class NumpyBackend(Backend):
+ def __init__(self):
+ super().__init__()
+ self.np = np
+ self.name = "numpy"
+ self.matrices = NumpyMatrices(self.dtype)
+ self.tensor_types = np.ndarray
+ self.versions = {"qibo": __version__, "numpy": self.np.__version__}
+ self.numeric_types = (
+ int,
+ float,
+ complex,
+ np.int32,
+ np.int64,
+ np.float32,
+ np.float64,
+ np.complex64,
+ np.complex128,
+ )
+
+ def set_precision(self, precision):
+ if precision != self.precision:
+ if precision == "single":
+ self.precision = precision
+ self.dtype = "complex64"
+ elif precision == "double":
+ self.precision = precision
+ self.dtype = "complex128"
+ else:
+ raise_error(ValueError, f"Unknown precision {precision}.")
+ if self.matrices:
+ self.matrices = self.matrices.__class__(self.dtype)
+
+ def set_device(self, device):
+ if device != "/CPU:0":
+ raise_error(
+ ValueError, f"Device {device} is not available for {self} backend."
+ )
+
+ def set_threads(self, nthreads):
+ if nthreads > 1:
+ raise_error(ValueError, "numpy does not support more than one thread.")
+
+ def cast(self, x, dtype=None, copy=False):
+ if dtype is None:
+ dtype = self.dtype
+ if isinstance(x, self.tensor_types):
+ return x.astype(dtype, copy=copy)
+ elif self.is_sparse(x):
+ return x.astype(dtype, copy=copy)
+ return np.array(x, dtype=dtype, copy=copy)
+
+ def is_sparse(self, x):
+ from scipy import sparse
+
+ return sparse.issparse(x)
+
+ def to_numpy(self, x):
+ if self.is_sparse(x):
+ return x.toarray()
+ return x
+
+ def compile(self, func):
+ return func
+
+ def zero_state(self, nqubits):
+ state = self.np.zeros(2**nqubits, dtype=self.dtype)
+ state[0] = 1
+ return state
+
+ def zero_density_matrix(self, nqubits):
+ state = self.np.zeros(2 * (2**nqubits,), dtype=self.dtype)
+ state[0, 0] = 1
+ return state
+
+ def identity_density_matrix(self, nqubits, normalize: bool = True):
+ state = self.np.eye(2**nqubits, dtype=self.dtype)
+ if normalize is True:
+ state /= 2**nqubits
+ return state
+
+ def plus_state(self, nqubits):
+ state = self.np.ones(2**nqubits, dtype=self.dtype)
+ state /= math.sqrt(2**nqubits)
+ return state
+
+ def plus_density_matrix(self, nqubits):
+ state = self.np.ones(2 * (2**nqubits,), dtype=self.dtype)
+ state /= 2**nqubits
+ return state
+
+ def matrix(self, gate):
+ """Convert a gate to its matrix representation in the computational basis."""
+ name = gate.__class__.__name__
+ _matrix = getattr(self.matrices, name)
+ if callable(_matrix):
+ _matrix = _matrix(2 ** len(gate.target_qubits))
+ return self.cast(_matrix, dtype=_matrix.dtype)
+
+ def matrix_parametrized(self, gate):
+ """Convert a parametrized gate to its matrix representation in the computational basis."""
+ name = gate.__class__.__name__
+ _matrix = getattr(self.matrices, name)(*gate.parameters)
+ return self.cast(_matrix, dtype=_matrix.dtype)
+
+ def matrix_fused(self, fgate):
+ rank = len(fgate.target_qubits)
+ matrix = sparse.eye(2**rank)
+
+ for gate in fgate.gates:
+ # transfer gate matrix to numpy as it is more efficient for
+ # small tensor calculations
+ # explicit to_numpy see https://github.com/qiboteam/qibo/issues/928
+ gmatrix = self.to_numpy(gate.matrix(self))
+ # add controls if controls were instantiated using
+ # the ``Gate.controlled_by`` method
+ num_controls = len(gate.control_qubits)
+ if num_controls > 0:
+ gmatrix = block_diag(
+ np.eye(2 ** len(gate.qubits) - len(gmatrix)), gmatrix
+ )
+ # Kronecker product with identity is needed to make the
+ # original matrix have shape (2**rank x 2**rank)
+ eye = np.eye(2 ** (rank - len(gate.qubits)))
+ gmatrix = np.kron(gmatrix, eye)
+ # Transpose the new matrix indices so that it targets the
+ # target qubits of the original gate
+ original_shape = gmatrix.shape
+ gmatrix = np.reshape(gmatrix, 2 * rank * (2,))
+ qubits = list(gate.qubits)
+ indices = qubits + [q for q in fgate.target_qubits if q not in qubits]
+ indices = np.argsort(indices)
+ transpose_indices = list(indices)
+ transpose_indices.extend(indices + rank)
+ gmatrix = np.transpose(gmatrix, transpose_indices)
+ gmatrix = np.reshape(gmatrix, original_shape)
+ # fuse the individual gate matrix to the total ``FusedGate`` matrix
+ # we are using sparse matrices to improve perfomances
+ matrix = sparse.csr_matrix(gmatrix).dot(matrix)
+
+ return self.cast(matrix.toarray())
+
+ def apply_gate(self, gate, state, nqubits):
+ state = self.np.reshape(state, nqubits * (2,))
+ matrix = gate.matrix(self)
+ if gate.is_controlled_by:
+ matrix = self.np.reshape(matrix, 2 * len(gate.target_qubits) * (2,))
+ ncontrol = len(gate.control_qubits)
+ nactive = nqubits - ncontrol
+ order, targets = einsum_utils.control_order(gate, nqubits)
+ state = self.np.transpose(state, order)
+ # Apply `einsum` only to the part of the state where all controls
+ # are active. This should be `state[-1]`
+ state = self.np.reshape(state, (2**ncontrol,) + nactive * (2,))
+ opstring = einsum_utils.apply_gate_string(targets, nactive)
+ updates = self.np.einsum(opstring, state[-1], matrix)
+ # Concatenate the updated part of the state `updates` with the
+ # part of of the state that remained unaffected `state[:-1]`.
+ state = self.np.concatenate([state[:-1], updates[None]], axis=0)
+ state = self.np.reshape(state, nqubits * (2,))
+ # Put qubit indices back to their proper places
+ state = self.np.transpose(state, einsum_utils.reverse_order(order))
+ else:
+ matrix = self.np.reshape(matrix, 2 * len(gate.qubits) * (2,))
+ opstring = einsum_utils.apply_gate_string(gate.qubits, nqubits)
+ state = self.np.einsum(opstring, state, matrix)
+ return self.np.reshape(state, (2**nqubits,))
+
+ def apply_gate_density_matrix(self, gate, state, nqubits):
+ state = self.cast(state)
+ state = self.np.reshape(state, 2 * nqubits * (2,))
+ matrix = gate.matrix(self)
+ if gate.is_controlled_by:
+ matrix = self.np.reshape(matrix, 2 * len(gate.target_qubits) * (2,))
+ matrixc = self.np.conj(matrix)
+ ncontrol = len(gate.control_qubits)
+ nactive = nqubits - ncontrol
+ n = 2**ncontrol
+
+ order, targets = einsum_utils.control_order_density_matrix(gate, nqubits)
+ state = self.np.transpose(state, order)
+ state = self.np.reshape(state, 2 * (n,) + 2 * nactive * (2,))
+
+ leftc, rightc = einsum_utils.apply_gate_density_matrix_controlled_string(
+ targets, nactive
+ )
+ state01 = state[: n - 1, n - 1]
+ state01 = self.np.einsum(rightc, state01, matrixc)
+ state10 = state[n - 1, : n - 1]
+ state10 = self.np.einsum(leftc, state10, matrix)
+
+ left, right = einsum_utils.apply_gate_density_matrix_string(
+ targets, nactive
+ )
+ state11 = state[n - 1, n - 1]
+ state11 = self.np.einsum(right, state11, matrixc)
+ state11 = self.np.einsum(left, state11, matrix)
+
+ state00 = state[range(n - 1)]
+ state00 = state00[:, range(n - 1)]
+ state01 = self.np.concatenate([state00, state01[:, None]], axis=1)
+ state10 = self.np.concatenate([state10, state11[None]], axis=0)
+ state = self.np.concatenate([state01, state10[None]], axis=0)
+ state = self.np.reshape(state, 2 * nqubits * (2,))
+ state = self.np.transpose(state, einsum_utils.reverse_order(order))
+ else:
+ matrix = self.np.reshape(matrix, 2 * len(gate.qubits) * (2,))
+ matrixc = self.np.conj(matrix)
+ left, right = einsum_utils.apply_gate_density_matrix_string(
+ gate.qubits, nqubits
+ )
+ state = self.np.einsum(right, state, matrixc)
+ state = self.np.einsum(left, state, matrix)
+ return self.np.reshape(state, 2 * (2**nqubits,))
+
+ def apply_gate_half_density_matrix(self, gate, state, nqubits):
+ state = self.cast(state)
+ state = self.np.reshape(state, 2 * nqubits * (2,))
+ matrix = gate.matrix(self)
+ if gate.is_controlled_by: # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "Gate density matrix half call is "
+ "not implemented for ``controlled_by``"
+ "gates.",
+ )
+ else:
+ matrix = self.np.reshape(matrix, 2 * len(gate.qubits) * (2,))
+ left, _ = einsum_utils.apply_gate_density_matrix_string(
+ gate.qubits, nqubits
+ )
+ state = self.np.einsum(left, state, matrix)
+ return self.np.reshape(state, 2 * (2**nqubits,))
+
+ def apply_channel(self, channel, state, nqubits):
+ probabilities = channel.coefficients + (1 - np.sum(channel.coefficients),)
+ index = self.sample_shots(probabilities, 1)[0]
+ if index != len(channel.gates):
+ gate = channel.gates[index]
+ state = self.apply_gate(gate, state, nqubits)
+ return state
+
+ def apply_channel_density_matrix(self, channel, state, nqubits):
+ state = self.cast(state)
+ new_state = (1 - channel.coefficient_sum) * state
+ for coeff, gate in zip(channel.coefficients, channel.gates):
+ new_state += coeff * self.apply_gate_density_matrix(gate, state, nqubits)
+ return new_state
+
+ def _append_zeros(self, state, qubits, results):
+ """Helper method for collapse."""
+ for q, r in zip(qubits, results):
+ state = self.np.expand_dims(state, q)
+ state = (
+ self.np.concatenate([self.np.zeros_like(state), state], q)
+ if r == 1
+ else self.np.concatenate([state, self.np.zeros_like(state)], q)
+ )
+ return state
+
+ def collapse_state(self, state, qubits, shot, nqubits, normalize=True):
+ state = self.cast(state)
+ shape = state.shape
+ binshot = self.samples_to_binary(shot, len(qubits))[0]
+ state = self.np.reshape(state, nqubits * (2,))
+ order = list(qubits) + [q for q in range(nqubits) if q not in qubits]
+ state = self.np.transpose(state, order)
+ subshape = (2 ** len(qubits),) + (nqubits - len(qubits)) * (2,)
+ state = self.np.reshape(state, subshape)[int(shot)]
+ if normalize:
+ norm = self.np.sqrt(self.np.sum(self.np.abs(state) ** 2))
+ state = state / norm
+ state = self._append_zeros(state, qubits, binshot)
+ return self.np.reshape(state, shape)
+
+ def collapse_density_matrix(self, state, qubits, shot, nqubits, normalize=True):
+ state = self.cast(state)
+ shape = state.shape
+ binshot = list(self.samples_to_binary(shot, len(qubits))[0])
+ order = list(qubits) + [q + nqubits for q in qubits]
+ order.extend(q for q in range(nqubits) if q not in qubits)
+ order.extend(q + nqubits for q in range(nqubits) if q not in qubits)
+ state = self.np.reshape(state, 2 * nqubits * (2,))
+ state = self.np.transpose(state, order)
+ subshape = 2 * (2 ** len(qubits),) + 2 * (nqubits - len(qubits)) * (2,)
+ state = self.np.reshape(state, subshape)[int(shot), int(shot)]
+ n = 2 ** (len(state.shape) // 2)
+ if normalize:
+ norm = self.np.trace(self.np.reshape(state, (n, n)))
+ state = state / norm
+ qubits = qubits + [q + nqubits for q in qubits]
+ state = self._append_zeros(state, qubits, 2 * binshot)
+ return self.np.reshape(state, shape)
+
+ def reset_error_density_matrix(self, gate, state, nqubits):
+ from qibo.gates import X # pylint: disable=C0415
+ from qibo.quantum_info.linalg_operations import ( # pylint: disable=C0415
+ partial_trace,
+ )
+
+ state = self.cast(state)
+ shape = state.shape
+ q = gate.target_qubits[0]
+ p_0, p_1 = gate.init_kwargs["p_0"], gate.init_kwargs["p_1"]
+ trace = partial_trace(state, (q,), backend=self)
+ trace = self.np.reshape(trace, 2 * (nqubits - 1) * (2,))
+ zero = self.zero_density_matrix(1)
+ zero = self.np.tensordot(trace, zero, 0)
+ order = list(range(2 * nqubits - 2))
+ order.insert(q, 2 * nqubits - 2)
+ order.insert(q + nqubits, 2 * nqubits - 1)
+ zero = self.np.reshape(self.np.transpose(zero, order), shape)
+ state = (1 - p_0 - p_1) * state + p_0 * zero
+ return state + p_1 * self.apply_gate_density_matrix(X(q), zero, nqubits)
+
+ def thermal_error_density_matrix(self, gate, state, nqubits):
+ state = self.cast(state)
+ shape = state.shape
+ state = self.apply_gate(gate, state.ravel(), 2 * nqubits)
+ return self.np.reshape(state, shape)
+
+ def depolarizing_error_density_matrix(self, gate, state, nqubits):
+ from qibo.quantum_info.linalg_operations import ( # pylint: disable=C0415
+ partial_trace,
+ )
+
+ state = self.cast(state)
+ shape = state.shape
+ q = gate.target_qubits
+ lam = gate.init_kwargs["lam"]
+ trace = partial_trace(state, q, backend=self)
+ trace = self.np.reshape(trace, 2 * (nqubits - len(q)) * (2,))
+ identity = self.identity_density_matrix(len(q))
+ identity = self.np.reshape(identity, 2 * len(q) * (2,))
+ identity = self.np.tensordot(trace, identity, 0)
+ qubits = list(range(nqubits))
+ for j in q:
+ qubits.pop(qubits.index(j))
+ qubits.sort()
+ qubits += list(q)
+ qubit_1 = list(range(nqubits - len(q))) + list(
+ range(2 * (nqubits - len(q)), 2 * nqubits - len(q))
+ )
+ qubit_2 = list(range(nqubits - len(q), 2 * (nqubits - len(q)))) + list(
+ range(2 * nqubits - len(q), 2 * nqubits)
+ )
+ qs = [qubit_1, qubit_2]
+ order = []
+ for qj in qs:
+ qj = [qj[qubits.index(i)] for i in range(len(qubits))]
+ order += qj
+ identity = self.np.reshape(self.np.transpose(identity, order), shape)
+ state = (1 - lam) * state + lam * identity
+ return state
+
+ def execute_circuit(self, circuit, initial_state=None, nshots=1000):
+
+ if isinstance(initial_state, type(circuit)):
+ if not initial_state.density_matrix == circuit.density_matrix:
+ raise_error(
+ ValueError,
+ f"""Cannot set circuit with density_matrix {initial_state.density_matrix} as
+ initial state for circuit with density_matrix {circuit.density_matrix}.""",
+ )
+ elif (
+ not initial_state.accelerators == circuit.accelerators
+ ): # pragma: no cover
+ raise_error(
+ ValueError,
+ f"""Cannot set circuit with accelerators {initial_state.density_matrix} as
+ initial state for circuit with accelerators {circuit.density_matrix}.""",
+ )
+ else:
+ return self.execute_circuit(initial_state + circuit, None, nshots)
+ elif initial_state is not None:
+ initial_state = self.cast(initial_state)
+ valid_shape = (
+ 2 * (2**circuit.nqubits,)
+ if circuit.density_matrix
+ else (2**circuit.nqubits,)
+ )
+ if tuple(initial_state.shape) != valid_shape:
+ raise_error(
+ ValueError,
+ f"Given initial state has shape {initial_state.shape} instead of "
+ f"the expected {valid_shape}.",
+ )
+
+ if circuit.repeated_execution:
+ if circuit.measurements or circuit.has_collapse:
+ return self.execute_circuit_repeated(circuit, nshots, initial_state)
+ else:
+ raise RuntimeError(
+ "Attempting to perform noisy simulation with `density_matrix=False` and no Measurement gate in the Circuit. If you wish to retrieve the statistics of the outcomes please include measurements in the circuit, otherwise set `density_matrix=True` to recover the final state."
+ )
+
+ if circuit.accelerators: # pragma: no cover
+ return self.execute_distributed_circuit(circuit, initial_state, nshots)
+
+ try:
+ nqubits = circuit.nqubits
+
+ if circuit.density_matrix:
+ if initial_state is None:
+ state = self.zero_density_matrix(nqubits)
+ else:
+ # cast to proper complex type
+ state = self.cast(initial_state)
+
+ for gate in circuit.queue:
+ state = gate.apply_density_matrix(self, state, nqubits)
+
+ else:
+ if initial_state is None:
+ state = self.zero_state(nqubits)
+ else:
+ # cast to proper complex type
+ state = self.cast(initial_state)
+
+ for gate in circuit.queue:
+ state = gate.apply(self, state, nqubits)
+
+ if circuit.has_unitary_channel:
+ # here we necessarily have `density_matrix=True`, otherwise
+ # execute_circuit_repeated would have been called
+ if circuit.measurements:
+ circuit._final_state = CircuitResult(
+ state, circuit.measurements, backend=self, nshots=nshots
+ )
+ return circuit._final_state
+ else:
+ circuit._final_state = QuantumState(state, backend=self)
+ return circuit._final_state
+
+ else:
+ if circuit.measurements:
+ circuit._final_state = CircuitResult(
+ state, circuit.measurements, backend=self, nshots=nshots
+ )
+ return circuit._final_state
+ else:
+ circuit._final_state = QuantumState(state, backend=self)
+ return circuit._final_state
+
+ except self.oom_error:
+ raise_error(
+ RuntimeError,
+ f"State does not fit in {self.device} memory."
+ "Please switch the execution device to a "
+ "different one using ``qibo.set_device``.",
+ )
+
+ def execute_circuits(
+ self, circuits, initial_states=None, nshots=1000, processes=None
+ ):
+ from qibo.parallel import parallel_circuits_execution
+
+ return parallel_circuits_execution(
+ circuits, initial_states, nshots, processes, backend=self
+ )
+
+ def execute_circuit_repeated(self, circuit, nshots, initial_state=None):
+ """
+ Execute the circuit `nshots` times to retrieve probabilities, frequencies
+ and samples. Note that this method is called only if a unitary channel
+ is present in the circuit (i.e. noisy simulation) and `density_matrix=False`, or
+ if some collapsing measurement is performed.
+ """
+
+ if (
+ circuit.has_collapse
+ and not circuit.measurements
+ and not circuit.density_matrix
+ ):
+ raise RuntimeError(
+ "The circuit contains only collapsing measurements (`collapse=True`) but `density_matrix=False`. Please set `density_matrix=True` to retrieve the final state after execution."
+ )
+
+ results, final_states = [], []
+ nqubits = circuit.nqubits
+
+ if not circuit.density_matrix:
+ samples = []
+ target_qubits = [
+ measurement.target_qubits for measurement in circuit.measurements
+ ]
+ target_qubits = sum(target_qubits, tuple())
+
+ for _ in range(nshots):
+ if circuit.density_matrix:
+ if initial_state is None:
+ state = self.zero_density_matrix(nqubits)
+ else:
+ state = self.cast(initial_state, copy=True)
+
+ for gate in circuit.queue:
+ if gate.symbolic_parameters:
+ gate.substitute_symbols()
+ state = gate.apply_density_matrix(self, state, nqubits)
+ else:
+ if circuit.accelerators: # pragma: no cover
+ # pylint: disable=E1111
+ state = self.execute_distributed_circuit(circuit, initial_state)
+ else:
+ if initial_state is None:
+ state = self.zero_state(nqubits)
+ else:
+ state = self.cast(initial_state, copy=True)
+
+ for gate in circuit.queue:
+ if gate.symbolic_parameters:
+ gate.substitute_symbols()
+ state = gate.apply(self, state, nqubits)
+
+ if circuit.density_matrix:
+ final_states.append(state)
+ if circuit.measurements:
+ result = CircuitResult(
+ state, circuit.measurements, backend=self, nshots=1
+ )
+ sample = result.samples()[0]
+ results.append(sample)
+ if not circuit.density_matrix:
+ samples.append("".join([str(int(s)) for s in sample]))
+ for gate in circuit.measurements:
+ gate.result.reset()
+
+ if circuit.density_matrix: # this implies also it has_collapse
+ assert circuit.has_collapse
+ final_state = self.cast(np.mean(self.to_numpy(final_states), 0))
+ if circuit.measurements:
+ final_result = CircuitResult(
+ final_state,
+ circuit.measurements,
+ backend=self,
+ samples=self.aggregate_shots(results),
+ nshots=nshots,
+ )
+ else:
+ final_result = QuantumState(final_state, backend=self)
+ circuit._final_state = final_result
+ return final_result
+ else:
+ final_result = MeasurementOutcomes(
+ circuit.measurements,
+ backend=self,
+ samples=self.aggregate_shots(results),
+ nshots=nshots,
+ )
+ final_result._repeated_execution_frequencies = self.calculate_frequencies(
+ samples
+ )
+ circuit._final_state = final_result
+ return final_result
+
+ def execute_distributed_circuit(self, circuit, initial_state=None, nshots=None):
+ raise_error(
+ NotImplementedError, f"{self} does not support distributed execution."
+ )
+
+ def calculate_symbolic(
+ self, state, nqubits, decimals=5, cutoff=1e-10, max_terms=20
+ ):
+ state = self.to_numpy(state)
+ terms = []
+ for i in np.nonzero(state)[0]:
+ b = bin(i)[2:].zfill(nqubits)
+ if np.abs(state[i]) >= cutoff:
+ x = np.round(state[i], decimals)
+ terms.append(f"{x}|{b}>")
+ if len(terms) >= max_terms:
+ terms.append("...")
+ return terms
+ return terms
+
+ def calculate_symbolic_density_matrix(
+ self, state, nqubits, decimals=5, cutoff=1e-10, max_terms=20
+ ):
+ state = self.to_numpy(state)
+ terms = []
+ indi, indj = np.nonzero(state)
+ for i, j in zip(indi, indj):
+ bi = bin(i)[2:].zfill(nqubits)
+ bj = bin(j)[2:].zfill(nqubits)
+ if np.abs(state[i, j]) >= cutoff:
+ x = np.round(state[i, j], decimals)
+ terms.append(f"{x}|{bi}><{bj}|")
+ if len(terms) >= max_terms:
+ terms.append("...")
+ return terms
+ return terms
+
+ def _order_probabilities(self, probs, qubits, nqubits):
+ """Arrange probabilities according to the given ``qubits`` ordering."""
+ unmeasured, reduced = [], {}
+ for i in range(nqubits):
+ if i in qubits:
+ reduced[i] = i - len(unmeasured)
+ else:
+ unmeasured.append(i)
+ return self.np.transpose(probs, [reduced.get(i) for i in qubits])
+
+ def calculate_probabilities(self, state, qubits, nqubits):
+ rtype = self.np.real(state).dtype
+ unmeasured_qubits = tuple(i for i in range(nqubits) if i not in qubits)
+ state = self.np.reshape(self.np.abs(state) ** 2, nqubits * (2,))
+ probs = self.np.sum(self.cast(state, dtype=rtype), axis=unmeasured_qubits)
+ return self._order_probabilities(probs, qubits, nqubits).ravel()
+
+ def calculate_probabilities_density_matrix(self, state, qubits, nqubits):
+ order = tuple(sorted(qubits))
+ order += tuple(i for i in range(nqubits) if i not in qubits)
+ order = order + tuple(i + nqubits for i in order)
+ shape = 2 * (2 ** len(qubits), 2 ** (nqubits - len(qubits)))
+ state = self.np.reshape(state, 2 * nqubits * (2,))
+ state = self.np.reshape(self.np.transpose(state, order), shape)
+ probs = self.np.abs(self.np.einsum("abab->a", state))
+ probs = self.np.reshape(probs, len(qubits) * (2,))
+ return self._order_probabilities(probs, qubits, nqubits).ravel()
+
+ def set_seed(self, seed):
+ self.np.random.seed(seed)
+
+ def sample_shots(self, probabilities, nshots):
+ return self.np.random.choice(
+ range(len(probabilities)), size=nshots, p=probabilities
+ )
+
+ def aggregate_shots(self, shots):
+ return self.cast(shots, dtype=shots[0].dtype)
+
+ def samples_to_binary(self, samples, nqubits):
+ ### This is faster just staying @ NumPy.
+ qrange = np.arange(nqubits - 1, -1, -1, dtype=np.int32)
+ samples = self.to_numpy(samples)
+ return np.mod(np.right_shift(samples[:, None], qrange), 2)
+
+ def samples_to_decimal(self, samples, nqubits):
+ ### This is faster just staying @ NumPy.
+ qrange = np.arange(nqubits - 1, -1, -1, dtype=np.int32)
+ qrange = (2**qrange)[:, None]
+ samples = np.asarray(samples.tolist())
+ return np.matmul(samples, qrange)[:, 0]
+
+ def calculate_frequencies(self, samples):
+ # Samples are a list of strings so there is no advantage in using other backends
+ res, counts = np.unique(samples, return_counts=True)
+ res = self.to_numpy(res).tolist()
+ counts = self.to_numpy(counts).tolist()
+ return collections.Counter(dict(zip(res, counts)))
+
+ def update_frequencies(self, frequencies, probabilities, nsamples):
+ samples = self.sample_shots(probabilities, nsamples)
+ res, counts = self.np.unique(samples, return_counts=True)
+ frequencies[res] += counts
+ return frequencies
+
+ def sample_frequencies(self, probabilities, nshots):
+ from qibo.config import SHOT_BATCH_SIZE
+
+ nprobs = probabilities / self.np.sum(probabilities)
+ frequencies = self.np.zeros(len(nprobs), dtype=self.np.int64)
+ for _ in range(nshots // SHOT_BATCH_SIZE):
+ frequencies = self.update_frequencies(frequencies, nprobs, SHOT_BATCH_SIZE)
+ frequencies = self.update_frequencies(
+ frequencies, nprobs, nshots % SHOT_BATCH_SIZE
+ )
+ return collections.Counter(
+ {i: int(f) for i, f in enumerate(frequencies) if f > 0}
+ )
+
+ def apply_bitflips(self, noiseless_samples, bitflip_probabilities):
+ noiseless_samples = self.cast(noiseless_samples, dtype=noiseless_samples.dtype)
+ fprobs = self.cast(bitflip_probabilities, dtype="float64")
+ sprobs = self.cast(np.random.random(noiseless_samples.shape), dtype="float64")
+ flip_0 = self.cast(sprobs < fprobs[0], dtype=noiseless_samples.dtype)
+ flip_1 = self.cast(sprobs < fprobs[1], dtype=noiseless_samples.dtype)
+ noisy_samples = noiseless_samples + (1 - noiseless_samples) * flip_0
+ noisy_samples = noisy_samples - noiseless_samples * flip_1
+ return noisy_samples
+
+ def calculate_norm(self, state, order=2):
+ state = self.cast(state)
+ return self.np.linalg.norm(state, order)
+
+ def calculate_norm_density_matrix(self, state, order="nuc"):
+ state = self.cast(state)
+ return self.np.linalg.norm(state, ord=order)
+
+ def calculate_overlap(self, state1, state2):
+ return self.np.abs(
+ self.np.sum(self.np.conj(self.cast(state1)) * self.cast(state2))
+ )
+
+ def calculate_overlap_density_matrix(self, state1, state2):
+ return self.np.trace(
+ self.np.matmul(self.np.conj(self.cast(state1)).T, self.cast(state2))
+ )
+
+ def calculate_eigenvalues(self, matrix, k=6, hermitian=True):
+ if self.is_sparse(matrix):
+ log.warning(
+ "Calculating sparse matrix eigenvectors because "
+ "sparse modules do not provide ``eigvals`` method."
+ )
+ return self.calculate_eigenvectors(matrix, k=k)[0]
+ if hermitian:
+ return np.linalg.eigvalsh(matrix)
+ return np.linalg.eigvals(matrix)
+
+ def calculate_eigenvectors(self, matrix, k=6, hermitian=True):
+ if self.is_sparse(matrix):
+ if k < matrix.shape[0]:
+ from scipy.sparse.linalg import eigsh
+
+ return eigsh(matrix, k=k, which="SA")
+ else: # pragma: no cover
+ matrix = self.to_numpy(matrix)
+ if hermitian:
+ return np.linalg.eigh(matrix)
+ return np.linalg.eig(matrix)
+
+ def calculate_expectation_state(self, hamiltonian, state, normalize):
+ statec = self.np.conj(state)
+ hstate = hamiltonian @ state
+ ev = self.np.real(self.np.sum(statec * hstate))
+ if normalize:
+ ev /= self.np.sum(self.np.square(self.np.abs(state)))
+ return ev
+
+ def calculate_expectation_density_matrix(self, hamiltonian, state, normalize):
+ ev = self.np.real(self.np.trace(self.cast(hamiltonian @ state)))
+ if normalize:
+ norm = self.np.real(self.np.trace(state))
+ ev /= norm
+ return ev
+
+ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
+ if eigenvectors is None or self.is_sparse(matrix):
+ if self.is_sparse(matrix):
+ from scipy.sparse.linalg import expm
+ else:
+ from scipy.linalg import expm
+ return expm(-1j * a * matrix)
+ expd = self.np.diag(self.np.exp(-1j * a * eigenvalues))
+ ud = self.np.transpose(np.conj(eigenvectors))
+ return self.np.matmul(eigenvectors, self.np.matmul(expd, ud))
+
+ # TODO: remove this method
+ def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
+ return matrix1 @ matrix2
+
+ # TODO: remove this method
+ def calculate_hamiltonian_state_product(self, matrix, state):
+ if len(tuple(state.shape)) > 2:
+ raise_error(
+ ValueError,
+ f"Cannot multiply Hamiltonian with rank-{len(tuple(state.shape))} tensor.",
+ )
+ return matrix @ state
+
+ def assert_allclose(self, value, target, rtol=1e-7, atol=0.0):
+ if isinstance(value, CircuitResult) or isinstance(value, QuantumState):
+ value = value.state()
+ if isinstance(target, CircuitResult) or isinstance(target, QuantumState):
+ target = target.state()
+ value = self.to_numpy(value)
+ target = self.to_numpy(target)
+ np.testing.assert_allclose(value, target, rtol=rtol, atol=atol)
+
+ def _test_regressions(self, name):
+ if name == "test_measurementresult_apply_bitflips":
+ return [
+ [0, 0, 0, 0, 2, 3, 0, 0, 0, 0],
+ [0, 0, 0, 0, 2, 3, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
+ ]
+ elif name == "test_probabilistic_measurement":
+ return {0: 249, 1: 231, 2: 253, 3: 267}
+ elif name == "test_unbalanced_probabilistic_measurement":
+ return {0: 171, 1: 148, 2: 161, 3: 520}
+ elif name == "test_post_measurement_bitflips_on_circuit":
+ return [
+ {5: 30},
+ {5: 18, 4: 5, 7: 4, 1: 2, 6: 1},
+ {4: 8, 2: 6, 5: 5, 1: 3, 3: 3, 6: 2, 7: 2, 0: 1},
+ ]
diff --git a/src/qibo/backends/pytorch.py b/src/qibo/backends/pytorch.py
new file mode 100644
index 000000000..392aeed40
--- /dev/null
+++ b/src/qibo/backends/pytorch.py
@@ -0,0 +1,222 @@
+"""PyTorch backend."""
+
+import numpy as np
+
+from qibo import __version__
+from qibo.backends.npmatrices import NumpyMatrices
+from qibo.backends.numpy import NumpyBackend
+
+
+class TorchMatrices(NumpyMatrices):
+ """Matrix representation of every gate as a torch Tensor.
+
+ Args:
+ dtype (torch.dtype): Data type of the matrices.
+ requires_grad (bool): If ``True`` the matrices require gradient.
+ """
+
+ def __init__(self, dtype, requires_grad):
+ import torch # pylint: disable=import-outside-toplevel
+
+ super().__init__(dtype)
+ self.np = torch
+ self.dtype = dtype
+ self.requires_grad = requires_grad
+
+ def _cast(self, x, dtype):
+ flattened = [item for sublist in x for item in sublist]
+ tensor_list = [self.np.as_tensor(i, dtype=dtype) for i in flattened]
+ return self.np.stack(tensor_list).reshape(len(x), len(x))
+
+ def _cast_parameter(self, x):
+ return self.np.tensor(x, dtype=self.dtype, requires_grad=self.requires_grad)
+
+ def Unitary(self, u):
+ return self._cast(u, dtype=self.dtype)
+
+
+class PyTorchBackend(NumpyBackend):
+ def __init__(self):
+ super().__init__()
+ import torch # pylint: disable=import-outside-toplevel
+
+ # Global variable to enable or disable gradient calculation
+ self.gradients = True
+
+ self.np = torch
+
+ self.name = "pytorch"
+ self.versions = {
+ "qibo": __version__,
+ "numpy": np.__version__,
+ "torch": self.np.__version__,
+ }
+
+ self.dtype = self._torch_dtype(self.dtype)
+ self.matrices = TorchMatrices(self.dtype, requires_grad=self.gradients)
+ self.device = self.np.device("cuda:0" if torch.cuda.is_available() else "cpu")
+ self.nthreads = 0
+ self.tensor_types = (self.np.Tensor, np.ndarray)
+
+ # These functions in Torch works in a different way than numpy or have different names
+ self.np.transpose = self.np.permute
+ self.np.copy = self.np.clone
+ self.np.expand_dims = self.np.unsqueeze
+ self.np.mod = self.np.remainder
+ self.np.right_shift = self.np.bitwise_right_shift
+ self.np.sign = self.np.sgn
+ self.np.flatnonzero = lambda x: self.np.nonzero(x).flatten()
+
+ def requires_grad(self, requires_grad):
+ """Enable or disable gradient calculation."""
+ self.gradients = requires_grad
+ self.matrices.requires_grad = requires_grad
+
+ def _torch_dtype(self, dtype):
+ if dtype == "float":
+ dtype += "32"
+ return getattr(self.np, dtype)
+
+ def set_device(self, device): # pragma: no cover
+ self.device = device
+
+ def cast(
+ self,
+ x,
+ dtype=None,
+ copy: bool = False,
+ requires_grad: bool = None,
+ ):
+ """Casts input as a Torch tensor of the specified dtype.
+
+ This method supports casting of single tensors or lists of tensors
+ as for the :class:`qibo.backends.PyTorchBackend`.
+
+ Args:
+ x (Union[torch.Tensor, list[torch.Tensor], np.ndarray, list[np.ndarray], int, float, complex]):
+ Input to be casted.
+ dtype (Union[str, torch.dtype, np.dtype, type]): Target data type.
+ If ``None``, the default dtype of the backend is used.
+ Defaults to ``None``.
+ copy (bool, optional): If ``True``, the input tensor is copied before casting.
+ Defaults to ``False``.
+ requires_grad (bool, optional): If ``True``, the input tensor requires gradient.
+ If ``False``, the input tensor does not require gradient.
+ If ``None``, the default gradient setting of the backend is used.
+ """
+ if requires_grad is None:
+ requires_grad = self.gradients
+
+ if dtype is None:
+ dtype = self.dtype
+ elif isinstance(dtype, type):
+ dtype = self._torch_dtype(dtype.__name__)
+ elif not isinstance(dtype, self.np.dtype):
+ dtype = self._torch_dtype(str(dtype))
+
+ # check if dtype is an integer to remove gradients
+ if dtype in [self.np.int32, self.np.int64, self.np.int8, self.np.int16]:
+ requires_grad = False
+
+ if isinstance(x, self.np.Tensor):
+ x = x.to(dtype)
+ elif isinstance(x, list) and all(isinstance(row, self.np.Tensor) for row in x):
+ x = self.np.stack(x)
+ else:
+ x = self.np.tensor(x, dtype=dtype, requires_grad=requires_grad)
+
+ if copy:
+ return x.clone()
+
+ return x
+
+ def is_sparse(self, x):
+ if isinstance(x, self.np.Tensor):
+ return x.is_sparse
+
+ return super().is_sparse(x)
+
+ def to_numpy(self, x):
+ if isinstance(x, list):
+ return np.asarray([self.to_numpy(i) for i in x])
+
+ if isinstance(x, self.np.Tensor):
+ return x.numpy(force=True)
+
+ return x
+
+ def _order_probabilities(self, probs, qubits, nqubits):
+ """Arrange probabilities according to the given ``qubits`` ordering."""
+ if probs.dim() == 0: # pragma: no cover
+ return probs
+ unmeasured, reduced = [], {}
+ for i in range(nqubits):
+ if i in qubits:
+ reduced[i] = i - len(unmeasured)
+ else:
+ unmeasured.append(i)
+ return self.np.transpose(probs, [reduced.get(i) for i in qubits])
+
+ def calculate_probabilities(self, state, qubits, nqubits):
+ rtype = self.np.real(state).dtype
+ unmeasured_qubits = tuple(i for i in range(nqubits) if i not in qubits)
+ state = self.np.reshape(self.np.abs(state) ** 2, nqubits * (2,))
+ if len(unmeasured_qubits) == 0:
+ probs = self.cast(state, dtype=rtype)
+ else:
+ probs = self.np.sum(self.cast(state, dtype=rtype), axis=unmeasured_qubits)
+ return self._order_probabilities(probs, qubits, nqubits).ravel()
+
+ def set_seed(self, seed):
+ self.np.manual_seed(seed)
+ np.random.seed(seed)
+
+ def sample_shots(self, probabilities, nshots):
+ return self.np.multinomial(
+ self.cast(probabilities, dtype="float"), nshots, replacement=True
+ )
+
+ def calculate_eigenvalues(self, matrix, k=6, hermitian=True):
+ if hermitian:
+ return self.np.linalg.eigvalsh(matrix) # pylint: disable=not-callable
+ return self.np.linalg.eigvals(matrix) # pylint: disable=not-callable
+
+ def calculate_eigenvectors(self, matrix, k=6, hermitian=True):
+ if hermitian:
+ return self.np.linalg.eigh(matrix) # pylint: disable=not-callable
+ return self.np.linalg.eig(matrix) # pylint: disable=not-callable
+
+ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
+ if eigenvectors is None or self.is_sparse(matrix):
+ return self.np.linalg.matrix_exp( # pylint: disable=not-callable
+ -1j * a * matrix
+ )
+ expd = self.np.diag(self.np.exp(-1j * a * eigenvalues))
+ ud = self.np.conj(eigenvectors).T
+ return self.np.matmul(eigenvectors, self.np.matmul(expd, ud))
+
+ def _test_regressions(self, name):
+ if name == "test_measurementresult_apply_bitflips":
+ return [
+ [0, 0, 0, 0, 2, 3, 0, 0, 0, 0],
+ [0, 0, 0, 0, 2, 3, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 2, 0, 0, 0, 0, 0],
+ ]
+
+ if name == "test_probabilistic_measurement":
+ if self.device == "cuda": # pragma: no cover
+ return {0: 273, 1: 233, 2: 242, 3: 252}
+ return {1: 270, 2: 248, 3: 244, 0: 238}
+
+ if name == "test_unbalanced_probabilistic_measurement":
+ if self.device == "cuda": # pragma: no cover
+ return {0: 196, 1: 153, 2: 156, 3: 495}
+ return {3: 492, 2: 176, 0: 168, 1: 164}
+
+ if name == "test_post_measurement_bitflips_on_circuit":
+ return [
+ {5: 30},
+ {5: 17, 4: 5, 7: 4, 1: 2, 6: 2},
+ {4: 9, 2: 5, 5: 5, 3: 4, 6: 4, 0: 1, 1: 1, 7: 1},
+ ]
diff --git a/src/qibo/backends/qulacs.py b/src/qibo/backends/qulacs.py
new file mode 100644
index 000000000..970ce3b36
--- /dev/null
+++ b/src/qibo/backends/qulacs.py
@@ -0,0 +1,86 @@
+import re
+
+import numpy as np
+import qulacs # pylint: disable=import-error
+from qulacs import ( # pylint: disable=no-name-in-module, import-error
+ QuantumCircuitSimulator,
+ converter,
+)
+
+from qibo import __version__
+from qibo.backends import NumpyBackend
+from qibo.config import raise_error
+from qibo.result import CircuitResult, QuantumState
+
+
+def circuit_to_qulacs(
+ circuit: "qibo.Circuit",
+) -> "qulacs.QuantumCircuit": # pylint: disable=no-member
+ """
+ Converts a qibo circuit in a qulacs circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Input circuit to convert.
+
+ Returns:
+ qulacs.QuantumCircuit: The converted qulacs circuit.
+ """
+ qasm_str = re.sub("^//.+\n", "", circuit.to_qasm())
+ qasm_str = re.sub(r"creg\s.+;", "", qasm_str)
+ qasm_str = re.sub(r"measure\s.+;", "", qasm_str)
+ circ = converter.convert_QASM_to_qulacs_circuit(qasm_str.splitlines())
+ return circ
+
+
+class QulacsBackend(NumpyBackend):
+
+ def __init__(self):
+ super().__init__()
+
+ self.name = "qulacs"
+ self.versions = {"qibo": __version__, "qulacs": qulacs.__version__}
+ self.device = "CPU"
+
+ def execute_circuit(
+ self,
+ circuit: "qibo.Circuit",
+ initial_state=None,
+ nshots: int = 1000,
+ ):
+ """Execute a circuit with qulacs.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Input circuit.
+ nshots (int, optional): Number of shots to perform if ``circuit`` has measurements.
+ Defaults to :math:`10^{3}`.
+
+ Returns:
+ :class:`qibo.result.CircuitResult`: Object storing to the final results.
+ """
+ if initial_state is not None:
+ raise_error(
+ NotImplementedError,
+ "The use of an initial state is not supported yet by the `QulacsBackend`.",
+ )
+ circ = circuit_to_qulacs(circuit)
+ state = (
+ qulacs.DensityMatrix(circuit.nqubits) # pylint: disable=no-member
+ if circuit.density_matrix
+ else qulacs.QuantumState(circuit.nqubits) # pylint: disable=no-member
+ )
+ sim = QuantumCircuitSimulator(circ, state)
+ sim.simulate()
+ if circuit.density_matrix:
+ dim = 2**circuit.nqubits
+ state = (
+ state.get_matrix()
+ .reshape(2 * circuit.nqubits * (2,))
+ .T.reshape(dim, dim)
+ )
+ else:
+ state = state.get_vector().reshape(circuit.nqubits * (2,)).T.ravel()
+ if len(circuit.measurements) > 0:
+ return CircuitResult(
+ state, circuit.measurements, backend=self, nshots=nshots
+ )
+ return QuantumState(state, backend=self)
diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py
new file mode 100644
index 000000000..6f4deed49
--- /dev/null
+++ b/src/qibo/backends/tensorflow.py
@@ -0,0 +1,238 @@
+import collections
+import os
+
+import numpy as np
+
+from qibo import __version__
+from qibo.backends.npmatrices import NumpyMatrices
+from qibo.backends.numpy import NumpyBackend
+from qibo.config import TF_LOG_LEVEL, log, raise_error
+
+
+class TensorflowMatrices(NumpyMatrices):
+ # Redefine parametrized gate matrices for backpropagation to work
+
+ def __init__(self, dtype):
+ super().__init__(dtype)
+ import tensorflow as tf # pylint: disable=import-error
+ import tensorflow.experimental.numpy as tnp # pylint: disable=import-error
+
+ self.tf = tf
+ self.np = tnp
+ self.np.linalg = tf.linalg
+
+ def _cast(self, x, dtype):
+ return self.tf.cast(x, dtype=dtype)
+
+ def Unitary(self, u):
+ return self._cast(u, dtype=self.dtype)
+
+
+class TensorflowBackend(NumpyBackend):
+ def __init__(self):
+ super().__init__()
+ self.name = "tensorflow"
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = str(TF_LOG_LEVEL)
+
+ import tensorflow as tf # pylint: disable=import-error
+ import tensorflow.experimental.numpy as tnp # pylint: disable=import-error
+
+ if TF_LOG_LEVEL >= 2:
+ tf.get_logger().setLevel("ERROR")
+
+ tnp.experimental_enable_numpy_behavior()
+ self.tf = tf
+ self.np = tnp
+ self.np.flatnonzero = np.flatnonzero
+ self.np.copy = np.copy
+
+ self.versions = {
+ "qibo": __version__,
+ "numpy": np.__version__,
+ "tensorflow": tf.__version__,
+ }
+
+ self.matrices = TensorflowMatrices(self.dtype)
+
+ from tensorflow.python.framework import ( # pylint: disable=E0611,import-error
+ errors_impl,
+ )
+
+ self.oom_error = errors_impl.ResourceExhaustedError
+
+ cpu_devices = tf.config.list_logical_devices("CPU")
+ gpu_devices = tf.config.list_logical_devices("GPU")
+ if gpu_devices: # pragma: no cover
+ # CI does not use GPUs
+ self.device = gpu_devices[0].name
+ elif cpu_devices:
+ self.device = cpu_devices[0].name
+
+ self.nthreads = 0
+
+ self.tensor_types = (np.ndarray, tf.Tensor, tf.Variable)
+
+ def set_device(self, device): # pragma: no cover
+ self.device = device
+
+ def set_threads(self, nthreads):
+ log.warning(
+ "`set_threads` is not supported by the tensorflow "
+ "backend. Please use tensorflow's thread setters: "
+ "`tf.config.threading.set_inter_op_parallelism_threads` "
+ "or `tf.config.threading.set_intra_op_parallelism_threads` "
+ "to switch the number of threads."
+ )
+
+ def cast(self, x, dtype=None, copy=False):
+ if dtype is None:
+ dtype = self.dtype
+ x = self.tf.cast(x, dtype=dtype)
+ if copy:
+ return self.tf.identity(x)
+ return x
+
+ def is_sparse(self, x):
+ return isinstance(x, self.tf.sparse.SparseTensor)
+
+ def to_numpy(self, x):
+ return np.array(x)
+
+ def compile(self, func):
+ return self.tf.function(func)
+
+ def zero_state(self, nqubits):
+ idx = self.tf.constant([[0]], dtype="int32")
+ state = self.tf.zeros((2**nqubits,), dtype=self.dtype)
+ update = self.tf.constant([1], dtype=self.dtype)
+ state = self.tf.tensor_scatter_nd_update(state, idx, update)
+ return state
+
+ def zero_density_matrix(self, nqubits):
+ idx = self.tf.constant([[0, 0]], dtype="int32")
+ state = self.tf.zeros(2 * (2**nqubits,), dtype=self.dtype)
+ update = self.tf.constant([1], dtype=self.dtype)
+ state = self.tf.tensor_scatter_nd_update(state, idx, update)
+ return state
+
+ def matrix(self, gate):
+ npmatrix = super().matrix(gate)
+ return self.tf.cast(npmatrix, dtype=self.dtype)
+
+ def matrix_parametrized(self, gate):
+ npmatrix = super().matrix_parametrized(gate)
+ return self.tf.cast(npmatrix, dtype=self.dtype)
+
+ def matrix_fused(self, gate):
+ npmatrix = super().matrix_fused(gate)
+ return self.tf.cast(npmatrix, dtype=self.dtype)
+
+ def execute_circuit(self, circuit, initial_state=None, nshots=1000):
+ with self.tf.device(self.device):
+ return super().execute_circuit(circuit, initial_state, nshots)
+
+ def execute_circuit_repeated(self, circuit, nshots, initial_state=None):
+ with self.tf.device(self.device):
+ return super().execute_circuit_repeated(circuit, nshots, initial_state)
+
+ def sample_shots(self, probabilities, nshots):
+ # redefining this because ``tnp.random.choice`` is not available
+ logits = self.tf.math.log(probabilities)[self.tf.newaxis]
+ samples = self.tf.random.categorical(logits, nshots)[0]
+ return samples
+
+ def samples_to_binary(self, samples, nqubits):
+ # redefining this because ``tnp.right_shift`` is not available
+ qrange = self.np.arange(nqubits - 1, -1, -1, dtype="int32")
+ samples = self.tf.cast(samples, dtype="int32")
+ samples = self.tf.bitwise.right_shift(samples[:, self.np.newaxis], qrange)
+ return self.tf.math.mod(samples, 2)
+
+ def calculate_frequencies(self, samples):
+ # redefining this because ``tnp.unique`` is not available
+ res, _, counts = self.tf.unique_with_counts(samples, out_idx="int64")
+ res, counts = self.np.array(res), self.np.array(counts)
+ if res.dtype == "string":
+ res = [r.numpy().decode("utf8") for r in res]
+ else:
+ res = [int(r) for r in res]
+ return collections.Counter({k: int(v) for k, v in zip(res, counts)})
+
+ def update_frequencies(self, frequencies, probabilities, nsamples):
+ # redefining this because ``tnp.unique`` and tensor update is not available
+ samples = self.sample_shots(probabilities, nsamples)
+ res, _, counts = self.tf.unique_with_counts(samples, out_idx="int64")
+ frequencies = self.tf.tensor_scatter_nd_add(
+ frequencies, res[:, self.tf.newaxis], counts
+ )
+ return frequencies
+
+ def calculate_norm(self, state, order=2):
+ state = self.cast(state)
+ return self.tf.norm(state, ord=order)
+
+ def calculate_norm_density_matrix(self, state, order="nuc"):
+ state = self.cast(state)
+ if order == "nuc":
+ return self.np.trace(state)
+ return self.tf.norm(state, ord=order)
+
+ def calculate_eigenvalues(self, matrix, k=6, hermitian=True):
+ if hermitian:
+ return self.tf.linalg.eigvalsh(matrix)
+ return self.tf.linalg.eigvals(matrix)
+
+ def calculate_eigenvectors(self, matrix, k=6, hermitian=True):
+ if hermitian:
+ return self.tf.linalg.eigh(matrix)
+ return self.tf.linalg.eig(matrix)
+
+ def calculate_matrix_exp(self, a, matrix, eigenvectors=None, eigenvalues=None):
+ if eigenvectors is None or self.is_sparse(matrix):
+ return self.tf.linalg.expm(-1j * a * matrix)
+ return super().calculate_matrix_exp(a, matrix, eigenvectors, eigenvalues)
+
+ def calculate_hamiltonian_matrix_product(self, matrix1, matrix2):
+ if self.is_sparse(matrix1) or self.is_sparse(matrix2):
+ raise_error(
+ NotImplementedError,
+ "Multiplication of sparse matrices is not supported with Tensorflow.",
+ )
+ return super().calculate_hamiltonian_matrix_product(matrix1, matrix2)
+
+ def calculate_hamiltonian_state_product(self, matrix, state):
+ rank = len(tuple(state.shape))
+ if rank == 1: # vector
+ return self.np.matmul(matrix, state[:, self.np.newaxis])[:, 0]
+ elif rank == 2: # matrix
+ return self.np.matmul(matrix, state)
+ else:
+ raise_error(
+ ValueError,
+ f"Cannot multiply Hamiltonian with rank-{rank} tensor.",
+ )
+
+ def _test_regressions(self, name):
+ if name == "test_measurementresult_apply_bitflips":
+ return [
+ [4, 0, 0, 1, 0, 0, 1, 0, 0, 0],
+ [0, 1, 1, 2, 1, 1, 4, 0, 0, 4],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 4, 0, 0, 0, 4],
+ ]
+ elif name == "test_probabilistic_measurement":
+ if "GPU" in self.device: # pragma: no cover
+ return {0: 273, 1: 233, 2: 242, 3: 252}
+ else:
+ return {0: 271, 1: 239, 2: 242, 3: 248}
+ elif name == "test_unbalanced_probabilistic_measurement":
+ if "GPU" in self.device: # pragma: no cover
+ return {0: 196, 1: 153, 2: 156, 3: 495}
+ else:
+ return {0: 168, 1: 188, 2: 154, 3: 490}
+ elif name == "test_post_measurement_bitflips_on_circuit":
+ return [
+ {5: 30},
+ {5: 12, 7: 6, 4: 6, 1: 5, 6: 1},
+ {3: 7, 6: 4, 2: 4, 7: 4, 0: 4, 5: 3, 4: 2, 1: 2},
+ ]
diff --git a/src/qibo/callbacks.py b/src/qibo/callbacks.py
new file mode 100644
index 000000000..2313e42e9
--- /dev/null
+++ b/src/qibo/callbacks.py
@@ -0,0 +1,358 @@
+from typing import List, Optional, Union
+
+from qibo.config import raise_error
+
+
+class Callback:
+ """Base callback class.
+
+ Results of a callback can be accessed by indexing the corresponding object.
+ """
+
+ def __init__(self):
+ self._results = []
+ self._nqubits = None
+
+ @property
+ def nqubits(self): # pragma: no cover
+ """Total number of qubits in the circuit that the callback was added in."""
+ # abstract method
+ return self._nqubits
+
+ @nqubits.setter
+ def nqubits(self, n: int): # pragma: no cover
+ # abstract method
+ self._nqubits = n
+
+ @property
+ def results(self):
+ return self._results
+
+ def append(self, x):
+ self._results.append(x)
+
+ def extend(self, x):
+ self._results.extend(x)
+
+ def __getitem__(self, k):
+ if not isinstance(k, (int, slice, list, tuple)):
+ raise_error(IndexError, f"Unrecognized type for index {k}.")
+
+ if isinstance(k, int) and k >= len(self._results):
+ raise_error(
+ IndexError,
+ f"Attempting to access callbacks {k} run but "
+ + f"the callback has been used in {len(self._results)} executions.",
+ )
+
+ return self._results[k]
+
+ def apply(self, backend, state): # pragma: no cover
+ pass
+
+ def apply_density_matrix(self, backend, state): # pragma: no cover
+ pass
+
+
+class EntanglementEntropy(Callback):
+ """Von Neumann entanglement entropy callback.
+
+ .. math::
+ S = \\mathrm{Tr} \\left ( \\rho \\log _2 \\rho \\right )
+
+ Args:
+ partition (list): List with qubit ids that defines the first subsystem
+ for the entropy calculation.
+ If `partition` is not given then the first subsystem is the first
+ half of the qubits.
+ compute_spectrum (bool): Compute the entanglement spectrum. Default is False.
+
+ Example:
+ .. testcode::
+
+ from qibo import models, gates, callbacks
+ # create entropy callback where qubit 0 is the first subsystem
+ entropy = callbacks.EntanglementEntropy([0], compute_spectrum=True)
+ # initialize circuit with 2 qubits and add gates
+ c = models.Circuit(2)
+ # add callback gates between normal gates
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.H(0))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ # execute the circuit
+ final_state = c()
+ print(entropy[:])
+ # Should print [0, 0, 1] which is the entanglement entropy
+ # after every gate in the calculation.
+ print(entropy.spectrum)
+ # Print the entanglement spectrum.
+ .. testoutput::
+ :hide:
+
+ ...
+ """
+
+ def __init__(
+ self,
+ partition: Optional[List[int]] = None,
+ compute_spectrum: bool = False,
+ base: float = 2,
+ check_hermitian: bool = False,
+ ):
+ super().__init__()
+ self.partition = partition
+ self.compute_spectrum = compute_spectrum
+ self.base = base
+ self.check_hermitian = check_hermitian
+ self.spectrum = list()
+
+ @Callback.nqubits.setter
+ def nqubits(self, n: int):
+ if self._nqubits is not None and self._nqubits != n:
+ raise_error(
+ RuntimeError,
+ f"Changing EntanglementEntropy nqubits from {self._nqubits} to {n}.",
+ )
+ self._nqubits = n
+ if self.partition is None:
+ self.partition = list(range(n // 2 + n % 2))
+ if len(self.partition) <= self._nqubits // 2:
+ self.partition = [
+ i for i in range(self._nqubits) if i not in set(self.partition)
+ ]
+
+ def apply(self, backend, state):
+ from qibo.quantum_info.entropies import entanglement_entropy
+
+ entropy, spectrum = entanglement_entropy(
+ state,
+ bipartition=self.partition,
+ base=self.base,
+ check_hermitian=self.check_hermitian,
+ return_spectrum=True,
+ backend=backend,
+ )
+ self.append(entropy)
+ if self.compute_spectrum:
+ self.spectrum.append(spectrum)
+ return entropy
+
+ def apply_density_matrix(self, backend, state):
+ return self.apply(backend, state)
+
+
+class State(Callback):
+ """Callback to keeps track of the full state during circuit execution.
+
+ Warning: Keeping many copies of states in memory requires a lot of memory
+ for circuits with many qubits.
+
+ Args:
+ copy (bool): If ``True`` the state vector or density matrix is
+ copied in memory. Otherwise a reference to the existing array
+ is stored in the callback.
+ The callback will not work as expected if ``copy=False``
+ is used with a backend that performs in-place updates,
+ such as qibojit.
+ Default is True
+ """
+
+ def __init__(self, copy=True):
+ super().__init__()
+ self.copy = copy
+
+ def apply(self, backend, state):
+ self.append(backend.cast(state, copy=self.copy))
+ return state
+
+ def apply_density_matrix(self, backend, state):
+ self.append(backend.cast(state, copy=self.copy))
+ return state
+
+
+class Norm(Callback):
+ """State norm callback.
+
+ .. math::
+ \\mathrm{Norm} = \\left \\langle \\Psi | \\Psi \\right \\rangle
+ = \\mathrm{Tr} (\\rho )
+ """
+
+ def apply(self, backend, state):
+ norm = backend.calculate_norm(state)
+ self.append(norm)
+ return norm
+
+ def apply_density_matrix(self, backend, state):
+ norm = backend.calculate_norm_density_matrix(state)
+ self.append(norm)
+ return norm
+
+
+class Overlap(Callback):
+ """State overlap callback.
+
+ Calculates the overlap between the circuit state and a given target state:
+
+ .. math::
+ \\mathrm{Overlap} = |\\left \\langle \\Phi | \\Psi \\right \\rangle |
+
+ Args:
+ state (np.ndarray): Target state to calculate overlap with.
+ normalize (bool): If ``True`` the states are normalized for the overlap
+ calculation.
+ """
+
+ def __init__(self, state):
+ super().__init__()
+ self.state = state
+
+ def apply(self, backend, state):
+ overlap = backend.calculate_overlap(self.state, state)
+ self.append(overlap)
+ return overlap
+
+ def apply_density_matrix(self, backend, state):
+ overlap = backend.calculate_overlap_density_matrix(self.state, state)
+ self.append(overlap)
+ return overlap
+
+
+class Energy(Callback):
+ """Energy expectation value callback.
+
+ Calculates the expectation value of a given Hamiltonian as:
+
+ .. math::
+ \\left \\langle H \\right \\rangle =
+ \\left \\langle \\Psi | H | \\Psi \\right \\rangle
+ = \\mathrm{Tr} (\\rho H)
+
+ assuming that the state is normalized.
+
+ Args:
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): Hamiltonian
+ object to calculate its expectation value.
+ """
+
+ def __init__(self, hamiltonian: "hamiltonians.Hamiltonian"):
+ super().__init__()
+ self.hamiltonian = hamiltonian
+
+ def apply(self, backend, state):
+ assert type(self.hamiltonian.backend) == type(backend)
+ expectation = self.hamiltonian.expectation(state)
+ self.append(expectation)
+ return expectation
+
+ def apply_density_matrix(self, backend, state):
+ assert type(self.hamiltonian.backend) == type(backend)
+ expectation = self.hamiltonian.expectation(state)
+ self.append(expectation)
+ return expectation
+
+
+class Gap(Callback):
+ """Callback for calculating the gap of adiabatic evolution Hamiltonians.
+
+ Can also be used to calculate the Hamiltonian eigenvalues at each time step
+ during the evolution.
+ Note that this callback can only be added in
+ :class:`qibo.evolution.AdiabaticEvolution` models.
+
+ Args:
+ mode (str/int): Defines which quantity this callback calculates.
+ If ``mode == 'gap'`` then the difference between ground state and
+ first excited state energy (gap) is calculated.
+ If ``mode`` is an integer, then the energy of the corresponding
+ eigenstate is calculated.
+ check_degenerate (bool): If ``True`` the excited state number is
+ increased until a non-zero gap is found. This is used to find the
+ proper gap in the case of degenerate Hamiltonians.
+ This flag is relevant only if ``mode`` is ``'gap'``.
+ Default is ``True``.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import callbacks, hamiltonians
+ from qibo.models import AdiabaticEvolution
+ # define easy and hard Hamiltonians for adiabatic evolution
+ h0 = hamiltonians.X(3)
+ h1 = hamiltonians.TFIM(3, h=1.0)
+ # define callbacks for logging the ground state, first excited
+ # and gap energy
+ ground = callbacks.Gap(0)
+ excited = callbacks.Gap(1)
+ gap = callbacks.Gap()
+ # define and execute the ``AdiabaticEvolution`` model
+ evolution = AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-1,
+ callbacks=[gap, ground, excited])
+ final_state = evolution(final_time=1.0)
+ # print results
+ print(ground[:])
+ print(excited[:])
+ print(gap[:])
+ .. testoutput::
+ :hide:
+
+ ...
+ """
+
+ def __init__(self, mode: Union[str, int] = "gap", check_degenerate: bool = True):
+ super().__init__()
+ if not isinstance(mode, (int, str)):
+ raise_error(
+ TypeError,
+ f"Gap callback mode should be integer or string but is {type(mode)}.",
+ )
+ elif isinstance(mode, str) and mode != "gap":
+ raise_error(ValueError, f"Unsupported mode {mode} for gap callback.")
+ self.mode = mode
+ self.check_degenerate = check_degenerate
+ self.evolution = None
+
+ def apply(self, backend, state):
+ from qibo.config import EIGVAL_CUTOFF, log
+
+ if self.evolution is None:
+ raise_error(
+ RuntimeError,
+ "Gap callback can only be used in " "adiabatic evolution models.",
+ )
+ hamiltonian = self.evolution.solver.current_hamiltonian # pylint: disable=E1101
+ assert type(hamiltonian.backend) == type(backend)
+ # Call the eigenvectors so that they are cached for the ``exp`` call
+ hamiltonian.eigenvectors()
+ eigvals = hamiltonian.eigenvalues()
+ if isinstance(self.mode, int):
+ gap = backend.np.real(eigvals[self.mode])
+ self.append(gap)
+ return gap
+
+ # case: self.mode == "gap"
+ excited = 1
+ gap = backend.np.real(eigvals[excited] - eigvals[0])
+
+ if not self.check_degenerate:
+ self.append(gap)
+ return gap
+
+ while backend.np.less(gap, EIGVAL_CUTOFF):
+ gap = backend.np.real(eigvals[excited] - eigvals[0])
+ excited += 1
+ if excited > 1:
+ log.warning(
+ f"The Hamiltonian is degenerate. Using eigenvalue {excited} to calculate gap."
+ )
+ self.append(gap)
+ return gap
+
+ def apply_density_matrix(self, backend, state):
+ raise_error(
+ NotImplementedError,
+ "Gap callback is not implemented for " "density matrices.",
+ )
diff --git a/src/qibo/config.py b/src/qibo/config.py
new file mode 100644
index 000000000..015c2a7a5
--- /dev/null
+++ b/src/qibo/config.py
@@ -0,0 +1,96 @@
+"""
+Define the default circuit, constants and types.
+"""
+
+import logging
+import os
+
+# Logging level from 0 (all) to 4 (errors) (see https://docs.python.org/3/library/logging.html#logging-levels)
+QIBO_LOG_LEVEL = 1
+if "QIBO_LOG_LEVEL" in os.environ: # pragma: no cover
+ QIBO_LOG_LEVEL = 10 * int(os.environ.get("QIBO_LOG_LEVEL"))
+
+# Logging level from 0 (all) to 3 (errors) for TensorFlow
+TF_LOG_LEVEL = 3
+if "TF_LOG_LEVEL" in os.environ: # pragma: no cover
+ TF_LOG_LEVEL = int(os.environ.get("TF_LOG_LEVEL"))
+
+# characters used in einsum strings
+EINSUM_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+# Entanglement entropy eigenvalue cut-off
+# Eigenvalues smaller than this cut-off are ignored in entropy calculation
+EIGVAL_CUTOFF = 1e-14
+
+# Tolerance for the probability sum check in the unitary channel
+PRECISION_TOL = 1e-8
+
+# Batch size for sampling shots in measurement frequencies calculation
+SHOT_BATCH_SIZE = 2**18
+
+# Threshold size for sampling shots in measurements frequencies with custom operator
+SHOT_METROPOLIS_THRESHOLD = 100000
+
+# Max iterations for normalizing bistochastic matrices
+MAX_ITERATIONS = 50
+
+
+def raise_error(exception, message=None):
+ """Raise exception with logging error.
+
+ Args:
+ exception (Exception): python exception.
+ message (str): the error message.
+ """
+ log.error(message)
+ raise exception(message)
+
+
+def get_batch_size():
+ """Returns batch size used for sampling measurement shots."""
+ return SHOT_BATCH_SIZE
+
+
+def set_batch_size(batch_size):
+ """Sets batch size used for sampling measurement shots."""
+ if not isinstance(batch_size, int):
+ raise_error(TypeError, "Shot batch size must be integer.")
+ elif batch_size < 1:
+ raise_error(ValueError, "Shot batch size must be a positive integer.")
+ elif batch_size > 2**31:
+ raise_error(ValueError, "Shot batch size cannot be greater than 2^31.")
+ global SHOT_BATCH_SIZE
+ SHOT_BATCH_SIZE = batch_size
+
+
+def get_metropolis_threshold():
+ """Returns threshold for using Metropolis algorithm for sampling measurement shots."""
+ return SHOT_METROPOLIS_THRESHOLD
+
+
+def set_metropolis_threshold(threshold):
+ """Sets threshold for using Metropolis algorithm for sampling measurement shots."""
+ if not isinstance(threshold, int):
+ raise_error(TypeError, "Shot threshold must be integer.")
+ elif threshold < 1:
+ raise_error(ValueError, "Shot threshold be a positive integer.")
+ global SHOT_METROPOLIS_THRESHOLD
+ SHOT_METROPOLIS_THRESHOLD = threshold
+
+
+# Configuration for logging mechanism
+class CustomHandler(logging.StreamHandler):
+ """Custom handler for logging algorithm."""
+
+ def format(self, record):
+ """Format the record with specific format."""
+ from qibo import __version__
+
+ fmt = f"[Qibo {__version__}|%(levelname)s|%(asctime)s]: %(message)s"
+ return logging.Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S").format(record)
+
+
+# allocate logger object
+log = logging.getLogger(__name__)
+log.setLevel(QIBO_LOG_LEVEL)
+log.addHandler(CustomHandler())
diff --git a/src/qibo/derivative.py b/src/qibo/derivative.py
new file mode 100644
index 000000000..65d4ef87b
--- /dev/null
+++ b/src/qibo/derivative.py
@@ -0,0 +1,230 @@
+import numpy as np
+
+from qibo.config import raise_error
+from qibo.hamiltonians.abstract import AbstractHamiltonian
+
+
+def parameter_shift(
+ circuit,
+ hamiltonian,
+ parameter_index,
+ initial_state=None,
+ scale_factor=1,
+ nshots=None,
+):
+ """In this method the parameter shift rule (PSR) is implemented.
+ Given a circuit U and an observable H, the PSR allows to calculate the derivative
+ of the expected value of H on the final state with respect to a variational
+ parameter of the circuit.
+ There is also the possibility of setting a scale factor. It is useful when a
+ circuit's parameter is obtained by combination of a variational
+ parameter and an external object, such as a training variable in a Quantum
+ Machine Learning problem. For example, performing a re-uploading strategy
+ to embed some data into a circuit, we apply to the quantum state rotations
+ whose angles are in the form: theta' = theta * x, where theta is a variational
+ parameter and x an input variable. The PSR allows to calculate the derivative
+ with respect of theta' but, if we want to optimize a system with respect its
+ variational parameters we need to "free" this procedure from the x depencency.
+ If the `scale_factor` is not provided, it is set equal to one and doesn't
+ affect the calculation.
+ If the PSR is needed to be executed on a real quantum device, it is important
+ to set `nshots` to some integer value. This enables the execution on the
+ hardware by calling the proper methods.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): custom quantum circuit.
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): target observable.
+ if you want to execute on hardware, a symbolic hamiltonian must be
+ provided as follows (example with Pauli Z and ``nqubits=1``):
+ ``SymbolicHamiltonian(np.prod([ Z(i) for i in range(1) ]))``.
+ parameter_index (int): the index which identifies the target parameter
+ in the ``circuit.get_parameters()`` list.
+ initial_state (ndarray, optional): initial state on which the circuit
+ acts. Default is ``None``.
+ scale_factor (float, optional): parameter scale factor. Default is ``1``.
+ nshots (int, optional): number of shots if derivative is evaluated on
+ hardware. If ``None``, the simulation mode is executed.
+ Default is ``None``.
+
+ Returns:
+ (float): Value of the derivative of the expectation value of the hamiltonian
+ with respect to the target variational parameter.
+
+ Example:
+
+ .. testcode::
+
+ import qibo
+ import numpy as np
+ from qibo import Circuit, gates, hamiltonians
+ from qibo.derivative import parameter_shift
+
+ # defining an observable
+ def hamiltonian(nqubits = 1):
+ m0 = (1/nqubits)*hamiltonians.Z(nqubits).matrix
+ ham = hamiltonians.Hamiltonian(nqubits, m0)
+
+ return ham
+
+ # defining a dummy circuit
+ def circuit(nqubits = 1):
+ c = Circuit(nqubits = 1)
+ c.add(gates.RY(q = 0, theta = 0))
+ c.add(gates.RX(q = 0, theta = 0))
+ c.add(gates.M(0))
+
+ return c
+
+ # initializing the circuit
+ c = circuit(nqubits = 1)
+
+ # some parameters
+ test_params = np.random.randn(2)
+ c.set_parameters(test_params)
+
+ test_hamiltonian = hamiltonian()
+
+ # running the psr with respect to the two parameters
+ grad_0 = parameter_shift(circuit=c, hamiltonian=test_hamiltonian, parameter_index=0)
+ grad_1 = parameter_shift(circuit=c, hamiltonian=test_hamiltonian, parameter_index=1)
+
+ """
+
+ # some raise_error
+ if parameter_index > len(circuit.get_parameters()):
+ raise_error(ValueError, """This index is out of bounds.""")
+
+ if not isinstance(hamiltonian, AbstractHamiltonian):
+ raise_error(
+ TypeError,
+ "hamiltonian must be a qibo.hamiltonians.Hamiltonian or qibo.hamiltonians.SymbolicHamiltonian object",
+ )
+
+ # inheriting hamiltonian's backend
+ backend = hamiltonian.backend
+
+ # getting the gate's type
+ gate = circuit.associate_gates_with_parameters()[parameter_index]
+
+ # getting the generator_eigenvalue
+ generator_eigenval = gate.generator_eigenvalue()
+
+ # defining the shift according to the psr
+ s = np.pi / (4 * generator_eigenval)
+
+ # saving original parameters and making a copy
+ original = np.asarray(circuit.get_parameters()).copy()
+ shifted = original.copy()
+
+ # forward shift
+ shifted[parameter_index] += s
+ circuit.set_parameters(shifted)
+
+ if nshots is None:
+ # forward evaluation
+ forward = hamiltonian.expectation(
+ backend.execute_circuit(
+ circuit=circuit, initial_state=initial_state
+ ).state()
+ )
+
+ # backward shift and evaluation
+ shifted[parameter_index] -= 2 * s
+ circuit.set_parameters(shifted)
+
+ backward = hamiltonian.expectation(
+ backend.execute_circuit(
+ circuit=circuit, initial_state=initial_state
+ ).state()
+ )
+
+ # same but using expectation from samples
+ else:
+ forward = backend.execute_circuit(
+ circuit=circuit, initial_state=initial_state, nshots=nshots
+ ).expectation_from_samples(hamiltonian)
+
+ shifted[parameter_index] -= 2 * s
+ circuit.set_parameters(shifted)
+
+ backward = backend.execute_circuit(
+ circuit=circuit, initial_state=initial_state, nshots=nshots
+ ).expectation_from_samples(hamiltonian)
+
+ circuit.set_parameters(original)
+
+ # float() necessary to not return a 0-dim ndarray
+ result = float(generator_eigenval * (forward - backward) * scale_factor)
+
+ return result
+
+
+def finite_differences(
+ circuit,
+ hamiltonian,
+ parameter_index,
+ initial_state=None,
+ step_size=1e-7,
+):
+ """
+ Calculate derivative of the expectation value of `hamiltonian` on the
+ final state obtained by executing `circuit` on `initial_state` with
+ respect to the variational parameter identified by `parameter_index`
+ in the circuit's parameters list. This method can be used only in
+ exact simulation mode.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): custom quantum circuit.
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): target observable.
+ if you want to execute on hardware, a symbolic hamiltonian must be
+ provided as follows (example with Pauli Z and ``nqubits=1``):
+ ``SymbolicHamiltonian(np.prod([ Z(i) for i in range(1) ]))``.
+ parameter_index (int): the index which identifies the target parameter
+ in the ``circuit.get_parameters()`` list.
+ initial_state (ndarray, optional): initial state on which the circuit
+ acts. Default is ``None``.
+ step_size (float): step size used to evaluate the finite difference
+ (default 1e-7).
+
+ Returns:
+ (float): Value of the derivative of the expectation value of the hamiltonian
+ with respect to the target variational parameter.
+ """
+
+ if parameter_index > len(circuit.get_parameters()):
+ raise_error(ValueError, f"""Index {parameter_index} is out of bounds.""")
+
+ if not isinstance(hamiltonian, AbstractHamiltonian):
+ raise_error(
+ TypeError,
+ "hamiltonian must be a qibo.hamiltonians.Hamiltonian or qibo.hamiltonians.SymbolicHamiltonian object",
+ )
+
+ backend = hamiltonian.backend
+
+ # parameters copies
+ parameters = np.asarray(circuit.get_parameters()).copy()
+ shifted = parameters.copy()
+
+ # shift the parameter_index element
+ shifted[parameter_index] += step_size
+ circuit.set_parameters(shifted)
+
+ # forward evaluation
+ forward = hamiltonian.expectation(
+ backend.execute_circuit(circuit=circuit, initial_state=initial_state).state()
+ )
+
+ # backward shift and evaluation
+ shifted[parameter_index] -= 2 * step_size
+ circuit.set_parameters(shifted)
+
+ backward = hamiltonian.expectation(
+ backend.execute_circuit(circuit=circuit, initial_state=initial_state).state()
+ )
+
+ circuit.set_parameters(parameters)
+
+ result = (forward - backward) / (2 * step_size)
+
+ return result
diff --git a/src/qibo/gates/__init__.py b/src/qibo/gates/__init__.py
new file mode 100644
index 000000000..9dd06e6c9
--- /dev/null
+++ b/src/qibo/gates/__init__.py
@@ -0,0 +1,4 @@
+from qibo.gates.channels import *
+from qibo.gates.gates import *
+from qibo.gates.measurements import *
+from qibo.gates.special import *
diff --git a/src/qibo/gates/abstract.py b/src/qibo/gates/abstract.py
new file mode 100644
index 000000000..023aaac48
--- /dev/null
+++ b/src/qibo/gates/abstract.py
@@ -0,0 +1,517 @@
+import collections
+import json
+from typing import List, Sequence, Tuple
+
+import sympy
+
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+
+REQUIRED_FIELDS = [
+ "name",
+ "init_args",
+ "init_kwargs",
+ "_target_qubits",
+ "_control_qubits",
+]
+REQUIRED_FIELDS_INIT_KWARGS = ["theta", "phi", "lam", "phi0", "phi1"]
+
+
+class Gate:
+ """The base class for gate implementation.
+
+ All base gates should inherit this class.
+
+ Attributes:
+ name (str): Name of the gate.
+ draw_label (str): Optional label for drawing the gate in a circuit
+ with :func:`qibo.models.Circuit.draw`.
+ is_controlled_by (bool): ``True`` if the gate was created using the
+ :meth:`qibo.gates.abstract.Gate.controlled_by` method,
+ otherwise ``False``.
+ init_args (list): Arguments used to initialize the gate.
+ init_kwargs (dict): Arguments used to initialize the gate.
+ target_qubits (tuple): Tuple with ids of target qubits.
+ control_qubits (tuple): Tuple with ids of control qubits sorted in
+ increasing order.
+ """
+
+ def __init__(self):
+ from qibo import config
+
+ self.name = None
+ self.draw_label = None
+ self.is_controlled_by = False
+ # args for creating gate
+ self.init_args = []
+ self.init_kwargs = {}
+
+ self.unitary = False
+ self._target_qubits = ()
+ self._control_qubits = ()
+ self._parameters = ()
+ config.ALLOW_SWITCHERS = False
+
+ self.symbolic_parameters = {}
+
+ # for distributed circuits
+ self.device_gates = set()
+ self.original_gate = None
+
+ @property
+ def clifford(self):
+ """Return boolean value representing if a Gate is Clifford or not."""
+ return False
+
+ @property
+ def raw(self) -> dict:
+ """Serialize to dictionary.
+
+ The values used in the serialization should be compatible with a
+ JSON dump (or any other one supporting a minimal set of scalar
+ types). Though the specific implementation is up to the specific
+ gate.
+ """
+ encoded = self.__dict__
+
+ encoded_simple = {
+ key: value for key, value in encoded.items() if key in REQUIRED_FIELDS
+ }
+
+ encoded_simple["init_kwargs"] = {
+ key: value
+ for key, value in encoded_simple["init_kwargs"].items()
+ if key in REQUIRED_FIELDS_INIT_KWARGS
+ }
+
+ encoded_simple["_class"] = type(self).__name__
+
+ return encoded_simple
+
+ @staticmethod
+ def from_dict(raw: dict):
+ """Load from serialization.
+
+ Essentially the counter-part of :meth:`raw`.
+ """
+ from qibo.gates import gates, measurements
+
+ for mod in (gates, measurements):
+ try:
+ cls = getattr(mod, raw["_class"])
+ break
+ except AttributeError:
+ # gate not found in given module, try next
+ pass
+ else:
+ raise ValueError(f"Unknown gate {raw['_class']}")
+
+ gate = cls(*raw["init_args"], **raw["init_kwargs"])
+ try:
+ return gate.controlled_by(*raw["_control_qubits"])
+ except RuntimeError as e:
+ if "controlled" in e.args[0]:
+ return gate
+ raise e
+
+ def to_json(self):
+ """Dump gate to JSON.
+
+ Note:
+ Consider using :meth:`raw` directly.
+ """
+ return json.dumps(self.raw)
+
+ @property
+ def target_qubits(self) -> Tuple[int]:
+ """Tuple with ids of target qubits."""
+ return self._target_qubits
+
+ @property
+ def control_qubits(self) -> Tuple[int]:
+ """Tuple with ids of control qubits sorted in increasing order."""
+ return tuple(sorted(self._control_qubits))
+
+ @property
+ def qubits(self) -> Tuple[int]:
+ """Tuple with ids of all qubits (control and target) that the gate acts."""
+ return self.control_qubits + self.target_qubits
+
+ @property
+ def qasm_label(self):
+ """String corresponding to OpenQASM operation of the gate."""
+ raise_error(
+ NotImplementedError,
+ f"{self.__class__.__name__} is not supported by OpenQASM",
+ )
+
+ def _set_target_qubits(self, qubits: Sequence[int]):
+ """Helper method for setting target qubits."""
+ self._target_qubits = tuple(qubits)
+ if len(self._target_qubits) != len(set(qubits)):
+ repeated = self._find_repeated(qubits)
+ raise_error(
+ ValueError,
+ f"Target qubit {repeated} was given twice for gate {self.__class__.__name__}.",
+ )
+
+ def _set_control_qubits(self, qubits: Sequence[int]):
+ """Helper method for setting control qubits."""
+ if len(set(qubits)) != len(qubits):
+ repeated = self._find_repeated(qubits)
+ raise_error(
+ ValueError,
+ f"Control qubit {repeated} was given twice for gate {self.__class__.__name__}.",
+ )
+ self._control_qubits = qubits
+
+ @target_qubits.setter
+ def target_qubits(self, qubits: Sequence[int]):
+ """Sets target qubits tuple."""
+ self._set_target_qubits(qubits)
+ self._check_control_target_overlap()
+
+ @control_qubits.setter
+ def control_qubits(self, qubits: Sequence[int]):
+ """Sets control qubits set."""
+ self._set_control_qubits(qubits)
+ self._check_control_target_overlap()
+
+ def _set_targets_and_controls(
+ self, target_qubits: Sequence[int], control_qubits: Sequence[int]
+ ):
+ """Sets target and control qubits simultaneously.
+
+ This is used for the reduced qubit updates in the distributed
+ circuits because using the individual setters may raise errors
+ due to temporary overlap of control and target qubits.
+ """
+ self._set_target_qubits(target_qubits)
+ self._set_control_qubits(control_qubits)
+ self._check_control_target_overlap()
+
+ @staticmethod
+ def _find_repeated(qubits: Sequence[int]) -> int:
+ """Finds the first qubit id that is repeated in a sequence of qubit ids."""
+ temp_set = set()
+ for qubit in qubits:
+ if qubit in temp_set:
+ return qubit
+ temp_set.add(qubit)
+
+ def _check_control_target_overlap(self):
+ """Checks that there are no qubits that are both target and
+ controls."""
+ control_and_target = self._control_qubits + self._target_qubits
+ common = len(set(control_and_target)) != len(control_and_target)
+ if common:
+ raise_error(
+ ValueError,
+ f"{set(self._target_qubits) & set(self._control_qubits)}"
+ + "qubits are both targets and controls "
+ + f"for gate {self.__class__.__name__}.",
+ )
+
+ @property
+ def parameters(self):
+ """Returns a tuple containing the current value of gate's parameters."""
+ return self._parameters
+
+ def commutes(self, gate: "Gate") -> bool:
+ """Checks if two gates commute.
+
+ Args:
+ gate: Gate to check if it commutes with the current gate.
+
+ Returns:
+ bool: ``True`` if the gates commute, ``False`` otherwise.
+ """
+ if isinstance(gate, SpecialGate): # pragma: no cover
+ return False
+ t1 = set(self.target_qubits)
+ t2 = set(gate.target_qubits)
+ a = self.__class__ == gate.__class__ and t1 == t2
+ b = not (t1 & set(gate.qubits) or t2 & set(self.qubits))
+ return a or b
+
+ def on_qubits(self, qubit_map) -> "Gate":
+ """Creates the same gate targeting different qubits.
+
+ Args:
+ qubit_map (int): Dictionary mapping original qubit indices to new ones.
+
+ Returns:
+ A :class:`qibo.gates.Gate` object of the original gate
+ type targeting the given qubits.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import models, gates
+ c = models.Circuit(4)
+ # Add some CNOT gates
+ c.add(gates.CNOT(2, 3).on_qubits({2: 2, 3: 3})) # equivalent to gates.CNOT(2, 3)
+ c.add(gates.CNOT(2, 3).on_qubits({2: 3, 3: 0})) # equivalent to gates.CNOT(3, 0)
+ c.add(gates.CNOT(2, 3).on_qubits({2: 1, 3: 3})) # equivalent to gates.CNOT(1, 3)
+ c.add(gates.CNOT(2, 3).on_qubits({2: 2, 3: 1})) # equivalent to gates.CNOT(2, 1)
+ print(c.draw())
+ .. testoutput::
+
+ q0: ───X─────
+ q1: ───|─o─X─
+ q2: ─o─|─|─o─
+ q3: ─X─o─X───
+ """
+ if self.is_controlled_by:
+ targets = (qubit_map.get(q) for q in self.target_qubits)
+ controls = (qubit_map.get(q) for q in self.control_qubits)
+ gate = self.__class__(*targets, **self.init_kwargs)
+ gate = gate.controlled_by(*controls)
+ else:
+ qubits = (qubit_map.get(q) for q in self.qubits)
+ gate = self.__class__(*qubits, **self.init_kwargs)
+ return gate
+
+ def _dagger(self) -> "Gate":
+ """Helper method for :meth:`qibo.gates.Gate.dagger`."""
+ # By default the ``_dagger`` method creates an equivalent gate, assuming
+ # that the gate is Hermitian (true for common gates like H or Paulis).
+ # If the gate is not Hermitian the ``_dagger`` method should be modified.
+ return self.__class__(*self.init_args, **self.init_kwargs)
+
+ def dagger(self) -> "Gate":
+ """Returns the dagger (conjugate transpose) of the gate.
+
+ Note that dagger is not persistent for parametrized gates.
+ For example, applying a dagger to an :class:`qibo.gates.gates.RX` gate
+ will change the sign of its parameter at the time of application.
+ However, if the parameter is updated after that, for example using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`, then the
+ action of dagger will be lost.
+
+ Returns:
+ :class:`qibo.gates.Gate`: object representing the dagger of the original gate.
+ """
+ new_gate = self._dagger()
+ new_gate.is_controlled_by = self.is_controlled_by
+ new_gate.control_qubits = self.control_qubits
+ return new_gate
+
+ def check_controls(func): # pylint: disable=E0213
+ def wrapper(self, *args):
+ if self.control_qubits:
+ raise_error(
+ RuntimeError,
+ "Cannot use `controlled_by` method "
+ + f"on gate {self} because it is already "
+ + f"controlled by {self.control_qubits}.",
+ )
+ return func(self, *args) # pylint: disable=E1102
+
+ return wrapper
+
+ @check_controls
+ def controlled_by(self, *qubits: int) -> "Gate":
+ """Controls the gate on (arbitrarily many) qubits.
+
+ To see how this method affects the underlying matrix representation of a gate,
+ please see the documentation of :meth:`qibo.gates.Gate.matrix`.
+
+ .. note::
+ Some gate classes default to another gate class depending on the number of controls
+ present. For instance, an :math:`1`-controlled :class:`qibo.gates.X` gate
+ will default to a :class:`qibo.gates.CNOT` gate, while a :math:`2`-controlled
+ :class:`qibo.gates.X` gate defaults to a :class:`qibo.gates.TOFFOLI` gate.
+ Other gates affected by this method are: :class:`qibo.gates.Y`, :class:`qibo.gates.Z`,
+ :class:`qibo.gates.RX`, :class:`qibo.gates.RY`, :class:`qibo.gates.RZ`,
+ :class:`qibo.gates.U1`, :class:`qibo.gates.U2`, and :class:`qibo.gates.U3`.
+
+ Args:
+ *qubits (int): Ids of the qubits that the gate will be controlled on.
+
+ Returns:
+ :class:`qibo.gates.Gate`: object in with the corresponding
+ gate being controlled in the given qubits.
+ """
+ if qubits:
+ self.is_controlled_by = True
+ self.control_qubits = qubits
+ return self
+
+ def decompose(self, *free) -> List["Gate"]:
+ """Decomposes multi-control gates to gates supported by OpenQASM.
+
+ Decompositions are based on `arXiv:9503016 `_.
+
+ Args:
+ free: Ids of free qubits to use for the gate decomposition.
+
+ Returns:
+ list: gates that have the same effect as applying the original gate.
+ """
+ # TODO: Implement this method for all gates not supported by OpenQASM.
+ # Currently this is implemented only for multi-controlled X gates.
+ # If it is used on a different gate it will just return a deep copy
+ # of the same gate.
+ return [self.__class__(*self.init_args, **self.init_kwargs)]
+
+ def matrix(self, backend=None):
+ """Returns the matrix representation of the gate.
+
+ If gate has controlled qubits inserted by :meth:`qibo.gates.Gate.controlled_by`,
+ then :meth:`qibo.gates.Gate.matrix` returns the matrix of the original gate.
+
+ .. code-block:: python
+
+ from qibo import gates
+
+ gate = gates.SWAP(3, 4).controlled_by(0, 1, 2)
+ print(gate.matrix())
+
+ To return the full matrix that takes the control qubits into account,
+ one should use :meth:`qibo.models.Circuit.unitary`, e.g.
+
+ .. code-block:: python
+
+ from qibo import Circuit, gates
+
+ nqubits = 5
+ circuit = Circuit(nqubits)
+ circuit.add(gates.SWAP(3, 4).controlled_by(0, 1, 2))
+ print(circuit.unitary())
+
+ Args:
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Matrix representation of gate.
+
+ .. note::
+ ``Gate.matrix`` was defined as an atribute in ``qibo`` versions prior to ``0.2.0``.
+ From ``0.2.0`` on, it has been converted into a method and has replaced the ``asmatrix`` method.
+ """
+ backend = _check_backend(backend)
+
+ return backend.matrix(self)
+
+ def generator_eigenvalue(self):
+ """This function returns the eigenvalues of the gate's generator.
+
+ Returns:
+ float: eigenvalue of the generator.
+ """
+
+ raise_error(
+ NotImplementedError,
+ f"Generator eigenvalue is not implemented for {self.__class__.__name__}",
+ )
+
+ def basis_rotation(self):
+ """Transformation required to rotate the basis for measuring the gate."""
+ raise_error(
+ NotImplementedError,
+ f"Basis rotation is not implemented for {self.__class__.__name__}",
+ )
+
+ def apply(self, backend, state, nqubits):
+ return backend.apply_gate(self, state, nqubits)
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ return backend.apply_gate_density_matrix(self, state, nqubits)
+
+ def apply_clifford(self, backend, state, nqubits):
+ return backend.apply_gate_clifford(self, state, nqubits)
+
+
+class SpecialGate(Gate):
+ """Abstract class for special gates."""
+
+ def commutes(self, gate):
+ return False
+
+ def on_qubits(self, qubit_map):
+ raise_error(NotImplementedError, "Cannot use special gates on subroutines.")
+
+ def matrix(self, backend=None): # pragma: no cover
+ raise_error(
+ NotImplementedError, "Special gates do not have matrix representation."
+ )
+
+
+class ParametrizedGate(Gate):
+ """Base class for parametrized gates.
+
+ Implements the basic functionality of parameter setters and getters.
+ """
+
+ def __init__(self, trainable=True):
+ super().__init__()
+ self.parameter_names = "theta"
+ self.nparams = 1
+ self.trainable = trainable
+
+ @Gate.parameters.setter
+ def parameters(self, x):
+ """Updates the values of gate's parameters."""
+ if isinstance(self.parameter_names, str):
+ nparams = 1
+ names = [self.parameter_names]
+ if not isinstance(x, collections.abc.Iterable):
+ x = [x]
+ else:
+ # Captures the ``Unitary`` gate case where the given parameter
+ # can be an array
+ try:
+ if len(x) != 1: # pragma: no cover
+ x = [x]
+ except TypeError: # tf.Variable case
+ s = tuple(x.shape)
+ if not s or s[0] != 1:
+ x = [x]
+ else:
+ nparams = len(self.parameter_names)
+ names = self.parameter_names
+
+ if not self._parameters:
+ params = nparams * [None]
+ else:
+ params = list(self._parameters)
+ if len(x) != nparams:
+ raise_error(
+ ValueError,
+ f"Parametrized gate has {nparams} parameters "
+ + f"but {len(x)} update values were given.",
+ )
+ for i, v in enumerate(x):
+ if isinstance(v, sympy.Expr):
+ self.symbolic_parameters[i] = v
+ params[i] = v
+ self._parameters = tuple(params)
+ self.init_kwargs.update(
+ {n: v for n, v in zip(names, self._parameters) if n in self.init_kwargs}
+ )
+
+ # set parameters in device gates
+ for gate in self.device_gates: # pragma: no cover
+ gate.parameters = x
+
+ def on_qubits(self, qubit_map):
+ gate = super().on_qubits(qubit_map)
+ gate.parameters = self.parameters
+ return gate
+
+ def substitute_symbols(self):
+ params = list(self._parameters)
+ for i, param in self.symbolic_parameters.items():
+ for symbol in param.free_symbols:
+ param = symbol.evaluate(param)
+ params[i] = float(param)
+ self.parameters = tuple(params)
+
+ def matrix(self, backend=None):
+ backend = _check_backend(backend)
+
+ return backend.matrix_parametrized(self)
diff --git a/src/qibo/gates/channels.py b/src/qibo/gates/channels.py
new file mode 100644
index 000000000..bde8c5027
--- /dev/null
+++ b/src/qibo/gates/channels.py
@@ -0,0 +1,836 @@
+"""Define quantum channels."""
+
+from itertools import product
+from math import exp, sqrt
+from typing import Optional, Tuple
+
+import numpy as np
+
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+from qibo.gates.abstract import Gate
+from qibo.gates.gates import I, Unitary, X, Y, Z
+from qibo.gates.special import FusedGate
+
+
+class Channel(Gate):
+ """Abstract class for channels."""
+
+ def __init__(self):
+ super().__init__()
+ self.coefficients = tuple()
+ self.gates = tuple()
+
+ def controlled_by(self, *q):
+ raise_error(ValueError, f"Noise channel cannot be controlled on qubits {q}.")
+
+ def on_qubits(self, qubit_map): # pragma: no cover
+ # future TODO
+ raise_error(
+ NotImplementedError,
+ "`on_qubits` method is not available for the `Channel` gate.",
+ )
+
+ def apply(self, backend, state, nqubits): # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ f"{self.__class__.__name__} cannot be applied to state vector.",
+ )
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ return backend.apply_channel_density_matrix(self, state, nqubits)
+
+ def apply_clifford(self, backend, state, nqubits):
+ return backend.apply_channel(self, state, nqubits)
+
+ def to_choi(self, nqubits: Optional[int] = None, order: str = "row", backend=None):
+ """Returns the Choi representation :math:`\\mathcal{E}`
+ of the Kraus channel :math:`\\{K_{\\alpha}\\}_{\\alpha}`.
+
+ .. math::
+ \\mathcal{E} = \\sum_{\\alpha} \\, |K_{\\alpha}\\rangle\\rangle
+ \\langle\\langle K_{\\alpha}|
+
+ Args:
+ nqubits (int, optional): total number of qubits to be considered
+ in a channel. Must be equal or greater than ``target_qubits``.
+ If ``None``, defaults to the number of target qubits in the
+ channel. Default is ``None``.
+ order (str, optional): if ``"row"``, vectorization of
+ Kraus operators is performed row-wise. If ``"column"``,
+ vectorization is done column-wise. If ``"system"``,
+ vectorization is done block-wise. Defaut is ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional):
+ backend to be used in the execution. If ``None``,
+ it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ Choi representation of the channel.
+ """
+
+ if nqubits is not None and nqubits < 1 + max(self.target_qubits):
+ raise_error(
+ ValueError,
+ f"nqubits={nqubits}, but channel acts on qubit "
+ + f"with index {max(self.target_qubits)}.",
+ )
+
+ from qibo.quantum_info.superoperator_transformations import ( # pylint: disable=C0415
+ vectorization,
+ )
+
+ backend = _check_backend(backend)
+
+ nqubits = 1 + max(self.target_qubits) if nqubits is None else nqubits
+
+ if type(self) not in [KrausChannel, ReadoutErrorChannel]:
+ p_0 = 1 - sum(self.coefficients)
+ if p_0 > PRECISION_TOL:
+ self.coefficients += (p_0,)
+ self.gates += (I(*self.target_qubits),)
+
+ super_op = np.zeros((4**nqubits, 4**nqubits), dtype=complex)
+ super_op = backend.cast(super_op, dtype=super_op.dtype)
+ for coeff, gate in zip(self.coefficients, self.gates):
+ kraus_op = FusedGate(*range(nqubits))
+ kraus_op.append(gate)
+ kraus_op = kraus_op.matrix(backend)
+ kraus_op = vectorization(kraus_op, order=order, backend=backend)
+ super_op = super_op + coeff * backend.np.outer(
+ kraus_op, backend.np.conj(kraus_op)
+ )
+ del kraus_op
+
+ return super_op
+
+ def to_liouville(self, nqubits: int = None, order: str = "row", backend=None):
+ """Returns the Liouville representation of the channel.
+
+ Args:
+ nqubits (int, optional): total number of qubits to be considered
+ in a channel. Must be equal or greater than ``target_qubits``.
+ If ``None``, defaults to the number of target qubits in the
+ channel. Default is ``None``.
+ order (str, optional): If ``"row"``, vectorization of
+ Kraus operators is performed row-wise. If ``"column"``,
+ vectorization is done column-wise. If ``"system"``,
+ it raises ``NotImplementedError``. Defaut is ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional):
+ backend to be used in the execution. If ``None``,
+ it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ Liouville representation of the channel.
+ """
+
+ from qibo.quantum_info.superoperator_transformations import ( # pylint: disable=C0415
+ choi_to_liouville,
+ )
+
+ backend = _check_backend(backend)
+
+ super_op = self.to_choi(nqubits=nqubits, order=order, backend=backend)
+ super_op = choi_to_liouville(super_op, order=order, backend=backend)
+
+ return super_op
+
+ def to_pauli_liouville(
+ self,
+ nqubits: int = None,
+ normalize: bool = False,
+ pauli_order: str = "IXYZ",
+ backend=None,
+ ):
+ """Returns the Liouville representation of the channel
+ in the Pauli basis.
+
+ Args:
+ nqubits (int, optional): total number of qubits to be considered
+ in a channel. Must be equal or greater than ``target_qubits``.
+ If ``None``, defaults to the number of target qubits in the
+ channel. Default is ``None``.
+ normalize (bool, optional): If ``True``, normalized basis is returned.
+ Defaults to False.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the basis. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ Pauli-Liouville representation of the channel.
+ """
+
+ from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415
+
+ backend = _check_backend(backend)
+
+ super_op = self.to_liouville(nqubits=nqubits, backend=backend)
+
+ if nqubits is None:
+ nqubits = int(np.log2(np.sqrt(super_op.shape[0])))
+
+ # unitary that transforms from comp basis to pauli basis
+ unitary = comp_basis_to_pauli(
+ nqubits, normalize, pauli_order=pauli_order, backend=backend
+ )
+
+ super_op = (
+ unitary @ super_op @ backend.np.transpose(backend.np.conj(unitary), (1, 0))
+ )
+
+ return super_op
+
+ def matrix(self, backend=None):
+ """"""
+ raise_error(
+ NotImplementedError,
+ "`matrix` method not defined for Channels. "
+ + "Please use one of the following methods: "
+ + "`to_choi` or `to_liouville` or `to_pauli_liouville`.",
+ )
+
+
+class KrausChannel(Channel):
+ """General channel defined by arbitrary Kraus operators.
+
+ Implements the following transformation:
+
+ .. math::
+ \\mathcal{E}(\\rho ) = \\sum _k A_k \\rho A_k^\\dagger
+
+ where A are arbitrary Kraus operators given by the user. Note that Kraus
+ operators set should be trace preserving, however this is not checked.
+ Simulation of this gate requires the use of density matrices.
+ For more information on channels and Kraus operators please check
+ `J. Preskill's notes `_.
+
+ Args:
+ qubits (int or list or tuple): Qubits that the Kraus operators act on.
+ Type ``int`` and ``tuple`` will be considered as the same qubit ids for
+ all operators. A ``list`` should contain tuples of qubits corresponding
+ to each operator. Can be ``[]`` if ``operators`` are of type :class:`qibo.gates.Gate`,
+ otherwise adds given gates on specified qubits.
+ operators (list): List of Kraus operators ``Ak`` as matrices of type
+ ``ndarray | tf.Tensor`` or gates :class:`qibo.gates.Gate`.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+
+ from qibo import Circuit, gates
+
+ # initialize circuit with 3 qubits
+ circuit = Circuit(3, density_matrix=True)
+ # define a sqrt(0.4) * X gate
+ a_1 = np.sqrt(0.4) * np.array([[0, 1], [1, 0]])
+ # define a sqrt(0.6) * CNOT gate
+ a_2 = np.sqrt(0.6) * np.array([[1, 0, 0, 0], [0, 1, 0, 0],
+ [0, 0, 0, 1], [0, 0, 1, 0]])
+ # define the channel rho -> 0.4 X{1} rho X{1} + 0.6 CNOT{0, 2} rho CNOT{0, 2}
+ channel_1 = gates.KrausChannel([(1,), (0, 2)], [a_1, a_2])
+ # add channel to the circuit
+ circuit.add(channel_1)
+
+ # define the same channel using qibo.gates.Unitary
+ a_1 = gates.Unitary(a_1, 1)
+ a_2 = gates.Unitary(a_2, 0, 2)
+ channel_2 = gates.KrausChannel([], [a_1, a_2])
+ # add channel to the circuit
+ circuit.add(channel_2)
+
+ # define the channel rho -> 0.4 X{0} rho X{0} + 0.6 CNOT{1, 2} rho CNOT{1, 2}
+ channel_3 = gates.KrausChannel([(0,), (1, 2)], [a_1, a_2])
+ # add channel to the circuit
+ circuit.add(channel_3)
+ """
+
+ def __init__(self, qubits, operators):
+ super().__init__()
+ self.name = "KrausChannel"
+ self.draw_label = "K"
+
+ # Check qubits type
+ if isinstance(qubits, int):
+ qubits = [(qubits,)] * len(operators)
+ elif isinstance(qubits, tuple):
+ qubits = [qubits] * len(operators)
+ elif not isinstance(qubits, list):
+ raise_error(
+ TypeError,
+ "``qubits`` must be of type int or tuple or int. "
+ + f"Got {type(qubits)} instead",
+ )
+ elif not all(isinstance(q, (tuple)) for q in qubits):
+ raise_error(TypeError, "All elements of ``qubits`` list must be tuples.")
+
+ if isinstance(operators[0], Gate):
+ if qubits:
+ operators = [
+ operators[k].on_qubits(
+ {
+ operators[k].qubits[i]: qubits[k][i]
+ for i in range(len(operators[k].qubits))
+ }
+ )
+ for k in range(len(operators))
+ ]
+ self.gates = tuple(operators)
+ self.target_qubits = tuple(
+ sorted({q for gate in operators for q in gate.target_qubits})
+ )
+ unitary_check = [gate.unitary for gate in self.gates]
+ elif len(qubits) != len(operators):
+ raise_error(
+ ValueError,
+ f"``qubits`` list has length {len(qubits)} while "
+ + f"{len(operators)} operators were given.",
+ )
+ else:
+ gates, qubitset, unitary_check = [], set(), []
+
+ for qubit_tuple, matrix in zip(qubits, operators):
+ rank = 2 ** len(qubit_tuple)
+ shape = tuple(matrix.shape)
+ if shape != (rank, rank):
+ raise_error(
+ ValueError,
+ f"Invalid Kraus operator shape {shape} for "
+ + f"acting on {len(qubit_tuple)} qubits.",
+ )
+ gate = Unitary(matrix, *list(qubit_tuple))
+
+ qubitset.update(qubit_tuple)
+ gates.append(gate)
+ unitary_check.append(gate.unitary)
+ self.gates = tuple(gates)
+ self.target_qubits = tuple(sorted(qubitset))
+ self.init_args = [self.gates]
+ self.coefficients = len(self.gates) * (1,)
+ self.coefficient_sum = 1
+ self._all_unitary_operators = True if all(unitary_check) else False
+
+
+class UnitaryChannel(KrausChannel):
+ """Channel that is a probabilistic sum of unitary operations.
+
+ Implements the following transformation:
+
+ .. math::
+ \\mathcal{E}(\\rho ) = \\left (1 - \\sum _k p_k \\right )\\rho +
+ \\sum _k p_k U_k \\rho U_k^\\dagger
+
+ where U are arbitrary unitary operators and p are floats between 0 and 1.
+ Note that unlike :class:`qibo.gates.KrausChannel` which requires
+ density matrices, it is possible to simulate the unitary channel using
+ state vectors and probabilistic sampling. For more information on this
+ approach we refer to :ref:`Using repeated execution `.
+
+ Args:
+ qubits (int or list or tuple): Qubits that the unitary operators
+ act on. Types ``int`` and ``tuple`` will be considered as the same
+ qubit(s) for all unitaries. A ``list`` should contain tuples of
+ qubits corresponding to each operator. Can be ``[]`` if ``operators`` are of type
+ :class:`qibo.gates.Gate`, otherwise adds given gates on specified qubits.
+ operators (list): List of operators as pairs ``(pk, Uk)`` where
+ ``pk`` is float probability corresponding to a unitary ``Uk``
+ of type ``ndarray``/``tf.Tensor`` or gates :class:`qibo.gates.Gate`.
+ """
+
+ def __init__(self, qubits, operators):
+ if not all(isinstance(pair, (tuple)) for pair in operators):
+ raise_error(
+ TypeError, "``operators`` must be a list of tuples ``(pk, Uk)``."
+ )
+
+ probabilities = [pair[0] for pair in operators]
+ operators = [pair[1] for pair in operators]
+ if any((p < 0 or p > 1) for p in probabilities):
+ raise_error(
+ ValueError,
+ "Probabilities should be between 0 and 1.",
+ )
+ super().__init__(qubits, operators)
+ self.name = "UnitaryChannel"
+ self.draw_label = "U"
+ self.coefficients = tuple(probabilities)
+ self.coefficient_sum = sum(probabilities)
+ if self.coefficient_sum > 1 + PRECISION_TOL or self.coefficient_sum < 0:
+ raise_error(
+ ValueError,
+ "UnitaryChannel probability sum should be "
+ + f"between 0 and 1 but is {self.coefficient_sum}.",
+ )
+
+ self.init_args = [probabilities, self.gates]
+
+ def apply(self, backend, state, nqubits):
+ return backend.apply_channel(self, state, nqubits)
+
+
+class PauliNoiseChannel(UnitaryChannel):
+ """Multi-qubit noise channel that applies Pauli operators with given probabilities.
+
+ Implements the following transformation:
+
+ .. math::
+ \\mathcal{E}(\\rho ) = \\left (1 - \\sum _{k} p_{k} \\right ) \\, \\rho +
+ \\sum_{k} \\, p_{k} \\, P_{k} \\, \\rho \\, P_{k}
+
+
+ where :math:`P_{k}` is the :math:`k`-th Pauli ``string`` and :math:`p_{k}` is
+ the probability associated to :math:`P_{k}`.
+
+ Example:
+ .. testcode::
+
+ from itertools import product
+
+ import numpy as np
+
+ from qibo.gates.channels import PauliNoiseChannel
+
+ qubits = (0, 2)
+ nqubits = len(qubits)
+
+ # excluding the Identity operator
+ paulis = list(product(["I", "X"], repeat=nqubits))[1:]
+ # this next line is optional
+ paulis = [''.join(pauli) for pauli in paulis]
+
+ probabilities = np.random.rand(len(paulis) + 1)
+ probabilities /= np.sum(probabilities)
+ #Excluding probability of Identity operator
+ probabilities = probabilities[1:]
+
+ channel = PauliNoiseChannel(
+ qubits, list(zip(paulis, probabilities))
+ )
+
+ This channel can be simulated using either density matrices or state vectors
+ and sampling with repeated execution.
+ See :ref:`How to perform noisy simulation? ` for more
+ information.
+
+ Args:
+ qubits (int or list or tuple): Qubits that the noise acts on.
+ operators (list): list of operators as pairs :math:`(P_{k}, p_{k})`.
+ """
+
+ def __init__(self, qubits: Tuple[int, list, tuple], operators: list):
+ if isinstance(qubits, int):
+ qubits = (qubits,)
+
+ probabilities, paulis = [], []
+ for pauli, probability in operators:
+ probabilities.append(probability)
+ paulis.append(pauli)
+
+ single_paulis = {"I": I, "X": X, "Y": Y, "Z": Z}
+
+ gates = []
+ for pauli in paulis:
+ fgate = FusedGate(*qubits)
+ for qubit, pauli_single in zip(qubits, pauli):
+ fgate.append(single_paulis[pauli_single](qubit))
+ gates.append(fgate)
+ self.gates = tuple(gates)
+ self.coefficients = tuple(probabilities)
+
+ super().__init__([], list(zip(probabilities, gates)))
+ self.name = "PauliNoiseChannel"
+ self.draw_label = "PN"
+ self.init_args = qubits
+ self.init_kwargs = dict(operators)
+
+
+class DepolarizingChannel(PauliNoiseChannel):
+ """:math:`n`-qubit Depolarizing quantum error channel,
+
+ .. math::
+ \\mathcal{E}(\\rho ) = (1 - \\lambda) \\rho +
+ \\lambda \\text{Tr}_q[\\rho]\\otimes \\frac{I}{2^n}
+
+ where :math:`\\lambda` is the depolarizing error parameter
+ and :math:`0 \\le \\lambda \\le 4^n / (4^n - 1)`.
+
+ * If :math:`\\lambda = 1` this is a completely depolarizing channel
+ :math:`E(\\rho) = I / 2^n`
+ * If :math:`\\lambda = 4^n / (4^n - 1)` this is a uniform Pauli
+ error channel: :math:`E(\\rho) = \\sum_j P_j \\rho P_j / (4^n - 1)` for
+ all :math:`P_j \\neq I`.
+
+ Args:
+ qubits (int or list or tuple): Qubit ids that the noise acts on.
+ lam (float): Depolarizing error parameter.
+ """
+
+ def __init__(self, qubits, lam: float):
+ if isinstance(qubits, int):
+ qubits = (qubits,)
+
+ num_qubits = len(qubits)
+ num_terms = 4**num_qubits
+ max_param = num_terms / (num_terms - 1)
+ if lam < 0 or lam > max_param:
+ raise_error(
+ ValueError,
+ f"Depolarizing parameter must be in between 0 and {max_param}.",
+ )
+
+ pauli_noise_params = list(product(["I", "X", "Y", "Z"], repeat=num_qubits))[1::]
+ pauli_noise_params = zip(
+ pauli_noise_params, [lam / num_terms] * (num_terms - 1)
+ )
+ super().__init__(qubits, pauli_noise_params)
+
+ self.name = "DepolarizingChannel"
+ self.draw_label = "D"
+ self.target_qubits = qubits
+
+ self.init_args = [qubits]
+ self.init_kwargs = {"lam": lam}
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ return backend.depolarizing_error_density_matrix(self, state, nqubits)
+
+
+class ThermalRelaxationChannel(KrausChannel):
+ """Single-qubit thermal relaxation error channel.
+
+ Implements the following transformation:
+
+ If :math:`T_1 \\geq T_2`:
+
+ .. math::
+ \\mathcal{E} (\\rho ) = (1 - p_z - p_0 - p_1) \\rho + p_z \\, Z\\rho Z
+ + \\mathrm{Tr}_{q}[\\rho] \\otimes (p_0 | 0\\rangle \\langle 0|
+ + p_1|1\\rangle \\langle 1|)
+
+
+ while if :math:`T_1 < T_2`:
+
+ .. math::
+ \\mathcal{E}(\\rho ) = \\mathrm{Tr}_\\mathcal{X}
+ \\left[\\Lambda_{\\mathcal{X}\\mathcal{Y}} (\\rho_\\mathcal{X}^T
+ \\otimes I_{\\mathcal{Y}}) \\right]
+
+ with
+
+ .. math::
+ \\Lambda = \\begin{pmatrix}
+ 1 - p_1 & 0 & 0 & e^{-t / T_2} \\\\
+ 0 & p_1 & 0 & 0 \\\\
+ 0 & 0 & p_0 & 0 \\\\
+ e^{-t / T_2} & 0 & 0 & 1 - p_0
+ \\end{pmatrix}
+
+ where :math:`p_0 = (1 - e^{-t / T_1})(1 - \\eta )`,
+ :math:`p_1 = (1 - e^{-t / T_1})\\eta`, and
+ :math:`p_z = (e^{-t / T_1} - e^{-t / T_2})/2`.
+ Here :math:`\\eta` is the ``excited_population``
+ and :math:`t` is the ``time``, both controlled by the user.
+ This gate is based on `Qiskit's thermal relaxation error channel
+ `_.
+
+ Args:
+ qubit (int): Qubit id that the noise channel acts on.
+ parameters (list): list of 3 or 4 parameters
+ (t_1, t_2, time, excited_population=0), where
+ t_1 (float): T1 relaxation time. Should satisfy ``t_1 > 0``.
+ t_2 (float): T2 dephasing time.
+ Should satisfy ``t_1 > 0`` and ``t_2 < 2 * t_1``.
+ time (float): the gate time for relaxation error.
+ excited_population (float): the population of the excited state at
+ equilibrium. Default is 0.
+ """
+
+ def __init__(self, qubit: int, parameters: list):
+ if len(parameters) not in [3, 4]:
+ raise_error(
+ ValueError,
+ "``parameters`` list must have 3 or 4 elements "
+ + f"while {len(parameters)} were given.",
+ )
+
+ t_1, t_2, time = parameters[:3]
+ excited_population = parameters[-1] if len(parameters) == 4 else 0.0
+
+ if excited_population < 0 or excited_population > 1:
+ raise_error(
+ ValueError, f"Invalid excited state population {excited_population}."
+ )
+ if time < 0.0:
+ raise_error(ValueError, f"Invalid gate time: {time} < 0.")
+ if t_1 <= 0.0:
+ raise_error(
+ ValueError, f"Invalid t_1 relaxation time parameter: {t_1} <= 0.0."
+ )
+ if t_2 <= 0.0:
+ raise_error(
+ ValueError, f"Invalid t_2 relaxation time parameter: {t_2} <= 0.0."
+ )
+ if t_2 > 2 * t_1:
+ raise_error(
+ ValueError,
+ "Invalid t_2 relaxation time parameter: t_2 > 2 * t_1.",
+ )
+
+ # calculate probabilities
+ self.t_1, self.t_2 = t_1, t_2
+ p_reset = 1 - exp(-time / t_1)
+ p_0 = p_reset * (1 - excited_population)
+ p_1 = p_reset * excited_population
+
+ if t_1 < t_2:
+ e_t2 = exp(-time / t_2)
+
+ operators = [
+ sqrt(p_0) * sqrt(p_0) * np.array([[0, 1], [0, 0]]),
+ sqrt(p_1) * np.array([[0, 0], [1, 0]]),
+ ]
+
+ k_term = sqrt(4 * e_t2**2 + (p_0 - p_1) ** 2)
+ kraus_coeff = sqrt(1 - (p_0 + p_1 + k_term) / 2)
+
+ operators.append(
+ kraus_coeff
+ * np.array(
+ [
+ [(p_0 - p_1 - k_term) / (2 * e_t2), 0],
+ [0, 1],
+ ]
+ )
+ )
+
+ operators.append(
+ kraus_coeff
+ * np.array(
+ [
+ [(p_0 - p_1 + k_term) / (2 * e_t2), 0],
+ [0, 1],
+ ]
+ )
+ )
+
+ super().__init__([(qubit,)] * len(operators), operators)
+ self.init_kwargs["e_t2"] = e_t2
+ else:
+ p_z = (exp(-time / t_1) - exp(-time / t_2)) / 2
+ operators = (
+ sqrt(p_0) * np.array([[1, 0], [0, 0]]),
+ sqrt(p_0) * np.array([[0, 1], [0, 0]]),
+ sqrt(p_1) * np.array([[0, 0], [1, 0]]),
+ sqrt(p_1) * np.array([[0, 0], [0, 1]]),
+ sqrt(p_z) * np.array([[1, 0], [0, -1]]),
+ sqrt(1 - p_0 - p_1 - p_z) * np.eye(2),
+ )
+ super().__init__([(qubit,)] * len(operators), operators)
+ self.init_kwargs["p_z"] = p_z
+
+ self.init_args = [qubit, t_1, t_2, time]
+ self.t_1, self.t_2 = t_1, t_2
+ self.init_kwargs["excited_population"] = excited_population
+ self.init_kwargs["p_0"] = p_0
+ self.init_kwargs["p_1"] = p_1
+
+ self.name = "ThermalRelaxationChannel"
+ self.draw_label = "TR"
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ qubit = self.target_qubits[0]
+
+ if self.t_1 < self.t_2:
+ preset0, preset1, e_t2 = (
+ self.init_kwargs["p_0"],
+ self.init_kwargs["p_1"],
+ self.init_kwargs["e_t2"],
+ )
+ matrix = [
+ [1 - preset1, 0, 0, preset0],
+ [0, e_t2, 0, 0],
+ [0, 0, e_t2, 0],
+ [preset1, 0, 0, 1 - preset0],
+ ]
+
+ qubits = (qubit, qubit + nqubits)
+ gate = Unitary(matrix, *qubits)
+
+ return backend.thermal_error_density_matrix(gate, state, nqubits)
+
+ p_z = self.init_kwargs["p_z"]
+
+ return (
+ backend.reset_error_density_matrix(self, state, nqubits)
+ - p_z * backend.cast(state)
+ + p_z * backend.apply_gate_density_matrix(Z(0), state, nqubits)
+ )
+
+
+class AmplitudeDampingChannel(KrausChannel):
+ """Single-qubit amplitude damping channel in its Kraus representation, i.e.
+
+ .. math::
+ K_{0} = \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & \\sqrt{1 - \\gamma} \\\\
+ \\end{pmatrix} \\,\\, , \\,\\,
+ K_{1} = \\begin{pmatrix}
+ 0 & \\sqrt{\\gamma} \\\\
+ 0 & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ qubit (int): Qubit id that the noise channel acts on.
+ gamma (float): amplitude damping strength.
+ """
+
+ def __init__(self, qubit, gamma: float):
+ if not isinstance(gamma, float):
+ raise_error(
+ TypeError, f"gamma must be type float, but it is type {type(gamma)}."
+ )
+ if gamma < 0.0 or gamma > 1.0:
+ raise_error(ValueError, "gamma must be a float between 0 and 1.")
+
+ operators = []
+ operators.append(np.array([[1, 0], [0, sqrt(1 - gamma)]], dtype=complex))
+ operators.append(np.array([[0, sqrt(gamma)], [0, 0]], dtype=complex))
+
+ super().__init__([(qubit,)] * len(operators), operators)
+ self.name = "AmplitudeDampingChannel"
+ self.draw_label = "AD"
+
+
+class PhaseDampingChannel(KrausChannel):
+ """Single-qubit phase damping channel in its Kraus representation, i.e.
+
+ .. math::
+ K_{0} = \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & \\sqrt{1 - \\gamma} \\\\
+ \\end{pmatrix} \\,\\, , \\,\\,
+ K_{1} = \\begin{pmatrix}
+ 0 & 0 \\\\
+ 0 & \\sqrt{\\gamma} \\\\
+ \\end{pmatrix}
+
+ Args:
+ qubit (int): Qubit id that the noise channel acts on.
+ gamma (float): phase damping strength.
+ """
+
+ def __init__(self, qubit, gamma: float):
+ if not isinstance(gamma, float):
+ raise_error(
+ TypeError, f"gamma must be type float, but it is type {type(gamma)}."
+ )
+ if gamma < 0.0 or gamma > 1.0:
+ raise_error(ValueError, "gamma must be a float between 0 and 1.")
+
+ operators = []
+ operators.append(np.array([[1, 0], [0, sqrt(1 - gamma)]], dtype=complex))
+ operators.append(np.array([[0, 0], [0, sqrt(gamma)]], dtype=complex))
+
+ super().__init__([(qubit,)] * len(operators), operators)
+ self.name = "PhaseDampingChannel"
+ self.draw_label = "PD"
+
+
+class ReadoutErrorChannel(KrausChannel):
+ """Readout error channel implemented as a quantum-to-classical channel.
+
+ Args:
+ qubits (int or list or tuple): Qubit ids that the channel acts on.
+ probabilities (array): row-stochastic matrix :math:`P` with all
+ readout transition probabilities.
+
+ Example:
+ For 1 qubit, the transition matrix :math:`P` would be
+
+ .. math::
+ P = \\begin{pmatrix}
+ p(0 \\, | \\, 0) & p(1 \\, | \\, 0) \\\\
+ p(0 \\, | \\, 1) & p(1 \\, | \\, 1)
+ \\end{pmatrix} \\, .
+ """
+
+ def __init__(self, qubits: Tuple[int, list, tuple], probabilities):
+ if any(sum(row) < 1 - PRECISION_TOL for row in probabilities) or any(
+ sum(row) > 1 + PRECISION_TOL for row in probabilities
+ ):
+ raise_error(ValueError, "all rows of probabilities must sum to 1.")
+
+ if isinstance(qubits, int):
+ qubits = (qubits,)
+
+ if isinstance(probabilities, list):
+ probabilities = np.array(probabilities)
+
+ dim = len(probabilities)
+ operators = []
+ for j in range(dim):
+ for k in range(dim):
+ operator = np.zeros((dim, dim))
+ operator[j, k] = sqrt(probabilities[k, j])
+ operators.append(operator)
+
+ super().__init__([qubits] * len(operators), operators)
+ self.name = "ReadoutErrorChannel"
+ self.draw_label = "RE"
+
+
+class ResetChannel(KrausChannel):
+ """Single-qubit reset channel.
+
+ Implements the following transformation:
+
+ .. math::
+ \\mathcal{E}(\\rho ) = (1 - p_{0} - p_{1}) \\rho
+ + \\mathrm{Tr}_{q}[\\rho] \\otimes (p_{0} \\, |0\\rangle \\langle 0|
+ + p_{1} \\, |1\\rangle \\langle 1|),
+
+ Args:
+ qubit (int): qubit id that the channel acts on.
+ probabilities (list or ndarray): list :math:`[p_{0}, p_{1}]`,
+ where :math:`p_{0}` and :math:`p_{1}` are the probabilities to
+ reset to 0 and 1, respectively.
+ """
+
+ def __init__(self, qubit, probabilities):
+ if len(probabilities) != 2:
+ raise_error(
+ ValueError,
+ f"ResetChannel needs 2 probabilities, got {len(probabilities)} instead.",
+ )
+ p_0, p_1 = probabilities
+ if p_0 < 0:
+ raise_error(ValueError, "Invalid p_0 ({p_0} < 0).")
+ if p_1 < 0:
+ raise_error(ValueError, "Invalid p_1 ({p_1} < 0).")
+ if p_0 + p_1 > 1 + PRECISION_TOL:
+ raise_error(
+ ValueError, f"Invalid probabilities (p_0 + p_1 = {p_0 + p_1} > 1)."
+ )
+
+ operators = [
+ sqrt(p_0) * np.array([[1, 0], [0, 0]]),
+ sqrt(p_0) * np.array([[0, 1], [0, 0]]),
+ sqrt(p_1) * np.array([[0, 0], [1, 0]]),
+ sqrt(p_1) * np.array([[0, 0], [0, 1]]),
+ ]
+
+ if p_0 + p_1 < 1:
+ operators.append(sqrt(np.abs(1 - p_0 - p_1)) * np.eye(2))
+
+ super().__init__([(qubit,)] * len(operators), operators)
+ self.init_kwargs = {"p_0": p_0, "p_1": p_1}
+ self.name = "ResetChannel"
+ self.draw_label = "R"
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ return backend.reset_error_density_matrix(self, state, nqubits)
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
new file mode 100644
index 000000000..a06f668bc
--- /dev/null
+++ b/src/qibo/gates/gates.py
@@ -0,0 +1,2569 @@
+import math
+from typing import List
+
+import numpy as np
+
+from qibo.config import PRECISION_TOL, raise_error
+from qibo.gates.abstract import Gate, ParametrizedGate
+from qibo.parameter import Parameter
+
+
+class H(Gate):
+ """The Hadamard gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{\\sqrt{2}} \\, \\begin{pmatrix}
+ 1 & 1 \\\\
+ 1 & -1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "h"
+ self.draw_label = "H"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "h"
+
+
+class X(Gate):
+ """The Pauli-:math:`X` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 0 & 1 \\\\
+ 1 & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "x"
+ self.draw_label = "X"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "x"
+
+ @Gate.check_controls
+ def controlled_by(self, *q):
+ """Fall back to CNOT and Toffoli if there is one or two controls."""
+ if len(q) == 1:
+ gate = CNOT(q[0], self.target_qubits[0])
+ elif len(q) == 2:
+ gate = TOFFOLI(q[0], q[1], self.target_qubits[0])
+ else:
+ gate = super().controlled_by(*q)
+ return gate
+
+ def decompose(self, *free, use_toffolis=True):
+ """Decomposes multi-control ``X`` gate to one-qubit, ``CNOT`` and ``TOFFOLI`` gates.
+
+ Args:
+ free: Ids of free qubits to use for the gate decomposition.
+ use_toffolis: If ``True`` the decomposition contains only ``TOFFOLI`` gates.
+ If ``False`` a congruent representation is used for ``TOFFOLI`` gates.
+ See :class:`qibo.gates.TOFFOLI` for more details on this representation.
+
+ Returns:
+ List with one-qubit, ``CNOT`` and ``TOFFOLI`` gates that have the
+ same effect as applying the original multi-control gate.
+ """
+ if set(free) & set(self.qubits):
+ raise_error(
+ ValueError,
+ "Cannot decompose multi-control X gate if free "
+ "qubits coincide with target or controls.",
+ )
+
+ controls = self.control_qubits
+ target = self.target_qubits[0]
+ m = len(controls)
+ if m < 3:
+ return [self.__class__(target).controlled_by(*controls)]
+
+ decomp_gates = []
+ n = m + 1 + len(free)
+ if (n >= 2 * m - 1) and (m >= 3):
+ gates1 = [
+ TOFFOLI(
+ controls[m - 2 - i], free[m - 4 - i], free[m - 3 - i]
+ ).congruent(use_toffolis=use_toffolis)
+ for i in range(m - 3)
+ ]
+ gates2 = TOFFOLI(controls[0], controls[1], free[0]).congruent(
+ use_toffolis=use_toffolis
+ )
+ first_toffoli = TOFFOLI(controls[m - 1], free[m - 3], target)
+
+ decomp_gates.append(first_toffoli)
+ for gates in gates1:
+ decomp_gates.extend(gates)
+ decomp_gates.extend(gates2)
+ for gates in gates1[::-1]:
+ decomp_gates.extend(gates)
+
+ elif len(free) >= 1:
+ m1 = n // 2
+ free1 = controls[m1:] + (target,) + tuple(free[1:])
+ x1 = self.__class__(free[0]).controlled_by(*controls[:m1])
+ part1 = x1.decompose(*free1, use_toffolis=use_toffolis)
+
+ free2 = controls[:m1] + tuple(free[1:])
+ controls2 = controls[m1:] + (free[0],)
+ x2 = self.__class__(target).controlled_by(*controls2)
+ part2 = x2.decompose(*free2, use_toffolis=use_toffolis)
+
+ decomp_gates = [*part1, *part2]
+
+ else: # pragma: no cover
+ # impractical case
+ raise_error(
+ NotImplementedError,
+ "X decomposition not implemented for zero free qubits.",
+ )
+
+ decomp_gates.extend(decomp_gates)
+ return decomp_gates
+
+ def basis_rotation(self):
+ return H(self.target_qubits[0])
+
+
+class Y(Gate):
+ """The Pauli-:math:`Y` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 0 & -i \\\\
+ i & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "y"
+ self.draw_label = "Y"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "y"
+
+ @Gate.check_controls
+ def controlled_by(self, *q):
+ """Fall back to CY if there is only one control."""
+ if len(q) == 1:
+ gate = CY(q[0], self.target_qubits[0])
+ else:
+ gate = super().controlled_by(*q)
+ return gate
+
+ def basis_rotation(self):
+ from qibo import matrices # pylint: disable=C0415
+
+ matrix = (matrices.Y + matrices.Z) / math.sqrt(2)
+ return Unitary(matrix, self.target_qubits[0], trainable=False)
+
+
+class Z(Gate):
+ """The Pauli-:math:`Z` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & -1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "z"
+ self.draw_label = "Z"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "z"
+
+ @Gate.check_controls
+ def controlled_by(self, *q):
+ """Fall back to CZ if there is only one control."""
+ if len(q) == 1:
+ gate = CZ(q[0], self.target_qubits[0])
+ else:
+ gate = super().controlled_by(*q)
+ return gate
+
+ def basis_rotation(self):
+ return None
+
+
+class SX(Gate):
+ """The :math:`\\sqrt{X}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{2} \\, \\begin{pmatrix}
+ 1 + i & 1 - i \\\\
+ 1 - i & 1 + i \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "sx"
+ self.draw_label = "SX"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "sx"
+
+ def decompose(self):
+ """Decomposition of :math:`\\sqrt{X}` up to global phase.
+
+ A global phase difference exists between the definitions of
+ :math:`\\sqrt{X}` and :math:`\\text{RX}(\\pi / 2)`, with :math:`\\text{RX}`
+ being the :class:`qibo.gates.RX` gate. More precisely,
+ :math:`\\sqrt{X} = e^{i \\pi / 4} \\, \\text{RX}(\\pi / 2)`.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+ def _dagger(self):
+ """"""
+ return SXDG(self.init_args[0])
+
+
+class SXDG(Gate):
+ """The conjugate transpose of the :math:`\\sqrt{X}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{2} \\, \\begin{pmatrix}
+ 1 - i & 1 + i \\\\
+ 1 + i & 1 - i \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "sxdg"
+ self.draw_label = "SXDG"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "sxdg"
+
+ def decompose(self):
+ """Decomposition of :math:`(\\sqrt{X})^{\\dagger}` up to global phase.
+
+ A global phase difference exists between the definitions of
+ :math:`\\sqrt{X}` and :math:`\\text{RX}(\\pi / 2)`, with :math:`\\text{RX}`
+ being the :class:`qibo.gates.RX` gate. More precisely,
+ :math:`(\\sqrt{X})^{\\dagger} = e^{-i \\pi / 4} \\, \\text{RX}(-\\pi / 2)`.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+ def _dagger(self):
+ """"""
+ return SX(self.init_args[0])
+
+
+class S(Gate):
+ """The :math:`S` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & i \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "s"
+ self.draw_label = "S"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "s"
+
+ def _dagger(self):
+ return SDG(*self.init_args)
+
+
+class SDG(Gate):
+ """The conjugate transpose of the :math:`S` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & -i \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "sdg"
+ self.draw_label = "SDG"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "sdg"
+
+ def _dagger(self):
+ return S(*self.init_args)
+
+
+class T(Gate):
+ """The T gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & e^{i \\pi / 4} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "t"
+ self.draw_label = "T"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "t"
+
+ def _dagger(self):
+ return TDG(*self.init_args)
+
+
+class TDG(Gate):
+ """The conjugate transpose of the T gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & e^{-i \\pi / 4} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ """
+
+ def __init__(self, q):
+ super().__init__()
+ self.name = "tdg"
+ self.draw_label = "TDG"
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "tdg"
+
+ def _dagger(self):
+ return T(*self.init_args)
+
+
+class I(Gate):
+ """The identity gate.
+
+ Args:
+ *q (int): the qubit id numbers.
+ """
+
+ def __init__(self, *q):
+ super().__init__()
+ self.name = "id"
+ self.draw_label = "I"
+ self.target_qubits = tuple(q)
+ self.init_args = q
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "id"
+
+
+class Align(ParametrizedGate):
+ """Aligns proceeding qubit operations and (optionally) waits ``delay`` amount of time.
+
+ Args:
+ q (int): The qubit ID.
+ delay (int, optional): The time (in ns) for which to delay circuit execution on the specified qubits.
+ Defaults to ``0`` (zero).
+ """
+
+ def __init__(self, q, delay=0, trainable=True):
+ if not isinstance(delay, int):
+ raise_error(
+ TypeError, f"delay must be type int, but it is type {type(delay)}."
+ )
+ if delay < 0.0:
+ raise_error(ValueError, "Delay must not be negative.")
+
+ super().__init__(trainable)
+ self.name = "align"
+ self.draw_label = f"A({delay})"
+ self.init_args = [q]
+ self.init_kwargs = {"name": self.name, "delay": delay, "trainable": trainable}
+ self.target_qubits = (q,)
+ self._parameters = (delay,)
+ self.nparams = 1
+
+
+def _is_clifford_given_angle(angle):
+ """Helper function to update Clifford boolean condition according to the given angle ``angle``."""
+ return isinstance(angle, (float, int)) and (angle % (np.pi / 2)).is_integer()
+
+
+class _Rn_(ParametrizedGate):
+ """Abstract class for defining the RX, RY and RZ rotations.
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = None
+ self._controlled_gate = None
+ self.target_qubits = (q,)
+ self.unitary = True
+
+ self.initparams = theta
+ if isinstance(theta, Parameter):
+ theta = theta()
+
+ self.parameters = theta
+ self.init_args = [q]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ @property
+ def clifford(self):
+ return _is_clifford_given_angle(self.parameters[0])
+
+ def _dagger(self) -> "Gate":
+ """"""
+ return self.__class__(
+ self.target_qubits[0], -self.parameters[0]
+ ) # pylint: disable=E1130
+
+ @Gate.check_controls
+ def controlled_by(self, *q):
+ """Fall back to CRn if there is only one control."""
+ if len(q) == 1:
+ gate = self._controlled_gate( # pylint: disable=E1102
+ q[0], self.target_qubits[0], **self.init_kwargs
+ )
+ else:
+ gate = super().controlled_by(*q)
+ return gate
+
+
+class RX(_Rn_):
+ """Rotation around the X-axis of the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos \\frac{\\theta }{2} &
+ -i\\sin \\frac{\\theta }{2} \\\\
+ -i\\sin \\frac{\\theta }{2} &
+ \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, trainable=True):
+ super().__init__(q, theta, trainable)
+ self.name = "rx"
+ self.draw_label = "RX"
+ self._controlled_gate = CRX
+
+ @property
+ def qasm_label(self):
+ return "rx"
+
+ def generator_eigenvalue(self):
+ return 0.5
+
+
+class RY(_Rn_):
+ """Rotation around the Y-axis of the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos \\frac{\\theta }{2} &
+ -\\sin \\frac{\\theta }{2} \\\\
+ \\sin \\frac{\\theta }{2} &
+ \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, trainable=True):
+ super().__init__(q, theta, trainable)
+ self.name = "ry"
+ self.draw_label = "RY"
+ self._controlled_gate = CRY
+
+ @property
+ def qasm_label(self):
+ return "ry"
+
+ def generator_eigenvalue(self):
+ return 0.5
+
+
+class RZ(_Rn_):
+ """Rotation around the Z-axis of the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ e^{-i \\theta / 2} & 0 \\\\
+ 0 & e^{i \\theta / 2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, trainable=True):
+ super().__init__(q, theta, trainable)
+ self.name = "rz"
+ self.draw_label = "RZ"
+ self._controlled_gate = CRZ
+
+ @property
+ def qasm_label(self):
+ return "rz"
+
+ def generator_eigenvalue(self):
+ return 0.5
+
+
+class PRX(ParametrizedGate):
+ """Phase :math:`RX` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos{(\\theta / 2)} & -i e^{-i \\phi} \\sin{(\\theta / 2)} \\
+ -i e^{i \\phi} \\sin{(\\theta / 2)} & \\cos{(\\theta / 2)}
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the first angle corresponding to a rotation angle.
+ phi (float): the second angle correspoding to a phase angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, phi, trainable=True):
+ super().__init__(trainable)
+ self.name = "prx"
+ self.draw_label = "prx"
+ self.target_qubits = (q,)
+ self.unitary = True
+
+ self.parameter_names = ["theta", "phi"]
+ self.parameters = theta, phi
+ self.theta = theta
+ self.phi = phi
+ self.nparams = 2
+
+ self.init_args = [q]
+ self.init_kwargs = {
+ "theta": theta,
+ "phi": phi,
+ "trainable": trainable,
+ }
+
+ @property
+ def qasm_label(self):
+ return "prx"
+
+ def _dagger(self) -> "Gate":
+ theta = -self.theta
+ phi = self.phi
+ return self.__class__(
+ self.target_qubits[0], theta, phi
+ ) # pylint: disable=E1130
+
+ def decompose(self):
+ """Decomposition of Phase-:math:`RX` gate."""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class GPI(ParametrizedGate):
+ """The GPI gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 0 & e^{- i \\phi} \\\\
+ e^{i \\phi} & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ phi (float): phase.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, phi, trainable=True):
+ super().__init__(trainable)
+ self.name = "gpi"
+ self.draw_label = "GPI"
+ self.target_qubits = (q,)
+ self.unitary = True
+
+ self.parameter_names = "phi"
+ self.parameters = phi
+ self.nparams = 1
+
+ self.init_args = [q]
+ self.init_kwargs = {"phi": phi, "trainable": trainable}
+
+ @property
+ def qasm_label(self):
+ return "gpi"
+
+
+class GPI2(ParametrizedGate):
+ """The GPI2 gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{\\sqrt{2}} \\, \\begin{pmatrix}
+ 1 & -i e^{- i \\phi} \\\\
+ -i e^{i \\phi} & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ phi (float): phase.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, phi, trainable=True):
+ super().__init__(trainable)
+ self.name = "gpi2"
+ self.draw_label = "GPI2"
+ self.target_qubits = (q,)
+ self.unitary = True
+
+ self.parameter_names = "phi"
+ self.parameters = phi
+ self.nparams = 1
+
+ self.init_args = [q]
+ self.init_kwargs = {"phi": phi, "trainable": trainable}
+
+ @property
+ def qasm_label(self):
+ return "gpi2"
+
+ @property
+ def clifford(self):
+ return _is_clifford_given_angle(self.parameters[0])
+
+ def _dagger(self) -> "Gate":
+ """"""
+ return self.__class__(self.target_qubits[0], self.parameters[0] + math.pi)
+
+
+class _Un_(ParametrizedGate):
+ """Abstract class for defining the U1, U2 and U3 gates.
+
+ Args:
+ q (int): the qubit id number.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, trainable=True):
+ super().__init__(trainable)
+ self.name = None
+ self._controlled_gate = None
+ self.nparams = 0
+ self.target_qubits = (q,)
+ self.init_args = [q]
+ self.unitary = True
+
+ self.init_kwargs = {"trainable": trainable}
+
+ @Gate.check_controls
+ def controlled_by(self, *q):
+ """Fall back to CUn if there is only one control."""
+ if len(q) == 1:
+ gate = self._controlled_gate( # pylint: disable=E1102
+ q[0], self.target_qubits[0], **self.init_kwargs
+ )
+ else:
+ gate = super().controlled_by(*q)
+ return gate
+
+
+class U1(_Un_):
+ """First general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 \\\\
+ 0 & e^{i \\theta} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, trainable=True):
+ super().__init__(q, trainable=trainable)
+ self.name = "u1"
+ self.draw_label = "U1"
+ self._controlled_gate = CU1
+
+ self.nparams = 1
+ self.parameters = theta
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ @property
+ def qasm_label(self):
+ return "u1"
+
+ def _dagger(self) -> "Gate":
+ theta = -self.parameters[0]
+ return self.__class__(self.target_qubits[0], theta) # pylint: disable=E1130
+
+
+class U2(_Un_):
+ """Second general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{\\sqrt{2}}
+ \\begin{pmatrix}
+ e^{-i(\\phi + \\lambda )/2} & -e^{-i(\\phi - \\lambda )/2} \\\\
+ e^{i(\\phi - \\lambda )/2} & e^{i (\\phi + \\lambda )/2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ phi (float): first rotation angle.
+ lamb (float): second rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, phi, lam, trainable=True):
+ super().__init__(q, trainable=trainable)
+ self.name = "u2"
+ self.draw_label = "U2"
+ self._controlled_gate = CU2
+ self.nparams = 2
+ self._phi, self._lam = None, None
+ self.init_kwargs = {"phi": phi, "lam": lam, "trainable": trainable}
+ self.parameter_names = ["phi", "lam"]
+ self.parameters = phi, lam
+
+ @property
+ def qasm_label(self):
+ return "u2"
+
+ def _dagger(self) -> "Gate":
+ """"""
+ phi, lam = self.parameters
+ phi, lam = math.pi - lam, -math.pi - phi
+ return self.__class__(self.target_qubits[0], phi, lam)
+
+
+class U3(_Un_):
+ """Third general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ e^{-i(\\phi + \\lambda )/2}\\cos\\left (\\frac{\\theta }{2}\\right ) &
+ -e^{-i(\\phi - \\lambda )/2}\\sin\\left (\\frac{\\theta }{2}\\right ) \\\\
+ e^{i(\\phi - \\lambda )/2}\\sin\\left (\\frac{\\theta }{2}\\right ) &
+ e^{i (\\phi + \\lambda )/2}\\cos\\left (\\frac{\\theta }{2}\\right ) \\\\
+ \\end{pmatrix}
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): first rotation angle.
+ phi (float): second rotation angle.
+ lamb (float): third rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, phi, lam, trainable=True):
+ super().__init__(q, trainable=trainable)
+ self.name = "u3"
+ self.draw_label = "U3"
+ self._controlled_gate = CU3
+ self.nparams = 3
+ self._theta, self._phi, self._lam = None, None, None
+ self.init_kwargs = {
+ "theta": theta,
+ "phi": phi,
+ "lam": lam,
+ "trainable": trainable,
+ }
+ self.parameter_names = ["theta", "phi", "lam"]
+ self.parameters = theta, phi, lam
+
+ @property
+ def qasm_label(self):
+ return "u3"
+
+ def _dagger(self) -> "Gate":
+ """"""
+ theta, lam, phi = tuple(-x for x in self.parameters) # pylint: disable=E1130
+ return self.__class__(self.target_qubits[0], theta, phi, lam)
+
+ def decompose(self) -> List[Gate]:
+ """Decomposition of :math:`U_{3}` up to global phase.
+
+ A global phase difference exists between the definitions of
+ :math:`U3` and this decomposition. More precisely,
+
+ .. math::
+ U_{3}(\\theta, \\phi, \\lambda) = e^{i \\, \\frac{3 \\pi}{2}}
+ \\, \\text{RZ}(\\phi + \\pi) \\, \\sqrt{X} \\, \\text{RZ}(\\theta + \\pi)
+ \\, \\sqrt{X} \\, \\text{RZ}(\\lambda) \\, ,
+
+ where :math:`\\text{RZ}` and :math:`\\sqrt{X}` are, respectively,
+ :class:`qibo.gates.RZ` and :class`qibo.gates.SX`.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class U1q(_Un_):
+ """Native single-qubit gate in the Quantinuum platform.
+
+ Corresponds to the following unitary matrix:
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos\\left(\\frac{\\theta}{2}\\right) &
+ -i \\, e^{-i \\, \\phi} \\, \\sin\\left(\\frac{\\theta}{2}\\right) \\\\
+ -i \\, e^{i \\, \\phi} \\, \\sin\\left(\\frac{\\theta}{2}\\right) &
+ \\cos\\left(\\frac{\\theta}{2}\\right) \\\\
+ \\end{pmatrix}
+
+ Note that
+ :math:`U_{1q}(\\theta, \\phi) = U_{3}(\\theta, \\phi - \\frac{\\pi}{2}, \\frac{\\pi}{2} - \\phi)`,
+ where :math:`U_{3}` is :class:`qibo.gates.U3`.
+
+ Args:
+ q (int): the qubit id number.
+ theta (float): first rotation angle.
+ phi (float): second rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q, theta, phi, trainable=True):
+ super().__init__(q, trainable=trainable)
+ self.name = "u1q"
+ self.draw_label = "U1q"
+ self.nparams = 2
+ self._theta, self._phi = None, None
+ self.init_kwargs = {"theta": theta, "phi": phi, "trainable": trainable}
+ self.parameter_names = ["theta", "phi"]
+ self.parameters = theta, phi
+
+ def _dagger(self) -> "Gate":
+ """"""
+ theta, phi = self.init_kwargs["theta"], self.init_kwargs["phi"]
+ return self.__class__(self.init_args[0], -theta, phi)
+
+
+class CNOT(Gate):
+ """The Controlled-NOT gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "cx"
+ self.draw_label = "X"
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "cx"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ q0, q1 = self.control_qubits[0], self.target_qubits[0]
+ return [self.__class__(q0, q1)]
+
+
+class CY(Gate):
+ """The Controlled-:math:`Y` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & -i \\\\
+ 0 & 0 & i & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "cy"
+ self.draw_label = "Y"
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "cy"
+
+ def decompose(self) -> List[Gate]:
+ """Decomposition of :math:`\\text{CY}` gate.
+
+ Decompose :math:`\\text{CY}` gate into :class:`qibo.gates.SDG` in
+ the target qubit, followed by :class:`qibo.gates.CNOT`, followed
+ by a :class:`qibo.gates.S` in the target qubit.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class CZ(Gate):
+ """The Controlled-Phase gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ 0 & 0 & 0 & -1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "cz"
+ self.draw_label = "Z"
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "cz"
+
+ def decompose(self) -> List[Gate]:
+ """Decomposition of :math:`\\text{CZ}` gate.
+
+ Decompose :math:`\\text{CZ}` gate into :class:`qibo.gates.H` in
+ the target qubit, followed by :class:`qibo.gates.CNOT`, followed
+ by another :class:`qibo.gates.H` in the target qubit
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class CSX(Gate):
+ """The Controlled-:math:`\\sqrt{X}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & e^{i\\pi/4} & e^{-i\\pi/4} \\\\
+ 0 & 0 & e^{-i\\pi/4} & e^{i\\pi/4} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "csx"
+ self.draw_label = "CSX"
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "csx"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """"""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+ def _dagger(self):
+ """"""
+ return CSXDG(*self.init_args)
+
+
+class CSXDG(Gate):
+ """The transpose conjugate of the Controlled-:math:`\\sqrt{X}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & e^{-i\\pi/4} & e^{i\\pi/4} \\\\
+ 0 & 0 & e^{i\\pi/4} & e^{-i\\pi/4} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "csxdg"
+ self.draw_label = "CSXDG"
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "csxdg"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """"""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+ def _dagger(self):
+ """"""
+ return CSX(*self.init_args)
+
+
+class _CRn_(ParametrizedGate):
+ """Abstract method for defining the CRX, CRY and CRZ gates.
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = None
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.parameters = theta
+ self.unitary = True
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ @property
+ def clifford(self):
+ return _is_clifford_given_angle(self.parameters[0])
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0 = self.control_qubits[0]
+ q1 = self.target_qubits[0]
+ theta = -self.parameters[0]
+ return self.__class__(q0, q1, theta) # pylint: disable=E1130
+
+
+class CRX(_CRn_):
+ """Controlled rotation around the X-axis for the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & \\cos \\frac{\\theta }{2} & -i\\sin \\frac{\\theta }{2} \\\\
+ 0 & 0 & -i\\sin \\frac{\\theta }{2} & \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "crx"
+ self.draw_label = "RX"
+
+ @property
+ def qasm_label(self):
+ return "crx"
+
+
+class CRY(_CRn_):
+ """Controlled rotation around the Y-axis for the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & \\cos \\frac{\\theta }{2} & -\\sin \\frac{\\theta }{2} \\\\
+ 0 & 0 & \\sin \\frac{\\theta }{2} & \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Note that this differs from the :class:`qibo.gates.RZ` gate.
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "cry"
+ self.draw_label = "RY"
+
+ @property
+ def qasm_label(self):
+ return "cry"
+
+
+class CRZ(_CRn_):
+ """Controlled rotation around the Z-axis for the Bloch sphere.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & e^{-i \\theta / 2} & 0 \\\\
+ 0 & 0 & 0 & e^{i \\theta / 2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "crz"
+ self.draw_label = "RZ"
+
+ @property
+ def qasm_label(self):
+ return "crz"
+
+
+class _CUn_(ParametrizedGate):
+ """Abstract method for defining the CU1, CU2 and CU3 gates.
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, trainable=True):
+ super().__init__(trainable)
+ self.name = None
+ self.nparams = 0
+ self.control_qubits = (q0,)
+ self.target_qubits = (q1,)
+ self.init_args = [q0, q1]
+ self.unitary = True
+ self.init_kwargs = {"trainable": trainable}
+
+
+class CU1(_CUn_):
+ """Controlled first general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ 0 & 0 & 0 & e^{i \\theta } \\\\
+ \\end{pmatrix}
+
+ Note that this differs from the :class:`qibo.gates.CRZ` gate.
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, trainable=trainable)
+ self.name = "cu1"
+ self.draw_label = "U1"
+ self.nparams = 1
+ self.parameters = theta
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ @property
+ def qasm_label(self):
+ return "cu1"
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0 = self.control_qubits[0]
+ q1 = self.target_qubits[0]
+ theta = -self.parameters[0]
+ return self.__class__(q0, q1, theta) # pylint: disable=E1130
+
+
+class CU2(_CUn_):
+ """Controlled second general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\frac{1}{\\sqrt{2}}
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & e^{-i(\\phi + \\lambda )/2} & -e^{-i(\\phi - \\lambda )/2} \\\\
+ 0 & 0 & e^{i(\\phi - \\lambda )/2} & e^{i (\\phi + \\lambda )/2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ phi (float): first rotation angle.
+ lamb (float): second rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, phi, lam, trainable=True):
+ super().__init__(q0, q1, trainable=trainable)
+ self.name = "cu2"
+ self.draw_label = "U2"
+ self.nparams = 2
+ self.init_kwargs = {"phi": phi, "lam": lam, "trainable": trainable}
+
+ self.parameter_names = ["phi", "lam"]
+ self.parameters = phi, lam
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0 = self.control_qubits[0]
+ q1 = self.target_qubits[0]
+ phi, lam = self.parameters
+ phi, lam = math.pi - lam, -math.pi - phi
+ return self.__class__(q0, q1, phi, lam)
+
+
+class CU3(_CUn_):
+ """Controlled third general unitary gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & e^{-i(\\phi + \\lambda )/2}\\cos\\left (\\frac{\\theta }{2}\\right ) &
+ -e^{-i(\\phi - \\lambda )/2}\\sin\\left (\\frac{\\theta }{2}\\right ) \\\\
+ 0 & 0 & e^{i(\\phi - \\lambda )/2}\\sin\\left (\\frac{\\theta }{2}\\right ) &
+ e^{i (\\phi + \\lambda )/2}\\cos\\left (\\frac{\\theta }{2}\\right ) \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the control qubit id number.
+ q1 (int): the target qubit id number.
+ theta (float): first rotation angle.
+ phi (float): second rotation angle.
+ lamb (float): third rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, phi, lam, trainable=True):
+ super().__init__(q0, q1, trainable=trainable)
+ self.name = "cu3"
+ self.draw_label = "U3"
+ self.nparams = 3
+ self._theta, self._phi, self._lam = None, None, None
+ self.init_kwargs = {
+ "theta": theta,
+ "phi": phi,
+ "lam": lam,
+ "trainable": trainable,
+ }
+ self.parameter_names = ["theta", "phi", "lam"]
+ self.parameters = theta, phi, lam
+
+ @property
+ def qasm_label(self):
+ return "cu3"
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0 = self.control_qubits[0]
+ q1 = self.target_qubits[0]
+ theta, lam, phi = tuple(-x for x in self.parameters) # pylint: disable=E1130
+ return self.__class__(q0, q1, theta, phi, lam)
+
+
+class SWAP(Gate):
+ """The swap gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "swap"
+ self.draw_label = "x"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "swap"
+
+
+class iSWAP(Gate):
+ """The iSWAP gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & i & 0 \\\\
+ 0 & i & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "iswap"
+ self.draw_label = "i"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "iswap"
+
+
+class SiSWAP(Gate):
+ """The :math:`\\sqrt{\\text{iSWAP}}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1/\\sqrt{2} & i/\\sqrt{2} & 0 \\\\
+ 0 & i/\\sqrt{2} & 1/\\sqrt{2} & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "siswap"
+ self.draw_label = "si"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ def _dagger(self) -> "Gate":
+ return SiSWAPDG(*self.qubits)
+
+
+class SiSWAPDG(Gate):
+ """The :math:`\\left(\\sqrt{\\text{iSWAP}}\\right)^{\\dagger}` gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 1/\\sqrt{2} & -i/\\sqrt{2} & 0 \\\\
+ 0 & -i/\\sqrt{2} & 1/\\sqrt{2} & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "siswapdg"
+ self.draw_label = "sidg"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ def _dagger(self) -> "Gate":
+ return SiSWAP(*self.qubits)
+
+
+class FSWAP(Gate):
+ """The fermionic swap gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & -1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be f-swapped id number.
+ q1 (int): the second qubit to be f-swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "fswap"
+ self.draw_label = "fx"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ @property
+ def qasm_label(self):
+ return "fswap"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """"""
+ q0, q1 = self.target_qubits
+ return [X(q1)] + GIVENS(q0, q1, np.pi / 2).decompose() + [X(q0)]
+
+
+class fSim(ParametrizedGate):
+ """The fSim gate defined in `arXiv:2001.08343
+ `_.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & \\cos \\theta & -i\\sin \\theta & 0 \\\\
+ 0 & -i\\sin \\theta & \\cos \\theta & 0 \\\\
+ 0 & 0 & 0 & e^{-i \\phi } \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ theta (float): Angle for the one-qubit rotation.
+ phi (float): Angle for the ``|11>`` phase.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ # TODO: Check how this works with QASM.
+
+ def __init__(self, q0, q1, theta, phi, trainable=True):
+ super().__init__(trainable)
+ self.name = "fsim"
+ self.draw_label = "f"
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ self.parameter_names = ["theta", "phi"]
+ self.parameters = theta, phi
+ self.nparams = 2
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"theta": theta, "phi": phi, "trainable": trainable}
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0, q1 = self.target_qubits
+ params = (-x for x in self.parameters) # pylint: disable=E1130
+ return self.__class__(q0, q1, *params)
+
+
+class SYC(Gate):
+ """The Sycamore gate, defined in the Supplementary Information of `Quantum
+ supremacy using a programmable superconducting processor
+ `_.
+
+ Corresponding to the following unitary matrix
+
+ .. math::
+ \\text{fSim}(\\pi / 2, \\, \\pi / 6) = \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & -i & 0 \\\\
+ 0 & -i & 0 & 0 \\\\
+ 0 & 0 & 0 & e^{-i \\pi / 6} \\\\
+ \\end{pmatrix} \\, ,
+
+ where :math:`\\text{fSim}` is the :class:`qibo.gates.fSim` gate.
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "syc"
+ self.draw_label = "SYC"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ def _dagger(self) -> "Gate":
+ """"""
+ return fSim(*self.target_qubits, -np.pi / 2, -np.pi / 6)
+
+
+class GeneralizedfSim(ParametrizedGate):
+ """The fSim gate with a general rotation.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & R_{00} & R_{01} & 0 \\\\
+ 0 & R_{10} & R_{11} & 0 \\\\
+ 0 & 0 & 0 & e^{-i \\phi } \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ unitary (np.ndarray): Unitary that corresponds to the one-qubit rotation.
+ phi (float): Angle for the ``|11>`` phase.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, unitary, phi, trainable=True):
+ super().__init__(trainable)
+ self.name = "generalizedfsim"
+ self.draw_label = "gf"
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ self.parameter_names = ["unitary", "phi"]
+ self.parameters = unitary, phi
+ self.nparams = 5
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"unitary": unitary, "phi": phi, "trainable": trainable}
+
+ def _dagger(self):
+ q0, q1 = self.target_qubits
+ u, phi = self.parameters
+ init_kwargs = dict(self.init_kwargs)
+ init_kwargs["unitary"] = np.conj(np.transpose(u))
+ init_kwargs["phi"] = -phi
+ return self.__class__(q0, q1, **init_kwargs)
+
+ @Gate.parameters.setter
+ def parameters(self, x):
+ shape = tuple(x[0].shape)
+ if shape != (2, 2):
+ raise_error(
+ ValueError,
+ f"Invalid rotation shape {shape} for generalized fSim gate",
+ )
+ ParametrizedGate.parameters.fset(self, x) # pylint: disable=no-member
+
+
+class _Rnn_(ParametrizedGate):
+ """Abstract class for defining the RXX, RYY, RZZ, and RZX rotations.
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = None
+ self._controlled_gate = None
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ self.parameters = theta
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0, q1 = self.target_qubits
+ return self.__class__(q0, q1, -self.parameters[0]) # pylint: disable=E1130
+
+
+class RXX(_Rnn_):
+ """Parametric 2-qubit XX interaction, or rotation about XX-axis.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos \\frac{\\theta }{2} & 0 & 0 & -i\\sin \\frac{\\theta }{2} \\\\
+ 0 & \\cos \\frac{\\theta }{2} & -i\\sin \\frac{\\theta }{2} & 0 \\\\
+ 0 & -i\\sin \\frac{\\theta }{2} & \\cos \\frac{\\theta }{2} & 0 \\\\
+ -i\\sin \\frac{\\theta }{2} & 0 & 0 & \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "rxx"
+ self.draw_label = "RXX"
+
+ @property
+ def qasm_label(self):
+ return "rxx"
+
+
+class RYY(_Rnn_):
+ """Parametric 2-qubit YY interaction, or rotation about YY-axis.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos \\frac{\\theta }{2} & 0 & 0 & i\\sin \\frac{\\theta }{2} \\\\
+ 0 & \\cos \\frac{\\theta }{2} & -i\\sin \\frac{\\theta }{2} & 0 \\\\
+ 0 & -i\\sin \\frac{\\theta }{2} & \\cos \\frac{\\theta }{2} & 0 \\\\
+ i\\sin \\frac{\\theta }{2} & 0 & 0 & \\cos \\frac{\\theta }{2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "ryy"
+ self.draw_label = "RYY"
+
+ @property
+ def qasm_label(self):
+ return "ryy"
+
+
+class RZZ(_Rnn_):
+ """Parametric 2-qubit ZZ interaction, or rotation about ZZ-axis.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ e^{-i \\theta / 2} & 0 & 0 & 0 \\\\
+ 0 & e^{i \\theta / 2} & 0 & 0 \\\\
+ 0 & 0 & e^{i \\theta / 2} & 0 \\\\
+ 0 & 0 & 0 & e^{-i \\theta / 2} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "rzz"
+ self.draw_label = "RZZ"
+
+ @property
+ def qasm_label(self):
+ return "rzz"
+
+
+class RZX(_Rnn_):
+ """Parametric 2-qubit ZX interaction, or rotation about ZX-axis.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\text{RX}(\\theta) & 0 \\\\
+ 0 & \\text{RX}(-\\theta) \\\\
+ \\end{pmatrix} =
+ \\begin{pmatrix}
+ \\cos{\\frac{\\theta}{2}} & -i \\sin{\\frac{\\theta}{2}} & 0 & 0 \\\\
+ -i \\sin{\\frac{\\theta}{2}} & \\cos{\\frac{\\theta}{2}} & 0 & 0 \\\\
+ 0 & 0 & \\cos{\\frac{\\theta}{2}} & i \\sin{\\frac{\\theta}{2}} \\\\
+ 0 & 0 & i \\sin{\\frac{\\theta}{2}} & \\cos{\\frac{\\theta}{2}} \\\\
+ \\end{pmatrix} \\, ,
+
+ where :math:`\\text{RX}` is the :class:`qibo.gates.RX` gate.
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "rzx"
+ self.draw_label = "RZX"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """"""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class RXXYY(_Rnn_):
+ """Parametric 2-qubit :math:`XX + YY` interaction, or rotation about
+ :math:`XX + YY`-axis.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\exp\\left(-i \\frac{\\theta}{4}(XX + YY)\\right) =
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & \\cos{\\frac{\\theta}{2}} & -i \\sin{\\frac{\\theta}{2}} & 0 \\\\
+ 0 & -i \\sin{\\frac{\\theta}{2}} & \\cos{\\frac{\\theta}{2}} & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix} \\, ,
+
+ Args:
+ q0 (int): the first entangled qubit id number.
+ q1 (int): the second entangled qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(q0, q1, theta, trainable)
+ self.name = "rxxyy"
+ self.draw_label = "RXXYY"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """Decomposition of :math:`\\text{R_{XX-YY}}` up to global phase.
+
+ This decomposition has a global phase difference with respect to
+ the original gate due to a phase difference in
+ :math:`\\left(\\sqrt{X}\\right)^{\\dagger}`.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class MS(ParametrizedGate):
+ """The Mølmer–Sørensen (MS) gate is a two-qubit gate native to trapped
+ ions.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ \\cos(\\theta / 2) & 0 & 0 & -i e^{-i( \\phi_0 + \\phi_1)} \\sin(\\theta / 2) \\\\
+ 0 & \\cos(\\theta / 2) & -i e^{-i( \\phi_0 - \\phi_1)} \\sin(\\theta / 2) & 0 \\\\
+ 0 & -i e^{i( \\phi_0 - \\phi_1)} \\sin(\\theta / 2) & \\cos(\\theta / 2) & 0 \\\\
+ -i e^{i( \\phi_0 + \\phi_1)} \\sin(\\theta / 2) & 0 & 0 & \\cos(\\theta / 2) \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit to be swapped id number.
+ q1 (int): the second qubit to be swapped id number.
+ phi0 (float): first qubit's phase.
+ phi1 (float): second qubit's phase
+ theta (float, optional): arbitrary angle in the interval
+ :math:`0 \\leq \\theta \\leq \\pi /2`. If :math:`\\theta \\rightarrow \\pi / 2`,
+ the fully-entangling MS gate is defined. Defaults to :math:`\\pi / 2`.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ # TODO: Check how this works with QASM.
+
+ def __init__(self, q0, q1, phi0, phi1, theta: float = math.pi / 2, trainable=True):
+ super().__init__(trainable)
+ self.name = "ms"
+ self.draw_label = "MS"
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ if theta < 0.0 or theta > math.pi / 2:
+ raise_error(
+ ValueError,
+ f"Theta is defined in the interval 0 <= theta <= pi/2, but it is {theta}.",
+ )
+
+ self.parameter_names = ["phi0", "phi1", "theta"]
+ self.parameters = phi0, phi1, theta
+ self.nparams = 3
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {
+ "phi0": phi0,
+ "phi1": phi1,
+ "theta": theta,
+ "trainable": trainable,
+ }
+
+ @property
+ def qasm_label(self):
+ return "ms"
+
+ def _dagger(self) -> "Gate":
+ """"""
+ q0, q1 = self.target_qubits
+ phi0, phi1, theta = self.parameters
+ return self.__class__(q0, q1, phi0 + math.pi, phi1, theta)
+
+
+class GIVENS(ParametrizedGate):
+ """The Givens gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & \\cos(\\theta) & -\\sin(\\theta) & 0 \\\\
+ 0 & \\sin(\\theta) & \\cos(\\theta) & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit id number.
+ q1 (int): the second qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = "g"
+ self.draw_label = "G"
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ self.parameter_names = "theta"
+ self.parameters = theta
+ self.nparams = 1
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ def _dagger(self) -> "Gate":
+ """"""
+ return self.__class__(*self.target_qubits, -self.parameters[0])
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """Decomposition of RBS gate according to `ArXiv:2109.09685
+ `_."""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class RBS(ParametrizedGate):
+ """The Reconfigurable Beam Splitter gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 \\\\
+ 0 & \\cos(\\theta) & \\sin(\\theta) & 0 \\\\
+ 0 & -\\sin(\\theta) & \\cos(\\theta) & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ \\end{pmatrix}
+
+ Note that, in our implementation, :math:`\\text{RBS}(\\theta) = \\text{Givens}(-\\theta)`,
+ where :math:`\\text{Givens}` is the :class:`qibo.gates.GIVENS` gate.
+ However, we point out that this definition is not unique.
+
+ Args:
+ q0 (int): the first qubit id number.
+ q1 (int): the second qubit id number.
+ theta (float): the rotation angle.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, q0, q1, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = "rbs"
+ self.draw_label = "RBS"
+ self.target_qubits = (q0, q1)
+ self.unitary = True
+
+ self.parameter_names = "theta"
+ self.parameters = theta
+ self.nparams = 1
+
+ self.init_args = [q0, q1]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+ def _dagger(self) -> "Gate":
+ """"""
+ return self.__class__(*self.target_qubits, -self.parameters[0])
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """Decomposition of RBS gate according to `ArXiv:2109.09685
+ `_."""
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class ECR(Gate):
+ """THe Echo Cross-Resonance gate.
+
+ Corresponds ot the following matrix
+
+ .. math::
+ \\frac{1}{\\sqrt{2}} \\left( X \\, I - Y \\, X \\right) =
+ \\frac{1}{\\sqrt{2}} \\, \\begin{pmatrix}
+ 0 & 0 & 1 & i \\\\
+ 0 & 0 & i & 1 \\\\
+ 1 & -i & 0 & 0 \\\\
+ -i & 1 & 0 & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first qubit id number.
+ q1 (int): the second qubit id number.
+ """
+
+ def __init__(self, q0, q1):
+ super().__init__()
+ self.name = "ecr"
+ self.draw_label = "ECR"
+ self.target_qubits = (q0, q1)
+ self.init_args = [q0, q1]
+ self.unitary = True
+
+ @property
+ def clifford(self):
+ return True
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ """Decomposition of :math:`\\textup{ECR}` gate up to global phase.
+
+ A global phase difference exists between the definitions of
+ :math:`\\textup{ECR}` and this decomposition. More precisely,
+
+ .. math::
+ \\textup{ECR} = e^{i 7 \\pi / 4} \\, S(q_{0}) \\, \\sqrt{X}(q_{1}) \\,
+ \\textup{CNOT}(q_{0}, q_{1}) \\, X(q_{0})
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class TOFFOLI(Gate):
+ """The Toffoli gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first control qubit id number.
+ q1 (int): the second control qubit id number.
+ q2 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1, q2):
+ super().__init__()
+ self.name = "ccx"
+ self.draw_label = "X"
+ self.control_qubits = (q0, q1)
+ self.target_qubits = (q2,)
+ self.init_args = [q0, q1, q2]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "ccx"
+
+ def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
+ c0, c1 = self.control_qubits
+ t = self.target_qubits[0]
+ return [self.__class__(c0, c1, t)]
+
+ def congruent(self, use_toffolis: bool = True) -> List[Gate]:
+ """Congruent representation of ``TOFFOLI`` gate.
+
+ This is a helper method for the decomposition of multi-control ``X`` gates.
+ The congruent representation is based on Sec. 6.2 of
+ `arXiv:9503016 `_.
+ The sequence of the gates produced here has the same effect as ``TOFFOLI``
+ with the phase of the ``|101>`` state reversed.
+
+ Args:
+ use_toffolis: If ``True`` a single ``TOFFOLI`` gate is returned.
+ If ``False`` the congruent representation is returned.
+
+ Returns:
+ List with ``RY`` and ``CNOT`` gates that have the same effect as
+ applying the original ``TOFFOLI`` gate.
+ """
+ if use_toffolis:
+ return self.decompose()
+
+ control0, control1 = self.control_qubits
+ target = self.target_qubits[0]
+ return [
+ RY(target, -math.pi / 4),
+ CNOT(control1, target),
+ RY(target, -math.pi / 4),
+ CNOT(control0, target),
+ RY(target, math.pi / 4),
+ CNOT(control1, target),
+ RY(target, math.pi / 4),
+ ]
+
+
+class CCZ(Gate):
+ """The controlled-CZ gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & 0 & -1 \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first control qubit id number.
+ q1 (int): the second control qubit id number.
+ q2 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1, q2):
+ super().__init__()
+ self.name = "ccz"
+ self.draw_label = "Z"
+ self.control_qubits = (q0, q1)
+ self.target_qubits = (q2,)
+ self.init_args = [q0, q1, q2]
+ self.unitary = True
+
+ @property
+ def qasm_label(self):
+ return "ccz"
+
+ def decompose(self) -> List[Gate]:
+ """Decomposition of :math:`\\text{CCZ}` gate.
+
+ Decompose :math:`\\text{CCZ}` gate into :class:`qibo.gates.H` in
+ the target qubit, followed by :class:`qibo.gates.TOFFOLI`, followed
+ by a :class:`qibo.gates.H` in the target qubit.
+ """
+ from qibo.transpiler.decompositions import ( # pylint: disable=C0415
+ standard_decompositions,
+ )
+
+ return standard_decompositions(self)
+
+
+class DEUTSCH(ParametrizedGate):
+ """The Deutsch gate.
+
+ Corresponds to the following unitary matrix
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & i \\cos{\\theta} & \\sin{\\theta} \\\\
+ 0 & 0 & 0 & 0 & 0 & 0 & \\sin{\\theta} & i \\cos{\\theta} \\\\
+ \\end{pmatrix}
+
+ Args:
+ q0 (int): the first control qubit id number.
+ q1 (int): the second control qubit id number.
+ q2 (int): the target qubit id number.
+ """
+
+ def __init__(self, q0, q1, q2, theta, trainable=True):
+ super().__init__(trainable)
+ self.name = "deutsch"
+ self.draw_label = "DE"
+ self.control_qubits = (q0, q1)
+ self.target_qubits = (q2,)
+ self.unitary = True
+
+ self.parameter_names = "theta"
+ self.parameters = theta
+ self.nparams = 1
+
+ self.init_args = [q0, q1, q2]
+ self.init_kwargs = {"theta": theta, "trainable": trainable}
+
+
+class Unitary(ParametrizedGate):
+ """Arbitrary unitary gate.
+
+ Args:
+ unitary: Unitary matrix as a tensor supported by the backend.
+ *q (int): Qubit id numbers that the gate acts on.
+ trainable (bool): whether gate parameters can be updated using
+ :meth:`qibo.models.circuit.Circuit.set_parameters`.
+ Defaults to ``True``.
+ name (str): Optional name for the gate.
+ check_unitary (bool): if ``True``, checks if ``unitary`` is an unitary operator.
+ If ``False``, check is not performed and ``unitary`` attribute
+ defaults to ``False``. Note that, even when the check is performed,
+ there is no enforcement. This allows the user to create
+ non-unitary gates. Default is ``True``.
+ """
+
+ def __init__(
+ self, unitary, *q, trainable=True, name: str = None, check_unitary: bool = True
+ ):
+ super().__init__(trainable)
+ self.name = "Unitary" if name is None else name
+ self.draw_label = "U"
+ self.target_qubits = tuple(q)
+ self._clifford = False
+
+ # TODO: Check that given ``unitary`` has proper shape?
+ self.parameter_names = "u"
+ self._parameters = (unitary,)
+ self.nparams = 4 ** len(self.target_qubits)
+
+ self.init_args = [unitary] + list(q)
+ self.init_kwargs = {
+ "name": name,
+ "check_unitary": check_unitary,
+ "trainable": trainable,
+ }
+
+ if check_unitary:
+ engine = _check_engine(unitary)
+ product = engine.transpose(engine.conj(unitary), (1, 0)) @ unitary
+ diagonals = all(engine.abs(1 - engine.diag(product)) < PRECISION_TOL)
+ off_diagonals = bool(
+ engine.all(
+ engine.abs(product - engine.diag(engine.diag(product)))
+ < PRECISION_TOL
+ )
+ )
+
+ self.unitary = True if diagonals and off_diagonals else False
+ del diagonals, off_diagonals, product
+
+ @Gate.parameters.setter
+ def parameters(self, x):
+ shape = self.parameters[0].shape
+ engine = _check_engine(self.parameters[0])
+ # Reshape doesn't accept a tuple if engine is pytorch.
+ x = x[0] if type(x) is tuple else x
+ self._parameters = (engine.reshape(x, shape),)
+ for gate in self.device_gates: # pragma: no cover
+ gate.parameters = x
+
+ @property
+ def clifford(self):
+ return self._clifford
+
+ @clifford.setter
+ def clifford(self, value):
+ self._clifford = value
+
+ def on_qubits(self, qubit_map):
+ args = [self.init_args[0]]
+ args.extend(qubit_map.get(i) for i in self.target_qubits)
+ gate = self.__class__(*args, **self.init_kwargs)
+ if self.is_controlled_by:
+ controls = (qubit_map.get(i) for i in self.control_qubits)
+ gate = gate.controlled_by(*controls)
+ gate.parameters = self.parameters
+ return gate
+
+ def _dagger(self):
+ engine = _check_engine(self.parameters[0])
+ ud = engine.conj(self.parameters[0].T)
+ return self.__class__(ud, *self.target_qubits, **self.init_kwargs)
+
+
+def _check_engine(array):
+ """Check if the array is a numpy or torch tensor and return the corresponding library."""
+ if array.__class__.__name__ == "Tensor":
+ import torch # pylint: disable=C0415
+
+ return torch
+ else:
+ return np
diff --git a/src/qibo/gates/measurements.py b/src/qibo/gates/measurements.py
new file mode 100644
index 000000000..50b4c29e7
--- /dev/null
+++ b/src/qibo/gates/measurements.py
@@ -0,0 +1,257 @@
+import json
+from typing import Dict, Optional, Tuple
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.gates.abstract import Gate
+from qibo.gates.gates import Z
+from qibo.measurements import MeasurementResult
+
+
+class M(Gate):
+ """The measure gate.
+
+ Args:
+ *q (int): id numbers of the qubits to measure.
+ It is possible to measure multiple qubits using ``gates.M(0, 1, 2, ...)``.
+ If the qubits to measure are held in an iterable (eg. list) the ``*``
+ operator can be used, for example ``gates.M(*[0, 1, 4])`` or
+ ``gates.M(*range(5))``.
+ register_name (str): Optional name of the register to distinguish it
+ from other registers when used in circuits.
+ collapse (bool): Collapse the state vector after the measurement is
+ performed. Can be used only for single shot measurements.
+ If ``True`` the collapsed state vector is returned. If ``False``
+ the measurement result is returned.
+ basis (:class:`qibo.gates.Gate`, list): Basis to measure.
+ Can be a qibo gate or a callable that accepts a qubit,
+ for example: ``lambda q: gates.RX(q, 0.2)``
+ or a list of these, if a different basis will be used for each
+ measurement qubit.
+ Default is Z.
+ p0 (dict): Optional bitflip probability map. Can be:
+ A dictionary that maps each measured qubit to the probability
+ that it is flipped, a list or tuple that has the same length
+ as the tuple of measured qubits or a single float number.
+ If a single float is given the same probability will be used
+ for all qubits.
+ p1 (dict): Optional bitflip probability map for asymmetric bitflips.
+ Same as ``p0`` but controls the 1->0 bitflip probability.
+ If ``p1`` is ``None`` then ``p0`` will be used both for 0->1 and
+ 1->0 bitflips.
+ """
+
+ def __init__(
+ self,
+ *q,
+ register_name: Optional[str] = None,
+ collapse: bool = False,
+ basis: Gate = Z,
+ p0: Optional["ProbsType"] = None,
+ p1: Optional["ProbsType"] = None,
+ ):
+ super().__init__()
+ self.name = "measure"
+ self.draw_label = "M"
+ self.target_qubits = tuple(q)
+ self.register_name = register_name
+ self.collapse = collapse
+ self.result = MeasurementResult(self)
+ # list of measurement pulses implementing the gate
+ # relevant for experiments only
+ self.pulses = None
+ # saving basis for __repr__ ans save to file
+ if not isinstance(basis, list):
+ self.basis_gates = len(q) * [basis]
+ else:
+ self.basis_gates = basis
+
+ self.init_args = q
+ self.init_kwargs = {
+ "register_name": register_name,
+ "collapse": collapse,
+ "p0": p0,
+ "p1": p1,
+ }
+ if collapse:
+ if p0 is not None or p1 is not None:
+ raise_error(
+ NotImplementedError,
+ "Bitflip measurement noise is not available when collapsing.",
+ )
+
+ if p1 is None:
+ p1 = p0
+ if p0 is None:
+ p0 = p1
+ self.bitflip_map = (self._get_bitflip_map(p0), self._get_bitflip_map(p1))
+
+ # list of gates that will be added to the circuit before the
+ # measurement, in order to rotate to the given basis
+ if not isinstance(basis, list):
+ basis = len(self.target_qubits) * [basis]
+ elif len(basis) != len(self.target_qubits):
+ raise_error(
+ ValueError,
+ f"Given basis list has length {len(basis)} while "
+ f"we are measuring {len(self.target_qubits)} qubits.",
+ )
+ self.basis = []
+ for qubit, basis_cls in zip(self.target_qubits, basis):
+ gate = basis_cls(qubit).basis_rotation()
+ if gate is not None:
+ self.basis.append(gate)
+
+ @staticmethod
+ def _get_bitflip_tuple(qubits: Tuple[int], probs: "ProbsType") -> Tuple[float]:
+ if isinstance(probs, float):
+ if probs < 0 or probs > 1: # pragma: no cover
+ raise_error(ValueError, f"Invalid bitflip probability {probs}.")
+ return len(qubits) * (probs,)
+
+ if isinstance(probs, (tuple, list)):
+ if len(probs) != len(qubits):
+ raise_error(
+ ValueError,
+ f"{len(qubits)} qubits were measured but the given "
+ + f"bitflip probability list contains {len(probs)} values.",
+ )
+ return tuple(probs)
+
+ if isinstance(probs, dict):
+ diff = set(probs.keys()) - set(qubits)
+ if diff:
+ raise_error(
+ KeyError,
+ f"Bitflip map contains {diff} qubits that are not measured.",
+ )
+ return tuple(probs[q] if q in probs else 0.0 for q in qubits)
+
+ raise_error(TypeError, f"Invalid type {probs} of bitflip map.")
+
+ def _get_bitflip_map(self, p: Optional["ProbsType"] = None) -> Dict[int, float]:
+ """Creates dictionary with bitflip probabilities."""
+ if p is None:
+ return {q: 0 for q in self.qubits}
+ pt = self._get_bitflip_tuple(self.qubits, p)
+ return dict(zip(self.qubits, pt))
+
+ def has_bitflip_noise(self):
+ return (
+ sum(self.bitflip_map[0].values()) > 0
+ or sum(self.bitflip_map[1].values()) > 0
+ )
+
+ def add(self, gate):
+ """Adds target qubits to a measurement gate.
+
+ This method is only used for creating the global measurement gate used
+ by the `models.Circuit`.
+ The user is not supposed to use this method and a `ValueError` is
+ raised if he does so.
+
+ Args:
+ gate: Measurement gate to add its qubits in the current gate.
+ """
+ assert isinstance(gate, self.__class__)
+ self.target_qubits += gate.target_qubits
+ self.bitflip_map[0].update(gate.bitflip_map[0])
+ self.bitflip_map[1].update(gate.bitflip_map[1])
+
+ def controlled_by(self, *q):
+ """"""
+ raise_error(NotImplementedError, "Measurement gates cannot be controlled.")
+
+ def matrix(self, backend=None):
+ """"""
+ raise_error(
+ NotImplementedError, "Measurement gates do not have matrix representation."
+ )
+
+ def apply(self, backend, state, nqubits):
+ self.result.backend = backend
+ if not self.collapse:
+ return state
+
+ qubits = sorted(self.target_qubits)
+ # measure and get result
+ probs = backend.calculate_probabilities(state, qubits, nqubits)
+ shot = self.result.add_shot(probs)
+ # collapse state
+ return backend.collapse_state(state, qubits, shot, nqubits)
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ self.result.backend = backend
+ if not self.collapse:
+ return state
+
+ qubits = sorted(self.target_qubits)
+ # measure and get result
+ probs = backend.calculate_probabilities_density_matrix(state, qubits, nqubits)
+ shot = self.result.add_shot(probs)
+ # collapse state
+ return backend.collapse_density_matrix(state, qubits, shot, nqubits)
+
+ def apply_clifford(self, backend, state, nqubits):
+ self.result.backend = backend
+ if not self.collapse:
+ return state
+
+ qubits = sorted(self.target_qubits)
+ sample = backend.sample_shots(state, qubits, nqubits, 1, self.collapse)
+ self.result.add_shot_from_sample(sample[0])
+ return state
+
+ def to_json(self):
+ """Serializes the measurement gate to json."""
+ encoding = json.loads(super().to_json())
+ encoding.pop("_control_qubits")
+ encoding.update({"basis": [g.__name__ for g in self.basis_gates]})
+ return json.dumps(encoding)
+
+ @classmethod
+ def load(cls, payload):
+ """Constructs a measurement gate starting from a json serialized
+ one."""
+ args = json.loads(payload)
+ # drop general serialization data, unused in this specialized loader
+ for key in ("name", "init_args", "_class"):
+ args.pop(key)
+ qubits = args.pop("_target_qubits")
+ args["basis"] = [getattr(gates, g) for g in args["basis"]]
+ args.update(args.pop("init_kwargs"))
+ return cls(*qubits, **args)
+
+ # Overload on_qubits to copy also gate.result, controlled by can be removed for measurements
+ def on_qubits(self, qubit_map) -> "Gate":
+ """Creates the same measurement gate targeting different qubits
+ and preserving the measurement result register.
+
+ Args:
+ qubit_map (int): Dictionary mapping original qubit indices to new ones.
+
+ Returns:
+ A :class:`qibo.gates.Gate.M` object of the original gate
+ type targeting the given qubits.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import models, gates
+ measurement = gates.M(0, 1)
+ c = models.Circuit(3)
+ c.add(measurement.on_qubits({0: 0, 1: 2}))
+ assert c.queue[0].result is measurement.result
+ print(c.draw())
+ .. testoutput::
+
+ q0: ─M─
+ q1: ─|─
+ q2: ─M─
+ """
+
+ qubits = (qubit_map.get(q) for q in self.qubits)
+ gate = self.__class__(*qubits, **self.init_kwargs)
+ gate.result = self.result
+ return gate
diff --git a/src/qibo/gates/special.py b/src/qibo/gates/special.py
new file mode 100644
index 000000000..3824f3672
--- /dev/null
+++ b/src/qibo/gates/special.py
@@ -0,0 +1,163 @@
+from qibo.backends import _check_backend
+from qibo.gates.abstract import SpecialGate
+from qibo.gates.measurements import M
+
+
+class CallbackGate(SpecialGate):
+ """Calculates a :class:`qibo.callbacks.Callback` at a specific point in the circuit.
+
+ This gate performs the callback calulation without affecting the state vector.
+
+ Args:
+ callback (:class:`qibo.callbacks.Callback`): Callback object to calculate.
+ """
+
+ def __init__(self, callback: "Callback"):
+ super().__init__()
+ self.name = callback.__class__.__name__
+ self.draw_label = "".join([c for c in self.name if c.isupper()])
+ self.callback = callback
+ self.init_args = [callback]
+
+ def apply(self, backend, state, nqubits):
+ self.callback.nqubits = nqubits
+ self.callback.apply(backend, state)
+ return state
+
+ def apply_density_matrix(self, backend, state, nqubits):
+ self.callback.nqubits = nqubits
+ self.callback.apply_density_matrix(backend, state)
+ return state
+
+
+class FusedGate(SpecialGate):
+ """Collection of gates that will be fused and applied as single gate during simulation.
+ This gate is constructed automatically by :meth:`qibo.models.circuit.Circuit.fuse`
+ and should not be used by user.
+ """
+
+ def __init__(self, *q):
+ super().__init__()
+ self.name = "Fused Gate"
+ self.draw_label = "[]"
+ self.target_qubits = tuple(sorted(q))
+ self.init_args = list(q)
+ self.qubit_set = set(q)
+ self.gates = []
+ self.marked = False
+ self.fused = False
+
+ self.left_neighbors = {}
+ self.right_neighbors = {}
+
+ @classmethod
+ def from_gate(cls, gate):
+ fgate = cls(*gate.qubits)
+ fgate.append(gate)
+ if isinstance(gate, (M, SpecialGate)):
+ # special gates do not participate in fusion
+ fgate.marked = True
+ return fgate
+
+ def prepend(self, gate):
+ self.qubit_set = self.qubit_set | set(gate.qubits)
+ self.init_args = sorted(self.qubit_set)
+ self.target_qubits = tuple(self.init_args)
+ if isinstance(gate, self.__class__):
+ self.gates = gate.gates + self.gates
+ else:
+ self.gates = [gate] + self.gates
+
+ def append(self, gate):
+ self.qubit_set = self.qubit_set | set(gate.qubits)
+ self.init_args = sorted(self.qubit_set)
+ self.target_qubits = tuple(self.init_args)
+ if isinstance(gate, self.__class__):
+ self.gates.extend(gate.gates)
+ else:
+ self.gates.append(gate)
+
+ def _dagger(self):
+ dagger = self.__class__(*self.init_args)
+ for gate in self.gates[::-1]:
+ dagger.append(gate.dagger())
+ return dagger
+
+ def can_fuse(self, gate, max_qubits):
+ """Check if two gates can be fused."""
+ if gate is None:
+ return False
+ if self.marked or gate.marked:
+ # gates are already fused
+ return False
+ if len(self.qubit_set | gate.qubit_set) > max_qubits:
+ # combined qubits are more than ``max_qubits``
+ return False
+ return True
+
+ def matrix(self, backend=None):
+ """Returns matrix representation of special gate.
+
+ Args:
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Matrix representation of special gate.
+ """
+ backend = _check_backend(backend)
+
+ return backend.matrix_fused(self)
+
+ def fuse(self, gate):
+ """Fuses two gates."""
+ left_gates = set(self.right_neighbors.values()) - {gate}
+ right_gates = set(gate.left_neighbors.values()) - {self}
+ if len(left_gates) > 0 and len(right_gates) > 0:
+ # abort if there are blocking gates between the two gates
+ # not in the shared qubits
+ return
+
+ qubits = self.qubit_set & gate.qubit_set
+ # the gate with most neighbors different than the two gates to
+ # fuse will be the parent
+ if len(left_gates) > len(right_gates):
+ parent, child = self, gate
+ between_gates = {parent.right_neighbors.get(q) for q in qubits}
+ if between_gates == {child}:
+ child.marked = True
+ parent.append(child)
+ for q in qubits:
+ neighbor = child.right_neighbors.get(q)
+ if neighbor is not None:
+ parent.right_neighbors[q] = neighbor
+ neighbor.left_neighbors[q] = parent
+ else:
+ parent.right_neighbors.pop(q)
+ else:
+ parent, child = gate, self
+ between_gates = {parent.left_neighbors.get(q) for q in qubits}
+ if between_gates == {child}:
+ child.marked = True
+ parent.prepend(child)
+ for q in qubits:
+ neighbor = child.left_neighbors.get(q)
+ if neighbor is not None:
+ parent.left_neighbors[q] = neighbor
+ neighbor.right_neighbors[q] = parent
+
+ if child.marked:
+ # update the neighbors graph
+ for q in child.qubit_set - qubits:
+ neighbor = child.right_neighbors.get(q)
+ if neighbor is not None:
+ parent.right_neighbors[q] = neighbor
+ neighbor.left_neighbors[q] = parent
+ neighbor = child.left_neighbors.get(q)
+ if neighbor is not None:
+ parent.left_neighbors[q] = neighbor
+ neighbor.right_neighbors[q] = parent
+
+ def apply_clifford(self, backend, state, nqubits):
+ for gate in self.gates:
+ state = gate.apply_clifford(backend, state, nqubits)
+ return state
diff --git a/src/qibo/hamiltonians/__init__.py b/src/qibo/hamiltonians/__init__.py
new file mode 100644
index 000000000..65bb0891a
--- /dev/null
+++ b/src/qibo/hamiltonians/__init__.py
@@ -0,0 +1,2 @@
+from qibo.hamiltonians.hamiltonians import *
+from qibo.hamiltonians.models import TFIM, XXZ, MaxCut, X, Y, Z
diff --git a/src/qibo/hamiltonians/abstract.py b/src/qibo/hamiltonians/abstract.py
new file mode 100644
index 000000000..749ad0b21
--- /dev/null
+++ b/src/qibo/hamiltonians/abstract.py
@@ -0,0 +1,128 @@
+from abc import abstractmethod
+
+from qibo.config import raise_error
+
+
+class AbstractHamiltonian:
+ """Qibo abstraction for Hamiltonian objects."""
+
+ def __init__(self):
+ self._nqubits = None
+
+ @property
+ def nqubits(self):
+ return self._nqubits
+
+ @nqubits.setter
+ def nqubits(self, n):
+ if not isinstance(n, int):
+ raise_error(RuntimeError, f"nqubits must be an integer but is {type(n)}.")
+ if n < 1:
+ raise_error(ValueError, f"nqubits must be a positive integer but is {n}")
+ self._nqubits = n
+
+ @abstractmethod
+ def eigenvalues(self, k=6): # pragma: no cover
+ """Computes the eigenvalues for the Hamiltonian.
+
+ Args:
+ k (int): Number of eigenvalues to calculate if the Hamiltonian
+ was created using a sparse matrix. This argument is ignored
+ if the Hamiltonian was created using a dense matrix.
+ See :meth:`qibo.backends.abstract.AbstractBackend.eigvalsh` for
+ more details.
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def eigenvectors(self, k=6): # pragma: no cover
+ """Computes a tensor with the eigenvectors for the Hamiltonian.
+
+ Args:
+ k (int): Number of eigenvalues to calculate if the Hamiltonian
+ was created using a sparse matrix. This argument is ignored
+ if the Hamiltonian was created using a dense matrix.
+ See :meth:`qibo.backends.abstract.AbstractBackend.eigh` for
+ more details.
+ """
+ raise_error(NotImplementedError)
+
+ def ground_state(self):
+ """Computes the ground state of the Hamiltonian.
+
+ Uses :meth:`qibo.hamiltonians.AbstractHamiltonian.eigenvectors`
+ and returns eigenvector corresponding to the lowest energy.
+ """
+ return self.eigenvectors()[:, 0]
+
+ @abstractmethod
+ def exp(self, a): # pragma: no cover
+ """Computes a tensor corresponding to exp(-1j * a * H).
+
+ Args:
+ a (complex): Complex number to multiply Hamiltonian before
+ exponentiation.
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def expectation(self, state, normalize=False): # pragma: no cover
+ """Computes the real expectation value for a given state.
+
+ Args:
+ state (array): the expectation state.
+ normalize (bool): If ``True`` the expectation value is divided
+ with the state's norm squared.
+
+ Returns:
+ Real number corresponding to the expectation value.
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def expectation_from_samples(self, freq, qubit_map=None): # pragma: no cover
+ """Computes the real expectation value of a diagonal observable given the frequencies when measuring in the computational basis.
+
+ Args:
+ freq (collections.Counter): the keys are the observed values in binary form
+ and the values the corresponding frequencies, that is the number
+ of times each measured value/bitstring appears.
+ qubit_map (tuple): Mapping between frequencies and qubits. If None, [1,...,len(key)]
+
+ Returns:
+ Real number corresponding to the expectation value.
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def __add__(self, o): # pragma: no cover
+ """Add operator."""
+ raise_error(NotImplementedError)
+
+ def __radd__(self, o):
+ """Right operator addition."""
+ return self.__add__(o)
+
+ @abstractmethod
+ def __sub__(self, o): # pragma: no cover
+ """Subtraction operator."""
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def __rsub__(self, o): # pragma: no cover
+ """Right subtraction operator."""
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def __mul__(self, o): # pragma: no cover
+ """Multiplication to scalar operator."""
+ raise_error(NotImplementedError)
+
+ def __rmul__(self, o):
+ """Right scalar multiplication."""
+ return self.__mul__(o)
+
+ @abstractmethod
+ def __matmul__(self, o): # pragma: no cover
+ """Matrix multiplication with other Hamiltonians or state vectors."""
+ raise_error(NotImplementedError)
diff --git a/src/qibo/hamiltonians/adiabatic.py b/src/qibo/hamiltonians/adiabatic.py
new file mode 100644
index 000000000..d436861c2
--- /dev/null
+++ b/src/qibo/hamiltonians/adiabatic.py
@@ -0,0 +1,153 @@
+from abc import ABC, abstractmethod
+from itertools import chain
+
+from qibo.config import raise_error
+from qibo.hamiltonians import hamiltonians, terms
+
+
+class AdiabaticHamiltonian(ABC):
+ """Constructor for Hamiltonians used in adiabatic evolution.
+
+ This object is never constructed, it falls back to one of
+ :class:`qibo.core.adiabatic.BaseAdiabaticHamiltonian` or
+ :class:`qibo.core.adiabatic.SymbolicAdiabaticHamiltonian`.
+
+ These objects allow constructing the adiabatic Hamiltonian of the
+ form ``(1 - s) * H0 + s * H1`` efficiently.
+ """
+
+ def __new__(cls, h0, h1):
+ if type(h1) != type(h0):
+ raise_error(
+ TypeError,
+ f"h1 should be of the same type {type(h0)} of h0 but is {type(h1)}.",
+ )
+ if isinstance(h0, hamiltonians.Hamiltonian):
+ return BaseAdiabaticHamiltonian(h0, h1)
+ elif isinstance(h0, hamiltonians.SymbolicHamiltonian):
+ return SymbolicAdiabaticHamiltonian(h0, h1)
+ else:
+ raise_error(
+ TypeError,
+ f"h0 should be a hamiltonians.Hamiltonian object but is {type(h0)}.",
+ )
+
+ def __init__(self, h0, h1): # pragma: no cover
+ self.h0, self.h1 = h0, h1
+ self.schedule = None
+ self.total_time = None
+
+ @abstractmethod
+ def ground_state(self): # pragma: no cover
+ """Returns the ground state of the ``H0`` Hamiltonian.
+
+ Usually used as the initial state for adiabatic evolution.
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def __call__(self, t): # pragma: no cover
+ """Hamiltonian object corresponding to the given time."""
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def circuit(self, dt, accelerators=None, t=0): # pragma: no cover
+ raise_error(NotImplementedError)
+
+
+class BaseAdiabaticHamiltonian:
+ """Adiabatic Hamiltonian that is a sum of :class:`qibo.hamiltonians.hamiltonians.Hamiltonian`."""
+
+ def __init__(self, h0, h1):
+ if h0.nqubits != h1.nqubits:
+ raise_error(
+ ValueError,
+ f"H0 has {h0.nqubits} qubits while H1 has {h1.nqubits}.",
+ )
+ self.nqubits = h0.nqubits
+ if h0.backend != h1.backend: # pragma: no cover
+ raise_error(ValueError, "H0 and H1 have different backends.")
+ self.backend = h0.backend
+ self.h0, self.h1 = h0, h1
+ self.schedule = None
+ self.total_time = None
+
+ def ground_state(self):
+ return self.h0.ground_state()
+
+ def __call__(self, t):
+ """Hamiltonian object corresponding to the given time.
+
+ Returns:
+ A :class:`qibo.hamiltonians.hamiltonians.Hamiltonian` object corresponding
+ to the adiabatic Hamiltonian at a given time.
+ """
+ if t == 0:
+ return self.h0
+ if self.total_time is None or self.schedule is None:
+ raise_error(
+ RuntimeError,
+ "Cannot access adiabatic evolution "
+ "Hamiltonian before setting the "
+ "the total evolution time and "
+ "scheduling.",
+ )
+ st = self.schedule(t / self.total_time) # pylint: disable=E1102
+ return self.h0 * (1 - st) + self.h1 * st
+
+ def circuit(self, dt, accelerators=None, t=0): # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "Trotter circuit is not available " "for full matrix Hamiltonians.",
+ )
+
+
+class SymbolicAdiabaticHamiltonian(BaseAdiabaticHamiltonian):
+ """Adiabatic Hamiltonian that is sum of :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian`."""
+
+ def __init__(self, h0, h1):
+ super().__init__(h0, h1)
+ self.trotter_circuit = None
+ self.groups0 = terms.TermGroup.from_terms(self.h0.terms)
+ self.groups1 = terms.TermGroup.from_terms(self.h1.terms)
+ all_terms = []
+ for group in self.groups0:
+ for term in group:
+ term.hamiltonian = self.h0
+ all_terms.append(term)
+ for group in self.groups1:
+ for term in group:
+ term.hamiltonian = self.h1
+ all_terms.append(term)
+ self.groups = terms.TermGroup.from_terms(all_terms)
+
+ def circuit(self, dt, accelerators=None, t=0):
+ """Circuit that implements the Trotterized evolution under the adiabatic Hamiltonian.
+
+ Args:
+ dt (float): Time step to use for exponentiation of the Hamiltonian.
+ accelerators (dict): Dictionary with accelerators for distributed
+ circuits.
+ t (float): Time that the Hamiltonian should be calculated.
+
+ Returns:
+ A :class:`qibo.models.Circuit` implementing the Trotterized evolution.
+ """
+ from qibo import Circuit # pylint: disable=import-outside-toplevel
+ from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel
+ TermGroup,
+ )
+
+ # pylint: disable=E1102
+ st = self.schedule(t / self.total_time) if t != 0 else 0
+ # pylint: enable=E1102
+ coefficients = {self.h0: 1 - st, self.h1: st}
+
+ groups = self.groups
+ circuit = Circuit(self.nqubits, accelerators=accelerators)
+ circuit.add(
+ group.to_term(coefficients).expgate(dt / 2.0)
+ for group in chain(groups, groups[::-1])
+ )
+
+ return circuit
diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py
new file mode 100644
index 000000000..bceb1b3b0
--- /dev/null
+++ b/src/qibo/hamiltonians/hamiltonians.py
@@ -0,0 +1,791 @@
+"""Module defining Hamiltonian classes."""
+
+from itertools import chain
+from typing import Optional
+
+import numpy as np
+import sympy
+
+from qibo.backends import PyTorchBackend, _check_backend
+from qibo.config import EINSUM_CHARS, log, raise_error
+from qibo.hamiltonians.abstract import AbstractHamiltonian
+from qibo.symbols import Z
+
+
+class Hamiltonian(AbstractHamiltonian):
+ """Hamiltonian based on a dense or sparse matrix representation.
+
+ Args:
+ nqubits (int): number of quantum bits.
+ matrix (np.ndarray): Matrix representation of the Hamiltonian in the
+ computational basis as an array of shape ``(2 ** nqubits, 2 ** nqubits)``.
+ Sparse matrices based on ``scipy.sparse`` for numpy/qibojit backends
+ or on ``tf.sparse`` for the tensorflow backend are also
+ supported.
+ """
+
+ def __init__(self, nqubits, matrix=None, backend=None):
+ from qibo.backends import _check_backend
+
+ self.backend = _check_backend(backend)
+
+ if not (
+ isinstance(matrix, self.backend.tensor_types)
+ or self.backend.is_sparse(matrix)
+ ):
+ raise_error(
+ TypeError,
+ f"Matrix of invalid type {type(matrix)} given during Hamiltonian initialization",
+ )
+ matrix = self.backend.cast(matrix)
+
+ super().__init__()
+ self.nqubits = nqubits
+ self.matrix = matrix
+ self._eigenvalues = None
+ self._eigenvectors = None
+ self._exp = {"a": None, "result": None}
+
+ @property
+ def matrix(self):
+ """Returns the full matrix representation.
+
+ Can be a dense ``(2 ** nqubits, 2 ** nqubits)`` array or a sparse
+ matrix, depending on how the Hamiltonian was created.
+ """
+ return self._matrix
+
+ @matrix.setter
+ def matrix(self, mat):
+ shape = tuple(mat.shape)
+ if shape != 2 * (2**self.nqubits,):
+ raise_error(
+ ValueError,
+ f"The Hamiltonian is defined for {self.nqubits} qubits "
+ + f"while the given matrix has shape {shape}.",
+ )
+ self._matrix = mat
+
+ @classmethod
+ def from_symbolic(cls, symbolic_hamiltonian, symbol_map, backend=None):
+ """Creates a ``Hamiltonian`` from a symbolic Hamiltonian.
+
+ We refer to the
+ :ref:`How to define custom Hamiltonians using symbols? `
+ example for more details.
+
+ Args:
+ symbolic_hamiltonian (sympy.Expr): The full Hamiltonian written
+ with symbols.
+ symbol_map (dict): Dictionary that maps each symbol that appears in
+ the Hamiltonian to a pair of (target, matrix).
+
+ Returns:
+ A :class:`qibo.hamiltonians.SymbolicHamiltonian` object
+ that implements the Hamiltonian represented by the given symbolic
+ expression.
+ """
+ log.warning(
+ "`Hamiltonian.from_symbolic` and the use of symbol maps is "
+ "deprecated. Please use `SymbolicHamiltonian` and Qibo symbols "
+ "to construct Hamiltonians using symbols."
+ )
+ return SymbolicHamiltonian(
+ symbolic_hamiltonian, symbol_map=symbol_map, backend=backend
+ )
+
+ def eigenvalues(self, k=6):
+ if self._eigenvalues is None:
+ self._eigenvalues = self.backend.calculate_eigenvalues(self.matrix, k)
+ return self._eigenvalues
+
+ def eigenvectors(self, k=6):
+ if self._eigenvectors is None:
+ self._eigenvalues, self._eigenvectors = self.backend.calculate_eigenvectors(
+ self.matrix, k
+ )
+ return self._eigenvectors
+
+ def exp(self, a):
+ from qibo.quantum_info.linalg_operations import ( # pylint: disable=C0415
+ matrix_exponentiation,
+ )
+
+ if self._exp.get("a") != a:
+ self._exp["a"] = a
+ self._exp["result"] = matrix_exponentiation(
+ a, self.matrix, self._eigenvectors, self._eigenvalues, self.backend
+ )
+ return self._exp.get("result")
+
+ def expectation(self, state, normalize=False):
+ if isinstance(state, self.backend.tensor_types):
+ state = self.backend.cast(state)
+ shape = tuple(state.shape)
+ if len(shape) == 1: # state vector
+ return self.backend.calculate_expectation_state(self, state, normalize)
+
+ if len(shape) == 2: # density matrix
+ return self.backend.calculate_expectation_density_matrix(
+ self, state, normalize
+ )
+
+ raise_error(
+ ValueError,
+ "Cannot calculate Hamiltonian expectation value "
+ + f"for state of shape {shape}",
+ )
+
+ raise_error(
+ TypeError,
+ "Cannot calculate Hamiltonian expectation "
+ + f"value for state of type {type(state)}",
+ )
+
+ def expectation_from_samples(self, freq, qubit_map=None):
+ obs = self.matrix
+ if (
+ self.backend.np.count_nonzero(
+ obs - self.backend.np.diag(self.backend.np.diagonal(obs))
+ )
+ != 0
+ ):
+ raise_error(NotImplementedError, "Observable is not diagonal.")
+ keys = list(freq.keys())
+ if qubit_map is None:
+ qubit_map = list(range(int(np.log2(len(obs)))))
+ counts = np.array(list(freq.values())) / sum(freq.values())
+ expval = 0
+ size = len(qubit_map)
+ for j, k in enumerate(keys):
+ index = 0
+ for i in qubit_map:
+ index += int(k[qubit_map.index(i)]) * 2 ** (size - 1 - i)
+ expval += obs[index, index] * counts[j]
+ return self.backend.np.real(expval)
+
+ def eye(self, dim: Optional[int] = None):
+ """Generate Identity matrix with dimension ``dim``"""
+ if dim is None:
+ dim = int(self.matrix.shape[0])
+ return self.backend.cast(self.backend.matrices.I(dim), dtype=self.matrix.dtype)
+
+ def energy_fluctuation(self, state):
+ """
+ Evaluate energy fluctuation:
+
+ .. math::
+ \\Xi_{k}(\\mu) = \\sqrt{\\langle\\mu|\\hat{H}^2|\\mu\\rangle - \\langle\\mu|\\hat{H}|\\mu\\rangle^2} \\,
+
+ for a given state :math:`|\\mu\\rangle`.
+
+ Args:
+ state (np.ndarray): quantum state to be used to compute the energy fluctuation.
+
+ Return:
+ Energy fluctuation value (float).
+ """
+ state = self.backend.cast(state)
+ energy = self.expectation(state)
+ h = self.matrix
+ h2 = Hamiltonian(nqubits=self.nqubits, matrix=h @ h, backend=self.backend)
+ average_h2 = self.backend.calculate_expectation_state(h2, state, normalize=True)
+ return self.backend.np.sqrt(self.backend.np.abs(average_h2 - energy**2))
+
+ def __add__(self, o):
+ if isinstance(o, self.__class__):
+ if self.nqubits != o.nqubits:
+ raise_error(
+ RuntimeError,
+ "Only hamiltonians with the same number of qubits can be added.",
+ )
+ new_matrix = self.matrix + o.matrix
+ elif isinstance(o, self.backend.numeric_types):
+ new_matrix = self.matrix + o * self.eye()
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian addition to {type(o)} not implemented.",
+ )
+ return self.__class__(self.nqubits, new_matrix, backend=self.backend)
+
+ def __sub__(self, o):
+ if isinstance(o, self.__class__):
+ if self.nqubits != o.nqubits:
+ raise_error(
+ RuntimeError,
+ "Only hamiltonians with the same number of qubits can be subtracted.",
+ )
+ new_matrix = self.matrix - o.matrix
+ elif isinstance(o, self.backend.numeric_types):
+ new_matrix = self.matrix - o * self.eye()
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian subtraction to {type(o)} not implemented.",
+ )
+ return self.__class__(self.nqubits, new_matrix, backend=self.backend)
+
+ def __rsub__(self, o):
+ if isinstance(o, self.__class__): # pragma: no cover
+ # impractical case because it will be handled by `__sub__`
+ if self.nqubits != o.nqubits:
+ raise_error(
+ RuntimeError,
+ "Only hamiltonians with the same number of qubits can be added.",
+ )
+ new_matrix = o.matrix - self.matrix
+ elif isinstance(o, self.backend.numeric_types):
+ new_matrix = o * self.eye() - self.matrix
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian subtraction to {type(o)} not implemented.",
+ )
+ return self.__class__(self.nqubits, new_matrix, backend=self.backend)
+
+ def __mul__(self, o):
+ if isinstance(o, self.backend.tensor_types):
+ o = complex(o)
+ elif not isinstance(o, self.backend.numeric_types):
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian multiplication to {type(o)} not implemented.",
+ )
+ new_matrix = self.matrix * o
+ r = self.__class__(self.nqubits, new_matrix, backend=self.backend)
+ o = self.backend.cast(o)
+ if self._eigenvalues is not None:
+ if self.backend.np.real(o) >= 0: # TODO: check for side effects K.qnp
+ r._eigenvalues = o * self._eigenvalues
+ elif not self.backend.is_sparse(self.matrix):
+ axis = (0,) if isinstance(self.backend, PyTorchBackend) else 0
+ r._eigenvalues = o * self.backend.np.flip(self._eigenvalues, axis)
+ if self._eigenvectors is not None:
+ if self.backend.np.real(o) > 0: # TODO: see above
+ r._eigenvectors = self._eigenvectors
+ elif o == 0:
+ r._eigenvectors = self.eye(int(self._eigenvectors.shape[0]))
+ return r
+
+ def __matmul__(self, o):
+ if isinstance(o, self.__class__):
+ matrix = self.backend.calculate_hamiltonian_matrix_product(
+ self.matrix, o.matrix
+ )
+ return self.__class__(self.nqubits, matrix, backend=self.backend)
+
+ if isinstance(o, self.backend.tensor_types):
+ return self.backend.calculate_hamiltonian_state_product(self.matrix, o)
+
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian matmul to {type(o)} not implemented.",
+ )
+
+
+class SymbolicHamiltonian(AbstractHamiltonian):
+ """Hamiltonian based on a symbolic representation.
+
+ Calculations using symbolic Hamiltonians are either done directly using
+ the given ``sympy`` expression as it is (``form``) or by parsing the
+ corresponding ``terms`` (which are :class:`qibo.core.terms.SymbolicTerm`
+ objects). The latter approach is more computationally costly as it uses
+ a ``sympy.expand`` call on the given form before parsing the terms.
+ For this reason the ``terms`` are calculated only when needed, for example
+ during Trotterization.
+ The dense matrix of the symbolic Hamiltonian can be calculated directly
+ from ``form`` without requiring ``terms`` calculation (see
+ :meth:`qibo.core.hamiltonians.SymbolicHamiltonian.calculate_dense` for details).
+
+ Args:
+ form (sympy.Expr): Hamiltonian form as a ``sympy.Expr``. Ideally the
+ Hamiltonian should be written using Qibo symbols.
+ See :ref:`How to define custom Hamiltonians using symbols? `
+ example for more details.
+ symbol_map (dict): Dictionary that maps each ``sympy.Symbol`` to a tuple
+ of (target qubit, matrix representation). This feature is kept for
+ compatibility with older versions where Qibo symbols were not available
+ and may be deprecated in the future.
+ It is not required if the Hamiltonian is constructed using Qibo symbols.
+ The symbol_map can also be used to pass non-quantum operator arguments
+ to the symbolic Hamiltonian, such as the parameters in the
+ :meth:`qibo.hamiltonians.models.MaxCut` Hamiltonian.
+ """
+
+ def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None):
+ super().__init__()
+ self._form = None
+ self._terms = None
+ self.constant = 0 # used only when we perform calculations using ``_terms``
+ self._dense = None
+ self.symbol_map = symbol_map
+ # if a symbol in the given form is not a Qibo symbol it must be
+ # included in the ``symbol_map``
+
+ from qibo.symbols import Symbol # pylint: disable=import-outside-toplevel
+
+ self._qiboSymbol = Symbol # also used in ``self._get_symbol_matrix``
+
+ from qibo.backends import _check_backend
+
+ self.backend = _check_backend(backend)
+
+ if form is not None:
+ self.form = form
+ if nqubits is not None:
+ self.nqubits = nqubits
+
+ @property
+ def dense(self) -> "MatrixHamiltonian":
+ """Creates the equivalent Hamiltonian matrix."""
+ if self._dense is None:
+ log.warning(
+ "Calculating the dense form of a symbolic Hamiltonian. "
+ "This operation is memory inefficient."
+ )
+ self.dense = self.calculate_dense()
+ return self._dense
+
+ @dense.setter
+ def dense(self, hamiltonian):
+ assert isinstance(hamiltonian, Hamiltonian)
+ self._dense = hamiltonian
+ self._eigenvalues = hamiltonian._eigenvalues
+ self._eigenvectors = hamiltonian._eigenvectors
+ self._exp = hamiltonian._exp
+
+ @property
+ def form(self):
+ return self._form
+
+ @form.setter
+ def form(self, form):
+ # Check that given form is a ``sympy`` expression
+ if not isinstance(form, sympy.Expr):
+ raise_error(
+ TypeError,
+ f"Symbolic Hamiltonian should be a ``sympy`` expression but is {type(form)}.",
+ )
+ # Calculate number of qubits in the system described by the given
+ # Hamiltonian formula
+ nqubits = 0
+ for symbol in form.free_symbols:
+ if isinstance(symbol, self._qiboSymbol):
+ q = symbol.target_qubit
+ elif isinstance(symbol, sympy.Expr):
+ if symbol not in self.symbol_map:
+ raise_error(ValueError, f"Symbol {symbol} is not in symbol map.")
+ q, matrix = self.symbol_map.get(symbol)
+ if not isinstance(matrix, self.backend.tensor_types):
+ # ignore symbols that do not correspond to quantum operators
+ # for example parameters in the MaxCut Hamiltonian
+ q = 0
+ if q > nqubits:
+ nqubits = q
+
+ self._form = form
+ self.nqubits = nqubits + 1
+
+ @property
+ def terms(self):
+ """List of terms of which the Hamiltonian is a sum of.
+
+ Terms will be objects of type :class:`qibo.core.terms.HamiltonianTerm`.
+ """
+ if self._terms is None:
+ # Calculate terms based on ``self.form``
+ from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel
+ SymbolicTerm,
+ )
+
+ form = sympy.expand(self.form)
+ terms = []
+ for f, c in form.as_coefficients_dict().items():
+ term = SymbolicTerm(c, f, self.symbol_map)
+ if term.target_qubits:
+ terms.append(term)
+ else:
+ self.constant += term.coefficient
+ self._terms = terms
+ return self._terms
+
+ @terms.setter
+ def terms(self, terms):
+ self._terms = terms
+ self.nqubits = max(q for term in self._terms for q in term.target_qubits) + 1
+
+ @property
+ def matrix(self):
+ """Returns the full matrix representation.
+
+ Consisting of ``(2 ** nqubits, 2 ** nqubits)`` elements.
+ """
+ return self.dense.matrix
+
+ def eigenvalues(self, k=6):
+ return self.dense.eigenvalues(k)
+
+ def eigenvectors(self, k=6):
+ return self.dense.eigenvectors(k)
+
+ def ground_state(self):
+ return self.eigenvectors()[:, 0]
+
+ def exp(self, a):
+ return self.dense.exp(a)
+
+ def _get_symbol_matrix(self, term):
+ """Calculates numerical matrix corresponding to symbolic expression.
+
+ This is partly equivalent to sympy's ``.subs``, which does not work
+ in our case as it does not allow us to substitute ``sympy.Symbol``
+ with numpy arrays and there are different complication when switching
+ to ``sympy.MatrixSymbol``. Here we calculate the full numerical matrix
+ given the symbolic expression using recursion.
+ Helper method for ``_calculate_dense_from_form``.
+
+ Args:
+ term (sympy.Expr): Symbolic expression containing local operators.
+
+ Returns:
+ Numerical matrix corresponding to the given expression as a numpy
+ array of size ``(2 ** self.nqubits, 2 ** self.nqubits).
+ """
+ if isinstance(term, sympy.Add):
+ # symbolic op for addition
+ result = sum(
+ self._get_symbol_matrix(subterm) for subterm in term.as_ordered_terms()
+ )
+
+ elif isinstance(term, sympy.Mul):
+ # symbolic op for multiplication
+ # note that we need to use matrix multiplication even though
+ # we use scalar symbols for convenience
+ factors = term.as_ordered_factors()
+ result = self._get_symbol_matrix(factors[0])
+ for subterm in factors[1:]:
+ result = result @ self._get_symbol_matrix(subterm)
+
+ elif isinstance(term, sympy.Pow):
+ # symbolic op for power
+ base, exponent = term.as_base_exp()
+ matrix = self._get_symbol_matrix(base)
+ # multiply ``base`` matrix ``exponent`` times to itself
+ result = matrix
+ for _ in range(exponent - 1):
+ result = result @ matrix
+
+ elif isinstance(term, sympy.Symbol):
+ # if the term is a ``Symbol`` then it corresponds to a quantum
+ # operator for which we can construct the full matrix directly
+ if isinstance(term, self._qiboSymbol):
+ # if we have a Qibo symbol the matrix construction is
+ # implemented in :meth:`qibo.core.terms.SymbolicTerm.full_matrix`.
+ result = term.full_matrix(self.nqubits)
+ else:
+ q, matrix = self.symbol_map.get(term)
+ if not isinstance(matrix, self.backend.tensor_types):
+ # symbols that do not correspond to quantum operators
+ # for example parameters in the MaxCut Hamiltonian
+ result = complex(matrix) * np.eye(2**self.nqubits)
+ else:
+ # if we do not have a Qibo symbol we construct one and use
+ # :meth:`qibo.core.terms.SymbolicTerm.full_matrix`.
+ result = self._qiboSymbol(q, matrix).full_matrix(self.nqubits)
+
+ elif term.is_number:
+ # if the term is number we should return in the form of identity
+ # matrix because in expressions like `1 + Z`, `1` is not correspond
+ # to the float 1 but the identity operator (matrix)
+ result = complex(term) * np.eye(2**self.nqubits)
+
+ else:
+ raise_error(
+ TypeError,
+ f"Cannot calculate matrix for symbolic term of type {type(term)}.",
+ )
+
+ return result
+
+ def _calculate_dense_from_form(self) -> Hamiltonian:
+ """Calculates equivalent Hamiltonian using symbolic form.
+
+ Useful when the term representation is not available.
+ """
+ matrix = self._get_symbol_matrix(self.form)
+ return Hamiltonian(self.nqubits, matrix, backend=self.backend)
+
+ def _calculate_dense_from_terms(self) -> Hamiltonian:
+ """Calculates equivalent Hamiltonian using the term representation."""
+ if 2 * self.nqubits > len(EINSUM_CHARS): # pragma: no cover
+ # case not tested because it only happens in large examples
+ raise_error(NotImplementedError, "Not enough einsum characters.")
+
+ matrix = 0
+ chars = EINSUM_CHARS[: 2 * self.nqubits]
+ for term in self.terms:
+ ntargets = len(term.target_qubits)
+ tmat = np.reshape(term.matrix, 2 * ntargets * (2,))
+ n = self.nqubits - ntargets
+ emat = np.reshape(np.eye(2**n, dtype=tmat.dtype), 2 * n * (2,))
+ gen = lambda x: (chars[i + x] for i in term.target_qubits)
+ tc = "".join(chain(gen(0), gen(self.nqubits)))
+ ec = "".join(c for c in chars if c not in tc)
+ matrix += np.einsum(f"{tc},{ec}->{chars}", tmat, emat)
+ matrix = np.reshape(matrix, 2 * (2**self.nqubits,))
+ return Hamiltonian(self.nqubits, matrix, backend=self.backend) + self.constant
+
+ def calculate_dense(self):
+ if self._terms is None:
+ # calculate dense matrix directly using the form to avoid the
+ # costly ``sympy.expand`` call
+ return self._calculate_dense_from_form()
+ return self._calculate_dense_from_terms()
+
+ def expectation(self, state, normalize=False):
+ return Hamiltonian.expectation(self, state, normalize)
+
+ def expectation_from_samples(self, freq, qubit_map=None):
+ terms = self.terms
+ for term in terms:
+ # pylint: disable=E1101
+ for factor in term.factors:
+ if not isinstance(factor, Z):
+ raise_error(
+ NotImplementedError, "Observable is not a Z Pauli string."
+ )
+ if len(term.factors) != len(set(term.factors)):
+ raise_error(NotImplementedError, "Z^k is not implemented since Z^2=I.")
+ keys = list(freq.keys())
+ counts = np.array(list(freq.values())) / sum(freq.values())
+ qubits = []
+ for term in terms:
+ qubits_term = []
+ for k in term.target_qubits:
+ qubits_term.append(k)
+ qubits.append(qubits_term)
+ if qubit_map is None:
+ qubit_map = list(range(len(keys[0])))
+ expval = 0
+ for j, q in enumerate(qubits):
+ subk = []
+ expval_q = 0
+ for i, k in enumerate(keys):
+ subk = [int(k[qubit_map.index(s)]) for s in q]
+ expval_k = 1
+ if subk.count(1) % 2 == 1:
+ expval_k = -1
+ expval_q += expval_k * counts[i]
+ expval += expval_q * self.terms[j].coefficient.real
+ return expval + self.constant.real
+
+ def __add__(self, o):
+ if isinstance(o, self.__class__):
+ if self.nqubits != o.nqubits:
+ raise_error(
+ RuntimeError,
+ "Only hamiltonians with the same number of qubits can be added.",
+ )
+ new_ham = self.__class__(
+ symbol_map=dict(self.symbol_map), backend=self.backend
+ )
+ if self._form is not None and o._form is not None:
+ new_ham.form = self.form + o.form
+ new_ham.symbol_map.update(o.symbol_map)
+ if self._terms is not None and o._terms is not None:
+ new_ham.terms = self.terms + o.terms
+ new_ham.constant = self.constant + o.constant
+ if self._dense is not None and o._dense is not None:
+ new_ham.dense = self.dense + o.dense
+
+ elif isinstance(o, self.backend.numeric_types):
+ new_ham = self.__class__(
+ symbol_map=dict(self.symbol_map), backend=self.backend
+ )
+ if self._form is not None:
+ new_ham.form = self.form + o
+ if self._terms is not None:
+ new_ham.terms = self.terms
+ new_ham.constant = self.constant + o
+ if self._dense is not None:
+ new_ham.dense = self.dense + o
+
+ else:
+ raise_error(
+ NotImplementedError,
+ f"SymbolicHamiltonian addition to {type(o)} not implemented.",
+ )
+ return new_ham
+
+ def __sub__(self, o):
+ if isinstance(o, self.__class__):
+ if self.nqubits != o.nqubits:
+ raise_error(
+ RuntimeError,
+ "Only hamiltonians with the same number of qubits can be subtracted.",
+ )
+ new_ham = self.__class__(
+ symbol_map=dict(self.symbol_map), backend=self.backend
+ )
+ if self._form is not None and o._form is not None:
+ new_ham.form = self.form - o.form
+ new_ham.symbol_map.update(o.symbol_map)
+ if self._terms is not None and o._terms is not None:
+ new_ham.terms = self.terms + [-1 * x for x in o.terms]
+ new_ham.constant = self.constant - o.constant
+ if self._dense is not None and o._dense is not None:
+ new_ham.dense = self.dense - o.dense
+
+ elif isinstance(o, self.backend.numeric_types):
+ new_ham = self.__class__(
+ symbol_map=dict(self.symbol_map), backend=self.backend
+ )
+ if self._form is not None:
+ new_ham.form = self.form - o
+ if self._terms is not None:
+ new_ham.terms = self.terms
+ new_ham.constant = self.constant - o
+ if self._dense is not None:
+ new_ham.dense = self.dense - o
+
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian subtraction to {type(o)} " "not implemented.",
+ )
+ return new_ham
+
+ def __rsub__(self, o):
+ if isinstance(o, self.backend.numeric_types):
+ new_ham = self.__class__(
+ symbol_map=dict(self.symbol_map), backend=self.backend
+ )
+ if self._form is not None:
+ new_ham.form = o - self.form
+ if self._terms is not None:
+ new_ham.terms = [-1 * x for x in self.terms]
+ new_ham.constant = o - self.constant
+ if self._dense is not None:
+ new_ham.dense = o - self.dense
+ else:
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian subtraction to {type(o)} not implemented.",
+ )
+ return new_ham
+
+ def __mul__(self, o):
+ if not isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)):
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian multiplication to {type(o)} not implemented.",
+ )
+ o = complex(o)
+ new_ham = self.__class__(symbol_map=dict(self.symbol_map), backend=self.backend)
+ if self._form is not None:
+ new_ham.form = o * self.form
+ if self._terms is not None:
+ new_ham.terms = [o * x for x in self.terms]
+ new_ham.constant = self.constant * o
+ if self._dense is not None:
+ new_ham.dense = o * self._dense
+ return new_ham
+
+ def apply_gates(self, state, density_matrix=False):
+ """Applies gates corresponding to the Hamiltonian terms.
+
+ Gates are applied to the given state.
+
+ Helper method for ``__matmul__``.
+ """
+ total = 0
+ for term in self.terms:
+ total += term(
+ self.backend,
+ self.backend.cast(state, copy=True),
+ self.nqubits,
+ density_matrix=density_matrix,
+ )
+ if self.constant: # pragma: no cover
+ total += self.constant * state
+ return total
+
+ def __matmul__(self, o):
+ """Matrix multiplication with other Hamiltonians or state vectors."""
+ if isinstance(o, self.__class__):
+ if self._form is None or o._form is None:
+ raise_error(
+ NotImplementedError,
+ "Multiplication of symbolic Hamiltonians "
+ "without symbolic form is not implemented.",
+ )
+ new_form = self.form * o.form
+ new_symbol_map = dict(self.symbol_map)
+ new_symbol_map.update(o.symbol_map)
+ new_ham = self.__class__(
+ new_form, symbol_map=new_symbol_map, backend=self.backend
+ )
+ if self._dense is not None and o._dense is not None:
+ new_ham.dense = self.dense @ o.dense
+ return new_ham
+
+ if isinstance(o, self.backend.tensor_types):
+ rank = len(tuple(o.shape))
+ if rank not in (1, 2):
+ raise_error(
+ NotImplementedError,
+ f"Cannot multiply Hamiltonian with rank-{rank} tensor.",
+ )
+ state_qubits = int(np.log2(int(o.shape[0])))
+ if state_qubits != self.nqubits:
+ raise_error(
+ ValueError,
+ f"Cannot multiply Hamiltonian on {self.nqubits} qubits to "
+ + f"state of {state_qubits} qubits.",
+ )
+ if rank == 1: # state vector
+ return self.apply_gates(o)
+
+ return self.apply_gates(o, density_matrix=True)
+
+ raise_error(
+ NotImplementedError,
+ f"Hamiltonian matmul to {type(o)} not implemented.",
+ )
+
+ def circuit(self, dt, accelerators=None):
+ """Circuit that implements a Trotter step of this Hamiltonian.
+
+ Args:
+ dt (float): Time step used for Trotterization.
+ accelerators (dict): Dictionary with accelerators for distributed circuits.
+ """
+ from qibo import Circuit # pylint: disable=import-outside-toplevel
+ from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel
+ TermGroup,
+ )
+
+ groups = TermGroup.from_terms(self.terms)
+ circuit = Circuit(self.nqubits, accelerators=accelerators)
+ circuit.add(
+ group.term.expgate(dt / 2.0) for group in chain(groups, groups[::-1])
+ )
+
+ return circuit
+
+
+class TrotterHamiltonian:
+ """"""
+
+ def __init__(self, *parts):
+ raise_error(
+ NotImplementedError,
+ "`TrotterHamiltonian` is substituted by `SymbolicHamiltonian` "
+ + "and is no longer supported. Please check the documentation "
+ + "of `SymbolicHamiltonian` for more details.",
+ )
+
+ @classmethod
+ def from_symbolic(cls, symbolic_hamiltonian, symbol_map):
+ return cls()
diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py
new file mode 100644
index 000000000..1fa030fac
--- /dev/null
+++ b/src/qibo/hamiltonians/models.py
@@ -0,0 +1,197 @@
+from qibo.backends import matrices
+from qibo.config import raise_error
+from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian
+from qibo.hamiltonians.terms import HamiltonianTerm
+
+
+def multikron(matrix_list):
+ """Calculates Kronecker product of a list of matrices.
+
+ Args:
+ matrices (list): List of matrices as ``np.ndarray``s.
+
+ Returns:
+ ``np.ndarray`` of the Kronecker product of all ``matrices``.
+ """
+ import numpy as np
+
+ h = 1
+ for m in matrix_list:
+ # TODO: check if we observe GPU deterioration
+ h = np.kron(h, m)
+ return h
+
+
+def _build_spin_model(nqubits, matrix, condition):
+ """Helper method for building nearest-neighbor spin model Hamiltonians."""
+ h = sum(
+ multikron(matrix if condition(i, j) else matrices.I for j in range(nqubits))
+ for i in range(nqubits)
+ )
+ return h
+
+
+def XXZ(nqubits, delta=0.5, dense=True, backend=None):
+ """Heisenberg XXZ model with periodic boundary conditions.
+
+ .. math::
+ H = \\sum _{i=0}^N \\left ( X_iX_{i + 1} + Y_iY_{i + 1} + \\delta Z_iZ_{i + 1} \\right ).
+
+ Args:
+ nqubits (int): number of quantum bits.
+ delta (float): coefficient for the Z component (default 0.5).
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+
+ Example:
+ .. testcode::
+
+ from qibo.hamiltonians import XXZ
+ h = XXZ(3) # initialized XXZ model with 3 qubits
+ """
+ if nqubits < 2:
+ raise_error(ValueError, "Number of qubits must be larger than one.")
+ if dense:
+ condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits}
+ hx = _build_spin_model(nqubits, matrices.X, condition)
+ hy = _build_spin_model(nqubits, matrices.Y, condition)
+ hz = _build_spin_model(nqubits, matrices.Z, condition)
+ matrix = hx + hy + delta * hz
+ return Hamiltonian(nqubits, matrix, backend=backend)
+
+ hx = multikron([matrices.X, matrices.X])
+ hy = multikron([matrices.Y, matrices.Y])
+ hz = multikron([matrices.Z, matrices.Z])
+ matrix = hx + hy + delta * hz
+ terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)]
+ terms.append(HamiltonianTerm(matrix, nqubits - 1, 0))
+ ham = SymbolicHamiltonian(backend=backend)
+ ham.terms = terms
+ return ham
+
+
+def _OneBodyPauli(nqubits, matrix, dense=True, backend=None):
+ """Helper method for constracting non-interacting X, Y, Z Hamiltonians."""
+ if dense:
+ condition = lambda i, j: i == j % nqubits
+ ham = -_build_spin_model(nqubits, matrix, condition)
+ return Hamiltonian(nqubits, ham, backend=backend)
+
+ matrix = -matrix
+ terms = [HamiltonianTerm(matrix, i) for i in range(nqubits)]
+ ham = SymbolicHamiltonian(backend=backend)
+ ham.terms = terms
+ return ham
+
+
+def X(nqubits, dense=True, backend=None):
+ """Non-interacting Pauli-X Hamiltonian.
+
+ .. math::
+ H = - \\sum _{i=0}^N X_i.
+
+ Args:
+ nqubits (int): number of quantum bits.
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+ """
+ return _OneBodyPauli(nqubits, matrices.X, dense, backend=backend)
+
+
+def Y(nqubits, dense=True, backend=None):
+ """Non-interacting Pauli-Y Hamiltonian.
+
+ .. math::
+ H = - \\sum _{i=0}^N Y_i.
+
+ Args:
+ nqubits (int): number of quantum bits.
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+ """
+ return _OneBodyPauli(nqubits, matrices.Y, dense, backend=backend)
+
+
+def Z(nqubits, dense=True, backend=None):
+ """Non-interacting Pauli-Z Hamiltonian.
+
+ .. math::
+ H = - \\sum _{i=0}^N Z_i.
+
+ Args:
+ nqubits (int): number of quantum bits.
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+ """
+ return _OneBodyPauli(nqubits, matrices.Z, dense, backend=backend)
+
+
+def TFIM(nqubits, h=0.0, dense=True, backend=None):
+ """Transverse field Ising model with periodic boundary conditions.
+
+ .. math::
+ H = - \\sum _{i=0}^N \\left ( Z_i Z_{i + 1} + h X_i \\right ).
+
+ Args:
+ nqubits (int): number of quantum bits.
+ h (float): value of the transverse field.
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+ """
+ if nqubits < 2:
+ raise_error(ValueError, "Number of qubits must be larger than one.")
+ if dense:
+ condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits}
+ ham = -_build_spin_model(nqubits, matrices.Z, condition)
+ if h != 0:
+ condition = lambda i, j: i == j % nqubits
+ ham -= h * _build_spin_model(nqubits, matrices.X, condition)
+ return Hamiltonian(nqubits, ham, backend=backend)
+
+ matrix = -(
+ multikron([matrices.Z, matrices.Z]) + h * multikron([matrices.X, matrices.I])
+ )
+ terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)]
+ terms.append(HamiltonianTerm(matrix, nqubits - 1, 0))
+ ham = SymbolicHamiltonian(backend=backend)
+ ham.terms = terms
+ return ham
+
+
+def MaxCut(nqubits, dense=True, backend=None):
+ """Max Cut Hamiltonian.
+
+ .. math::
+ H = - \\sum _{i,j=0}^N \\frac{1 - Z_i Z_j}{2}.
+
+ Args:
+ nqubits (int): number of quantum bits.
+ dense (bool): If ``True`` it creates the Hamiltonian as a
+ :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
+ a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
+ """
+ import sympy as sp
+ from numpy import ones
+
+ Z = sp.symbols(f"Z:{nqubits}")
+ V = sp.symbols(f"V:{nqubits**2}")
+ sham = -sum(
+ V[i * nqubits + j] * (1 - Z[i] * Z[j])
+ for i in range(nqubits)
+ for j in range(nqubits)
+ )
+ sham /= 2
+
+ v = ones(nqubits**2)
+ smap = {s: (i, matrices.Z) for i, s in enumerate(Z)}
+ smap.update({s: (i, v[i]) for i, s in enumerate(V)})
+
+ ham = SymbolicHamiltonian(sham, symbol_map=smap, backend=backend)
+ if dense:
+ return ham.dense
+ return ham
diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py
new file mode 100644
index 000000000..f1d2f7cda
--- /dev/null
+++ b/src/qibo/hamiltonians/terms.py
@@ -0,0 +1,331 @@
+import numpy as np
+import sympy
+
+from qibo import gates
+from qibo.config import raise_error
+
+
+class HamiltonianTerm:
+ """Term of a :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian`.
+
+ Symbolic Hamiltonians are represented by a list of
+ :class:`qibo.hamiltonians.terms.HamiltonianTerm` objects storred in the
+ ``SymbolicHamiltonian.terms`` attribute. The mathematical expression of
+ the Hamiltonian is the sum of these terms.
+
+ Args:
+ matrix (np.ndarray): Full matrix corresponding to the term representation
+ in the computational basis. Has size (2^n, 2^n) where n is the
+ number of target qubits of this term.
+ q (list): List of target qubit ids.
+ """
+
+ def __init__(self, matrix, *q):
+ for qi in q:
+ if qi < 0:
+ raise_error(
+ ValueError,
+ f"Invalid qubit id {qi} < 0 was given in Hamiltonian term.",
+ )
+ if not isinstance(matrix, np.ndarray):
+ raise_error(TypeError, f"Invalid type {type(matrix)} of symbol matrix.")
+ dim = int(matrix.shape[0])
+ if 2 ** len(q) != dim:
+ raise_error(
+ ValueError,
+ f"Matrix dimension {dim} given in Hamiltonian "
+ + "term is not compatible with the number "
+ + f"of target qubits {len(q)}.",
+ )
+ self.target_qubits = tuple(q)
+ self._gate = None
+ self.hamiltonian = None
+ self._matrix = matrix
+
+ @property
+ def matrix(self):
+ """Matrix representation of the term."""
+ return self._matrix
+
+ @property
+ def gate(self):
+ """:class:`qibo.gates.gates.Unitary` gate that implements the action of the term on states."""
+ if self._gate is None:
+ self._gate = gates.Unitary(self.matrix, *self.target_qubits)
+ return self._gate
+
+ def exp(self, x):
+ """Matrix exponentiation of the term."""
+ from scipy.linalg import expm
+
+ return expm(-1j * x * self.matrix)
+
+ def expgate(self, x):
+ """:class:`qibo.gates.gates.Unitary` gate implementing the action of exp(term) on states."""
+ return gates.Unitary(self.exp(x), *self.target_qubits)
+
+ def merge(self, term):
+ """Creates a new term by merging the given term to the current one.
+
+ The resulting term corresponds to the sum of the two original terms.
+ The target qubits of the given term should be a subset of the target
+ qubits of the current term.
+ """
+ if not set(term.target_qubits).issubset(set(self.target_qubits)):
+ raise_error(
+ ValueError,
+ "Cannot merge HamiltonianTerm acting on "
+ + f"qubits {term.target_qubits} to term on qubits {self.target_qubits}.",
+ )
+ matrix = np.kron(term.matrix, np.eye(2 ** (len(self) - len(term))))
+ matrix = np.reshape(matrix, 2 * len(self) * (2,))
+ order = []
+ i = len(term)
+ for qubit in self.target_qubits:
+ if qubit in term.target_qubits:
+ order.append(term.target_qubits.index(qubit))
+ else:
+ order.append(i)
+ i += 1
+ order.extend([x + len(order) for x in order])
+ matrix = np.transpose(matrix, order)
+ matrix = np.reshape(matrix, 2 * (2 ** len(self),))
+ return HamiltonianTerm(self.matrix + matrix, *self.target_qubits)
+
+ def __len__(self):
+ return len(self.target_qubits)
+
+ def __mul__(self, x):
+ return HamiltonianTerm(x * self.matrix, *self.target_qubits)
+
+ def __rmul__(self, x):
+ return self.__mul__(x)
+
+ def __call__(self, backend, state, nqubits, gate=None, density_matrix=False):
+ """Applies the term on a given state vector or density matrix."""
+ # TODO: improve this and understand why it works
+ if isinstance(gate, bool) or gate is None:
+ gate = self.gate
+ if density_matrix:
+ return backend.apply_gate_half_density_matrix(gate, state, nqubits)
+ return backend.apply_gate(gate, state, nqubits) # pylint: disable=E1102
+
+
+class SymbolicTerm(HamiltonianTerm):
+ """:class:`qibo.hamiltonians.terms.HamiltonianTerm` constructed using ``sympy`` expression.
+
+ Example:
+ .. testcode::
+
+ from qibo.symbols import X, Y
+ from qibo.hamiltonians.terms import SymbolicTerm
+ sham = X(0) * X(1) + 2 * Y(0) * Y(1)
+ termsdict = sham.as_coefficients_dict()
+ sterms = [SymbolicTerm(c, f) for f, c in termsdict.items()]
+
+ Args:
+ coefficient (complex): Complex number coefficient of the underlying
+ term in the Hamiltonian.
+ factors (sympy.Expr): Sympy expression for the underlying term.
+ symbol_map (dict): Dictionary that maps symbols in the given ``factors``
+ expression to tuples of (target qubit id, matrix).
+ This is required only if the expression is not created using Qibo
+ symbols and to keep compatibility with older versions where Qibo
+ symbols were not available.
+ """
+
+ def __init__(self, coefficient, factors=1, symbol_map={}):
+ self.coefficient = complex(coefficient)
+ self._matrix = None
+ self._gate = None
+ self.hamiltonian = None
+
+ # List of :class:`qibo.symbols.Symbol` that represent the term factors
+ self.factors = []
+ # Dictionary that maps target qubit ids to a list of matrices that act on each qubit
+ self.matrix_map = {}
+ if factors != 1:
+ for factor in factors.as_ordered_factors():
+ # check if factor has some power ``power`` so that the corresponding
+ # matrix is multiplied ``pow`` times
+ if isinstance(factor, sympy.Pow):
+ factor, pow = factor.args
+ assert isinstance(pow, sympy.Integer)
+ assert isinstance(factor, sympy.Symbol)
+ pow = int(pow)
+ else:
+ pow = 1
+
+ # if the user is using ``symbol_map`` instead of qibo symbols,
+ # create the corresponding symbols
+ if factor in symbol_map:
+ from qibo.symbols import Symbol
+
+ q, matrix = symbol_map.get(factor)
+ factor = Symbol(q, matrix, name=factor.name)
+
+ if isinstance(factor, sympy.Symbol):
+ if isinstance(factor.matrix, np.ndarray):
+ self.factors.extend(pow * [factor])
+ q = factor.target_qubit
+ # if pow > 1 the matrix should be multiplied multiple
+ # when calculating the term's total matrix so we
+ # repeat it in the corresponding list that will
+ # be used during this calculation
+ # see the ``SymbolicTerm.matrix`` property for the
+ # full matrix calculation
+ if q in self.matrix_map:
+ self.matrix_map[q].extend(pow * [factor.matrix])
+ else:
+ self.matrix_map[q] = pow * [factor.matrix]
+ else:
+ self.coefficient *= factor.matrix
+ elif factor == sympy.I:
+ self.coefficient *= 1j
+ elif factor.is_number:
+ self.coefficient *= complex(factor)
+ else: # pragma: no cover
+ raise_error(TypeError, f"Cannot parse factor {factor}.")
+
+ self.target_qubits = tuple(sorted(self.matrix_map.keys()))
+
+ @property
+ def matrix(self):
+ """Calculates the full matrix corresponding to this term.
+
+ Returns:
+ Matrix as a ``np.ndarray`` of shape ``(2 ** ntargets, 2 ** ntargets)``
+ where ``ntargets`` is the number of qubits included in the factors
+ of this term.
+ """
+ if self._matrix is None:
+
+ def matrices_product(matrices):
+ """Product of matrices that act on the same tuple of qubits.
+
+ Args:
+ matrices (list): List of matrices to multiply, as exists in
+ the values of ``SymbolicTerm.matrix_map``.
+ """
+ if len(matrices) == 1:
+ return matrices[0]
+ matrix = np.copy(matrices[0])
+ for m in matrices[1:]:
+ matrix = matrix @ m
+ return matrix
+
+ self._matrix = self.coefficient
+ for q in self.target_qubits:
+ matrix = matrices_product(self.matrix_map.get(q))
+ self._matrix = np.kron(self._matrix, matrix)
+ return self._matrix
+
+ def copy(self):
+ """Creates a shallow copy of the term with the same attributes."""
+ new = self.__class__(self.coefficient)
+ new.factors = self.factors
+ new.matrix_map = self.matrix_map
+ new.target_qubits = self.target_qubits
+ return new
+
+ def __mul__(self, x):
+ """Multiplication of scalar to the Hamiltonian term."""
+ new = self.copy()
+ new.coefficient *= x
+ if self._matrix is not None:
+ new._matrix = x * self._matrix
+ return new
+
+ def __call__(self, backend, state, nqubits, density_matrix=False):
+ for factor in self.factors:
+ state = super().__call__(
+ backend, state, nqubits, factor.gate, density_matrix
+ )
+ return self.coefficient * state
+
+
+class TermGroup(list):
+ """Collection of multiple :class:`qibo.hamiltonians.terms.HamiltonianTerm` objects.
+
+ Allows merging multiple terms to a single one for faster exponentiation
+ during Trotterized evolution.
+
+ Args:
+ term (:class:`qibo.hamiltonians.terms.HamiltonianTerm`): Parent term of the group.
+ All terms appended later should target a subset of the parents'
+ target qubits.
+ """
+
+ def __init__(self, term):
+ super().__init__([term])
+ self.target_qubits = set(term.target_qubits)
+ self._term = None
+
+ def append(self, term):
+ """Appends a new :class:`qibo.hamiltonians.terms.HamiltonianTerm` to the collection."""
+ super().append(term)
+ self.target_qubits |= set(term.target_qubits)
+ self._term = None
+
+ def can_append(self, term):
+ """Checks if a term can be appended to the group based on its target qubits."""
+ return set(term.target_qubits).issubset(self.target_qubits)
+
+ @classmethod
+ def from_terms(cls, terms):
+ """Divides a list of terms to multiple :class:`qibo.hamiltonians.terms.TermGroup`s.
+
+ Terms that target the same qubits are grouped to the same group.
+
+ Args:
+ terms (list): List of :class:`qibo.hamiltonians.terms.HamiltonianTerm` objects.
+
+ Returns:
+ List of :class:`qibo.hamiltonians.terms.TermGroup` objects that contain
+ all the given terms.
+ """
+ # split given terms according to their order
+ # order = len(term.target_qubits)
+ orders = {}
+ for term in terms:
+ if len(term) in orders:
+ orders[len(term)].append(term)
+ else:
+ orders[len(term)] = [term]
+
+ groups = []
+ # start creating groups with the higher order terms as parents and then
+ # append each term of lower order to the first compatible group
+ for order in sorted(orders.keys())[::-1]:
+ for child in orders[order]:
+ flag = True
+ for i, group in enumerate(groups):
+ if group.can_append(child):
+ group.append(child)
+ flag = False
+ break
+ if flag:
+ groups.append(cls(child))
+ return groups
+
+ @property
+ def term(self):
+ """Returns a single :class:`qibo.hamiltonians.terms.HamiltonianTerm`. after merging all terms in the group."""
+ if self._term is None:
+ self._term = self.to_term()
+ return self._term
+
+ def to_term(self, coefficients={}):
+ """Calculates a single :class:`qibo.hamiltonians.terms.HamiltonianTerm` by merging all terms in the group.
+
+ Args:
+ coefficients (dict): Optional dictionary that allows passing a different
+ coefficient to each term according to its parent Hamiltonian.
+ Useful for :class:`qibo.core.adiabatic.AdiabaticHamiltonian` calculations.
+ """
+ c = coefficients.get(self[0].hamiltonian)
+ merged = self[0] * c if c is not None else self[0]
+ for term in self[1:]:
+ c = coefficients.get(term.hamiltonian)
+ merged = merged.merge(term * c if c is not None else term)
+ return merged
diff --git a/src/qibo/measurements.py b/src/qibo/measurements.py
new file mode 100644
index 000000000..9f45d27fd
--- /dev/null
+++ b/src/qibo/measurements.py
@@ -0,0 +1,205 @@
+import collections
+
+import numpy as np
+import sympy
+
+from qibo import gates
+from qibo.config import raise_error
+
+
+def frequencies_to_binary(frequencies, nqubits):
+ return collections.Counter(
+ {"{:b}".format(k).zfill(nqubits): v for k, v in frequencies.items()}
+ )
+
+
+def apply_bitflips(result, p0, p1=None):
+ gate = result.measurement_gate
+ if p1 is None:
+ probs = 2 * (gate._get_bitflip_tuple(gate.qubits, p0),)
+ else:
+ probs = (
+ gate._get_bitflip_tuple(gate.qubits, p0),
+ gate._get_bitflip_tuple(gate.qubits, p1),
+ )
+ noiseless_samples = result.samples()
+ return result.backend.apply_bitflips(noiseless_samples, probs)
+
+
+class MeasurementSymbol(sympy.Symbol):
+ """``sympy.Symbol`` connected to measurement results.
+
+ Used by :class:`qibo.gates.measurements.M` with ``collapse=True`` to allow
+ controlling subsequent gates from the measurement results.
+ """
+
+ _counter = 0
+
+ def __new__(cls, *args, **kwargs):
+ name = f"m{cls._counter}"
+ cls._counter += 1
+ return super().__new__(cls=cls, name=name)
+
+ def __init__(self, index, result):
+ self.index = index
+ self.result = result
+
+ def __getstate__(self):
+ return {"index": self.index, "result": self.result, "name": self.name}
+
+ def __setstate__(self, data):
+ self.index = data.get("index")
+ self.result = data.get("result")
+ self.name = data.get("name")
+
+ def outcome(self):
+ return self.result.samples(binary=True)[-1][self.index]
+
+ def evaluate(self, expr):
+ """Substitutes the symbol's value in the given expression.
+
+ Args:
+ expr (sympy.Expr): Sympy expression that involves the current
+ measurement symbol.
+ """
+ return expr.subs(self, self.outcome())
+
+
+class MeasurementResult:
+ """Data structure for holding measurement outcomes.
+
+ :class:`qibo.measurements.MeasurementResult` objects can be obtained
+ when adding measurement gates to a circuit.
+
+ Args:
+ gate (:class:`qibo.gates.M`): Measurement gate associated with
+ this result object.
+ nshots (int): Number of measurement shots.
+ backend (:class:`qibo.backends.abstract.AbstractBackend`): Backend
+ to use for calculations.
+ """
+
+ def __init__(self, gate, nshots=0, backend=None):
+ self.measurement_gate = gate
+ self.backend = backend
+ self.nshots = nshots
+ self.circuit = None
+
+ self._samples = None
+ self._frequencies = None
+ self._bitflip_p0 = None
+ self._bitflip_p1 = None
+ self._symbols = None
+
+ def __repr__(self):
+ qubits = self.measurement_gate.qubits
+ nshots = self.nshots
+ return f"MeasurementResult(qubits={qubits}, nshots={nshots})"
+
+ def add_shot(self, probs):
+ qubits = sorted(self.measurement_gate.target_qubits)
+ shot = self.backend.sample_shots(probs, 1)
+ bshot = self.backend.samples_to_binary(shot, len(qubits))
+ if self._samples:
+ self._samples.append(bshot[0])
+ else:
+ self._samples = [bshot[0]]
+ self.nshots += 1
+ return shot
+
+ def add_shot_from_sample(self, sample):
+ if self._samples:
+ self._samples.append(sample)
+ else:
+ self._samples = [sample]
+ self.nshots += 1
+
+ def has_samples(self):
+ return self._samples is not None
+
+ def register_samples(self, samples, backend=None):
+ """Register samples array to the ``MeasurementResult`` object."""
+ self._samples = samples
+ self.nshots = len(samples)
+
+ def register_frequencies(self, frequencies, backend=None):
+ """Register frequencies to the ``MeasurementResult`` object."""
+ self._frequencies = frequencies
+ self.nshots = sum(frequencies.values())
+
+ def reset(self):
+ """Remove all registered samples and frequencies."""
+ self._samples = None
+ self._frequencies = None
+
+ @property
+ def symbols(self):
+ """List of ``sympy.Symbols`` associated with the results of the measurement.
+
+ These symbols are useful for conditioning parametrized gates on measurement outcomes.
+ """
+ if self._symbols is None:
+ qubits = self.measurement_gate.target_qubits
+ self._symbols = [MeasurementSymbol(i, self) for i in range(len(qubits))]
+
+ return self._symbols
+
+ def samples(self, binary=True, registers=False):
+ """Returns raw measurement samples.
+
+ Args:
+ binary (bool): Return samples in binary or decimal form.
+ registers (bool): Group samples according to registers.
+
+ Returns:
+ If `binary` is `True`
+ samples are returned in binary form as a tensor
+ of shape `(nshots, n_measured_qubits)`.
+ If `binary` is `False`
+ samples are returned in decimal form as a tensor
+ of shape `(nshots,)`.
+ """
+ if self._samples is None:
+ if self.circuit is None:
+ raise_error(
+ RuntimeError, "Cannot calculate samples if circuit is not provided."
+ )
+ # calculate samples for the whole circuit so that
+ # individual register samples are registered here
+ self.circuit.final_state.samples()
+ if binary:
+ return self._samples
+ else:
+ qubits = self.measurement_gate.target_qubits
+ return self.backend.samples_to_decimal(self._samples, len(qubits))
+
+ def frequencies(self, binary=True, registers=False):
+ """Returns the frequencies of measured samples.
+
+ Args:
+ binary (bool): Return frequency keys in binary or decimal form.
+ registers (bool): Group frequencies according to registers.
+
+ Returns:
+ A `collections.Counter` where the keys are the observed values
+ and the values the corresponding frequencies, that is the number
+ of times each measured value/bitstring appears.
+
+ If `binary` is `True`
+ the keys of the `Counter` are in binary form, as strings of
+ 0s and 1s.
+ If `binary` is `False`
+ the keys of the `Counter` are integers.
+ """
+ if self._frequencies is None:
+ self._frequencies = self.backend.calculate_frequencies(
+ self.samples(binary=False)
+ )
+ if binary:
+ qubits = self.measurement_gate.target_qubits
+ return frequencies_to_binary(self._frequencies, len(qubits))
+ else:
+ return self._frequencies
+
+ def apply_bitflips(self, p0, p1=None): # pragma: no cover
+ return apply_bitflips(self, p0, p1)
diff --git a/src/qibo/models/__init__.py b/src/qibo/models/__init__.py
new file mode 100644
index 000000000..e9c2abb92
--- /dev/null
+++ b/src/qibo/models/__init__.py
@@ -0,0 +1,14 @@
+from qibo.models import hep, tsp
+from qibo.models.circuit import Circuit
+from qibo.models.encodings import (
+ comp_basis_encoder,
+ entangling_layer,
+ phase_encoder,
+ unary_encoder,
+ unary_encoder_random_gaussian,
+)
+from qibo.models.error_mitigation import CDR, ICS, ZNE, vnCDR
+from qibo.models.evolution import AdiabaticEvolution, StateEvolution
+from qibo.models.grover import Grover
+from qibo.models.qft import QFT
+from qibo.models.variational import AAVQE, FALQON, QAOA, VQE
diff --git a/src/qibo/models/_openqasm.py b/src/qibo/models/_openqasm.py
new file mode 100644
index 000000000..0ebd1c7ac
--- /dev/null
+++ b/src/qibo/models/_openqasm.py
@@ -0,0 +1,299 @@
+"""Qibo wrapper for QASM 3.0 parser."""
+
+from itertools import repeat
+from typing import Union
+
+import numpy as np
+import openqasm3
+
+import qibo
+from qibo.config import raise_error
+from qibo.gates import FusedGate
+
+
+class CustomQASMGate:
+ """Object that handles the definition of custom gates in QASM via the `gate` command.
+
+ Args:
+ gates (list): List of gates composing the defined gate.
+ qubits (list or tuple): Qubits identifiers (e.g. (q0, q1, q2, ...)).
+ args (list or tuple): Arguments identifiers (e.g. (theta, alpha, gamma, ...)).
+ """
+
+ def __init__(
+ self,
+ name: str,
+ gates: list,
+ qubits: Union[list, tuple],
+ args: Union[list, tuple],
+ ):
+ self.name = name
+ self.gates = gates
+ self.qubits = qubits
+ self.args = args
+
+ def get_gate(self, qubits: Union[list, tuple], args: Union[list, tuple]):
+ """Returns the gates composing the defined gate applied on the
+ specified qubits with the specified ``args`` as a unique :class:`qibo.gates.special.FusedGate`.
+
+ Args:
+ qubits (list or tuple): Qubits where to apply the gates.
+ args (list or tuple): Arguments to evaluate the gates on.
+
+ Returns:
+ :class:`qibo.gates.special.FusedGate`: the composed gate evaluated on the input qubits with the input arguments.
+ """
+ if len(self.args) != len(args):
+ raise_error(
+ ValueError,
+ f"Invalid `args` argument passed to the user-defined gate `{self.name}` upon construction. {args} was passed but something of the form {self.args} is expected.",
+ )
+ elif len(self.qubits) != len(qubits):
+ raise_error(
+ ValueError,
+ f"Invalid `qubits` argument passed to the user-defined gate `{self.name}` upon construction. {qubits} was passed but something of the form {self.qubits} is expected.",
+ )
+ qubit_map = dict(zip(self.qubits, qubits))
+ args_map = dict(zip(self.args, args))
+ return self._construct_fused_gate(self.gates, qubits, qubit_map, args_map)
+
+ def _construct_fused_gate(self, gates, qubits, qubit_map, args_map):
+ """Constructs a :class:`qibo.gates.special.FusedGate` out of the provided list of gates on the specified qubits.
+
+ Args:
+ gates (list(:class:`qibo.gates.Gate`)): List of gates to build the fused gate from.
+ qubits (list(int)): List of qubits to construct the gate on.
+ qubit_map (dict): Mapping between the placeholders for the qubits contained in `gates` and the actual qubits indices to apply them on.
+ args_map (dict): Mapping between the placeholders for the kwargs contained in `gates` and the actual kwargs values.
+
+ Returns:
+ (qibo.gates.special.FusedGate): The resulting fused gate.
+ """
+ fused_gate = FusedGate(*qubits)
+ for gate in gates:
+ if not isinstance(gate, FusedGate):
+ new_qubits, new_args = self._compile_gate_qubits_and_args(
+ gate, qubit_map, args_map
+ )
+ fused_gate.append(gate.__class__(*new_qubits, *new_args))
+ else:
+ qubits = [qubit_map[q] for q in gate.qubits]
+ fused_gate.append(
+ self._construct_fused_gate(gate.gates, qubits, qubit_map, args_map)
+ )
+ return fused_gate
+
+ def _compile_gate_qubits_and_args(self, gate, qubit_map, args_map):
+ """Compile the qubits and arguments placeholders contained in the input gate with their actual values.
+
+ Args:
+ gate (:class:`qibo.gates.Gate`): The input gate containing the qubits and arguments placeholders.
+ qubit_map (dict): Mapping between the placeholders for the qubits contained in `gate` and the actual qubits indices to apply them on.
+ args_map (dict): Mapping between the placeholders for the kwargs contained in `gate` and the actual kwargs values.
+
+ Returns:
+ tuple(list, list): The compiled qubits and arguments.
+ """
+ new_qubits = [qubit_map[q] for q in gate.qubits]
+ new_args = [args_map.get(arg, arg) for arg in gate.init_kwargs.values()]
+ return new_qubits, new_args
+
+
+def _qibo_gate_name(gate):
+ if gate == "cx":
+ return "CNOT"
+
+ if gate == "id":
+ return "I"
+
+ if gate == "ccx":
+ return "TOFFOLI"
+
+ return gate.upper()
+
+
+class QASMParser:
+ """Wrapper around the :class:`openqasm3.parser` for QASM 3.0."""
+
+ def __init__(
+ self,
+ ):
+ self.parser = openqasm3.parser
+ self.defined_gates = {}
+ self.q_registers = {}
+ self.c_registers = set()
+
+ def to_circuit(
+ self, qasm_string: str, accelerators: dict = None, density_matrix: bool = False
+ ):
+ """Converts a QASM program into a :class:`qibo.models.Circuit`.
+
+ Args:
+ qasm_string (str): QASM program.
+ accelerators (dict, optional): Maps device names to the number of times each
+ device will be used. Defaults to ``None``.
+ density_matrix (bool, optional): If ``True``, the constructed circuit would
+ evolve density matrices.
+
+ Returns:
+ :class:`qibo.models.Circuit`: circuit constructed from QASM string.
+ """
+ parsed = self.parser.parse(qasm_string)
+ gates = []
+ self.defined_gates, self.q_registers, self.c_registers = {}, {}, {}
+
+ nqubits = 0
+ for statement in parsed.statements:
+ if isinstance(statement, openqasm3.ast.QuantumGate):
+ gates.append(self._get_gate(statement))
+ elif isinstance(statement, openqasm3.ast.QuantumMeasurementStatement):
+ gates.append(self._get_measurement(statement))
+ elif isinstance(statement, openqasm3.ast.QubitDeclaration):
+ q_name, q_size = self._get_qubit(statement)
+ self.q_registers.update(
+ {q_name: list(range(nqubits, nqubits + q_size))}
+ )
+ nqubits += q_size
+ elif isinstance(statement, openqasm3.ast.QuantumGateDefinition):
+ self._def_gate(statement)
+ elif isinstance(statement, openqasm3.ast.Include):
+ continue
+ elif isinstance(statement, openqasm3.ast.ClassicalDeclaration):
+ name = statement.identifier.name
+ size = statement.type.size.value
+ self.c_registers.update({name: list(range(size))})
+ else:
+ raise_error(RuntimeError, f"Unsupported {type(statement)} statement.")
+ circ = qibo.Circuit(
+ nqubits,
+ accelerators,
+ density_matrix,
+ wire_names=self._construct_wire_names(),
+ )
+ circ.add(self._merge_measurements(gates))
+ return circ
+
+ def _get_measurement(self, measurement):
+ """Converts a :class:`openqasm3.ast.QuantumMeasurementStatement` statement
+ into :class:`qibo.gates.measurements.M`."""
+ qubit = self._get_qubit(measurement.measure.qubit)
+ register = measurement.target.name.name
+ if register not in self.c_registers:
+ raise_error(ValueError, f"Undefined measurement register `{register}`.")
+ ind = measurement.target.indices[0][0].value
+ if ind >= len(self.c_registers[register]):
+ raise_error(
+ IndexError, f"Index `{ind}` is out of bounds of register `{register}`."
+ )
+ self.c_registers[register][ind] = qubit
+ return getattr(qibo.gates, "M")(qubit, register_name=register)
+
+ def _get_qubit(self, qubit):
+ """Extracts the qubit from a :class:`openqasm3.ast.QubitDeclaration` statement."""
+ if isinstance(qubit, openqasm3.ast.QubitDeclaration):
+ return qubit.qubit.name, qubit.size.value
+
+ if not isinstance(qubit, openqasm3.ast.Identifier):
+ return self.q_registers[qubit.name.name][qubit.indices[0][0].value]
+
+ return qubit.name
+
+ def _get_gate(self, gate):
+ """Converts a :class:`openqasm3.ast.QuantumGate` statement
+ into :class:`qibo.gates.Gate`."""
+ qubits = [self._get_qubit(q) for q in gate.qubits]
+ init_args = []
+ for arg in gate.arguments:
+ arg = self._unroll_expression(arg)
+ try:
+ arg = eval(arg.replace("pi", "np.pi"))
+ except:
+ pass
+ init_args.append(arg)
+ # check whether the gate exists in qibo.gates already
+ if _qibo_gate_name(gate.name.name) in dir(qibo.gates):
+ try:
+ gate = getattr(qibo.gates, _qibo_gate_name(gate.name.name))(
+ *qubits, *init_args
+ )
+ # the gate exists in qibo.gates but invalid construction
+ except TypeError:
+ raise_error(
+ ValueError, f"Invalid gate declaration at span: {gate.span}"
+ )
+ # check whether the gate was defined by the user
+ elif gate.name.name in self.defined_gates:
+ try:
+ gate = self.defined_gates.get(gate.name.name).get_gate(
+ qubits, init_args
+ )
+ # the gate exists in self.defined_gates but invalid construction
+ except ValueError:
+ raise_error(
+ ValueError, f"Invalid gate declaration at span: {gate.span}"
+ )
+ # undefined gate
+ else:
+ raise_error(ValueError, f"Undefined gate at span: {gate.span}")
+ return gate
+
+ def _unroll_expression(self, expr):
+ """Unrolls an argument definition expression to retrieve the
+ complete argument as a string."""
+ # check whether the expression is a simple string, e.g. `pi` or `theta`
+ if "name" in dir(expr):
+ return expr.name
+ # check whether the expression is a single value, e.g. `0.1234`
+ if "value" in dir(expr):
+ return expr.value
+ # the expression is composite, e.g. `2*pi` or `3*theta/2`
+ expr_dict = {}
+ for attr in ("lhs", "op", "expression", "rhs"):
+ expr_dict[attr] = ""
+ if attr in dir(expr):
+ val = self._unroll_expression(getattr(expr, attr))
+ expr_dict[attr] += str(val)
+
+ return "".join(list(expr_dict.values()))
+
+ def _def_gate(self, definition):
+ """Converts a :class:`openqasm3.ast.QuantumGateDefinition` statement
+ into :class:`qibo.parser.CustomQASMGate` object."""
+ name = definition.name.name
+ qubits = [self._get_qubit(q) for q in definition.qubits]
+ args = [self._unroll_expression(expr) for expr in definition.arguments]
+ gates = [self._get_gate(gate) for gate in definition.body]
+ self.defined_gates.update({name: CustomQASMGate(name, gates, qubits, args)})
+
+ def _merge_measurements(self, gates):
+ """Merges separated measurements of a same register into a single one.
+ This is needed because qibo doesn't allow to separetely define two measurements in a same register:
+
+ # not allowed
+ c.add(gates.M(0, register="m0"))
+ c.add(gates.M(1, register="m0"))
+ """
+ updated_queue = []
+ for gate in gates:
+ if isinstance(gate, qibo.gates.M):
+ if gate.register_name in self.c_registers:
+ updated_queue.append(
+ qibo.gates.M(
+ *self.c_registers.pop(gate.register_name),
+ register_name=gate.register_name,
+ )
+ )
+ else:
+ updated_queue.append(gate)
+ return updated_queue
+
+ def _construct_wire_names(self):
+ """Builds the wires names from the declared quantum registers."""
+ wire_names = []
+ for reg_name, reg_qubits in self.q_registers.items():
+ wires = sorted(
+ zip(repeat(reg_name, len(reg_qubits)), reg_qubits), key=lambda x: x[1]
+ )
+ for wire in wires:
+ wire_names.append(f"{wire[0]}{wire[1]}")
+ return wire_names
diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py
new file mode 100644
index 000000000..c4586c5ae
--- /dev/null
+++ b/src/qibo/models/circuit.py
@@ -0,0 +1,1371 @@
+import collections
+import copy
+from typing import Dict, List, Optional, Tuple, Union
+
+import numpy as np
+
+import qibo
+from qibo import gates
+from qibo.config import raise_error
+from qibo.gates.abstract import Gate
+from qibo.models._openqasm import QASMParser
+
+NoiseMapType = Union[Tuple[int, int, int], Dict[int, Tuple[int, int, int]]]
+
+
+class _ParametrizedGates(list):
+ """Simple data structure for keeping track of parametrized gates.
+
+ Useful for the ``circuit.set_parameters()`` method.
+ Holds parametrized gates in a list and a set and also keeps track of the
+ total number of parameters.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set = set()
+ self.nparams = 0
+
+ def append(self, gate):
+ super().append(gate)
+ self.set.add(gate)
+ self.nparams += gate.nparams
+
+
+class _Queue(list):
+ """List that holds the queue of gates of a circuit.
+
+ In addition to the queue, it holds a list of gate moments, where
+ each gate is placed in the earliest possible position depending for
+ the qubits it acts.
+ """
+
+ def __init__(self, nqubits):
+ super().__init__(self)
+ self.nqubits = nqubits
+
+ def to_fused(self):
+ """Transform all gates in queue to :class:`qibo.gates.FusedGate`."""
+ last_gate = {}
+ queue = self.__class__(self.nqubits)
+ for gate in self:
+ fgate = gates.FusedGate.from_gate(gate)
+ if isinstance(gate, gates.SpecialGate):
+ fgate.qubit_set = set(range(self.nqubits))
+ fgate.init_args = sorted(fgate.qubit_set)
+ fgate.target_qubits = tuple(fgate.init_args)
+
+ for q in fgate.qubits:
+ if q in last_gate:
+ neighbor = last_gate.get(q)
+ fgate.left_neighbors[q] = neighbor
+ neighbor.right_neighbors[q] = fgate
+ last_gate[q] = fgate
+ queue.append(fgate)
+ return queue
+
+ def from_fused(self):
+ """Create queue from fused circuit.
+
+ Create the fused circuit queue by removing gates that have been
+ fused to others.
+ """
+ queue = self.__class__(self.nqubits)
+ for gate in self:
+ if not gate.marked:
+ if len(gate.gates) == 1:
+ # replace ``FusedGate``s that contain only one gate
+ # by this gate for efficiency
+ queue.append(gate.gates[0])
+ else:
+ queue.append(gate)
+ elif isinstance(gate.gates[0], (gates.SpecialGate, gates.M)):
+ # special gates are marked by default so we need
+ # to add them manually
+ queue.append(gate.gates[0])
+ return queue
+
+ @property
+ def nmeasurements(self):
+ return len(list(filter(lambda gate: isinstance(gate, gates.M), self)))
+
+ @property
+ def moments(self):
+ moments = [self.nqubits * [None]]
+ moment_index = self.nqubits * [0]
+ for gate in self:
+ qubits = (
+ gate.qubits
+ if not isinstance(gate, gates.CallbackGate)
+ else tuple(range(self.nqubits)) # special gate acting on all qubits
+ )
+
+ # calculate moment index for this gate
+ idx = max(moment_index[q] for q in qubits)
+ for q in qubits:
+ if idx >= len(moments):
+ # Add a moment
+ moments.append(len(moments[-1]) * [None])
+ moments[idx][q] = gate
+ moment_index[q] = idx + 1
+ return moments
+
+
+class Circuit:
+ """Circuit object which holds a list of gates.
+
+ This circuit is symbolic and cannot perform calculations.
+ A specific backend has to be used for performing calculations.
+
+ Args:
+ nqubits (int): Total number of qubits in the circuit.
+ init_kwargs (dict): a dictionary with the following keys
+
+ - *nqubits*
+ - *accelerators*
+ - *density_matrix*
+ - *wire_names*.
+
+ queue (_Queue): List that holds the queue of gates of a circuit.
+ parametrized_gates (_ParametrizedGates): List of parametric gates.
+ trainable_gates (_ParametrizedGates): List of trainable gates.
+ measurements (list): List of non-collapsible measurements.
+ _final_state : Final result after full simulation of the circuit.
+ compiled (CompiledExecutor): Circuit executor. Defaults to ``None``.
+ repeated_execution (bool): If `True`, the circuit would be re-executed when sampling.
+ Defaults to ``False``.
+ density_matrix (bool, optional): If `True`, the circuit would evolve density matrices.
+ If ``False``, defaults to statevector simulation.
+ Defaults to ``False``.
+ accelerators (dict, optional): Dictionary that maps device names to the number of times each
+ device will be used. Defaults to ``None``.
+ wire_names (list or dict, optional): Names for qubit wires.
+ If ``None``, defaults to (``q0``, ``q1``... ``qn``).
+ If ``list`` is passed, length of ``list`` must match ``nqubits``.
+ If ``dict`` is passed, the keys should match the default pattern.
+ Defaults to ``None``.
+ ndevices (int): Total number of devices. Defaults to ``None``.
+ nglobal (int): Base two logarithm of the number of devices. Defaults to ``None``.
+ nlocal (int): Total number of available qubits in each device. Defaults to ``None``.
+ queues (DistributedQueues): Gate queues for each accelerator device.
+ Defaults to ``None``.
+ """
+
+ def __init__(
+ self,
+ nqubits: int,
+ accelerators=None,
+ density_matrix: bool = False,
+ wire_names: Optional[Union[list, dict]] = None,
+ ):
+ if not isinstance(nqubits, int):
+ raise_error(
+ TypeError,
+ f"Number of qubits must be an integer but is {nqubits}.",
+ )
+ if nqubits < 1:
+ raise_error(
+ ValueError,
+ f"Number of qubits must be positive but is {nqubits}.",
+ )
+ self.nqubits = nqubits
+ self.wire_names = wire_names
+ self.init_kwargs = {
+ "nqubits": nqubits,
+ "accelerators": accelerators,
+ "density_matrix": density_matrix,
+ "wire_names": wire_names,
+ }
+ self.queue = _Queue(nqubits)
+ # Keep track of parametrized gates for the ``set_parameters`` method
+ self.parametrized_gates = _ParametrizedGates()
+ self.trainable_gates = _ParametrizedGates()
+ self.measurements = [] # list of non-collapsible measurements
+
+ self._final_state = None
+ self.compiled = None
+
+ self.has_collapse = False
+ self.has_unitary_channel = False
+ self.density_matrix = density_matrix
+
+ # for distributed circuits
+ self.accelerators = accelerators
+ self.ndevices = None
+ self.nglobal = None
+ self.nlocal = None
+ self.queues = None
+ if accelerators: # pragma: no cover
+ if density_matrix:
+ raise_error(
+ NotImplementedError,
+ "Distributed circuit is not implemented for density matrices.",
+ )
+ self._distributed_init(nqubits, accelerators)
+
+ def _distributed_init(self, nqubits, accelerators): # pragma: no cover
+ """Distributed implementation of :class:`qibo.models.circuit.Circuit`.
+
+ Uses multiple `accelerator` devices (GPUs) for applying gates to the state vector.
+ The full state vector is saved in the given `memory device` (usually the CPU)
+ during the simulation. A gate is applied by splitting the state to pieces
+ and copying each piece to an accelerator device that is used to perform the
+ matrix multiplication. An `accelerator` device can be used more than once
+ resulting to logical devices that are more than the physical accelerators in
+ the system.
+
+ Distributed circuits currently do not support native tensorflow gates,
+ compilation and callbacks.
+
+ Example:
+ .. code-block:: python
+
+ from qibo import Circuit
+ # The system has two GPUs and we would like to use each GPU twice
+ # resulting to four total logical accelerators
+ accelerators = {'/GPU:0': 2, '/GPU:1': 2}
+ # Define a circuit on 32 qubits to be run in the above GPUs keeping
+ # the full state vector in the CPU memory.
+ c = Circuit(32, accelerators)
+
+ Args:
+ nqubits (int): Total number of qubits in the circuit.
+ accelerators (dict): Dictionary that maps device names to the number of
+ times each device will be used.
+ The total number of logical devices must be a power of 2.
+ """
+ self.ndevices = sum(accelerators.values())
+ self.nglobal = float(np.log2(self.ndevices))
+ if not (self.nglobal.is_integer() and self.nglobal > 0):
+ raise_error(
+ ValueError,
+ "Number of calculation devices should be a power "
+ + f"of 2 but is {self.ndevices}.",
+ )
+ self.nglobal = int(self.nglobal)
+ self.nlocal = self.nqubits - self.nglobal
+
+ from qibo.models.distcircuit import DistributedQueues
+
+ self.queues = DistributedQueues(self)
+
+ def __add__(self, circuit):
+ """Add circuits.
+
+ Args:
+ circuit: Circuit to be added to the current one.
+
+ Returns:
+ The resulting circuit from the addition.
+ """
+ for k, kwarg1 in self.init_kwargs.items():
+ kwarg2 = circuit.init_kwargs[k]
+ if kwarg1 != kwarg2:
+ raise_error(
+ ValueError,
+ "Cannot add circuits with different kwargs. "
+ + f"{k} is {kwarg1} for first circuit and {kwarg2} "
+ + "for the second.",
+ )
+
+ newcircuit = self.__class__(**self.init_kwargs)
+ # Add gates from `self` to `newcircuit` (including measurements)
+ for gate in self.queue:
+ newcircuit.add(gate)
+ # Add gates from `circuit` to `newcircuit` (including measurements)
+ for gate in circuit.queue:
+ newcircuit.add(gate)
+
+ return newcircuit
+
+ @property
+ def wire_names(self):
+ return self._wire_names
+
+ @wire_names.setter
+ def wire_names(self, wire_names: Union[list, dict]):
+ if not isinstance(wire_names, (list, dict, type(None))):
+ raise_error(
+ TypeError,
+ f"``wire_names`` must be type ``list`` or ``dict``, but is {type(wire_names)}.",
+ )
+
+ if isinstance(wire_names, list):
+ if len(wire_names) != self.nqubits:
+ raise_error(
+ ValueError,
+ "Number of wire names must be equal to the number of qubits, "
+ f"but is {len(wire_names)}.",
+ )
+
+ if any([not isinstance(name, str) for name in wire_names]):
+ raise_error(ValueError, "all wire names must be type ``str``.")
+
+ self._wire_names = wire_names
+ elif isinstance(wire_names, dict):
+ if len(wire_names.keys()) > self.nqubits:
+ raise_error(
+ ValueError,
+ "number of elements in the ``wire_names`` dictionary "
+ + "cannot be bigger than ``nqubits``.",
+ )
+
+ if any([not isinstance(name, str) for name in wire_names.keys()]) or any(
+ [not isinstance(name, str) for name in wire_names.values()]
+ ):
+ raise_error(
+ ValueError,
+ "all keys and values in the ``wire_names`` dictionary must be type ``str``.",
+ )
+
+ self._wire_names = [
+ wire_names.get(f"q{i}", f"q{i}") for i in range(self.nqubits)
+ ]
+ else:
+ self._wire_names = [f"q{i}" for i in range(self.nqubits)]
+
+ @property
+ def repeated_execution(self):
+ return self.has_collapse or (
+ self.has_unitary_channel and not self.density_matrix
+ )
+
+ def on_qubits(self, *qubits):
+ """Generator of gates contained in the circuit acting on specified
+ qubits.
+
+ Useful for adding a circuit as a subroutine in a larger circuit.
+
+ Args:
+ qubits (int): Qubit ids that the gates should act.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import gates, models
+ # create small circuit on 4 qubits
+ smallc = models.Circuit(4)
+ smallc.add((gates.RX(i, theta=0.1) for i in range(4)))
+ smallc.add((gates.CNOT(0, 1), gates.CNOT(2, 3)))
+ # create large circuit on 8 qubits
+ largec = models.Circuit(8)
+ largec.add((gates.RY(i, theta=0.1) for i in range(8)))
+ # add the small circuit to the even qubits of the large one
+ largec.add(smallc.on_qubits(*range(0, 8, 2)))
+ """
+ if len(qubits) != self.nqubits:
+ raise_error(
+ ValueError,
+ f"Cannot return gates on {len(qubits)} qubits because "
+ + f"the circuit contains {self.nqubits} qubits.",
+ )
+ if self.accelerators and self.queues.queues: # pragma: no cover
+ raise_error(
+ RuntimeError,
+ "Cannot use distributed circuit as a subroutine after it was executed.",
+ )
+
+ qubit_map = {i: q for i, q in enumerate(qubits)}
+ for gate in self.queue:
+ yield gate.on_qubits(qubit_map)
+
+ def light_cone(self, *qubits):
+ """Reduces circuit to the qubits relevant for an observable.
+
+ Useful for calculating expectation values of local observables without
+ requiring simulation of large circuits.
+ Uses the light cone construction described in
+ `issue #571 `_.
+
+ Args:
+ qubits (int): Qubit ids that the observable has support on.
+
+ Returns:
+ circuit (qibo.models.Circuit): Circuit that contains only
+ the qubits that are required for calculating expectation
+ involving the given observable qubits.
+ qubit_map (dict): Dictionary mapping the qubit ids of the original
+ circuit to the ids in the new one.
+ """
+ # original qubits that are in the light cone
+ qubits = set(qubits)
+ # original gates that are in the light cone
+ list_of_gates = []
+ for gate in reversed(self.queue):
+ gate_qubits = set(gate.qubits)
+ if gate_qubits & qubits:
+ # if the gate involves any qubit included in the
+ # light cone, add all its qubits in the light cone
+ qubits |= gate_qubits
+ list_of_gates.append(gate)
+
+ # Create a new circuit ignoring gates that are not in the light cone
+ qubit_map = {q: i for i, q in enumerate(sorted(qubits))}
+ kwargs = dict(self.init_kwargs)
+ kwargs["nqubits"] = len(qubits)
+ circuit = self.__class__(**kwargs)
+ circuit.wire_names = [self.wire_names[q] for q in list(sorted(qubits))]
+ circuit.add(gate.on_qubits(qubit_map) for gate in reversed(list_of_gates))
+ return circuit, qubit_map
+
+ def _shallow_copy(self):
+ """Helper method for :meth:`qibo.models.circuit.Circuit.copy` and
+ :meth:`qibo.core.circuit.Circuit.fuse`."""
+ new_circuit = self.__class__(**self.init_kwargs)
+ new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates)
+ new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates)
+ new_circuit.measurements = self.measurements
+ return new_circuit
+
+ def copy(self, deep: bool = False):
+ """Creates a copy of the current ``circuit`` as a new ``Circuit`` model.
+
+ Args:
+ deep (bool): If ``True`` copies of the gate objects will be created
+ for the new circuit. If ``False``, the same gate objects of
+ ``circuit`` will be used.
+
+ Returns:
+ The copied circuit object.
+ """
+ if deep:
+ new_circuit = self.__class__(**self.init_kwargs)
+ for gate in self.queue:
+ if isinstance(gate, gates.FusedGate): # pragma: no cover
+ # impractical case
+ raise_error(
+ NotImplementedError,
+ "Cannot create deep copy of fused circuit.",
+ )
+ if isinstance(gate, gates.M):
+ new_circuit.add(gate.__class__(*gate.init_args, **gate.init_kwargs))
+ else:
+ new_circuit.add(copy.copy(gate))
+ else:
+ if self.accelerators: # pragma: no cover
+ raise_error(
+ ValueError,
+ "Non-deep copy is not allowed for distributed "
+ "circuits because they modify gate objects.",
+ )
+ new_circuit = self.__class__(**self.init_kwargs)
+ for gate in self.queue:
+ new_circuit.add(gate)
+ return new_circuit
+
+ def invert(self):
+ """Creates a new ``Circuit`` that is the inverse of the original.
+
+ Inversion is obtained by taking the dagger of all gates in reverse order.
+ If the original circuit contains parametrized gates, dagger will change
+ their parameters. This action is not persistent, so if the parameters
+ are updated afterwards, for example using :meth:`qibo.models.circuit.Circuit.set_parameters`,
+ the action of dagger will be overwritten.
+ If the original circuit contains measurement gates, these are included
+ in the inverted circuit.
+
+ Returns:
+ The circuit inverse.
+ """
+ from qibo.gates import ParametrizedGate
+
+ skip_measurements = True
+ measurements = []
+ new_circuit = self.__class__(**self.init_kwargs)
+ for gate in self.queue[::-1]:
+ if isinstance(gate, gates.Channel):
+ raise_error(
+ NotImplementedError,
+ "`invert` method not implemented for circuits that contain noise channels.",
+ )
+ elif isinstance(gate, gates.M) and skip_measurements:
+ measurements.append(gate)
+ else:
+ new_gate = gate.dagger()
+ if isinstance(gate, ParametrizedGate):
+ new_gate.trainable = gate.trainable
+ new_circuit.add(new_gate)
+ skip_measurements = False
+ new_circuit.add(measurements[::-1])
+ return new_circuit
+
+ def _check_noise_map(self, noise_map: NoiseMapType) -> NoiseMapType:
+ if isinstance(noise_map, list) and not all(
+ isinstance(n, (tuple, list)) for n in noise_map
+ ):
+ raise_error(
+ TypeError,
+ f"Type {type(noise_map)} of noise map is not recognized.",
+ )
+ elif isinstance(noise_map, dict):
+ if len(noise_map) != self.nqubits:
+ raise_error(
+ ValueError,
+ f"Noise map has {len(noise_map)} qubits while the circuit has {self.nqubits}.",
+ )
+
+ return noise_map
+
+ return {q: noise_map for q in range(self.nqubits)}
+
+ def decompose(self, *free: int):
+ """Decomposes circuit's gates to gates supported by OpenQASM.
+
+ Args:
+ free: Ids of free (work) qubits to use for gate decomposition.
+
+ Returns:
+ Circuit that contains only gates that are supported by OpenQASM
+ and has the same effect as the original circuit.
+ """
+ # FIXME: This method is not completed until the ``decompose`` is
+ # implemented for all gates not supported by OpenQASM.
+ decomp_circuit = self.__class__(self.nqubits)
+ for gate in self.queue:
+ decomp_circuit.add(gate.decompose(*free))
+ return decomp_circuit
+
+ def with_pauli_noise(self, noise_map: NoiseMapType):
+ """Creates a copy of the circuit with Pauli noise gates after each
+ gate.
+
+ If the original circuit uses state vectors then noise simulation will
+ be done using sampling and repeated circuit execution.
+ In order to use density matrices the original circuit should be created
+ setting the flag ``density_matrix=True``.
+ For more information we refer to the
+ :ref:`How to perform noisy simulation? ` example.
+
+ Args:
+ noise_map (dict): list of tuples :math:`(P_{k}, p_{k})`, where
+ :math:`P_{k}` is a ``str`` representing the :math:`k`-th
+ :math:`n`-qubit Pauli operator, and :math:`p_{k}` is the
+ associated probability.
+
+ Returns:
+ Circuit object that contains all the gates of the original circuit
+ and additional noise channels on all qubits after every gate.
+
+ Example:
+ .. testcode::
+
+ from qibo import Circuit, gates
+ # use density matrices for noise simulation
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+ noise_map = {
+ 0: list(zip(["X", "Z"], [0.1, 0.2])),
+ 1: list(zip(["Y", "Z"], [0.2, 0.1]))
+ }
+ noisy_c = c.with_pauli_noise(noise_map)
+ # ``noisy_c`` will be equivalent to the following circuit
+ c2 = Circuit(2, density_matrix=True)
+ c2.add(gates.H(0))
+ c2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Z", 0.2)]))
+ c2.add(gates.H(1))
+ c2.add(gates.PauliNoiseChannel(1, [("Y", 0.2), ("Z", 0.1)]))
+ c2.add(gates.CNOT(0, 1))
+ c2.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Z", 0.2)]))
+ c2.add(gates.PauliNoiseChannel(1, [("Y", 0.2), ("Z", 0.1)]))
+ """
+ if self.accelerators: # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "Distributed circuit does not support density matrices yet.",
+ )
+
+ noise_map = self._check_noise_map(noise_map)
+ # Generate noise gates
+ noise_gates = []
+ for gate in self.queue:
+ if isinstance(gate, gates.KrausChannel):
+ raise_error(
+ ValueError,
+ "`.with_pauli_noise` method is not available "
+ + "for circuits that already contain "
+ + "channels.",
+ )
+ noise_gates.append([])
+ if not isinstance(gate, gates.M):
+ for q in gate.qubits:
+ if q in noise_map and sum([row[1] for row in noise_map[q]]) > 0:
+ noise_gates[-1].append(gates.PauliNoiseChannel(q, noise_map[q]))
+
+ # Create new circuit with noise gates inside
+ noisy_circuit = self.__class__(**self.init_kwargs)
+ for i, gate in enumerate(self.queue):
+ noisy_circuit.add(gate)
+ for noise_gate in noise_gates[i]:
+ noisy_circuit.add(noise_gate)
+ return noisy_circuit
+
+ def add(self, gate):
+ """Add a gate to a given queue.
+
+ Args:
+ gate (:class:`qibo.gates.Gate`): the gate object to add.
+ See :ref:`Gates` for a list of available gates.
+ `gate` can also be an iterable or generator of gates.
+ In this case all gates in the iterable will be added in the
+ circuit.
+
+ Returns:
+ If the circuit contains measurement gates with ``collapse=True``
+ a ``sympy.Symbol`` that parametrizes the corresponding outcome.
+ """
+ if isinstance(gate, collections.abc.Iterable):
+ for g in gate:
+ self.add(g)
+
+ else:
+ if self.accelerators: # pragma: no cover
+ if isinstance(gate, gates.KrausChannel):
+ raise_error(
+ NotImplementedError,
+ "Distributed circuits do not support channels.",
+ )
+ elif self.nqubits - len(
+ gate.target_qubits
+ ) < self.nglobal and not isinstance(gate, gates.M):
+ # Check if there is sufficient number of local qubits
+ raise_error(
+ ValueError,
+ "Insufficient qubits to use for global in distributed circuit.",
+ )
+
+ if not isinstance(gate, gates.Gate):
+ raise_error(TypeError, f"Unknown gate type {type(gate)}.")
+
+ if self._final_state is not None:
+ raise_error(
+ RuntimeError,
+ "Cannot add gates to a circuit after it is executed.",
+ )
+
+ for q in gate.target_qubits:
+ if q >= self.nqubits:
+ raise_error(
+ ValueError,
+ f"Attempting to add gate with target qubits {gate.target_qubits} "
+ + f"on a circuit of {self.nqubits} qubits.",
+ )
+
+ if isinstance(gate, gates.M):
+ # The following loop is useful when two circuits are added together:
+ # all the gates in the basis of the measure gates should not
+ # be added to the new circuit, otherwise once the measure gate is added in the circuit
+ # there will be two of the same.
+
+ for base in gate.basis:
+ if base not in self.queue:
+ self.add(base)
+
+ self.queue.append(gate)
+ if gate.register_name is None:
+ # add default register name
+ nreg = self.queue.nmeasurements - 1
+ gate.register_name = f"register{nreg}"
+ else:
+ name = gate.register_name
+ for mgate in self.measurements:
+ if name == mgate.register_name:
+ raise_error(
+ KeyError, f"Register {name} already exists in circuit."
+ )
+
+ gate.result.circuit = self
+ if gate.collapse:
+ self.has_collapse = True
+ else:
+ self.measurements.append(gate)
+ return gate.result
+
+ else:
+ self.queue.append(gate)
+ for measurement in list(self.measurements):
+ if set(measurement.qubits) & set(gate.qubits):
+ measurement.collapse = True
+ self.has_collapse = True
+ self.measurements.remove(measurement)
+
+ if isinstance(gate, gates.UnitaryChannel):
+ self.has_unitary_channel = True
+ if isinstance(gate, gates.ParametrizedGate):
+ self.parametrized_gates.append(gate)
+ if gate.trainable:
+ self.trainable_gates.append(gate)
+
+ @property
+ def measurement_tuples(self):
+ # used for testing only
+ return {m.register_name: m.target_qubits for m in self.measurements}
+
+ @property
+ def ngates(self) -> int:
+ """Total number of gates/operations in the circuit."""
+ return len(self.queue)
+
+ @property
+ def depth(self) -> int:
+ """Circuit depth if each gate is placed at the earliest possible
+ position."""
+ return len(self.queue.moments)
+
+ @property
+ def gate_types(self) -> collections.Counter:
+ """``collections.Counter`` with the number of appearances of each gate type."""
+ gatecounter = collections.Counter()
+ for gate in self.queue:
+ gatecounter[gate.__class__] += 1
+ return gatecounter
+
+ @property
+ def gate_names(self) -> collections.Counter:
+ """``collections.Counter`` with the number of appearances of each gate name."""
+ gatecounter = collections.Counter()
+ for gate in self.queue:
+ gatecounter[gate.name] += 1
+ return gatecounter
+
+ def gates_of_type(self, gate: Union[str, type]) -> List[Tuple[int, gates.Gate]]:
+ """Finds all gate objects of specific type or name.
+
+ This method can be affected by how :meth:`qibo.gates.Gate.controlled_by`
+ behaves with certain gates. To see how :meth:`qibo.gates.Gate.controlled_by`
+ affects gates, we refer to the documentation of :meth:`qibo.gates.Gate.controlled_by`.
+
+ Args:
+ gate (str or type): The name of a gate or the corresponding gate class.
+
+ Returns:
+ list: gates that are in the circuit and have the same type as ``gate``.
+ The list contains tuples ``(k, g)`` where ``k`` is the index of the gate
+ ``g`` in the circuit's gate queue.
+ """
+ if isinstance(gate, str):
+ return [(i, g) for i, g in enumerate(self.queue) if g.name == gate]
+ if isinstance(gate, type) and issubclass(gate, gates.Gate):
+ return [(i, g) for i, g in enumerate(self.queue) if isinstance(g, gate)]
+ raise_error(TypeError, f"Gate identifier {gate} not recognized.")
+
+ def _set_parameters_list(self, parameters, n):
+ """Helper method for ``set_parameters`` when a list is given.
+
+ Also works if ``parameters`` is ``np.ndarray`` or ``tf.Tensor``.
+ """
+ if n == len(self.trainable_gates):
+ for i, gate in enumerate(self.trainable_gates):
+ gate.parameters = parameters[i]
+ elif n == self.trainable_gates.nparams:
+ parameters = list(parameters)
+ k = 0
+ for i, gate in enumerate(self.trainable_gates):
+ if gate.nparams == 1:
+ gate.parameters = parameters[i + k]
+ else:
+ gate.parameters = parameters[i + k : i + k + gate.nparams]
+ k += gate.nparams - 1
+ else:
+ raise_error(
+ ValueError,
+ f"Given list of parameters has length {n} while "
+ + f"the circuit contains {len(self.trainable_gates)} parametrized gates.",
+ )
+
+ def set_parameters(self, parameters):
+ """Updates the parameters of the circuit's parametrized gates.
+
+ For more information on how to use this method we refer to the
+ :ref:`How to use parametrized gates?` example.
+
+ Args:
+ parameters: Container holding the new parameter values.
+ It can have one of the following types:
+ List with length equal to the number of parametrized gates and
+ each of its elements compatible with the corresponding gate.
+ Dictionary with keys that are references to the parametrized
+ gates and values that correspond to the new parameters for
+ each gate.
+ Flat list with length equal to the total number of free
+ parameters in the circuit.
+ A backend supported tensor (for example ``np.ndarray`` or
+ ``tf.Tensor``) may also be given instead of a flat list.
+
+
+ Example:
+ .. testcode::
+
+ from qibo import Circuit, gates
+ # create a circuit with all parameters set to 0.
+ c = Circuit(3)
+ c.add(gates.RX(0, theta=0))
+ c.add(gates.RY(1, theta=0))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.fSim(0, 2, theta=0, phi=0))
+ c.add(gates.H(2))
+
+ # set new values to the circuit's parameters using list
+ params = [0.123, 0.456, (0.789, 0.321)]
+ c.set_parameters(params)
+ # or using dictionary
+ params = {c.queue[0]: 0.123, c.queue[1]: 0.456,
+ c.queue[3]: (0.789, 0.321)}
+ c.set_parameters(params)
+ # or using flat list (or an equivalent `np.array`/`tf.Tensor`)
+ params = [0.123, 0.456, 0.789, 0.321]
+ c.set_parameters(params)
+ """
+ from collections.abc import Iterable
+
+ if isinstance(parameters, dict):
+ diff = set(parameters.keys()) - self.trainable_gates.set
+ if diff:
+ raise_error(
+ KeyError,
+ f"Dictionary contains gates {diff} which are "
+ + "not on the list of parametrized gates of the circuit.",
+ )
+ for gate, params in parameters.items():
+ gate.parameters = params
+ elif isinstance(parameters, Iterable) and not isinstance(
+ parameters, (set, str)
+ ):
+ try:
+ nparams = int(parameters.shape[0])
+ except AttributeError:
+ nparams = len(parameters)
+ self._set_parameters_list(parameters, nparams)
+ else:
+ raise_error(TypeError, f"Invalid type of parameters {type(parameters)}.")
+
+ def get_parameters(
+ self, format: str = "list", include_not_trainable: bool = False
+ ) -> Union[List, Dict]: # pylint: disable=W0622
+ """Returns the parameters of all parametrized gates in the circuit.
+
+ Inverse method of :meth:`qibo.models.circuit.Circuit.set_parameters`.
+
+ Args:
+ format (str): How to return the variational parameters.
+ Available formats are ``'list'``, ``'dict'`` and ``'flatlist'``.
+ See :meth:`qibo.models.circuit.Circuit.set_parameters`
+ for more details on each format. Default is ``'list'``.
+ include_not_trainable (bool): If ``True`` it includes the parameters
+ of non-trainable parametrized gates in the returned list or
+ dictionary. Default is ``False``.
+ """
+ if include_not_trainable:
+ parametrized_gates = self.parametrized_gates
+ else:
+ parametrized_gates = self.trainable_gates
+
+ if format == "list":
+ params = [gate.parameters for gate in parametrized_gates]
+ elif format == "dict":
+ params = {gate: gate.parameters for gate in parametrized_gates}
+ elif format == "flatlist":
+ params = []
+ for gate in parametrized_gates:
+ gparams = gate.parameters
+ if len(gparams) == 1:
+ gparams = gparams[0]
+ if isinstance(gparams, np.ndarray):
+
+ def traverse(x):
+ if isinstance(x, np.ndarray):
+ for v1 in x:
+ yield from traverse(v1)
+ else:
+ yield x
+
+ params.extend(traverse(gparams))
+ elif isinstance(gparams, collections.abc.Iterable):
+ params.extend(gparams)
+ else:
+ params.append(gparams)
+ else:
+ raise_error(
+ ValueError,
+ f"Unknown format {format} given in ``get_parameters``.",
+ )
+ return params
+
+ def associate_gates_with_parameters(self):
+ """Associates to each parameter its gate.
+
+ Returns:
+ A nparams-long flatlist whose i-th element is the gate parameterized
+ by the i-th parameter.
+ """
+
+ parameter_to_gate = []
+ for gate in self.parametrized_gates:
+ npar = len(gate.parameters)
+ parameter_to_gate.extend([gate] * npar)
+
+ return parameter_to_gate
+
+ def summary(self) -> str:
+ """Generates a summary of the circuit.
+
+ The summary contains the circuit depths, total number of qubits and
+ the all gates sorted in decreasing number of appearance.
+
+ Example:
+ .. testcode::
+
+ from qibo import Circuit, gates
+
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.H(2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+
+ print(c.summary())
+ # Prints
+ '''
+ Circuit depth = 5
+ Total number of gates = 6
+ Number of qubits = 3
+ Most common gates:
+ h: 3
+ cx: 2
+ ccx: 1
+ '''
+
+ .. testoutput::
+ :hide:
+
+ Circuit depth = 5
+ Total number of gates = 6
+ Number of qubits = 3
+ Most common gates:
+ h: 3
+ cx: 2
+ ccx: 1
+ """
+ logs = [
+ f"Circuit depth = {self.depth}",
+ f"Total number of gates = {self.ngates}",
+ f"Number of qubits = {self.nqubits}",
+ "Most common gates:",
+ ]
+ common_gates = self.gate_names.most_common()
+ logs.extend(f"{g}: {n}" for g, n in common_gates)
+ return "\n".join(logs)
+
+ def fuse(self, max_qubits=2):
+ """Creates an equivalent circuit by fusing gates for increased
+ simulation performance.
+
+ Args:
+ max_qubits (int): Maximum number of qubits in the fused gates.
+
+ Returns:
+ A :class:`qibo.core.circuit.Circuit` object containing
+ :class:`qibo.gates.FusedGate` gates, each of which
+ corresponds to a group of some original gates.
+ For more details on the fusion algorithm we refer to the
+ :ref:`Circuit fusion ` section.
+
+ Example:
+ .. testcode::
+
+ from qibo import gates, models
+ c = models.Circuit(2)
+ c.add([gates.H(0), gates.H(1)])
+ c.add(gates.CNOT(0, 1))
+ c.add([gates.Y(0), gates.Y(1)])
+ # create circuit with fused gates
+ fused_c = c.fuse()
+ # now ``fused_c`` contains a single ``FusedGate`` that is
+ # equivalent to applying the five original gates
+ """
+ if self.accelerators: # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "Fusion is not implemented for distributed circuits.",
+ )
+
+ queue = self.queue.to_fused()
+ for gate in queue:
+ if not gate.marked:
+ for q in gate.qubits:
+ # fuse nearest neighbors forth in time
+ neighbor = gate.right_neighbors.get(q)
+ if gate.can_fuse(neighbor, max_qubits):
+ gate.fuse(neighbor)
+ # fuse nearest neighbors back in time
+ neighbor = gate.left_neighbors.get(q)
+ if gate.can_fuse(neighbor, max_qubits):
+ neighbor.fuse(gate)
+ # create a circuit and assign the new queue
+ circuit = self._shallow_copy()
+ circuit.queue = queue.from_fused()
+ return circuit
+
+ def unitary(self, backend=None):
+ """Creates the unitary matrix corresponding to all circuit gates.
+
+ This is a :math:`2^{n} \\times 2^{n}`` matrix obtained by
+ multiplying all circuit gates, where :math:`n` is ``nqubits``.
+ """
+
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ fgate = gates.FusedGate(*range(self.nqubits))
+ for gate in self.queue:
+ if isinstance(gate, gates.Channel):
+ raise_error(
+ NotImplementedError,
+ "`unitary` method not implemented for circuits that contain noise channels.",
+ )
+ elif not isinstance(gate, (gates.SpecialGate, gates.M)):
+ fgate.append(gate)
+ return fgate.matrix(backend)
+
+ @property
+ def final_state(self):
+ """Returns the final state after full simulation of the circuit.
+
+ If the circuit is executed more than once, only the last final
+ state is returned.
+ """
+ if self._final_state is None:
+ raise_error(
+ RuntimeError,
+ "Cannot access final state before the circuit is executed.",
+ )
+ return self._final_state
+
+ def compile(self, backend=None):
+ if self.accelerators: # pragma: no cover
+ raise_error(
+ RuntimeError, "Cannot compile circuit that uses custom operators."
+ )
+
+ if self.compiled:
+ raise_error(RuntimeError, "Circuit is already compiled.")
+ if not self.queue:
+ raise_error(RuntimeError, "Cannot compile circuit without gates.")
+ for gate in self.queue:
+ if isinstance(gate, gates.CallbackGate): # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "Circuit compilation is not available with callbacks.",
+ )
+
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ from qibo.result import CircuitResult, QuantumState
+
+ executor = lambda state, nshots: backend.execute_circuit(
+ self, state, nshots
+ ).state()
+ self.compiled = type("CompiledExecutor", (), {})()
+ self.compiled.executor = backend.compile(executor)
+ if self.measurements:
+ self.compiled.result = lambda state, nshots: CircuitResult(
+ state, self.measurements, backend, nshots=nshots
+ )
+ else:
+ self.compiled.result = lambda state, nshots: QuantumState(state, backend)
+
+ def execute(self, initial_state=None, nshots=1000):
+ """Executes the circuit. Exact implementation depends on the backend.
+
+ Args:
+ initial_state (`np.ndarray` or :class:`qibo.models.circuit.Circuit`):
+ Initial configuration. It can be specified by the setting the state
+ vector using an array or a circuit. If ``None``, the initial state
+ is ``|000..00>``.
+ nshots (int): Number of shots.
+
+ Returns:
+ either a ``qibo.result.QuantumState``, ``qibo.result.MeasurementOutcomes``
+ or ``qibo.result.CircuitResult`` depending on the circuit's configuration.
+ """
+ if self.compiled:
+ # pylint: disable=E1101
+ state = self.compiled.executor(initial_state, nshots)
+ self._final_state = self.compiled.result(state, nshots)
+ return self._final_state
+ else:
+ from qibo.backends import GlobalBackend
+
+ if self.accelerators: # pragma: no cover
+ return GlobalBackend().execute_distributed_circuit(
+ self, initial_state, nshots
+ )
+ else:
+ return GlobalBackend().execute_circuit(self, initial_state, nshots)
+
+ def __call__(self, initial_state=None, nshots=1000):
+ """Equivalent to ``circuit.execute``."""
+ return self.execute(initial_state=initial_state, nshots=nshots)
+
+ @property
+ def raw(self) -> dict:
+ """Serialize to dictionary.
+
+ This is a thin wrapper over :meth:`Gate.raw`.
+ """
+ return {
+ "queue": [gate.raw for gate in self.queue],
+ "nqubits": self.nqubits,
+ "density_matrix": self.density_matrix,
+ "qibo_version": qibo.__version__,
+ }
+
+ @classmethod
+ def from_dict(cls, raw):
+ """Load from serialization.
+
+ Essentially the counter-part of :meth:`raw`.
+ """
+ circ = cls(raw["nqubits"], density_matrix=raw["density_matrix"])
+
+ for gate in raw["queue"]:
+ circ.add(Gate.from_dict(gate))
+
+ return circ
+
+ def to_qasm(self):
+ """Convert circuit to QASM.
+
+ Args:
+ filename (str): The filename where the code is saved.
+ """
+ from qibo import __version__
+
+ code = [f"// Generated by QIBO {__version__}"]
+ code += ["OPENQASM 2.0;"]
+ code += ['include "qelib1.inc";']
+ code += [f"qreg q[{self.nqubits}];"]
+
+ # Set measurements
+ for register, qubits in self.measurement_tuples.items():
+ if not register.islower():
+ raise_error(
+ NameError,
+ "OpenQASM does not support capital letters in "
+ + f"register names but {register} was used",
+ )
+ code.append(f"creg {register}[{len(qubits)}];")
+
+ # Add gates
+ for gate in self.queue:
+ if isinstance(gate, gates.M):
+ continue
+
+ if gate.is_controlled_by:
+ raise_error(
+ ValueError, "OpenQASM does not support multi-controlled gates."
+ )
+
+ qubits = ",".join(f"q[{i}]" for i in gate.qubits)
+ if isinstance(gate, gates.ParametrizedGate):
+ params = (str(x) for x in gate.parameters)
+ name = f"{gate.qasm_label}({', '.join(params)})"
+ else:
+ name = gate.qasm_label
+ code.append(f"{name} {qubits};")
+
+ # Add measurements
+ for register, qubits in self.measurement_tuples.items():
+ for i, q in enumerate(qubits):
+ code.append(f"measure q[{q}] -> {register}[{i}];")
+
+ return "\n".join(code)
+
+ @classmethod
+ def from_qasm(cls, qasm_code, accelerators=None, density_matrix=False):
+ """Constructs a circuit from QASM code.
+
+ Args:
+ qasm_code (str): String with the QASM script.
+
+ Returns:
+ A :class:`qibo.models.circuit.Circuit` that contains the gates
+ specified by the given QASM script.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import gates, models
+ qasm_code = '''OPENQASM 2.0;
+ include "qelib1.inc";
+ qreg q[2];
+ h q[0];
+ h q[1];
+ cx q[0],q[1];'''
+ c = models.Circuit.from_qasm(qasm_code)
+ # is equivalent to creating the following circuit
+ c2 = models.Circuit(2)
+ c2.add(gates.H(0))
+ c2.add(gates.H(1))
+ c2.add(gates.CNOT(0, 1))
+ """
+ parser = QASMParser()
+ return parser.to_circuit(qasm_code, accelerators, density_matrix)
+
+ def _update_draw_matrix(self, matrix, idx, gate, gate_symbol=None):
+ """Helper method for :meth:`qibo.models.circuit.Circuit.draw`."""
+ if gate_symbol is None:
+ if gate.draw_label:
+ gate_symbol = gate.draw_label
+ elif gate.name:
+ gate_symbol = gate.name[:4]
+ else:
+ raise_error(
+ NotImplementedError,
+ f"{gate.__class__.__name__} gate is not supported by `circuit.draw`",
+ )
+
+ if isinstance(gate, gates.CallbackGate):
+ targets = list(range(self.nqubits))
+ else:
+ targets = list(gate.target_qubits)
+ controls = list(gate.control_qubits)
+
+ # identify boundaries
+ qubits = targets + controls
+ qubits.sort()
+ min_qubits_id = qubits[0]
+ max_qubits_id = qubits[-1]
+
+ # identify column
+ col = idx[targets[0]] if not controls and len(targets) == 1 else max(idx)
+
+ # extend matrix
+ for iq in range(self.nqubits):
+ matrix[iq].extend((1 + col - len(matrix[iq])) * [""])
+
+ # fill
+ for iq in range(min_qubits_id, max_qubits_id + 1):
+ if iq in targets:
+ matrix[iq][col] = gate_symbol
+ elif iq in controls:
+ matrix[iq][col] = "o"
+ else:
+ matrix[iq][col] = "|"
+
+ # update indexes
+ if not controls and len(targets) == 1:
+ idx[targets[0]] += 1
+ else:
+ idx = [col + 1] * self.nqubits
+
+ return matrix, idx
+
+ def draw(self, line_wrap=70, legend=False) -> str:
+ """Draw text circuit using unicode symbols.
+
+ Args:
+ line_wrap (int): maximum number of characters per line. This option
+ split the circuit text diagram in chunks of line_wrap characters.
+ legend (bool): If ``True`` prints a legend below the circuit for
+ callbacks and channels. Default is ``False``.
+
+ Return:
+ String containing text circuit diagram.
+ """
+ # build string representation of gates
+ matrix = [[] for _ in range(self.nqubits)]
+ idx = [0] * self.nqubits
+
+ for gate in self.queue:
+ if isinstance(gate, gates.FusedGate):
+ # start fused gate
+ matrix, idx = self._update_draw_matrix(matrix, idx, gate, "[")
+ # draw gates contained in the fused gate
+ for subgate in gate.gates:
+ matrix, idx = self._update_draw_matrix(matrix, idx, subgate)
+ # end fused gate
+ matrix, idx = self._update_draw_matrix(matrix, idx, gate, "]")
+ else:
+ matrix, idx = self._update_draw_matrix(matrix, idx, gate)
+
+ # Add some spacers
+ for col in range(len(matrix[0])):
+ maxlen = max(len(matrix[l][col]) for l in range(self.nqubits))
+ for row in range(self.nqubits):
+ matrix[row][col] += "─" * (1 + maxlen - len(matrix[row][col]))
+
+ # Print to terminal
+ max_name_len = max(len(name) for name in self.wire_names)
+ output = ""
+ for q in range(self.nqubits):
+ output += (
+ self.wire_names[q]
+ + " " * (max_name_len - len(self.wire_names[q]))
+ + ": ─"
+ + "".join(matrix[q])
+ + "\n"
+ )
+
+ # legend
+ if legend:
+ from tabulate import tabulate
+
+ legend_rows = {
+ (i.name, i.draw_label)
+ for i in self.queue
+ if isinstance(i, (gates.SpecialGate, gates.Channel))
+ }
+
+ table = tabulate(
+ [list(l) for l in sorted(legend_rows)],
+ headers=["Gate", "Symbol"],
+ tablefmt="orgtbl",
+ )
+ table = "\n Legend for callbacks and channels: \n" + table
+
+ # line wrap
+ if line_wrap:
+ loutput = output.splitlines()
+
+ def chunkstring(string, length):
+ nchunks = range(0, len(string), length)
+ return (string[i : length + i] for i in nchunks), len(nchunks)
+
+ for row in range(self.nqubits):
+ chunks, nchunks = chunkstring(
+ loutput[row][3 + max_name_len - 1 :], line_wrap
+ )
+ if nchunks == 1:
+ loutput = None
+ break
+ for i, c in enumerate(chunks):
+ loutput += ["" for _ in range(self.nqubits)]
+ suffix = " ...\n"
+ prefix = (
+ self.wire_names[row]
+ + " " * (max_name_len - len(self.wire_names[row]))
+ + ": "
+ )
+ if i == 0:
+ prefix += " " * 4
+ elif row == 0:
+ prefix = "\n" + prefix + "... "
+ else:
+ prefix += "... "
+ if i == nchunks - 1:
+ suffix = "\n"
+ loutput[row + i * self.nqubits] = prefix + c + suffix
+ if loutput is not None:
+ output = "".join(loutput)
+
+ if legend:
+ output += table
+
+ return output.rstrip("\n")
diff --git a/src/qibo/models/dbi/__init__.py b/src/qibo/models/dbi/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/qibo/models/dbi/double_bracket.py b/src/qibo/models/dbi/double_bracket.py
new file mode 100644
index 000000000..ef870535e
--- /dev/null
+++ b/src/qibo/models/dbi/double_bracket.py
@@ -0,0 +1,376 @@
+from copy import copy
+from enum import Enum, auto
+from typing import Optional
+
+import numpy as np
+
+from qibo.config import raise_error
+from qibo.hamiltonians import Hamiltonian
+from qibo.models.dbi.utils import *
+from qibo.models.dbi.utils_scheduling import (
+ grid_search_step,
+ hyperopt_step,
+ polynomial_step,
+ simulated_annealing_step,
+)
+from qibo.quantum_info.linalg_operations import commutator, matrix_exponentiation
+
+
+class DoubleBracketGeneratorType(Enum):
+ """Define DBF evolution."""
+
+ canonical = auto()
+ """Use canonical commutator."""
+ single_commutator = auto()
+ """Use single commutator."""
+ group_commutator = auto()
+ """Use group commutator approximation"""
+ group_commutator_third_order = auto()
+ """Implements Eq. (8) of Ref. [1], i.e.
+
+ .. math::
+ e^{\\frac{\\sqrt{5}-1}{2}isH} \\,
+ e^{\\frac{\\sqrt{5}-1}{2}isD} \\,
+ e^{-isH} \\,
+ e^{isD} \\,
+ e^{\\frac{3-\\sqrt{5}}{2}isH} \\,
+ e^{isD} \\approx e^{-s^2[H,D]} + O(s^4) \\, .
+
+ :math:`s` must be taken as :math:`\\sqrt{s}` to approximate the flow using the commutator.
+ """
+
+
+class DoubleBracketCostFunction(str, Enum):
+ """Define the DBI cost function."""
+
+ off_diagonal_norm = "off_diagonal_norm"
+ """Use off-diagonal norm as cost function."""
+ least_squares = "least_squares"
+ """Use least squares as cost function."""
+ energy_fluctuation = "energy_fluctuation"
+ """Use energy fluctuation as cost function."""
+
+
+class DoubleBracketScheduling(Enum):
+ """Define the DBI scheduling strategies."""
+
+ hyperopt = hyperopt_step
+ """Use hyperopt package."""
+ grid_search = grid_search_step
+ """Use greedy grid search."""
+ polynomial_approximation = polynomial_step
+ """Use polynomial expansion (analytical) of the loss function."""
+ simulated_annealing = simulated_annealing_step
+ """Use simulated annealing algorithm"""
+
+
+class DoubleBracketIteration:
+ """
+ Class implementing the Double Bracket iteration algorithm. For more details, see Ref. [1].
+
+ Args:
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): starting Hamiltonian.
+ mode (:class:`qibo.models.dbi.double_bracket.DoubleBracketGeneratorType`):
+ type of generator of the evolution.
+ scheduling (:class:`qibo.models.dbi.double_bracket.DoubleBracketScheduling`):
+ type of scheduling strategy.
+ cost (:class:`qibo.models.dbi.double_bracket.DoubleBracketCostFunction`):
+ type of cost function.
+ ref_state (ndarray): reference state for computing the energy fluctuation.
+
+ Example:
+ .. testcode::
+
+ from qibo.models.dbi.double_bracket import DoubleBracketIteration, DoubleBracketGeneratorType
+ from qibo.quantum_info import random_hermitian
+ from qibo.hamiltonians import Hamiltonian
+
+ nqubits = 4
+ h0 = random_hermitian(2**nqubits, seed=2)
+ dbf = DoubleBracketIteration(Hamiltonian(nqubits=nqubits, matrix=h0))
+
+ # diagonalized matrix
+ dbf.h
+
+ References:
+ 1. M. Gluza, *Double-bracket quantum algorithms for diagonalization*.
+ `arXiv:2206.11772 [quant-ph] `_.
+ """
+
+ def __init__(
+ self,
+ hamiltonian,
+ mode=DoubleBracketGeneratorType.canonical,
+ scheduling=DoubleBracketScheduling.grid_search,
+ cost=DoubleBracketCostFunction.off_diagonal_norm,
+ ref_state=None,
+ ):
+ self.h = hamiltonian
+ self.h0 = copy(self.h)
+ self.mode = mode
+ self.scheduling = scheduling
+ self.cost = cost
+ self.ref_state = ref_state
+
+ def __call__(self, step: float, mode=None, d=None):
+ """We use the following convention:
+
+ .. math::
+ H^{'} = U^{\\dagger} \\, H \\, U \\, ,
+
+ where :math:`U=e^{-s\\,W}`, and :math:`W =[D, H]` (or depending on ``mode`` an
+ approximation, see `eval_dbr_unitary`). If :math:`s > 0`, then,
+ for :math:`D = \\Delta(H)`, the GWW DBR will give a :math:`\\sigma`-decrease.
+
+ References:
+ 1. M. Gluza, *Double-bracket quantum algorithms for diagonalization*.
+ `arXiv:2206.11772 [quant-ph] `_."""
+
+ operator = self.eval_dbr_unitary(step, mode, d)
+ operator_dagger = self.backend.cast(
+ np.array(np.matrix(self.backend.to_numpy(operator)).getH())
+ )
+ self.h.matrix = operator_dagger @ self.h.matrix @ operator
+ return operator
+
+ def eval_dbr_unitary(
+ self,
+ step: float,
+ mode=None,
+ d=None,
+ ):
+ """In :meth:`qibo.models.dbi.double_bracket.DoubleBracketIteration.__call__`,
+ we are working in the following convention:
+
+ .. math::
+ H^{'} = U^{\\dagger} \\, H \\, U \\, ,
+
+ where :math:`U = e^{-s\\,W}`, and :math:`W = [D, H]`
+ (or an approximation of that by a group commutator).
+ That is convenient because if we switch from the DBI in the Heisenberg picture for the
+ Hamiltonian, we get that the transformation of the state is
+ :math:`|\\psi'\\rangle = U \\, |\\psi\\rangle`, so that
+
+ .. math::
+ \\langle H\\rangle_{\\psi'} = \\langle H' \\rangle_\\psi \\, ,
+
+ i.e. when writing the unitary acting on the state dagger notation is avoided).
+ The group commutator must approximate :math:`U = e^{-s\\, [D,H]}`.
+ This is achieved by setting :math:`r = \\sqrt{s}` so that
+
+ .. math::
+ V = e^{-i\\,r\\,H} \\, e^{i\\,r\\,D} \\, e^{i\\,r\\,H} \\, e^{-i\\,r\\,D}
+
+ because
+
+ .. math::
+ e^{-i\\,r\\,H} \\, D \\, e^{i\\,r\\,H} = D + i\\,r\\,[D, H] +O(r^2)
+
+ so
+
+ .. math::
+ V \\approx \\exp\\left(i\\,r\\,D + i^2 \\, r^2 \\, [D, H] + O(r^2) -i\\,r\\,D\\right) \\approx U \\, .
+
+ See the Appendix in Ref. [1] for the complete derivation.
+
+ References:
+ 1. M. Gluza, *Double-bracket quantum algorithms for diagonalization*.
+ `arXiv:2206.11772 [quant-ph] `_.
+ """
+ if mode is None:
+ mode = self.mode
+
+ if mode is DoubleBracketGeneratorType.canonical:
+ operator = matrix_exponentiation(
+ -1.0j * step,
+ self.commutator(self.diagonal_h_matrix, self.h.matrix),
+ backend=self.backend,
+ )
+ elif mode is DoubleBracketGeneratorType.single_commutator:
+ if d is None:
+ d = self.diagonal_h_matrix
+ operator = matrix_exponentiation(
+ -1.0j * step,
+ self.commutator(self.backend.cast(d), self.h.matrix),
+ backend=self.backend,
+ )
+ elif mode is DoubleBracketGeneratorType.group_commutator:
+ if d is None:
+ d = self.diagonal_h_matrix
+ operator = (
+ self.h.exp(step)
+ @ matrix_exponentiation(-step, d, backend=self.backend)
+ @ self.h.exp(-step)
+ @ matrix_exponentiation(step, d, backend=self.backend)
+ )
+ elif mode is DoubleBracketGeneratorType.group_commutator_third_order:
+ if d is None:
+ d = self.diagonal_h_matrix
+ operator = (
+ self.h.exp(-step * (np.sqrt(5) - 1) / 2)
+ @ matrix_exponentiation(
+ -step * (np.sqrt(5) - 1) / 2, d, backend=self.backend
+ )
+ @ self.h.exp(step)
+ @ matrix_exponentiation(
+ step * (np.sqrt(5) + 1) / 2, d, backend=self.backend
+ )
+ @ self.h.exp(-step * (3 - np.sqrt(5)) / 2)
+ @ matrix_exponentiation(-step, d, backend=self.backend)
+ )
+ operator = (
+ matrix_exponentiation(step, d, backend=self.backend)
+ @ self.h.exp(step * (3 - np.sqrt(5)) / 2)
+ @ matrix_exponentiation(
+ -step * (np.sqrt(5) + 1) / 2, d, backend=self.backend
+ )
+ @ self.h.exp(-step)
+ @ matrix_exponentiation(
+ step * (np.sqrt(5) - 1) / 2, d, backend=self.backend
+ )
+ @ self.h.exp(step * (np.sqrt(5) - 1) / 2)
+ )
+ else:
+ raise_error(
+ NotImplementedError, f"The mode {mode} is not supported"
+ ) # pragma: no cover
+
+ return operator
+
+ @staticmethod
+ def commutator(a, b):
+ """Compute commutator between two arrays."""
+ return commutator(a, b)
+
+ @property
+ def diagonal_h_matrix(self):
+ """Diagonal H matrix."""
+ return self.backend.cast(np.diag(np.diag(self.backend.to_numpy(self.h.matrix))))
+
+ @property
+ def off_diag_h(self):
+ """Off-diagonal H matrix."""
+ return self.h.matrix - self.diagonal_h_matrix
+
+ @property
+ def off_diagonal_norm(self):
+ """Hilbert Schmidt norm of off-diagonal part of H matrix, namely :math:`\\text{Tr}(\\sqrt{A^{\\dagger} A})`."""
+ off_diag_h_dag = self.backend.cast(
+ np.matrix(self.backend.to_numpy(self.off_diag_h)).getH()
+ )
+ return np.sqrt(
+ np.real(np.trace(self.backend.to_numpy(off_diag_h_dag @ self.off_diag_h)))
+ )
+
+ @property
+ def backend(self):
+ """Get Hamiltonian's backend."""
+ return self.h0.backend
+
+ @property
+ def nqubits(self):
+ """Number of qubits."""
+ return self.h.nqubits
+
+ def least_squares(self, d: np.array):
+ """Least squares cost function."""
+ d = self.backend.to_numpy(d)
+ return np.real(
+ 0.5 * np.linalg.norm(d) ** 2
+ - np.trace(self.backend.to_numpy(self.h.matrix) @ d)
+ )
+
+ def choose_step(
+ self,
+ d: Optional[np.array] = None,
+ scheduling: Optional[DoubleBracketScheduling] = None,
+ **kwargs,
+ ):
+ """Calculate the optimal step using respective the `scheduling` methods."""
+ if scheduling is None:
+ scheduling = self.scheduling
+ step = scheduling(self, d=d, **kwargs)
+ # TODO: write test for this case
+ if (
+ step is None
+ and scheduling is DoubleBracketScheduling.polynomial_approximation
+ ): # pragma: no cover
+ kwargs["n"] = kwargs.get("n", 3)
+ kwargs["n"] += 1
+ # if n==n_max, return None
+ step = scheduling(self, d=d, **kwargs)
+ # if for a given polynomial order n, no solution is found, we increase the order of the polynomial by 1
+ return step
+
+ def loss(self, step: float, d: np.array = None, look_ahead: int = 1):
+ """
+ Compute loss function distance between `look_ahead` steps.
+
+ Args:
+ step (float): iteration step.
+ d (np.array): diagonal operator, use canonical by default.
+ look_ahead (int): number of iteration steps to compute the loss function;
+ """
+ # copy initial hamiltonian
+ h_copy = copy(self.h)
+
+ for _ in range(look_ahead):
+ self.__call__(mode=self.mode, step=step, d=d)
+
+ # loss values depending on the cost function
+ if self.cost is DoubleBracketCostFunction.off_diagonal_norm:
+ loss = self.off_diagonal_norm
+ elif self.cost is DoubleBracketCostFunction.least_squares:
+ loss = self.least_squares(d)
+ elif self.cost == DoubleBracketCostFunction.energy_fluctuation:
+ loss = self.energy_fluctuation(self.ref_state)
+
+ # set back the initial configuration
+ self.h = h_copy
+
+ return loss
+
+ def energy_fluctuation(self, state):
+ """
+ Evaluate energy fluctuation.
+
+ .. math::
+ \\Xi(\\mu) = \\sqrt{\\langle\\mu|\\hat{H}^2|\\mu\\rangle - \\langle\\mu|\\hat{H}|\\mu\\rangle^2} \\,
+
+ for a given state :math:`|\\mu\\rangle`.
+
+ Args:
+ state (np.ndarray): quantum state to be used to compute the energy fluctuation with H.
+ """
+ return self.h.energy_fluctuation(state)
+
+ def sigma(self, h: np.array):
+ """Returns the off-diagonal restriction of matrix `h`."""
+ return self.backend.cast(h) - self.backend.cast(
+ np.diag(np.diag(self.backend.to_numpy(h)))
+ )
+
+ def generate_gamma_list(self, n: int, d: np.array):
+ r"""Computes the n-nested Gamma functions, where $\Gamma_k=[W,...,[W,[W,H]]...]$, where we take k nested commutators with $W = [D, H]$"""
+ W = self.commutator(self.backend.cast(d), self.sigma(self.h.matrix))
+ gamma_list = [self.h.matrix]
+ for _ in range(n - 1):
+ gamma_list.append(self.commutator(W, gamma_list[-1]))
+ return gamma_list
+
+ def cost_expansion(self, d, n):
+ d = self.backend.cast(d)
+
+ if self.cost is DoubleBracketCostFunction.off_diagonal_norm:
+ coef = off_diagonal_norm_polynomial_expansion_coef(self, d, n)
+ elif self.cost is DoubleBracketCostFunction.least_squares:
+ coef = least_squares_polynomial_expansion_coef(
+ self, d, n, backend=self.backend
+ )
+ elif self.cost is DoubleBracketCostFunction.energy_fluctuation:
+ coef = energy_fluctuation_polynomial_expansion_coef(
+ self, d, n, self.ref_state
+ )
+ else: # pragma: no cover
+ raise ValueError(f"Cost function {self.cost} not recognized.")
+ return coef
diff --git a/src/qibo/models/dbi/utils.py b/src/qibo/models/dbi/utils.py
new file mode 100644
index 000000000..4a04c7a4f
--- /dev/null
+++ b/src/qibo/models/dbi/utils.py
@@ -0,0 +1,286 @@
+import math
+from enum import Enum, auto
+from itertools import combinations, product
+
+import numpy as np
+
+from qibo import symbols
+from qibo.backends import _check_backend
+from qibo.hamiltonians import SymbolicHamiltonian
+
+
+def generate_Z_operators(nqubits: int, backend=None):
+ """Generate a dictionary containing 1) all possible products of Pauli Z operators for L = n_qubits and 2) their respective names.
+ Return: Dictionary with operator names (str) as keys and operators (np.array) as values
+
+ Example:
+ .. testcode::
+
+ from qibo.models.dbi.utils import generate_Z_operators
+ from qibo.models.dbi.double_bracket import DoubleBracketIteration
+ from qibo.quantum_info import random_hermitian
+ from qibo.hamiltonians import Hamiltonian
+ import numpy as np
+
+ nqubits = 4
+ h0 = random_hermitian(2**nqubits)
+ dbi = DoubleBracketIteration(Hamiltonian(nqubits=nqubits, matrix=h0))
+ generate_Z = generate_Z_operators(nqubits)
+ Z_ops = list(generate_Z.values())
+
+ delta_h0 = dbi.diagonal_h_matrix
+ dephasing_channel = (sum([Z_op @ h0 @ Z_op for Z_op in Z_ops])+h0)/2**nqubits
+ norm_diff = np.linalg.norm(delta_h0 - dephasing_channel)
+ """
+
+ backend = _check_backend(backend)
+ # list of tuples, e.g. ('Z','I','Z')
+ combination_strings = product("ZI", repeat=nqubits)
+ output_dict = {}
+
+ for zi_string_combination in combination_strings:
+ # except for the identity
+ if "Z" in zi_string_combination:
+ op_name = "".join(zi_string_combination)
+ tensor_op = str_to_symbolic(op_name)
+ # append in output_dict
+ output_dict[op_name] = SymbolicHamiltonian(
+ tensor_op, backend=backend
+ ).dense.matrix
+ return output_dict
+
+
+def str_to_symbolic(name: str):
+ """Convert string into symbolic hamiltonian.
+ Example:
+ .. testcode::
+
+ from qibo.models.dbi.utils import str_to_symbolic
+ op_name = "ZYXZI"
+ # returns 5-qubit symbolic hamiltonian
+ ZIXZI_op = str_to_symbolic(op_name)
+ """
+ tensor_op = 1
+ for qubit, char in enumerate(name):
+ tensor_op *= getattr(symbols, char)(qubit)
+ return tensor_op
+
+
+def cs_angle_sgn(dbi_object, d, backend=None):
+ """Calculates the sign of Cauchy-Schwarz Angle :math:`\\langle W(Z), W({\\rm canonical}) \\rangle_{\\rm HS}`."""
+ backend = _check_backend(backend)
+ d = backend.cast(d)
+ norm = backend.np.trace(
+ backend.np.matmul(
+ backend.np.conj(
+ dbi_object.commutator(dbi_object.diagonal_h_matrix, dbi_object.h.matrix)
+ ).T,
+ dbi_object.commutator(d, dbi_object.h.matrix),
+ )
+ )
+ return backend.np.real(backend.np.sign(norm))
+
+
+def decompose_into_pauli_basis(h_matrix: np.array, pauli_operators: list, backend=None):
+ """finds the decomposition of hamiltonian `h_matrix` into Pauli-Z operators"""
+ nqubits = int(np.log2(h_matrix.shape[0]))
+ backend = _check_backend(backend)
+ decomposition = []
+ for Z_i in pauli_operators:
+ expect = backend.np.trace(h_matrix @ Z_i) / 2**nqubits
+ decomposition.append(expect)
+ return decomposition
+
+
+def generate_pauli_index(nqubits, order):
+ """
+ Generate all possible combinations of qubits for a given order of Pauli operators.
+ """
+ if order == 1:
+ return list(range(nqubits))
+ else:
+ indices = list(range(nqubits))
+ return indices + [
+ comb for i in range(2, order + 1) for comb in combinations(indices, i)
+ ]
+
+
+def generate_pauli_operator_dict(
+ nqubits: int,
+ parameterization_order: int = 1,
+ symbols_pauli=symbols.Z,
+ backend=None,
+):
+ """Generates a dictionary containing Pauli `symbols_pauli` operators of locality `parameterization_order` for `nqubits` qubits.
+
+ Args:
+ nqubits (int): number of qubits in the system.
+ parameterization_order (int, optional): the locality of the operators generated. Defaults to 1.
+ symbols_pauli (qibo.symbols, optional): the symbol of the intended Pauli operator. Defaults to symbols.Z.
+
+ Returns:
+ pauli_operator_dict (dictionary): dictionary with structure {"operator_name": operator}
+
+ Example:
+ pauli_operator_dict = generate_pauli_operator_dict)
+ """
+ backend = _check_backend(backend)
+ pauli_index = generate_pauli_index(nqubits, order=parameterization_order)
+ pauli_operators = [
+ generate_pauli_operators(nqubits, symbols_pauli, index, backend=backend)
+ for index in pauli_index
+ ]
+ return {index: operator for index, operator in zip(pauli_index, pauli_operators)}
+
+
+def generate_pauli_operators(nqubits, symbols_pauli, positions, backend=None):
+ # generate matrix of an nqubit-pauli operator with `symbols_pauli` at `positions`
+ if isinstance(positions, int):
+ return SymbolicHamiltonian(
+ symbols_pauli(positions),
+ nqubits=nqubits,
+ backend=backend,
+ ).dense.matrix
+ else:
+ terms = [symbols_pauli(pos) for pos in positions]
+ return SymbolicHamiltonian(
+ math.prod(terms), nqubits=nqubits, backend=backend
+ ).dense.matrix
+
+
+class ParameterizationTypes(Enum):
+ """Define types of parameterization for diagonal operator."""
+
+ pauli = auto()
+ """Uses Pauli-Z operators (magnetic field)."""
+ computational = auto()
+ """Uses computational basis."""
+
+
+def params_to_diagonal_operator(
+ params: np.array,
+ nqubits: int,
+ parameterization: ParameterizationTypes = ParameterizationTypes.pauli,
+ pauli_parameterization_order: int = 1,
+ normalize: bool = False,
+ pauli_operator_dict: dict = None,
+ backend=None,
+):
+ r"""Creates the $D$ operator for the double-bracket iteration ansatz depending on the parameterization type."""
+ backend = _check_backend(backend)
+ if parameterization is ParameterizationTypes.pauli:
+ # raise error if dimension mismatch
+ d = sum(
+ [
+ backend.to_numpy(params[i])
+ * backend.to_numpy(list(pauli_operator_dict.values())[i])
+ for i in range(nqubits)
+ ]
+ )
+ elif parameterization is ParameterizationTypes.computational:
+ d = np.zeros((len(params), len(params)))
+ for i in range(len(params)):
+ d[i, i] = backend.to_numpy(params[i])
+
+ # TODO: write proper tests for normalize=True
+ if normalize: # pragma: no cover
+ d = d / np.linalg.norm(d)
+ return backend.cast(d)
+
+
+def off_diagonal_norm_polynomial_expansion_coef(dbi_object, d, n):
+ # generate Gamma's where $\Gamma_{k+1}=[W, \Gamma_{k}], $\Gamma_0=H
+ W = dbi_object.commutator(
+ dbi_object.backend.cast(d), dbi_object.sigma(dbi_object.h.matrix)
+ )
+ gamma_list = dbi_object.generate_gamma_list(n + 2, d)
+ sigma_gamma_list = list(map(dbi_object.sigma, gamma_list))
+ gamma_list_np = list(map(dbi_object.backend.to_numpy, sigma_gamma_list))
+ exp_list = np.array([1 / math.factorial(k) for k in range(n + 1)])
+ # coefficients for rotation with [W,H] and H
+ c1 = exp_list.reshape((-1, 1, 1)) * gamma_list_np[1:]
+ c2 = exp_list.reshape((-1, 1, 1)) * gamma_list_np[:-1]
+ # product coefficient
+ trace_coefficients = [0] * (2 * n + 1)
+ for k in range(n + 1):
+ for j in range(n + 1):
+ power = k + j
+ product_matrix = c1[k] @ c2[j]
+ trace_coefficients[power] += 2 * np.trace(product_matrix)
+ # coefficients from high to low (n:0)
+ coef = list(reversed(trace_coefficients[: n + 1]))
+ return coef
+
+
+def least_squares_polynomial_expansion_coef(dbi_object, d, n: int = 3, backend=None):
+ """Return the Taylor expansion coefficients of least square cost of `dbi_object.h` and diagonal operator `d` with respect to double bracket rotation duration `s`."""
+ # generate Gamma's where $\Gamma_{k+1}=[W, \Gamma_{k}], $\Gamma_0=H
+ backend = _check_backend(backend)
+ Gamma_list = dbi_object.generate_gamma_list(n + 1, d)
+ exp_list = np.array([1 / math.factorial(k) for k in range(n + 1)])
+ # coefficients
+ coef = np.empty(n)
+ for i in range(n):
+ coef[i] = backend.np.real(
+ exp_list[i]
+ * backend.np.trace(dbi_object.backend.cast(d) @ Gamma_list[i + 1])
+ )
+ coef = list(reversed(coef))
+ return coef
+
+
+def energy_fluctuation_polynomial_expansion_coef(
+ dbi_object, d: np.array, n: int = 3, state=0
+):
+ """Return the Taylor expansion coefficients of energy fluctuation of `dbi_object` with respect to double bracket rotation duration `s`."""
+ # generate Gamma's where $\Gamma_{k+1}=[W, \Gamma_{k}], $\Gamma_0=H
+ Gamma_list = dbi_object.generate_gamma_list(n + 1, d)
+ # coefficients
+ coef = np.empty(3)
+ state_cast = dbi_object.backend.cast(state)
+ state_dag = dbi_object.backend.cast(state.conj().T)
+
+ def variance(a):
+ """Calculates the variance of a matrix A with respect to a state:
+ Var($A$) = $\\langle\\mu|A^2|\\mu\rangle-\\langle\\mu|A|\\mu\rangle^2$"""
+ b = a @ a
+ return state_dag @ b @ state_cast - (state_dag @ a @ state_cast) ** 2
+
+ def covariance(a, b):
+ """This is a generalization of the notion of covariance, needed for the polynomial expansion of the energy fluctuation,
+ applied to two operators A and B with respect to a state:
+ Cov($A,B$) = $\\langle\\mu|AB|\\mu\rangle-\\langle\\mu|A|\\mu\rangle\\langle\\mu|B|\\mu\rangle$
+ """
+
+ c = a @ b + b @ a
+ return (
+ state_dag @ c @ state_cast
+ - 2 * state_dag @ a @ state_cast * state_dag @ b @ state_cast
+ )
+
+ coef[0] = np.real(2 * covariance(Gamma_list[0], Gamma_list[1]))
+ coef[1] = np.real(2 * variance(Gamma_list[1]))
+ coef[2] = np.real(
+ covariance(Gamma_list[0], Gamma_list[3])
+ + 3 * covariance(Gamma_list[1], Gamma_list[2])
+ )
+ coef = list(reversed(coef))
+ return coef
+
+
+def copy_dbi_object(dbi_object):
+ """
+ Return a copy of the DoubleBracketIteration object.
+ This is necessary for the `select_best_dbr_generator` function as pytorch do not support deepcopy for leaf tensors.
+ """
+ from copy import copy, deepcopy # pylint: disable=import-outside-toplevel
+
+ dbi_class = dbi_object.__class__
+ new = dbi_class.__new__(dbi_class)
+
+ # Manually copy h and h0 as they may be torch tensors
+ new.h, new.h0 = copy(dbi_object.h), copy(dbi_object.h0)
+ # Deepcopy the rest of the attributes
+ for attr in ("mode", "scheduling", "cost", "ref_state"):
+ setattr(new, attr, deepcopy(getattr(dbi_object, attr, None)))
+ return new
diff --git a/src/qibo/models/dbi/utils_dbr_strategies.py b/src/qibo/models/dbi/utils_dbr_strategies.py
new file mode 100644
index 000000000..fb71cdf49
--- /dev/null
+++ b/src/qibo/models/dbi/utils_dbr_strategies.py
@@ -0,0 +1,306 @@
+import hyperopt
+
+from qibo.backends import _check_backend
+from qibo.models.dbi.double_bracket import *
+from qibo.models.dbi.utils import *
+
+
+def select_best_dbr_generator(
+ dbi_object: DoubleBracketIteration,
+ d_list: list,
+ step: Optional[float] = None,
+ compare_canonical: bool = True,
+ scheduling: DoubleBracketScheduling = None,
+ **kwargs,
+):
+ """Selects the best double bracket rotation generator from a list and execute the rotation.
+
+ Args:
+ dbi_object (`DoubleBracketIteration`): the target DoubleBracketIteration object.
+ d_list (list): list of diagonal operators (np.array) to select from.
+ step (float): fixed iteration duration.
+ Defaults to ``None``, optimize with `scheduling` method and `choose_step` function.
+ compare_canonical (boolean): if `True`, the diagonalization effect with operators from `d_list` is compared with the canonical bracket.
+ scheduling (`DoubleBracketScheduling`): scheduling method for finding the optimal step.
+
+ Returns:
+ The updated dbi_object (`DoubleBracketIteration`), index of the optimal diagonal operator (int), respective step duration (float), and sign (int).
+
+ Example:
+ from qibo.hamiltonians import Hamiltonian
+ from qibo.models.dbi.double_bracket import *
+ from qibo.models.dbi.utils_dbr_strategies import select_best_dbr_generator
+ from qibo.quantum_info import random_hermitian
+
+ nqubits = 3
+ NSTEPS = 3
+ h0 = random_hermitian(2**nqubits)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ generate_local_Z = generate_Z_operators(nqubits)
+ Z_ops = list(generate_local_Z.values())
+ for _ in range(NSTEPS):
+ dbi, idx, step, flip_sign = select_best_dbr_generator(
+ dbi, Z_ops, compare_canonical=True
+ )
+ """
+ if scheduling is None:
+ scheduling = dbi_object.scheduling
+
+ if compare_canonical:
+ norms_off_diagonal_restriction = [dbi_object.off_diagonal_norm] * (
+ len(d_list) + 1
+ )
+ optimal_steps = np.zeros(len(d_list) + 1)
+ flip_list = np.ones(len(d_list) + 1)
+ else:
+ norms_off_diagonal_restriction = [dbi_object.off_diagonal_norm] * (len(d_list))
+ optimal_steps = np.zeros(len(d_list))
+ flip_list = np.ones(len(d_list))
+
+ for i, d in enumerate(d_list):
+ # prescribed step durations
+ dbi_eval = copy_dbi_object(dbi_object)
+ d = dbi_eval.backend.cast(d)
+ flip_list[i] = cs_angle_sgn(dbi_eval, d, backend=dbi_object.backend)
+ if flip_list[i] != 0:
+ if step is None:
+ step_best = dbi_eval.choose_step(
+ d=flip_list[i] * d, scheduling=scheduling, **kwargs
+ )
+ else:
+ step_best = step
+ dbi_eval(step=step_best, d=flip_list[i] * d)
+ optimal_steps[i] = step_best
+ norms_off_diagonal_restriction[i] = dbi_eval.off_diagonal_norm
+ # canonical
+ if compare_canonical is True:
+ dbi_eval = copy_dbi_object(dbi_object)
+ dbi_eval.mode = DoubleBracketGeneratorType.canonical
+ if step is None:
+ step_best = dbi_eval.choose_step(scheduling=scheduling, **kwargs)
+ else:
+ step_best = step
+ dbi_eval(step=step_best)
+ optimal_steps[-1] = step_best
+ norms_off_diagonal_restriction[-1] = dbi_eval.off_diagonal_norm
+ # find best d
+ idx_max_loss = np.argmin(norms_off_diagonal_restriction)
+ flip = flip_list[idx_max_loss]
+ step_optimal = optimal_steps[idx_max_loss]
+ dbi_eval = copy_dbi_object(dbi_object)
+ if idx_max_loss == len(d_list) and compare_canonical is True:
+ # canonical
+ dbi_eval(step=step_optimal, mode=DoubleBracketGeneratorType.canonical)
+
+ else:
+ d_optimal = flip * d_list[idx_max_loss]
+ dbi_eval(step=step_optimal, d=d_optimal)
+ return dbi_eval, idx_max_loss, step_optimal, flip
+
+
+def gradient_numerical(
+ dbi_object: DoubleBracketIteration,
+ d_params: list,
+ parameterization: ParameterizationTypes,
+ s: float = 1e-2,
+ delta: float = 1e-3,
+ backend=None,
+ **kwargs,
+):
+ r"""
+ Gradient of the DBI with respect to the parametrization of D. A simple finite difference is used to calculate the gradient.
+
+ Args:
+ dbi_object (DoubleBracketIteration): DoubleBracketIteration object.
+ d_params (np.array): Parameters for the ansatz (note that the dimension must be 2**nqubits for full ansazt and nqubits for Pauli ansatz).
+ s (float): A short flow duration for finding the numerical gradient.
+ delta (float): Step size for numerical gradient.
+ Returns:
+ grad (np.array): Gradient of the D operator.
+ """
+ backend = _check_backend(backend)
+ nqubits = dbi_object.nqubits
+ grad = np.zeros(len(d_params))
+ d = params_to_diagonal_operator(
+ d_params, nqubits, parameterization=parameterization, **kwargs, backend=backend
+ )
+ for i in range(len(d_params)):
+ params_new = backend.to_numpy(d_params).copy()
+ params_new[i] = params_new[i] + delta
+ d_new = params_to_diagonal_operator(
+ params_new,
+ nqubits,
+ parameterization=parameterization,
+ **kwargs,
+ backend=backend,
+ )
+ # find the increment of a very small step
+ grad[i] = (dbi_object.loss(s, d_new) - dbi_object.loss(s, d)) / delta
+ return grad
+
+
+def gradient_descent(
+ dbi_object: DoubleBracketIteration,
+ iterations: int,
+ d_params: list,
+ parameterization: ParameterizationTypes,
+ pauli_operator_dict: dict = None,
+ pauli_parameterization_order: int = 1,
+ normalize: bool = False,
+ lr_min: float = 1e-5,
+ lr_max: float = 1,
+ max_evals: int = 100,
+ space: callable = None,
+ optimizer: callable = hyperopt.tpe,
+ verbose: bool = False,
+ backend=None,
+):
+ r"""Numerical gradient descent method for variating diagonal operator in each double bracket rotation.
+
+ Args:
+ dbi_object (DoubleBracketIteration): the target double bracket object.
+ iterations (int): number of double bracket rotations.
+ d_params (list): the parameters for the initial diagonal operator.
+ parameterization (ParameterizationTypes): the parameterization method for diagonal operator.
+ Options include pauli and computational.
+ pauli_operator_dict (dictionary, optional): dictionary of "name": Pauli-operator for Pauli-based parameterization type.
+ Defaults to None.
+ pauli_parameterization_order (int, optional): the order of parameterization or locality in Pauli basis. Defaults to 1.
+ normalize (bool, optional): option to normalize the diagonal operator. Defaults to False.
+ lr_min (float, optional): the minimal gradient step. Defaults to 1e-5.
+ lr_max (float, optional): the maximal gradient step. Defaults to 1.
+ max_evals (int, optional): maximum number of evaluations for `lr` using `hyperopt`. Defaults to 100.
+ space (callable, optional): evalutation space for `hyperopt`. Defaults to None.
+ optimizer (callable, optional): optimizer option for `hyperopt`. Defaults to `hyperopt.tpe`.
+ verbose (bool, optional): option for printing `hyperopt` process. Defaults to False.
+
+ Returns:
+ loss_hist (list): list of history losses of `dbi_object` throughout the double bracket rotations.
+ d_params_hist (list): list of history of `d` parameters after gradient descent.
+ s_hist (list): list of history of optimal `s` found.
+ Example:
+ from qibo import set_backend
+ from qibo.hamiltonians import Hamiltonian
+ from qibo.models.dbi.double_bracket import *
+ from qibo.models.dbi.utils import *
+ from qibo.models.dbi.utils_dbr_strategies import gradient_descent
+ from qibo.quantum_info import random_hermitian
+
+ nqubits = 3
+ NSTEPS = 5
+ set_backend("numpy")
+ h0 = random_hermitian(2**nqubits)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ scheduling=DoubleBracketScheduling.hyperopt,
+ cost=DoubleBracketCostFunction.off_diagonal_norm,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ pauli_operator_dict = generate_pauli_operator_dict(
+ nqubits, parameterization_order=1
+ )
+ pauli_operators = list(pauli_operator_dict.values())
+ # let initial d be approximation of $\Delta(H)
+ d_coef_pauli = decompose_into_Pauli_basis(
+ dbi.diagonal_h_matrix, pauli_operators=pauli_operators
+ )
+ d_pauli = sum([d_coef_pauli[i] * pauli_operators[i] for i in range(nqubits)])
+ loss_hist_pauli, d_params_hist_pauli, s_hist_pauli = gradient_descent(
+ dbi,
+ NSTEPS,
+ d_coef_pauli,
+ ParameterizationTypes.pauli,
+ pauli_operator_dict=pauli_operator_dict,
+ )
+ """
+ backend = _check_backend(backend)
+
+ nqubits = dbi_object.nqubits
+ # TODO: write tests where this condition applies
+ if (
+ parameterization is ParameterizationTypes.pauli and pauli_operator_dict is None
+ ): # pragma: no cover
+ pauli_operator_dict = generate_pauli_operator_dict(
+ nqubits=nqubits, parameterization_order=pauli_parameterization_order
+ )
+ d = params_to_diagonal_operator(
+ d_params,
+ nqubits,
+ parameterization=parameterization,
+ pauli_operator_dict=pauli_operator_dict,
+ normalize=normalize,
+ backend=backend,
+ )
+ loss_hist = [dbi_object.loss(0.0, d=d)]
+ d_params_hist = [d_params]
+ s_hist = [0]
+ # TODO: write tests where this condition applies
+ if (
+ parameterization is ParameterizationTypes.pauli and pauli_operator_dict is None
+ ): # pragma: no cover
+ pauli_operator_dict = generate_pauli_operator_dict(
+ nqubits=nqubits,
+ parameterization_order=pauli_parameterization_order,
+ backend=backend,
+ )
+ # first step
+ s = dbi_object.choose_step(d=d)
+ dbi_object(step=s, d=d)
+ for _ in range(iterations):
+ grad = gradient_numerical(
+ dbi_object,
+ d_params,
+ parameterization,
+ pauli_operator_dict=pauli_operator_dict,
+ pauli_parameterization_order=pauli_parameterization_order,
+ normalize=normalize,
+ backend=backend,
+ )
+
+ # set up hyperopt to find optimal lr
+ def func_loss_to_lr(lr):
+ d_params_eval = [d_params[j] - grad[j] * lr for j in range(len(grad))]
+ d_eval = params_to_diagonal_operator(
+ d_params_eval,
+ nqubits,
+ parameterization=parameterization,
+ pauli_operator_dict=pauli_operator_dict,
+ normalize=normalize,
+ backend=backend,
+ )
+ return dbi_object.loss(step=s, d=d_eval)
+
+ if space is None:
+ space = hyperopt.hp.loguniform("lr", np.log(lr_min), np.log(lr_max))
+
+ best = hyperopt.fmin(
+ fn=func_loss_to_lr,
+ space=space,
+ algo=optimizer.suggest,
+ max_evals=max_evals,
+ verbose=verbose,
+ )
+ lr = best["lr"]
+
+ d_params = [d_params[j] - grad[j] * lr for j in range(len(grad))]
+ d = params_to_diagonal_operator(
+ d_params,
+ nqubits,
+ parameterization=parameterization,
+ pauli_operator_dict=pauli_operator_dict,
+ normalize=normalize,
+ backend=backend,
+ )
+ s = dbi_object.choose_step(d=d)
+ dbi_object(step=s, d=d)
+
+ # record history
+ loss_hist.append(dbi_object.loss(0.0, d=d))
+ d_params_hist.append(d_params)
+ s_hist.append(s)
+ return loss_hist, d_params_hist, s_hist
diff --git a/src/qibo/models/dbi/utils_scheduling.py b/src/qibo/models/dbi/utils_scheduling.py
new file mode 100644
index 000000000..130cd88f3
--- /dev/null
+++ b/src/qibo/models/dbi/utils_scheduling.py
@@ -0,0 +1,212 @@
+import math
+from functools import partial
+from typing import Optional
+
+import hyperopt
+import numpy as np
+
+error = 1e-3
+
+
+def grid_search_step(
+ dbi_object,
+ step_min: float = 1e-5,
+ step_max: float = 1,
+ num_evals: int = 100,
+ space: Optional[np.array] = None,
+ d: Optional[np.array] = None,
+):
+ """
+ Greedy optimization of the iteration step.
+
+ Args:
+ step_min: lower bound of the search grid;
+ step_max: upper bound of the search grid;
+ mnum_evals: number of iterations between step_min and step_max;
+ d: diagonal operator for generating double-bracket iterations.
+
+ Returns:
+ (float): optimized best iteration step (minimizing off-diagonal norm).
+ """
+ if space is None:
+ space = np.linspace(step_min, step_max, num_evals)
+
+ if d is None:
+ d = dbi_object.diagonal_h_matrix
+
+ loss_list = [dbi_object.loss(step, d=d) for step in space]
+
+ idx_max_loss = np.argmin(loss_list)
+ return space[idx_max_loss]
+
+
+def hyperopt_step(
+ dbi_object,
+ step_min: float = 1e-5,
+ step_max: float = 1,
+ max_evals: int = 100,
+ space: callable = None,
+ optimizer: callable = None,
+ look_ahead: int = 1,
+ d: Optional[np.array] = None,
+):
+ """
+ Optimize iteration step using hyperopt.
+
+ Args:
+ step_min: lower bound of the search grid;
+ step_max: upper bound of the search grid;
+ max_evals: maximum number of iterations done by the hyperoptimizer;
+ space: see hyperopt.hp possibilities;
+ optimizer: see hyperopt algorithms;
+ look_ahead: number of iteration steps to compute the loss function;
+ d: diagonal operator for generating double-bracket iterations.
+
+ Returns:
+ (float): optimized best iteration step (minimizing loss function).
+ """
+ if space is None:
+ space = hyperopt.hp.uniform
+ if optimizer is None:
+ optimizer = hyperopt.tpe
+ if d is None:
+ d = dbi_object.diagonal_h_matrix
+
+ space = space("step", step_min, step_max)
+
+ best = hyperopt.fmin(
+ fn=partial(dbi_object.loss, d=d, look_ahead=look_ahead),
+ space=space,
+ algo=optimizer.suggest,
+ max_evals=max_evals,
+ show_progressbar=False,
+ )
+ return best["step"]
+
+
+def polynomial_step(
+ dbi_object,
+ n: int = 2,
+ n_max: int = 5,
+ d: np.array = None,
+ coef: Optional[list] = None,
+ cost: Optional[str] = None,
+):
+ r"""
+ Optimizes iteration step by solving the n_th order polynomial expansion of the loss function.
+ e.g. $n=2$: $2\Trace(\sigma(\Gamma_1 + s\Gamma_2 + s^2/2\Gamma_3)\sigma(\Gamma_0 + s\Gamma_1 + s^2/2\Gamma_2))
+ Args:
+ n (int, optional): the order to which the loss function is expanded. Defaults to 4.
+ n_max (int, optional): maximum order allowed for recurring calls of `polynomial_step`. Defaults to 5.
+ d (np.array, optional): diagonal operator, default as $\delta(H)$.
+ backup_scheduling (`DoubleBracketScheduling`): the scheduling method to use in case no real positive roots are found.
+ """
+ if cost is None:
+ cost = dbi_object.cost
+
+ if d is None:
+ d = dbi_object.diagonal_h_matrix
+
+ if n > n_max:
+ raise ValueError(
+ "No solution can be found with polynomial approximation. Increase `n_max` or use other scheduling methods."
+ )
+ if coef is None:
+ coef = dbi_object.cost_expansion(d=d, n=n)
+ roots = np.roots(coef)
+ real_positive_roots = [
+ np.real(root) for root in roots if np.imag(root) < 1e-3 and np.real(root) > 0
+ ]
+ # solution exists, return minimum s
+ if len(real_positive_roots) > 0:
+ losses = [dbi_object.loss(step=root, d=d) for root in real_positive_roots]
+ return real_positive_roots[losses.index(min(losses))]
+ # solution does not exist, return None
+ else:
+ return None
+
+
+def simulated_annealing_step(
+ dbi_object,
+ d: Optional[np.array] = None,
+ initial_s=None,
+ step_min=1e-5,
+ step_max=1,
+ s_jump_range=None,
+ s_jump_range_divident=5,
+ initial_temp=1,
+ cooling_rate=0.85,
+ min_temp=1e-5,
+ max_iter=200,
+):
+ """
+ Perform a single step of simulated annealing optimization.
+
+ Parameters:
+ dbi_object: DBI object
+ The object representing the problem to be optimized.
+ d: Optional[np.array], optional
+ The diagonal matrix 'd' used in optimization. If None, it uses the diagonal
+ matrix 'diagonal_h_matrix' from dbi_object.
+ initial_s: float or None, optional
+ Initial value for 's', the step size. If None, it is initialized using
+ polynomial_step function with 'n=4'. If 'polynomial_step' returns None,
+ 'initial_s' is set to 'step_min'.
+ step_min: float, optional
+ Minimum value for the step size 's'.
+ step_max: float, optional
+ Maximum value for the step size 's'.
+ s_jump_range: float or None, optional
+ Range for the random jump in step size. If None, it's calculated based on
+ 'step_min', 'step_max', and 's_jump_range_divident'.
+ s_jump_range_divident: int, optional
+ Dividend to determine the range for random jump in step size.
+ initial_temp: float, optional
+ Initial temperature for simulated annealing.
+ cooling_rate: float, optional
+ Rate at which temperature decreases in simulated annealing.
+ min_temp: float, optional
+ Minimum temperature threshold for termination of simulated annealing.
+ max_iter: int, optional
+ Maximum number of iterations for simulated annealing.
+
+ Returns:
+ float:
+ The optimized step size 's'.
+ """
+
+ if d is None:
+ d = dbi_object.diagonal_h_matrix
+ if initial_s is None:
+ initial_s = polynomial_step(dbi_object=dbi_object, d=d, n=4)
+ # TODO: implement test to catch this if statement
+ if initial_s is None: # pragma: no cover
+ initial_s = step_min
+ if s_jump_range is None:
+ s_jump_range = (step_max - step_min) / s_jump_range_divident
+ current_s = initial_s
+ current_loss = dbi_object.loss(d=d, step=current_s)
+ temp = initial_temp
+
+ for _ in range(max_iter):
+ candidate_s = max(
+ step_min,
+ min(
+ current_s + np.random.uniform(-1 * s_jump_range, s_jump_range), step_max
+ ),
+ )
+ candidate_loss = dbi_object.loss(d=d, step=candidate_s)
+
+ # Calculate change in loss
+ delta_loss = candidate_loss - current_loss
+
+ # Determine if the candidate solution is an improvement
+ if delta_loss < 0 or np.random.rand() < math.exp(-delta_loss / temp):
+ current_s = candidate_s
+ current_loss = candidate_loss
+ # Cool down
+ temp *= cooling_rate
+ if temp < min_temp or current_s > step_max or current_s < step_min:
+ break
+
+ return current_s
diff --git a/src/qibo/models/distcircuit.py b/src/qibo/models/distcircuit.py
new file mode 100644
index 000000000..49fe2f785
--- /dev/null
+++ b/src/qibo/models/distcircuit.py
@@ -0,0 +1,330 @@
+import copy
+from typing import Dict, List, Sequence, Tuple
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.gates.abstract import Gate, ParametrizedGate, SpecialGate
+from qibo.models.circuit import Circuit
+
+
+class DistributedQubits:
+ """Data structure that holds lists related to global qubit IDs.
+
+ Holds the following data:
+ * ``list``: Sorted list with the ids of global qubits.
+ * ``set``: Same as ``list`` but in a set to allow O(1) search.
+ * ``local``: Sorted list with the ids of local qubits.
+ * ``reduced_global``: Map from global qubit ids to their reduced value.
+ The reduced value is the effective id in a hypothetical circuit
+ that does not contain the local qubits.
+ * ``reduced_local``: Map from local qubit ids to their reduced value.
+ * ``transpose_order``: Order of indices used to split a full state vector
+ to state pieces.
+ * ``reverse_tranpose_order``: Order of indices used to merge state pieces
+ to a full state vector.
+ """
+
+ def __init__(self, qubits: Sequence[int], nqubits: int):
+ self.set = set(qubits)
+ self.list = sorted(qubits)
+ self.local = [q for q in range(nqubits) if q not in self.set]
+ self.reduced_global = {q: self.list.index(q) for q in self.list}
+ self.reduced_local = {q: q - self.reduction_number(q) for q in self.local}
+
+ self.transpose_order = self.list + self.local
+ self.reverse_transpose_order = nqubits * [0]
+ for i, v in enumerate(self.transpose_order):
+ self.reverse_transpose_order[v] = i
+
+ def reduction_number(self, q: int) -> int:
+ """Calculates the effective id in a circuit without the global qubits."""
+ for i, gq in enumerate(self.list):
+ if gq > q:
+ return i
+ return i + 1
+
+
+class DistributedQueues:
+ """Data structure that holds gate queues for each accelerator device.
+
+ For a distributed simulation we have to swap global qubits multiple times.
+ For each global qubit configuration a several gates can be applied to the
+ state forming a gate group. Once all gates in the group are applied the
+ global qubits are swapped and we proceed to the next gate group.
+
+ Holds the following data (in addition to ``DistributedBase``):
+ * ``device_to_ids``: Dictionary that maps device (str) to list of piece indices.
+ When a device is used multiple times then it is responsible for updating
+ multiple state pieces. The list of indices specifies which pieces the device
+ will update.
+ * ``ids_to_device``: Inverse dictionary of ``device_to_ids``.
+ * ``queues``: Nested list of shape ``(ngroups, ndevices, group size)``.
+ For example ``queues[2][1]`` gives the gate queue of the second gate
+ group to be run in the first device.
+ If ``gate[i]`` is an empty list it means that this the i-th group
+ consists of a special gate to be run on CPU.
+ * ``special_queue``: List with special gates than run on the full state vector
+ on CPU. Special gates have no target qubits and can be
+ ``CallbackGate``, ``Flatten`` or SWAPs between local and global qubits.
+ """
+
+ def __init__(self, circuit):
+ self.circuit = circuit
+ self.queues = []
+ self.special_queue = []
+ self.qubits = None
+
+ # List that holds the global-local SWAP pairs so that we can reset them
+ # in the end
+ self.swaps_list = []
+
+ self.device_to_ids = {d: v for d, v in self._ids(circuit.accelerators)}
+ self.ids_to_device = self.ndevices * [None]
+ for device, ids in self.device_to_ids.items():
+ for i in ids:
+ self.ids_to_device[i] = device
+
+ @property
+ def nqubits(self):
+ return self.circuit.nqubits
+
+ @property
+ def nglobal(self):
+ return self.circuit.nglobal
+
+ @property
+ def nlocal(self):
+ return self.circuit.nlocal
+
+ @property
+ def ndevices(self):
+ return self.circuit.ndevices
+
+ def set(self, queue: List[Gate]):
+ """Prepares gates for device-specific gate execution.
+
+ Each gate has to be recreated in the device that will be executed to
+ allow parallel execution. This method creates the gate groups that
+ contain these device gates.
+ A gate group is identified by looping through the circuit's gate queue
+ and adding gates in the group until the number of global becomes ``nglobal``.
+ Once this happens no more gates can be added in the group. In order to
+ apply new gates some global qubits have to be swapped to global and a
+ new gate group will be defined for the new global qubit configuration.
+
+ This method also creates the ``DistributedQubits`` object holding the
+ global qubits list.
+ """
+ queue = [
+ gate for gate in queue if not isinstance(gate, gates.M) or gate.collapse
+ ]
+ counter = self.count(queue, self.nqubits)
+ if self.qubits is None:
+ self.qubits = DistributedQubits(
+ counter.argsort()[: self.nglobal], self.nqubits
+ )
+ if queue:
+ transformed_queue = self.transform(queue, counter)
+ self.create(transformed_queue)
+
+ def _ids(self, accelerators: Dict[str, int]) -> Tuple[str, List[int]]:
+ """Generator of device piece indices."""
+ start = 0
+ for device, n in accelerators.items():
+ stop = start + n
+ yield device, list(range(start, stop))
+ start = stop
+
+ def _create_device_gate(self, gate: Gate) -> Gate:
+ """Creates a copy of a gate for specific device application.
+
+ Target and control qubits are modified according to the local qubits of
+ the circuit when this gate will be applied.
+
+ Args:
+ gate: The :class:`qibo.gates.abstract.Gate` object of the gate to copy.
+
+ Returns:
+ A :class:`qibo.gates.abstract.Gate` object with the proper target and
+ control qubit indices for device-specific application.
+ """
+ devgate = copy.copy(gate)
+ # Recompute the target/control indices considering only local qubits.
+ new_target_qubits = tuple(
+ q - self.qubits.reduction_number(q) for q in devgate.target_qubits
+ )
+ new_control_qubits = tuple(
+ q - self.qubits.reduction_number(q)
+ for q in devgate.control_qubits
+ if q not in self.qubits.set
+ )
+ devgate._set_targets_and_controls(new_target_qubits, new_control_qubits)
+ devgate.original_gate = gate
+ devgate.device_gates = set()
+ return devgate
+
+ @staticmethod
+ def count(queue: List[Gate], nqubits: int):
+ """Counts how many gates target each qubit.
+
+ Args:
+ queue: List of gates.
+ nqubits: Number of total qubits in the circuit.
+
+ Returns:
+ Array of integers with shape (nqubits,) with the number of gates
+ for each qubit id.
+ """
+ import numpy as np
+
+ counter = np.zeros(nqubits, dtype=np.int64)
+ for gate in queue:
+ for qubit in gate.target_qubits:
+ counter[qubit] += 1
+ return counter
+
+ def _transform(
+ self, queue: List[Gate], remaining_queue: List[Gate], counter
+ ) -> List[Gate]:
+ """Helper recursive method for ``transform``."""
+ new_remaining_queue = []
+ for gate in remaining_queue:
+ if isinstance(gate, (SpecialGate, gates.M)): # pragma: no cover
+ gate.swap_reset = list(self.swaps_list)
+
+ global_targets = set(gate.target_qubits) & self.qubits.set
+ accept = isinstance(gate, gates.SWAP) and len(global_targets) == 1
+ accept = accept or not global_targets
+ for skipped_gate in new_remaining_queue:
+ accept = accept and skipped_gate.commutes(gate)
+ if not accept:
+ break
+ if accept:
+ queue.append(gate)
+ for q in gate.target_qubits:
+ counter[q] -= 1
+ else:
+ new_remaining_queue.append(gate)
+
+ if not new_remaining_queue:
+ return queue
+
+ # Find which qubits to swap
+ gate = new_remaining_queue[0]
+ target_set = set(gate.target_qubits)
+ global_targets = target_set & self.qubits.set
+ if isinstance(gate, gates.SWAP): # pragma: no cover
+ # special case of swap on two global qubits
+ assert len(global_targets) == 2
+ global_targets.remove(target_set.pop())
+
+ available_swaps = (
+ q for q in counter.argsort() if q not in self.qubits.set | target_set
+ )
+ qubit_map = {}
+ for q in global_targets:
+ qs = next(available_swaps)
+ # Update qubit map that holds the swaps
+ qubit_map[q] = qs
+ qubit_map[qs] = q
+ # Keep SWAPs in memory to reset them in the end
+ self.swaps_list.append((min(q, qs), max(q, qs)))
+ # Add ``SWAP`` gate in ``queue``.
+ queue.append(gates.SWAP(q, qs))
+ # Modify ``counter`` to take into account the swaps
+ counter[q], counter[qs] = counter[qs], counter[q]
+
+ # Modify gates to take into account the swaps
+ for gate in new_remaining_queue:
+ new_target_qubits = tuple(
+ qubit_map[q] if q in qubit_map else q for q in gate.target_qubits
+ )
+ new_control_qubits = tuple(
+ qubit_map[q] if q in qubit_map else q for q in gate.control_qubits
+ )
+ gate._set_targets_and_controls(new_target_qubits, new_control_qubits)
+
+ return self._transform(queue, new_remaining_queue, counter)
+
+ def transform(self, queue, counter=None):
+ """Transforms gate queue to be compatible with distributed simulation.
+
+ Adds SWAP gates between global and local qubits so that no gates are
+ applied to global qubits.
+
+ Args:
+ queue (list): Original gate queue.
+ counter (np.ndarray): Counter of how many gates target each qubit.
+ If ``None`` this is calculated using the ``count`` method.
+
+ Returns:
+ List of gates that have the same effect as the original queue but
+ are compatible with distributed run (do not have global qubits as
+ targets).
+ """
+ if counter is None:
+ counter = self.count(queue, self.nqubits)
+ new_queue = self._transform([], queue, counter)
+ new_queue.extend(gates.SWAP(*p) for p in reversed(self.swaps_list))
+ return new_queue
+
+ def create(self, queue: List[Gate]):
+ """Creates the queues for each accelerator device.
+
+ Args:
+ queue (list): List of gates compatible with distributed run.
+ If the original ``queue`` contains gates that target global qubits
+ then ``transform` should be used to obtain a compatible queue.
+ """
+ for gate in queue:
+ is_collapse = isinstance(gate, gates.M) and gate.collapse
+ if not gate.target_qubits or is_collapse: # pragma: no cover
+ # special gate
+ gate.nqubits = self.nqubits
+ self.special_queue.append(gate)
+ self.queues.append([])
+
+ elif set(gate.target_qubits) & self.qubits.set: # global swap gate
+ global_qubits = set(gate.target_qubits) & self.qubits.set
+ if not isinstance(gate, gates.SWAP):
+ raise_error(
+ ValueError,
+ "Only SWAP gates are supported for " "global qubits.",
+ )
+ if len(global_qubits) > 1:
+ raise_error(
+ ValueError, "SWAPs between global qubits are not allowed."
+ )
+
+ global_qubit = global_qubits.pop()
+ local_qubit = gate.target_qubits[0]
+ if local_qubit == global_qubit:
+ local_qubit = gate.target_qubits[1]
+
+ self.special_queue.append((global_qubit, local_qubit))
+ self.queues.append([])
+
+ else:
+ if not self.queues or not self.queues[-1]:
+ self.queues.append([[] for _ in range(self.ndevices)])
+
+ for device, ids in self.device_to_ids.items():
+ devgate = self._create_device_gate(gate)
+ # Gate matrix should be constructed in the calculation
+ # device otherwise device parallelization will break
+ devgate.device = device
+ devgate.nqubits = self.nlocal
+ for i in ids:
+ flag = True
+ # If there are control qubits that are global then
+ # the gate should not be applied by all devices
+ for control in set(gate.control_qubits) & self.qubits.set:
+ ic = self.qubits.list.index(control)
+ ic = self.nglobal - ic - 1
+ flag = bool((i // (2**ic)) % 2)
+ if not flag:
+ break
+ if flag:
+ self.queues[-1][i].append(devgate)
+ if isinstance(gate, ParametrizedGate):
+ gate.device_gates.add(devgate)
diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py
new file mode 100644
index 000000000..3dd0b88dd
--- /dev/null
+++ b/src/qibo/models/encodings.py
@@ -0,0 +1,472 @@
+"""Module with functions that encode classical data into quantum circuits."""
+
+import math
+from inspect import signature
+from typing import Optional, Union
+
+import numpy as np
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.gates.gates import _check_engine
+from qibo.models.circuit import Circuit
+
+
+def comp_basis_encoder(
+ basis_element: Union[int, str, list, tuple], nqubits: Optional[int] = None, **kwargs
+):
+ """Creates circuit that performs encoding of bitstrings into computational basis states.
+
+ Args:
+ basis_element (int or str or list or tuple): bitstring to be encoded.
+ If ``int``, ``nqubits`` must be specified.
+ If ``str``, must be composed of only :math:`0`s and :math:`1`s.
+ If ``list`` or ``tuple``, must be composed of :math:`0`s and
+ :math:`1`s as ``int`` or ``str``.
+ nqubits (int, optional): total number of qubits in the circuit.
+ If ``basis_element`` is ``int``, ``nqubits`` must be specified.
+ If ``nqubits`` is ``None``, ``nqubits`` defaults to length of ``basis_element``.
+ Defaults to ``None``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: circuit encoding computational basis element.
+ """
+ if not isinstance(basis_element, (int, str, list, tuple)):
+ raise_error(
+ TypeError,
+ "basis_element must be either type int or str or list or tuple, "
+ + f"but it is type {type(basis_element)}.",
+ )
+
+ if isinstance(basis_element, (str, list, tuple)):
+ if any(elem not in ["0", "1", 0, 1] for elem in basis_element):
+ raise_error(ValueError, "all elements must be 0 or 1.")
+
+ if nqubits is not None and not isinstance(nqubits, int):
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+
+ if nqubits is None:
+ if isinstance(basis_element, int):
+ raise_error(
+ ValueError, f"nqubits must be specified when basis_element is type int."
+ )
+ else:
+ nqubits = len(basis_element)
+
+ if isinstance(basis_element, int):
+ basis_element = f"{basis_element:0{nqubits}b}"
+
+ if isinstance(basis_element, (str, tuple)):
+ basis_element = list(basis_element)
+
+ basis_element = list(map(int, basis_element))
+
+ circuit = Circuit(nqubits, **kwargs)
+ for qubit, elem in enumerate(basis_element):
+ if elem == 1:
+ circuit.add(gates.X(qubit))
+
+ return circuit
+
+
+def phase_encoder(data, rotation: str = "RY", **kwargs):
+ """Creates circuit that performs the phase encoding of ``data``.
+
+ Args:
+ data (ndarray or list): :math:`1`-dimensional array of phases to be loaded.
+ rotation (str, optional): If ``"RX"``, uses :class:`qibo.gates.gates.RX` as rotation.
+ If ``"RY"``, uses :class:`qibo.gates.gates.RY` as rotation.
+ If ``"RZ"``, uses :class:`qibo.gates.gates.RZ` as rotation.
+ Defaults to ``"RY"``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in phase encoding.
+ """
+ if isinstance(data, list):
+ data = np.array(data)
+
+ if len(data.shape) != 1:
+ raise_error(
+ TypeError,
+ f"``data`` must be a 1-dimensional array, but it has dimensions {data.shape}.",
+ )
+
+ if not isinstance(rotation, str):
+ raise_error(
+ TypeError,
+ f"``rotation`` must be type str, but it is type {type(rotation)}.",
+ )
+
+ if rotation not in ["RX", "RY", "RZ"]:
+ raise_error(ValueError, f"``rotation`` {rotation} not found.")
+
+ nqubits = len(data)
+ gate = getattr(gates, rotation.upper())
+
+ circuit = Circuit(nqubits, **kwargs)
+ circuit.add(gate(qubit, 0.0) for qubit in range(nqubits))
+ circuit.set_parameters(data)
+
+ return circuit
+
+
+def unary_encoder(data, architecture: str = "tree", **kwargs):
+ """Creates circuit that performs the (deterministic) unary encoding of ``data``.
+
+ Args:
+ data (ndarray): :math:`1`-dimensional array of data to be loaded.
+ architecture(str, optional): circuit architecture used for the unary loader.
+ If ``diagonal``, uses a ladder-like structure.
+ If ``tree``, uses a binary-tree-based structure.
+ Defaults to ``tree``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in unary representation.
+ """
+ if isinstance(data, list):
+ data = np.array(data)
+
+ if len(data.shape) != 1:
+ raise_error(
+ TypeError,
+ f"``data`` must be a 1-dimensional array, but it has dimensions {data.shape}.",
+ )
+
+ if not isinstance(architecture, str):
+ raise_error(
+ TypeError,
+ f"``architecture`` must be type str, but it is type {type(architecture)}.",
+ )
+
+ if architecture not in ["diagonal", "tree"]:
+ raise_error(ValueError, f"``architecture`` {architecture} not found.")
+
+ if architecture == "tree" and not math.log2(data.shape[0]).is_integer():
+ raise_error(
+ ValueError,
+ "When ``architecture = 'tree'``, len(data) must be a power of 2. "
+ + f"However, it is {len(data)}.",
+ )
+
+ nqubits = len(data)
+
+ circuit = Circuit(nqubits, **kwargs)
+ circuit.add(gates.X(nqubits - 1))
+ circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture, **kwargs)
+ circuit += circuit_rbs
+
+ # calculating phases and setting circuit parameters
+ phases = _generate_rbs_angles(data, nqubits, architecture)
+ circuit.set_parameters(phases)
+
+ return circuit
+
+
+def unary_encoder_random_gaussian(
+ nqubits: int, architecture: str = "tree", seed=None, **kwargs
+):
+ """Creates a circuit that performs the unary encoding of a random Gaussian state.
+
+ At depth :math:`h` of the tree architecture, the angles :math:`\\theta_{k} \\in [0, 2\\pi]` of the the
+ gates :math:`RBS(\\theta_{k})` are sampled from the following probability density function:
+
+ .. math::
+ p_{h}(\\theta) = \\frac{1}{2} \\, \\frac{\\Gamma(2^{h-1})}{\\Gamma^{2}(2^{h-2})} \\,
+ \\left|\\sin(\\theta) \\, \\cos(\\theta)\\right|^{2^{h-1} - 1} \\, ,
+
+ where :math:`\\Gamma(\\cdot)` is the
+ `Gamma function `_.
+
+ Args:
+ nqubits (int): number of qubits.
+ architecture(str, optional): circuit architecture used for the unary loader.
+ If ``tree``, uses a binary-tree-based structure.
+ Defaults to ``tree``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: circuit that loads a random Gaussian array in unary representation.
+
+ References:
+ 1. A. Bouland, A. Dandapani, and A. Prakash, *A quantum spectral method for simulating
+ stochastic processes, with applications to Monte Carlo*.
+ `arXiv:2303.06719v1 [quant-ph] `_
+ """
+ if not isinstance(nqubits, int):
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+
+ if nqubits <= 0.0:
+ raise_error(
+ ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
+ )
+
+ if not isinstance(architecture, str):
+ raise_error(
+ TypeError,
+ f"``architecture`` must be type str, but it is type {type(architecture)}.",
+ )
+
+ if architecture != "tree":
+ raise_error(
+ NotImplementedError,
+ f"Currently, this function only accepts ``architecture=='tree'``.",
+ )
+
+ if not math.log2(nqubits).is_integer():
+ raise_error(ValueError, f"nqubits must be a power of 2, but it is {nqubits}.")
+
+ if (
+ seed is not None
+ and not isinstance(seed, int)
+ and not isinstance(seed, np.random.Generator)
+ ):
+ raise_error(
+ TypeError, "seed must be either type int or numpy.random.Generator."
+ )
+
+ from qibo.quantum_info.random_ensembles import ( # pylint: disable=C0415
+ _ProbabilityDistributionGaussianLoader,
+ )
+
+ local_state = (
+ np.random.default_rng(seed) if seed is None or isinstance(seed, int) else seed
+ )
+
+ sampler = _ProbabilityDistributionGaussianLoader(
+ a=0, b=2 * math.pi, seed=local_state
+ )
+
+ circuit = Circuit(nqubits, **kwargs)
+ circuit.add(gates.X(nqubits - 1))
+ circuit_rbs, pairs_rbs = _generate_rbs_pairs(nqubits, architecture, **kwargs)
+ circuit += circuit_rbs
+
+ phases = []
+ for depth, row in enumerate(pairs_rbs, 1):
+ phases.extend(sampler.rvs(depth=depth, size=len(row)))
+
+ circuit.set_parameters(phases)
+
+ return circuit
+
+
+def entangling_layer(
+ nqubits: int,
+ architecture: str = "diagonal",
+ entangling_gate: Union[str, gates.Gate] = "CNOT",
+ closed_boundary: bool = False,
+ **kwargs,
+):
+ """Creates a layer of two-qubit, entangling gates.
+
+ If the chosen gate is a parametrized gate, all phases are set to :math:`0.0`.
+
+ Args:
+ nqubits (int): Total number of qubits in the circuit.
+ architecture (str, optional): Architecture of the entangling layer.
+ Options are ``diagonal``, ``shifted``, ``even-layer``, and ``odd-layer``.
+ Defaults to ``"diagonal"``.
+ entangling_gate (str or :class:`qibo.gates.Gate`, optional): Two-qubit gate to be used
+ in the entangling layer. If ``entangling_gate`` is a parametrized gate,
+ all phases are initialized as :math:`0.0`. Defaults to ``"CNOT"``.
+ closed_boundary (bool, optional): If ``True`` adds a closed-boundary condition
+ to the entangling layer. Defaults to ``False``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: Circuit containing layer of two-qubit gates.
+ """
+
+ if not isinstance(nqubits, int):
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+
+ if nqubits <= 0.0:
+ raise_error(
+ ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
+ )
+
+ if not isinstance(architecture, str):
+ raise_error(
+ TypeError,
+ f"``architecture`` must be type str, but it is type {type(architecture)}.",
+ )
+
+ if architecture not in ["diagonal", "shifted", "even-layer", "odd-layer"]:
+ raise_error(
+ NotImplementedError,
+ f"``architecture`` {architecture} not found.",
+ )
+
+ if not isinstance(closed_boundary, bool):
+ raise_error(
+ TypeError,
+ f"closed_boundary must be type bool, but it is type {type(closed_boundary)}.",
+ )
+
+ gate = (
+ getattr(gates, entangling_gate)
+ if isinstance(entangling_gate, str)
+ else entangling_gate
+ )
+
+ if gate.__name__ == "GeneralizedfSim":
+ raise_error(
+ NotImplementedError,
+ "This function does not support the ``GeneralizedfSim`` gate.",
+ )
+
+ # Finds the number of correct number of parameters to initialize the gate class.
+ parameters = list(signature(gate).parameters)
+
+ if "q2" in parameters:
+ raise_error(
+ NotImplementedError, f"This function does not accept three-qubit gates."
+ )
+
+ # If gate is parametrized, sets all angles to 0.0
+ parameters = (0.0,) * (len(parameters) - 3) if len(parameters) > 2 else None
+
+ circuit = Circuit(nqubits, **kwargs)
+
+ if architecture == "diagonal":
+ circuit.add(
+ _parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
+ for qubit in range(nqubits - 1)
+ )
+ elif architecture == "even-layer":
+ circuit.add(
+ _parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
+ for qubit in range(0, nqubits - 1, 2)
+ )
+ elif architecture == "odd-layer":
+ circuit.add(
+ _parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
+ for qubit in range(1, nqubits - 1, 2)
+ )
+ else:
+ circuit.add(
+ _parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
+ for qubit in range(0, nqubits - 1, 2)
+ )
+ circuit.add(
+ _parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
+ for qubit in range(1, nqubits - 1, 2)
+ )
+
+ if closed_boundary:
+ circuit.add(_parametrized_two_qubit_gate(gate, nqubits - 1, 0, parameters))
+
+ return circuit
+
+
+def _generate_rbs_pairs(nqubits: int, architecture: str, **kwargs):
+ """Generating list of indexes representing the RBS connections
+
+ Creates circuit with all RBS initialised with 0.0 phase.
+
+ Args:
+ nqubits (int): number of qubits.
+ architecture(str, optional): circuit architecture used for the unary loader.
+ If ``diagonal``, uses a ladder-like structure.
+ If ``tree``, uses a binary-tree-based structure.
+ Defaults to ``tree``.
+ kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
+ For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`, list): circuit composed of :class:`qibo.gates.gates.RBS`
+ and list of indexes of target qubits per depth.
+ """
+
+ if architecture == "diagonal":
+ pairs_rbs = np.arange(nqubits)
+ pairs_rbs = [[pair] for pair in zip(pairs_rbs[:-1], pairs_rbs[1:])]
+
+ if architecture == "tree":
+ pairs_rbs = [[(0, int(nqubits / 2))]]
+ indexes = list(pairs_rbs[0][0])
+ for depth in range(2, int(math.log2(nqubits)) + 1):
+ pairs_rbs_per_depth = [
+ [(index, index + int(nqubits / 2**depth)) for index in indexes]
+ ]
+ pairs_rbs += pairs_rbs_per_depth
+ indexes = list(np.array(pairs_rbs_per_depth).flatten())
+
+ pairs_rbs = [
+ [(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs
+ ]
+
+ circuit = Circuit(nqubits, **kwargs)
+ for row in pairs_rbs:
+ for pair in row:
+ circuit.add(gates.RBS(*pair, 0.0, trainable=True))
+
+ return circuit, pairs_rbs
+
+
+def _generate_rbs_angles(data, nqubits: int, architecture: str):
+ """Generating list of angles for RBS gates based on ``architecture``.
+
+ Args:
+ data (ndarray, optional): :math:`1`-dimensional array of data to be loaded.
+ nqubits (int): number of qubits.
+ architecture(str, optional): circuit architecture used for the unary loader.
+ If ``diagonal``, uses a ladder-like structure.
+ If ``tree``, uses a binary-tree-based structure.
+ Defaults to ``tree``.
+
+ Returns:
+ list: list of phases for RBS gates.
+ """
+ if architecture == "diagonal":
+ engine = _check_engine(data)
+ phases = [
+ math.atan2(engine.linalg.norm(data[k + 1 :]), data[k])
+ for k in range(len(data) - 2)
+ ]
+ phases.append(math.atan2(data[-1], data[-2]))
+
+ if architecture == "tree":
+ j_max = int(nqubits / 2)
+
+ r_array = np.zeros(nqubits - 1, dtype=float)
+ phases = np.zeros(nqubits - 1, dtype=float)
+ for j in range(1, j_max + 1):
+ r_array[j_max + j - 2] = math.sqrt(
+ data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2
+ )
+ theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2])
+ if data[2 * j - 1] < 0.0:
+ theta = 2 * math.pi - theta
+ phases[j_max + j - 2] = theta
+
+ for j in range(j_max - 1, 0, -1):
+ r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2)
+ phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1])
+
+ return phases
+
+
+def _parametrized_two_qubit_gate(gate, q0, q1, params=None):
+ """Returns two-qubit gate initialized with or without phases."""
+ if params is not None:
+ return gate(q0, q1, *params)
+
+ return gate(q0, q1)
diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py
new file mode 100644
index 000000000..dd1f92750
--- /dev/null
+++ b/src/qibo/models/error_mitigation.py
@@ -0,0 +1,1077 @@
+"""Error Mitigation Methods."""
+
+import math
+from itertools import product
+
+import numpy as np
+from scipy.optimize import curve_fit
+
+from qibo import gates
+from qibo.backends import GlobalBackend, _check_backend, _check_backend_and_local_state
+from qibo.config import raise_error
+
+
+def get_gammas(noise_levels, analytical: bool = True):
+ """Standalone function to compute the ZNE coefficients given the noise levels.
+
+ Args:
+ noise_levels (numpy.ndarray): array containing the different noise levels.
+ Note that in the CNOT insertion paradigm this corresponds to
+ the number of CNOT pairs to be inserted. The canonical ZNE
+ noise levels are obtained as ``2 * c + 1``.
+ analytical (bool, optional): if ``True``, computes the coeffients by solving the
+ linear system. If ``False``, use the analytical solution valid
+ for the CNOT insertion method. Default is ``True``.
+
+ Returns:
+ numpy.ndarray: the computed coefficients.
+ """
+ if analytical:
+ noise_levels = 2 * noise_levels + 1
+ a_matrix = np.array([noise_levels**i for i in range(len(noise_levels))])
+ b_vector = np.zeros(len(noise_levels))
+ b_vector[0] = 1
+ zne_coefficients = np.linalg.solve(a_matrix, b_vector)
+ else:
+ max_noise_level = noise_levels[-1]
+ zne_coefficients = np.array(
+ [
+ 1
+ / (2 ** (2 * max_noise_level) * math.factorial(i))
+ * (-1) ** i
+ / (1 + 2 * i)
+ * math.factorial(1 + 2 * max_noise_level)
+ / (
+ math.factorial(max_noise_level)
+ * math.factorial(max_noise_level - i)
+ )
+ for i in noise_levels
+ ]
+ )
+
+ return zne_coefficients
+
+
+def get_noisy_circuit(circuit, num_insertions: int, insertion_gate: str = "CNOT"):
+ """Standalone function to generate the noisy circuit with the inverse gate pairs insertions.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to modify.
+ num_insertions (int): number of insertion gate pairs to add.
+ insertion_gate (str, optional): gate to be used in the insertion.
+ If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``.
+ Default is ``"CNOT"``.
+
+ Returns:
+ :class:`qibo.models.Circuit`: circuit with the inserted gate pairs.
+ """
+ if insertion_gate not in ("CNOT", "RX"): # pragma: no cover
+ raise_error(
+ ValueError,
+ "Invalid insertion gate specification. Please select between 'CNOT' and 'RX'.",
+ )
+ if insertion_gate == "CNOT" and circuit.nqubits < 2: # pragma: no cover
+ raise_error(
+ ValueError,
+ "Provide a circuit with at least 2 qubits when using the 'CNOT' insertion gate. "
+ + "Alternatively, try with the 'RX' insertion gate instead.",
+ )
+
+ i_gate = gates.CNOT if insertion_gate == "CNOT" else gates.RX
+
+ theta = np.pi / 2
+ noisy_circuit = circuit.__class__(**circuit.init_kwargs)
+
+ for gate in circuit.queue:
+ noisy_circuit.add(gate)
+
+ if isinstance(gate, i_gate):
+ if insertion_gate == "CNOT":
+ control = gate.control_qubits[0]
+ target = gate.target_qubits[0]
+ for _ in range(num_insertions):
+ noisy_circuit.add(gates.CNOT(control, target))
+ noisy_circuit.add(gates.CNOT(control, target))
+ elif gate.init_kwargs["theta"] == theta:
+ qubit = gate.qubits[0]
+ for _ in range(num_insertions):
+ noisy_circuit.add(gates.RX(qubit, theta=theta))
+ noisy_circuit.add(gates.RX(qubit, theta=-theta))
+
+ return noisy_circuit
+
+
+def ZNE(
+ circuit,
+ observable,
+ noise_levels,
+ noise_model=None,
+ nshots=10000,
+ solve_for_gammas=False,
+ insertion_gate="CNOT",
+ readout=None,
+ qubit_map=None,
+ seed=None,
+ backend=None,
+):
+ """Runs the Zero Noise Extrapolation method for error mitigation.
+
+ The different noise levels are realized by the insertion of pairs of
+ either ``CNOT`` or ``RX(pi/2)`` gates that resolve to the identiy in
+ the noise-free case.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit.
+ observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): Observable to measure.
+ noise_levels (numpy.ndarray): Sequence of noise levels.
+ noise_model (:class:`qibo.noise.NoiseModel`, optional): Noise model applied
+ to simulate noisy computation.
+ nshots (int, optional): Number of shots. Defaults to :math:`10000`.
+ solve_for_gammas (bool, optional): If ``True``, explicitly solve the
+ equations to obtain the ``gamma`` coefficients. Default is ``False``.
+ insertion_gate (str, optional): gate to be used in the insertion.
+ If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``.
+ Defaults to ``"CNOT"``.
+ readout (dict, optional): a dictionary that may contain the following keys:
+
+ * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation.
+ * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation.
+ * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}.
+
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ numpy.ndarray: Estimate of the expected value of ``observable`` in the noise free condition.
+
+ Reference:
+ 1. K. Temme, S. Bravyi et al, *Error mitigation for short-depth quantum circuits*.
+ `arXiv:1612.02058 [quant-ph] `_.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if readout is None:
+ readout = {}
+
+ expected_values = []
+ for num_insertions in noise_levels:
+ noisy_circuit = get_noisy_circuit(
+ circuit, num_insertions, insertion_gate=insertion_gate
+ )
+ val = get_expectation_val_with_readout_mitigation(
+ noisy_circuit,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ expected_values.append(val)
+
+ gamma = get_gammas(noise_levels, analytical=solve_for_gammas)
+
+ return np.sum(gamma * expected_values)
+
+
+def sample_training_circuit_cdr(
+ circuit,
+ replacement_gates: list = None,
+ sigma: float = 0.5,
+ seed=None,
+ backend=None,
+):
+ """Samples a training circuit for CDR by susbtituting some of the non-Clifford gates.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): circuit to sample from,
+ decomposed in ``RX(pi/2)``, ``X``, ``CNOT`` and ``RZ`` gates.
+ replacement_gates (list, optional): candidates for the substitution of the
+ non-Clifford gates. The ``list`` should be composed by ``tuples`` of the
+ form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default:
+ ``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``.
+ sigma (float, optional): standard devation of the Gaussian distribution used for sampling.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.models.Circuit`: The sampled circuit.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if replacement_gates is None:
+ replacement_gates = [(gates.RZ, {"theta": n * np.pi / 2}) for n in range(4)]
+
+ gates_to_replace = []
+ for i, gate in enumerate(circuit.queue):
+ if isinstance(gate, gates.RZ):
+ if not gate.clifford:
+ gates_to_replace.append((i, gate))
+
+ if not gates_to_replace:
+ raise_error(ValueError, "No non-Clifford RZ gate found, no circuit sampled.")
+
+ replacement, distance = [], []
+ for _, gate in gates_to_replace:
+ rep_gates = np.array(
+ [rg(*gate.init_args, **kwargs) for rg, kwargs in replacement_gates]
+ )
+
+ replacement.append(rep_gates)
+ distance.append(
+ backend.np.real(
+ backend.np.linalg.norm(
+ gate.matrix(backend)
+ - backend.cast(
+ [rep_gate.matrix(backend) for rep_gate in rep_gates]
+ ),
+ ord="fro",
+ axis=(1, 2),
+ )
+ )
+ )
+
+ distance = backend.np.vstack(distance)
+ prob = backend.np.exp(-(distance**2) / sigma**2)
+
+ index = local_state.choice(
+ range(len(gates_to_replace)),
+ size=min(int(len(gates_to_replace) / 2), 50),
+ replace=False,
+ p=backend.to_numpy(backend.np.sum(prob, -1) / backend.np.sum(prob)),
+ )
+
+ gates_to_replace = np.array([gates_to_replace[i] for i in index])
+ prob = [prob[i] for i in index]
+ prob = backend.cast(prob, dtype=prob[0].dtype)
+ prob = backend.to_numpy(prob)
+
+ replacement = np.array([replacement[i] for i in index])
+ replacement = [
+ replacement[i][local_state.choice(range(len(p)), size=1, p=p / np.sum(p))[0]]
+ for i, p in enumerate(prob)
+ ]
+ replacement = {i[0]: g for i, g in zip(gates_to_replace, replacement)}
+
+ sampled_circuit = circuit.__class__(**circuit.init_kwargs)
+ for i, gate in enumerate(circuit.queue):
+ sampled_circuit.add(replacement.get(i, gate))
+
+ return sampled_circuit
+
+
+def CDR(
+ circuit,
+ observable,
+ noise_model,
+ nshots: int = 10000,
+ model=lambda x, a, b: a * x + b,
+ n_training_samples: int = 100,
+ full_output: bool = False,
+ readout=None,
+ qubit_map=None,
+ seed=None,
+ backend=None,
+):
+ """Runs the Clifford Data Regression error mitigation method.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit decomposed in the
+ primitive gates ``X``, ``CNOT``, ``RX(pi/2)``, ``RZ(theta)``.
+ observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): observable to be measured.
+ noise_model (:class:`qibo.noise.NoiseModel`): noise model used for simulating
+ noisy computation.
+ nshots (int, optional): number of shots. Defaults :math:`10000`.
+ model (callable, optional): model used for fitting. This should be a callable
+ function object ``f(x, *params)``, taking as input the predictor variable
+ and the parameters. Default is a simple linear model ``f(x,a,b) := a*x + b``.
+ n_training_samples (int, optional): number of training circuits to sample. Defaults to 100.
+ full_output (bool, optional): if ``True``, this function returns additional
+ information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``.
+ readout (dict, optional): a dictionary that may contain the following keys:
+
+ * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation.
+ * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation.
+ * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}.
+
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ mit_val (float): Mitigated expectation value of `observable`.
+ val (float): Noisy expectation value of `observable`.
+ optimal_params (list): Optimal values for `params`.
+ train_val (dict): Contains the noise-free and noisy expectation values obtained with the training circuits.
+
+ Reference:
+ 1. P. Czarnik, A. Arrasmith et al, *Error mitigation with Clifford quantum-circuit data*.
+ `arXiv:2005.10189 [quant-ph] `_.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if readout is None:
+ readout = {}
+
+ training_circuits = [
+ sample_training_circuit_cdr(circuit, seed=local_state, backend=backend)
+ for _ in range(n_training_samples)
+ ]
+
+ train_val = {"noise-free": [], "noisy": []}
+ for circ in training_circuits:
+ result = backend.execute_circuit(circ, nshots=nshots)
+ val = result.expectation_from_samples(observable)
+ train_val["noise-free"].append(val)
+ val = get_expectation_val_with_readout_mitigation(
+ circ,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ train_val["noisy"].append(val)
+
+ optimal_params = curve_fit(model, train_val["noisy"], train_val["noise-free"])[0]
+
+ val = get_expectation_val_with_readout_mitigation(
+ circuit,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ mit_val = model(val, *optimal_params)
+
+ if full_output:
+ return mit_val, val, optimal_params, train_val
+
+ return mit_val
+
+
+def vnCDR(
+ circuit,
+ observable,
+ noise_levels,
+ noise_model,
+ nshots: int = 10000,
+ model=lambda x, *params: (x * np.array(params).reshape(-1, 1)).sum(0),
+ n_training_samples: int = 100,
+ insertion_gate: str = "CNOT",
+ full_output: bool = False,
+ readout=None,
+ qubit_map=None,
+ seed=None,
+ backend=None,
+):
+ """Runs the variable-noise Clifford Data Regression error mitigation method.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit decomposed in the
+ primitive gates ``X``, ``CNOT``, ``RX(pi/2)``, ``RZ(theta)``.
+ observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): observable to be measured.
+ noise_levels (numpy.ndarray): sequence of noise levels.
+ noise_model (:class:`qibo.noise.NoiseModel`): noise model used for
+ simulating noisy computation.
+ nshots (int, optional): number of shots. Defaults to :math:`10000`.
+ model (callable, optional): model used for fitting. This should be a callable
+ function object ``f(x, *params)``, taking as input the predictor variable
+ and the parameters. Default is a simple linear model ``f(x,a,b) := a*x + b``.
+ n_training_samples (int, optional): number of training circuits to sample.
+ insertion_gate (str, optional): gate to be used in the insertion.
+ If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``.
+ Default is ``"CNOT"``.
+ full_output (bool, optional): if ``True``, this function returns additional
+ information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``.
+ readout (dict, optional): a dictionary that may contain the following keys:
+
+ * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation.
+ * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation.
+ * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}.
+
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ mit_val (float): Mitigated expectation value of `observable`.
+ val (list): Expectation value of `observable` with increased noise levels.
+ optimal_params (list): Optimal values for `params`.
+ train_val (dict): Contains the noise-free and noisy expectation values obtained
+ with the training circuits.
+
+ Reference:
+ 1. A. Lowe, MH. Gordon et al, *Unified approach to data-driven quantum error mitigation*.
+ `arXiv:2011.01157 [quant-ph] `_.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if readout is None:
+ readout = {}
+
+ training_circuits = [
+ sample_training_circuit_cdr(circuit, seed=local_state, backend=backend)
+ for _ in range(n_training_samples)
+ ]
+ train_val = {"noise-free": [], "noisy": []}
+
+ for circ in training_circuits:
+ result = backend.execute_circuit(circ, nshots=nshots)
+ val = result.expectation_from_samples(observable)
+ train_val["noise-free"].append(val)
+ for level in noise_levels:
+ noisy_c = get_noisy_circuit(circ, level, insertion_gate=insertion_gate)
+ val = get_expectation_val_with_readout_mitigation(
+ noisy_c,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ train_val["noisy"].append(val)
+
+ noisy_array = np.array(train_val["noisy"]).reshape(-1, len(noise_levels))
+
+ params = local_state.random(len(noise_levels))
+ optimal_params = curve_fit(model, noisy_array.T, train_val["noise-free"], p0=params)
+
+ val = []
+ for level in noise_levels:
+ noisy_c = get_noisy_circuit(circuit, level, insertion_gate=insertion_gate)
+ expval = get_expectation_val_with_readout_mitigation(
+ noisy_c,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ val.append(expval)
+
+ mit_val = model(np.array(val).reshape(-1, 1), *optimal_params[0])[0]
+
+ if full_output:
+ return mit_val, val, optimal_params, train_val
+
+ return mit_val
+
+
+def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10):
+ """
+ Iterative Bayesian Unfolding (IBU) method for readout mitigation.
+
+ Args:
+ probabilities (numpy.ndarray): the input probabilities to be unfolded.
+ response_matrix (numpy.ndarray): the response matrix.
+ iterations (int, optional): the number of iterations to perform. Defaults to 10.
+
+ Returns:
+ numpy.ndarray: the unfolded probabilities.
+
+ Reference:
+ 1. B. Nachman, M. Urbanek et al, *Unfolding Quantum Computer Readout Noise*.
+ `arXiv:1910.01969 [quant-ph] `_.
+ 2. S. Srinivasan, B. Pokharel et al, *Scalable Measurement Error Mitigation via Iterative Bayesian Unfolding*.
+ `arXiv:2210.12284 [quant-ph] `_.
+ """
+ unfolded_probabilities = np.ones((len(probabilities), 1)) / len(probabilities)
+
+ for _ in range(iterations):
+ unfolded_probabilities = unfolded_probabilities * (
+ np.transpose(response_matrix)
+ @ (probabilities / (response_matrix @ unfolded_probabilities))
+ )
+
+ return unfolded_probabilities
+
+
+def get_response_matrix(
+ nqubits, qubit_map=None, noise_model=None, nshots: int = 10000, backend=None
+):
+ """Computes the response matrix for readout mitigation.
+
+ Args:
+ nqubits (int): Total number of qubits.
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``.
+ noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model used for simulating
+ noisy computation. This matrix can be used to mitigate the effect of
+ `qibo.noise.ReadoutError`.
+ nshots (int, optional): number of shots. Defaults to :math:`10000`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ numpy.ndarray : the computed (`nqubits`, `nqubits`) response matrix for
+ readout mitigation.
+ """
+ from qibo import Circuit # pylint: disable=import-outside-toplevel
+
+ backend = _check_backend(backend)
+
+ response_matrix = np.zeros((2**nqubits, 2**nqubits))
+
+ for i in range(2**nqubits):
+ binary_state = format(i, f"0{nqubits}b")
+
+ circuit = Circuit(nqubits, density_matrix=True)
+ for qubit, bit in enumerate(binary_state):
+ if bit == "1":
+ circuit.add(gates.X(qubit))
+ circuit.add(gates.M(*range(nqubits)))
+
+ circuit_result = _execute_circuit(
+ circuit, qubit_map, noise_model, nshots, backend=backend
+ )
+
+ frequencies = circuit_result.frequencies()
+
+ column = np.zeros(2**nqubits)
+ for key, value in frequencies.items():
+ column[int(key, 2)] = value / nshots
+ response_matrix[:, i] = column
+
+ return response_matrix
+
+
+def apply_resp_mat_readout_mitigation(state, response_matrix, iterations=None):
+ """
+ Applies readout error mitigation to the given state using the provided response matrix.
+
+ Args:
+ state (:class:`qibo.measurements.CircuitResult`): the input state to be updated. This state should contain the
+ frequencies that need to be mitigated.
+ response_matrix (numpy.ndarray): the response matrix for readout mitigation.
+ iterations (int, optional): the number of iterations to use for the Iterative Bayesian Unfolding method.
+ If ``None`` the 'inverse' method is used. Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.measurements.CircuitResult`: the input state with the updated (mitigated) frequencies.
+ """
+ frequencies = np.zeros(2 ** len(state.measurements[0].qubits))
+ for key, value in state.frequencies().items():
+ frequencies[int(key, 2)] = value
+
+ frequencies = frequencies.reshape(-1, 1)
+
+ if iterations is None:
+ calibration_matrix = np.linalg.inv(response_matrix)
+ mitigated_frequencies = calibration_matrix @ frequencies
+ else:
+ mitigated_probabilities = iterative_bayesian_unfolding(
+ frequencies / np.sum(frequencies), response_matrix, iterations
+ )
+ mitigated_frequencies = np.round(
+ mitigated_probabilities * np.sum(frequencies), 0
+ )
+ mitigated_frequencies = (
+ mitigated_frequencies / np.sum(mitigated_frequencies)
+ ) * np.sum(frequencies)
+
+ for i, value in enumerate(mitigated_frequencies):
+ state._frequencies[i] = float(value)
+
+ return state
+
+
+def apply_randomized_readout_mitigation(
+ circuit,
+ noise_model=None,
+ nshots: int = 10000,
+ ncircuits: int = 10,
+ qubit_map=None,
+ seed=None,
+ backend=None,
+):
+ """Readout mitigation method that transforms the bias in an expectation value into a
+ measurable multiplicative factor.
+
+ This factor can be eliminated at the expense of increased sampling complexity
+ for the observable.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit.
+ noise_model(:class:`qibo.noise.NoiseModel`, optional): noise model used for
+ simulating noisy computation. Defaults to ``None``.
+ nshots (int, optional): number of shots. Defaults to :math:`10000`.
+ ncircuits (int, optional): number of randomized circuits. Each of them uses
+ ``int(nshots / ncircuits)`` shots. Defaults to 10.
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Return:
+ :class:`qibo.measurements.CircuitResult`: the state of the input circuit with
+ mitigated frequencies.
+
+
+ Reference:
+ 1. Ewout van den Berg, Zlatko K. Minev et al,
+ *Model-free readout-error mitigation for quantum expectation values*.
+ `arXiv:2012.09738 [quant-ph] `_.
+ """
+ from qibo import Circuit # pylint: disable=import-outside-toplevel
+ from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel
+ random_pauli,
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ meas_qubits = circuit.measurements[0].qubits
+ nshots_r = int(nshots / ncircuits)
+ freq = np.zeros((ncircuits, 2), object)
+ for k in range(ncircuits):
+ circuit_c = circuit.copy(True)
+ circuit_c.queue.pop()
+ cal_circuit = Circuit(circuit.nqubits, density_matrix=True)
+
+ x_gate = random_pauli(
+ circuit.nqubits, 1, subset=["I", "X"], seed=local_state
+ ).queue
+
+ error_map = {}
+ for j, gate in enumerate(x_gate):
+ if isinstance(gate, gates.X) and gate.qubits[0] in meas_qubits:
+ error_map[gate.qubits[0]] = 1
+
+ circuits = [circuit_c, cal_circuit]
+ results = []
+ freqs = []
+ for circ in circuits:
+ circ.add(x_gate)
+ circ.add(gates.M(*meas_qubits))
+
+ result = _execute_circuit(
+ circ, qubit_map, noise_model, nshots_r, backend=backend
+ )
+ result._samples = result.apply_bitflips(error_map)
+ results.append(result)
+ freqs.append(result.frequencies(binary=False))
+ freq[k, :] = freqs
+
+ for j in range(2):
+ results[j].nshots = nshots
+ freq_sum = freq[0, j]
+ for frs in freq[1::, j]:
+ freq_sum += frs
+ results[j]._frequencies = freq_sum
+
+ return results
+
+
+def get_expectation_val_with_readout_mitigation(
+ circuit,
+ observable,
+ noise_model=None,
+ nshots: int = 10000,
+ readout=None,
+ qubit_map=None,
+ seed=None,
+ backend=None,
+):
+ """
+ Applies readout error mitigation to the given circuit and observable.
+
+ Args:
+ circuit (qibo.models.Circuit): input circuit.
+ observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): The observable to be measured.
+ noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``.
+ nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`.
+ readout (dict, optional): a dictionary that may contain the following keys:
+
+ * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation.
+ * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation.
+ * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}.
+
+ qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution.
+ If None, it uses the global backend. Defaults to ``None``.
+
+ Returns:
+ float: the mitigated expectation value of the observable.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if readout is None: # pragma: no cover
+ readout = {}
+
+ if "ncircuits" in readout:
+ circuit_result, circuit_result_cal = apply_randomized_readout_mitigation(
+ circuit,
+ noise_model,
+ nshots,
+ readout["ncircuits"],
+ seed=local_state,
+ backend=backend,
+ )
+ else:
+ circuit_result = _execute_circuit(
+ circuit, qubit_map, noise_model, nshots, backend=backend
+ )
+ if "response_matrix" in readout:
+ circuit_result = apply_resp_mat_readout_mitigation(
+ circuit_result,
+ readout["response_matrix"],
+ readout.get("ibu_iters", None),
+ )
+
+ exp_val = circuit_result.expectation_from_samples(observable)
+
+ if "ncircuits" in readout:
+ exp_val /= circuit_result_cal.expectation_from_samples(observable)
+
+ return exp_val
+
+
+def sample_clifford_training_circuit(
+ circuit,
+ seed=None,
+ backend=None,
+):
+ """Samples a training circuit for CDR by susbtituting all the non-Clifford gates.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): circuit to sample from.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.models.Circuit`: the sampled circuit.
+ """
+ from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel
+ random_clifford,
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ non_clifford_gates_indices = [
+ i
+ for i, gate in enumerate(circuit.queue)
+ if not gate.clifford and not isinstance(gate, gates.M)
+ ]
+
+ if not non_clifford_gates_indices:
+ raise_error(ValueError, "No non-Clifford gate found, no circuit sampled.")
+
+ sampled_circuit = circuit.__class__(**circuit.init_kwargs)
+
+ for i, gate in enumerate(circuit.queue):
+ if isinstance(gate, gates.M):
+ for q in gate.qubits:
+ gate_rand = gates.Unitary(
+ random_clifford(
+ 1, return_circuit=False, seed=local_state, backend=backend
+ ),
+ q,
+ )
+ gate_rand.clifford = True
+ sampled_circuit.add(gate_rand)
+ sampled_circuit.add(gate)
+ else:
+ if i in non_clifford_gates_indices:
+ gate = gates.Unitary(
+ random_clifford(
+ len(gate.qubits),
+ return_circuit=False,
+ seed=local_state,
+ backend=backend,
+ ),
+ *gate.qubits,
+ )
+ gate.clifford = True
+ sampled_circuit.add(gate)
+
+ return sampled_circuit
+
+
+def error_sensitive_circuit(circuit, observable, seed=None, backend=None):
+ """
+ Generates a Clifford circuit that preserves the same circuit frame as the input circuit, and stabilizes the specified Pauli observable.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit.
+ observable (:class:`qibo.hamiltonians.Hamiltonian` or :class:`qibo.hamiltonians.SymbolicHamiltonian`):
+ Pauli observable to be measured.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. if ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.models.Circuit`: the error sensitive circuit.
+ :class:`qibo.models.Circuit`: the sampled Clifford circuit.
+ list: the list of adjustment gates.
+
+ Reference:
+ 1. Dayue Qin, Yanzhu Chen et al, *Error statistics and scalability of quantum error mitigation formulas*.
+ `arXiv:2112.06255 [quant-ph] `_.
+ """
+ from qibo import matrices # pylint: disable=import-outside-toplevel
+ from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel
+ comp_basis_to_pauli,
+ random_clifford,
+ vectorization,
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ sampled_circuit = sample_clifford_training_circuit(
+ circuit, seed=local_state, backend=backend
+ )
+ unitary_matrix = sampled_circuit.unitary(backend=backend)
+ num_qubits = sampled_circuit.nqubits
+
+ comp_to_pauli = comp_basis_to_pauli(num_qubits, backend=backend)
+ observable.nqubits = num_qubits
+ observable_liouville = vectorization(
+ backend.np.transpose(backend.np.conj(unitary_matrix), (1, 0))
+ @ observable.matrix
+ @ unitary_matrix,
+ order="row",
+ backend=backend,
+ )
+ observable_pauli_liouville = comp_to_pauli @ observable_liouville
+
+ index = int(
+ backend.np.where(backend.np.abs(observable_pauli_liouville) >= 1e-5)[0][0]
+ )
+
+ observable_pauli = list(product(["I", "X", "Y", "Z"], repeat=num_qubits))[index]
+
+ pauli_gates = {
+ "I": backend.cast(matrices.I, dtype=matrices.I.dtype),
+ "X": backend.cast(matrices.X, dtype=matrices.X.dtype),
+ "Y": backend.cast(matrices.Y, dtype=matrices.Y.dtype),
+ "Z": backend.cast(matrices.Z, dtype=matrices.Z.dtype),
+ }
+
+ adjustment_gates = []
+ for i in range(num_qubits):
+ observable_i = pauli_gates[observable_pauli[i]]
+ random_init = pauli_gates["I"]
+ while backend.np.any(
+ backend.np.abs(observable_i - pauli_gates["Z"]) > 1e-5
+ ) and backend.np.any(abs(observable_i - pauli_gates["I"]) > 1e-5):
+ random_init = random_clifford(
+ 1, return_circuit=False, seed=local_state, backend=backend
+ )
+ observable_i = (
+ backend.np.conj(backend.np.transpose(random_init, (1, 0)))
+ @ pauli_gates[observable_pauli[i]]
+ @ random_init
+ )
+
+ adjustment_gate = gates.Unitary(random_init, i)
+ adjustment_gate.clifford = True
+ adjustment_gates.append(adjustment_gate)
+
+ sensitive_circuit = sampled_circuit.__class__(**sampled_circuit.init_kwargs)
+
+ for gate in adjustment_gates:
+ sensitive_circuit.add(gate)
+ for gate in sampled_circuit.queue:
+ sensitive_circuit.add(gate)
+
+ return sensitive_circuit, sampled_circuit, adjustment_gates
+
+
+def ICS(
+ circuit,
+ observable,
+ readout=None,
+ qubit_map=None,
+ noise_model=None,
+ nshots=int(1e4),
+ n_training_samples=10,
+ full_output=False,
+ seed=None,
+ backend=None,
+):
+ """
+ Computes the Important Clifford Sampling method.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): input circuit.
+ observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): the observable to be measured.
+ readout (dict, optional): a dictionary that may contain the following keys:
+
+ * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation.
+ * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation.
+ * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}.
+
+ qubit_map (list, optional): the qubit map. If ``None``, a list of range of circuit's qubits is used.
+ Defaults to ``None``.
+ noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``.
+ nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`.
+ n_training_samples (int, optional): the number of training samples. Defaults to 10.
+ full_output (bool, optional): if ``True``, this function returns additional
+ information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution.
+ If None, it uses the global backend. Defaults to ``None``.
+
+ Returns:
+ mitigated_expectation (float): the mitigated expectated value.
+ mitigated_expectation_std (float): the standard deviation of the mitigated expectated value.
+ dep_param (float): the depolarizing parameter.
+ dep_param_std (float): the standard deviation of the depolarizing parameter.
+ lambda_list (list): the list of the depolarizing parameters.
+ data (dict): the data dictionary containing the noise-free and noisy expectation values obtained with the training circuits.
+
+ Reference:
+ 1. Dayue Qin, Yanzhu Chen et al, *Error statistics and scalability of quantum error mitigation formulas*.
+ `arXiv:2112.06255 [quant-ph] `_.
+ """
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if readout is None:
+ readout = {}
+
+ if qubit_map is None:
+ qubit_map = list(range(circuit.nqubits))
+
+ training_circuits = [
+ error_sensitive_circuit(circuit, observable, seed=local_state, backend=backend)[
+ 0
+ ]
+ for _ in range(n_training_samples)
+ ]
+
+ data = {"noise-free": [], "noisy": []}
+ lambda_list = []
+
+ for training_circuit in training_circuits:
+ circuit_result = backend.execute_circuit(training_circuit, nshots=nshots)
+ expectation = observable.expectation_from_samples(circuit_result.frequencies())
+
+ noisy_expectation = get_expectation_val_with_readout_mitigation(
+ training_circuit,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+
+ data["noise-free"].append(expectation)
+ data["noisy"].append(noisy_expectation)
+ lambda_list.append(1 - noisy_expectation / expectation)
+
+ dep_param = np.mean(lambda_list)
+ dep_param_std = np.std(lambda_list)
+
+ noisy_expectation = get_expectation_val_with_readout_mitigation(
+ circuit,
+ observable,
+ noise_model,
+ nshots,
+ readout,
+ qubit_map,
+ seed=local_state,
+ backend=backend,
+ )
+ one_dep_squared = (1 - dep_param) ** 2
+ dep_std_squared = dep_param_std**2
+
+ mitigated_expectation = (
+ (1 - dep_param) * noisy_expectation / (one_dep_squared + dep_std_squared)
+ )
+ mitigated_expectation_std = (
+ dep_param_std
+ * abs(noisy_expectation)
+ * abs((1 - dep_param) ** 2 - dep_std_squared)
+ / (one_dep_squared + dep_std_squared) ** 2
+ )
+
+ if full_output:
+ return (
+ mitigated_expectation,
+ mitigated_expectation_std,
+ dep_param,
+ dep_param_std,
+ lambda_list,
+ data,
+ )
+
+ return mitigated_expectation
+
+
+def _execute_circuit(circuit, qubit_map, noise_model=None, nshots=10000, backend=None):
+ """
+ Helper function to execute the given circuit with the specified parameters.
+
+ Args:
+ circuit (qibo.models.Circuit): input circuit.
+ qubit_map (list): the qubit map. If ``None``, a list of range of circuit's qubits is used. Defaults to ``None``.
+ noise_model (qibo.models.noise.Noise, optional): The noise model to be applied. Defaults to ``None``.
+ nshots (int): the number of shots for the circuit execution. Defaults to :math:`10000`..
+ backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution.
+ If None, it uses the global backend. Defaults to ``None``.
+
+ Returns:
+ qibo.states.CircuitResult: The result of the circuit execution.
+ """
+ from qibo.transpiler.placer import Custom
+
+ if backend is None: # pragma: no cover
+ backend = GlobalBackend()
+ elif backend.name == "qibolab": # pragma: no cover
+ backend.transpiler.passes[1] = Custom(
+ initial_map=qubit_map, connectivity=backend.platform.topology
+ )
+ elif noise_model is not None:
+ circuit = noise_model.apply(circuit)
+
+ circuit_result = backend.execute_circuit(circuit, nshots=nshots)
+
+ return circuit_result
diff --git a/src/qibo/models/evolution.py b/src/qibo/models/evolution.py
new file mode 100644
index 000000000..0a53e4917
--- /dev/null
+++ b/src/qibo/models/evolution.py
@@ -0,0 +1,299 @@
+"""Models for time evolution of state vectors."""
+
+from qibo import optimizers, solvers
+from qibo.callbacks import Gap, Norm
+from qibo.config import log, raise_error
+from qibo.hamiltonians.abstract import AbstractHamiltonian
+from qibo.hamiltonians.adiabatic import AdiabaticHamiltonian, BaseAdiabaticHamiltonian
+from qibo.hamiltonians.hamiltonians import SymbolicHamiltonian
+
+
+class StateEvolution:
+ """Unitary time evolution of a state vector under a Hamiltonian.
+
+ Args:
+ hamiltonian (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Hamiltonian
+ to evolve under.
+ dt (float): Time step to use for the numerical integration of
+ Schrondiger's equation.
+ solver (str): Solver to use for integrating Schrodinger's equation.
+ Available solvers are 'exp' which uses the exact unitary evolution
+ operator and 'rk4' or 'rk45' which use Runge-Kutta methods to
+ integrate the Schordinger's time-dependent equation in time.
+ When the 'exp' solver is used to evolve a
+ :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` then the
+ Trotter decomposition of the evolution operator will be calculated
+ and used automatically. If the 'exp' is used on a dense
+ :class:`qibo.core.hamiltonians.hamiltonians.Hamiltonian` the full Hamiltonian
+ matrix will be exponentiated to obtain the exact evolution operator.
+ Runge-Kutta solvers use simple matrix multiplications of the
+ Hamiltonian to the state and no exponentiation is involved.
+ callbacks (list): List of callbacks to calculate during evolution.
+ accelerators (dict): Dictionary of devices to use for distributed
+ execution. This option is available only when the Trotter
+ decomposition is used for the time evolution.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import models, hamiltonians
+ # create critical (h=1.0) TFIM Hamiltonian for three qubits
+ hamiltonian = hamiltonians.TFIM(3, h=1.0)
+ # initialize evolution model with step dt=1e-2
+ evolve = models.StateEvolution(hamiltonian, dt=1e-2)
+ # initialize state to |+++>
+ initial_state = np.ones(8) / np.sqrt(8)
+ # execute evolution for total time T=2
+ final_state2 = evolve(final_time=2, initial_state=initial_state)
+ """
+
+ def __init__(self, hamiltonian, dt, solver="exp", callbacks=[], accelerators=None):
+ hamtypes = (AbstractHamiltonian, BaseAdiabaticHamiltonian)
+ if isinstance(hamiltonian, hamtypes):
+ ham = hamiltonian
+ else:
+ ham = hamiltonian(0)
+ if not isinstance(ham, AbstractHamiltonian):
+ raise TypeError(f"Hamiltonian type {type(ham)} not understood.")
+ self.nqubits = ham.nqubits
+ self.backend = ham.backend
+ if dt <= 0:
+ raise_error(ValueError, f"Time step dt should be positive but is {dt}.")
+ self.dt = dt
+
+ disthamtypes = (SymbolicHamiltonian, BaseAdiabaticHamiltonian)
+ if accelerators is not None: # pragma: no cover
+ if not isinstance(ham, disthamtypes) or solver != "exp":
+ raise_error(
+ NotImplementedError,
+ "Distributed evolution is only "
+ + "implemented using the Trotter "
+ + "exponential solver.",
+ )
+ ham.circuit(dt, accelerators)
+ self.solver = solvers.get_solver(solver, self.dt, hamiltonian)
+ self.callbacks = callbacks
+ self.accelerators = accelerators
+ self.normalize_state = self._create_normalize_state(solver)
+ self.calculate_callbacks = self._create_calculate_callbacks(accelerators)
+
+ def _create_normalize_state(self, solver):
+ if "rk" in solver:
+ log.info("Normalizing state during RK solution.")
+ return lambda s: s / self.backend.calculate_norm(s)
+ else:
+ return lambda s: s
+
+ def _create_calculate_callbacks(self, accelerators):
+ def calculate_callbacks(state):
+ for callback in self.callbacks:
+ callback.nqubits = self.nqubits
+ # by executing callbacks.apply we also append the object to history
+ # see callbacks module for this
+ callback.apply(self.backend, state)
+
+ if accelerators is None:
+ return calculate_callbacks
+
+ else: # pragma: no cover
+
+ def calculate_callbacks_distributed(state):
+ if not isinstance(state, self.backend.tensor_types):
+ state = state.state()
+ calculate_callbacks(state)
+
+ return calculate_callbacks_distributed
+
+ def execute(self, final_time, start_time=0.0, initial_state=None):
+ """Runs unitary evolution for a given total time.
+
+ Args:
+ final_time (float): Final time of evolution.
+ start_time (float): Initial time of evolution. Defaults to t=0.
+ initial_state (np.ndarray): Initial state of the evolution.
+
+ Returns:
+ Final state vector a ``tf.Tensor`` or a
+ :class:`qibo.core.distutils.DistributedState` when a
+ distributed execution is used.
+ """
+ if initial_state is None:
+ raise_error(
+ ValueError, "StateEvolution cannot be used without " "initial state."
+ )
+ state = self.backend.cast(initial_state)
+ self.solver.t = start_time
+ nsteps = int((final_time - start_time) / self.solver.dt)
+ self.calculate_callbacks(state)
+ for _ in range(nsteps):
+ state = self.solver(state)
+ if self.callbacks:
+ state = self.normalize_state(state)
+ self.calculate_callbacks(state)
+ state = self.normalize_state(state)
+ return state
+
+ def __call__(self, final_time, start_time=0.0, initial_state=None):
+ """Equivalent to :meth:`qibo.models.StateEvolution.execute`."""
+ return self.execute(final_time, start_time, initial_state)
+
+
+class AdiabaticEvolution(StateEvolution):
+ """Adiabatic evolution of a state vector under the following Hamiltonian:
+
+ .. math::
+ H(t) = (1 - s(t)) H_0 + s(t) H_1
+
+ Args:
+ h0 (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Easy Hamiltonian.
+ h1 (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Problem Hamiltonian.
+ These Hamiltonians should be time-independent.
+ s (callable): Function of time that defines the scheduling of the
+ adiabatic evolution. Can be either a function of time s(t) or a
+ function with two arguments s(t, p) where p corresponds to a vector
+ of parameters to be optimized.
+ dt (float): Time step to use for the numerical integration of
+ Schrondiger's equation.
+ solver (str): Solver to use for integrating Schrodinger's equation.
+ Available solvers are 'exp' which uses the exact unitary evolution
+ operator and 'rk4' or 'rk45' which use Runge-Kutta methods to
+ integrate the Schordinger's time-dependent equation in time.
+ When the 'exp' solver is used to evolve a
+ :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` then the
+ Trotter decomposition of the evolution operator will be calculated
+ and used automatically. If the 'exp' is used on a dense
+ :class:`qibo.hamiltonians.hamiltonians.Hamiltonian` the full Hamiltonian
+ matrix will be exponentiated to obtain the exact evolution operator.
+ Runge-Kutta solvers use simple matrix multiplications of the
+ Hamiltonian to the state and no exponentiation is involved.
+ callbacks (list): List of callbacks to calculate during evolution.
+ accelerators (dict): Dictionary of devices to use for distributed
+ execution. This option is available only when the Trotter
+ decomposition is used for the time evolution.
+ """
+
+ ATOL = 1e-7 # Tolerance for checking s(0) = 0 and s(T) = 1.
+
+ def __init__(self, h0, h1, s, dt, solver="exp", callbacks=[], accelerators=None):
+ self.hamiltonian = AdiabaticHamiltonian(h0, h1) # pylint: disable=E0110
+ super().__init__(self.hamiltonian, dt, solver, callbacks, accelerators)
+
+ # Set evolution model to "Gap" callback if one exists
+ for callback in self.callbacks:
+ if isinstance(callback, Gap):
+ callback.evolution = self
+
+ # Flag to control if loss messages are shown during optimization
+ self.opt_messages = False
+ self.opt_history = {"params": [], "loss": []}
+
+ self.parametrized_schedule = None
+ nparams = s.__code__.co_argcount
+ if nparams == 1: # given ``s`` is a function of time only
+ self.schedule = s
+ elif nparams == 2: # given ``s`` has undefined parameters
+ self.parametrized_schedule = s
+ else:
+ raise_error(
+ ValueError,
+ f"Scheduling function shoud take one or "
+ "two arguments but it takes {nparams}.",
+ )
+
+ @property
+ def schedule(self):
+ """Returns scheduling as a function of time."""
+ if self.hamiltonian.schedule is None:
+ raise_error(
+ ValueError,
+ "Cannot access scheduling function before "
+ "setting its free parameters.",
+ )
+ return self.hamiltonian.schedule
+
+ @schedule.setter
+ def schedule(self, f):
+ """Sets scheduling s(t) function."""
+ s0 = f(0)
+ if abs(s0) > self.ATOL:
+ raise_error(ValueError, f"s(0) should be 0 but is {s0}.")
+ s1 = f(1)
+ if abs(s1 - 1) > self.ATOL:
+ raise_error(ValueError, f"s(1) should be 1 but is {s1}.")
+ self.hamiltonian.schedule = f
+
+ def set_parameters(self, params):
+ """Sets the variational parameters of the scheduling function."""
+ if self.parametrized_schedule is not None:
+ self.schedule = lambda t: self.parametrized_schedule(t, params[:-1])
+ self.hamiltonian.total_time = params[-1]
+
+ def execute(self, final_time, start_time=0.0, initial_state=None):
+ """"""
+ if start_time != 0:
+ raise_error(
+ NotImplementedError,
+ "Adiabatic evolution supports only t=0 " "as initial time.",
+ )
+ self.hamiltonian.total_time = final_time - start_time
+ if initial_state is None:
+ initial_state = self.backend.cast(
+ self.hamiltonian.ground_state(), copy=True
+ )
+ return super().execute(final_time, start_time, initial_state)
+
+ @staticmethod
+ def _loss(params, adiabatic_evolution, h1, opt_messages, opt_history):
+ """Expectation value of H1 for a choice of scheduling parameters.
+
+ Returns a ``tf.Tensor``.
+ """
+ adiabatic_evolution.set_parameters(params)
+ ham = adiabatic_evolution.hamiltonian
+ initial_state = ham.backend.cast(ham.h0.ground_state(), copy=True)
+ final_state = super(AdiabaticEvolution, adiabatic_evolution).execute(
+ params[-1], initial_state=initial_state
+ )
+ loss = h1.expectation(final_state, normalize=True)
+ if opt_messages:
+ opt_history["params"].append(params)
+ opt_history["loss"].append(loss)
+ log.info(f"Params: {params} - = {loss}")
+ return loss
+
+ def minimize(self, initial_parameters, method="BFGS", options=None, messages=False):
+ """Optimize the free parameters of the scheduling function.
+
+ Args:
+ initial_parameters (np.ndarray): Initial guess for the variational
+ parameters that are optimized.
+ The last element of the given array should correspond to the
+ guess for the total evolution time T.
+ method (str): The desired minimization method.
+ One of ``"cma"`` (genetic optimizer), ``"sgd"`` (gradient descent) or
+ any of the methods supported by
+ `scipy.optimize.minimize `_.
+ options (dict): a dictionary with options for the different optimizers.
+ messages (bool): If ``True`` the loss evolution is shown during
+ optimization.
+ """
+ self.opt_messages = messages
+ if method == "sgd":
+ loss = self._loss
+ else:
+ loss = lambda p, ae, h1, msg, hist: self.backend.to_numpy(
+ self._loss(p, ae, h1, msg, hist)
+ )
+
+ args = (self, self.hamiltonian.h1, self.opt_messages, self.opt_history)
+ result, parameters, extra = optimizers.optimize(
+ loss, initial_parameters, args=args, method=method, options=options
+ )
+ if isinstance(parameters, self.backend.tensor_types) and not len(
+ parameters.shape
+ ): # pragma: no cover
+ # some optimizers like ``Powell`` return number instead of list
+ parameters = [parameters]
+ self.set_parameters(parameters)
+ return result, parameters, extra
diff --git a/src/qibo/models/grover.py b/src/qibo/models/grover.py
new file mode 100644
index 000000000..c18fab512
--- /dev/null
+++ b/src/qibo/models/grover.py
@@ -0,0 +1,280 @@
+import numpy as np
+
+from qibo import gates
+from qibo.config import log, raise_error
+from qibo.models.circuit import Circuit
+
+
+class Grover:
+ """Model that performs Grover's algorithm.
+
+ For Grover's original search algorithm: `arXiv:quant-ph/9605043 `_
+ For the iterative version with unknown solutions:`arXiv:quant-ph/9605034 `_
+ For the Grover algorithm with any superposition:`arXiv:quant-ph/9712011 `_
+
+ Args:
+ oracle (:class:`qibo.core.circuit.Circuit`): quantum circuit that flips
+ the sign using a Grover ancilla initialized with -X-H-. Grover ancilla
+ expected to be last qubit of oracle circuit.
+ superposition_circuit (:class:`qibo.core.circuit.Circuit`): quantum circuit that
+ takes an initial state to a superposition. Expected to use the first
+ set of qubits to store the relevant superposition.
+ initial_state_circuit (:class:`qibo.core.circuit.Circuit`): quantum circuit
+ that initializes the state. If empty defaults to ``|000..00>``
+ superposition_qubits (int): number of qubits that store the relevant superposition.
+ Leave empty if superposition does not use ancillas.
+ superposition_size (int): how many states are in a superposition.
+ Leave empty if its an equal superposition of quantum states.
+ number_solutions (int): number of expected solutions. Needed for normal Grover.
+ Leave empty for iterative version.
+ target_amplitude (float): absolute value of the amplitude of the target state. Only for
+ advanced use and known systems.
+ check (function): function that returns True if the solution has been
+ found. Required of iterative approach.
+ First argument should be the bitstring to check.
+ check_args (tuple): arguments needed for the check function.
+ The found bitstring not included.
+ iterative (bool): force the use of the iterative Grover
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+
+ from qibo import Circuit, gates
+ from qibo.models.grover import Grover
+
+ # Create an oracle. Ex: Oracle that detects state |11111>
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+
+ # Create superoposition circuit. Ex: Full superposition over 5 qubits.
+ superposition = Circuit(5)
+ superposition.add([gates.H(i) for i in range(5)])
+
+ # Generate and execute Grover class
+ grover = Grover(oracle, superposition_circuit=superposition, number_solutions=1)
+ solution, iterations = grover()
+ """
+
+ def __init__(
+ self,
+ oracle,
+ superposition_circuit=None,
+ initial_state_circuit=None,
+ superposition_qubits=None,
+ superposition_size=None,
+ number_solutions=None,
+ target_amplitude=None,
+ check=None,
+ check_args=(),
+ iterative=False,
+ ):
+ self.oracle = oracle
+ self.initial_state_circuit = initial_state_circuit
+
+ if superposition_circuit:
+ self.superposition = superposition_circuit
+ else:
+ if not superposition_qubits:
+ raise_error(
+ ValueError,
+ "Cannot create Grover model if the "
+ "superposition circuit or number of "
+ "qubits is not specified.",
+ )
+ self.superposition = Circuit(superposition_qubits)
+ self.superposition.add([gates.H(i) for i in range(superposition_qubits)])
+
+ if superposition_qubits:
+ self.sup_qubits = superposition_qubits
+ else:
+ self.sup_qubits = self.superposition.nqubits
+
+ if superposition_size:
+ self.sup_size = superposition_size
+ else:
+ self.sup_size = int(2**self.sup_qubits)
+
+ assert oracle.nqubits > self.sup_qubits
+
+ self.anc_qubits_sup = self.superposition.nqubits - self.sup_qubits
+ self.anc_qubits_ora = self.oracle.nqubits - self.sup_qubits - 1
+
+ self.nqubits = (
+ self.sup_qubits + max(self.anc_qubits_sup, self.anc_qubits_ora) + 1
+ )
+
+ self.check = check
+ self.check_args = check_args
+ self.num_sol = number_solutions
+ self.targ_a = target_amplitude
+ self.iterative = iterative
+
+ self.space_sup = list(range(self.sup_qubits + self.anc_qubits_sup))
+ self.space_ora = list(range(self.sup_qubits + self.anc_qubits_ora)) + [
+ self.nqubits - 1
+ ]
+
+ def initialize(self):
+ """Initialize the Grover algorithm with the superposition and Grover ancilla."""
+ c = Circuit(self.nqubits)
+ c.add(gates.X(self.nqubits - 1))
+ c.add(gates.H(self.nqubits - 1))
+ if self.initial_state_circuit:
+ c.add(
+ self.initial_state_circuit.invert().on_qubits(
+ *range(self.initial_state_circuit.nqubits)
+ )
+ )
+ c.add(self.superposition.on_qubits(*self.space_sup))
+ return c
+
+ def diffusion(self):
+ """Construct the diffusion operator out of the superposition circuit."""
+ nqubits = self.superposition.nqubits + 1
+ c = Circuit(nqubits)
+ c.add(self.superposition.invert().on_qubits(*range(nqubits - 1)))
+ if self.initial_state_circuit:
+ c.add(
+ self.initial_state_circuit.invert().on_qubits(
+ *range(self.initial_state_circuit.nqubits)
+ )
+ )
+ c.add([gates.X(i) for i in range(self.sup_qubits)])
+ c.add(gates.X(nqubits - 1).controlled_by(*range(self.sup_qubits)))
+ c.add([gates.X(i) for i in range(self.sup_qubits)])
+ if self.initial_state_circuit:
+ c.add(
+ self.initial_state_circuit.on_qubits(
+ *range(self.initial_state_circuit.nqubits)
+ )
+ )
+ c.add(self.superposition.on_qubits(*range(nqubits - 1)))
+ return c
+
+ def step(self):
+ """Combine oracle and diffusion for a Grover step."""
+ c = Circuit(self.nqubits)
+ c.add(self.oracle.on_qubits(*self.space_ora))
+ c.add(self.diffusion().on_qubits(*(self.space_sup + [self.nqubits - 1])))
+ return c
+
+ def circuit(self, iterations):
+ """Creates circuit that performs Grover's algorithm with a set amount of iterations.
+
+ Args:
+ iterations (int): number of times to repeat the Grover step.
+
+ Returns:
+ :class:`qibo.core.circuit.Circuit` that performs Grover's algorithm.
+ """
+ c = Circuit(self.nqubits)
+ c += self.initialize()
+ for _ in range(iterations):
+ c += self.step()
+ c.add(gates.M(*range(self.sup_qubits)))
+ return c
+
+ def iterative_grover(self, lamda_value=6 / 5, backend=None):
+ """Iterative approach of Grover for when the number of solutions is not known.
+
+ Args:
+ lamda_value (real): parameter that controls the evolution of the iterative method.
+ Must be between 1 and 4/3.
+ backend (:class:`qibo.backends.abstract.Backend`): Backend to use for circuit execution.
+
+ Returns:
+ measured (str): bitstring measured and checked as a valid solution.
+ total_iterations (int): number of times the oracle has been called.
+ """
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ k = 1
+ lamda = lamda_value
+ total_iterations = 0
+ while True:
+ it = np.random.randint(k + 1)
+ if it != 0:
+ total_iterations += it
+ circuit = self.circuit(it)
+ result = backend.execute_circuit(circuit, nshots=1)
+ measured = result.frequencies(binary=True).most_common(1)[0][0]
+ if self.check(measured, *self.check_args):
+ return measured, total_iterations
+ k = min(lamda * k, np.sqrt(self.sup_size))
+ if total_iterations > (9 / 4) * np.sqrt(self.sup_size):
+ log.warning("Too many total iterations, output might not be solution.")
+ return measured, total_iterations
+
+ def execute(self, nshots=100, freq=False, logs=False, backend=None):
+ """Execute Grover's algorithm.
+
+ If the number of solutions is given, calculates iterations,
+ otherwise it uses an iterative approach.
+
+ Args:
+ nshots (int): number of shots in order to get the frequencies.
+ freq (bool): print the full frequencies after the exact Grover algorithm.
+ backend (:class:`qibo.backends.abstract.Backend`): Backend to use for circuit execution.
+
+ Returns:
+ solution (str): bitstring (or list of bitstrings) measured as solution of the search.
+ iterations (int): number of oracle calls done to reach a solution.
+ """
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ if (self.num_sol or self.targ_a) and not self.iterative:
+ if self.targ_a:
+ it = int(np.pi * (1 / self.targ_a) / 4)
+ else:
+ it = int(np.pi * np.sqrt(self.sup_size / self.num_sol) / 4)
+ circuit = self.circuit(it)
+ result = backend.execute_circuit(circuit, nshots=nshots)
+ result = result.frequencies(binary=True)
+ if freq:
+ if logs:
+ log.info("Result of sampling Grover's algorihm")
+ log.info(result)
+ self.frequencies = result
+ if logs:
+ log.info(
+ f"Most common states found using Grover's algorithm with {it} iterations:"
+ )
+ if self.targ_a:
+ most_common = result.most_common(1)
+ else:
+ most_common = result.most_common(self.num_sol)
+ self.solution = []
+ self.iterations = it
+ for i in most_common:
+ if logs:
+ log.info(i[0])
+ self.solution.append(i[0])
+ if logs:
+ if self.check:
+ if self.check(i[0], *self.check_args):
+ log.info("Solution checked and successful.")
+ else:
+ log.info(
+ "Not a solution of the problem. Something went wrong."
+ )
+ else:
+ if not self.check:
+ raise_error(ValueError, "Check function needed for iterative approach.")
+ measured, total_iterations = self.iterative_grover(backend=backend)
+ if logs:
+ log.info("Solution found in an iterative process.")
+ log.info(f"Solution: {measured}")
+ log.info(f"Total Grover iterations taken: {total_iterations}")
+ self.solution = measured
+ self.iterations = total_iterations
+ return self.solution, self.iterations
+
+ def __call__(self, nshots=100, freq=False, logs=False, backend=None):
+ """Equivalent to :meth:`qibo.models.grover.Grover.execute`."""
+ return self.execute(nshots=nshots, freq=freq, logs=logs, backend=backend)
diff --git a/src/qibo/models/hep.py b/src/qibo/models/hep.py
new file mode 100644
index 000000000..e03429136
--- /dev/null
+++ b/src/qibo/models/hep.py
@@ -0,0 +1,276 @@
+import numpy as np
+
+from qibo import gates
+from qibo.backends import matrices
+from qibo.config import raise_error
+from qibo.hamiltonians import Hamiltonian
+from qibo.models.circuit import Circuit
+
+
+class qPDF:
+ """Variational Circuit for Quantum PDFs (qPDF).
+
+ Args:
+ ansatz (str): the ansatz name, options are 'Weighted' and 'Fourier'.
+ layers (int): the number of layers for the ansatz.
+ nqubits (int): the number of qubits for the circuit.
+ multi_output (bool): allocates a multi-output model per PDF flavour (default is False).
+ backend (:class:`qibo.backends.abstract.Backend`): Backend object to use for execution.
+ If ``None`` the currently active global backend is used.
+ Default is ``None``.
+ """
+
+ def __init__(self, ansatz, layers, nqubits, multi_output=False, backend=None):
+ """Initialize qPDF."""
+ if not isinstance(layers, int) or layers < 1: # pragma: no cover
+ raise_error(RuntimeError, "Layers must be positive and integer.")
+ if not isinstance(nqubits, int) or nqubits < 1: # pragma: no cover
+ raise_error(RuntimeError, "Number of qubits must be positive and integer.")
+ if not isinstance(multi_output, bool): # pragma: no cover
+ raise_error(TypeError, "multi-output must be a boolean.")
+
+ # parse ansatz
+ if ansatz == "Weighted":
+ ansatz_function = ansatz_Weighted
+ elif ansatz == "Fourier":
+ ansatz_function = ansatz_Fourier
+ else: # pragma: no cover
+ raise_error(NotImplementedError, f"Ansatz {ansatz} not found.")
+
+ # load ansatz
+ self.circuit, self.rotation, self.nparams = ansatz_function(layers, nqubits)
+
+ # load backend
+ from qibo.backends import _check_backend
+
+ self.backend = _check_backend(backend)
+
+ # load hamiltonian
+ if multi_output:
+ self.hamiltonian = [
+ qpdf_hamiltonian(nqubits, z_qubit=q, backend=self.backend)
+ for q in range(nqubits)
+ ]
+ else:
+ self.hamiltonian = [qpdf_hamiltonian(nqubits, backend=self.backend)]
+
+ def _model(self, state, hamiltonian):
+ """Internal function for the evaluation of PDFs.
+
+ Args:
+ state (numpy.array): state vector.
+ hamiltonian (qibo.hamiltonian.Hamiltonian): the Hamiltonian object.
+
+ Returs:
+ The qPDF object following the (1-z)/(1+z) structure.
+ """
+ z = hamiltonian.expectation(state)
+ y = (1 - z) / (1 + z)
+ return y
+
+ def predict(self, parameters, x):
+ """Predict PDF model from underlying circuit.
+
+ Args:
+ parameters (numpy.array): list of parameters for the gates.
+ x (numpy.array): a numpy array with the points in x to be evaluated.
+
+ Returns:
+ A numpy array with the PDF values.
+ """
+ if len(parameters) != self.nparams: # pragma: no cover
+ raise_error(
+ RuntimeError, "Mismatch between number of parameters and model size."
+ )
+ pdf = np.zeros(shape=(len(x), len(self.hamiltonian)))
+ for i, x_value in enumerate(x):
+ params = self.rotation(parameters, x_value)
+ self.circuit.set_parameters(params)
+ result = self.backend.execute_circuit(self.circuit)
+ state = result.state()
+ for flavour, flavour_hamiltonian in enumerate(self.hamiltonian):
+ pdf[i, flavour] = self._model(state, flavour_hamiltonian)
+ return pdf
+
+
+def qpdf_hamiltonian(nqubits, z_qubit=0, backend=None):
+ """Precomputes Hamiltonian.
+
+ Args:
+ nqubits (int): number of qubits.
+ z_qubit (int): qubit where the Z measurement is applied, must be z_qubit < nqubits
+ backend (:class:`qibo.backends.abstract.Backend`): Backend object to use for execution.
+ If ``None`` the currently active global backend is used.
+ Default is ``None``.
+
+ Returns:
+ An Hamiltonian object.
+ """
+ eye = matrices.I
+ if z_qubit == 0:
+ h = matrices.Z
+ for _ in range(nqubits - 1):
+ h = np.kron(eye, h)
+
+ elif z_qubit == nqubits - 1:
+ h = eye
+ for _ in range(nqubits - 2):
+ h = np.kron(eye, h)
+ h = np.kron(matrices.Z, h)
+ else:
+ h = eye
+ for _ in range(nqubits - 1):
+ if _ + 1 == z_qubit:
+ h = np.kron(matrices.Z, h)
+ else:
+ h = np.kron(eye, h)
+ return Hamiltonian(nqubits, h, backend=backend)
+
+
+def map_to(x):
+ """Auxiliary function"""
+ return 2 * np.pi * x
+
+
+def maplog_to(x):
+ """Auxiliary function"""
+ return -np.pi * np.log10(x)
+
+
+def ansatz_Fourier(layers, qubits=1):
+ """Fourier Ansatz implementation. It is composed by 3 parameters per layer
+ and qubit: U3(a, b, c) Ry(x) || U3(a, b, c) Ry(log x).
+
+ Args:
+ layers (int): number of layers.
+ qubits (int): number of qubits.
+
+ Returns:
+ The circuit, the rotation function and the total number of parameters.
+ """
+ circuit = Circuit(qubits)
+ for l in range(layers - 1):
+ for q in range(qubits):
+ for _ in range(2):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.RZ(q, theta=0))
+ circuit.add(gates.RY(q, theta=0))
+
+ if qubits > 1:
+ for q in range(0, qubits, 2):
+ circuit.add(gates.CU1(q, q + 1, theta=0))
+ if qubits > 2:
+ for q in range(1, qubits + 1, 2):
+ circuit.add(gates.CU1(q, (q + 1) % qubits, theta=0))
+
+ for q in range(qubits):
+ for _ in range(2):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.RZ(q, theta=0))
+ circuit.add(gates.RY(q, theta=0))
+
+ def rotation(theta, x):
+ p = circuit.get_parameters()
+ i = 0
+ j = 0
+ for l in range(layers - 1):
+ for q in range(qubits):
+ p[i] = map_to(x)
+ p[i + 1 : i + 3] = theta[j : j + 2]
+ i += 3
+ j += 2
+
+ p[i] = 0.5 * maplog_to(x)
+ p[i + 1 : i + 3] = theta[j : j + 2]
+ i += 3
+ j += 2
+ if qubits > 1:
+ for q in range(0, qubits, 2):
+ p[i] = theta[j]
+ i += 1
+ j += 1
+ if qubits > 2:
+ for q in range(1, qubits + 1, 2):
+ p[i] = theta[j]
+ i += 1
+ j += 1
+ for q in range(qubits):
+ p[i] = 0.5 * map_to(x)
+ p[i + 1 : i + 3] = theta[j : j + 2]
+ i += 3
+ j += 2
+
+ p[i] = 0.5 * maplog_to(x)
+ p[i + 1 : i + 3] = theta[j : j + 2]
+ i += 3
+ j += 2
+ return p
+
+ nparams = 4 * layers * qubits + (layers - 1) * int(np.ceil(qubits / 2)) * (
+ int(qubits > 1) + int(qubits > 2)
+ )
+
+ return circuit, rotation, nparams
+
+
+def ansatz_Weighted(layers, qubits=1):
+ """Fourier Ansatz implementation. 4 parameters per layer and
+ qubit: Ry(wx + a), Rz(v log(x) + b)
+
+ Args:
+ layers (int): number of layers.
+ qubits (int): number of qubits.
+
+ Returns:
+ The circuit, the rotation function and the total number of
+ parameters.
+ """
+ circuit = Circuit(qubits)
+ for _ in range(layers - 1):
+ for q in range(qubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.RZ(q, theta=0))
+ if qubits > 1:
+ for q in range(0, qubits, 2):
+ circuit.add(gates.CU1(q, (q + 1) % qubits, theta=0))
+ if qubits > 2:
+ for q in range(1, qubits + 1, 2):
+ circuit.add(gates.CU1(q, (q + 1) % qubits, theta=0))
+ for q in range(qubits):
+ circuit.add(gates.RY(q, theta=0))
+ circuit.add(gates.RZ(q, theta=0))
+
+ def rotation(theta, x):
+ p = circuit.get_parameters()
+ i = 0
+ j = 0
+ for l in range(layers - 1):
+ for q in range(qubits):
+ p[i] = theta[j] + theta[j + 1] * map_to(x)
+ p[i + 1] = theta[j + 2] + theta[j + 3] * maplog_to(x)
+ i += 2
+ j += 4
+
+ if qubits > 1:
+ for q in range(0, qubits, 2):
+ p[i] = theta[j]
+ i += 1
+ j += 1
+ if qubits > 2:
+ for q in range(1, qubits + 1, 2):
+ p[i] = theta[j]
+ i += 1
+ j += 1
+
+ for q in range(qubits):
+ p[i] = theta[j] + theta[j + 1] * map_to(x)
+ p[i + 1] = theta[j + 2] + theta[j + 3] * maplog_to(x)
+ i += 2
+ j += 4
+
+ return p
+
+ nparams = 4 * layers * qubits + (layers - 1) * int(np.ceil(qubits / 2)) * (
+ int(qubits > 1) + int(qubits > 2)
+ )
+ return circuit, rotation, nparams
diff --git a/src/qibo/models/iqae.py b/src/qibo/models/iqae.py
new file mode 100644
index 000000000..f4ab67dca
--- /dev/null
+++ b/src/qibo/models/iqae.py
@@ -0,0 +1,406 @@
+import numpy as np
+import scipy.stats
+
+from qibo import Circuit, gates
+from qibo.config import raise_error
+
+
+class IQAE:
+ """Model that performs the Iterative Quantum Amplitude Estimation algorithm.
+
+ The implemented class in this code utilizes the Iterative Quantum Amplitude
+ Estimation (IQAE) algorithm, which was proposed in `arxiv:1912.05559
+ `_. The algorithm provides an estimated
+ output that, with a probability ``alpha``, differs from the target value by
+ ``epsilon``. Both ``alpha`` and ``epsilon`` can be specified.
+
+ Unlike Brassard's original QAE algorithm `arxiv:quant-ph/0005055
+ `_, this implementation does not rely
+ on Quantum Phase Estimation but instead is based solely on Grover's
+ algorithm. The IQAE algorithm employs a series of carefully selected Grover
+ iterations to determine an estimate for the target amplitude.
+
+ Args:
+ circuit_a (:class:`qibo.models.circuit.Circuit`): quantum circuit that
+ specifies the QAE problem.
+
+ circuit_q (:class:`qibo.models.circuit.Circuit`): quantum circuit of the
+ Grover/Amplification operator.
+
+ alpha (float): confidence level, the target probability is 1 - ``alpha``,
+ has values between 0 and 1.
+
+ epsilon (float): target precision for estimation target `a`, has values
+ between 0 and 0.5.
+
+ method (str): statistical method used to estimate the confidence
+ intervals in each iteration, can be either `chernoff` (default) for
+ the Chernoff intervals or `beta` for the Clopper-Pearson intervals.
+
+ n_shots (int): number of shots.
+
+ Raises:
+ ValueError: If ``epsilon`` is not in (0, 0.5].
+ ValueError: If ``alpha`` is not in (0, 1).
+ ValueError: If ``method`` is not supported.
+ ValueError: If the number of qubits in ``circuit_a`` is greater than in ``circuit_q``.
+
+ Example:
+ .. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.models.iqae import IQAE
+
+ # Defining circuit A to integrate sin(x)^2 from [0,1]
+ a_circuit = Circuit(2)
+ a_circuit.add(gates.H(0))
+ a_circuit.add(gates.RY(q = 1, theta = 1 / 2))
+ a_circuit.add(gates.CU3(0, 1, 1, 0, 0))
+ # Defining circuit Q = -A S_0 A^-1 S_X
+ q_circuit = Circuit(2)
+ # S_X
+ q_circuit.add(gates.Z(q = 1))
+ # A^-1
+ q_circuit = q_circuit + a_circuit.invert()
+ # S_0
+ q_circuit.add(gates.X(0))
+ q_circuit.add(gates.X(1))
+ q_circuit.add(gates.CZ(0, 1))
+ # A
+ q_circuit = q_circuit + a_circuit
+
+ # Executing IQAE and obtaining the result
+ iae = IQAE(a_circuit, q_circuit)
+ results = iae.execute()
+ integral_value = results.estimation
+ integral_error = results.epsilon_estimated
+ """
+
+ def __init__(
+ self,
+ circuit_a,
+ circuit_q,
+ alpha=0.05,
+ epsilon=0.005,
+ n_shots=1024,
+ method="chernoff",
+ ):
+ self.circuit_a = circuit_a
+ self.circuit_q = circuit_q
+ if circuit_a.nqubits > circuit_q.nqubits:
+ raise_error(
+ ValueError,
+ "The number of qubits for Q must be greater or equal than the number"
+ "of qubits of A.",
+ )
+ if not isinstance(n_shots, int):
+ raise_error(
+ ValueError,
+ "The number of shots must be an integer number.",
+ )
+ # validate ranges of input arguments
+ if not 0 < epsilon <= 0.5:
+ raise_error(ValueError, f"Epsilon must be in (0, 0.5], but is {epsilon}.")
+
+ if not 0 < alpha < 1:
+ raise_error(
+ ValueError,
+ f"The confidence level alpha must be in (0, 1), but is {alpha}.",
+ )
+
+ if method not in {"chernoff", "beta"}:
+ raise_error(
+ ValueError,
+ f"The confidence interval method must be chernoff or beta, but is {method}.",
+ )
+
+ self.alpha = alpha
+ self.epsilon = epsilon
+ self.n_shots = n_shots
+ self.method = method
+
+ def construct_qae_circuit(self, k):
+ """Generates quantum circuit for QAE.
+
+ Args:
+ k (int): number of times the amplification operator ``circuit_q`` is applied.
+ Returns:
+ The quantum circuit of the QAE algorithm.
+ """
+ initialization_circuit_a = self.circuit_a
+ amplification_circuit_q = self.circuit_q
+
+ qc = Circuit(amplification_circuit_q.nqubits)
+
+ qc.add(
+ initialization_circuit_a.on_qubits(
+ *range(0, initialization_circuit_a.nqubits, 1)
+ )
+ )
+ for i in range(k):
+ qc = qc + amplification_circuit_q
+ qc.add(gates.M(initialization_circuit_a.nqubits - 1))
+ return qc
+
+ def clopper_pearson(self, count, n, alpha):
+ """Calculates the confidence interval for the quantity to estimate `a`.
+
+ Args:
+ count (int): number of successes.
+ n (int): total number of trials.
+ alpha (float): significance level. Must be in (0, 0.5).
+
+ Return:
+ The confidence interval [a_min, a_max].
+ """
+ beta_prob_function = scipy.stats.beta.ppf
+ a_min = beta_prob_function(alpha / 2, count, n - count + 1)
+ a_max = beta_prob_function(1 - alpha / 2, count + 1, n - count)
+ if np.isnan(a_min):
+ a_min = 0
+ if np.isnan(a_max):
+ a_max = 1
+ return a_min, a_max
+
+ def h_calc_CP(self, n_successes, n_total_shots, upper_bound_t):
+ """Calculates the `h` function.
+
+ Args:
+ n_successes (int): number of successes.
+ n_total_shots (int): total number of trials.
+ upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
+
+ Returns:
+ The h function for the given inputs.
+ """
+ a_min, a_max = self.clopper_pearson(
+ n_successes, n_total_shots, alpha=(self.alpha / upper_bound_t)
+ )
+ return np.abs(np.arccos(1 - 2 * a_max) - np.arccos(1 - 2 * a_min)) / 2
+
+ def calc_L_range_CP(self, n_shots, upper_bound_t):
+ """Calculate the confidence interval for the Clopper-Pearson method.
+
+ Args:
+ n_shots (int): number of shots.
+ upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
+
+ Returns:
+ max_L, min_L (float, float): The maximum and minimum possible error which could be returned
+ on a given iteration.
+ """
+ x = np.linspace(0, np.pi, 10000)
+ x_domain = [x <= 1.0 + 1 / 10 / n_shots]
+ y = [
+ self.h_calc_CP(int(t * n_shots), n_shots, upper_bound_t)
+ for t in x[tuple(x_domain)]
+ ]
+ max_L = np.max(y) / (2 * np.pi)
+ min_L = np.min(y) / (2 * np.pi)
+ return max_L, min_L
+
+ def calc_L_range_CH(self, n_shots, upper_bound_t):
+ """Calculate the confidence interval for the Chernoff method.
+
+ Args:
+ n_shots (int): number of shots.
+ upper_bound_t (int): maximum number of rounds to achieve the desired absolute error.
+
+ Returns:
+ max_L, min_L (float, float): The maximum and minimum possible error which could be returned
+ on a given iteration.
+ """
+ max_L = (
+ np.arcsin(
+ (2 / (n_shots) * np.log(2 * upper_bound_t / self.alpha)) ** (1 / 4)
+ )
+ / 2
+ / np.pi
+ )
+ min_L = np.arcsin(np.sin(max_L) ** 2)
+ return max_L, min_L
+
+ def find_next_k(self, uppercase_k_i, up_i, theta_l, theta_u, r=2):
+ r"""Find the largest integer ``uppercase_k`` such that the interval ``uppercase_k`` * [ ``theta_l`` , ``theta_u`` ]
+ lies completely in [0, `\pi`] or [`\pi`, 2 `\pi`].
+
+ Args:
+ uppercase_k_i (int): the current ``uppercase_k`` such ``uppercase_k`` = 4 ``k`` + 2,
+ where ``k`` is the power of the operator ``circuit_q``.
+
+ up_i (bool): boolean flag of whether theta_interval lies in the
+ upper half-circle [0, `\pi`] or in the lower one [`\pi`, 2 `\pi`].
+
+ theta_l (float): the current lower limit of the confidence interval for the angle theta.
+
+ theta_u (float): the current upper limit of the confidence interval for the angle theta.
+
+ r (int): lower bound for ``uppercase_k``.
+
+ Returns:
+ The next power `K_i`, and boolean flag for the extrapolated interval.
+ """
+ uppercase_k_max = int(1 / (2 * (theta_u - theta_l)))
+ uppercase_k = uppercase_k_max - (uppercase_k_max - 2) % 4
+ while uppercase_k >= r * uppercase_k_i:
+ theta_min = uppercase_k * theta_l - int(uppercase_k * theta_l)
+ theta_max = uppercase_k * theta_u - int(uppercase_k * theta_u)
+ if int(uppercase_k * theta_u) == int(uppercase_k * theta_l):
+ if theta_max <= 1 / 2 and theta_min <= 1 / 2:
+ up = True
+ return (uppercase_k, up)
+ elif theta_max >= 1 / 2 and theta_min >= 1 / 2:
+ up = False
+ return (uppercase_k, up)
+ uppercase_k -= 4
+ return (uppercase_k_i, up_i)
+
+ def execute(self, backend=None):
+ """Execute IQAE algorithm.
+
+ Args:
+ backend: the qibo backend.
+
+ Returns:
+ A :class:`qibo.models.iqae.IterativeAmplitudeEstimationResult` results object.
+ """
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ # Initializing all parameters
+ k = [0]
+ # uppercase_k=4k+2
+ uppercase_k = [2]
+
+ theta_u = 1 / 4
+ theta_l = 0
+ theta_intervals = [theta_l, theta_u]
+ a_intervals = [
+ np.sin(2 * np.pi * theta_l) ** 2,
+ np.sin(2 * np.pi * theta_u) ** 2,
+ ]
+ a_min = [0]
+ a_max = [1]
+ theta_dif = np.abs(theta_u - theta_l)
+ up = [True]
+ samples_history = []
+ n_shots_history = []
+
+ eps = self.epsilon / (2 * np.pi)
+ upper_bound_t = int(np.log2(np.pi / (8 * self.epsilon))) + 1
+ n_total_shots = self.n_shots
+ num_oracle_queries = 0
+
+ if self.method == "chernoff":
+ # Chernoff method
+ max_L, min_L = self.calc_L_range_CH(n_total_shots, upper_bound_t)
+
+ else:
+ # Clopper-Pearson (beta) method
+ max_L, min_L = self.calc_L_range_CP(n_total_shots, upper_bound_t)
+
+ i = 0
+ while theta_dif > 2 * eps:
+ i = i + 1
+ uppercase_k_i, up_i = self.find_next_k(
+ uppercase_k[-1], up[-1], theta_l, theta_u
+ )
+ k_i = int((uppercase_k_i - 2) / 4)
+ uppercase_k.append(uppercase_k_i)
+ up.append(up_i)
+ k.append(k_i)
+ # Checking the no-overshooting condition
+ if uppercase_k_i > int(max_L / eps):
+ # We ensure to not make unnecessary measurement shots at last iterations of the algorithm
+ n_shots_i = int((max_L / eps) * self.n_shots / uppercase_k_i / 10)
+ # To avoid having a null number of shots
+ if n_shots_i == 0:
+ n_shots_i = 1
+ else:
+ n_shots_i = self.n_shots
+
+ # Calling and executing the quantum circuit
+ qc = self.construct_qae_circuit(k_i)
+ samples = backend.execute_circuit(qc, nshots=n_shots_i).frequencies(
+ binary=True
+ )["1"]
+
+ samples_history.append(samples)
+ n_shots_history.append(n_shots_i)
+
+ num_oracle_queries += n_shots_i * k_i
+
+ m = 1
+ if i > 1:
+ while uppercase_k[i - m] == uppercase_k[i] and i >= m + 1:
+ m += 1
+ sum_total_samples = sum([samples_history[j] for j in range(i - m, i)])
+ n_total_shots = sum([n_shots_history[j] for j in range(i - m, i)])
+
+ else:
+ sum_total_samples = samples
+ n_total_shots = n_shots_i
+
+ a = sum_total_samples / n_total_shots
+
+ if self.method == "chernoff":
+ delta_a = np.sqrt(
+ np.log(2 * upper_bound_t / self.alpha) / 2 / n_total_shots
+ )
+ a_min_i = max(0, a - delta_a)
+ a_max_i = min(1, a + delta_a)
+ else:
+ a_min_i, a_max_i = self.clopper_pearson(
+ sum_total_samples, n_total_shots, alpha=self.alpha / upper_bound_t
+ )
+ a_min.append(a_min_i)
+ a_max.append(a_max_i)
+
+ if up_i:
+ theta_min_i = np.arccos(1 - 2 * a_min_i) / 2 / np.pi
+ theta_max_i = np.arccos(1 - 2 * a_max_i) / 2 / np.pi
+ else:
+ theta_min_i = 1 - np.arccos(1 - 2 * a_max_i) / 2 / np.pi
+ theta_max_i = 1 - np.arccos(1 - 2 * a_min_i) / 2 / np.pi
+
+ theta = min(int(uppercase_k_i * theta_u), int(uppercase_k_i * theta_l))
+
+ theta_u = (theta + theta_max_i) / uppercase_k_i
+ theta_l = (theta + theta_min_i) / uppercase_k_i
+ theta_dif = np.abs(theta_u - theta_l)
+ theta_intervals.append([theta_l, theta_u])
+
+ a_l = np.sin(2 * np.pi * theta_l) ** 2
+ a_u = np.sin(2 * np.pi * theta_u) ** 2
+ a_intervals.append([a_l, a_u])
+
+ result = IterativeAmplitudeEstimationResult()
+ result.alpha = self.alpha
+ result.epsilon_target = self.epsilon
+ result.epsilon_estimated = (a_u - a_l) / 2
+ result.estimate_intervals = a_intervals
+ result.num_oracle_queries = num_oracle_queries
+ result.estimation = (a_u + a_l) / 2
+ result.theta_intervals = theta_intervals
+ result.k_list = k
+ result.ratios = samples_history
+ result.shots = n_shots_history
+
+ return result
+
+
+class IterativeAmplitudeEstimationResult:
+ """The ``IterativeAmplitudeEstimationResult`` result object."""
+
+ def __init__(self):
+ self._alpha = None
+ self._epsilon_target = None
+ self._epsilon_estimated = None
+ self._num_oracle_queries = None
+ self._estimation = None
+ self._estimate_intervals = None
+ self._theta_intervals = None
+ self._k_list = None
+ self._ratios = None
+ self._shots = None
diff --git a/src/qibo/models/qcnn.py b/src/qibo/models/qcnn.py
new file mode 100644
index 000000000..1066971fa
--- /dev/null
+++ b/src/qibo/models/qcnn.py
@@ -0,0 +1,410 @@
+""" This module implements a Quantum Convolutional Neural Network (QCNN) for classification tasks. The QCNN model was originally proposed in: arXiv:1810.03787 _ for the identification of quantum phases.
+
+The QuantumCNN class in this module provides methods to construct the QCNN.
+"""
+
+import numpy as np
+
+from qibo import gates, get_backend
+from qibo.models import Circuit
+
+
+class QuantumCNN:
+ """
+ Model that implements and trains a variational quantum convolutional network (QCNN) for
+ classification tasks.
+ The QCNN model was originally proposed in: `arXiv:1810.03787 `_
+ for the identification of quantum phases.
+
+ Args:
+ nqubits (int): number of qubits of the input states. Currently supports powers of 2.
+ nlayers (int): number of convolutional and pooling layers.
+ nclasses (int): number of classes to be classified. Default setting of 2 (phases).
+ params: initial list of variational parameters. If not provided, all parameters
+ will be initialized to zero.
+ twoqubitansatz (:class:`qibo.models.Circuit`): a two qubit ansatz that can be input by the user to form the two qubit ansatz used in the convolutional circuit.
+ Example:
+ .. testcode::
+
+ import math
+ import numpy as np
+ import random
+ import qibo
+ from qibo.models.qcnn import QuantumCNN
+
+
+ qibo.set_backend("numpy")
+ data = np.random.rand(16)
+ data = data / np.linalg.norm(data)
+ data = [data]
+ labels = [[1]]
+ testbias = np.zeros(1)
+ testangles = [random.uniform(0, 2 * np.pi) for i in range(21 * 2)]
+ init_theta = np.concatenate((testbias, testangles))
+ test_qcnn = QuantumCNN(nqubits=4, nlayers=1, nclasses=2, params=init_theta)
+ testcircuit = test_qcnn._circuit
+ result = test_qcnn.minimize(
+ init_theta, data=data, labels=labels, nshots=10000, method="Powell"
+ )
+ predict0 = test_qcnn.predict(data, nshots=10000)
+
+ """
+
+ def __init__(
+ self,
+ nqubits,
+ nlayers,
+ nclasses=2,
+ params=None,
+ twoqubitansatz=None,
+ copy_init_state=None,
+ ):
+ """
+ Initializes the QuantumCNN object.
+
+ Args:
+ nqubits (int): The number of qubits in the QCNN.
+ nlayers (int): The number of layers in the QCNN.
+ nclasses (int, optional): The number of classes for the classification task. Defaults to 2.
+ params (np.ndarray, optional): The initial parameters for the QCNN. If None, random parameters are generated. Defaults to None.
+ twoqubitansatz (qibo.models.circuit.Circuit, optional): A two-qubit ansatz for the convolutional layers. If None, a default ansatz is used. Defaults to None.
+ copy_init_state (bool, optional): Whether to copy the initial state for each shot in the simulation. If None, the behavior depends on the backend. Defaults to None.
+
+ Raises:
+ ValueError: If nqubits is not larger than 1.
+ """
+
+ self.nclasses = nclasses
+ self.nqubits = nqubits
+ self.nlayers = nlayers
+ self.twoqubitansatz = twoqubitansatz
+
+ if copy_init_state is None:
+ if "qibojit" in get_backend():
+ self.copy_init_state = True
+ else:
+ self.copy_init_state = False
+
+ if self.twoqubitansatz is None:
+ self.nparams_conv = 15
+ else:
+ self.nparams_conv = len(self.twoqubitansatz.get_parameters())
+
+ self.nparams_pool = 6
+ self.nparams_layer = self.nparams_conv + self.nparams_pool
+ self.measured_qubits = int(np.ceil(np.log2(self.nclasses)))
+
+ if self.nqubits <= 1:
+ raise ValueError("nqubits must be larger than 1")
+
+ self._circuit = self.ansatz(nlayers, params=params)
+
+ def quantum_conv_circuit(self, bits, symbols):
+ """
+ Internal helper function to construct a single convolutional layer.
+
+ Args:
+ bits: list or numpy.array with the qubits that the convolutional layer should apply to.
+ symbols: list or numpy.array with the angles to be used in the circuit.
+ Returns:
+ Circuit for a single convolutional layer
+ """
+ c = Circuit(self.nqubits)
+ for first, second in zip(bits[0::2], bits[1::2]):
+ c += self.two_qubit_unitary([first, second], symbols)
+
+ # check that there are more than 2 qubits to prevent double conv
+ if len(bits) > 2:
+ for first, second in zip(bits[1::2], bits[2::2] + [bits[0]]):
+ c += self.two_qubit_unitary([first, second], symbols)
+ return c
+
+ def quantum_pool_circuit(self, source_bits, sink_bits, symbols):
+ """
+ Internal helper function to construct a single pooling layer.
+
+ Args:
+ source_bits: list or numpy.array with the source qubits for the pooling layer.
+ sink_bits: list or numpy.array with the sink qubits for the pooling layer.
+ symbols: list or numpy.array with the angles to be used in the circuit.
+ Returns:
+ Circuit for a single pooling layer
+ """
+ c = Circuit(self.nqubits)
+ for source, sink in zip(source_bits, sink_bits):
+ c += self.two_qubit_pool(source, sink, symbols)
+ return c
+
+ def ansatz(self, nlayers, params=None):
+ """
+ Args:
+ theta: list or numpy.array with the angles to be used in the circuit.
+ nlayers: int number of layers of the varitional circuit ansatz.
+ Returns:
+ Circuit implementing the QCNN variational ansatz.
+ """
+
+ nparams_conv = self.nparams_conv
+ nparams_layer = self.nparams_layer
+
+ if params is not None:
+ symbols = params
+ else:
+ symbols = [0 for _ in range(nlayers * nparams_layer)]
+
+ nbits = self.nqubits
+
+ qubits = [_ for _ in range(nbits)]
+ c = Circuit(self.nqubits)
+ for layer in range(nlayers):
+ conv_start = int(nbits - nbits / (2**layer))
+ pool_start = int(nbits - nbits / (2 ** (layer + 1)))
+ param_start = layer * nparams_layer
+ c += self.quantum_conv_circuit(
+ qubits[conv_start:], symbols[param_start : param_start + nparams_conv]
+ )
+ c += self.quantum_pool_circuit(
+ qubits[conv_start:pool_start],
+ qubits[pool_start:],
+ symbols[param_start + nparams_conv : param_start + nparams_layer],
+ )
+
+ # Measurements
+ c.add(gates.M(*[nbits - 1 - i for i in range(self.measured_qubits)]))
+
+ return c
+
+ def one_qubit_unitary(self, bit, symbols):
+ """
+ Internal helper function to make a circuit enacting a rotation of the bloch sphere about the X,
+ Y and Z axis, that depends on the values in `symbols`.
+
+ Args:
+ bit: the qubit to apply the one-qubit unitaries to
+ symbols: length 3 array containing the parameters
+ Returns:
+ Circuit containing the unitaries added to the specified qubit.
+ """
+ c = Circuit(self.nqubits)
+ c.add(gates.RX(bit, symbols[0]))
+ c.add(gates.RY(bit, symbols[1]))
+ c.add(gates.RZ(bit, symbols[2]))
+
+ return c
+
+ def two_qubit_unitary(self, bits, symbols):
+ """
+ Internal helper function to create a circuit consisting of two qubit unitaries.
+
+ Args:
+ bits: the two qubits to apply the unitaries to
+ symbols: length 15 array containing the parameters
+ Returns:
+ Circuit containing the unitaries added to the specified qubits.
+ """
+
+ if self.twoqubitansatz is None:
+ c = Circuit(self.nqubits)
+ c += self.one_qubit_unitary(bits[0], symbols[0:3])
+ c += self.one_qubit_unitary(bits[1], symbols[3:6])
+ c.add(gates.RZZ(bits[0], bits[1], symbols[6]))
+ c.add(gates.RYY(bits[0], bits[1], symbols[7]))
+ c.add(gates.RXX(bits[0], bits[1], symbols[8]))
+
+ c += self.one_qubit_unitary(bits[0], symbols[9:12])
+ c += self.one_qubit_unitary(bits[1], symbols[12:])
+
+ else:
+ c = Circuit(self.nqubits)
+ c.add(self.twoqubitansatz.on_qubits(bits[0], bits[1]))
+ c.set_parameters(symbols[0 : self.nparams_conv])
+
+ return c
+
+ def two_qubit_pool(self, source_qubit, sink_qubit, symbols):
+ """
+ Internal helper function to create a circuit to do a parameterized 'pooling' operation with controlled unitaries, which
+ attempts to reduce entanglement down from two qubits to just one.
+
+ Args:
+ source_qubit: the control qubit.
+ sink_qubit: the target qubit for the controlled unitaries.
+ symbols: array with 6 elements containing the parameters.
+ Returns:
+ Circuit containing the unitaries added to the specified qubits.
+ """
+ pool_circuit = Circuit(self.nqubits)
+ sink_basis_selector = self.one_qubit_unitary(sink_qubit, symbols[0:3])
+ source_basis_selector = self.one_qubit_unitary(source_qubit, symbols[3:6])
+ pool_circuit += sink_basis_selector
+ pool_circuit += source_basis_selector
+ pool_circuit.add(gates.CNOT(source_qubit, sink_qubit))
+ pool_circuit += sink_basis_selector.invert()
+
+ return pool_circuit
+
+ def set_circuit_params(self, angles, has_bias=False):
+ """
+ Sets the parameters of the QCNN circuit. Can be used to load previously saved or optimized parameters.
+
+ Args:
+ angles: the parameters to be loaded.
+ has_bias: specify whether the list of angles contains the bias.
+
+ """
+ if not has_bias:
+ params = list(angles)
+ else:
+ self._optimal_angles = angles
+ params = list(angles[self.measured_qubits :])
+
+ expanded_params = []
+ nbits = self.nqubits
+ for layer in range(self.nlayers):
+ nleft = nbits / (2**layer)
+ param_start = layer * self.nparams_layer
+ conv_params = params[param_start : param_start + self.nparams_conv]
+ pool_params = params[
+ param_start + self.nparams_conv : param_start + self.nparams_layer
+ ]
+ pool_params += [-pool_params[2], -pool_params[1], -pool_params[0]]
+ expanded_params += conv_params * int(nleft if nleft > 2 else 1)
+ expanded_params += pool_params * int(nleft / 2)
+
+ self._circuit.set_parameters(expanded_params)
+
+ def Classifier_circuit(self, theta):
+ """
+ Args:
+ theta: list or numpy.array with the biases and the angles to be used in the circuit.
+ Returns:
+ Circuit implementing the variational ansatz for angles "theta".
+ """
+ bias = np.array(theta[0 : self.measured_qubits])
+ angles = theta[self.measured_qubits :]
+
+ self.set_circuit_params(angles)
+ return self._circuit
+
+ def Predictions(self, circuit, theta, init_state, nshots=10000):
+ """
+ Args:
+ theta: list or numpy.array with the biases to be used in the circuit.
+ init_state: numpy.array with the quantum state to be classified.
+ nshots: int number of runs of the circuit during the sampling process (default=10000).
+ Returns:
+ numpy.array() with predictions for each qubit, for the initial state.
+ """
+ bias = np.array(theta[0 : self.measured_qubits])
+ if self.copy_init_state:
+ init_state_copy = init_state.copy()
+ else:
+ init_state_copy = init_state
+ circuit_exec = circuit(init_state_copy, nshots)
+ result = circuit_exec.frequencies(binary=False)
+ prediction = np.zeros(self.measured_qubits)
+
+ for qubit in range(self.measured_qubits):
+ for clase in range(self.nclasses):
+ binary = bin(clase)[2:].zfill(self.measured_qubits)
+ prediction[qubit] += result[clase] * (1 - 2 * int(binary[-qubit - 1]))
+
+ return prediction / nshots + bias
+
+ def square_loss(self, labels, predictions):
+ """
+ Args:
+ labels: list or numpy.array with the qubit labels of the quantum states to be classified.
+ predictions: list or numpy.array with the qubit predictions for the quantum states to be classified.
+ Returns:
+ numpy.float32 with the value of the square-loss function.
+ """
+ loss = 0
+ for l, p in zip(labels, predictions):
+ for qubit in range(self.measured_qubits):
+ loss += (l[qubit] - p[qubit]) ** 2
+
+ return loss / len(labels)
+
+ def Cost_function(self, theta, data=None, labels=None, nshots=10000):
+ """
+ Args:
+ theta: list or numpy.array with the biases and the angles to be used in the circuit.
+ data: numpy.array data[page][word] (this is an array of kets).
+ labels: list or numpy.array with the labels of the quantum states to be classified.
+ nshots: int number of runs of the circuit during the sampling process (default=10000).
+ Returns:
+ numpy.float32 with the value of the square-loss function.
+ """
+ circ = self.Classifier_circuit(theta)
+
+ Bias = np.array(theta[0 : self.measured_qubits])
+ predictions = np.zeros(shape=(len(data), self.measured_qubits))
+
+ for i, text in enumerate(data):
+ predictions[i] = self.Predictions(circ, Bias, text, nshots)
+
+ s = self.square_loss(labels, predictions)
+
+ return s
+
+ def minimize(
+ self, init_theta, data=None, labels=None, nshots=10000, method="Powell"
+ ):
+ """
+ Args:
+ init_theta: list or numpy.array with the angles to be used in the circuit.
+ data: the training data to be used in the minimization.
+ labels: the corresponding ground truth for the training data.
+ nshots: int number of runs of the circuit during the sampling process (default=10000).
+ method: str 'classical optimizer for the minimization'. All methods from qibo.optimizers.optimize are suported (default='Powell').
+ Returns:
+ numpy.float64 with value of the minimum found, numpy.ndarray with the optimal angles.
+ """
+ from qibo.optimizers import optimize
+
+ loss, optimal_angles, result = optimize(
+ self.Cost_function, init_theta, args=(data, labels, nshots), method=method
+ )
+
+ self._optimal_angles = optimal_angles
+
+ self.set_circuit_params(optimal_angles[self.measured_qubits :])
+ return loss, optimal_angles
+
+ def Accuracy(self, labels, predictions, sign=True, tolerance=1e-2):
+ """
+ Args:
+ labels: numpy.array with the labels of the quantum states to be classified.
+ predictions: numpy.array with the predictions for the quantum states classified.
+ sign: if True, labels = np.sign(labels) and predictions = np.sign(predictions) (default=True).
+ tolerance: float tolerance level to consider a prediction correct (default=1e-2).
+ Returns:
+ float with the proportion of states classified successfully.
+ """
+ if sign:
+ labels = [np.sign(label) for label in labels]
+ predictions = [np.sign(prediction) for prediction in predictions]
+
+ accur = 0
+ for l, p in zip(labels, predictions):
+ if np.allclose(l, p, rtol=0.0, atol=tolerance):
+ accur += 1
+
+ accur = accur / len(labels)
+
+ return accur
+
+ def predict(self, init_state, nshots=10000):
+ """
+ This function is used to produce predictions on new input state after the model is trained. Currently it only takes in one input data.
+
+ Args:
+ init_state: the input state to be predicted.
+ nshots (default=10000): number of shots.
+ Returns:
+ numpy.array() with predictions for each qubit, for the initial state.
+ """
+ return self.Predictions(
+ self._circuit, self._optimal_angles, init_state, nshots=nshots
+ )
diff --git a/src/qibo/models/qdp/__init__.py b/src/qibo/models/qdp/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/qibo/models/qdp/memory_usage_query.py b/src/qibo/models/qdp/memory_usage_query.py
new file mode 100644
index 000000000..c2911236d
--- /dev/null
+++ b/src/qibo/models/qdp/memory_usage_query.py
@@ -0,0 +1,122 @@
+import numpy as np
+import scipy
+
+from qibo import gates
+from qibo.models.qdp.quantum_dynamic_programming import (
+ MeasurementEmulation,
+ MeasurementReset,
+ SequentialInstruction,
+)
+from qibo.transpiler.unitary_decompositions import two_qubit_decomposition
+
+
+class DensityMatrixExponentiation(SequentialInstruction):
+ """
+ Subclass of AbstractQuantumDynamicProgramming for density matrix exponentiation,
+ where we attempt to instruct the work qubit to do an X rotation, using SWAP gate.
+
+ Args:
+ theta (float): Overall rotation angle.
+ N (int): Number of steps.
+ num_work_qubits (int): Number of work qubits.
+ num_instruction_qubits (int): Number of instruction qubits.
+ number_muq_per_call (int): Number of memory units per call.
+
+ Example:
+ import numpy as np
+ from qibo.models.qdp.dynamic_programming import DensityMatrixExponentiation
+ my_protocol = DensityMatrixExponentiation(theta=np.pi,N=3,num_work_qubits=1,num_instruction_qubits=3,number_muq_per_call=1)
+ my_protocol.memory_call_circuit(num_instruction_qubits_per_query=3)
+ print('DME, q0 is target qubit, q1,q2 and q3 are instruction qubit')
+ print(my_protocol.c.draw())
+ my_protocol.c.execute(nshots=1000).frequencies()
+ """
+
+ def __init__(
+ self, theta, N, num_work_qubits, num_instruction_qubits, number_muq_per_call
+ ):
+ super().__init__(
+ num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ )
+ self.theta = theta # overall rotation angle
+ self.N = N # number of steps
+ self.delta = theta / N # small rotation angle
+ self.id_current_work_reg = self.list_id_work_reg[0]
+
+ def memory_usage_query_circuit(self):
+ """Defines the memory usage query circuit."""
+ delta_swap = scipy.linalg.expm(
+ -1j
+ * gates.SWAP(
+ self.id_current_work_reg, self.id_current_instruction_reg
+ ).matrix()
+ * self.delta
+ )
+ for decomposed_gate in two_qubit_decomposition(
+ self.id_current_work_reg,
+ self.id_current_instruction_reg,
+ unitary=delta_swap,
+ ):
+ self.c.add(decomposed_gate)
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.X(instruction_qubit))
+
+
+class DME_reset(MeasurementReset):
+ """
+ Warning: Functional, but without a way to actually do reset.
+ DME using reset method.
+ """
+
+ def __init__(
+ self, theta, N, num_work_qubits, num_instruction_qubits, number_muq_per_call
+ ):
+ super().__init__(
+ num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ )
+ self.theta = theta # overall rotation angle
+ self.N = N # number of steps
+ self.delta = theta / N # small rotation angle
+ self.id_current_work_reg = self.list_id_work_reg[0]
+
+ def current_register_reset(self):
+ """
+ Resets a single register.
+
+ Args:
+ register (int): The register index.
+ _c = self.c.copy()
+ """
+ # todo: find a way to do reset
+ # result = _c.execute(nshots=1).samples(binary=True)[0][0]
+ result = 1
+ if result == 1:
+ self.c.add(gates.X(self.id_current_instruction_reg))
+ elif result == 0:
+ pass
+ else:
+ print("Warning: qubit wasn't reset")
+
+ def memory_usage_query_circuit(self):
+ """Defines the memory usage query circuit."""
+ delta_SWAP = scipy.linalg.expm(
+ -1j
+ * gates.SWAP(
+ self.id_current_work_reg, self.id_current_instruction_reg
+ ).matrix()
+ * self.delta
+ )
+ for decomposed_gate in two_qubit_decomposition(
+ self.id_current_work_reg,
+ self.id_current_instruction_reg,
+ unitary=delta_SWAP,
+ ):
+ self.c.add(decomposed_gate)
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.X(instruction_qubit))
diff --git a/src/qibo/models/qdp/oblivious_schmidt_decomposition.py b/src/qibo/models/qdp/oblivious_schmidt_decomposition.py
new file mode 100644
index 000000000..36e9b6433
--- /dev/null
+++ b/src/qibo/models/qdp/oblivious_schmidt_decomposition.py
@@ -0,0 +1,196 @@
+import numpy as np
+import scipy
+
+from qibo import gates
+from qibo.models.qdp.oblivious_schmidt_decomposition import *
+from qibo.models.qdp.quantum_dynamic_programming import (
+ AbstractQuantumDynamicProgramming,
+ SequentialInstruction,
+)
+from qibo.transpiler.unitary_decompositions import two_qubit_decomposition
+
+
+def unitary_expm(H, t):
+ """
+ Compute the matrix exponential of the Hamiltonian H scaled by time t.
+
+ Args:
+ H (numpy.ndarray): The Hamiltonian matrix.
+ t (float): Time parameter.
+
+ Returns:
+ numpy.ndarray: The unitary matrix resulting from the exponential of -1j * t * H.
+ """
+ U = scipy.linalg.expm(-1j * t * H)
+ return U
+
+
+def off_diagonal_norm(H):
+ """
+ Compute the Hilbert-Schmidt norm of the off-diagonal part of a Hermitian matrix H.
+
+ Args:
+ H (numpy.ndarray): The Hermitian matrix.
+
+ Returns:
+ float: The Hilbert-Schmidt norm of the off-diagonal part of H.
+ """
+ diagonal_h = np.diag(np.diag(H))
+ off_diag_h = H - diagonal_h
+ off_diag_h_dag = np.asarray(np.asmatrix(off_diag_h).H)
+ return np.sqrt(np.real(np.trace(off_diag_h_dag @ off_diag_h)))
+
+
+class ObliviousSchmidtDecompositionSingleQubit(SequentialInstruction):
+ """
+ Subclass of AbstractQuantumDynamicProgramming for Oblivious Schmidt Decomposition.
+
+ Args:
+ t (float): Total duration.
+ num_work_qubits (int): Number of work qubits.
+ num_instruction_qubits (int): Number of instruction qubits.
+ number_muq_per_call (int, optional): Number of memory units per call. Defaults to 1.
+ """
+
+ def __init__(
+ self, t, num_work_qubits, num_instruction_qubits, number_muq_per_call=1
+ ):
+ super().__init__(
+ num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ )
+ self.t = t
+ self.id_current_work_reg = self.list_id_work_reg[0]
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits by applying the X gate."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.X(instruction_qubit))
+
+ def memory_usage_query_circuit(self):
+ """Defines the memory usage query circuit."""
+ D = np.array([[1, 0], [0, -1]])
+ unitary_D = unitary_expm(D, self.t)
+ self.c.add(gates.Unitary(unitary_D, self.id_current_work_reg))
+
+ delta_swap = unitary_expm(
+ gates.SWAP(
+ self.id_current_work_reg, self.id_current_instruction_reg
+ ).matrix(),
+ self.t,
+ )
+ for decomposed_gate in two_qubit_decomposition(
+ self.id_current_work_reg,
+ self.id_current_instruction_reg,
+ unitary=delta_swap,
+ ):
+ self.c.add(decomposed_gate)
+
+ unitary_minus_D = unitary_expm(-D, self.t)
+ self.c.add(gates.Unitary(unitary_minus_D, self.id_current_work_reg))
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits by applying the RX gate with a pi/2 rotation."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.RX(instruction_qubit, np.pi / 2)) # rho_A
+ # self.c.add(gates.X(instruction_qubit + 1)) # rho_B
+
+
+class TwoQubitsSequentialInstruction(AbstractQuantumDynamicProgramming):
+ """
+ Abstract class for sequential instructions involving two qubits.
+
+ Args:
+ num_instruction_qubits_per_query (int): Number of instruction qubits per query.
+ """
+
+ def memory_call_circuit(self, num_instruction_qubits_per_query):
+ """
+ Executes the memory call circuit. Every instruction qubit is used once then discarded.
+
+ Args:
+ num_instruction_qubits_per_query (int): Number of instruction qubits per query.
+ """
+ current_instruction_index = self.instruction_index(
+ self.id_current_instruction_reg
+ )
+ self.list_id_current_instruction_reg = self.list_id_instruction_reg[
+ current_instruction_index : self.M * num_instruction_qubits_per_query
+ + current_instruction_index
+ ]
+ self.instruction_qubits_initialization()
+ for _register in self.list_id_current_instruction_reg[0::2]:
+ self.memory_usage_query_circuit()
+ self.trace_one_instruction_qubit(_register)
+ if self.instruction_index(_register) + 1 < len(
+ list(self.list_id_current_instruction_reg)
+ ):
+ self.trace_one_instruction_qubit(_register + 1)
+ if self.instruction_index(_register) + 2 < len(
+ list(self.list_id_current_instruction_reg)
+ ):
+ self.increment_current_instruction_register()
+ self.increment_current_instruction_register()
+ self.instruction_reg_delegation()
+
+
+class ObliviousSchmidtDecompositionTwoQubits(TwoQubitsSequentialInstruction):
+ """
+ Subclass of AbstractQuantumDynamicProgramming for Oblivious Schmidt Decomposition involving two qubits.
+
+ Args:
+ t (float): Total duration.
+ num_work_qubits (int): Number of work qubits.
+ num_instruction_qubits (int): Number of instruction qubits.
+ number_muq_per_call (int, optional): Number of memory units per call. Defaults to 1.
+ """
+
+ def __init__(
+ self, t, num_work_qubits, num_instruction_qubits, number_muq_per_call=1
+ ):
+ super().__init__(
+ num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ )
+ self.t = t
+ self.id_current_work_reg = self.list_id_work_reg[0]
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits by applying the X gate."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.X(instruction_qubit))
+
+ def memory_usage_query_circuit(self):
+ """Defines the memory usage query circuit."""
+ Z = np.array([[1, 0], [0, -1]])
+ unitary_Z = unitary_expm(Z, self.t)
+ self.c.add(gates.Unitary(unitary_Z, self.id_current_work_reg))
+ self.c.add(gates.Unitary(unitary_Z, self.id_current_work_reg + 1))
+
+ delta_swap = unitary_expm(
+ gates.SWAP(
+ self.id_current_work_reg, self.id_current_instruction_reg
+ ).matrix(),
+ self.t,
+ )
+ for decomposed_gate in two_qubit_decomposition(
+ self.id_current_work_reg,
+ self.id_current_instruction_reg,
+ unitary=delta_swap,
+ ):
+ self.c.add(decomposed_gate)
+
+ for decomposed_gate in two_qubit_decomposition(
+ self.id_current_work_reg + 1,
+ self.id_current_instruction_reg + 1,
+ unitary=delta_swap,
+ ):
+ self.c.add(decomposed_gate)
+
+ unitary_minus_Z = unitary_expm(-Z, self.t)
+ self.c.add(gates.Unitary(unitary_minus_Z, self.id_current_work_reg))
+ self.c.add(gates.Unitary(unitary_minus_Z, self.id_current_work_reg + 1))
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits by applying the RX gate with a pi/2 rotation."""
+ for instruction_qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.RX(instruction_qubit, np.pi / 2)) # rho_A
+ # self.c.add(gates.X(instruction_qubit + 1)) # rho_B
diff --git a/src/qibo/models/qdp/quantum_dynamic_programming.py b/src/qibo/models/qdp/quantum_dynamic_programming.py
new file mode 100644
index 000000000..fe0170785
--- /dev/null
+++ b/src/qibo/models/qdp/quantum_dynamic_programming.py
@@ -0,0 +1,267 @@
+"""
+Module for Quantum Dynamic Programming Implementation Framework.
+
+Usage:
+ - Instantiate one of the concrete subclasses based on the desired memory handling strategy.
+ - Implement custom memory usage query circuits by subclassing `AbstractQuantumDynamicProgramming` and overriding the `memory_usage_query_circuit` method.
+ - Customize the QDP framework as per application requirements.
+
+Classes:
+ AbstractQuantumDynamicProgramming: Base class representing the QDP implementation framework.
+ - Implements methods for memory usage query, instruction qubit management, and circuit handling.
+ - Must be subclassed to define specific memory handling strategies.
+ SequentialInstruction: Subclass implementing sequential instruction execution strategy.
+ - Executes memory call circuit sequentially for each instruction qubit.
+ MeasurementEmulation: Subclass implementing quantum measurement emulation strategy.
+ - Emulates quantum measurement using rotation gates.
+ MeasurementReset: Subclass implementing memory reset strategy.
+ - Resets instruction qubits based on measurement outcomes.
+
+References:
+ - Son, J., Gluza, M., Takagi, R., & Ng, N. H. Y. (2024).
+ Quantum Dynamic Programming. arXiv preprint arXiv:2403.09187.
+ Retrieved from https://arxiv.org/abs/2403.09187
+ - Kjaergaard, M., Schwartz, M. E., Greene, A., Samach, G. O., Bengtsson, A., O'Keeffe, M., ... Oliver, W. D. (2020).
+ Programming a quantum computer with quantum instructions. arXiv preprint arXiv:2001.08838.
+ Retrieved from https://arxiv.org/abs/2001.08838
+
+"""
+
+import random
+from abc import abstractmethod
+from enum import Enum, auto
+
+import numpy as np
+
+from qibo import gates, models
+from qibo.config import raise_error
+
+
+class QDP_memory_type(Enum):
+ """
+ Enumerated type representing memory types for quantum dynamic programming.
+ """
+
+ default = auto()
+ reset = auto()
+ quantum_measurement_emulation = auto()
+
+
+class AbstractQuantumDynamicProgramming:
+ """
+ Class representing the implementation framework of quantum dynamic programming.
+ - List essential functions. Which one need to be implemented?
+ - What is the intended way of using it?
+
+ Args:
+ num_work_qubits (int): Number of work qubits.
+ num_instruction_qubits (int): Number of instruction qubits.
+ number_muq_per_call (int): Number of memory units per call.
+ QME_rotation_gate (callable): Optional. Rotation gate for quantum measurement emulation.
+
+ Abstract functions:
+ memory_usage_query_circuit: define a memory usage circuit
+ (the circuit to be iterated over)
+ memory_call_circuit: define how memory call work,
+ which instruction qubits to use, how many iterations etc.
+
+ Functions:
+ instruction_qubits_initialization: initialize instruction qubit
+ trace_one_instruction_qubit: trace the instruction qubit, an important step in QDP
+ trace_all_instruction_qubit: trace all instruction qubit
+ instruction_reg_delegation: helper function for memory_call_circuit,
+ define how instruction register is used.
+ increment_current_instruction_register: use the next instruction qubit in the specified list.
+ Instruction qubit does not need to be in order.
+ circuit_reset: Reset the circuit
+ return_circuit: Return the circuit
+ """
+
+ def __init__(
+ self, num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ ):
+ self.num_work_qubits = int(num_work_qubits)
+ self.num_instruction_qubits = int(num_instruction_qubits)
+
+ self.list_id_work_reg = np.arange(0, num_work_qubits, 1)
+ self.list_id_instruction_reg = (
+ np.arange(0, num_instruction_qubits, 1) + num_work_qubits
+ )
+ self.id_current_instruction_reg = self.list_id_instruction_reg[0]
+ self.M = number_muq_per_call
+ self.list_id_current_instruction_reg = self.list_id_instruction_reg
+
+ if circuit is None:
+ self.c = models.Circuit(self.num_work_qubits + self.num_instruction_qubits)
+ else:
+ self.c = circuit
+
+ @abstractmethod
+ def memory_usage_query_circuit(self):
+ """Defines the memory usage query circuit."""
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def memory_call_circuit(self):
+ """Executes the memory call circuit based on the selected memory type."""
+ raise_error(NotImplementedError)
+
+ def instruction_qubits_initialization(self):
+ """Initializes the instruction qubits."""
+ pass
+
+ def trace_one_instruction_qubit(self, qubit_reg):
+ """Traces the user specified instruction qubit."""
+ self.c.add(gates.M(qubit_reg))
+
+ def trace_all_instruction_qubit(self):
+ """Traces all instruction qubits."""
+ for qubit in self.list_id_current_instruction_reg:
+ self.c.add(gates.M(qubit))
+
+ def instruction_reg_delegation(self):
+ """Uses a work qubit as an instruction qubit."""
+ pass
+
+ def instruction_index(self, id_reg):
+ return list(self.list_id_instruction_reg).index(id_reg)
+
+ def increment_current_instruction_register(self):
+ """Increments the current instruction register index."""
+ current_instruction_index = self.instruction_index(
+ self.id_current_instruction_reg
+ )
+ self.id_current_instruction_reg = list(self.list_id_instruction_reg)[
+ current_instruction_index + 1
+ ]
+
+ def circuit_reset(self):
+ """Resets the entire quantum circuit."""
+ self.c = models.Circuit(self.num_work_qubits + self.num_instruction_qubits)
+
+ def return_circuit(self):
+ """Return the whole circuit"""
+ return self.c
+
+
+class SequentialInstruction(AbstractQuantumDynamicProgramming):
+ def memory_call_circuit(self, num_instruction_qubits_per_query):
+ """
+ Executes the memory call circuit. Every instruction qubit is used once then discarded.
+
+ Args:
+ num_instruction_qubits_per_query (int): Number of instruction qubits per query.
+ """
+ current_instruction_index = self.instruction_index(
+ self.id_current_instruction_reg
+ )
+ self.list_id_current_instruction_reg = self.list_id_instruction_reg[
+ current_instruction_index : self.M * num_instruction_qubits_per_query
+ + current_instruction_index
+ ]
+ self.instruction_qubits_initialization()
+ for _register in self.list_id_current_instruction_reg:
+ self.memory_usage_query_circuit()
+ self.trace_one_instruction_qubit(_register)
+ if self.instruction_index(_register) + 1 < len(
+ list(self.list_id_current_instruction_reg)
+ ):
+ self.increment_current_instruction_register()
+ self.instruction_reg_delegation()
+
+
+class MeasurementEmulation(AbstractQuantumDynamicProgramming):
+ def __init__(
+ self,
+ num_work_qubits,
+ num_instruction_qubits,
+ number_muq_per_call,
+ QME_rotation_gate,
+ circuit=None,
+ ):
+ super().__init__(
+ num_work_qubits, num_instruction_qubits, number_muq_per_call, circuit=None
+ )
+ self.QME_rotation_gate = QME_rotation_gate
+
+ def quantum_measurement_emulation(self, register):
+ """
+ Performs quantum measurement emulation,
+ where a rotation gate or identity gate is applied randomly with p = 0.5
+
+ Args:
+ register (int): The register index.
+ rotation_gate (callable): The rotation gate about an axis parallel
+ to the instruction qubit(s).
+ """
+ coin_flip = random.choice([0, 1])
+ if coin_flip == 0:
+ QME_gate = self.QME_rotation_gate(np.pi, register)
+ elif coin_flip == 1:
+ QME_gate = gates.I(register)
+ self.c.add(QME_gate)
+
+ def memory_call_circuit(self):
+ """
+ Executes the memory call circuit based on quantum measurement emulation.
+
+ Args:
+ num_instruction_qubits_per_query (int): Number of instruction qubits per query.
+ """
+ if self.QME_rotation_gate is None:
+ raise TypeError("Rotation gate for QME protocol is not set.")
+ for _register_use in range(self.M):
+ self.memory_usage_query_circuit()
+ self.trace_one_instruction_qubit(self.id_current_instruction_reg)
+ self.quantum_measurement_emulation(self.id_current_instruction_reg)
+ self.instruction_reg_delegation()
+
+
+class MeasurementReset(AbstractQuantumDynamicProgramming):
+
+ def memory_call_circuit(self, num_instruction_qubits_per_query):
+ """
+ Executes the memory call circuit, where each qubit is reset after usage.
+
+ Args:
+ num_instruction_qubits_per_query (int): Number of instruction qubits per query.
+ """
+ self.instruction_qubits_initialization()
+ for _register_use in range(self.M):
+ self.memory_usage_query_circuit()
+ self.trace_one_instruction_qubit(self.id_current_instruction_reg)
+ self.current_register_reset()
+ self.instruction_reg_delegation()
+
+ @abstractmethod
+ def current_register_reset(self):
+ """
+ Resets a single register.
+
+ Args:
+ register (int): The register index.
+ _c = self.c.copy()
+ Ideas:
+ #result = _c.execute(nshots=1).samples(binary=True)[0][0]
+ result = 1
+ if result == 1:
+ self.c.add(gates.X(self.id_current_instruction_reg))
+ elif result == 0:
+ pass
+ else:
+ print("Warning: qubit wasn't reset")
+ """
+ raise_error(NotImplementedError)
+
+ @abstractmethod
+ def all_register_reset(self):
+ # todo: find way to do reset gate: disruptive measurement?
+ """
+ Resets all instruction registers.
+
+ Example:
+ for qubit in self.num_instruction_qubits:
+ self.c.add(gates.M(qubit))
+ self.id_current_instruction_reg = 0
+ """
+ raise_error(NotImplementedError)
diff --git a/src/qibo/models/qft.py b/src/qibo/models/qft.py
new file mode 100644
index 000000000..d9426ff4c
--- /dev/null
+++ b/src/qibo/models/qft.py
@@ -0,0 +1,86 @@
+import math
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.models.circuit import Circuit
+
+
+def QFT(nqubits: int, with_swaps: bool = True, accelerators=None, **kwargs) -> Circuit:
+ """Creates a circuit that implements the Quantum Fourier Transform.
+
+ Args:
+ nqubits (int): number of qubits in the circuit.
+ with_swaps (bool, optional): If ``True``, uses :class:`qibo.gates.SWAP` gates
+ at the end of the circuit so that the qubit order in the final
+ state is the same as the initial state. Defauts to ``True``.
+ accelerators (dict, optional): Accelerator device dictionary in order to use a
+ distributed circuit. If ``None``, a simple (non-distributed)
+ circuit will be used.
+ kwargs (dict, optional): Additional arguments used to initialize
+ :class:`qibo.models.Circuit`. For details, see the documentation
+ of :class:`qibo.models.circuit.Circuit`.
+
+ Returns:
+ :class:`qibo.models.Circuit`: implementation of the Quantum Fourier Transform.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo.models import QFT
+ nqubits = 6
+ c = QFT(nqubits)
+ # Random normalized initial state vector
+ init_state = np.random.random(2 ** nqubits) + 1j * np.random.random(2 ** nqubits)
+ init_state = init_state / np.sqrt((np.abs(init_state)**2).sum())
+ # Execute the circuit
+ final_state = c(init_state)
+ """
+ if accelerators is not None:
+ if not with_swaps:
+ raise_error(
+ NotImplementedError,
+ "Distributed QFT is only implemented " "with SWAPs.",
+ )
+ return _DistributedQFT(nqubits, accelerators, **kwargs)
+
+ circuit = Circuit(nqubits, **kwargs)
+ for i1 in range(nqubits):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, nqubits):
+ theta = math.pi / 2 ** (i2 - i1)
+ circuit.add(gates.CU1(i2, i1, theta))
+
+ if with_swaps:
+ for i in range(nqubits // 2):
+ circuit.add(gates.SWAP(i, nqubits - i - 1))
+
+ return circuit
+
+
+def _DistributedQFT(nqubits, accelerators=None, **kwargs):
+ """QFT with the order of gates optimized for reduced multi-device communication."""
+ circuit = Circuit(nqubits, accelerators, **kwargs)
+ icrit = nqubits // 2 + nqubits % 2
+ if accelerators is not None:
+ circuit.global_qubits = range(circuit.nlocal, nqubits) # pylint: disable=E1101
+ if icrit < circuit.nglobal: # pylint: disable=E1101
+ raise_error(
+ NotImplementedError,
+ f"Cannot implement QFT for {nqubits} qubits "
+ + f"using {circuit.nglobal} global qubits.",
+ ) # pylint: disable=E1101
+
+ for i1 in range(nqubits):
+ if i1 < icrit:
+ i1eff = i1
+ else:
+ i1eff = nqubits - i1 - 1
+ circuit.add(gates.SWAP(i1, i1eff))
+
+ circuit.add(gates.H(i1eff))
+ for i2 in range(i1 + 1, nqubits):
+ theta = math.pi / 2 ** (i2 - i1)
+ circuit.add(gates.CU1(i2, i1eff, theta))
+
+ return circuit
diff --git a/src/qibo/models/tsp.py b/src/qibo/models/tsp.py
new file mode 100644
index 000000000..250360aa8
--- /dev/null
+++ b/src/qibo/models/tsp.py
@@ -0,0 +1,173 @@
+import numpy as np
+
+from qibo import gates
+from qibo.hamiltonians import SymbolicHamiltonian
+from qibo.models.circuit import Circuit
+from qibo.symbols import X, Y, Z
+
+
+def calculate_two_to_one(num_cities):
+ return np.arange(num_cities**2).reshape(num_cities, num_cities)
+
+
+def tsp_phaser(distance_matrix, backend=None):
+ num_cities = distance_matrix.shape[0]
+ two_to_one = calculate_two_to_one(num_cities)
+ form = 0
+ for i in range(num_cities):
+ for u in range(num_cities):
+ for v in range(num_cities):
+ if u != v:
+ form += (
+ distance_matrix[u, v]
+ * Z(int(two_to_one[u, i]))
+ * Z(int(two_to_one[v, (i + 1) % num_cities]))
+ )
+ ham = SymbolicHamiltonian(form, backend=backend)
+ return ham
+
+
+def tsp_mixer(num_cities, backend=None):
+ two_to_one = calculate_two_to_one(num_cities)
+ splus = lambda u, i: X(int(two_to_one[u, i])) + 1j * Y(int(two_to_one[u, i]))
+ sminus = lambda u, i: X(int(two_to_one[u, i])) - 1j * Y(int(two_to_one[u, i]))
+ form = 0
+ for i in range(num_cities):
+ for u in range(num_cities):
+ for v in range(num_cities):
+ if u != v:
+ form += splus(u, i) * splus(v, (i + 1) % num_cities) * sminus(
+ u, (i + 1) % num_cities
+ ) * sminus(v, i) + sminus(u, i) * sminus(
+ v, (i + 1) % num_cities
+ ) * splus(
+ u, (i + 1) % num_cities
+ ) * splus(
+ v, i
+ )
+ ham = SymbolicHamiltonian(form, backend=backend)
+ return ham
+
+
+class TSP:
+ """
+ The travelling salesman problem (also called the travelling salesperson problem or TSP)
+ asks the following question: "Given a list of cities and the distances between each pair of cities,
+ what is the shortest possible route for a salesman to visit each city exactly once and return to the origin city?"
+ It is an NP-hard problem in combinatorial optimization. It is also important in theoretical computer science and
+ operations research.
+
+ This is a TSP class that enables us to implement TSP according to
+ `arxiv:1709.03489 `_ by Hadfield (2017).
+
+ Args:
+ distance_matrix: a numpy matrix encoding the distance matrix.
+ backend: Backend to use for calculations. If not given the global backend will be used.
+
+ Example:
+ .. testcode::
+
+ from qibo.models.tsp import TSP
+ import numpy as np
+ from collections import defaultdict
+ from qibo import gates
+ from qibo.models import QAOA
+ from qibo.result import CircuitResult
+
+
+ def convert_to_standard_Cauchy(config):
+ m = int(np.sqrt(len(config)))
+ cauchy = [-1] * m # Cauchy's notation for permutation, e.g. (1,2,0) or (2,0,1)
+ for i in range(m):
+ for j in range(m):
+ if config[m * i + j] == '1':
+ cauchy[j] = i # citi i is in slot j
+ for i in range(m):
+ if cauchy[i] == 0:
+ cauchy = cauchy[i:] + cauchy[:i]
+ return tuple(cauchy) # now, the cauchy notation for permutation begins with 0
+
+
+ def evaluate_dist(cauchy):
+ '''
+ Given a permutation of 0 to n-1, we compute the distance of the tour
+
+ '''
+ m = len(cauchy)
+ return sum(distance_matrix[cauchy[i]][cauchy[(i+1)%m]] for i in range(m))
+
+
+ def qaoa_function_of_layer(layer, distance_matrix):
+ '''
+ This is a function to study the impact of the number of layers on QAOA, it takes
+ in the number of layers and compute the distance of the mode of the histogram obtained
+ from QAOA
+
+ '''
+ small_tsp = TSP(distance_matrix)
+ obj_hamil, mixer = small_tsp.hamiltonians()
+ qaoa = QAOA(obj_hamil, mixer=mixer)
+ best_energy, final_parameters, extra = qaoa.minimize(initial_p=[0.1] * layer,
+ initial_state=initial_state, method='BFGS')
+ qaoa.set_parameters(final_parameters)
+ quantum_state = qaoa.execute(initial_state)
+ circuit = Circuit(9)
+ circuit.add(gates.M(*range(9)))
+ result = CircuitResult(quantum_state, circuit.measurements, small_tsp.backend, nshots=1000)
+ freq_counter = result.frequencies()
+ # let's combine freq_counter here, first convert each key and sum up the frequency
+ cauchy_dict = defaultdict(int)
+ for freq_key in freq_counter:
+ standard_cauchy_key = convert_to_standard_Cauchy(freq_key)
+ cauchy_dict[standard_cauchy_key] += freq_counter[freq_key]
+ max_key = max(cauchy_dict, key=cauchy_dict.get)
+ return evaluate_dist(max_key)
+
+ np.random.seed(42)
+ num_cities = 3
+ distance_matrix = np.array([[0, 0.9, 0.8], [0.4, 0, 0.1],[0, 0.7, 0]])
+ distance_matrix = distance_matrix.round(1)
+ small_tsp = TSP(distance_matrix)
+ initial_parameters = np.random.uniform(0, 1, 2)
+ initial_state = small_tsp.prepare_initial_state([i for i in range(num_cities)])
+ qaoa_function_of_layer(2, distance_matrix)
+
+ """
+
+ def __init__(self, distance_matrix, backend=None):
+ from qibo.backends import _check_backend
+
+ self.backend = _check_backend(backend)
+
+ self.distance_matrix = distance_matrix
+ self.num_cities = distance_matrix.shape[0]
+ self.two_to_one = calculate_two_to_one(self.num_cities)
+
+ def hamiltonians(self):
+ """
+ Returns:
+ The pair of Hamiltonian describes the phaser hamiltonian
+ and the mixer hamiltonian.
+
+ """
+ return (
+ tsp_phaser(self.distance_matrix, backend=self.backend),
+ tsp_mixer(self.num_cities, backend=self.backend),
+ )
+
+ def prepare_initial_state(self, ordering):
+ """
+ To run QAOA by Hadsfield, we need to start from a valid permutation function to ensure feasibility.
+
+ Args:
+ ordering (array): A list describing permutation from 0 to n-1
+
+ Returns:
+ An initial state that is used to start TSP QAOA.
+
+ """
+ c = Circuit(len(ordering) ** 2)
+ for i in range(len(ordering)):
+ c.add(gates.X(int(self.two_to_one[ordering[i], i])))
+ result = self.backend.execute_circuit(c)
+ return result.state()
diff --git a/src/qibo/models/utils.py b/src/qibo/models/utils.py
new file mode 100644
index 000000000..dce386af5
--- /dev/null
+++ b/src/qibo/models/utils.py
@@ -0,0 +1,216 @@
+import numpy as np
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.models.circuit import Circuit
+
+
+def convert_bit_to_energy(hamiltonian, bitstring):
+ """
+ Given a binary string and a hamiltonian, we compute the corresponding energy.
+ make sure the bitstring is of the right length
+ """
+ n = len(bitstring)
+ c = Circuit(n)
+ active_bit = [i for i in range(n) if bitstring[i] == "1"]
+ for i in active_bit:
+ c.add(gates.X(i))
+ result = c() # this is an execution result, a quantum state
+ return hamiltonian.expectation(result.state())
+
+
+def convert_state_to_count(state):
+ """
+ This is a function that convert a quantum state to a dictionary keeping track of
+ energy and its frequency.
+ """
+ return np.abs(state) ** 2
+
+
+def compute_cvar(probabilities, values, alpha, threshold=0.001):
+ """
+ Auxilliary method to computes CVaR for given probabilities, values, and confidence level.
+
+ Args:
+ probabilities (list): list/array of probabilities
+ values (list): list/array of corresponding values
+ alpha (float): confidence level
+ threshold (float): a small positive number to avoid division by zero.
+
+ Returns:
+ CVaR
+ """
+ sorted_indices = np.argsort(values)
+ probs = np.array(probabilities)[sorted_indices]
+ vals = np.array(values)[sorted_indices]
+ cum_probs = np.cumsum(probs)
+ exceed_index = np.searchsorted(cum_probs, alpha, side="right")
+ cvar = np.sum(probs[:exceed_index] * vals[:exceed_index]) / max(
+ cum_probs[exceed_index - 1], threshold
+ ) # avodiing division by 0
+ return cvar
+
+
+def cvar(hamiltonian, state, alpha=0.1):
+ """
+ Given the hamiltonian and state, this function estimate the
+ corresponding cvar function
+ """
+ counts = convert_state_to_count(state)
+ probabilities = np.zeros(len(counts))
+ values = np.zeros(len(counts))
+ m = int(np.log2(state.size))
+ for i, p in enumerate(counts):
+ values[i] = convert_bit_to_energy(hamiltonian, bin(i)[2:].zfill(m))
+ probabilities[i] = p
+ cvar_ans = compute_cvar(probabilities, values, alpha)
+ return cvar_ans
+
+
+def gibbs(hamiltonian, state, eta=0.1):
+ """
+ Given the hamiltonian and the state, and optional eta value
+ it estimate the gibbs function value.
+ """
+ counts = convert_state_to_count(state)
+ avg = 0
+ sum_count = 0
+ m = int(np.log2(state.size))
+ for bitstring, count in enumerate(counts):
+ obj = convert_bit_to_energy(hamiltonian, bin(bitstring)[2:].zfill(m))
+ avg += np.exp(-eta * obj)
+ sum_count += count
+ return -np.log(avg / sum_count)
+
+
+def initialize(nqubits: int, basis=gates.Z, eigenstate="+"):
+ """This function returns a circuit that prepeares all the
+ qubits in a specific state.
+
+ Args:
+ - nqubits (int): Number of qubit in the circuit.
+ - baisis (gates): Can be a qibo gate or a callable that accepts a qubit,
+ the default value is `gates.Z`.
+ - eigenstate (str): Specify which eigenstate of the operator defined in
+ `basis` will be the qubits' state. The default value is "+". Regarding the eigenstates
+ of `gates.Z`, the `+` eigenstate is mapped in the zero state and the `-` eigenstate in the one state.
+ """
+ circuit_basis = Circuit(nqubits)
+ circuit_eigenstate = Circuit(nqubits)
+ if eigenstate == "-":
+ for i in range(nqubits):
+ circuit_eigenstate.add(gates.X(i))
+ elif eigenstate != "+":
+ raise_error(NotImplementedError, f"Invalid eigenstate {eigenstate}")
+
+ for i in range(nqubits):
+ value = basis(i).basis_rotation()
+ if value is not None:
+ circuit_basis.add(value)
+ circuit_basis = circuit_basis.invert()
+ return circuit_eigenstate + circuit_basis
+
+
+def calculate_fourier_coeffs_unfiltered(input_function, frequency_degree):
+ """Calculates the Fourier spectrum for a periodic function or quantum circuit within the range of the specified frequency degree.
+
+ This function blindly computes the coefficients without applying any filtering and serves as a helper for the main
+ ``fourier_coefficients`` function.
+
+ Args:
+ input_function (callable): A function that takes a 1D array of scalar inputs.
+ frequency_degree (int or tuple[int]): The maximum frequency degree for which the Fourier coefficients will be computed.
+ For a degree :math:`d`, the coefficients from frequencies :math:`-d, -d+1,...0,..., d-1, d` will be computed.
+
+ Returns:
+ array[complex]: The Fourier coefficients of the input function up to the specified degree.
+ """
+ frequency_degree = np.array(frequency_degree)
+ number_of_coefficients = 2 * frequency_degree + 1
+
+ # Create ranges of indices for each dimension
+ index_ranges = [np.arange(-d, d + 1) for d in frequency_degree]
+
+ # Generate all combinations of indices
+ def product(*args):
+ """Returns the cartesian product of the input iterables"""
+ pools = [tuple(pool) for pool in args]
+ result = [()]
+ for pool in pools:
+ result = [x + (y,) for x in result for y in pool]
+ yield from result
+
+ indices = product(*index_ranges)
+
+ function_discretized = np.zeros(shape=tuple(number_of_coefficients))
+ spacing = (2 * np.pi) / number_of_coefficients
+
+ # Evaluate the function at each sampling point
+ for index in indices:
+ sampling_point = spacing * np.array(index)
+ function_discretized[index] = input_function(sampling_point)
+
+ # Compute the Fourier coefficients using the Fast Fourier Transform (FFT)
+ coefficients = np.fft.fftn(function_discretized) / function_discretized.size
+ return coefficients
+
+
+def fourier_coefficients(
+ f, n_inputs, degree, lowpass_filter=True, filter_threshold=None
+):
+ """Calculates the Fourier coefficients of a multivariate function up to a specified degree.
+ This function can also compute the Fourier series for the expectation value of a quantum circuit.
+
+ Args:
+ f (callable): The input function to compute the Fourier coefficients for.
+ n_inputs (int): The number of inputs (dimensions) of the function.
+ degree (int or tuple[int]): The maximum degree of the Fourier series expansion.
+ lowpass_filter (bool, optional): Flag to indicate whether to apply a low-pass filter to the coefficients. Default is True.
+ filter_threshold (int or tuple[int], optional): The filter threshold for each input dimension. Default is None.
+
+ Returns:
+ array[complex]: The Fourier coefficients of the input function up to the specified degree.
+ """
+ if isinstance(degree, int):
+ degree = (degree,) * n_inputs
+ elif len(degree) != n_inputs:
+ raise ValueError("The number of provided degrees must match n_inputs.")
+ if not lowpass_filter:
+ return calculate_fourier_coeffs_unfiltered(f, degree)
+ if filter_threshold is None:
+ filter_threshold = tuple(2 * d for d in degree)
+ elif isinstance(filter_threshold, int):
+ filter_threshold = (filter_threshold,) * n_inputs
+ elif len(filter_threshold) != n_inputs:
+ raise ValueError(
+ "The number of provided filter thresholds must match n_inputs."
+ )
+
+ # Calculate unfiltered Fourier coefficients
+ unfiltered_coeffs = calculate_fourier_coeffs_unfiltered(f, filter_threshold)
+ # Shift the unfiltered coefficients
+ shifted_unfiltered_coeffs = np.fft.fftshift(unfiltered_coeffs)
+ shifted_filtered_coeffs = shifted_unfiltered_coeffs.copy()
+
+ # Iterate to remove excess coefficients
+ for axis in reversed(range(n_inputs)):
+ num_excess = filter_threshold[axis] - degree[axis]
+ slice_object = slice(
+ num_excess, shifted_filtered_coeffs.shape[axis] - num_excess
+ )
+ shifted_filtered_coeffs = np.take(
+ shifted_filtered_coeffs,
+ np.arange(shifted_filtered_coeffs.shape[axis])[slice_object],
+ axis=axis,
+ )
+
+ # Shift the filtered coefficients back
+ filtered_coeffs = np.fft.ifftshift(shifted_filtered_coeffs)
+ return filtered_coeffs
+
+
+def vqe_loss(params, circuit, hamiltonian):
+ circuit.set_parameters(params)
+ result = hamiltonian.backend.execute_circuit(circuit)
+ final_state = result.state()
+ return hamiltonian.expectation(final_state)
diff --git a/src/qibo/models/variational.py b/src/qibo/models/variational.py
new file mode 100644
index 000000000..7bdc828ba
--- /dev/null
+++ b/src/qibo/models/variational.py
@@ -0,0 +1,670 @@
+import numpy as np
+
+from qibo.config import raise_error
+from qibo.models.evolution import StateEvolution
+from qibo.models.utils import vqe_loss
+
+
+class VQE:
+ """This class implements the variational quantum eigensolver algorithm.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Circuit that
+ implements the variaional ansatz.
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): Hamiltonian object.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import gates, models, hamiltonians
+ # create circuit ansatz for two qubits
+ circuit = models.Circuit(2)
+ circuit.add(gates.RY(0, theta=0))
+ # create XXZ Hamiltonian for two qubits
+ hamiltonian = hamiltonians.XXZ(2)
+ # create VQE model for the circuit and Hamiltonian
+ vqe = models.VQE(circuit, hamiltonian)
+ # optimize using random initial variational parameters
+ initial_parameters = np.random.uniform(0, 2, 1)
+ vqe.minimize(initial_parameters)
+ """
+
+ from qibo import optimizers
+
+ def __init__(self, circuit, hamiltonian):
+ """Initialize circuit ansatz and hamiltonian."""
+ self.circuit = circuit
+ self.hamiltonian = hamiltonian
+ self.backend = hamiltonian.backend
+
+ def minimize(
+ self,
+ initial_state,
+ method="Powell",
+ loss_func=None,
+ jac=None,
+ hess=None,
+ hessp=None,
+ bounds=None,
+ constraints=(),
+ tol=None,
+ callback=None,
+ options=None,
+ compile=False,
+ processes=None,
+ ):
+ """Search for parameters which minimizes the hamiltonian expectation.
+
+ Args:
+ initial_state (array): a initial guess for the parameters of the
+ variational circuit.
+ method (str): the desired minimization method.
+ See :meth:`qibo.optimizers.optimize` for available optimization
+ methods.
+ loss (callable): loss function, the default one is :func:`qibo.models.utils.vqe_loss`.
+ jac (dict): Method for computing the gradient vector for scipy optimizers.
+ hess (dict): Method for computing the hessian matrix for scipy optimizers.
+ hessp (callable): Hessian of objective function times an arbitrary
+ vector for scipy optimizers.
+ bounds (sequence or Bounds): Bounds on variables for scipy optimizers.
+ constraints (dict): Constraints definition for scipy optimizers.
+ tol (float): Tolerance of termination for scipy optimizers.
+ callback (callable): Called after each iteration for scipy optimizers.
+ options (dict): a dictionary with options for the different optimizers.
+ compile (bool): whether the TensorFlow graph should be compiled.
+ processes (int): number of processes when using the paralle BFGS method.
+
+ Return:
+ The final expectation value.
+ The corresponding best parameters.
+ The optimization result object. For scipy methods it returns
+ the ``OptimizeResult``, for ``'cma'`` the ``CMAEvolutionStrategy.result``,
+ and for ``'sgd'`` the options used during the optimization.
+ """
+ if loss_func is None:
+ loss_func = vqe_loss
+ if compile:
+ loss = self.hamiltonian.backend.compile(loss_func)
+ else:
+ loss = loss_func
+
+ if method == "cma":
+ # TODO: check if we can use this shortcut
+ # dtype = getattr(self.hamiltonian.backend.np, self.hamiltonian.backend._dtypes.get('DTYPE'))
+ dtype = self.hamiltonian.backend.np.float64
+ loss = (
+ (lambda p, c, h: loss_func(p, c, h).item())
+ if str(dtype) == "torch.float64"
+ else (lambda p, c, h: dtype(loss_func(p, c, h)))
+ )
+ elif method != "sgd":
+ loss = lambda p, c, h: self.hamiltonian.backend.to_numpy(loss_func(p, c, h))
+ result, parameters, extra = self.optimizers.optimize(
+ loss,
+ initial_state,
+ args=(self.circuit, self.hamiltonian),
+ method=method,
+ jac=jac,
+ hess=hess,
+ hessp=hessp,
+ bounds=bounds,
+ constraints=constraints,
+ tol=tol,
+ callback=callback,
+ options=options,
+ compile=compile,
+ processes=processes,
+ backend=self.hamiltonian.backend,
+ )
+ self.circuit.set_parameters(parameters)
+ return result, parameters, extra
+
+ def energy_fluctuation(self, state):
+ """
+ Evaluate energy fluctuation
+
+ .. math::
+ \\Xi_{k}(\\mu) = \\sqrt{\\langle\\mu|\\hat{H}^2|\\mu\\rangle - \\langle\\mu|\\hat{H}|\\mu\\rangle^2} \\,
+
+ for a given state :math:`|\\mu\\rangle`.
+
+ Args:
+ state (np.ndarray): quantum state to be used to compute the energy fluctuation with H.
+ """
+ return self.hamiltonian.energy_fluctuation(state)
+
+
+class AAVQE:
+ """This class implements the Adiabatically Assisted Variational Quantum Eigensolver
+ algorithm. See https://arxiv.org/abs/1806.02287.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): variational ansatz.
+ easy_hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): initial Hamiltonian object.
+ problem_hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): problem Hamiltonian object.
+ s (callable): scheduling function of time that defines the adiabatic
+ evolution. It must verify boundary conditions: s(0) = 0 and s(1) = 1.
+ nsteps (float): number of steps of the adiabatic evolution.
+ t_max (float): total time evolution.
+ bounds_tolerance (float): tolerance for checking s(0) = 0 and s(1) = 1.
+ time_tolerance (float): tolerance for checking if time is greater than t_max.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import gates, models, hamiltonians
+ # create circuit ansatz for two qubits
+ circuit = models.Circuit(2)
+ circuit.add(gates.RY(0, theta=0))
+ circuit.add(gates.RY(1, theta=0))
+ # define the easy and the problem Hamiltonians.
+ easy_hamiltonian=hamiltonians.X(2)
+ problem_hamiltonian=hamiltonians.XXZ(2)
+ # define a scheduling function with only one parameter
+ # and boundary conditions s(0) = 0, s(1) = 1
+ s = lambda t: t
+ # create AAVQE model
+ aavqe = models.AAVQE(circuit, easy_hamiltonian, problem_hamiltonian,
+ s, nsteps=10, t_max=1)
+ # optimize using random initial variational parameters
+ np.random.seed(0)
+ initial_parameters = np.random.uniform(0, 2*np.pi, 2)
+ ground_energy, params = aavqe.minimize(initial_parameters)
+ """
+
+ def __init__(
+ self,
+ circuit,
+ easy_hamiltonian,
+ problem_hamiltonian,
+ s,
+ nsteps=10,
+ t_max=1,
+ bounds_tolerance=1e-7,
+ time_tolerance=1e-7,
+ ):
+ if nsteps <= 0: # pragma: no cover
+ raise_error(
+ ValueError,
+ f"Number of steps nsteps should be positive but is {nsteps}.",
+ )
+ if t_max <= 0: # pragma: no cover
+ raise_error(
+ ValueError,
+ f"Maximum time t_max should be positive but is {t_max}.",
+ )
+ if easy_hamiltonian.nqubits != problem_hamiltonian.nqubits: # pragma: no cover
+ raise_error(
+ ValueError,
+ f"The easy Hamiltonian has {easy_hamiltonian.nqubits} qubits "
+ + f"while problem Hamiltonian has {problem_hamiltonian.nqubits}.",
+ )
+
+ self.ATOL = bounds_tolerance
+ self.ATOL_TIME = time_tolerance
+
+ self._circuit = circuit
+ self._h0 = easy_hamiltonian
+ self._h1 = problem_hamiltonian
+ self._nsteps = nsteps
+ self._t_max = t_max
+ self._dt = 1.0 / (nsteps - 1)
+
+ self._schedule = None
+ nparams = s.__code__.co_argcount
+ if not nparams == 1: # pragma: no cover
+ raise_error(
+ ValueError,
+ "Scheduling function must take only one argument,"
+ + f"but the function proposed takes {nparams}.",
+ )
+ self.set_schedule(s)
+
+ def set_schedule(self, func):
+ """Set scheduling function s(t) as func."""
+ # check boundary conditions
+ s0 = func(0)
+ if abs(s0) > self.ATOL: # pragma: no cover
+ raise_error(ValueError, f"s(0) should be 0 but it is {s0}.")
+ s1 = func(1)
+ if abs(s1 - 1) > self.ATOL: # pragma: no cover
+ raise_error(ValueError, f"s(1) should be 1 but it is {s1}.")
+ self._schedule = func
+
+ def schedule(self, t):
+ """Returns scheduling function evaluated at time t: s(t/Tmax)."""
+ if self._schedule is None: # pragma: no cover
+ raise_error(ValueError, "Cannot access scheduling before it is set.")
+ if (t - self._t_max) > self.ATOL_TIME: # pragma: no cover
+ raise_error(
+ ValueError,
+ f"t cannot be greater than {self._t_max}, but it is {t}.",
+ )
+
+ s = self._schedule(t / self._t_max)
+ if (abs(s) - 1) > self.ATOL: # pragma: no cover
+ raise_error(ValueError, f"s cannot be greater than 1 but it is {s}.")
+ return s
+
+ def hamiltonian(self, t):
+ """Returns the adiabatic evolution Hamiltonian at a given time."""
+ if (t - self._t_max) > self.ATOL: # pragma: no cover
+ raise_error(
+ ValueError,
+ f"t cannot be greater than {self._t_max}, but it is {t}.",
+ )
+ # boundary conditions s(0)=0, s(total_time)=1
+ st = self.schedule(t)
+ return self._h0 * (1 - st) + self._h1 * st
+
+ def minimize(
+ self,
+ params,
+ method="BFGS",
+ jac=None,
+ hess=None,
+ hessp=None,
+ bounds=None,
+ constraints=(),
+ tol=None,
+ options=None,
+ compile=False,
+ processes=None,
+ ):
+ """
+ Performs minimization to find the ground state of the problem Hamiltonian.
+
+ Args:
+ params (np.ndarray or list): initial guess for the parameters of the variational circuit.
+ method (str): optimizer to employ.
+ jac (dict): Method for computing the gradient vector for scipy optimizers.
+ hess (dict): Method for computing the hessian matrix for scipy optimizers.
+ hessp (callable): Hessian of objective function times an arbitrary
+ vector for scipy optimizers.
+ bounds (sequence or Bounds): Bounds on variables for scipy optimizers.
+ constraints (dict): Constraints definition for scipy optimizers.
+ tol (float): Tolerance of termination for scipy optimizers.
+ options (dict): a dictionary with options for the different optimizers.
+ compile (bool): whether the TensorFlow graph should be compiled.
+ processes (int): number of processes when using the parallel BFGS method.
+ """
+ from qibo import models
+
+ t = 0.0
+ while (t - self._t_max) <= self.ATOL_TIME:
+ H = self.hamiltonian(t)
+ vqe = models.VQE(self._circuit, H)
+ best, params, _ = vqe.minimize(
+ params,
+ method=method,
+ jac=jac,
+ hess=hess,
+ hessp=hessp,
+ bounds=bounds,
+ constraints=constraints,
+ tol=tol,
+ options=options,
+ compile=compile,
+ processes=processes,
+ )
+ t += self._dt
+ return best, params
+
+
+class QAOA:
+ """Quantum Approximate Optimization Algorithm (QAOA) model.
+
+ The QAOA is introduced in `arXiv:1411.4028 `_.
+
+ Args:
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): problem Hamiltonian
+ whose ground state is sought.
+ mixer (:class:`qibo.hamiltonians.Hamiltonian`): mixer Hamiltonian.
+ Must be of the same type and act on the same number of qubits as ``hamiltonian``.
+ If ``None``, :class:`qibo.hamiltonians.X` is used.
+ solver (str): solver used to apply the exponential operators.
+ Default solver is 'exp' (:class:`qibo.solvers.Exponential`).
+ callbacks (list): List of callbacks to calculate during evolution.
+ accelerators (dict): Dictionary of devices to use for distributed
+ execution. This option is available only when ``hamiltonian``
+ is a :class:`qibo.hamiltonians.SymbolicHamiltonian`.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import models, hamiltonians
+ # create XXZ Hamiltonian for four qubits
+ hamiltonian = hamiltonians.XXZ(4)
+ # create QAOA model for this Hamiltonian
+ qaoa = models.QAOA(hamiltonian)
+ # optimize using random initial variational parameters
+ # and default options and initial state
+ initial_parameters = 0.01 * np.random.random(4)
+ best_energy, final_parameters, extra = qaoa.minimize(initial_parameters, method="BFGS")
+ """
+
+ from qibo import hamiltonians, optimizers
+
+ def __init__(
+ self, hamiltonian, mixer=None, solver="exp", callbacks=[], accelerators=None
+ ):
+ from qibo.hamiltonians.abstract import AbstractHamiltonian
+
+ # list of QAOA variational parameters (angles)
+ self.params = None
+ # problem hamiltonian
+ if not isinstance(hamiltonian, AbstractHamiltonian):
+ raise_error(TypeError, f"Invalid Hamiltonian type {type(hamiltonian)}.")
+ self.hamiltonian = hamiltonian
+ self.nqubits = hamiltonian.nqubits
+ # mixer hamiltonian (default = -sum(sigma_x))
+ if mixer is None:
+ trotter = isinstance(
+ self.hamiltonian, self.hamiltonians.SymbolicHamiltonian
+ )
+ self.mixer = self.hamiltonians.X(
+ self.nqubits, dense=not trotter, backend=self.hamiltonian.backend
+ )
+ else:
+ if type(mixer) != type(hamiltonian):
+ raise_error(
+ TypeError,
+ f"Given Hamiltonian is of type {type(hamiltonian)} "
+ + f"while mixer is of type {type(mixer)}.",
+ )
+ if mixer.nqubits != hamiltonian.nqubits:
+ raise_error(
+ ValueError,
+ f"Given Hamiltonian acts on {hamiltonian.nqubits} qubits "
+ + f"while mixer acts on {mixer.nqubits}.",
+ )
+ self.mixer = mixer
+
+ # create circuits for Trotter Hamiltonians
+ if accelerators is not None and (
+ not isinstance(self.hamiltonian, self.hamiltonians.SymbolicHamiltonian)
+ or solver != "exp"
+ ):
+ raise_error(
+ NotImplementedError,
+ "Distributed QAOA is implemented "
+ + "only with SymbolicHamiltonian and "
+ + "exponential solver.",
+ )
+ if isinstance(self.hamiltonian, self.hamiltonians.SymbolicHamiltonian):
+ self.hamiltonian.circuit(1e-2, accelerators)
+ self.mixer.circuit(1e-2, accelerators)
+
+ # evolution solvers
+ from qibo.solvers import get_solver
+
+ self.ham_solver = get_solver(solver, 1e-2, self.hamiltonian)
+ self.mix_solver = get_solver(solver, 1e-2, self.mixer)
+
+ self.callbacks = callbacks
+ self.backend = (
+ hamiltonian.backend
+ ) # to avoid error with _create_calculate_callbacks
+ self.accelerators = accelerators
+ self.normalize_state = StateEvolution._create_normalize_state(self, solver)
+ self.calculate_callbacks = StateEvolution._create_calculate_callbacks(
+ self, accelerators
+ )
+
+ def set_parameters(self, p):
+ """Sets the variational parameters.
+
+ Args:
+ p (np.ndarray): 1D-array holding the new values for the variational
+ parameters. Length should be an even number.
+ """
+ self.params = p
+
+ def _apply_exp(self, state, solver, p):
+ """Helper method for ``execute``."""
+ solver.dt = p
+ state = solver(state)
+ if self.callbacks:
+ state = self.normalize_state(state)
+ self.calculate_callbacks(state)
+ return state
+
+ def execute(self, initial_state=None):
+ """Applies the QAOA exponential operators to a state.
+
+ Args:
+ initial_state (np.ndarray): Initial state vector.
+
+ Returns:
+ State vector after applying the QAOA exponential gates.
+ """
+ if initial_state is None:
+ state = self.hamiltonian.backend.plus_state(self.nqubits)
+ else:
+ state = self.hamiltonian.backend.cast(initial_state)
+
+ self.calculate_callbacks(state)
+ n = int(self.params.shape[0])
+ for i in range(n // 2):
+ state = self._apply_exp(state, self.ham_solver, self.params[2 * i])
+ state = self._apply_exp(state, self.mix_solver, self.params[2 * i + 1])
+ return self.normalize_state(state)
+
+ def __call__(self, initial_state=None):
+ """Equivalent to :meth:`qibo.models.QAOA.execute`."""
+ return self.execute(initial_state)
+
+ def minimize(
+ self,
+ initial_p,
+ initial_state=None,
+ method="Powell",
+ loss_func=None,
+ loss_func_param=dict(),
+ jac=None,
+ hess=None,
+ hessp=None,
+ bounds=None,
+ constraints=(),
+ tol=None,
+ callback=None,
+ options=None,
+ compile=False,
+ processes=None,
+ ):
+ """Optimizes the variational parameters of the QAOA. A few loss functions are
+ provided for QAOA optimizations such as expected value (default), CVar which is introduced in
+ `Quantum 4, 256 `_, and
+ Gibbs loss function which is introduced in
+ `PRR 2, 023074 (2020) `_.
+
+ Args:
+ initial_p (np.ndarray): initial guess for the parameters.
+ initial_state (np.ndarray): initial state vector of the QAOA.
+ method (str): the desired minimization method.
+ See :meth:`qibo.optimizers.optimize` for available optimization
+ methods.
+ loss_func (function): the desired loss function. If it is None, the expectation is used.
+ loss_func_param (dict): a dictionary to pass in the loss function parameters.
+ jac (dict): Method for computing the gradient vector for scipy optimizers.
+ hess (dict): Method for computing the hessian matrix for scipy optimizers.
+ hessp (callable): Hessian of objective function times an arbitrary
+ vector for scipy optimizers.
+ bounds (sequence or Bounds): Bounds on variables for scipy optimizers.
+ constraints (dict): Constraints definition for scipy optimizers.
+ tol (float): Tolerance of termination for scipy optimizers.
+ callback (callable): Called after each iteration for scipy optimizers.
+ options (dict): a dictionary with options for the different optimizers.
+ compile (bool): whether the TensorFlow graph should be compiled.
+ processes (int): number of processes when using the paralle BFGS method.
+
+ Return:
+ The final energy (expectation value of the ``hamiltonian``).
+ The corresponding best parameters.
+ The optimization result object. For scipy methods it
+ returns the ``OptimizeResult``, for ``'cma'`` the
+ ``CMAEvolutionStrategy.result``, and for ``'sgd'``
+ the options used during the optimization.
+
+ Example:
+ .. testcode::
+
+ from qibo import hamiltonians
+ from qibo.models.utils import cvar, gibbs
+
+ h = hamiltonians.XXZ(3)
+ qaoa = models.QAOA(h)
+ initial_p = [0.314, 0.22, 0.05, 0.59]
+ best, params, _ = qaoa.minimize(initial_p)
+ best, params, _ = qaoa.minimize(initial_p, loss_func=cvar, loss_func_param={'alpha':0.1})
+ best, params, _ = qaoa.minimize(initial_p, loss_func=gibbs, loss_func_param={'eta':0.1})
+
+ """
+ if len(initial_p) % 2 != 0:
+ raise_error(
+ ValueError,
+ "Initial guess for the parameters must "
+ + "contain an even number of values but "
+ + "contains {len(initial_p)}.",
+ )
+
+ def _loss(params, qaoa, hamiltonian, state):
+ if state is not None:
+ state = hamiltonian.backend.cast(state, copy=True)
+ qaoa.set_parameters(params)
+ state = qaoa(state)
+ if loss_func is None:
+ return hamiltonian.expectation(state)
+ else:
+ func_hyperparams = {
+ key: loss_func_param[key]
+ for key in loss_func_param
+ if key in loss_func.__code__.co_varnames
+ }
+ param = {**func_hyperparams, "hamiltonian": hamiltonian, "state": state}
+
+ return loss_func(**param)
+
+ if method == "sgd":
+ loss = lambda p, c, h, s: _loss(self.hamiltonian.backend.cast(p), c, h, s)
+ else:
+ loss = lambda p, c, h, s: self.hamiltonian.backend.to_numpy(
+ _loss(p, c, h, s)
+ )
+
+ result, parameters, extra = self.optimizers.optimize(
+ loss,
+ initial_p,
+ args=(self, self.hamiltonian, initial_state),
+ method=method,
+ jac=jac,
+ hess=hess,
+ hessp=hessp,
+ bounds=bounds,
+ constraints=constraints,
+ tol=tol,
+ callback=callback,
+ options=options,
+ compile=compile,
+ processes=processes,
+ backend=self.backend,
+ )
+ self.set_parameters(parameters)
+ return result, parameters, extra
+
+
+class FALQON(QAOA):
+ """Feedback-based ALgorithm for Quantum OptimizatioN (FALQON) model.
+
+ The FALQON is introduced in `arXiv:2103.08619 `_.
+ It inherits the QAOA class.
+
+ Args:
+ hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): problem Hamiltonian
+ whose ground state is sought.
+ mixer (:class:`qibo.hamiltonians.Hamiltonian`): mixer Hamiltonian.
+ If ``None``, :class:`qibo.hamiltonians.X` is used.
+ solver (str): solver used to apply the exponential operators.
+ Default solver is 'exp' (:class:`qibo.solvers.Exponential`).
+ callbacks (list): List of callbacks to calculate during evolution.
+ accelerators (dict): Dictionary of devices to use for distributed
+ execution. This option is available only when ``hamiltonian``
+ is a :class:`qibo.hamiltonians.SymbolicHamiltonian`.
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import models, hamiltonians
+ # create XXZ Hamiltonian for four qubits
+ hamiltonian = hamiltonians.XXZ(4)
+ # create FALQON model for this Hamiltonian
+ falqon = models.FALQON(hamiltonian)
+ # optimize using random initial variational parameters
+ # and default options and initial state
+ delta_t = 0.01
+ max_layers = 3
+ best_energy, final_parameters, extra = falqon.minimize(delta_t, max_layers)
+ """
+
+ def __init__(
+ self, hamiltonian, mixer=None, solver="exp", callbacks=[], accelerators=None
+ ):
+ super().__init__(hamiltonian, mixer, solver, callbacks, accelerators)
+ self.evol_hamiltonian = 1j * (
+ self.hamiltonian @ self.mixer - self.mixer @ self.hamiltonian
+ )
+
+ def minimize(
+ self, delta_t, max_layers, initial_state=None, tol=None, callback=None
+ ):
+ """Optimizes the variational parameters of the FALQON.
+
+ Args:
+ delta_t (float): initial guess for the time step. A too large delta_t will make the algorithm fail.
+ max_layers (int): maximum number of layers allowed for the FALQON.
+ initial_state (np.ndarray): initial state vector of the FALQON.
+ tol (float): Tolerance of energy change. If not specified, no check is done.
+ callback (callable): Called after each iteration for scipy optimizers.
+ options (dict): a dictionary with options for the different optimizers.
+
+ Return:
+ The final energy (expectation value of the ``hamiltonian``).
+ The corresponding best parameters.
+ extra: variable with historical data for the energy and callbacks.
+ """
+ parameters = np.array([delta_t, 0])
+
+ def _loss(params, falqon, hamiltonian):
+ falqon.set_parameters(params)
+ state = falqon(initial_state)
+ return hamiltonian.expectation(state)
+
+ energy = [np.inf]
+ callback_result = []
+ for _ in range(1, max_layers + 1):
+ beta = self.hamiltonian.backend.to_numpy(
+ _loss(parameters, self, self.evol_hamiltonian)
+ )
+
+ if tol is not None:
+ energy.append(
+ self.hamiltonian.backend.to_numpy(
+ _loss(parameters, self, self.hamiltonian)
+ )
+ )
+ if abs(energy[-1] - energy[-2]) < tol:
+ break
+
+ if callback is not None:
+ callback_result.append(callback(parameters))
+
+ parameters = np.concatenate([parameters, [delta_t, delta_t * beta]])
+
+ self.set_parameters(parameters)
+ final_loss = _loss(parameters, self, self.hamiltonian)
+ extra = {"energies": energy, "callbacks": callback_result}
+ return final_loss, parameters, extra
diff --git a/src/qibo/noise.py b/src/qibo/noise.py
new file mode 100644
index 000000000..6ceec82ce
--- /dev/null
+++ b/src/qibo/noise.py
@@ -0,0 +1,569 @@
+"""Module defining Error channels and NoiseModel class(es)."""
+
+import collections
+from itertools import combinations
+from math import log2
+from typing import Optional, Union
+
+from qibo import gates
+from qibo.config import raise_error
+
+
+class KrausError:
+ """Quantum error associated with the :class:`qibo.gates.KrausChannel`.
+
+ Args:
+ ops (list): List of Kraus operators of type ``np.ndarray``
+ or ``tf.Tensor`` and of the same shape.
+
+ """
+
+ def __init__(self, ops):
+ shape = ops[0].shape
+ if any(o.shape != shape for o in ops):
+ raise_error(
+ ValueError,
+ "Kraus operators of different shapes."
+ "Use qibo.noise.CustomError instead.",
+ )
+
+ self.rank = shape[0]
+ self.options = ops
+
+ def channel(self, qubits, options):
+ """Returns the quantum channel associated to the quantum error."""
+ return [
+ gates.KrausChannel(q, options)
+ for q in combinations(qubits, int(log2(self.rank)))
+ ]
+
+
+class UnitaryError:
+ """Quantum error associated with the :class:`qibo.gates.UnitaryChannel`.
+
+ Args:
+ probabilities (list): List of floats that correspond to the probability
+ that each unitary Uk is applied.
+ unitaries (list): List of unitary matrices as ``np.ndarray``/``tf.Tensor``
+ of the same shape. Must have the same length as the given
+ probabilities ``p``.
+
+ """
+
+ def __init__(self, probabilities, unitaries):
+ shape = unitaries[0].shape
+ if any(o.shape != shape for o in unitaries):
+ raise_error(
+ ValueError,
+ "Unitary matrices have different shapes."
+ "Use qibo.noise.CustomError instead.",
+ )
+ self.rank = shape[0]
+ self.options = list(zip(probabilities, unitaries))
+
+ def channel(self, qubits, options):
+ """Returns the quantum channel associated to the quantum error."""
+ return [
+ gates.UnitaryChannel(q, options)
+ for q in combinations(qubits, int(log2(self.rank)))
+ ]
+
+
+class PauliError:
+ """Quantum error associated with the :class:`qibo.gates.PauliNoiseChannel`.
+
+ Args:
+ operators (list): see :class:`qibo.gates.PauliNoiseChannel`
+ """
+
+ def __init__(self, operators):
+ self.options = operators
+
+ def channel(self, qubits, options):
+ """Returns the quantum channel associated to the quantum error."""
+ return [gates.PauliNoiseChannel(q, options) for q in qubits]
+
+
+class DepolarizingError:
+ """Quantum error associated with the :class:`qibo.gates.DepolarizingChannel`.
+
+ Args:
+ options (float): see :class:`qibo.gates.DepolarizingChannel`
+ """
+
+ def __init__(self, lam):
+ self.options = lam
+ self.channel = gates.DepolarizingChannel
+
+
+class ThermalRelaxationError:
+ """Quantum error associated with the :class:`qibo.gates.ThermalRelaxationChannel`.
+
+ Args:
+ options (tuple): see :class:`qibo.gates.ThermalRelaxationChannel`
+ """
+
+ def __init__(self, t1, t2, time, excited_population=0):
+ self.options = [t1, t2, time, excited_population]
+ self.channel = gates.ThermalRelaxationChannel
+
+
+class AmplitudeDampingError:
+ """Quantum error associated with the :class:`qibo.gates.AmplitudeDampingChannel`.
+
+ Args:
+ options (float): see :class:`qibo.gates.AmplitudeDampingChannel`
+ """
+
+ def __init__(self, gamma):
+ self.options = gamma
+ self.channel = gates.AmplitudeDampingChannel
+
+
+class PhaseDampingError:
+ """Quantum error associated with the :class:`qibo.gates.PhaseDampingChannel`.
+
+ Args:
+ options (float): see :class:`qibo.gates.PhaseDampingChannel`
+ """
+
+ def __init__(self, gamma):
+ self.options = gamma
+ self.channel = gates.PhaseDampingChannel
+
+
+class ReadoutError:
+ """Quantum error associated with :class:'qibo.gates;ReadoutErrorChannel'.
+
+ Args:
+ options (array): see :class:'qibo.gates.ReadoutErrorChannel'
+ """
+
+ def __init__(self, probabilities):
+ self.options = probabilities
+ self.channel = gates.ReadoutErrorChannel
+
+
+class ResetError:
+ """Quantum error associated with the `qibo.gates.ResetChannel`.
+
+ Args:
+ options (tuple): see :class:`qibo.gates.ResetChannel`
+ """
+
+ def __init__(self, p0, p1):
+ self.options = [p0, p1]
+ self.channel = gates.ResetChannel
+
+
+class CustomError:
+ """Quantum error associated with the :class:`qibo.gates.Channel`
+
+ Args:
+ channel (:class:`qibo.gates.Channel`): any channel
+
+ Example:
+
+ .. testcode::
+
+ import numpy as np
+ from qibo.gates import KrausChannel
+ from qibo.noise import CustomError
+
+ # define |0><0|
+ a1 = np.array([[1, 0], [0, 0]])
+ # define |0><1|
+ a2 = np.array([[0, 1], [0, 0]])
+
+ # Create an Error associated with Kraus Channel
+ # rho -> |0><0| rho |0><0| + |0><1| rho |0><1|
+ error = CustomError(gates.KrausChannel((0,), [a1, a2]))
+
+ """
+
+ def __init__(self, channel):
+ self.channel = channel
+
+
+class NoiseModel:
+ """Class for the implementation of a custom noise model.
+
+ Example:
+
+ .. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.noise import NoiseModel, PauliError
+
+ # Build specific noise model with 2 quantum errors:
+ # - Pauli error on H only for qubit 1.
+ # - Pauli error on CNOT for all the qubits.
+
+ noise_model = NoiseModel()
+ noise_model.add(PauliError([("X", 0.5)]), gates.H, 1)
+ noise_model.add(PauliError([("Y", 0.5)]), gates.CNOT)
+
+ # Generate noiseless circuit.
+ circuit = Circuit(2)
+ circuit.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+
+ # Apply noise to the circuit according to the noise model.
+ noisy_circuit = noise_model.apply(circuit)
+
+ """
+
+ def __init__(self):
+ self.errors = collections.defaultdict(list)
+
+ def add(
+ self,
+ error,
+ gate: Optional[gates.Gate] = None,
+ qubits: Optional[Union[int, tuple]] = None,
+ conditions=None,
+ ):
+ """Add a quantum error for a specific gate and qubit to the noise model.
+
+ Args:
+ error: quantum error to associate with the gate. Possible choices
+ are :class:`qibo.noise.PauliError`,
+ :class:`qibo.noise.DepolarizingError`,
+ :class:`qibo.noise.ThermalRelaxationError`,
+ :class:`qibo.noise.AmplitudeDampingError`,
+ :class:`qibo.noise.PhaseDampingError`,
+ :class:`qibo.noise.ReadoutError`,
+ :class:`qibo.noise.ResetError`,
+ :class:`qibo.noise.UnitaryError`,
+ :class:`qibo.noise.KrausError`, and
+ :class:`qibo.noise.CustomError`.
+ gate (:class:`qibo.gates.Gate`, optional): gate after which the noise will be added.
+ If ``None``, the noise will be added after each gate except
+ :class:`qibo.gates.Channel` and :class:`qibo.gates.M`.
+ qubits (int or tuple, optional): qubits where the noise will be applied. If ``None``,
+ the noise will be added after every instance of the gate.
+ Defaults to ``None``.
+ condition (callable, optional): function that takes :class:`qibo.gates.Gate`
+ object as an input and returns ``True`` if noise should be added to it.
+
+ Example:
+
+ .. testcode::
+
+ import numpy as np
+ from qibo import Circuit, gates
+ from qibo.noise import NoiseModel, PauliError
+
+ # Check if a gate is RX(pi/2).
+ def is_sqrt_x(gate):
+ return np.pi/2 in gate.parameters
+
+ # Build a noise model with a Pauli error on RX(pi/2) gates.
+ error = PauliError(list(zip(["X", "Y", "Z"], [0.01, 0.5, 0.1])))
+ noise = NoiseModel()
+ noise.add(PauliError([("X", 0.5)]), gates.RX, conditions=is_sqrt_x)
+
+ # Generate a noiseless circuit.
+ circuit = Circuit(1)
+ circuit.add(gates.RX(0, np.pi / 2))
+ circuit.add(gates.RX(0, 3 * np.pi / 2))
+ circuit.add(gates.X(0))
+
+ # Apply noise to the circuit.
+ noisy_circuit = noise.apply(circuit)
+
+ """
+
+ if isinstance(qubits, int):
+ qubits = (qubits,)
+
+ if (
+ conditions is not None
+ and not callable(conditions)
+ and not isinstance(conditions, list)
+ ):
+ raise_error(
+ TypeError,
+ "`conditions` should be either a callable or a list of callables. "
+ + f"Got {type(conditions)} instead.",
+ )
+
+ if isinstance(conditions, list) and not all(
+ callable(condition) for condition in conditions
+ ):
+ raise_error(
+ TypeError,
+ "A element of `conditions` list is not a callable.",
+ )
+
+ if callable(conditions):
+ conditions = [conditions]
+
+ self.errors[gate].append((conditions, error, qubits))
+
+ def apply(self, circuit):
+ """Generate a noisy quantum circuit according to the noise model built.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): quantum circuit
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: initial circuit with noise gates
+ added according to the noise model.
+ """
+
+ noisy_circuit = circuit.__class__(**circuit.init_kwargs)
+ for gate in circuit.queue:
+ errors_list = (
+ self.errors[gate.__class__]
+ if isinstance(gate, (gates.Channel, gates.M))
+ else self.errors[gate.__class__] + self.errors[None]
+ )
+
+ if all(not isinstance(error, ReadoutError) for _, error, _ in errors_list):
+ noisy_circuit.add(gate)
+
+ for conditions, error, qubits in errors_list:
+ if conditions is None or all(
+ condition(gate) for condition in conditions
+ ):
+ qubits = (
+ gate.qubits
+ if qubits is None
+ else tuple(set(gate.qubits) & set(qubits))
+ )
+
+ if len(qubits) == 0:
+ continue
+
+ if isinstance(error, CustomError) and qubits:
+ noisy_circuit.add(error.channel)
+ elif (
+ isinstance(
+ error,
+ (
+ ThermalRelaxationError,
+ AmplitudeDampingError,
+ PhaseDampingError,
+ ResetError,
+ ),
+ )
+ and qubits
+ ):
+ for qubit in qubits:
+ noisy_circuit.add(error.channel(qubit, error.options))
+ elif isinstance(error, ReadoutError) and qubits:
+ noisy_circuit.add(error.channel(qubits, error.options))
+ noisy_circuit.add(gate)
+ else:
+ noisy_circuit.add(error.channel(qubits, error.options))
+
+ if gate.name == "measure":
+ readout_error_qubits = [
+ qubits
+ for _, error, qubits in errors_list
+ if isinstance(error, ReadoutError)
+ ]
+ if (
+ gate.qubits not in readout_error_qubits
+ and gate.register_name
+ not in noisy_circuit.measurement_tuples.keys()
+ ):
+ noisy_circuit.add(gate)
+
+ return noisy_circuit
+
+
+class _Conditions:
+ def __init__(self, qubits=None):
+ self.qubits = qubits
+
+ def condition_qubits(self, gate):
+ return gate.qubits == self.qubits
+
+ def condition_gate_single(self, gate):
+ """Condition that had to be matched to apply noise channel to single-qubit ``gate``."""
+ return len(gate.qubits) == 1
+
+ def condition_gate_two(self, gate):
+ """Condition that had to be matched to apply noise channel to two-qubit ``gate``."""
+ return len(gate.qubits) == 2
+
+
+class IBMQNoiseModel(NoiseModel):
+ """Class for the implementation of a IBMQ noise model.
+
+ This noise model applies a :class:`qibo.gates.DepolarizingChannel` followed by a
+ :class:`qibo.gates.ThermalRelaxationChannel` after each one- or two-qubit gate in the circuit.
+ It also applies single-qubit :class:`qibo.gates.ReadoutErrorChannel`
+ *before* every measurement gate.
+
+
+ Example:
+
+ .. testcode::
+
+ from qibo import Circuit, gates
+ from qibo.models.encodings import phase_encoder
+ from qibo.noise import DepolarizingError, ThermalRelaxationError, ReadoutError
+ from qibo.noise import IBMQNoiseModel, NoiseModel
+
+ nqubits = 4
+
+ # creating circuit
+ phases = list(range(nqubits))
+ circuit = phase_encoder(phases, rotation="RY")
+ circuit.add(gates.CNOT(qubit, qubit + 1) for qubit in range(nqubits - 1))
+ circuit.add(gates.M(qubit) for qubit in range(1, nqubits - 1))
+
+ # creating noise model from dictionary
+ parameters = {
+ "depolarizing_one_qubit" : {"0": 0.1, "2": 0.04, "3": 0.15},
+ "depolarizing_two_qubit": {"0-1": 0.2},
+ "t1" : {"0": 0.1, "1": 0.2, "3": 0.01},
+ "t2" : {"0": 0.01, "1": 0.02, "3": 0.0001},
+ "gate_times" : (0.1, 0.2),
+ "excited_population" : 0.1,
+ "readout_one_qubit" : {"0": (0.1, 0.1), "1": 0.1, "3": [0.1, 0.1]},
+ }
+
+ noise_model = IBMQNoiseModel()
+ noise_model.from_dict(parameters)
+ noisy_circuit = noise_model.apply(circuit)
+ """
+
+ def from_dict(self, parameters: dict):
+ """Method used to pass noise ``parameters`` as inside dictionary.
+
+ Args:
+ parameters (dict): Contains parameters necessary to initialise
+ :class:`qibo.noise.DepolarizingError`, :class:`qibo.noise.ThermalRelaxationError`,
+ and :class:`qibo.noise.ReadoutError`.
+
+ The keys and values of the dictionary parameters are defined below:
+
+ - ``"depolarizing_one_qubit"`` (*int* or *float* or *dict*): If ``int`` or
+ ``float``, all qubits share the same single-qubit depolarizing parameter.
+ If ``dict``, expects qubit indexes as keys and their respective
+ depolarizing parameter as values.
+ See :class:`qibo.gates.channels.DepolarizingChannel`
+ for a detailed definition of depolarizing parameter.
+ - ``"depolarizing_two_qubit"`` (*int* or *float* or *dict*): If ``int`` or
+ ``float``, all two-qubit gates share the same two-qubit depolarizing
+ parameter regardless in which pair of qubits the two-qubit gate is acting on.
+ If ``dict``, expects pair qubit indexes as keys separated by a hiphen
+ (e.g. "0-1" for gate that has "0" as control and "1" as target)
+ and their respective depolarizing parameter as values.
+ See :class:`qibo.gates.channels.DepolarizingChannel`
+ for a detailed definition of depolarizing parameter.
+ - ``"t1"`` (*int* or *float* or *dict*): If ``int`` or ``float``, all qubits
+ share the same ``t1``. If ``dict``, expects qubit indexes as keys and its
+ respective ``t1`` as values.
+ See :class:`qibo.gates.channels.ThermalRelaxationChannel`
+ for a detailed definition of ``t1``.
+ Note that ``t1`` and ``t2`` must be passed with the same type.
+ - ``"t2"`` (*int* or *float* or *dict*): If ``int`` or ``float``, all qubits share
+ the same ``t2``. If ``dict``, expects qubit indexes as keys and its
+ respective ``t2`` as values.
+ See :class:`qibo.gates.channels.ThermalRelaxationChannel`
+ for a detailed definition of ``t2``.
+ Note that ``t2`` and ``t1`` must be passed with the same type.
+ - ``"gate_times"`` (*tuple* or *list*): pair of gate times representing
+ gate times for :class:`ThermalRelaxationError` following, respectively,
+ one- and two-qubit gates.
+ - ``"excited_population"`` (*int* or *float*): See
+ :class:`ThermalRelaxationChannel`.
+ - ``"readout_one_qubit"`` (*int* or *float* or *dict*): If ``int`` or ``float``,
+ :math:`p(0|1) = p(1|0)`, and all qubits share the same readout error
+ probabilities. If ``dict``, expects qubit indexes as keys and
+ values as ``tuple`` (or ``list``) in the format :math:`(p(0|1),\\,p(1|0))`.
+ If values are ``tuple`` or ``list`` of length 1 or ``float`` or ``int``,
+ then it is assumed that :math:`p(0|1) = p(1|0)`.
+ """
+ t_1 = parameters["t1"]
+ t_2 = parameters["t2"]
+ gate_time_1, gate_time_2 = parameters["gate_times"]
+ excited_population = parameters["excited_population"]
+ depolarizing_one_qubit = parameters["depolarizing_one_qubit"]
+ depolarizing_two_qubit = parameters["depolarizing_two_qubit"]
+ readout_one_qubit = parameters["readout_one_qubit"]
+
+ if isinstance(depolarizing_one_qubit, (float, int)):
+ self.add(
+ DepolarizingError(depolarizing_one_qubit),
+ conditions=_Conditions().condition_gate_single,
+ )
+
+ if isinstance(depolarizing_one_qubit, dict):
+ for qubit_key, lamb in depolarizing_one_qubit.items():
+ self.add(
+ DepolarizingError(lamb),
+ qubits=int(qubit_key),
+ conditions=_Conditions().condition_gate_single,
+ )
+
+ if isinstance(depolarizing_two_qubit, (float, int)):
+ self.add(
+ DepolarizingError(depolarizing_two_qubit),
+ conditions=_Conditions().condition_gate_two,
+ )
+
+ if isinstance(depolarizing_two_qubit, dict):
+ for key, lamb in depolarizing_two_qubit.items():
+ qubits = key.replace(" ", "").split("-")
+ qubits = tuple(map(int, qubits))
+ self.add(
+ DepolarizingError(lamb),
+ qubits=qubits,
+ conditions=[
+ _Conditions().condition_gate_two,
+ _Conditions(qubits).condition_qubits,
+ ],
+ )
+
+ if isinstance(t_1, (float, int)) and isinstance(t_2, (float, int)):
+ self.add(
+ ThermalRelaxationError(t_1, t_2, gate_time_1, excited_population),
+ conditions=_Conditions().condition_gate_single,
+ )
+ self.add(
+ ThermalRelaxationError(t_1, t_2, gate_time_2, excited_population),
+ conditions=_Conditions().condition_gate_two,
+ )
+
+ if isinstance(t_1, dict) and isinstance(t_2, dict):
+ for qubit_key in t_1.keys():
+ self.add(
+ ThermalRelaxationError(
+ t_1[qubit_key], t_2[qubit_key], gate_time_1, excited_population
+ ),
+ qubits=int(qubit_key),
+ conditions=_Conditions().condition_gate_single,
+ )
+ self.add(
+ ThermalRelaxationError(
+ t_1[qubit_key], t_2[qubit_key], gate_time_2, excited_population
+ ),
+ qubits=int(qubit_key),
+ conditions=_Conditions().condition_gate_two,
+ )
+
+ if isinstance(readout_one_qubit, (int, float)):
+ probabilities = [
+ [1 - readout_one_qubit, readout_one_qubit],
+ [readout_one_qubit, 1 - readout_one_qubit],
+ ]
+ self.add(ReadoutError(probabilities), gate=gates.M)
+
+ if isinstance(readout_one_qubit, dict):
+ for qubit, probs in readout_one_qubit.items():
+ if isinstance(probs, (int, float)):
+ probs = (probs, probs)
+ elif isinstance(probs, (tuple, list)) and len(probs) == 1:
+ probs *= 2
+
+ probabilities = [[1 - probs[0], probs[0]], [probs[1], 1 - probs[1]]]
+ self.add(
+ ReadoutError(probabilities),
+ gate=gates.M,
+ qubits=int(qubit),
+ )
diff --git a/src/qibo/noise_model.py b/src/qibo/noise_model.py
new file mode 100644
index 000000000..79ae249be
--- /dev/null
+++ b/src/qibo/noise_model.py
@@ -0,0 +1,392 @@
+import numpy as np
+
+from qibo import gates, models
+from qibo.quantum_info.utils import hellinger_fidelity, hellinger_shot_error
+
+
+def noisy_circuit(circuit, params):
+ """Creates a noisy circuit from the circuit given as argument.
+
+ The function applies a :class:`qibo.gates.ThermalRelaxationChannel` after each step of the circuit
+ and, after each gate, a :class:`qibo.gates.DepolarizingChannel`, whose parameter depends on whether the
+ gate applies on one or two qubits. In the end are applied asymmetric bitflips on measurement gates.
+
+
+ Args:
+ circuit (qibo.models.Circuit): Circuit on which noise will be applied. Since in the end are
+ applied bitflips, measurement gates are required.
+ params (dict): contains the parameters of the channels organized as follow \n
+ {'t1' : (``t1``, ``t2``,..., ``tn``),
+ 't2' : (``t1``, ``t2``,..., ``tn``),
+ 'gate time' : (``time1``, ``time2``),
+ 'excited population' : 0,
+ 'depolarizing error' : (``lambda1``, ``lambda2``),
+ 'bitflips error' : ([``p1``, ``p2``,..., ``pm``], [``p1``, ``p2``,..., ``pm``]),
+ 'idle_qubits' : True}
+ where `n` is the number of qubits, and `m` the number of measurement gates.
+ The first four parameters are used by the thermal relaxation error. The first two elements are the
+ tuple containing the :math:`T_1` and :math:`T_2` parameters; the third one is a tuple which contain the gate times,
+ for single and two qubit gates; then we have the excited population parameter.
+ The fifth parameter is a tuple containing the depolaraziong errors for single and 2 qubit gate.
+ The sisxth parameter is a tuple containg the two arrays for bitflips probability errors: the first one implements 0->1 errors, the other one 1->0.
+ The last parameter is a boolean variable: if True the noise model takes into account idle qubits.
+ Returns:
+ The new noisy circuit (qibo.models.Circuit).
+
+
+ """
+ # parameters of the model
+ t1 = params["t1"]
+ t2 = params["t2"]
+ time1 = params["gate_time"][0]
+ time2 = params["gate_time"][1]
+ excited_population = params["excited_population"]
+ depolarizing_error_1 = params["depolarizing_error"][0]
+ depolarizing_error_2 = params["depolarizing_error"][1]
+ bitflips_01 = params["bitflips_error"][0]
+ bitflips_10 = params["bitflips_error"][1]
+ idle_qubits = params["idle_qubits"]
+
+ # new circuit
+ noisy_circ = models.Circuit(circuit.nqubits, density_matrix=True)
+
+ # time steps of the circuit
+ time_steps = max(circuit.queue.moment_index)
+
+ # current_time keeps track of the time spent by the qubits
+ # being manipulated by the gates of the circuit
+ current_time = np.zeros(circuit.nqubits)
+
+ # the idea behind ths loop is to build the old circuit adding the noise channels and
+ # keeping track of the time qubits spend being manipulated by the gates, in order
+ # to correct the thermal relaxation time of each qubit, even if they are idle.
+ for t in range(time_steps):
+ # for each time step, I look for each qubit what gate are applied
+ for qubit in range(circuit.nqubits):
+ # if there's no gate, move on!
+ if circuit.queue.moments[t][qubit] is None:
+ pass
+ # measurement gates
+ elif isinstance(circuit.queue.moments[t][qubit], gates.measurements.M):
+ for key in list(circuit.measurement_tuples):
+ # if there is a 2-qubits measurement gate we must check that both qubit intercated
+ # with the environment for the same amount of time. If not, before applying
+ # the 2-qubits gate we apply the therm-rel channel for the time difference
+ if len(circuit.measurement_tuples[key]) > 1:
+ q1 = circuit.measurement_tuples[key][0]
+ q2 = circuit.measurement_tuples[key][1]
+ if current_time[q1] != current_time[q2] and idle_qubits:
+ q_min = q1
+ q_max = q2
+ if current_time[q1] > current_time[q2]:
+ q_min = q2
+ q_max = q1
+ time_difference = current_time[q_max] - current_time[q_min]
+ # this is the thermal relaxation channel which model the intercation
+ # of the idle qubit with the environment
+ noisy_circ.add(
+ gates.ThermalRelaxationChannel(
+ q_min,
+ [
+ t1[q_min],
+ t2[q_min],
+ time_difference,
+ excited_population,
+ ],
+ )
+ )
+ # update the qubit time
+ current_time[q_min] += time_difference
+ q = circuit.queue.moments[t][qubit].qubits
+ # adding measurements gates
+ p0q = [bitflips_01[j] for j in q]
+ p1q = [bitflips_10[j] for j in q]
+ noisy_circ.add(gates.M(*q, p0=p0q, p1=p1q))
+ if len(circuit.queue.moments[t][qubit].qubits) != 1:
+ circuit.queue.moments[t][
+ max(circuit.queue.moments[t][qubit].qubits)
+ ] = None
+ # if there is a 1-qubit gate I add the old gate, the dep and therm-rel channels
+ elif len(circuit.queue.moments[t][qubit].qubits) == 1:
+ noisy_circ.add(circuit.queue.moments[t][qubit])
+ noisy_circ.add(
+ gates.DepolarizingChannel(
+ circuit.queue.moments[t][qubit].qubits, depolarizing_error_1
+ )
+ )
+ noisy_circ.add(
+ gates.ThermalRelaxationChannel(
+ qubit,
+ [t1[qubit], t2[qubit], time1, excited_population],
+ )
+ )
+ # I update the qubit time
+ current_time[qubit] += time1
+ # if there is a 2-qubits gate we must check that both qubit intercated
+ # with the environment for the same amount of time. If not, before applying
+ # the 2-qubits gate we apply the therm-rel channel for the time difference
+ else:
+ q1 = circuit.queue.moments[t][qubit].qubits[0]
+ q2 = circuit.queue.moments[t][qubit].qubits[1]
+ if current_time[q1] != current_time[q2] and idle_qubits:
+ q_min = q1
+ q_max = q2
+ if current_time[q1] > current_time[q2]:
+ q_min = q2
+ q_max = q1
+ time_difference = current_time[q_max] - current_time[q_min]
+ # this is the thermal relaxation channel which model the intercation
+ # of the idle qubit with the environment
+ noisy_circ.add(
+ gates.ThermalRelaxationChannel(
+ q_min,
+ [t1[q_min], t2[q_min], time_difference, excited_population],
+ )
+ )
+ # I update the qubit time
+ current_time[q_min] += time_difference
+ # I add the 2-qubit gate, dep and therm-rel channels
+ noisy_circ.add(circuit.queue.moments[t][qubit])
+ noisy_circ.add(
+ gates.DepolarizingChannel(
+ tuple(set(circuit.queue.moments[t][qubit].qubits)),
+ depolarizing_error_2,
+ )
+ )
+ noisy_circ.add(
+ gates.ThermalRelaxationChannel(
+ q1, [t1[q1], t2[q1], time2, excited_population]
+ )
+ )
+ noisy_circ.add(
+ gates.ThermalRelaxationChannel(
+ q2, [t1[q2], t2[q2], time2, excited_population]
+ )
+ )
+ # I update the qubit time
+ current_time[circuit.queue.moments[t][qubit].qubits[0]] += time2
+ current_time[circuit.queue.moments[t][qubit].qubits[1]] += time2
+ circuit.queue.moments[t][
+ max(circuit.queue.moments[t][qubit].qubits)
+ ] = None
+
+ # setting noisy_circ.measurements
+ measurements = []
+ for m in circuit.measurements:
+ q = m.qubits
+ p0q = [bitflips_01[j] for j in q]
+ p1q = [bitflips_10[j] for j in q]
+ measurements.append(gates.M(*q, p0=p0q, p1=p1q))
+ noisy_circ.measurements = measurements
+
+ return noisy_circ
+
+
+def freq_to_prob(freq):
+ """Transforms a dictionary of frequencies in an array of probabilities.
+
+ Args:
+ freq (CircuitResult.frequencies): frequencies you want to transform.
+
+ Returns:
+ The new array (numpy.ndarray).
+ """
+ norm = sum(freq.values())
+ nqubits = len(list(freq.keys())[0])
+ prob = np.zeros(2**nqubits)
+ for k in range(2**nqubits):
+ index = "{:b}".format(k).zfill(nqubits)
+ prob[k] = freq[index] / norm
+ return prob
+
+
+def loss(parameters, *args):
+ """The loss function used to be maximized in the fit method of the :class:`qibo.noise_model.CompositeNoiseModel`.
+ It is the hellinger fidelity calculated between the probability distribution of the noise model and the experimental target distribution using the :func:`qibo.quantum_info.hellinger_fidelity`.
+ It is possible to return also the finite shot error correction calculated with the :func:`qibo.noise_model.hellinger_shot_error`.
+
+ Args:
+ parameters (numpy.ndarray): parameters of the :func:`qibo.noise_model.noisy_circuit` which must be inferred.
+ They must be given in form of array as
+ array([params["t1"], params["t2"], params["gate_time"], params["depolarizing_error"], params["bitflips_error"]])
+ q (numpy.ndarray): (discrete) probability distribution :math:`q`.
+ nshots (int): the number of shots we used to run the circuit to obtain :math:`p` and :math:`q`.
+ args (numpy.ndarray): other parameters which don't need to be inferred as
+ array([circuit, nshots, target_prob, idle_qubits, backend, error]).
+ The circuit you want to simulate; the number of shots of the simulatin; the target probability; the boolean variable idle_qubits,
+ if you want the noise model to take into account idle qubits; the backend; the boolean variable error, if you want to take into account the hellinger fidelity error due to shot noise.
+
+ Returns:
+ (float): The Hellinger fidelity if error is False.
+ (list): [Hellinger fidelity, Hellinger fidelity error] if error is True.
+ """
+ circuit = args[0]
+ nshots = args[1]
+ target_prob = args[2]
+ idle_qubits = args[3]
+ backend = args[4]
+ error = args[5]
+ qubits = circuit.nqubits
+ parameters = np.array(parameters)
+
+ if any(2 * parameters[0:qubits] - parameters[qubits : 2 * qubits] < 0):
+ return np.inf
+
+ params = {
+ "t1": tuple(parameters[0:qubits]),
+ "t2": tuple(parameters[qubits : 2 * qubits]),
+ "gate_time": tuple(parameters[2 * qubits : 2 * qubits + 2]),
+ "excited_population": 0,
+ "depolarizing_error": tuple(parameters[2 * qubits + 2 : 2 * qubits + 4]),
+ "bitflips_error": (
+ parameters[2 * qubits + 4 : 3 * qubits + 4],
+ parameters[3 * qubits + 4 : 4 * qubits + 4],
+ ),
+ "idle_qubits": idle_qubits,
+ }
+
+ noisy_circ = noisy_circuit(circuit, params)
+ freq = backend.execute_circuit(circuit=noisy_circ, nshots=nshots).frequencies()
+ prob = freq_to_prob(freq)
+
+ hellinger_fid = hellinger_fidelity(target_prob, prob)
+
+ if error:
+ return [-hellinger_fid, hellinger_shot_error(target_prob, prob, nshots)]
+
+ return -hellinger_fid
+
+
+class CompositeNoiseModel:
+ """Class associated with a realistic representation of a noisy circuit modeled by the :func:`qibo.noise_model.noisy_circuit`.
+ This class is able to fit the parameters of the noise model to reproduce an experimental realization of the circuit
+ you want to simulate.
+
+ Args:
+ noisy_circuit (:class:`qibo.models.circuit.Circuit`): the noisy circuit. See :func:`qibo.noise_model.noisy_circuit`.
+ params (dictionary): the parameters of the noise model. See :func:`qibo.noise_model.noisy_circuit`.
+ hellinger (float): current value of the hellinger fidelity between the noisy simulation and the given target result.
+ hellinger0 (dictionary): the fidelity and the shot error fidelity organized as {"fidelity": (float) f, "shot_error": (float) e}.
+ """
+
+ def __init__(self, params):
+ self.noisy_circuit = {}
+ self.params = params
+ self.hellinger = {}
+ self.hellinger0 = {}
+
+ def apply(self, circuit):
+ """Creates the noisy circuit from the circuit given as argument by using the :func:`qibo.noise_model.noisy_circuit`.
+ Args:
+ circuit (qibo.models.Circuit): the circuit you want to simulate.
+ """
+ self.noisy_circuit = noisy_circuit(circuit, self.params)
+
+ def fit(
+ self,
+ circuit,
+ target_result,
+ bounds=True,
+ eps=1e-4,
+ maxfun=None,
+ maxiter=1000,
+ locally_biased=True,
+ f_min_rtol=None,
+ vol_tol=1e-16,
+ len_tol=1e-6,
+ callback=None,
+ backend=None,
+ ):
+ r"""Performs the fitting procedure of the noise model parameters, using the method nlopt.opt from the library NLopt. The fitting procedure is implemented to maximize the hellinger fidelity calculated using the :func:`qibo.noise_model.loss` between the probability distribution function estimated by the noise model and the one measured experimentally. Since, we are using probability distribution functions estimated using a finite number of shots, the hellinger fidelity is going to have an error caused by an imperfect estimation of the probabilities. This method takes into account this effect and stops when the fidelity reaches a corrected maximum $1-\epsilon$, with $\epsilon$=:func:`qibo.noise_model.hellinger_shot_error`.
+
+ Args:
+ target_result (:class:`qibo.measurements.CircuitResult`): the circuit result with frequencies you want to emulate.
+ bounds: if True are given the default bounds for the depolarizing and thermal relaxation channels' parameters.
+ Otherwise it's possible to pass a matrix of size (2, 4 * nqubits + 4), where bounds[0] and bounds[1]
+ will be respectively the lower and the upper bounds for the parameters. The first 2 * nqubit columns are related
+ to the :math:`T_1` and :math:`T_2` parameters; the subsequent 2 columns are related to the gate time parameters; the other subsequent 2 columns are related to depolarizing error parameters; the last 2 * nqubit columns are related to bitflips errors.
+ eps (float): Minimal required difference of the objective function values between the current best hyperrectangle and the next potentially optimal hyperrectangle to be divided.
+ maxfun (int or None): Approximate upper bound on objective function evaluations. If None, will be automatically set to :math:`1000N` where :math:`N` represents the number of dimensions.
+ maxiter (int): Maximum number of iterations.
+ locally_biased (bool): If True, use DIRECT_L. If False, use DIRECT.
+ f_min_rtol (float): the tolerance of the optimization. The optimization will finish when the fidelity reaches the value
+ :math:`1-f_min_rtol`, by default f_min_rtol is set to be the fidelity error caused by the finite number of shots and calculated by the :func:`qibo.noise_model.hellinger_shot_error`.
+ vol_tol (float): Stop the optimization process when the volume of the hyperrectangle that contains the lowest function value becomes smaller than vol_tol.
+ len_tol (float): When "locally_biased" is set to True, stop the optimization process if the length of the longest side of the hyperrectangle containing the lowest function value, normalized by the maximal side length, is less than half of "len_tol". If "locally_biased" is False, terminate the optimization process when half of the normalized diagonal of the hyperrectangle containing the lowest function value is smaller than "len_tol".
+ callback (callable): This function takes one parameter, `xk`, which represents the current best function value found by the algorithm.
+ backend: you can specify your backend. If None qibo.backends.GlobalBackend is used.
+ """
+
+ from scipy.optimize import Bounds, direct
+
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ nshots = target_result.nshots
+ target_prob = freq_to_prob(target_result.frequencies())
+
+ idle_qubits = self.params["idle_qubits"]
+ qubits = target_result.nqubits
+
+ if bounds:
+ qubits = target_result.nqubits
+ lb = np.zeros(4 * qubits + 4)
+ ub = [10000] * (2 * qubits + 2) + [4 / 3, 16 / 15] + [1] * 2 * qubits
+ else:
+ lb = bounds[0]
+ ub = bounds[1]
+ bounds = Bounds(lb, ub)
+
+ shot_error = True
+ args = (circuit, nshots, target_prob, idle_qubits, backend, shot_error)
+ result = np.inf
+ while result == np.inf:
+ initial_params = np.random.uniform(lb, ub)
+ result = loss(initial_params, *args)
+
+ if f_min_rtol is None:
+ f_min_rtol = result[1]
+
+ args = list(args)
+ args[5] = False
+ args = tuple(args)
+
+ self.hellinger0 = {"fidelity": abs(result[0]), "shot_error": result[1]}
+
+ res = direct(
+ loss,
+ bounds,
+ args=args,
+ eps=eps,
+ maxfun=maxfun,
+ maxiter=maxiter,
+ locally_biased=locally_biased,
+ f_min=-1,
+ f_min_rtol=f_min_rtol,
+ vol_tol=vol_tol,
+ len_tol=len_tol,
+ callback=callback,
+ )
+
+ parameters = res.x
+ params = {
+ "t1": tuple(parameters[0:qubits]),
+ "t2": tuple(parameters[qubits : 2 * qubits]),
+ "gate_time": tuple(parameters[2 * qubits : 2 * qubits + 2]),
+ "excited_population": 0,
+ "depolarizing_error": tuple(parameters[2 * qubits + 2 : 2 * qubits + 4]),
+ "bitflips_error": (
+ parameters[2 * qubits + 4 : 3 * qubits + 4],
+ parameters[3 * qubits + 4 : 4 * qubits + 4],
+ ),
+ "idle_qubits": idle_qubits,
+ }
+ self.hellinger = abs(res.fun)
+ self.params = params
+ self.extra = {
+ "success": res.success,
+ "message": res.message,
+ "nfev": res.nfev,
+ "nit": res.nit,
+ }
diff --git a/src/qibo/optimizers.py b/src/qibo/optimizers.py
new file mode 100644
index 000000000..2696deda2
--- /dev/null
+++ b/src/qibo/optimizers.py
@@ -0,0 +1,442 @@
+from qibo.config import log, raise_error
+
+
+def optimize(
+ loss,
+ initial_parameters,
+ args=(),
+ method="Powell",
+ jac=None,
+ hess=None,
+ hessp=None,
+ bounds=None,
+ constraints=(),
+ tol=None,
+ callback=None,
+ options=None,
+ compile=False,
+ processes=None,
+ backend=None,
+):
+ """Main optimization method. Selects one of the following optimizers:
+ - :meth:`qibo.optimizers.cmaes`
+ - :meth:`qibo.optimizers.newtonian`
+ - :meth:`qibo.optimizers.sgd`
+
+ Args:
+ loss (callable): Loss as a function of ``parameters`` and optional extra
+ arguments. Make sure the loss function returns a tensor for ``method=sgd``
+ and numpy object for all the other methods.
+ initial_parameters (np.ndarray): Initial guess for the variational
+ parameters that are optimized.
+ args (tuple): optional arguments for the loss function.
+ method (str): Name of optimizer to use. Can be ``'cma'``, ``'sgd'`` or
+ one of the Newtonian methods supported by
+ :meth:`qibo.optimizers.newtonian` and ``'parallel_L-BFGS-B'``. ``sgd`` is
+ only available for backends based on tensorflow.
+ jac (dict): Method for computing the gradient vector for scipy optimizers.
+ hess (dict): Method for computing the hessian matrix for scipy optimizers.
+ hessp (callable): Hessian of objective function times an arbitrary
+ vector for scipy optimizers.
+ bounds (sequence or Bounds): Bounds on variables for scipy optimizers.
+ constraints (dict): Constraints definition for scipy optimizers.
+ tol (float): Tolerance of termination for scipy optimizers.
+ callback (callable): Called after each iteration for scipy optimizers.
+ options (dict): Dictionary with options. See the specific optimizer
+ bellow for a list of the supported options.
+ compile (bool): If ``True`` the Tensorflow optimization graph is compiled.
+ This is relevant only for the ``'sgd'`` optimizer.
+ processes (int): number of processes when using the parallel BFGS method.
+
+ Returns:
+ (float, float, custom): Final best loss value; best parameters obtained by the optimizer; extra: optimizer-specific return object. For scipy methods it
+ returns the ``OptimizeResult``, for ``'cma'`` the ``CMAEvolutionStrategy.result``,
+ and for ``'sgd'`` the options used during the optimization.
+
+
+ Example:
+ .. testcode::
+
+ import numpy as np
+ from qibo import gates, models
+ from qibo.optimizers import optimize
+
+ # create custom loss function
+ # make sure the return type matches the optimizer requirements.
+ def myloss(parameters, circuit):
+ circuit.set_parameters(parameters)
+ return np.square(np.sum(circuit().state())) # returns numpy array
+
+ # create circuit ansatz for two qubits
+ circuit = models.Circuit(2)
+ circuit.add(gates.RY(0, theta=0))
+
+ # optimize using random initial variational parameters
+ initial_parameters = np.random.uniform(0, 2, 1)
+ best, params, extra = optimize(myloss, initial_parameters, args=(circuit))
+
+ # set parameters to circuit
+ circuit.set_parameters(params)
+ """
+ if method == "cma":
+ if bounds is not None: # pragma: no cover
+ raise_error(
+ RuntimeError,
+ "The keyword 'bounds' cannot be used with the cma optimizer. Please use 'options' instead as defined by the cma documentation: ex. options['bounds'] = [0.0, 1.0].",
+ )
+ return cmaes(loss, initial_parameters, args, callback, options)
+ elif method == "sgd":
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ return sgd(loss, initial_parameters, args, callback, options, compile, backend)
+ else:
+ from qibo.backends import _check_backend
+
+ backend = _check_backend(backend)
+
+ return newtonian(
+ loss,
+ initial_parameters,
+ args,
+ method,
+ jac,
+ hess,
+ hessp,
+ bounds,
+ constraints,
+ tol,
+ callback,
+ options,
+ processes,
+ backend,
+ )
+
+
+def cmaes(loss, initial_parameters, args=(), callback=None, options=None):
+ """Genetic optimizer based on `pycma `_.
+
+ Args:
+ loss (callable): Loss as a function of variational parameters to be
+ optimized.
+ initial_parameters (np.ndarray): Initial guess for the variational
+ parameters.
+ args (tuple): optional arguments for the loss function.
+ callback (list[callable]): List of callable called after each optimization
+ iteration. According to cma-es implementation take ``CMAEvolutionStrategy``
+ instance as argument.
+ See: https://cma-es.github.io/apidocs-pycma/cma.evolution_strategy.CMAEvolutionStrategy.html.
+ options (dict): Dictionary with options accepted by the ``cma``
+ optimizer. The user can use ``import cma; cma.CMAOptions()`` to view the
+ available options.
+ """
+ import cma
+
+ es = cma.CMAEvolutionStrategy(initial_parameters, sigma0=1.7, inopts=options)
+
+ if callback is not None:
+ while not es.stop():
+ solutions = es.ask()
+ objective_values = [loss(x, *args) for x in solutions]
+ for solution in solutions:
+ callback(solution)
+ es.tell(solutions, objective_values)
+ es.logger.add()
+ else:
+ es.optimize(loss, args=args)
+
+ return es.result.fbest, es.result.xbest, es.result
+
+
+def newtonian(
+ loss,
+ initial_parameters,
+ args=(),
+ method="Powell",
+ jac=None,
+ hess=None,
+ hessp=None,
+ bounds=None,
+ constraints=(),
+ tol=None,
+ callback=None,
+ options=None,
+ processes=None,
+ backend=None,
+):
+ """Newtonian optimization approaches based on ``scipy.optimize.minimize``.
+
+ For more details check the `scipy documentation `_.
+
+ .. note::
+ When using the method ``parallel_L-BFGS-B`` the ``processes`` option controls the
+ number of processes used by the parallel L-BFGS-B algorithm through the ``multiprocessing`` library.
+ By default ``processes=None``, in this case the total number of logical cores are used.
+ Make sure to select the appropriate number of processes for your computer specification,
+ taking in consideration memory and physical cores. In order to obtain optimal results
+ you can control the number of threads used by each process with the ``qibo.set_threads`` method.
+ For example, for small-medium size circuits you may benefit from single thread per process, thus set
+ ``qibo.set_threads(1)`` before running the optimization.
+
+ Args:
+ loss (callable): Loss as a function of variational parameters to be
+ optimized.
+ initial_parameters (np.ndarray): Initial guess for the variational
+ parameters.
+ args (tuple): optional arguments for the loss function.
+ method (str): Name of method supported by ``scipy.optimize.minimize`` and ``'parallel_L-BFGS-B'`` for
+ a parallel version of L-BFGS-B algorithm.
+ jac (dict): Method for computing the gradient vector for scipy optimizers.
+ hess (dict): Method for computing the hessian matrix for scipy optimizers.
+ hessp (callable): Hessian of objective function times an arbitrary
+ vector for scipy optimizers.
+ bounds (sequence or Bounds): Bounds on variables for scipy optimizers.
+ constraints (dict): Constraints definition for scipy optimizers.
+ tol (float): Tolerance of termination for scipy optimizers.
+ callback (callable): Called after each iteration for scipy optimizers.
+ options (dict): Dictionary with options accepted by
+ ``scipy.optimize.minimize``.
+ processes (int): number of processes when using the parallel BFGS method.
+ """
+ if method == "parallel_L-BFGS-B": # pragma: no cover
+ o = ParallelBFGS(
+ loss,
+ args=args,
+ processes=processes,
+ bounds=bounds,
+ callback=callback,
+ options=options,
+ )
+ m = o.run(initial_parameters)
+ else:
+ from scipy.optimize import minimize
+
+ m = minimize(
+ loss,
+ initial_parameters,
+ args=args,
+ method=method,
+ jac=jac,
+ hess=hess,
+ hessp=hessp,
+ bounds=bounds,
+ constraints=constraints,
+ tol=tol,
+ callback=callback,
+ options=options,
+ )
+ return m.fun, m.x, m
+
+
+def sgd(
+ loss,
+ initial_parameters,
+ args=(),
+ callback=None,
+ options=None,
+ compile=False,
+ backend=None,
+):
+ """Stochastic Gradient Descent (SGD) optimizer using Tensorflow backpropagation.
+
+ See `tf.keras.Optimizers `_
+ for a list of the available optimizers for Tensorflow.
+ See `torch.optim `_ for a list of the available
+ optimizers for PyTorch.
+
+ Args:
+ loss (callable): Loss as a function of variational parameters to be
+ optimized.
+ initial_parameters (np.ndarray): Initial guess for the variational
+ parameters.
+ args (tuple): optional arguments for the loss function.
+ callback (callable): Called after each iteration.
+ options (dict): Dictionary with options for the SGD optimizer. Supports
+ the following keys:
+
+ - ``'optimizer'`` (str, default: ``'Adagrad'``): Name of optimizer.
+ - ``'learning_rate'`` (float, default: ``'1e-3'``): Learning rate.
+ - ``'nepochs'`` (int, default: ``1e6``): Number of epochs for optimization.
+ - ``'nmessage'`` (int, default: ``1e3``): Every how many epochs to print
+ a message of the loss function.
+ """
+
+ sgd_options = {
+ "nepochs": 1000000,
+ "nmessage": 1000,
+ "optimizer": "Adagrad",
+ "learning_rate": 0.001,
+ }
+ if options is not None:
+ sgd_options.update(options)
+
+ if backend.name == "tensorflow":
+ return _sgd_tf(
+ loss,
+ initial_parameters,
+ args,
+ sgd_options,
+ compile,
+ backend,
+ callback=callback,
+ )
+
+ if backend.name == "pytorch":
+ if compile:
+ log.warning(
+ "PyTorch does not support compilation of the optimization graph."
+ )
+ return _sgd_torch(
+ loss, initial_parameters, args, sgd_options, backend, callback=callback
+ )
+
+ raise_error(RuntimeError, "SGD optimizer requires Tensorflow or PyTorch backend.")
+
+
+def _sgd_torch(loss, initial_parameters, args, sgd_options, backend, callback=None):
+
+ vparams = initial_parameters
+ optimizer = getattr(backend.np.optim, sgd_options["optimizer"])(
+ params=[vparams], lr=sgd_options["learning_rate"]
+ )
+
+ for e in range(sgd_options["nepochs"]):
+ optimizer.zero_grad()
+ l = loss(vparams, *args)
+ l.backward()
+ optimizer.step()
+ if callback is not None:
+ callback(backend.to_numpy(vparams))
+ if e % sgd_options["nmessage"] == 1:
+ log.info("ite %d : loss %f", e, l.item())
+
+ return loss(vparams, *args).item(), vparams.detach().numpy(), sgd_options
+
+
+def _sgd_tf(
+ loss, initial_parameters, args, sgd_options, compile, backend, callback=None
+):
+
+ vparams = backend.tf.Variable(initial_parameters)
+ optimizer = getattr(backend.tf.optimizers, sgd_options["optimizer"])(
+ learning_rate=sgd_options["learning_rate"]
+ )
+
+ def opt_step():
+ with backend.tf.GradientTape() as tape:
+ l = loss(vparams, *args)
+ grads = tape.gradient(l, [vparams])
+ optimizer.apply_gradients(zip(grads, [vparams]))
+ return l
+
+ if compile:
+ loss = backend.compile(loss)
+ opt_step = backend.compile(opt_step)
+
+ for e in range(sgd_options["nepochs"]):
+ l = opt_step()
+ if callback is not None:
+ callback(vparams)
+ if e % sgd_options["nmessage"] == 1:
+ log.info("ite %d : loss %f", e, l.numpy())
+
+ return loss(vparams, *args).numpy(), vparams.numpy(), sgd_options
+
+
+class ParallelBFGS: # pragma: no cover
+ """Computes the L-BFGS-B using parallel evaluation using multiprocessing.
+ This implementation here is based on https://doi.org/10.32614/RJ-2019-030.
+
+ Args:
+ function (function): loss function which returns a numpy object.
+ args (tuple): optional arguments for the loss function.
+ bounds (list): list of bound values for ``scipy.optimize.minimize`` L-BFGS-B.
+ callback (function): function callback ``scipy.optimize.minimize`` L-BFGS-B.
+ options (dict): follows ``scipy.optimize.minimize`` syntax for L-BFGS-B.
+ processes (int): number of processes when using the paralle BFGS method.
+ """
+
+ import numpy as np
+
+ def __init__(
+ self,
+ function,
+ args=(),
+ bounds=None,
+ callback=None,
+ options=None,
+ processes=None,
+ ):
+ self.function = function
+ self.args = args
+ self.xval = None
+ self.function_value = None
+ self.jacobian_value = None
+ self.precision = self.np.finfo("float64").eps
+ self.bounds = bounds
+ self.callback = callback
+ self.options = options
+ self.processes = processes
+
+ def run(self, x0):
+ """Executes parallel L-BFGS-B minimization.
+ Args:
+ x0 (numpy.array): guess for initial solution.
+
+ Returns:
+ scipy.minimize result object
+ """
+ from scipy.optimize import minimize
+
+ out = minimize(
+ fun=self.fun,
+ x0=x0,
+ jac=self.jac,
+ method="L-BFGS-B",
+ bounds=self.bounds,
+ callback=self.callback,
+ options=self.options,
+ )
+ out.hess_inv = out.hess_inv * self.np.identity(len(x0))
+ return out
+
+ @staticmethod
+ def _eval_approx(eps_at, fun, x, eps):
+ if eps_at == 0:
+ x_ = x
+ else:
+ x_ = x.copy()
+ if eps_at <= len(x):
+ x_[eps_at - 1] += eps
+ else:
+ x_[eps_at - 1 - len(x)] -= eps
+ return fun(x_)
+
+ def evaluate(self, x, eps=1e-8):
+ if not (
+ self.xval is not None and all(abs(self.xval - x) <= self.precision * 2)
+ ):
+ eps_at = range(len(x) + 1)
+ self.xval = x.copy()
+
+ def operation(epsi):
+ return self._eval_approx(
+ epsi, lambda y: self.function(y, *self.args), x, eps
+ )
+
+ from joblib import Parallel, delayed
+
+ ret = Parallel(self.processes, prefer="threads")(
+ delayed(operation)(epsi) for epsi in eps_at
+ )
+ self.function_value = ret[0]
+ self.jacobian_value = (ret[1 : (len(x) + 1)] - self.function_value) / eps
+
+ def fun(self, x):
+ self.evaluate(x)
+ return self.function_value
+
+ def jac(self, x):
+ self.evaluate(x)
+ return self.jacobian_value
diff --git a/src/qibo/parallel.py b/src/qibo/parallel.py
new file mode 100644
index 000000000..22a65fafe
--- /dev/null
+++ b/src/qibo/parallel.py
@@ -0,0 +1,183 @@
+"""
+Resources for parallel circuit evaluation.
+"""
+
+from typing import Iterable
+
+from joblib import Parallel, delayed
+
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+
+
+def parallel_execution(circuit, states, processes=None, backend=None):
+ """Execute circuit for multiple states.
+
+ Example:
+ .. code-block:: python
+
+ import qibo
+ qibo.set_backend('qibojit')
+ from qibo import models, set_threads
+ from qibo.parallel import parallel_execution
+ import numpy as np
+ # create circuit
+ nqubits = 22
+ circuit = models.QFT(nqubits)
+ # create random states
+ states = [ np.random.random(2**nqubits) for i in range(5)]
+ # set threads to 1 per process (optional, requires tuning)
+ set_threads(1)
+ # execute in parallel
+ results = parallel_execution(circuit, states, processes=2)
+
+ Args:
+ circuit (qibo.models.Circuit): the input circuit.
+ states (list): list of states for the circuit evaluation.
+ processes (int): number of processes for parallel evaluation.
+
+ Returns:
+ Circuit evaluation for input states.
+ """
+ backend = _check_backend(backend)
+
+ if states is None or not isinstance(states, list): # pragma: no cover
+ raise_error(TypeError, "states must be a list.")
+
+ def operation(state, circuit):
+ backend.set_threads(backend.nthreads)
+ return backend.execute_circuit(circuit, state)
+
+ results = Parallel(n_jobs=processes, prefer="threads")(
+ delayed(operation)(state, circuit) for state in states
+ )
+
+ return results
+
+
+def parallel_circuits_execution(
+ circuits, states=None, nshots=1000, processes=None, backend=None
+):
+ """Execute multiple circuits
+
+ Example:
+ .. code-block:: python
+
+ import qibo
+ qibo.set_backend('qibojit')
+ from qibo import models, set_threads
+ from qibo.parallel import parallel_circuits_execution
+ import numpy as np
+ # create different circuits
+ circuits = [models.QFT(n) for n in range(5, 16)]
+ # set threads to 1 per process (optional, requires tuning)
+ set_threads(1)
+ # execute in parallel
+ results = parallel_circuits_execution(circuits, processes=2)
+
+ Args:
+ circuits (list): list of circuits to execute.
+ states (optional, list): list of states to use as initial for each circuit.
+ Must have the same length as ``circuits``.
+ If one state is given it is used on all circuits.
+ If not given the default initial state on all circuits.
+ nshots (int): Number of shots when performing measurements, same for all circuits.
+ processes (int): number of processes for parallel evaluation.
+
+ Returns:
+ Circuit evaluation for input states.
+ """
+ backend = _check_backend(backend)
+
+ if not isinstance(circuits, Iterable): # pragma: no cover
+ raise_error(TypeError, "circuits must be iterable.")
+
+ if (
+ isinstance(states, (list, tuple))
+ and isinstance(circuits, (list, tuple))
+ and len(states) != len(circuits)
+ ):
+ raise_error(ValueError, "states must have the same length as circuits.")
+ elif states is not None and not isinstance(states, Iterable):
+ raise_error(TypeError, "states must be iterable.")
+
+ def operation(circuit, state):
+ backend.set_threads(backend.nthreads)
+ return backend.execute_circuit(circuit, state, nshots)
+
+ if states is None or isinstance(states, backend.tensor_types):
+ results = Parallel(n_jobs=processes, prefer="threads")(
+ delayed(operation)(circuit, states) for circuit in circuits
+ )
+ else:
+ results = Parallel(n_jobs=processes, prefer="threads")(
+ delayed(operation)(circuit, state)
+ for circuit, state in zip(circuits, states)
+ )
+
+ return results
+
+
+def parallel_parametrized_execution(
+ circuit, parameters, initial_state=None, processes=None, backend=None
+):
+ """Execute circuit for multiple parameters and fixed initial_state.
+
+ Example:
+ .. code-block:: python
+
+ import qibo
+ qibo.set_backend('qibojit')
+ from qibo import models, gates, set_threads
+ from qibo.parallel import parallel_parametrized_execution
+ import numpy as np
+ # create circuit
+ nqubits = 6
+ nlayers = 2
+ circuit = models.Circuit(nqubits)
+ for l in range(nlayers):
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(0, nqubits-1, 2)))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ circuit.add((gates.CZ(q, q+1) for q in range(1, nqubits-2, 2)))
+ circuit.add(gates.CZ(0, nqubits-1))
+ circuit.add((gates.RY(q, theta=0) for q in range(nqubits)))
+ # create random parameters
+ size = len(circuit.get_parameters())
+ parameters = [ np.random.uniform(0, 2*np.pi, size) for _ in range(10) ]
+ # set threads to 1 per process (optional, requires tuning)
+ set_threads(1)
+ # execute in parallel
+ results = parallel_parametrized_execution(circuit, parameters, processes=2)
+
+ Args:
+ circuit (qibo.models.Circuit): the input circuit.
+ parameters (list): list of parameters for the circuit evaluation.
+ initial_state (np.array): initial state for the circuit evaluation.
+ processes (int): number of processes for parallel evaluation.
+ This corresponds to the number of threads, if a single thread is used
+ for each circuit evaluation. If more threads are used for each circuit
+ evaluation then some tuning may be required to obtain optimal performance.
+ Default is ``None`` which corresponds to a single thread.
+
+ Returns:
+ Circuit evaluation for input parameters.
+ """
+ backend = _check_backend(backend)
+
+ if not isinstance(parameters, list): # pragma: no cover
+ raise_error(TypeError, "parameters must be a list.")
+
+ def operation(params, circuit, state):
+ backend.set_threads(backend.nthreads)
+ if state is not None:
+ state = backend.cast(state, copy=True)
+ circuit.set_parameters(params)
+ return backend.execute_circuit(circuit, state)
+
+ results = Parallel(n_jobs=processes, prefer="threads")(
+ delayed(operation)(param, circuit.copy(deep=True), initial_state)
+ for param in parameters
+ )
+
+ return results
diff --git a/src/qibo/parameter.py b/src/qibo/parameter.py
new file mode 100644
index 000000000..64a99651f
--- /dev/null
+++ b/src/qibo/parameter.py
@@ -0,0 +1,130 @@
+import numpy as np
+import sympy as sp
+
+from qibo.config import raise_error
+
+
+def calculate_derivatives(func):
+ """Calculates derivatives w.r.t. to all parameters of a target function `func`."""
+ vars = []
+ for i in range(func.__code__.co_argcount):
+ vars.append(sp.Symbol(f"p{i}"))
+
+ expr = sp.sympify(func(*vars))
+
+ derivatives = []
+ for i in range(len(vars)):
+ derivative_expr = sp.diff(expr, vars[i])
+ derivatives.append(sp.lambdify(vars, derivative_expr))
+
+ return derivatives
+
+
+class Parameter:
+ """Object which allows for variational gate parameters. Several trainable parameters
+ and possibly features are linked through a lambda function which returns the
+ final gate parameter. All possible analytical derivatives of the lambda function are
+ calculated at the object initialisation using Sympy.
+
+ Example::
+
+ from qibo.parameter import Parameter
+ param = Parameter(
+ lambda x, th1, th2, th3: x**2 * th1 + th2 * th3**2,
+ features=[7.0],
+ trainable=[1.5, 2.0, 3.0],
+ )
+
+ partial_derivative = param.get_partial_derivative(3)
+
+ param.update_parameters(trainable=[15.0, 10.0, 7.0], feature=[5.0])
+ param_value = param()
+
+
+ Args:
+ func (function): lambda function which builds the gate parameter. If both features and trainable parameters
+ compose the function, it must be passed by first providing the features and then the parameters, as
+ described in the code example above.
+ features (list or np.ndarray): array containing possible input features x.
+ trainable (list or np.ndarray): array with initial trainable parameters theta.
+ """
+
+ def __init__(self, func, trainable=None, features=None):
+ self.trainable = trainable if trainable is not None else []
+ self.features = features if features is not None else []
+
+ if self.nfeat + self.nparams != func.__code__.co_argcount:
+ raise_error(
+ TypeError,
+ f"{self.nfeat + self.nparams} parameters are provided, but {func.__code__.co_argcount} are required, please initialize features and trainable according to the defined function.",
+ )
+ # lambda function
+ self.lambdaf = func
+
+ # calculate derivatives
+ # maybe here use JAX ?
+ self.derivatives = calculate_derivatives(func=self.lambdaf)
+
+ def __call__(self, features=None, trainable=None):
+ """Return parameter value with given features and/or trainable."""
+
+ params = []
+
+ if features is None:
+ params.extend(self.features)
+ else:
+ if len(features) != self.nfeat:
+ raise_error(
+ TypeError,
+ f"The number of features provided is not compatible with the problem's dimensionality, which is {self.nfeat}.",
+ )
+ else:
+ params.extend(features)
+ if trainable is None:
+ params.extend(self.trainable)
+ else:
+ if len(trainable) != self.nparams:
+ raise_error(
+ TypeError,
+ f"The number of trainable provided is different from the number of required parameters, which is {self.nparams}.",
+ )
+ else:
+ params.extend(trainable)
+
+ return self.lambdaf(*params)
+
+ @property
+ def nparams(self):
+ """Returns the number of trainable parameters"""
+ return len(self.trainable)
+
+ @property
+ def nfeat(self):
+ """Returns the number of features"""
+ return len(self.features)
+
+ @property
+ def ncomponents(self):
+ """Return the number of elements which compose the Parameter"""
+ return self.nparams + self.nfeat
+
+ def trainable_parameter_indices(self, start_index):
+ """Return list of respective indices of trainable parameters within
+ the larger trainable parameter list of a circuit for example"""
+ return (np.arange(self.nparams) + start_index).tolist()
+
+ def unaffected_by(self, trainable_idx):
+ """Retrieve constant term of lambda function with regard to a specific trainable parameter"""
+ params = self.trainable.copy()
+ params[trainable_idx] = 0.0
+ return self(trainable=params)
+
+ def partial_derivative(self, trainable_idx):
+ """Get derivative w.r.t a trainable parameter"""
+ deriv = self.derivatives[trainable_idx]
+
+ params = []
+ params.extend(self.features)
+ params.extend(self.trainable)
+
+ return deriv(*params)
diff --git a/src/qibo/quantum_info/__init__.py b/src/qibo/quantum_info/__init__.py
new file mode 100644
index 000000000..9835cb962
--- /dev/null
+++ b/src/qibo/quantum_info/__init__.py
@@ -0,0 +1,10 @@
+from qibo.quantum_info.basis import *
+from qibo.quantum_info.clifford import *
+from qibo.quantum_info.entanglement import *
+from qibo.quantum_info.entropies import *
+from qibo.quantum_info.linalg_operations import *
+from qibo.quantum_info.metrics import *
+from qibo.quantum_info.quantum_networks import *
+from qibo.quantum_info.random_ensembles import *
+from qibo.quantum_info.superoperator_transformations import *
+from qibo.quantum_info.utils import *
diff --git a/src/qibo/quantum_info/_clifford_utils.py b/src/qibo/quantum_info/_clifford_utils.py
new file mode 100644
index 000000000..176aa2d28
--- /dev/null
+++ b/src/qibo/quantum_info/_clifford_utils.py
@@ -0,0 +1,569 @@
+"""Utility functions that support the Clifford submodule."""
+
+from functools import reduce
+from itertools import product
+
+import numpy as np
+
+from qibo import Circuit, gates
+from qibo.config import raise_error
+
+
+def _one_qubit_paulis_string_product(pauli_1: str, pauli_2: str):
+ """Calculate the product of two single-qubit Paulis represented as strings.
+
+ Args:
+ pauli_1 (str): First Pauli operator.
+ pauli_2 (str): Second Pauli operator.
+
+ Returns:
+ (str): Product of the two Pauli operators.
+ """
+ products = {
+ "XY": "iZ",
+ "YZ": "iX",
+ "ZX": "iY",
+ "YX": "-iZ",
+ "ZY": "-iX",
+ "XZ": "iY",
+ "XX": "I",
+ "ZZ": "I",
+ "YY": "I",
+ "XI": "X",
+ "IX": "X",
+ "YI": "Y",
+ "IY": "Y",
+ "ZI": "Z",
+ "IZ": "Z",
+ }
+ prod = products[
+ "".join([p.replace("i", "").replace("-", "") for p in (pauli_1, pauli_2)])
+ ]
+ # calculate the phase
+ sign = len([True for p in (pauli_1, pauli_2, prod) if "-" in p])
+ n_i = len([True for p in (pauli_1, pauli_2, prod) if "i" in p])
+ sign = "-" if sign % 2 == 1 else ""
+ if n_i == 0:
+ i = ""
+ elif n_i == 1:
+ i = "i"
+ elif n_i == 2:
+ i = ""
+ sign = "-" if sign == "" else ""
+ elif n_i == 3:
+ i = "i"
+ sign = "-" if sign == "" else ""
+ return "".join([sign, i, prod.replace("i", "").replace("-", "")])
+
+
+def _string_product(operators: list):
+ """Calculates the tensor product of a list of operators represented as strings.
+
+ Args:
+ operators (list): list of operators.
+
+ Returns:
+ (str): String representing the tensor product of the operators.
+ """
+ # calculate global sign
+ phases = len([True for op in operators if "-" in op])
+ i = len([True for op in operators if "i" in op])
+ # remove the - signs and the i
+ operators = "|".join(operators).replace("-", "").replace("i", "").split("|")
+
+ prod = []
+ for op in zip(*operators):
+ op = [o for o in op if o != "I"]
+ if len(op) == 0:
+ tmp = "I"
+ elif len(op) > 1:
+ tmp = reduce(_one_qubit_paulis_string_product, op)
+ else:
+ tmp = op[0]
+ # append signs coming from products
+ if tmp[0] == "-":
+ phases += 1
+ # count i coming from products
+ if "i" in tmp:
+ i += 1
+ prod.append(tmp.replace("i", "").replace("-", ""))
+ result = "".join(prod)
+
+ # product of the i-s
+ if i % 4 == 1 or i % 4 == 3:
+ result = f"i{result}"
+ if i % 4 == 2 or i % 4 == 3:
+ phases += 1
+
+ phases = "-" if phases % 2 == 1 else ""
+
+ return f"{phases}{result}"
+
+
+def _decomposition_AG04(clifford):
+ """Returns a Clifford object decomposed into a circuit based on Aaronson-Gottesman method.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: Clifford circuit.
+
+ References:
+ 1. S. Aaronson, D. Gottesman, *Improved Simulation of Stabilizer Circuits*,
+ Phys. Rev. A 70, 052328 (2004).
+ `arXiv:quant-ph/0406196 `_
+ """
+ nqubits = clifford.nqubits
+
+ circuit = Circuit(nqubits)
+ clifford_copy = clifford.copy(deep=True)
+
+ if nqubits == 1:
+ return _single_qubit_clifford_decomposition(clifford_copy.symplectic_matrix)
+
+ for k in range(nqubits):
+ # put a 1 one into position by permuting and using Hadamards(i,i)
+ _set_qubit_x_to_true(clifford_copy, circuit, k)
+ # make all entries in row i except ith equal to 0
+ # by using phase gate and CNOTS
+ _set_row_x_to_zero(clifford_copy, circuit, k)
+ # treat Zs
+ _set_row_z_to_zero(clifford_copy, circuit, k)
+
+ for k in range(nqubits):
+ if clifford_copy.symplectic_matrix[:nqubits, -1][k]:
+ clifford_copy.symplectic_matrix = clifford._backend.engine.Z(
+ clifford_copy.symplectic_matrix, k, nqubits
+ )
+ circuit.add(gates.Z(k))
+ if clifford_copy.symplectic_matrix[nqubits:-1, -1][k]:
+ clifford_copy.symplectic_matrix = clifford._backend.engine.X(
+ clifford_copy.symplectic_matrix, k, nqubits
+ )
+ circuit.add(gates.X(k))
+
+ return circuit.invert()
+
+
+def _decomposition_BM20(clifford):
+ """Optimal CNOT-cost decomposition of a Clifford operator on :math:`n \\in \\{2, 3 \\}`
+ into a circuit based on Bravyi-Maslov method.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: Clifford circuit.
+
+ References:
+ 1. S. Bravyi, D. Maslov, *Hadamard-free circuits expose the structure of the Clifford group*,
+ `arXiv:2003.09412 [quant-ph] `_.
+ """
+ nqubits = clifford.nqubits
+ clifford_copy = clifford.copy(deep=True)
+
+ if nqubits > 3:
+ raise_error(
+ ValueError, "This method can only be implemented for ``nqubits <= 3``."
+ )
+
+ if nqubits == 1:
+ return _single_qubit_clifford_decomposition(clifford_copy.symplectic_matrix)
+
+ inverse_circuit = Circuit(nqubits)
+
+ cnot_cost = _cnot_cost(clifford_copy)
+
+ while cnot_cost > 0:
+ clifford_copy, inverse_circuit, cnot_cost = _reduce_cost(
+ clifford_copy, inverse_circuit, cnot_cost
+ )
+
+ last_row = clifford_copy.engine.cast([False] * 3, dtype=bool)
+ circuit = Circuit(nqubits)
+ for qubit in range(nqubits):
+ position = [qubit, qubit + nqubits]
+ single_qubit_circuit = _single_qubit_clifford_decomposition(
+ clifford_copy.engine.np.append(
+ clifford_copy.symplectic_matrix[position][:, position + [-1]], last_row
+ ).reshape(3, 3)
+ )
+ if len(single_qubit_circuit.queue) > 0:
+ for gate in single_qubit_circuit.queue:
+ gate.init_args = [qubit]
+ gate.target_qubits = (qubit,)
+ circuit.queue.extend([gate])
+
+ if len(inverse_circuit.queue) > 0:
+ circuit.queue.extend(inverse_circuit.invert().queue)
+
+ return circuit
+
+
+def _single_qubit_clifford_decomposition(symplectic_matrix):
+ """Decompose symplectic matrix of a single-qubit Clifford into a Clifford circuit.
+
+ Args:
+ symplectic_matrix (ndarray): Symplectic matrix to be decomposed.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: Clifford circuit.
+ """
+ circuit = Circuit(nqubits=1)
+
+ destabilizer_phase, stabilizer_phase = symplectic_matrix[:-1, -1]
+ if destabilizer_phase and not stabilizer_phase:
+ circuit.add(gates.Z(0))
+ elif not destabilizer_phase and stabilizer_phase:
+ circuit.add(gates.X(0))
+ elif destabilizer_phase and stabilizer_phase:
+ circuit.add(gates.Y(0))
+
+ destabilizer_x, destabilizer_z = symplectic_matrix[0, 0], symplectic_matrix[0, 1]
+ stabilizer_x, stabilizer_z = symplectic_matrix[1, 0], symplectic_matrix[1, 1]
+
+ if stabilizer_z and not stabilizer_x:
+ if destabilizer_z:
+ circuit.add(gates.S(0))
+ elif not stabilizer_z and stabilizer_x:
+ if destabilizer_x:
+ circuit.add(gates.SDG(0))
+ circuit.add(gates.H(0))
+ else:
+ if not destabilizer_z:
+ circuit.add(gates.S(0))
+ circuit.add(gates.H(0))
+ circuit.add(gates.S(0))
+
+ return circuit
+
+
+def _set_qubit_x_to_true(clifford, circuit: Circuit, qubit: int):
+ """Set a :math:`X`-destabilizer to ``True``.
+
+ This is done by permuting columns ``l > qubit`` or, if necessary, applying a Hadamard.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit object.
+ qubit (int): index of the qubit to operate on.
+ """
+ nqubits = clifford.nqubits
+
+ x = clifford.destabilizers(symplectic=True)
+ x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit]
+
+ if x[qubit]:
+ return
+
+ for k in range(qubit + 1, nqubits):
+ if np.all(x[k]):
+ clifford.symplectic_matrix = clifford._backend.engine.SWAP(
+ clifford.symplectic_matrix, k, qubit, nqubits
+ )
+ circuit.add(gates.SWAP(k, qubit))
+ return
+
+ for k in range(qubit, nqubits):
+ if np.all(z[k]):
+ clifford.symplectic_matrix = clifford._backend.engine.H(
+ clifford.symplectic_matrix, k, nqubits
+ )
+ circuit.add(gates.H(k))
+ if k != qubit:
+ clifford.symplectic_matrix = clifford._backend.engine.SWAP(
+ clifford.symplectic_matrix, k, qubit, nqubits
+ )
+ circuit.add(gates.SWAP(k, qubit))
+ return
+
+
+def _set_row_x_to_zero(clifford, circuit: Circuit, qubit: int):
+ """Set :math:`X`-destabilizer to ``False`` for all ``k > qubit``.
+
+ This is done by applying CNOTs, assuming ``k <= N`` and ``clifford.symplectic_matrix[k][k]=1``.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit object.
+ qubit (int): index of the qubit to operate on.
+ """
+ nqubits = clifford.nqubits
+
+ x = clifford.destabilizers(symplectic=True)
+ x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit]
+
+ # Check X first
+ for k in range(qubit + 1, nqubits):
+ if x[k]:
+ clifford.symplectic_matrix = clifford._backend.engine.CNOT(
+ clifford.symplectic_matrix, qubit, k, nqubits
+ )
+ circuit.add(gates.CNOT(qubit, k))
+
+ if np.any(z[qubit:]):
+ if not z[qubit]:
+ # to treat Zs: make sure row.Z[k] to True
+ clifford.symplectic_matrix = clifford._backend.engine.S(
+ clifford.symplectic_matrix, qubit, nqubits
+ )
+ circuit.add(gates.S(qubit))
+
+ for k in range(qubit + 1, nqubits):
+ if z[k]:
+ clifford.symplectic_matrix = clifford._backend.engine.CNOT(
+ clifford.symplectic_matrix, k, qubit, nqubits
+ )
+ circuit.add(gates.CNOT(k, qubit))
+
+ clifford.symplectic_matrix = clifford._backend.engine.S(
+ clifford.symplectic_matrix, qubit, nqubits
+ )
+ circuit.add(gates.S(qubit))
+
+
+def _set_row_z_to_zero(clifford, circuit: Circuit, qubit: int):
+ """Set :math:`Z`-stabilizer to ``False`` for all ``i > qubit``.
+
+ Implemented by applying (reverse) CNOTs.
+ It assumes ``qubit < nqubits`` and that ``_set_row_x_to_zero`` has been called first.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit object.
+ qubit (int): index of the qubit to operate on.
+ """
+ nqubits = clifford.nqubits
+
+ x = clifford.stabilizers(symplectic=True)
+ x, z = x[:, :nqubits][qubit], x[:, nqubits:-1][qubit]
+
+ if np.any(z[qubit + 1 :]):
+ for k in range(qubit + 1, nqubits):
+ if z[k]:
+ clifford.symplectic_matrix = clifford._backend.engine.CNOT(
+ clifford.symplectic_matrix, k, qubit, nqubits
+ )
+ circuit.add(gates.CNOT(k, qubit))
+
+ if np.any(x[qubit:]):
+ clifford.symplectic_matrix = clifford._backend.engine.H(
+ clifford.symplectic_matrix, qubit, nqubits
+ )
+ circuit.add(gates.H(qubit))
+ for k in range(qubit + 1, nqubits):
+ if x[k]:
+ clifford.symplectic_matrix = clifford._backend.engine.CNOT(
+ clifford.symplectic_matrix, qubit, k, nqubits
+ )
+ circuit.add(gates.CNOT(qubit, k))
+ if z[qubit]:
+ clifford.symplectic_matrix = clifford._backend.engine.S(
+ clifford.symplectic_matrix, qubit, nqubits
+ )
+ circuit.add(gates.S(qubit))
+ clifford.symplectic_matrix = clifford._backend.engine.H(
+ clifford.symplectic_matrix, qubit, nqubits
+ )
+ circuit.add(gates.H(qubit))
+
+
+def _cnot_cost(clifford):
+ """Returns the number of CNOT gates required for Clifford decomposition.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+
+ Returns:
+ int: Number of CNOT gates required.
+ """
+ if clifford.nqubits > 3:
+ raise_error(ValueError, "No Clifford CNOT cost function for ``nqubits > 3``.")
+
+ if clifford.nqubits == 3:
+ return _cnot_cost3(clifford)
+
+ return _cnot_cost2(clifford)
+
+
+def _rank_2(a: bool, b: bool, c: bool, d: bool):
+ """Returns rank of 2x2 boolean matrix."""
+ if (a & d) ^ (b & c):
+ return 2
+ if a or b or c or d:
+ return 1
+ return 0
+
+
+def _cnot_cost2(clifford):
+ """Returns CNOT cost of a two-qubit Clifford.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+
+ Returns:
+ int: Number of CNOT gates required.
+ """
+ symplectic_matrix = clifford.symplectic_matrix[:-1, :-1]
+
+ r00 = _rank_2(
+ symplectic_matrix[0, 0],
+ symplectic_matrix[0, 2],
+ symplectic_matrix[2, 0],
+ symplectic_matrix[2, 2],
+ )
+ r01 = _rank_2(
+ symplectic_matrix[0, 1],
+ symplectic_matrix[0, 3],
+ symplectic_matrix[2, 1],
+ symplectic_matrix[2, 3],
+ )
+
+ if r00 == 2:
+ return r01
+
+ return r01 + 1 - r00
+
+
+def _cnot_cost3(clifford): # pragma: no cover
+ """Return CNOT cost of a 3-qubit clifford.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+
+ Returns:
+ int: Number of CNOT gates required.
+ """
+
+ symplectic_matrix = clifford.symplectic_matrix[:-1, :-1]
+
+ nqubits = 3
+
+ R1 = np.zeros((nqubits, nqubits), dtype=int)
+ R2 = np.zeros((nqubits, nqubits), dtype=int)
+ for q1 in range(nqubits):
+ for q2 in range(nqubits):
+ R2[q1, q2] = _rank_2(
+ symplectic_matrix[q1, q2],
+ symplectic_matrix[q1, q2 + nqubits],
+ symplectic_matrix[q1 + nqubits, q2],
+ symplectic_matrix[q1 + nqubits, q2 + nqubits],
+ )
+ mask = np.zeros(2 * nqubits, dtype=int)
+ mask = clifford.engine.cast(mask, dtype=mask.dtype)
+ mask[[q2, q2 + nqubits]] = 1
+ loc_y_x = np.array_equal(
+ symplectic_matrix[q1, :] & mask, symplectic_matrix[q1, :]
+ )
+ loc_y_z = np.array_equal(
+ symplectic_matrix[q1 + nqubits, :] & mask,
+ symplectic_matrix[q1 + nqubits, :],
+ )
+ loc_y_y = np.array_equal(
+ (symplectic_matrix[q1, :] ^ symplectic_matrix[q1 + nqubits, :]) & mask,
+ (symplectic_matrix[q1, :] ^ symplectic_matrix[q1 + nqubits, :]),
+ )
+ R1[q1, q2] = 1 * (loc_y_x or loc_y_z or loc_y_y) + 1 * (
+ loc_y_x and loc_y_z and loc_y_y
+ )
+
+ diag1 = np.sort(np.diag(R1)).tolist()
+ diag2 = np.sort(np.diag(R2)).tolist()
+
+ nz1 = np.count_nonzero(R1)
+ nz2 = np.count_nonzero(R2)
+
+ if diag1 == [2, 2, 2]:
+ return 0
+
+ if diag1 == [1, 1, 2]:
+ return 1
+
+ if (
+ diag1 == [0, 1, 1]
+ or (diag1 == [1, 1, 1] and nz2 < 9)
+ or (diag1 == [0, 0, 2] and diag2 == [1, 1, 2])
+ ):
+ return 2
+
+ if (
+ (diag1 == [1, 1, 1] and nz2 == 9)
+ or (
+ diag1 == [0, 0, 1]
+ and (nz1 == 1 or diag2 == [2, 2, 2] or (diag2 == [1, 1, 2] and nz2 < 9))
+ )
+ or (diag1 == [0, 0, 2] and diag2 == [0, 0, 2])
+ or (diag2 == [1, 2, 2] and nz1 == 0)
+ ):
+ return 3
+
+ if diag2 == [0, 0, 1] or (
+ diag1 == [0, 0, 0]
+ and (
+ (diag2 == [1, 1, 1] and nz2 == 9 and nz1 == 3)
+ or (diag2 == [0, 1, 1] and nz2 == 8 and nz1 == 2)
+ )
+ ):
+ return 5
+
+ if nz1 == 3 and nz2 == 3:
+ return 6
+
+ return 4
+
+
+def _reduce_cost(clifford, inverse_circuit: Circuit, cost: int): # pragma: no cover
+ """Step that tries to reduce the two-qubit cost of a Clifford circuit.
+
+ Args:
+ clifford (:class:`qibo.quantum_info.clifford.Clifford`): Clifford object.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit object.
+ cost (int): initial cost.
+ """
+ nqubits = clifford.nqubits
+
+ for control in range(nqubits):
+ for target in range(control + 1, nqubits):
+ for n0, n1 in product(range(3), repeat=2):
+ reduced = clifford.copy(deep=True)
+ for qubit, n in [(control, n0), (target, n1)]:
+ if n == 1:
+ reduced.symplectic_matrix = reduced._backend.engine.SDG(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ reduced.symplectic_matrix = reduced._backend.engine.H(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ elif n == 2:
+ reduced.symplectic_matrix = reduced._backend.engine.SDG(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ reduced.symplectic_matrix = reduced._backend.engine.H(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ reduced.symplectic_matrix = reduced._backend.engine.SDG(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ reduced.symplectic_matrix = reduced._backend.engine.H(
+ reduced.symplectic_matrix, qubit, nqubits
+ )
+ reduced.symplectic_matrix = reduced._backend.engine.CNOT(
+ reduced.symplectic_matrix, control, target, nqubits
+ )
+
+ new_cost = _cnot_cost(reduced)
+
+ if new_cost == cost - 1:
+ for qubit, n in [(control, n0), (target, n1)]:
+ if n == 1:
+ inverse_circuit.add(gates.SDG(qubit))
+ inverse_circuit.add(gates.H(qubit))
+ elif n == 2:
+ inverse_circuit.add(gates.H(qubit))
+ inverse_circuit.add(gates.S(qubit))
+ inverse_circuit.add(gates.CNOT(control, target))
+
+ return reduced, inverse_circuit, new_cost
+
+ raise_error(RuntimeError, "Failed to reduce CNOT cost.")
diff --git a/src/qibo/quantum_info/basis.py b/src/qibo/quantum_info/basis.py
new file mode 100644
index 000000000..5ea541bca
--- /dev/null
+++ b/src/qibo/quantum_info/basis.py
@@ -0,0 +1,288 @@
+from functools import reduce
+from itertools import product
+from typing import Optional
+
+import numpy as np
+
+from qibo import matrices
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+from qibo.quantum_info.superoperator_transformations import vectorization
+
+
+def pauli_basis(
+ nqubits: int,
+ normalize: bool = False,
+ vectorize: bool = False,
+ sparse: bool = False,
+ order: Optional[str] = None,
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Creates the ``nqubits``-qubit Pauli basis.
+
+ Args:
+ nqubits (int): number of qubits.
+ normalize (bool, optional): If ``True``, normalized basis is returned.
+ Defaults to False.
+ vectorize (bool, optional): If ``False``, returns a nested array with
+ all Pauli matrices. If ``True``, retuns an array where every
+ row is a vectorized Pauli matrix. Defaults to ``False``.
+ sparse (bool, optional) If ``True``, retuns Pauli basis in a sparse
+ representation. Default is ``False``.
+ order (str, optional): If ``"row"``, vectorization of Pauli basis is
+ performed row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, system-wise vectorization is
+ performed. If ``vectorization=False``, then ``order=None`` is
+ forced. Default is ``None``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray or tuple: all Pauli matrices forming the basis. If ``sparse=True``
+ and ``vectorize=True``, tuple is composed of an array of non-zero
+ elements and an array with their row-wise indexes.
+ """
+
+ if nqubits <= 0:
+ raise_error(ValueError, "nqubits must be a positive int.")
+
+ if not isinstance(normalize, bool):
+ raise_error(
+ TypeError,
+ f"normalize must be type bool, but it is type {type(normalize)} instead.",
+ )
+
+ if not isinstance(vectorize, bool):
+ raise_error(
+ TypeError,
+ f"vectorize must be type bool, but it is type {type(vectorize)} instead.",
+ )
+
+ if not isinstance(sparse, bool):
+ raise_error(
+ TypeError,
+ f"sparse must be type bool, but it is type {type(sparse)} instead.",
+ )
+
+ if not isinstance(pauli_order, str):
+ raise_error(
+ TypeError,
+ f"pauli_order must be type str, but it is type {type(pauli_order)} instead.",
+ )
+
+ if set(pauli_order) != {"I", "X", "Y", "Z"}:
+ raise_error(
+ ValueError,
+ f"pauli_order has to contain 4 symbols: I, X, Y, Z. Got {pauli_order} instead.",
+ )
+
+ if vectorize and order is None:
+ raise_error(ValueError, "when vectorize=True, order must be specified.")
+
+ if sparse and not vectorize:
+ raise_error(
+ NotImplementedError,
+ "sparse representation is not implemented for unvectorized Pauli basis.",
+ )
+
+ backend = _check_backend(backend)
+
+ pauli_labels = {"I": matrices.I, "X": matrices.X, "Y": matrices.Y, "Z": matrices.Z}
+ basis_single = [pauli_labels[label] for label in pauli_order]
+
+ if nqubits > 1:
+ basis_full = list(product(basis_single, repeat=nqubits))
+ basis_full = [reduce(np.kron, row) for row in basis_full]
+ else:
+ basis_full = basis_single
+
+ basis_full = backend.cast(basis_full, dtype=basis_full[0].dtype)
+
+ if vectorize and sparse:
+ basis, indexes = [], []
+ for row in basis_full:
+ row = vectorization(row, order=order, backend=backend)
+ row_indexes = backend.np.flatnonzero(row)
+ indexes.append(row_indexes)
+ basis.append(row[row_indexes])
+ del row
+ elif vectorize and not sparse:
+ basis = [
+ vectorization(
+ backend.cast(matrix, dtype=matrix.dtype), order=order, backend=backend
+ )
+ for matrix in basis_full
+ ]
+ else:
+ basis = basis_full
+
+ basis = backend.cast(basis, dtype=basis[0].dtype)
+
+ if normalize:
+ basis = basis / np.sqrt(2**nqubits)
+
+ if vectorize and sparse:
+ indexes = backend.cast(indexes, dtype=indexes[0][0].dtype)
+
+ return basis, indexes
+
+ return basis
+
+
+def comp_basis_to_pauli(
+ nqubits: int,
+ normalize: bool = False,
+ sparse: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Unitary matrix :math:`U` that converts operators from the Liouville
+ representation in the computational basis to the Pauli-Liouville
+ representation.
+
+ The unitary :math:`U` is given by
+
+ .. math::
+ U = \\sum_{k = 0}^{d^{2} - 1} \\, \\ketbra{k}{P_{k}} \\,\\, ,
+
+ where :math:`\\ket{P_{k}}` is the system-vectorization of the :math:`k`-th
+ Pauli operator :math:`P_{k}`, and :math:`\\ket{k}` is the computational
+ basis element.
+
+ When converting a state :math:`\\ket{\\rho}` to its Pauli-Liouville
+ representation :math:`\\ket{\\rho'}`, one should use ``order="system"``
+ in :func:`vectorization`.
+
+ Example:
+ .. code-block:: python
+
+ from qibo.quantum_info import random_density_matrix, vectorization, comp_basis_to_pauli
+ nqubits = 2
+ d = 2**nqubits
+ rho = random_density_matrix(d)
+ U_c2p = comp_basis_to_pauli(nqubits)
+ rho_liouville = vectorization(rho, order="system")
+ rho_pauli_liouville = U_c2p @ rho_liouville
+
+ Args:
+ nqubits (int): number of qubits.
+ normalize (bool, optional): If ``True``, converts to the
+ Pauli basis. Defaults to ``False``.
+ sparse (bool, optional): If ``True``, returns unitary matrix in
+ sparse representation. Default is ``False``.
+ order (str, optional): If ``"row"``, vectorization of Pauli basis is
+ performed row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, system-wise vectorization is
+ performed. Default is ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray or tuple: Unitary matrix :math:`U`. If ``sparse=True``,
+ tuple is composed of array of non-zero elements and an
+ array with their row-wise indexes.
+
+ """
+ backend = _check_backend(backend)
+
+ if sparse:
+ elements, indexes = pauli_basis(
+ nqubits,
+ normalize,
+ vectorize=True,
+ sparse=sparse,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ elements = backend.np.conj(elements)
+
+ return elements, indexes
+
+ unitary = pauli_basis(
+ nqubits,
+ normalize,
+ vectorize=True,
+ sparse=sparse,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ unitary = backend.np.conj(unitary)
+
+ return unitary
+
+
+def pauli_to_comp_basis(
+ nqubits: int,
+ normalize: bool = False,
+ sparse: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Unitary matrix :math:`U` that converts operators from the
+ Pauli-Liouville representation to the Liouville representation
+ in the computational basis.
+
+ The unitary :math:`U` is given by
+
+ .. math::
+ U = \\sum_{k = 0}^{d^{2} - 1} \\, \\ketbra{P_{k}}{b_{k}} \\, .
+
+ Args:
+ nqubits (int): number of qubits.
+ normalize (bool, optional): If ``True``, converts to the
+ Pauli basis. Defaults to ``False``.
+ sparse (bool, optional): If ``True``, returns unitary matrix in
+ sparse representation. Default is ``False``.
+ order (str, optional): If ``"row"``, vectorization of Pauli basis is
+ performed row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, system-wise vectorization is
+ performed. Default is ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray or tuple: Unitary matrix :math:`U`. If ``sparse=True``,
+ tuple is composed of array of non-zero elements and an
+ array with their row-wise indexes.
+ """
+ backend = _check_backend(backend)
+
+ unitary = pauli_basis(
+ nqubits,
+ normalize,
+ vectorize=True,
+ sparse=False,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ unitary = unitary.T
+
+ if sparse:
+ elements, indexes = [], []
+ for row in unitary:
+ index_list = backend.np.flatnonzero(row)
+ indexes.append(index_list)
+ elements.append(row[index_list])
+
+ elements = backend.cast(elements)
+ indexes = backend.cast(indexes)
+
+ return elements, indexes
+
+ return unitary
diff --git a/src/qibo/quantum_info/clifford.py b/src/qibo/quantum_info/clifford.py
new file mode 100644
index 000000000..11d1b4e9e
--- /dev/null
+++ b/src/qibo/quantum_info/clifford.py
@@ -0,0 +1,432 @@
+"""Module definig the Clifford object, which allows phase-space representation of Clifford circuits and stabilizer states."""
+
+from dataclasses import dataclass, field
+from functools import reduce
+from itertools import product
+from typing import Optional, Union
+
+import numpy as np
+
+from qibo import Circuit
+from qibo.backends import CliffordBackend
+from qibo.config import raise_error
+from qibo.gates import M
+from qibo.measurements import frequencies_to_binary
+
+from ._clifford_utils import _decomposition_AG04, _decomposition_BM20, _string_product
+
+
+@dataclass
+class Clifford:
+ """Object storing the results of a circuit execution with the :class:`qibo.backends.clifford.CliffordBackend`.
+
+ Args:
+ data (ndarray or :class:`qibo.models.circuit.Circuit`): If ``ndarray``, it is the
+ symplectic matrix of the stabilizer state in phase-space representation.
+ If :class:`qibo.models.circuit.Circuit`, it is a circuit composed only of Clifford
+ gates and computational-basis measurements.
+ nqubits (int, optional): number of qubits of the state.
+ measurements (list, optional): list of measurements gates :class:`qibo.gates.M`.
+ Defaults to ``None``.
+ nshots (int, optional): number of shots used for sampling the measurements.
+ Defaults to :math:`1000`.
+ engine (str, optional): engine to use in the execution of the
+ :class:`qibo.backends.CliffordBackend`. It accepts ``"numpy"``, ``"numba"``,
+ ``"cupy"``, and ``"stim"`` (see `stim `_).
+ If ``None``, defaults to the corresponding engine
+ from :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+ """
+
+ symplectic_matrix: np.ndarray = field(init=False)
+ data: Union[np.ndarray, Circuit] = field(repr=False)
+ nqubits: Optional[int] = None
+ measurements: Optional[list] = None
+ nshots: int = 1000
+ engine: Optional[str] = None
+
+ _backend: Optional[CliffordBackend] = None
+ _measurement_gate = None
+ _samples: Optional[int] = None
+
+ def __post_init__(self):
+ if isinstance(self.data, Circuit):
+ clifford = self.from_circuit(self.data, engine=self.engine)
+ self.symplectic_matrix = clifford.symplectic_matrix
+ self.nqubits = clifford.nqubits
+ self.measurements = clifford.measurements
+ self._samples = clifford._samples
+ self._measurement_gate = clifford._measurement_gate
+ else:
+ # adding the scratch row if not provided
+ self.symplectic_matrix = self.data
+ if self.symplectic_matrix.shape[0] % 2 == 0:
+ self.symplectic_matrix = np.vstack(
+ (self.symplectic_matrix, np.zeros(self.symplectic_matrix.shape[1]))
+ )
+ self.nqubits = int((self.symplectic_matrix.shape[1] - 1) / 2)
+ if self._backend is None:
+ self._backend = CliffordBackend(self.engine)
+ self.engine = self._backend.engine
+
+ @classmethod
+ def from_circuit(
+ cls,
+ circuit: Circuit,
+ initial_state: Optional[np.ndarray] = None,
+ nshots: int = 1000,
+ engine: Optional[str] = None,
+ ):
+ """Allows to create a :class:`qibo.quantum_info.clifford.Clifford` object by executing the input circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Clifford circuit to run.
+ initial_state (ndarray, optional): symplectic matrix of the initial state.
+ If ``None``, defaults to the symplectic matrix of the zero state.
+ Defaults to ``None``.
+ nshots (int, optional): number of measurement shots to perform
+ if ``circuit`` has measurement gates. Defaults to :math:`10^{3}`.
+ engine (str, optional): engine to use in the execution of the
+ :class:`qibo.backends.CliffordBackend`. It accepts ``"numpy"``, ``"numba"``,
+ ``"cupy"``, and ``"stim"`` (see `stim `_).
+ If ``None``, defaults to the corresponding engine
+ from :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (:class:`qibo.quantum_info.clifford.Clifford`): Object storing the result of the circuit execution.
+ """
+ cls._backend = CliffordBackend(engine)
+
+ return cls._backend.execute_circuit(circuit, initial_state, nshots)
+
+ def to_circuit(self, algorithm: Optional[str] = "AG04"):
+ """Converts symplectic matrix into a Clifford circuit.
+
+ Args:
+ algorithm (str, optional): If ``AG04``, uses the decomposition algorithm from
+ `Aaronson & Gottesman (2004) `_.
+ If ``BM20`` and ``Clifford.nqubits <= 3``, uses the decomposition algorithm from
+ `Bravyi & Maslov (2020) `_.
+ Defaults to ``AG04``.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: circuit composed of Clifford gates.
+ """
+ if not isinstance(algorithm, str):
+ raise_error(
+ TypeError,
+ f"``algorithm`` must be type str, but it is type {type(algorithm)}",
+ )
+
+ if algorithm not in ["AG04", "BM20"]:
+ raise_error(ValueError, f"``algorithm`` {algorithm} not found.")
+
+ if algorithm == "BM20":
+ return _decomposition_BM20(self)
+
+ return _decomposition_AG04(self)
+
+ def generators(self, return_array: bool = False):
+ """Extracts the generators of stabilizers and destabilizers.
+
+ Args:
+ return_array (bool, optional): If ``True`` returns the generators as ``ndarray``.
+ If ``False``, their representation as strings is returned. Defaults to ``False``.
+
+ Returns:
+ (list, list): Generators and their corresponding phases, respectively.
+ """
+ return self._backend.symplectic_matrix_to_generators(
+ self.symplectic_matrix, return_array
+ )
+
+ def stabilizers(self, symplectic: bool = False, return_array: bool = False):
+ """Extracts the stabilizers of the state.
+
+ Args:
+ symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix
+ that correspond to the :math:`n` generators of the :math:`2^{n}` total stabilizers,
+ independently of ``return_array``.
+ return_array (bool, optional): To be used when ``symplectic = False``.
+ If ``True`` returns the stabilizers as ``ndarray``.
+ If ``False``, returns stabilizers as strings. Defaults to ``False``.
+
+ Returns:
+ (ndarray or list): Stabilizers of the state.
+ """
+ if not symplectic:
+ generators, phases = self.generators(return_array)
+
+ return self._construct_operators(
+ generators[self.nqubits :],
+ phases[self.nqubits :],
+ )
+
+ return self.symplectic_matrix[self.nqubits : -1, :]
+
+ def destabilizers(self, symplectic: bool = False, return_array: bool = False):
+ """Extracts the destabilizers of the state.
+
+ Args:
+ symplectic (bool, optional): If ``True``, returns the rows of the symplectic matrix
+ that correspond to the :math:`n` generators of the :math:`2^{n}` total
+ destabilizers, independently of ``return_array``.
+ return_array (bool, optional): To be used when ``symplectic = False``.
+ If ``True`` returns the destabilizers as ``ndarray``.
+ If ``False``, their representation as strings is returned.
+ Defaults to ``False``.
+
+ Returns:
+ (ndarray or list): Destabilizers of the state.
+ """
+ if not symplectic:
+ generators, phases = self.generators(return_array)
+
+ return self._construct_operators(
+ generators[: self.nqubits], phases[: self.nqubits]
+ )
+
+ return self.symplectic_matrix[: self.nqubits, :]
+
+ def state(self):
+ """Builds the density matrix representation of the state.
+
+ .. note::
+ This method is inefficient in runtime and memory for a large number of qubits.
+
+ Returns:
+ (ndarray): Density matrix of the state.
+ """
+ stabilizers = self.stabilizers(return_array=True)
+
+ return self.engine.np.sum(stabilizers, axis=0) / len(stabilizers)
+
+ @property
+ def measurement_gate(self):
+ """Single measurement gate containing all measured qubits.
+
+ Useful for sampling all measured qubits at once when simulating.
+ """
+ if self._measurement_gate is None:
+ for gate in self.measurements:
+ if self._measurement_gate is None:
+ self._measurement_gate = M(*gate.init_args, **gate.init_kwargs)
+ else:
+ self._measurement_gate.add(gate)
+
+ return self._measurement_gate
+
+ def samples(self, binary: bool = True, registers: bool = False):
+ """Returns raw measurement samples.
+
+ Args:
+ binary (bool, optional): If ``False``, return samples in binary form.
+ If ``True``, returns samples in decimal form. Defalts to ``True``.
+ registers (bool, optional): If ``True``, groups samples according to registers.
+ Defaults to ``False``.
+
+ Returns:
+ If ``binary`` is ``True``
+ samples are returned in binary form as a tensor
+ of shape ``(nshots, n_measured_qubits)``.
+ If ``binary`` is ``False``
+ samples are returned in decimal form as a tensor
+ of shape ``(nshots,)``.
+ If ``registers`` is ``True``
+ samples are returned in a ``dict`` where the keys are the register
+ names and the values are the samples tensors for each register.
+ If ``registers`` is ``False``
+ a single tensor is returned which contains samples from all the
+ measured qubits, independently of their registers.
+ """
+ if not self.measurements:
+ raise_error(RuntimeError, "No measurement provided.")
+
+ measured_qubits = self.measurement_gate.qubits
+
+ if self._samples is None:
+ if self.measurements[0].result.has_samples():
+ samples = np.concatenate(
+ [gate.result.samples() for gate in self.measurements], axis=1
+ )
+ else:
+ samples = self._backend.sample_shots(
+ self.symplectic_matrix, measured_qubits, self.nqubits, self.nshots
+ )
+ if self.measurement_gate.has_bitflip_noise():
+ p0, p1 = self.measurement_gate.bitflip_map
+ bitflip_probabilities = self._backend.cast(
+ [
+ [p0.get(q) for q in measured_qubits],
+ [p1.get(q) for q in measured_qubits],
+ ]
+ )
+ samples = self._backend.cast(samples, dtype="int32")
+ samples = self._backend.apply_bitflips(samples, bitflip_probabilities)
+ # register samples to individual gate ``MeasurementResult``
+ qubit_map = {
+ q: i for i, q in enumerate(self.measurement_gate.target_qubits)
+ }
+ self._samples = self._backend.cast(samples, dtype="int32")
+ for gate in self.measurements:
+ rqubits = tuple(qubit_map.get(q) for q in gate.target_qubits)
+ gate.result.register_samples(self._samples[:, rqubits], self._backend)
+
+ if registers:
+ return {
+ gate.register_name: gate.result.samples(binary)
+ for gate in self.measurements
+ }
+
+ if binary:
+ return self._samples
+
+ return self._backend.samples_to_decimal(self._samples, len(measured_qubits))
+
+ def frequencies(self, binary: bool = True, registers: bool = False):
+ """Returns the frequencies of measured samples.
+
+ Args:
+ binary (bool, optional): If ``True``, return frequency keys in binary form.
+ If ``False``, return frequency keys in decimal form. Defaults to ``True``.
+ registers (bool, optional): If ``True``, groups frequencies according to registers.
+ Defaults to ``False``.
+
+ Returns:
+ A `collections.Counter` where the keys are the observed values
+ and the values the corresponding frequencies, that is the number
+ of times each measured value/bitstring appears.
+
+ If ``binary`` is ``True``
+ the keys of the `Counter` are in binary form, as strings of
+ :math:`0`s and :math`1`s.
+ If ``binary`` is ``False``
+ the keys of the ``Counter`` are integers.
+ If ``registers`` is ``True``
+ a `dict` of `Counter` s is returned where keys are the name of
+ each register.
+ If ``registers`` is ``False``
+ a single ``Counter`` is returned which contains samples from all
+ the measured qubits, independently of their registers.
+ """
+ measured_qubits = self.measurement_gate.target_qubits
+ freq = self._backend.calculate_frequencies(self.samples(False))
+ if registers:
+ if binary:
+ return {
+ gate.register_name: frequencies_to_binary(
+ self._backend.calculate_frequencies(gate.result.samples(False)),
+ len(gate.target_qubits),
+ )
+ for gate in self.measurements
+ }
+
+ return {
+ gate.register_name: self._backend.calculate_frequencies(
+ gate.result.samples(False)
+ )
+ for gate in self.measurements
+ }
+
+ if binary:
+ return frequencies_to_binary(freq, len(measured_qubits))
+
+ return freq
+
+ def probabilities(self, qubits: Optional[Union[tuple, list]] = None):
+ """Computes the probabilities of the selected qubits from the measured samples.
+
+ Args:
+ qubits (tuple or list, optional): Qubits for which to compute the probabilities.
+
+ Returns:
+ (ndarray): Measured probabilities.
+ """
+ if isinstance(qubits, list):
+ qubits = tuple(qubits)
+
+ measured_qubits = self.measurement_gate.qubits
+ if qubits is not None:
+ if not set(qubits).issubset(set(measured_qubits)):
+ raise_error(
+ RuntimeError,
+ f"Asking probabilities for qubits {qubits}, but only qubits {measured_qubits} were measured.",
+ )
+ qubits = [measured_qubits.index(q) for q in qubits]
+ else:
+ qubits = range(len(measured_qubits))
+
+ probs = [0 for _ in range(2 ** len(measured_qubits))]
+ samples = self.samples(binary=False)
+ for s in samples:
+ probs[int(s)] += 1
+
+ probs = self.engine.cast(probs, float) / len(samples)
+
+ return self._backend.calculate_probabilities(
+ self.engine.np.sqrt(probs), qubits, len(measured_qubits)
+ )
+
+ def copy(self, deep: bool = False):
+ """Returns copy of :class:`qibo.quantum_info.clifford.Clifford` object.
+
+ Args:
+ deep (bool, optional): If ``True``, creates another copy in memory.
+ Defaults to ``False``.
+
+ Returns:
+ :class:`qibo.quantum_info.clifford.Clifford`: copy of original ``Clifford`` object.
+ """
+ if not isinstance(deep, bool):
+ raise_error(
+ TypeError, f"``deep`` must be type bool, but it is type {type(deep)}."
+ )
+
+ symplectic_matrix = (
+ self.engine.np.copy(self.symplectic_matrix)
+ if deep
+ else self.symplectic_matrix
+ )
+
+ return self.__class__(
+ symplectic_matrix,
+ self.nqubits,
+ self.measurements,
+ self.nshots,
+ _backend=self._backend,
+ )
+
+ def _construct_operators(self, generators: list, phases: list):
+ """Helper function to construct all the operators from their generators.
+
+ Args:
+ generators (list or ndarray): generators.
+ phases (list or ndarray): phases of the generators.
+
+ Returns:
+ (list): All operators generated by the generators of the stabilizer group.
+ """
+
+ if not isinstance(generators[0], str):
+ generators = self._backend.cast(generators)
+ phases = self._backend.cast(phases)
+
+ operators = generators * phases.reshape(-1, 1, 1)
+ identity = self.engine.identity_density_matrix(
+ self.nqubits, normalize=False
+ )
+ operators = self._backend.cast([(g, identity) for g in operators])
+
+ return self._backend.cast(
+ [reduce(self.engine.np.matmul, ops) for ops in product(*operators)]
+ )
+
+ operators = list(np.copy(generators))
+ for i in (phases == -1).nonzero()[0]:
+ i = int(i)
+ operators[i] = f"-{operators[i]}"
+
+ identity = "".join(["I" for _ in range(self.nqubits)])
+
+ operators = [(g, identity) for g in operators]
+
+ return [_string_product(ops) for ops in product(*operators)]
diff --git a/src/qibo/quantum_info/entanglement.py b/src/qibo/quantum_info/entanglement.py
new file mode 100644
index 000000000..23c2f7d3f
--- /dev/null
+++ b/src/qibo/quantum_info/entanglement.py
@@ -0,0 +1,288 @@
+"""Submodules with entanglement measures."""
+
+import numpy as np
+
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+from qibo.quantum_info.linalg_operations import partial_trace
+from qibo.quantum_info.metrics import fidelity, purity
+
+
+def concurrence(state, bipartition, check_purity: bool = True, backend=None):
+ """Calculates concurrence of a pure bipartite quantum state
+ :math:`\\rho \\in \\mathcal{H}_{A} \\otimes \\mathcal{H}_{B}` as
+
+ .. math::
+ C(\\rho) = \\sqrt{2 \\, (\\text{tr}^{2}(\\rho) - \\text{tr}(\\rho_{A}^{2}))} \\, ,
+
+ where :math:`\\rho_{A} = \\text{tr}_{B}(\\rho)` is the reduced density operator
+ obtained by tracing out the qubits in the ``bipartition`` :math:`B`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ bipartition (list or tuple or ndarray): qubits in the subsystem to be traced out.
+ check_purity (bool, optional): if ``True``, checks if ``state`` is pure. If ``False``,
+ it assumes ``state`` is pure . Defaults to ``True``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Concurrence of :math:`\\rho`.
+ """
+ backend = _check_backend(backend)
+
+ if (
+ (len(state.shape) not in [1, 2])
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if not isinstance(check_purity, bool):
+ raise_error(
+ TypeError,
+ f"check_purity must be type bool, but it is type {type(check_purity)}.",
+ )
+
+ if check_purity is True:
+ purity_total_system = purity(state, backend=backend)
+
+ mixed = bool(abs(purity_total_system - 1.0) > PRECISION_TOL)
+ if mixed is True:
+ raise_error(
+ NotImplementedError,
+ "concurrence only implemented for pure quantum states.",
+ )
+
+ reduced_density_matrix = partial_trace(state, bipartition, backend=backend)
+
+ purity_reduced = purity(reduced_density_matrix, backend=backend)
+ if purity_reduced - 1.0 > 0.0:
+ purity_reduced = round(purity_reduced, 7)
+
+ concur = np.sqrt(2 * (1 - purity_reduced))
+
+ return concur
+
+
+def entanglement_of_formation(
+ state, bipartition, base: float = 2, check_purity: bool = True, backend=None
+):
+ """Calculates the entanglement of formation :math:`E_{f}` of a pure bipartite
+ quantum state :math:`\\rho`, which is given by
+
+ .. math::
+ E_{f} = H([1 - x, x]) \\, ,
+
+ where
+
+ .. math::
+ x = \\frac{1 + \\sqrt{1 - C^{2}(\\rho)}}{2} \\, ,
+
+ :math:`C(\\rho)` is the :func:`qibo.quantum_info.concurrence` of :math:`\\rho`,
+ and :math:`H` is the :func:`qibo.quantum_info.entropies.shannon_entropy`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ bipartition (list or tuple or ndarray): qubits in the subsystem to be traced out.
+ base (float): the base of the log in :func:`qibo.quantum_info.entropies.shannon_entropy`.
+ Defaults to :math:`2`.
+ check_purity (bool, optional): if ``True``, checks if ``state`` is pure. If ``False``,
+ it assumes ``state`` is pure . Default: ``True``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+
+ Returns:
+ float: entanglement of formation of state :math:`\\rho`.
+ """
+ from qibo.quantum_info.entropies import shannon_entropy # pylint: disable=C0415
+
+ backend = _check_backend(backend)
+
+ concur = concurrence(
+ state, bipartition=bipartition, check_purity=check_purity, backend=backend
+ )
+ concur = (1 + np.sqrt(1 - concur**2)) / 2
+ probabilities = [1 - concur, concur]
+
+ ent_of_form = shannon_entropy(probabilities, base=base, backend=backend)
+
+ return ent_of_form
+
+
+def entanglement_fidelity(
+ channel, nqubits: int, state=None, check_hermitian: bool = False, backend=None
+):
+ """Entanglement fidelity :math:`F_{\\mathcal{E}}` of a ``channel`` :math:`\\mathcal{E}`
+ on ``state`` :math:`\\rho` is given by
+
+ .. math::
+ F_{\\mathcal{E}}(\\rho) = F(\\rho_{f}, \\rho)
+
+ where :math:`F` is the :func:`qibo.quantum_info.fidelity` function for states,
+ and :math:`\\rho_{f} = \\mathcal{E}_{A} \\otimes I_{B}(\\rho)`
+ is the state after the channel :math:`\\mathcal{E}` was applied to
+ partition :math:`A`.
+
+ Args:
+ channel (:class:`qibo.gates.channels.Channel`): quantum channel
+ acting on partition :math:`A`.
+ nqubits (int): total number of qubits in ``state``.
+ state (ndarray, optional): statevector or density matrix to be evolved
+ by ``channel``. If ``None``, defaults to the maximally entangled state
+ :math:`\\frac{1}{2^{n}} \\, \\sum_{k} \\, \\ket{k}\\ket{k}`, where
+ :math:`n` is ``nqubits``. Defaults to ``None``.
+ check_hermitian (bool, optional): if ``True``, checks if the final state
+ :math:`\\rho_{f}` is Hermitian. If ``False``, it assumes it is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Entanglement fidelity :math:`F_{\\mathcal{E}}`.
+ """
+ if not isinstance(nqubits, int):
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+
+ if nqubits <= 0:
+ raise_error(
+ ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
+ )
+
+ if state is not None and (
+ (len(state.shape) not in [1, 2])
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if not isinstance(check_hermitian, bool):
+ raise_error(
+ TypeError,
+ f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
+ )
+
+ backend = _check_backend(backend)
+
+ if state is None:
+ state = backend.plus_density_matrix(nqubits)
+
+ # necessary because this function do support repeated execution,
+ # so it has to default to density matrices
+ if len(state.shape) == 1:
+ state = np.outer(state, np.conj(state))
+
+ state_final = backend.apply_channel_density_matrix(channel, state, nqubits)
+
+ entang_fidelity = fidelity(
+ state_final, state, check_hermitian=check_hermitian, backend=backend
+ )
+
+ return entang_fidelity
+
+
+def meyer_wallach_entanglement(circuit, backend=None):
+ """Computes the Meyer-Wallach entanglement Q of the `circuit`,
+
+ .. math::
+ Q(\\theta) = 1 - \\frac{1}{N} \\, \\sum_{k} \\,
+ \\text{tr}\\left(\\rho_{k^{2}}(\\theta)\\right) \\, .
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Meyer-Wallach entanglement.
+ """
+
+ backend = _check_backend(backend)
+
+ circuit.density_matrix = True
+ nqubits = circuit.nqubits
+
+ rho = backend.execute_circuit(circuit).state()
+
+ ent = 0
+ for j in range(nqubits):
+ trace_q = list(range(nqubits))
+ trace_q.pop(j)
+
+ rho_r = partial_trace(rho, trace_q, backend=backend)
+
+ trace = purity(rho_r, backend=backend)
+
+ ent += trace
+
+ entanglement = 1 - ent / nqubits
+
+ return entanglement
+
+
+def entangling_capability(circuit, samples: int, seed=None, backend=None):
+ """Returns the entangling capability :math:`\\text{Ent}` of a parametrized
+ circuit, which is average Meyer-Wallach entanglement Q of the circuit, i.e.
+
+ .. math::
+ \\text{Ent} = \\frac{2}{S}\\sum_{k}Q_k \\, ,
+
+ where :math:`S` is the number of samples.
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
+ samples (int): number of samples to estimate the integral.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Entangling capability.
+ """
+
+ if not isinstance(samples, int):
+ raise_error(
+ TypeError, f"samples must be type int, but it is type {type(samples)}."
+ )
+
+ if (
+ seed is not None
+ and not isinstance(seed, int)
+ and not isinstance(seed, np.random.Generator)
+ ):
+ raise_error(
+ TypeError, "seed must be either type int or numpy.random.Generator."
+ )
+
+ backend = _check_backend(backend)
+
+ local_state = (
+ np.random.default_rng(seed) if seed is None or isinstance(seed, int) else seed
+ )
+
+ res = []
+ for _ in range(samples):
+ params = local_state.uniform(-np.pi, np.pi, circuit.trainable_gates.nparams)
+ circuit.set_parameters(params)
+ entanglement = meyer_wallach_entanglement(circuit, backend=backend)
+ res.append(entanglement)
+
+ capability = 2 * np.real(np.sum(res)) / samples
+
+ return capability
diff --git a/src/qibo/quantum_info/entropies.py b/src/qibo/quantum_info/entropies.py
new file mode 100644
index 000000000..05bf3f823
--- /dev/null
+++ b/src/qibo/quantum_info/entropies.py
@@ -0,0 +1,899 @@
+"""Submodule with entropy measures."""
+
+from typing import Union
+
+import numpy as np
+from scipy.linalg import fractional_matrix_power
+
+from qibo.backends import _check_backend
+from qibo.backends.pytorch import PyTorchBackend
+from qibo.config import PRECISION_TOL, raise_error
+from qibo.quantum_info.linalg_operations import partial_trace
+from qibo.quantum_info.metrics import _check_hermitian_or_not_gpu, purity
+
+
+def shannon_entropy(prob_dist, base: float = 2, backend=None):
+ """Calculate the Shannon entropy of a probability array :math:`\\mathbf{p}`, which is given by
+
+ .. math::
+ H(\\mathbf{p}) = - \\sum_{k = 0}^{d^{2} - 1} \\, p_{k} \\, \\log_{b}(p_{k}) \\, ,
+
+ where :math:`d = \\text{dim}(\\mathcal{H})` is the dimension of the
+ Hilbert space :math:`\\mathcal{H}`, :math:`b` is the log base (default 2),
+ and :math:`0 \\log_{b}(0) \\equiv 0`.
+
+ Args:
+ prob_dist (ndarray or list): a probability array :math:`\\mathbf{p}`.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ (float): Shannon entropy :math:`H(\\mathcal{p})`.
+ """
+ backend = _check_backend(backend)
+
+ if isinstance(prob_dist, list):
+ # np.float64 is necessary instead of native float because of tensorflow
+ prob_dist = backend.cast(prob_dist, dtype=np.float64)
+
+ if base <= 0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if len(prob_dist.shape) != 1:
+ raise_error(
+ TypeError,
+ f"Probability array must have dims (k,) but it has {prob_dist.shape}.",
+ )
+
+ if len(prob_dist) == 0:
+ raise_error(TypeError, "Empty array.")
+
+ if any(prob_dist < 0) or any(prob_dist > 1.0):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+
+ total_sum = backend.np.sum(prob_dist)
+
+ if np.abs(float(total_sum) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Probability array must sum to 1.")
+
+ log_prob = backend.np.where(
+ prob_dist != 0, backend.np.log2(prob_dist) / np.log2(base), 0.0
+ )
+
+ shan_entropy = -backend.np.sum(prob_dist * log_prob)
+
+ # absolute value if entropy == 0.0 to avoid returning -0.0
+ shan_entropy = backend.np.abs(shan_entropy) if shan_entropy == 0.0 else shan_entropy
+
+ return np.real(float(shan_entropy))
+
+
+def classical_relative_entropy(prob_dist_p, prob_dist_q, base: float = 2, backend=None):
+ """Calculates the relative entropy between two discrete probability distributions.
+
+ For probabilities :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, it is defined as
+
+ ..math::
+ D(\\mathbf{p} \\, \\| \\, \\mathbf{q}) = \\sum_{x} \\, \\mathbf{p}(x) \\,
+ \\log\\left( \\frac{\\mathbf{p}(x)}{\\mathbf{q}(x)} \\right) \\, .
+
+ The classical relative entropy is also known as the
+ `Kullback-Leibler (KL) divergence `_.
+
+ Args:
+ prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+ prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Classical relative entropy between :math:`\\mathbf{p}` and :math:`\\mathbf{q}`.
+ """
+ backend = _check_backend(backend)
+ prob_dist_p = backend.cast(prob_dist_p, dtype=np.float64)
+ prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64)
+
+ if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1):
+ raise_error(
+ TypeError,
+ "Probability arrays must have dims (k,) but have "
+ + f"dims {prob_dist_p.shape} and {prob_dist_q.shape}.",
+ )
+
+ if (len(prob_dist_p) == 0) or (len(prob_dist_q) == 0):
+ raise_error(TypeError, "At least one of the arrays is empty.")
+
+ if base <= 0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if (any(prob_dist_p < 0) or any(prob_dist_p > 1.0)) or (
+ any(prob_dist_q < 0) or any(prob_dist_q > 1.0)
+ ):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+ total_sum_p = backend.np.sum(prob_dist_p)
+
+ total_sum_q = backend.np.sum(prob_dist_q)
+
+ if np.abs(float(total_sum_p) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "First probability array must sum to 1.")
+
+ if np.abs(float(total_sum_q) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Second probability array must sum to 1.")
+
+ entropy_p = -1 * shannon_entropy(prob_dist_p, base=base, backend=backend)
+
+ log_prob_q = backend.np.where(
+ prob_dist_q != 0.0, backend.np.log2(prob_dist_q) / np.log2(base), -np.inf
+ )
+
+ log_prob = backend.np.where(prob_dist_p != 0.0, log_prob_q, 0.0)
+
+ relative = backend.np.sum(prob_dist_p * log_prob)
+
+ return entropy_p - relative
+
+
+def classical_renyi_entropy(
+ prob_dist, alpha: Union[float, int], base: float = 2, backend=None
+):
+ """Calculates the classical Rényi entropy :math:`H_{\\alpha}` of a discrete probability distribution.
+
+ For :math:`\\alpha \\in (0, \\, 1) \\cup (1, \\, \\infty)` and probability distribution
+ :math:`\\mathbf{p}`, the classical Rényi entropy is defined as
+
+ .. math::
+ H_{\\alpha}(\\mathbf{p}) = \\frac{1}{1 - \\alpha} \\, \\log\\left( \\sum_{x}
+ \\, \\mathbf{p}^{\\alpha}(x) \\right) \\, .
+
+ A special case is the limit :math:`\\alpha \\to 1`, in which the classical Rényi entropy
+ coincides with the :func:`qibo.quantum_info.entropies.shannon_entropy`.
+
+ Another special case is the limit :math:`\\alpha \\to 0`, where the function is
+ reduced to :math:`\\log\\left(|\\mathbf{p}|\\right)`, with :math:`|\\mathbf{p}|`
+ being the support of :math:`\\mathbf{p}`.
+ This is known as the `Hartley entropy `_
+ (also known as *Hartley function* or *max-entropy*).
+
+ In the limit :math:`\\alpha \\to \\infty`, the function reduces to
+ :math:`-\\log(\\max_{x}(\\mathbf{p}(x)))`, which is called the
+ `min-entropy `_.
+
+ Args:
+ prob_dist (ndarray): discrete probability distribution.
+ alpha (float or int): order of the Rényi entropy.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Classical Rényi entropy :math:`H_{\\alpha}`.
+ """
+ backend = _check_backend(backend)
+ prob_dist = backend.cast(prob_dist, dtype=np.float64)
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if len(prob_dist.shape) != 1:
+ raise_error(
+ TypeError,
+ f"Probability array must have dims (k,) but it has {prob_dist.shape}.",
+ )
+
+ if len(prob_dist) == 0:
+ raise_error(TypeError, "Empty array.")
+
+ if any(prob_dist < 0) or any(prob_dist > 1.0):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+
+ total_sum = backend.np.sum(prob_dist)
+
+ if np.abs(float(total_sum) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Probability array must sum to 1.")
+
+ if alpha == 0.0:
+ return np.log2(len(prob_dist)) / np.log2(base)
+
+ if alpha == 1.0:
+ return shannon_entropy(prob_dist, base=base, backend=backend)
+
+ if alpha == np.inf:
+ return -1 * backend.np.log2(max(prob_dist)) / np.log2(base)
+
+ total_sum = backend.np.sum(prob_dist**alpha)
+
+ renyi_ent = (1 / (1 - alpha)) * backend.np.log2(total_sum) / np.log2(base)
+
+ return renyi_ent
+
+
+def classical_relative_renyi_entropy(
+ prob_dist_p, prob_dist_q, alpha: Union[float, int], base: float = 2, backend=None
+):
+ """Calculates the classical relative Rényi entropy between two discrete probability distributions.
+
+ This function is also known as
+ `Rényi divergence `_.
+
+ For :math:`\\alpha \\in (0, \\, 1) \\cup (1, \\, \\infty)` and probability distributions
+ :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, the classical relative Rényi entropy is defined as
+
+ .. math::
+ H_{\\alpha}(\\mathbf{p} \\, \\| \\, \\mathbf{q}) = \\frac{1}{\\alpha - 1} \\,
+ \\log\\left( \\sum_{x} \\, \\frac{\\mathbf{p}^{\\alpha}(x)}
+ {\\mathbf{q}^{\\alpha - 1}(x)} \\right) \\, .
+
+ A special case is the limit :math:`\\alpha \\to 1`, in which the classical Rényi divergence
+ coincides with the :func:`qibo.quantum_info.entropies.classical_relative_entropy`.
+
+ Another special case is the limit :math:`\\alpha \\to 1/2`, where the function is
+ reduced to :math:`-2 \\log\\left(\\sum_{x} \\, \\sqrt{\\mathbf{p}(x) \\, \\mathbf{q}(x)} \\right)`.
+ The sum inside the :math:`\\log` is known as the
+ `Bhattacharyya coefficient `_.
+
+ In the limit :math:`\\alpha \\to \\infty`, the function reduces to
+ :math:`\\log(\\max_{x}(\\mathbf{p}(x) \\, \\mathbf{q}(x))`.
+
+ Args:
+ prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+ prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+ alpha (float or int): order of the Rényi entropy.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Classical relative Rényi entropy :math:`H_{\\alpha}(\\mathbf{p} \\, \\| \\, \\mathbf{q})`.
+ """
+ backend = _check_backend(backend)
+ prob_dist_p = backend.cast(prob_dist_p, dtype=np.float64)
+ prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64)
+
+ if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1):
+ raise_error(
+ TypeError,
+ "Probability arrays must have dims (k,) but have "
+ + f"dims {prob_dist_p.shape} and {prob_dist_q.shape}.",
+ )
+
+ if (len(prob_dist_p) == 0) or (len(prob_dist_q) == 0):
+ raise_error(TypeError, "At least one of the arrays is empty.")
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if (any(prob_dist_p < 0) or any(prob_dist_p > 1.0)) or (
+ any(prob_dist_q < 0) or any(prob_dist_q > 1.0)
+ ):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+
+ total_sum_p = backend.np.sum(prob_dist_p)
+ total_sum_q = backend.np.sum(prob_dist_q)
+
+ if np.abs(float(total_sum_p) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "First probability array must sum to 1.")
+
+ if np.abs(float(total_sum_q) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Second probability array must sum to 1.")
+
+ if alpha == 0.5:
+ total_sum = backend.np.sqrt(prob_dist_p * prob_dist_q)
+ total_sum = backend.np.sum(total_sum)
+
+ return -2 * backend.np.log2(total_sum) / np.log2(base)
+
+ if alpha == 1.0:
+ return classical_relative_entropy(
+ prob_dist_p, prob_dist_q, base=base, backend=backend
+ )
+
+ if alpha == np.inf:
+ return backend.np.log2(max(prob_dist_p / prob_dist_q)) / np.log2(base)
+
+ prob_p = prob_dist_p**alpha
+ prob_q = prob_dist_q ** (1 - alpha)
+
+ total_sum = backend.np.sum(prob_p * prob_q)
+
+ return (1 / (alpha - 1)) * backend.np.log2(total_sum) / np.log2(base)
+
+
+def classical_tsallis_entropy(prob_dist, alpha: float, base: float = 2, backend=None):
+ """Calculates the classical Tsallis entropy for a discrete probability distribution.
+
+ This is defined as
+
+ .. math::
+ S_{\\alpha}(\\mathbf{p}) = \\frac{1}{\\alpha - 1} \\,
+ \\left(1 - \\sum_{x} \\, \\mathbf{p}^{\\alpha}(x) \\right)
+
+ Args:
+ prob_dist (ndarray): discrete probability distribution.
+ alpha (float or int): entropic index.
+ base (float): the base of the log. Used when ``alpha=1.0``.
+ Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Classical Tsallis entropy :math:`S_{\\alpha}(\\mathbf{p})`.
+ """
+ backend = _check_backend(backend)
+
+ if isinstance(prob_dist, list):
+ # np.float64 is necessary instead of native float because of tensorflow
+ prob_dist = backend.cast(prob_dist, dtype=np.float64)
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if len(prob_dist.shape) != 1:
+ raise_error(
+ TypeError,
+ f"Probability array must have dims (k,) but it has {prob_dist.shape}.",
+ )
+
+ if len(prob_dist) == 0:
+ raise_error(TypeError, "Empty array.")
+
+ if any(prob_dist < 0) or any(prob_dist > 1.0):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+
+ total_sum = backend.np.sum(prob_dist)
+
+ if np.abs(float(total_sum) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Probability array must sum to 1.")
+
+ if alpha == 1.0:
+ return shannon_entropy(prob_dist, base=base, backend=backend)
+
+ total_sum = prob_dist**alpha
+ total_sum = backend.np.sum(total_sum)
+
+ return (1 / (1 - alpha)) * (total_sum - 1)
+
+
+def von_neumann_entropy(
+ state,
+ base: float = 2,
+ check_hermitian: bool = False,
+ return_spectrum: bool = False,
+ backend=None,
+):
+ """Calculates the von-Neumann entropy :math:`S(\\rho)` of a quantum ``state`` :math:`\\rho`.
+
+ It is given by
+
+ .. math::
+ S(\\rho) = - \\text{tr}\\left[\\rho \\, \\log(\\rho)\\right]
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ base (float, optional): the base of the log. Defaults to :math:`2`.
+ check_hermitian (bool, optional): if ``True``, checks if ``state`` is Hermitian.
+ If ``False``, it assumes ``state`` is Hermitian .
+ Defaults to ``False``.
+ return_spectrum: if ``True``, returns ``entropy`` and
+ :math:`-\\log_{\\textup{b}}(\\textup{eigenvalues})`, where :math:`b` is ``base``.
+ If ``False``, returns only ``entropy``. Default is ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: The von-Neumann entropy :math:`S` of ``state`` :math:`\\rho`.
+ """
+ backend = _check_backend(backend)
+
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if not isinstance(check_hermitian, bool):
+ raise_error(
+ TypeError,
+ f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
+ )
+
+ if purity(state, backend=backend) == 1.0:
+ if return_spectrum:
+ return 0.0, backend.cast([0.0], dtype=float)
+
+ return 0.0
+
+ eigenvalues = backend.calculate_eigenvalues(
+ state,
+ hermitian=(
+ not check_hermitian or _check_hermitian_or_not_gpu(state, backend=backend)
+ ),
+ )
+
+ log_prob = backend.np.where(
+ backend.np.real(eigenvalues) > 0.0,
+ backend.np.log2(eigenvalues) / np.log2(base),
+ 0.0,
+ )
+
+ ent = -backend.np.sum(eigenvalues * log_prob)
+ # absolute value if entropy == 0.0 to avoid returning -0.0
+ ent = backend.np.abs(ent) if ent == 0.0 else backend.np.real(ent)
+
+ if return_spectrum:
+ log_prob = backend.cast(log_prob, dtype=log_prob.dtype)
+ return ent, -log_prob
+
+ return ent
+
+
+def relative_von_neumann_entropy(
+ state, target, base: float = 2, check_hermitian: bool = False, backend=None
+):
+ """Calculates the relative entropy :math:`S(\\rho \\, \\| \\, \\sigma)` between ``state`` :math:`\\rho` and ``target`` :math:`\\sigma`.
+
+ It is given by
+
+ .. math::
+ S(\\rho \\, \\| \\, \\sigma) = \\text{tr}\\left[\\rho \\, \\log(\\rho)\\right]
+ - \\text{tr}\\left[\\rho \\, \\log(\\sigma)\\right]
+
+ Args:
+ state (ndarray): statevector or density matrix :math:`\\rho`.
+ target (ndarray): statevector or density matrix :math:`\\sigma`.
+ base (float, optional): the base of the log. Defaults to :math:`2`.
+ check_hermitian (bool, optional): If ``True``, checks if ``state`` is Hermitian.
+ If ``False``, it assumes ``state`` is Hermitian .
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Relative (von-Neumann) entropy :math:`S(\\rho \\, \\| \\, \\sigma)`.
+ """
+ backend = _check_backend(backend)
+ state = backend.cast(state)
+ target = backend.cast(target)
+
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if (
+ (len(target.shape) >= 3)
+ or (len(target) == 0)
+ or (len(target.shape) == 2 and target.shape[0] != target.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"target must have dims either (k,) or (k,k), but have dims {target.shape}.",
+ )
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if not isinstance(check_hermitian, bool):
+ raise_error(
+ TypeError,
+ f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
+ )
+
+ if purity(state, backend=backend) == 1.0 and purity(target, backend=backend) == 1.0:
+ return 0.0
+
+ if len(state.shape) == 1:
+ state = backend.np.outer(state, backend.np.conj(state))
+
+ if len(target.shape) == 1:
+ target = backend.np.outer(target, backend.np.conj(target))
+
+ eigenvalues_state = backend.calculate_eigenvalues(
+ state,
+ hermitian=(
+ not check_hermitian or _check_hermitian_or_not_gpu(state, backend=backend)
+ ),
+ )
+ eigenvalues_target = backend.calculate_eigenvalues(
+ target,
+ hermitian=(
+ not check_hermitian or _check_hermitian_or_not_gpu(target, backend=backend)
+ ),
+ )
+
+ log_state = backend.np.where(
+ backend.np.real(eigenvalues_state) > 0,
+ backend.np.log2(eigenvalues_state) / np.log2(base),
+ 0.0,
+ )
+ log_target = backend.np.where(
+ backend.np.real(eigenvalues_target) > 0,
+ backend.np.log2(eigenvalues_target) / np.log2(base),
+ -np.inf,
+ )
+
+ log_target = backend.np.where(eigenvalues_state != 0.0, log_target, 0.0)
+
+ entropy_state = backend.np.sum(eigenvalues_state * log_state)
+
+ relative = backend.np.sum(eigenvalues_state * log_target)
+
+ return float(backend.np.real(entropy_state - relative))
+
+
+def renyi_entropy(state, alpha: Union[float, int], base: float = 2, backend=None):
+ """Calculates the Rényi entropy :math:`H_{\\alpha}` of a quantum state :math:`\\rho`.
+
+ For :math:`\\alpha \\in (0, \\, 1) \\cup (1, \\, \\infty)`, the Rényi entropy is defined as
+
+ .. math::
+ H_{\\alpha}(\\rho) = \\frac{1}{1 - \\alpha} \\, \\log\\left( \\rho^{\\alpha} \\right) \\, .
+
+ A special case is the limit :math:`\\alpha \\to 1`, in which the Rényi entropy
+ coincides with the :func:`qibo.quantum_info.entropies.entropy`.
+
+ Another special case is the limit :math:`\\alpha \\to 0`, where the function is
+ reduced to :math:`\\log\\left(d\\right)`, with :math:`d = 2^{n}`
+ being the dimension of the Hilbert space in which ``state`` :math:`\\rho` lives in.
+ This is known as the `Hartley entropy `_
+ (also known as *Hartley function* or *max-entropy*).
+
+ In the limit :math:`\\alpha \\to \\infty`, the function reduces to
+ :math:`-\\log(\\|\\rho\\|_{\\infty})`, with :math:`\\|\\cdot\\|_{\\infty}`
+ being the `spectral norm `_.
+ This is known as the `min-entropy `_.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ alpha (float or int): order of the Rényi entropy.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Rényi entropy :math:`H_{\\alpha}`.
+ """
+ backend = _check_backend(backend)
+
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if abs(purity(state, backend=backend) - 1.0) < PRECISION_TOL:
+ return 0.0
+
+ if alpha == 0.0:
+ return np.log2(len(state)) / np.log2(base)
+
+ if alpha == 1.0:
+ return von_neumann_entropy(state, base=base, backend=backend)
+
+ if alpha == np.inf:
+ return (
+ -1
+ * backend.np.log2(backend.calculate_norm_density_matrix(state, order=2))
+ / np.log2(base)
+ )
+
+ log = backend.np.log2(backend.np.trace(_matrix_power(state, alpha, backend)))
+
+ return (1 / (1 - alpha)) * log / np.log2(base)
+
+
+def relative_renyi_entropy(
+ state, target, alpha: Union[float, int], base: float = 2, backend=None
+):
+ """Calculates the relative Rényi entropy between two quantum states.
+
+ For :math:`\\alpha \\in (0, \\, 1) \\cup (1, \\, \\infty)` and quantum states
+ :math:`\\rho` and :math:`\\sigma`, the relative Rényi entropy is defined as
+
+ .. math::
+ H_{\\alpha}(\\rho \\, \\| \\, \\sigma) = \\frac{1}{\\alpha - 1} \\,
+ \\log\\left( \\textup{tr}\\left( \\rho^{\\alpha} \\,
+ \\sigma^{1 - \\alpha} \\right) \\right) \\, .
+
+ A special case is the limit :math:`\\alpha \\to 1`, in which the Rényi entropy
+ coincides with the :func:`qibo.quantum_info.entropies.relative_entropy`.
+
+ In the limit :math:`\\alpha \\to \\infty`, the function reduces to
+ :math:`-2 \\, \\log(\\|\\sqrt{\\rho} \\, \\sqrt{\\sigma}\\|_{1})`,
+ with :math:`\\|\\cdot\\|_{1}` being the
+ `Schatten 1-norm `_.
+ This is known as the `min-relative entropy `_.
+
+ .. note::
+ Function raises ``NotImplementedError`` when ``target`` :math:`sigma`
+ is a pure state and :math:`\\alpha > 1`. This is due to the fact that
+ it is not possible to calculate :math:`\\sigma^{1 - \\alpha}` when
+ :math:`\\alpha > 1` and :math:`\\sigma` is a projector, i.e. a singular matrix.
+
+ Args:
+ state (ndarray): statevector or density matrix :math:`\\rho`.
+ target (ndarray): statevector or density matrix :math:`\\sigma`.
+ alpha (float or int): order of the Rényi entropy.
+ base (float): the base of the log. Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Relative Rényi entropy :math:`H_{\\alpha}(\\rho \\, \\| \\, \\sigma)`.
+ """
+ backend = _check_backend(backend)
+ state = backend.cast(state)
+ target = backend.cast(target)
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if (
+ (len(target.shape) >= 3)
+ or (len(target) == 0)
+ or (len(target.shape) == 2 and target.shape[0] != target.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"target must have dims either (k,) or (k,k), but have dims {target.shape}.",
+ )
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ purity_target = purity(target, backend=backend)
+ if (
+ abs(purity(state, backend=backend) - 1.0) < PRECISION_TOL
+ and abs(purity_target - 1) < PRECISION_TOL
+ ):
+ return 0.0
+
+ if alpha > 1.0 and abs(purity_target - 1) < PRECISION_TOL:
+ raise_error(
+ NotImplementedError,
+ "It is not possible to invert a singular matrix. ``target`` is a pure state and alpha > 1.",
+ )
+
+ if len(state.shape) == 1:
+ state = backend.np.outer(state, backend.np.conj(state))
+
+ if alpha == 1.0:
+ return relative_von_neumann_entropy(state, target, base, backend=backend)
+
+ if alpha == np.inf:
+ new_state = _matrix_power(state, 0.5, backend)
+ new_target = _matrix_power(target, 0.5, backend)
+
+ log = backend.np.log2(
+ backend.calculate_norm_density_matrix(new_state @ new_target, order=1)
+ )
+
+ return -2 * log / np.log2(base)
+
+ log = _matrix_power(state, alpha, backend)
+ log = log @ _matrix_power(target, 1 - alpha, backend)
+ log = backend.np.log2(backend.np.trace(log))
+
+ return (1 / (alpha - 1)) * log / np.log2(base)
+
+
+def tsallis_entropy(state, alpha: float, base: float = 2, backend=None):
+ """Calculates the Tsallis entropy of a quantum state.
+
+ .. math::
+ S_{\\alpha}(\\rho) = \\frac{1}{1 - \\alpha} \\,
+ \\left( \\text{tr}(\\rho^{\\alpha}) - 1 \\right)
+
+ When :math:`\\alpha = 1`, the functions defaults to
+ :func:`qibo.quantum_info.entropies.entropy`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ alpha (float or int): entropic index.
+ base (float, optional): the base of the log. Used when ``alpha=1.0``.
+ Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Tsallis entropy :math:`S_{\\alpha}(\\rho)`.
+ """
+ backend = _check_backend(backend)
+
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if not isinstance(alpha, (float, int)):
+ raise_error(
+ TypeError, f"alpha must be type float, but it is type {type(alpha)}."
+ )
+
+ if alpha < 0.0:
+ raise_error(ValueError, "alpha must a non-negative float.")
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if abs(purity(state, backend=backend) - 1.0) < PRECISION_TOL:
+ return 0.0
+
+ if alpha == 1.0:
+ return von_neumann_entropy(state, base=base, backend=backend)
+
+ return (1 / (1 - alpha)) * (
+ backend.np.trace(_matrix_power(state, alpha, backend)) - 1
+ )
+
+
+def entanglement_entropy(
+ state,
+ bipartition,
+ base: float = 2,
+ check_hermitian: bool = False,
+ return_spectrum: bool = False,
+ backend=None,
+):
+ """Calculates the entanglement entropy :math:`S` of bipartition :math:`A`
+ of ``state`` :math:`\\rho`. This is given by
+
+ .. math::
+ S(\\rho_{A}) = -\\text{tr}(\\rho_{A} \\, \\log(\\rho_{A})) \\, ,
+
+ where :math:`\\rho_{A} = \\text{tr}_{B}(\\rho)` is the reduced density matrix calculated
+ by tracing out the ``bipartition`` :math:`B`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ bipartition (list or tuple or ndarray): qubits in the subsystem to be traced out.
+ base (float, optional): the base of the log. Defaults to :math: `2`.
+ check_hermitian (bool, optional): if ``True``, checks if :math:`\\rho_{A}` is Hermitian.
+ If ``False``, it assumes ``state`` is Hermitian . Default: ``False``.
+ return_spectrum: if ``True``, returns ``entropy`` and eigenvalues of ``state``.
+ If ``False``, returns only ``entropy``. Default is ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ float: Entanglement entropy :math:`S` of ``state`` :math:`\\rho`.
+ """
+ backend = _check_backend(backend)
+
+ if base <= 0.0:
+ raise_error(ValueError, "log base must be non-negative.")
+
+ if (
+ (len(state.shape) not in [1, 2])
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ reduced_density_matrix = partial_trace(state, bipartition, backend=backend)
+
+ entropy_entanglement = von_neumann_entropy(
+ reduced_density_matrix,
+ base=base,
+ check_hermitian=check_hermitian,
+ return_spectrum=return_spectrum,
+ backend=backend,
+ )
+
+ return entropy_entanglement
+
+
+def _matrix_power(matrix, alpha, backend):
+ """Calculates ``matrix ** alpha`` according to backend."""
+ if backend.__class__.__name__ in [
+ "CupyBackend",
+ "CuQuantumBackend",
+ ]: # pragma: no cover
+ new_matrix = backend.to_numpy(matrix)
+ else:
+ new_matrix = backend.np.copy(matrix)
+
+ if len(new_matrix.shape) == 1:
+ new_matrix = backend.np.outer(new_matrix, backend.np.conj(new_matrix))
+
+ return backend.cast(fractional_matrix_power(backend.to_numpy(new_matrix), alpha))
diff --git a/src/qibo/quantum_info/linalg_operations.py b/src/qibo/quantum_info/linalg_operations.py
new file mode 100644
index 000000000..ae87ba4e1
--- /dev/null
+++ b/src/qibo/quantum_info/linalg_operations.py
@@ -0,0 +1,198 @@
+"""Module with common linear algebra operations for quantum information."""
+
+import math
+from typing import List, Tuple, Union
+
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+
+
+def commutator(operator_1, operator_2):
+ """Returns the commutator of ``operator_1`` and ``operator_2``.
+
+ The commutator of two matrices :math:`A` and :math:`B` is given by
+
+ .. math::
+ [A, B] = A \\, B - B \\, A \\,.
+
+ Args:
+ operator_1 (ndarray): First operator.
+ operator_2 (ndarray): Second operator.
+
+ Returns:
+ ndarray: Commutator of ``operator_1`` and ``operator_2``.
+ """
+ if (
+ (len(operator_1.shape) >= 3)
+ or (len(operator_1) == 0)
+ or (len(operator_1.shape) == 2 and operator_1.shape[0] != operator_1.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"``operator_1`` must have shape (k,k), but have shape {operator_1.shape}.",
+ )
+
+ if (
+ (len(operator_2.shape) >= 3)
+ or (len(operator_2) == 0)
+ or (len(operator_2.shape) == 2 and operator_2.shape[0] != operator_2.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"``operator_2`` must have shape (k,k), but have shape {operator_2.shape}.",
+ )
+
+ if operator_1.shape != operator_2.shape:
+ raise_error(
+ TypeError,
+ "``operator_1`` and ``operator_2`` must have the same shape, "
+ + f"but {operator_1.shape} != {operator_2.shape}",
+ )
+
+ return operator_1 @ operator_2 - operator_2 @ operator_1
+
+
+def anticommutator(operator_1, operator_2):
+ """Returns the anticommutator of ``operator_1`` and ``operator_2``.
+
+ The anticommutator of two matrices :math:`A` and :math:`B` is given by
+
+ .. math::
+ \\{A, B\\} = A \\, B + B \\, A \\,.
+
+ Args:
+ operator_1 (ndarray): First operator.
+ operator_2 (ndarray): Second operator.
+
+ Returns:
+ ndarray: Anticommutator of ``operator_1`` and ``operator_2``.
+ """
+ if (
+ (len(operator_1.shape) >= 3)
+ or (len(operator_1) == 0)
+ or (len(operator_1.shape) == 2 and operator_1.shape[0] != operator_1.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"``operator_1`` must have shape (k,k), but have shape {operator_1.shape}.",
+ )
+
+ if (
+ (len(operator_2.shape) >= 3)
+ or (len(operator_2) == 0)
+ or (len(operator_2.shape) == 2 and operator_2.shape[0] != operator_2.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"``operator_2`` must have shape (k,k), but have shape {operator_2.shape}.",
+ )
+
+ if operator_1.shape != operator_2.shape:
+ raise_error(
+ TypeError,
+ "``operator_1`` and ``operator_2`` must have the same shape, "
+ + f"but {operator_1.shape} != {operator_2.shape}",
+ )
+
+ return operator_1 @ operator_2 + operator_2 @ operator_1
+
+
+def partial_trace(state, traced_qubits: Union[List[int], Tuple[int]], backend=None):
+ """Returns the density matrix resulting from tracing out ``traced_qubits`` from ``state``.
+
+ Total number of qubits is inferred by the shape of ``state``.
+
+ Args:
+ state (ndarray): density matrix or statevector.
+ traced_qubits (Union[List[int], Tuple[int]]): indices of qubits to be traced out.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Density matrix of the remaining qubit(s).
+ """
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"``state`` must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ backend = _check_backend(backend)
+
+ state = backend.cast(state, dtype=state.dtype)
+ nqubits = math.log2(state.shape[0])
+
+ if not nqubits.is_integer():
+ raise_error(
+ ValueError,
+ "dimension(s) of ``state`` must be a power of 2, "
+ + f"but it is {2**nqubits}.",
+ )
+
+ nqubits = int(nqubits)
+
+ statevector = bool(len(state.shape) == 1)
+
+ factor = 1 if statevector else 2
+ state = backend.np.reshape(state, factor * nqubits * (2,))
+
+ if statevector:
+ axes = 2 * [list(traced_qubits)]
+ rho = backend.np.tensordot(state, backend.np.conj(state), axes)
+ shape = 2 * (2 ** (nqubits - len(traced_qubits)),)
+
+ return backend.np.reshape(rho, shape)
+
+ order = tuple(sorted(traced_qubits))
+ order += tuple(i for i in range(nqubits) if i not in traced_qubits)
+ order += tuple(i + nqubits for i in order)
+ shape = 2 * (2 ** len(traced_qubits), 2 ** (nqubits - len(traced_qubits)))
+
+ state = backend.np.transpose(state, order)
+ state = backend.np.reshape(state, shape)
+
+ return backend.np.einsum("abac->bc", state)
+
+
+def matrix_exponentiation(
+ phase: Union[float, complex],
+ matrix,
+ eigenvectors=None,
+ eigenvalues=None,
+ backend=None,
+):
+ """Calculates the exponential of a matrix.
+
+ Given a ``matrix`` :math:`H` and a ``phase`` :math:`\\theta`,
+ it returns the exponential of the form
+
+ .. math::
+ \\exp\\left(-i \\, \\theta \\, H \\right) \\, .
+
+ If the ``eigenvectors`` and ``eigenvalues`` are given, the matrix diagonalization
+ is used for the exponentiation.
+
+ Args:
+ phase (float or complex): phase that multiplies the matrix.
+ matrix (ndarray): matrix to be exponentiated.
+ eigenvectors (ndarray, optional): _if not ``None``, eigenvectors are used
+ to calculate ``matrix`` exponentiation as part of diagonalization.
+ Must be used together with ``eigenvalues``. Defaults to ``None``.
+ eigenvalues (ndarray, optional): if not ``None``, eigenvalues are used
+ to calculate ``matrix`` exponentiation as part of diagonalization.
+ Must be used together with ``eigenvectors``. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: matrix exponential of :math:`-i \\, \\theta \\, H`.
+ """
+ backend = _check_backend(backend)
+
+ return backend.calculate_matrix_exp(phase, matrix, eigenvectors, eigenvalues)
diff --git a/src/qibo/quantum_info/metrics.py b/src/qibo/quantum_info/metrics.py
new file mode 100644
index 000000000..4699efbda
--- /dev/null
+++ b/src/qibo/quantum_info/metrics.py
@@ -0,0 +1,851 @@
+"""Distances, metrics, and measures for quantum states and channels."""
+
+from typing import Optional, Union
+
+import numpy as np
+from scipy import sparse
+
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+
+
+def purity(state, backend=None):
+ """Purity of a quantum state :math:`\\rho`.
+
+ This is given by
+
+ .. math::
+ \\text{purity}(\\rho) = \\text{tr}(\\rho^{2}) \\, .
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ Returns:
+ float: Purity of quantum ``state`` :math:`\\rho`.
+ """
+ backend = _check_backend(backend)
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"state must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if len(state.shape) == 1:
+ pur = backend.np.real(backend.calculate_norm(state)) ** 2
+ else:
+ pur = backend.np.real(backend.np.trace(backend.np.matmul(state, state)))
+ return float(pur)
+
+
+def impurity(state, backend=None):
+ """Impurity of quantum state :math:`\\rho`.
+
+ This is given by :math:`1 - \\text{purity}(\\rho)`, where :math:`\\text{purity}`
+ is defined in :func:`qibo.quantum_info.purity`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+
+ Returns:
+ float: impurity of ``state`` :math:`\\rho`.
+ """
+ return 1 - purity(state, backend=backend)
+
+
+def trace_distance(state, target, check_hermitian: bool = False, backend=None):
+ """Trace distance between two quantum states, :math:`\\rho` and
+ :math:`\\sigma`:
+
+ .. math::
+ T(\\rho, \\sigma) = \\frac{1}{2} \\, \\|\\rho - \\sigma\\|_{1} = \\frac{1}{2} \\,
+ \\text{tr}\\left[ \\sqrt{(\\rho - \\sigma)^{\\dagger}(\\rho - \\sigma)}
+ \\right] \\, ,
+
+ where :math:`\\|\\cdot\\|_{1}` is the Schatten 1-norm.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ check_hermitian (bool, optional): if ``True``, checks if
+ :math:`\\rho - \\sigma` is Hermitian. If ``False``,
+ it assumes the difference is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Trace distance between ``state`` :math:`\\rho` and ``target`` :math:`\\sigma`.
+ """
+ backend = _check_backend(backend)
+
+ if state.shape != target.shape:
+ raise_error(
+ TypeError,
+ f"State has dims {state.shape} while target has dims {target.shape}.",
+ )
+
+ if (len(state.shape) >= 3) or (len(state) == 0):
+ raise_error(
+ TypeError,
+ "Both objects must have dims either (k,) or (k,l), "
+ + f"but have dims {state.shape} and {target.shape}",
+ )
+
+ if isinstance(check_hermitian, bool) is False:
+ raise_error(
+ TypeError,
+ f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
+ )
+
+ if len(state.shape) == 1:
+ state = backend.np.outer(backend.np.conj(state), state)
+ target = backend.np.outer(backend.np.conj(target), target)
+
+ difference = state - target
+ if check_hermitian is True:
+ hermitian = bool(
+ float(
+ backend.calculate_norm_density_matrix(
+ backend.np.transpose(backend.np.conj(difference), (1, 0))
+ - difference,
+ order=2,
+ )
+ )
+ <= PRECISION_TOL
+ )
+ if (
+ not hermitian and backend.__class__.__name__ == "CupyBackend"
+ ): # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "CupyBackend does not support `np.linalg.eigvals`"
+ + "for non-Hermitian `state - target`.",
+ )
+ eigenvalues = backend.calculate_eigenvalues(difference, hermitian=hermitian)
+ else:
+ eigenvalues = backend.calculate_eigenvalues(difference)
+
+ distance = backend.np.sum(backend.np.absolute(eigenvalues)) / 2
+
+ return distance
+
+
+def hilbert_schmidt_distance(state, target, backend=None):
+ """Hilbert-Schmidt distance between two quantum states:
+
+ .. math::
+ \\langle \\rho \\, , \\, \\sigma \\rangle_{\\text{HS}} =
+ \\text{tr}\\left((\\rho - \\sigma)^{2}\\right)
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Hilbert-Schmidt distance between ``state`` :math:`\\rho`
+ and ``target`` :math:`\\sigma`.
+ """
+ backend = _check_backend(backend)
+
+ if state.shape != target.shape:
+ raise_error(
+ TypeError,
+ f"State has dims {state.shape} while target has dims {target.shape}.",
+ )
+
+ if (len(state.shape) >= 3) or (len(state) == 0):
+ raise_error(
+ TypeError,
+ "Both objects must have dims either (k,) or (k,l), "
+ + f"but have dims {state.shape} and {target.shape}",
+ )
+
+ if len(state.shape) == 1:
+ state = backend.np.outer(backend.np.conj(state), state)
+ target = backend.np.outer(backend.np.conj(target), target)
+
+ distance = backend.np.real(backend.np.trace((state - target) ** 2))
+ distance = float(distance)
+
+ return distance
+
+
+def fidelity(state, target, check_hermitian: bool = False, backend=None):
+ """Fidelity :math:`F(\\rho, \\sigma)` between ``state`` :math:`\\rho` and
+ ``target`` state :math:`\\sigma`. In general,
+
+ .. math::
+ F(\\rho, \\sigma) = \\text{tr}^{2}\\left( \\sqrt{\\sqrt{\\sigma} \\,
+ \\rho^{\\dagger} \\, \\sqrt{\\sigma}} \\right) \\, .
+
+ However, when at least one of the states is pure, then
+
+ .. math::
+ F(\\rho, \\sigma) = \\text{tr}(\\rho \\, \\sigma)
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ check_hermitian (bool, optional): if ``True``, checks if ``state`` is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Fidelity between ``state`` :math:`\\rho` and ``target`` :math:`\\sigma`.
+ """
+ backend = _check_backend(backend)
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ if state.shape != target.shape:
+ raise_error(
+ TypeError,
+ f"State has dims {state.shape} while target has dims {target.shape}.",
+ )
+
+ if len(state.shape) >= 3 or len(state.shape) == 0:
+ raise_error(
+ TypeError,
+ "Both objects must have dims either (k,) or (k,l), "
+ + f"but have dims {state.shape} and {target.shape}",
+ )
+
+ if isinstance(check_hermitian, bool) is False:
+ raise_error(
+ TypeError,
+ f"check_hermitian must be type bool, but it is type {type(check_hermitian)}.",
+ )
+
+ # check purity if both states are density matrices
+ if len(state.shape) == 2 and len(target.shape) == 2:
+ purity_state = purity(state, backend=backend)
+ purity_target = purity(target, backend=backend)
+
+ # if both states are mixed, default to full fidelity calculation
+ if (
+ abs(purity_state - 1) > PRECISION_TOL
+ and abs(purity_target - 1) > PRECISION_TOL
+ ):
+ hermitian = check_hermitian is False or _check_hermitian_or_not_gpu(
+ state, backend=backend
+ )
+ # using eigh since rho is supposed to be Hermitian
+ eigenvalues, eigenvectors = backend.calculate_eigenvectors(
+ state, hermitian=hermitian
+ )
+ state = np.zeros(state.shape, dtype=complex)
+ state = backend.cast(state, dtype=state.dtype)
+ for eig, eigvec in zip(
+ eigenvalues, backend.np.transpose(eigenvectors, (1, 0))
+ ):
+ matrix = backend.np.sqrt(eig) * backend.np.outer(
+ eigvec, backend.np.conj(eigvec)
+ )
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ state = state + matrix
+ del matrix
+
+ fid = state @ target @ state
+
+ # since sqrt(rho) is Hermitian, we can use eigh again
+ eigenvalues, eigenvectors = backend.calculate_eigenvectors(
+ fid, hermitian=hermitian
+ )
+ fid = np.zeros(state.shape, dtype=complex)
+ fid = backend.cast(fid, dtype=fid.dtype)
+ for eig, eigvec in zip(
+ eigenvalues, backend.np.transpose(eigenvectors, (1, 0))
+ ):
+ if backend.np.real(eig) > PRECISION_TOL:
+ matrix = backend.np.sqrt(eig) * backend.np.outer(
+ eigvec, backend.np.conj(eigvec)
+ )
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ fid = fid + matrix
+ del matrix
+
+ fid = backend.np.real(backend.np.trace(fid)) ** 2
+
+ return fid
+
+ # if any of the states is pure, perform lighter calculation
+ fid = (
+ backend.np.abs(backend.np.matmul(backend.np.conj(state), target)) ** 2
+ if len(state.shape) == 1
+ else backend.np.real(backend.np.trace(backend.np.matmul(state, target)))
+ )
+
+ return fid
+
+
+def infidelity(state, target, check_hermitian: bool = False, backend=None):
+ """Infidelity between ``state`` :math:`\\rho` and ``target`` state
+ :math:`\\sigma`, which is given by
+
+ .. math::
+ 1 - F(\\rho, \\, \\sigma) \\, ,
+
+ where :math:`F(\\rho, \\, \\sigma)` is the :func:`qibo.quantum_info.fidelity`
+ between ``state`` and ``target``.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ check_hermitian (bool, optional): if ``True``, checks if ``state`` is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Infidelity between ``state`` :math:`\\rho` and ``target`` :math:`\\sigma`.
+ """
+ return 1 - fidelity(state, target, check_hermitian=check_hermitian, backend=backend)
+
+
+def bures_angle(state, target, check_hermitian: bool = False, backend=None):
+ """Calculates the Bures angle :math:`D_{A}` between a ``state``
+ :math:`\\rho` and a ``target`` state :math:`\\sigma`. This is given by
+
+ .. math::
+ D_{A}(\\rho, \\, \\sigma) = \\text{arccos}\\left(\\sqrt{F(\\rho, \\, \\sigma)}\\right) \\, ,
+
+ where :math:`F(\\rho, \\sigma)` is the :func:`qibo.quantum_info.fidelity`
+ between `state` and `target`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ check_hermitian (bool, optional): if ``True``, checks if ``state`` is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Bures angle between ``state`` and ``target``.
+ """
+ backend = _check_backend(backend)
+
+ angle = backend.np.arccos(
+ backend.np.sqrt(fidelity(state, target, check_hermitian, backend=backend))
+ )
+
+ return angle
+
+
+def bures_distance(state, target, check_hermitian: bool = False, backend=None):
+ """Calculates the Bures distance :math:`D_{B}` between a ``state``
+ :math:`\\rho` and a ``target`` state :math:`\\sigma`. This is given by
+
+ .. math::
+ D_{B}(\\rho, \\, \\sigma) = \\sqrt{2 \\, \\left(1 - \\sqrt{F(\\rho, \\, \\sigma)}\\right)}
+
+ where :math:`F(\\rho, \\sigma)` is the :func:`qibo.quantum_info.fidelity`
+ between `state` and `target`.
+
+ Args:
+ state (ndarray): statevector or density matrix.
+ target (ndarray): statevector or density matrix.
+ check_hermitian (bool, optional): if ``True``, checks if ``state`` is Hermitian.
+ Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Bures distance between ``state`` and ``target``.
+ """
+ backend = _check_backend(backend)
+ sqrt_fid = backend.np.sqrt(
+ fidelity(state, target, check_hermitian, backend=backend)
+ )
+ distance = backend.np.sqrt(2 * (1 - sqrt_fid))
+
+ return distance
+
+
+def process_fidelity(channel, target=None, check_unitary: bool = False, backend=None):
+ """Process fidelity between a quantum ``channel`` :math:`\\mathcal{E}` and
+ a ``target`` unitary channel :math:`U`. The process fidelity is defined as
+
+ .. math::
+ F_{\\text{pro}}(\\mathcal{E}, \\mathcal{U}) = \\frac{1}{d^{2}} \\,
+ \\text{tr}(\\mathcal{E}^{\\dagger} \\, \\mathcal{U})
+
+ Args:
+ channel: quantum channel :math:`\\mathcal{E}`.
+ target (optional): quantum channel :math:`U`. If ``None``, target is the
+ Identity channel. Defaults to ``None``.
+ check_unitary (bool, optional): if ``True``, checks if one of the
+ input channels is unitary. Default: ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Process fidelity between ``channel`` and ``target``.
+ """
+ backend = _check_backend(backend)
+
+ if target is not None:
+ if channel.shape != target.shape:
+ raise_error(
+ TypeError,
+ f"Channels must have the same dims, but {channel.shape} != {target.shape}",
+ )
+
+ dim = int(np.sqrt(channel.shape[0]))
+
+ if check_unitary is True:
+ norm_channel = float(
+ backend.calculate_norm_density_matrix(
+ backend.np.matmul(
+ backend.np.conj(backend.np.transpose(channel, (1, 0))), channel
+ )
+ - backend.np.eye(dim**2)
+ )
+ )
+ if target is None and norm_channel > PRECISION_TOL:
+ raise_error(TypeError, "Channel is not unitary and Target is None.")
+ if target is not None:
+ norm_target = float(
+ backend.calculate_norm(
+ backend.np.matmul(
+ backend.np.conj(backend.np.transpose(target, (1, 0))), target
+ )
+ - backend.np.eye(dim**2)
+ )
+ )
+ if (norm_channel > PRECISION_TOL) and (norm_target > PRECISION_TOL):
+ raise_error(TypeError, "Neither channel is unitary.")
+
+ if target is None:
+ # With no target, return process fidelity with Identity channel
+ process_fid = backend.np.real(backend.np.trace(channel)) / dim**2
+ process_fid = float(process_fid)
+
+ return process_fid
+
+ process_fid = backend.np.matmul(
+ backend.np.transpose(backend.np.conj(channel), (1, 0)), target
+ )
+ process_fid = backend.np.real(backend.np.trace(process_fid)) / dim**2
+
+ return process_fid
+
+
+def process_infidelity(channel, target=None, check_unitary: bool = False, backend=None):
+ """Process infidelity between quantum channel :math:`\\mathcal{E}` and a
+ ``target`` unitary channel :math:`U`. The process infidelity is defined as
+
+ .. math::
+ 1 - F_{\\text{pro}}(\\mathcal{E}, \\mathcal{U}) \\, ,
+
+ where :math:`F_{\\text{pro}}` is the :func:`qibo.quantum_info.process_fidelity`.
+
+ Args:
+ channel: quantum channel :math:`\\mathcal{E}`.
+ target (optional): quantum channel :math:`U`. If ``None``, target is the
+ Identity channel. Defaults to ``None``.
+ check_unitary (bool, optional): if ``True``, checks if one of the
+ input channels is unitary. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+
+ Returns:
+ float: Process infidelity between ``channel`` :math:`\\mathcal{E}`
+ and ``target`` :math:`U`.
+ """
+ return 1 - process_fidelity(
+ channel, target=target, check_unitary=check_unitary, backend=backend
+ )
+
+
+def average_gate_fidelity(
+ channel, target=None, check_unitary: bool = False, backend=None
+):
+ """Average gate fidelity between a quantum ``channel`` :math:`\\mathcal{E}`
+ and a ``target`` unitary channel :math:`U`. The average gate fidelity is
+ defined as
+
+ .. math::
+ F_{\\text{avg}}(\\mathcal{E}, \\mathcal{U}) = \\frac{d \\,
+ F_{pro}(\\mathcal{E}, \\mathcal{U}) + 1}{d + 1}
+
+ where :math:`d` is the dimension of the channels and
+ :math:`F_{pro}(\\mathcal{E}, \\mathcal{U})` is the
+ :meth:`~qibo.metrics.process_fidelily` of channel
+ :math:`\\mathcal{E}` with respect to the unitary
+ channel :math:`\\mathcal{U}`.
+
+ Args:
+ channel: quantum channel :math:`\\mathcal{E}`.
+ target (optional): quantum channel :math:`\\mathcal{U}`.
+ If ``None``, target is the Identity channel. Defaults to ``None``.
+ check_unitary (bool, optional): if ``True``, checks if one of the
+ input channels is unitary. Default: ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Process fidelity between channel :math:`\\mathcal{E}`
+ and target unitary channel :math:`\\mathcal{U}`.
+ """
+
+ dim = channel.shape[0]
+
+ process_fid = process_fidelity(
+ channel, target, check_unitary=check_unitary, backend=backend
+ )
+ process_fid = (dim * process_fid + 1) / (dim + 1)
+
+ return process_fid
+
+
+def gate_error(channel, target=None, check_unitary: bool = False, backend=None):
+ """Gate error between a quantum ``channel`` :math:`\\mathcal{E}` and a
+ ``target`` unitary channel :math:`U`, which is defined as
+
+ .. math::
+ E(\\mathcal{E}, \\mathcal{U}) = 1 - F_{\\text{avg}}(\\mathcal{E}, \\mathcal{U}) \\, ,
+
+ where :math:`F_{\\text{avg}}(\\mathcal{E}, \\mathcal{U})` is the
+ :func:`qibo.quantum_info.average_gate_fidelity`.
+
+ Args:
+ channel: quantum channel :math:`\\mathcal{E}`.
+ target (optional): quantum channel :math:`\\mathcal{U}`. If ``None``,
+ target is the Identity channel. Defaults to ``None``.
+ check_unitary (bool, optional): if ``True``, checks if one of the
+ input channels is unitary. Default: ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Gate error between ``channel`` :math:`\\mathcal{E}`
+ and ``target`` :math:`\\mathcal{U}`.
+ """
+ error = 1 - average_gate_fidelity(
+ channel, target, check_unitary=check_unitary, backend=backend
+ )
+
+ return error
+
+
+def diamond_norm(channel, target=None, backend=None, **kwargs): # pragma: no cover
+ """Calculates the diamond norm :math:`\\|\\mathcal{E}\\|_{\\diamond}` of
+ ``channel`` :math:`\\mathcal{E}`, which is given by
+
+ .. math::
+ \\|\\mathcal{E}\\|_{\\diamond} = \\max_{\\rho} \\, \\| \\left(\\mathcal{E} \\otimes I_{d^{2}}\\right)(\\rho) \\|_{1} \\, ,
+
+ where :math:`I_{d^{2}}` is the :math:`d^{2} \\times d^{2}` Identity operator,
+ :math:`d = 2^{n}`, :math:`n` is the number of qubits,
+ and :math:`\\|\\cdot\\|_{1}` denotes the trace norm.
+
+ If a ``target`` channel :math:`\\Lambda` is specified,
+ then it calculates :math:`\\| \\mathcal{E} - \\Lambda\\|_{\\diamond}`.
+
+ Example::
+
+ from qibo.quantum_info import diamond_norm, random_unitary, to_choi
+
+ nqubits = 1
+ dim = 2**nqubits
+
+ unitary = random_unitary(dim)
+ unitary = to_choi(unitary, order="row")
+
+ unitary_2 = random_unitary(dim)
+ unitary_2 = to_choi(unitary_2, order="row")
+
+ dnorm = diamond_norm(unitary, unitary_2)
+
+ Args:
+ channel (ndarray): row-vectorized Choi representation of a quantum channel.
+ target (ndarray, optional): row-vectorized Choi representation of a target
+ quantum channel. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ kwargs: optional arguments to pass to CVXPY solver. For more information,
+ please visit `CVXPY's API documentation
+ `_.
+
+ Returns:
+ float: diamond norm of either ``channel`` or ``channel - target``.
+
+ .. note::
+ This function requires the optional CVXPY package to be installed.
+ """
+ import cvxpy
+
+ backend = _check_backend(backend)
+
+ if target is not None:
+ if channel.shape != target.shape:
+ raise_error(
+ TypeError,
+ f"Channels must have the same dims, but {channel.shape} != {target.shape}",
+ )
+
+ if target is not None:
+ channel -= target
+
+ # `CVXPY` only works with `numpy`, so this function has to
+ # convert any channel to the `numpy` backend by default
+ backend = _check_backend(backend)
+
+ channel = backend.to_numpy(channel)
+
+ channel = backend.np.transpose(channel, (1, 0))
+ channel_real = backend.np.real(channel)
+ channel_imag = backend.np.imag(channel)
+
+ dim = int(np.sqrt(channel.shape[0]))
+
+ first_variables_real = cvxpy.Variable(shape=(dim, dim))
+ first_variables_imag = cvxpy.Variable(shape=(dim, dim))
+ first_variables = cvxpy.bmat(
+ [
+ [first_variables_real, -first_variables_imag],
+ [first_variables_imag, first_variables_real],
+ ]
+ )
+
+ second_variables_real = cvxpy.Variable(shape=(dim, dim))
+ second_variables_imag = cvxpy.Variable(shape=(dim, dim))
+ second_variables = cvxpy.bmat(
+ [
+ [second_variables_real, -second_variables_imag],
+ [second_variables_imag, second_variables_real],
+ ]
+ )
+
+ variables_real = cvxpy.Variable(shape=(dim**2, dim**2))
+ variables_imag = cvxpy.Variable(shape=(dim**2, dim**2))
+ identity = sparse.eye(dim)
+
+ constraints_real = cvxpy.bmat(
+ [
+ [cvxpy.kron(identity, first_variables_real), variables_real],
+ [variables_real.T, cvxpy.kron(identity, second_variables_real)],
+ ]
+ )
+ constraints_imag = cvxpy.bmat(
+ [
+ [cvxpy.kron(identity, first_variables_imag), variables_imag],
+ [-variables_imag.T, cvxpy.kron(identity, second_variables_imag)],
+ ]
+ )
+ constraints_block = cvxpy.bmat(
+ [[constraints_real, -constraints_imag], [constraints_imag, constraints_real]]
+ )
+
+ constraints = [
+ first_variables >> 0,
+ first_variables_real == first_variables_real.T,
+ first_variables_imag == -first_variables_imag.T,
+ cvxpy.trace(first_variables_real) == 1,
+ second_variables >> 0,
+ second_variables_real == second_variables_real.T,
+ second_variables_imag == -second_variables_imag.T,
+ cvxpy.trace(second_variables_real) == 1,
+ constraints_block >> 0,
+ ]
+
+ objective_function = cvxpy.Maximize(
+ cvxpy.trace(channel_real @ variables_real)
+ + cvxpy.trace(channel_imag @ variables_imag)
+ )
+ problem = cvxpy.Problem(objective=objective_function, constraints=constraints)
+ solution = problem.solve(**kwargs)
+
+ return solution
+
+
+def expressibility(
+ circuit,
+ power_t: int,
+ samples: int,
+ order: Optional[Union[int, float, str]] = 2,
+ backend=None,
+):
+ """Returns the expressibility :math:`\\|A\\|` of a parametrized circuit,
+ where
+
+ .. math::
+ A = \\int_{\\text{Haar}} d\\psi \\, \\left(|\\psi\\rangle\\right.\\left.
+ \\langle\\psi|\\right)^{\\otimes t} - \\int_{\\Theta} d\\psi \\,
+ \\left(|\\psi_{\\theta}\\rangle\\right.\\left.
+ \\langle\\psi_{\\theta}|\\right)^{\\otimes t}
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
+ power_t (int): power that defines the :math:`t`-design.
+ samples (int): number of samples to estimate the integrals.
+ order (int or float or str, optional): order of the norm :math:`\\|A\\|`.
+ For specifications, see :meth:`qibo.backends.abstract.calculate_norm`.
+ Defaults to :math:`2`.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Expressibility of parametrized circuit.
+ """
+
+ if isinstance(power_t, int) is False:
+ raise_error(
+ TypeError, f"power_t must be type int, but it is type {type(power_t)}."
+ )
+
+ if isinstance(samples, int) is False:
+ raise_error(
+ TypeError, f"samples must be type int, but it is type {type(samples)}."
+ )
+
+ from qibo.quantum_info.utils import ( # pylint: disable=C0415
+ haar_integral,
+ pqc_integral,
+ )
+
+ backend = _check_backend(backend)
+
+ deviation = haar_integral(
+ circuit.nqubits, power_t, samples=None, backend=backend
+ ) - pqc_integral(circuit, power_t, samples, backend=backend)
+
+ fid = float(backend.calculate_norm(deviation, order=order))
+
+ return fid
+
+
+def frame_potential(
+ circuit,
+ power_t: int,
+ samples: int = None,
+ backend=None,
+):
+ """Returns the frame potential of a parametrized circuit under uniform
+ sampling of the parameters.
+
+ For :math:`n` qubits and moment :math:`t`, the frame potential
+ :math:`\\mathcal{F}_{\\mathcal{U}}^{(t)}` if given by [1]
+
+ .. math::
+ \\mathcal{F}_{\\mathcal{U}}^{(t)} = \\int_{U,V \\in \\mathcal{U}} \\,
+ \\text{d}U \\, \\text{d}V \\, \\bigl| \\, \\text{tr}(U^{\\dagger} \\, V)
+ \\, \\bigr|^{2t} \\, ,
+
+ where :math:`\\mathcal{U}` is the group of unitaries defined by the parametrized circuit.
+ The frame potential is approximated by the average
+
+ .. math::
+ \\mathcal{F}_{\\mathcal{U}}^{(t)} \\approx \\frac{1}{N} \\,
+ \\sum_{k=1}^{N} \\, \\bigl| \\, \\text{tr}(U_{k}^{\\dagger} \\, V_{k}) \\, \\bigr|^{2t} \\, ,
+
+ where :math:`N` is the number of ``samples``.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Parametrized circuit.
+ power_t (int): power that defines the :math:`t`-design.
+ samples (int): number of samples to estimate the integral.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ float: Frame potential of the parametrized circuit.
+
+ References:
+ 1. M. Liu *et al.*, *Estimating the randomness of quantum circuit ensembles up to 50 qubits*.
+ `arXiv:2205.09900 [quant-ph] `_.
+ """
+ if not isinstance(power_t, int):
+ raise_error(
+ TypeError, f"power_t must be type int, but it is type {type(power_t)}."
+ )
+
+ if not isinstance(samples, int):
+ raise_error(
+ TypeError, f"samples must be type int, but it is type {type(samples)}."
+ )
+
+ backend = _check_backend(backend)
+
+ nqubits = circuit.nqubits
+ dim = 2**nqubits
+
+ potential = 0
+ for _ in range(samples):
+ unitary_1 = circuit.copy()
+ params_1 = np.random.uniform(-np.pi, np.pi, circuit.trainable_gates.nparams)
+ unitary_1.set_parameters(params_1)
+ unitary_1 = unitary_1.unitary(backend) / np.sqrt(dim)
+
+ for _ in range(samples):
+ unitary_2 = circuit.copy()
+ params_2 = np.random.uniform(-np.pi, np.pi, circuit.trainable_gates.nparams)
+ unitary_2.set_parameters(params_2)
+ unitary_2 = unitary_2.unitary(backend) / np.sqrt(dim)
+
+ potential += backend.np.abs(
+ backend.np.trace(
+ backend.np.transpose(backend.np.conj(unitary_1), (1, 0)) @ unitary_2
+ )
+ ) ** (2 * power_t)
+
+ return potential / samples**2
+
+
+def _check_hermitian_or_not_gpu(matrix, backend=None):
+ """Checks if a given matrix is Hermitian and whether the backend is neither
+ :class:`qibojit.backends.CupyBackend` nor
+ :class:`qibojit.backends.CuQuantumBackend`.
+
+ Args:
+ matrix: input array.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ bool: whether the matrix is Hermitian.
+
+ Raises:
+ NotImplementedError: If `matrix` is not Hermitian and
+ `backend` is not :class:`qibojit.backends.CupyBackend`
+ """
+ backend = _check_backend(backend)
+
+ norm = backend.calculate_norm_density_matrix(
+ backend.np.conj(matrix).T - matrix, order=2
+ )
+
+ hermitian = bool(float(norm) <= PRECISION_TOL)
+
+ if hermitian is False and backend.__class__.__name__ in [
+ "CupyBackend",
+ "CuQuantumBackend",
+ ]: # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ "GPU backends do not support `np.linalg.eig` "
+ + "or `np.linalg.eigvals` for non-Hermitian matrices.",
+ )
+
+ return hermitian
diff --git a/src/qibo/quantum_info/quantum_networks.py b/src/qibo/quantum_info/quantum_networks.py
new file mode 100644
index 000000000..691f40a51
--- /dev/null
+++ b/src/qibo/quantum_info/quantum_networks.py
@@ -0,0 +1,1118 @@
+"""Module defining the `QuantumNetwork` class and adjacent functions."""
+
+from functools import reduce
+from logging import warning
+from operator import mul
+from typing import List, Optional, Tuple, Union
+
+import numpy as np
+
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+
+
+class QuantumNetwork:
+ """This class stores the representation of the quantum network as a tensor.
+ This is a unique representation of the quantum network.
+
+ A minimum quantum network is a quantum channel, which is a quantum network of the form
+ :math:`J[n \\to m]`, where :math:`n` is the dimension of the input system ,
+ and :math:`m` is the dimension of the output system.
+ A quantum state is a quantum network of the form :math:`J: 1 \\to n`,
+ such that the input system is trivial.
+ An observable is a quantum network of the form :math:`J: n \\to 1`,
+ such that the output system is trivial.
+
+ A quantum network may contain multiple input and output systems.
+ For example, a "quantum comb" is a quantum network of the form :math:`J: n', n \\to m, m'`,
+ which convert a quantum channel of the form :math:`J: n \\to m`
+ to a quantum channel of the form :math:`J: n' \\to m'`.
+
+ Args:
+ tensor (ndarray): input Choi operator.
+ partition (List[int] or Tuple[int]): partition of ``tensor``.
+ system_input (List[bool] or Tuple[bool], optional): mask on the output system of the
+ Choi operator. If ``None``, defaults to
+ ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``.
+ Defaults to ``None``.
+ pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure
+ state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+
+ def __init__(
+ self,
+ tensor,
+ partition: Optional[Union[List[int], Tuple[int]]] = None,
+ system_input: Optional[Union[List[bool], Tuple[bool]]] = None,
+ pure: bool = False,
+ backend=None,
+ ):
+ self._tensor = tensor
+ self.partition = tuple(partition)
+ self.system_input = system_input
+ self._pure = pure
+ self._backend = backend
+
+ self._run_checks(self.partition, self.system_input, self._pure)
+
+ self._set_parameters()
+
+ self.dims = reduce(mul, self.partition) if len(self.partition) > 0 else 1
+
+ @staticmethod
+ def _order_tensor_to_operator(dims: int):
+ """Returns the order to reshape a tensor into an operator.
+
+ Given a tenosr of ``2 * dims`` leads, the order is
+ :math:`[0, 2, 4, ..., 1, 3, 5, ...]`.
+
+ Args:
+ dims (int): dimension.
+
+ Returns:
+ list: order to reshape tensor into an operator.
+ """
+ return list(range(0, 2 * dims, 2)) + list(range(1, 2 * dims, 2))
+
+ @staticmethod
+ def _order_operator_to_tensor(nsystems: int):
+ """Returns the order to reshape an operator to a tensor.
+
+ Given a operator of :math:`2n` systems, the order is
+ :math:`[0, n, 1, n+1, 2, n+2, ...]`.
+
+ Args:
+ nsystems (int): number of systems.
+
+ Returns:
+ list: order to reshape operator into tensor.
+ """
+ return list(
+ sum(zip(list(range(0, nsystems)), list(range(nsystems, nsystems * 2))), ())
+ )
+
+ @classmethod
+ def _operator_to_tensor(cls, operator, partition: List[int]):
+
+ n = len(partition)
+ order = cls._order_operator_to_tensor(n)
+
+ # Check if the `partition` matches the shape of the input matrix
+ if np.prod(tuple(operator.shape)) != np.prod(
+ tuple(dim**2 for dim in partition)
+ ):
+ raise_error(
+ ValueError,
+ "``partition`` does not match the shape of the input matrix. "
+ + f"Cannot reshape matrix of size {operator.shape} to partition {partition}",
+ )
+
+ # Check if `operator` is a pytourch tensor
+ tensor = operator.reshape(list(partition) * 2)
+ if operator.__class__.__name__ == "Tensor":
+ tensor = tensor.permute(order)
+ else:
+ tensor = tensor.transpose(order)
+ return tensor.reshape([dim**2 for dim in partition])
+
+ @classmethod
+ def from_operator(
+ cls,
+ operator,
+ partition: Optional[Union[List[int], Tuple[int]]] = None,
+ system_input: Optional[Union[List[bool], Tuple[bool]]] = None,
+ pure: bool = False,
+ backend=None,
+ ):
+ """Construct a :class:`qibo.quantum_info.QuantumNetwork` object from a ndarray.
+
+ This method converts a Choi operator to the internal representation of
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`.
+ The input array can be a pure state, a Choi operator, a unitary operator, etc.
+
+ Args:
+ arr (ndarray): input numpy array.
+ partition (List[int] or Tuple[int], optional): partition of ``arr``. If ``None``,
+ defaults to the shape of ``arr``. Defaults to ``None``.
+ system_input (List[bool] or Tuple[bool], optional): mask on the input system of the
+ Choi operator. If ``None``, defaults to
+ ``(True,False,True,False...)``, where ``len(system_input)=len(partition)``.
+ Defaults to ``None``.
+ pure (bool, optional): ``True`` when ``arr`` is a "pure" representation (e.g. a pure
+ state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`:
+ quantum network constructed from the input Choi operator.
+ """
+
+ if pure:
+ if partition is None:
+ partition = tuple(operator.shape)
+ tensor = operator
+ else:
+ if tuple(partition) not in [
+ tuple(operator.shape),
+ tuple(int(np.sqrt(dim)) for dim in operator.shape) * 2,
+ ]:
+ raise_error(
+ ValueError,
+ "``partition`` does not match the shape of the input matrix. "
+ + f"Cannot reshape matrix of size {operator.shape} "
+ + f"to partition {partition}",
+ )
+
+ tensor = operator.reshape(partition)
+ else:
+ # check if arr is a valid choi operator
+ len_sys = len(operator.shape)
+ if (len_sys % 2 != 0) or (
+ operator.shape[: len_sys // 2] != operator.shape[len_sys // 2 :]
+ ):
+ raise_error(
+ ValueError,
+ "The opertor must be a square operator where the first half of the shape "
+ + "is the same as the second half of the shape. "
+ + f"However, the shape of the input is {operator.shape}. "
+ + "If the input is pure, set `pure=True`.",
+ )
+
+ if partition is None:
+ partition = operator.shape[: len_sys // 2]
+
+ tensor = cls._operator_to_tensor(operator, partition)
+
+ return cls(
+ tensor,
+ partition=partition,
+ system_input=system_input,
+ pure=pure,
+ backend=backend,
+ )
+
+ def operator(self, full: bool = False, backend=None):
+ """Returns the Choi operator of the quantum network.
+
+ The shape of the returned operator is :math:`(*self.partition, *self.partition)`.
+
+ Args:
+ full (bool, optional): If this is ``False``, and the network is pure, the method
+ will only return the eigenvector (unique when the network is pure).
+ If ``True``, returns the full tensor of the quantum network. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used
+ to return the Choi operator. If ``None``, defaults to the backend defined
+ when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`
+ object. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi operator of the quantum network.
+ """
+ if backend is None: # pragma: no cover
+ backend = self._backend
+
+ if self.is_pure() and not full:
+ return backend.cast(self._tensor, dtype=self._tensor.dtype)
+
+ tensor = self.full(backend) if self.is_pure() else self._tensor
+
+ n = len(self.partition)
+ order = self._order_tensor_to_operator(n)
+
+ operator = self._backend.np.transpose(
+ tensor.reshape(tuple(np.repeat(self.partition, 2))), order
+ )
+
+ return backend.cast(operator, dtype=self._tensor.dtype)
+
+ def matrix(self, backend=None):
+ """Returns the Choi operator of the quantum network in the matrix form.
+ The shape of the returned operator is :math:`(self.dims, self.dims)`.
+
+ Args:
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used
+ to return the Choi operator. If ``None``, defaults to the backend defined
+ when initializing the :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`
+ object. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi operator of the quantum network.
+ """
+ return self.operator(full=True, backend=backend).reshape((self.dims, self.dims))
+
+ def is_pure(self):
+ """Returns bool indicading if the Choi operator of the network is pure."""
+ return self._pure
+
+ def is_hermitian(
+ self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8
+ ):
+ """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is Hermitian.
+
+ Hermicity is calculated as distance between :math:`\\mathcal{J}` and
+ :math:`\\mathcal{J}^{\\dagger}` with respect to a given norm.
+ Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm).
+
+ For specifications on the other possible values of the
+ parameter ``order`` for the ``tensorflow`` backend, please refer to
+ `tensorflow.norm `_.
+ For all other backends, please refer to
+ `numpy.linalg.norm
+ `_.
+
+ Args:
+ order (str or int, optional): order of the norm. Defaults to ``None``.
+ precision_tol (float, optional): threshold :math:`\\epsilon` that defines if
+ Choi operator of the network is :math:`\\epsilon`-close to Hermicity in
+ the norm given by ``order``. Defaults to :math:`10^{-8}`.
+
+ Returns:
+ bool: Hermiticity condition. If the adjoint of the Choi operator is equal to the
+ Choi operator, the method returns ``True``.
+ If the input is pure, the its always Hermitian.
+ """
+ if precision_tol < 0.0:
+ raise_error(
+ ValueError,
+ f"``precision_tol`` must be non-negative float, but it is {precision_tol}",
+ )
+
+ if order is None and self._backend.__class__.__name__ == "TensorflowBackend":
+ order = "euclidean"
+
+ if self.is_pure(): # if the input is pure, it is always hermitian
+ return True
+
+ reshaped = self._backend.cast(
+ self.matrix(),
+ dtype=self._tensor.dtype,
+ )
+ if self._backend.__class__.__name__ == "PyTorchBackend":
+ adjoint = self._backend.np.transpose(reshaped, (1, 0))
+ else:
+ adjoint = self._backend.np.transpose(reshaped)
+
+ mat_diff = self._backend.np.conj(adjoint) - reshaped
+ norm = self._backend.calculate_norm_density_matrix(mat_diff, order=order)
+
+ return float(norm) <= precision_tol
+
+ def is_positive_semidefinite(self, precision_tol: float = 1e-8):
+ """Returns bool indicating if Choi operator :math:`\\mathcal{J}` is positive-semidefinite.
+
+ Args:
+ precision_tol (float, optional): threshold value used to check if eigenvalues of
+ the Choi operator :math:`\\mathcal{J}` are such that
+ :math:`\\textup{eigenvalues}(\\mathcal{J}) >= - \\textup{precision_tol}`.
+ Note that this parameter can be set to negative values.
+ Defaults to :math:`0.0`.
+
+ Returns:
+ bool: Positive-semidefinite condition.
+ """
+ if precision_tol < 0.0:
+ raise_error(
+ ValueError,
+ f"``precision_tol`` must be non-negative float, but it is {precision_tol}",
+ )
+
+ if self.is_pure(): # if the input is pure, it is always positive semidefinite
+ return True
+
+ reshaped = self._backend.cast(
+ self.matrix(),
+ dtype=self._tensor.dtype,
+ )
+
+ if self.is_hermitian():
+ eigenvalues = self._backend.calculate_eigenvalues(reshaped)
+ else:
+ return False
+
+ return all(
+ self._backend.np.real(eigenvalue) >= -precision_tol
+ for eigenvalue in eigenvalues
+ )
+
+ def link_product(self, subscripts: str, second_network):
+ """Link product between two quantum networks.
+
+ The link product is not commutative. Here, we assume that
+ :math:`A.\\textup{link_product}(B)` means "applying :math:`B` to :math:`A`".
+ However, the ``link_product`` is associative, so we override the `@` operation
+ in order to simplify notation.
+
+ Args:
+ subscripts (str, optional): Specifies the subscript for summation using
+ the Einstein summation convention. For more details, please refer to
+ `numpy.einsum
+ `_.
+ second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum
+ network to be applied to the original network.
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting
+ from the link product between two quantum networks.
+ """
+
+ return link_product(subscripts, self, second_network, backend=self._backend)
+
+ def copy(self):
+ """Returns a copy of the :class:`qibo.quantum_info.QuantumNetwork` object."""
+ return self.__class__(
+ self._backend.np.copy(self._tensor),
+ partition=self.partition,
+ system_input=self.system_input,
+ pure=self._pure,
+ backend=self._backend,
+ )
+
+ def conj(self):
+ """Returns the conjugate of the quantum network."""
+ return self.__class__(
+ self._backend.np.conj(self._tensor),
+ partition=self.partition,
+ system_input=self.system_input,
+ pure=self._pure,
+ backend=self._backend,
+ )
+
+ def __add__(self, second_network):
+ """Add two Quantum Networks by adding their Choi operators.
+
+ This operation always returns a non-pure Quantum Network.
+
+ Args:
+ second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum
+ network to be added to the original network.
+
+ Returns:
+ (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum network resulting
+ from the summation of two Choi operators.
+ """
+ if not isinstance(second_network, QuantumNetwork):
+ raise_error(
+ TypeError,
+ "It is not possible to add a object of type ``QuantumNetwork`` "
+ + f"and and object of type ``{type(second_network)}``.",
+ )
+
+ if self.full().shape != second_network.full().shape:
+ raise_error(
+ ValueError,
+ f"The Choi operators must have the same shape, but {self.full().shape} != "
+ + f"{second_network.full().shape}.",
+ )
+
+ if self.system_input != second_network.system_input:
+ raise_error(ValueError, "The networks must have the same input systems.")
+
+ new_first_tensor = self.full()
+ new_second_tensor = second_network.full()
+
+ return QuantumNetwork(
+ new_first_tensor + new_second_tensor,
+ self.partition,
+ self.system_input,
+ pure=False,
+ backend=self._backend,
+ )
+
+ def __mul__(self, number: Union[float, int]):
+ """Returns quantum network with its Choi operator multiplied by a scalar.
+
+ If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum
+ network with its Choi operator multiplied by the square root of ``number``.
+ This is equivalent to multiplying `self.to_full()` by the ``number``.
+ Otherwise, this method will return a full quantum network.
+
+ Args:
+ number (float or int): scalar to multiply the Choi operator of the network with.
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its
+ Choi operator multiplied by ``number``.
+ """
+ if not isinstance(number, (float, int)):
+ raise_error(
+ TypeError,
+ "It is not possible to multiply a ``QuantumNetwork`` by a non-scalar.",
+ )
+
+ if self.is_pure() and number > 0.0:
+ return QuantumNetwork(
+ np.sqrt(number) * self._tensor,
+ partition=self.partition,
+ system_input=self.system_input,
+ pure=True,
+ backend=self._backend,
+ )
+
+ tensor = self.full()
+
+ return QuantumNetwork(
+ number * tensor,
+ partition=self.partition,
+ system_input=self.system_input,
+ pure=False,
+ backend=self._backend,
+ )
+
+ def __rmul__(self, number: Union[float, int]):
+ """"""
+ return self.__mul__(number)
+
+ def __truediv__(self, number: Union[float, int]):
+ """Returns quantum network with its Choi operator divided by a scalar.
+
+ If the quantum network is pure and ``number > 0.0``, the method returns a pure quantum
+ network with its Choi operator divided by the square root of ``number``.
+ This is equivalent to dividing `self.to_full()` by the ``number``.
+ Otherwise, this method will return a full quantum network.
+
+ Args:
+ number (float or int): scalar to divide the Choi operator of the network with.
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network with its
+ Choi operator divided by ``number``.
+ """
+ if not isinstance(number, (float, int)):
+ raise_error(
+ TypeError,
+ "It is not possible to divide a ``QuantumNetwork`` by a non-scalar.",
+ )
+
+ number = np.sqrt(number) if self.is_pure() and number > 0.0 else number
+
+ return QuantumNetwork(
+ self._tensor / number,
+ partition=self.partition,
+ system_input=self.system_input,
+ pure=self.is_pure(),
+ backend=self._backend,
+ )
+
+ def __matmul__(self, second_network):
+ """Defines matrix multiplication between two ``QuantumNetwork`` objects.
+
+ If ``self.partition == second_network.partition in [2, 4]``, this method is overwritten by
+ :meth:`qibo.quantum_info.quantum_networks.QuantumNetwork.link_product`.
+
+ Args:
+ second_network (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`):
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting
+ from the link product operation.
+ """
+ if not isinstance(second_network, QuantumNetwork):
+ raise_error(
+ TypeError,
+ "It is not possible to implement matrix multiplication of a "
+ + "``QuantumNetwork`` by a non-``QuantumNetwork``.",
+ )
+
+ if len(self.partition) == 2: # `self` is a channel
+ if len(second_network.partition) != 2:
+ raise_error(
+ ValueError,
+ f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. "
+ + "Use `link_product` method to specify the subscript.",
+ )
+ if self.partition[1] != second_network.partition[0]:
+ raise_error(
+ ValueError,
+ "partitions of the networks do not match: "
+ + f"{self.partition[1]} != {second_network.partition[0]}.",
+ )
+
+ subscripts = "jk,kl -> jl"
+
+ elif len(self.partition) == 4: # `self` is a super-channel
+ if len(second_network.partition) != 2:
+ raise_error(
+ ValueError,
+ f"`QuantumNetwork {second_network} is assumed to be a channel, but it is not. "
+ + "Use `link_product` method to specify the subscript.",
+ )
+ if self.partition[1] != second_network.partition[0]:
+ raise_error(
+ ValueError,
+ "Systems of the channel do not match the super-channel: "
+ + f"{self.partition[1], self.partition[2]} != "
+ + f"{second_network.partition[0],second_network.partition[1]}.",
+ )
+
+ subscripts = "jklm,kl -> jm"
+ else:
+ raise_error(
+ NotImplementedError,
+ "`partitions` do not match any implemented pattern``. "
+ + "Use `link_product` method to specify the subscript.",
+ )
+
+ return self.link_product(subscripts, second_network) # pylint: disable=E0606
+
+ def __str__(self):
+ """Method to define how to print relevant information of the quantum network."""
+ systems = []
+
+ for i, dim in enumerate(self.partition):
+ if self.system_input[i]:
+ systems.append(f"┍{dim}┑")
+ else:
+ systems.append(f"┕{dim}┙")
+
+ return f"J[{', '.join(systems)}]"
+
+ def _run_checks(self, partition, system_input, pure):
+ """Checks if all inputs are correct in type and value."""
+ if not isinstance(partition, (list, tuple)):
+ raise_error(
+ TypeError,
+ "``partition`` must be type ``tuple`` or ``list``, "
+ + f"but it is type ``{type(partition)}``.",
+ )
+
+ if any(not isinstance(party, int) for party in partition):
+ raise_error(
+ ValueError,
+ "``partition`` must be a ``tuple`` or ``list`` of positive integers, "
+ + "but contains non-integers.",
+ )
+
+ if any(party <= 0 for party in partition):
+ raise_error(
+ ValueError,
+ "``partition`` must be a ``tuple`` or ``list`` of positive integers, "
+ + "but contains non-positive integers.",
+ )
+
+ if system_input is not None and len(system_input) != len(partition):
+ raise_error(
+ ValueError,
+ "``len(system_input)`` must be the same as ``len(partition)``, "
+ + f"but {len(system_input)} != {len(partition)}.",
+ )
+
+ if not isinstance(pure, bool):
+ raise_error(
+ TypeError,
+ f"``pure`` must be type ``bool``, but it is type ``{type(pure)}``.",
+ )
+
+ @staticmethod
+ def _check_system_input(system_input, partition) -> Tuple[bool]:
+ """
+ If `system_input` not defined, assume the network follows the order of a quantum Comb.
+ """
+
+ if system_input is None:
+ system_input = [
+ False,
+ ] * len(partition)
+ for k in range(len(partition) // 2):
+ system_input[k * 2] = True
+ return tuple(system_input)
+
+ def _set_parameters(self):
+ """Standarize the parameters."""
+ self._backend = _check_backend(self._backend)
+
+ self.partition = tuple(self.partition)
+
+ self.system_input = self._check_system_input(self.system_input, self.partition)
+
+ self._einsum = self._backend.np.einsum
+ self._tensordot = self._backend.np.tensordot
+ self._tensor = self._backend.cast(self._tensor, dtype=self._tensor.dtype)
+
+ if self._pure:
+ if np.prod(tuple(self._tensor.shape)) != np.prod(tuple(self.partition)):
+ raise_error(
+ ValueError,
+ "``partition`` does not match the shape of the input matrix. "
+ + f"Cannot reshape matrix of size {self._tensor.shape} "
+ + f"to partition {self.partition}.",
+ )
+ self._tensor = self._backend.np.reshape(self._tensor, self.partition)
+ else:
+ if np.prod(tuple(self._tensor.shape)) != np.prod(
+ tuple(dim**2 for dim in self.partition)
+ ):
+ raise_error(
+ ValueError,
+ "``partition`` does not match the shape of the input matrix. "
+ + f"Cannot reshape matrix of size {self._tensor.shape} "
+ + f"to partition {self.partition}.",
+ )
+ matrix_partition = [dim**2 for dim in self.partition]
+ self._tensor = self._backend.np.reshape(self._tensor, matrix_partition)
+
+ def full(self, update: bool = False, backend=None):
+ """Convert the internal representation to the full tensor of the network.
+
+ Args:
+ update (bool, optional): If ``True``, updates the internal representation of the
+ network to the full tensor. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: full reprentation of the quantum network.
+ """
+ if backend is None: # pragma: no cover
+ backend = self._backend
+ tensor = self._backend.np.copy(self._tensor)
+ tensor = backend.cast(tensor, dtype=self._tensor.dtype)
+ conj = backend.np.conj
+
+ if self.is_pure():
+ # Reshapes input matrix based on purity.
+ tensor.reshape(self.dims)
+ if self._backend.__class__.__name__ == "PyTorchBackend":
+ tensor = self._tensordot(tensor, conj(tensor), dims=0)
+ else:
+ tensor = self._tensordot(tensor, conj(tensor), axes=0)
+ tensor = self._operator_to_tensor(tensor, self.partition)
+
+ if update:
+ self._tensor = tensor
+ self._pure = False
+
+ return tensor
+
+
+class QuantumComb(QuantumNetwork):
+ """Stores a Quantum comb, which is a network in which the systems follows a sequential order.
+
+ It is also called the *non-Markovian quantum process* in many literatures.
+ A quantum comb is a quantum network of the form :math:`J[┍i1┑,┕o1┙,┍i2┑,┕o2┙, ...]`,
+ where the process first take an input state from system :math:`i1`,
+ then output a state to system :math:`o1`, and so on.
+ This is a non-Markovian process as the output of the system :math:`o2` may depend on
+ what happened in systems :math:`i1`, and :math:`o1`.
+
+ A quantum channel is a special case of quantum comb, where there are only one input
+ system and one output system.
+
+ Args:
+ tensor (ndarray): the tensor representations of the quantum Comb.
+ partition (List[int] or Tuple[int]): partition of ``matrix``.
+ system_input (List[bool] or Tuple[bool], optional): mask on the input system of the
+ Choi operator. If ``None``, defaults to
+ ``(True,False,True,False,...)``, where ``len(system_input)=len(partition)``.
+ Defaults to ``None``.
+ pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure
+ state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+
+ def __init__(
+ self,
+ tensor,
+ partition: Optional[Union[List[int], Tuple[int]]] = None,
+ system_input: Optional[Union[List[bool], Tuple[bool]]] = None,
+ pure: bool = False,
+ backend=None,
+ ):
+ if partition is None:
+ if pure:
+ partition = tensor.shape
+ else:
+ partition = tuple(int(np.sqrt(d)) for d in tensor.shape)
+ if len(partition) % 2 != 0:
+ raise_error(
+ ValueError,
+ "A quantum comb should only contain equal number of input and output systems. "
+ + "For general quantum networks, one should use the ``QuantumNetwork`` class.",
+ )
+ if system_input is not None:
+ warning("system_input is ignored for QuantumComb")
+
+ super().__init__(
+ tensor, partition, [True, False] * (len(partition) // 2), pure, backend
+ )
+
+ def is_causal(
+ self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8
+ ):
+ """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` satisfies causal order
+
+ Causality is calculated based on a recursive constrains.
+ This method reduce a n-comb to a (n-1)-comb at each step,
+ and checks if the reduced comb is independent on the last output system.
+
+ Args:
+ order (str or int, optional): order of the norm. Defaults to ``None``.
+ precision_tol (float, optional): threshold :math:`\\epsilon` that defines
+ if Choi operator of the network is :math:`\\epsilon`-close to causality
+ in the norm given by ``order``. Defaults to :math:`10^{-8}`.
+
+ Returns:
+ bool: Causal order condition.
+ """
+ if precision_tol < 0.0:
+ raise_error(
+ ValueError,
+ f"``precision_tol`` must be non-negative float, but it is {precision_tol}",
+ )
+
+ if order is None and self._backend.__class__.__name__ == "TensorflowBackend":
+ order = "euclidean"
+
+ backend = self._backend
+
+ dim_out = self.partition[-1]
+ dim_in = self.partition[-2]
+
+ trace_out = TraceOperation(dim_out, backend=backend).full()
+ trace_in = TraceOperation(dim_in, backend=backend).full()
+
+ if self._backend.__class__.__name__ == "PyTorchBackend":
+ reduced = self._tensordot(self.full(), trace_out, dims=([-1], [0]))
+ sub_comb = self._tensordot(reduced, trace_in, dims=([-1], [0]))
+ expected = self._tensordot(sub_comb, trace_in / dim_in, dims=0)
+ else:
+ reduced = self._tensordot(self.full(), trace_out, axes=(-1, 0))
+ sub_comb = self._tensordot(reduced, trace_in, axes=(-1, 0))
+ expected = self._tensordot(sub_comb, trace_in / dim_in, axes=0)
+
+ norm = self._backend.calculate_norm(reduced - expected, order=order)
+
+ if float(norm) > precision_tol:
+ return False
+
+ if len(self.partition) == 2:
+ return True
+
+ return QuantumComb(
+ sub_comb, self.partition[:-2], pure=False, backend=self._backend
+ ).is_causal(order, precision_tol)
+
+ @classmethod
+ def from_operator(
+ cls, operator, partition=None, inverse=False, pure=False, backend=None
+ ): # pylint: disable=W0237
+ comb = super().from_operator(operator, partition, None, pure, backend)
+ if inverse:
+ # Convert mathmetical convention of Choi operator to physical convention
+ comb.partition = comb.partition[::-1]
+ comb._tensor = comb._tensor.T # pylint: disable=W0212
+ return comb
+
+
+class QuantumChannel(QuantumComb):
+ """Stores a Quantum channel, which is a special case of quantum comb.
+
+ A quantum channel is a quantum comb with only one input and one output.
+ This class includes all quantum channels, unitary operators, and quantum states.
+
+ To construct a `QuantumChannel` object, one can use the `QuantumNetwork.from_nparray` method.
+ **Note**: if one try to construct a quantum network from a unitary operator or Choi operator,
+ the first system will be the output.
+ However, here we assume the first system is the input system.
+ It is important to specify `inverse=True` when constructing by `QuantumNetwork.from_nparray`.
+
+ Args:
+ tensor (ndarray): the tensor representations of the quantum comb.
+ partition (List[int] or Tuple[int], optional): partition of ``matrix``.
+ If not provided and `system_input` is `None`, assume the input is a quantum state,
+ whose input is a trivial system. If `system_input` is set to `True`,
+ assume the input is an observable, whose output is a trivial system.
+ system_input (List[bool] or Tuple[bool], optional): mask on the input system of the
+ Choi operator. If ``None`` the default is ``(True,False)``.
+ Defaults to ``None``.
+ pure (bool, optional): ``True`` when ``tensor`` is a "pure" representation (e.g. a pure
+ state, a unitary operator, etc.), ``False`` otherwise. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+
+ def __init__(
+ self,
+ tensor,
+ partition: Optional[Union[List[int], Tuple[int]]] = None,
+ system_input: Optional[Union[List[bool], Tuple[bool]]] = None,
+ pure: bool = False,
+ backend=None,
+ ):
+ if isinstance(partition, int):
+ partition = (partition,)
+
+ if partition is not None:
+ if len(partition) > 2:
+ raise_error(
+ ValueError,
+ "A quantum channel should only contain one input system and one output system."
+ + "For general quantum networks, one should use the ``QuantumNetwork`` class.",
+ )
+ if len(partition) == 1:
+ if system_input is None: # Assume the input is a quantum state
+ partition = (1, partition[0])
+ else:
+ if isinstance(system_input, bool):
+ system_input = (system_input,)
+
+ partition = (
+ (partition[0], 1) if system_input[0] else (1, partition[0])
+ )
+
+ super().__init__(tensor, partition, pure=pure, backend=backend)
+
+ def is_unital(
+ self, order: Optional[Union[int, str]] = None, precision_tol: float = 1e-8
+ ):
+ """Returns bool indicating if the Choi operator :math:`\\mathcal{J}` is unital.
+
+ A map is unital if it preserves the identity operator.
+ Unitality is calculated as distance between the partial trace of :math:`\\mathcal{J}`
+ and the Identity operator :math:`I`, with respect to a given norm.
+ Default is the ``Hilbert-Schmidt`` norm (also known as ``Frobenius`` norm).
+
+ For specifications on the other possible values of the
+ parameter ``order`` for the ``tensorflow`` backend, please refer to
+ `tensorflow.norm `_.
+ For all other backends, please refer to
+ `numpy.linalg.norm
+ `_.
+
+ Args:
+ order (str or int, optional): order of the norm. Defaults to ``None``.
+ precision_tol (float, optional): threshold :math:`\\epsilon` that defines
+ if Choi operator of the network is :math:`\\epsilon`-close to unitality
+ in the norm given by ``order``. Defaults to :math:`10^{-8}`.
+
+ Returns:
+ bool: Unitality condition.
+ """
+ if precision_tol < 0.0:
+ raise_error(
+ ValueError,
+ f"``precision_tol`` must be non-negative float, but it is {precision_tol}",
+ )
+
+ if order is None and self._backend.__class__.__name__ == "TensorflowBackend":
+ order = "euclidean"
+
+ backend = self._backend
+
+ dim_out = self.partition[1]
+ dim_in = self.partition[0]
+
+ trace_out = TraceOperation(dim_out, backend=backend).full()
+ trace_in = TraceOperation(dim_in, backend=backend).full()
+
+ if self._backend.__class__.__name__ == "PyTorchBackend":
+ reduced = self._tensordot(self.full(), trace_in, dims=([0], [0]))
+ sub_comb = self._tensordot(
+ reduced,
+ trace_out,
+ dims=([0], [0]),
+ )
+ expected = self._tensordot(trace_out / dim_out, sub_comb, dims=0)
+ else:
+ reduced = self._tensordot(self.full(), trace_in, axes=(0, 0))
+ sub_comb = self._tensordot(reduced, trace_out, axes=(0, 0))
+ expected = self._tensordot(trace_out / dim_out, sub_comb, axes=0)
+
+ norm = self._backend.calculate_norm((reduced - expected), order=order)
+ if float(norm) > precision_tol:
+ return False
+
+ if len(self.partition) == 2:
+ return True
+
+ # Unital is defined for quantum channels only.
+ # But we can extend it to quantum combs as follows:
+ return QuantumChannel( # pragma: no cover
+ sub_comb, self.partition[2:], pure=False, backend=self._backend
+ ).is_unital(order, precision_tol)
+
+ def is_channel(
+ self,
+ order: Optional[Union[int, str]] = None,
+ precision_tol_causal: float = 1e-8,
+ precision_tol_psd: float = 1e-8,
+ ):
+ """Returns bool indicating if Choi operator :math:`\\mathcal{E}` is a channel.
+
+ Args:
+ order (int or str, optional): order of the norm used to calculate causality.
+ Defaults to ``None``.
+ precision_tol_causal (float, optional): threshold :math:`\\epsilon` that defines if
+ Choi operator of the network is :math:`\\epsilon`-close to causality in the norm
+ given by ``order``. Defaults to :math:`10^{-8}`.
+ precision_tol_psd (float, optional): threshold value used to check if eigenvalues of
+ the Choi operator :math:`\\mathcal{E}` are such that
+ :math:`\\textup{eigenvalues}(\\mathcal{E}) >= \\textup{precision_tol_psd}`.
+ Note that this parameter can be set to negative values.
+ Defaults to :math:`0.0`.
+
+ Returns:
+ bool: Channel condition.
+ """
+ return self.is_causal(
+ order, precision_tol_causal
+ ) and self.is_positive_semidefinite(precision_tol_psd)
+
+ def apply(self, state):
+ """Apply the Choi operator :math:`\\mathcal{E}` to ``state`` :math:`\\varrho`.
+
+ It is assumed that ``state`` :math:`\\varrho` is a density matrix.
+
+ Args:
+ state (ndarray): density matrix of a ``state``.
+
+ Returns:
+ ndarray: Resulting state :math:`\\mathcal{E}(\\varrho)`.
+ """
+ operator = self.copy().operator()
+ conj = self._backend.np.conj
+
+ if self.is_pure():
+ return self._einsum("ij,lk,il", operator, conj(operator), state)
+
+ return self._einsum("ijkl, jl", operator, state)
+
+
+def link_product(
+ subscripts: str,
+ *operands: QuantumNetwork,
+ backend=None,
+ surpress_warning=False,
+):
+ """Link product between two quantum networks.
+
+ The link product is not commutative. Here, we assume that
+ :math:`A.\\textup{link_product}(B)` means "applying :math:`B` to :math:`A`".
+ However, the ``link_product`` is associative, so we override the `@` operation
+ in order to simplify notation.
+
+ Args:
+ subscripts (str, optional): Specifies the subscript for summation using
+ the Einstein summation convention. For more details, please refer to
+ `numpy.einsum `_.
+ operands (:class:`qibo.quantum_info.quantum_networks.QuantumNetwork`): Quantum
+ networks to be contracted.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ surpress_warning (bool, optional): If ``True``, surpresses the warning
+ regarding if the same index connects two input or two output
+ systems. Defaults to ``False``.
+
+ Returns:
+ :class:`qibo.quantum_info.quantum_networks.QuantumNetwork`: Quantum network resulting
+ from the link product between two quantum networks.
+ """
+
+ if not isinstance(subscripts, str):
+ raise_error(
+ TypeError,
+ f"subscripts must be type str, but it is type {type(subscripts)}.",
+ )
+
+ for i, operand in enumerate(operands):
+ if not isinstance(operand, QuantumNetwork):
+ raise_error(TypeError, f"The {i}-th operator is not a ``QuantumNetwork``.")
+
+ if backend is None: # pragma: no cover
+ backend = operands[0]._backend # pylint: disable=W0212
+
+ tensors = [
+ (
+ backend.to_numpy(operand.full())
+ if operand.is_pure()
+ else backend.to_numpy(operand._tensor) # pylint: disable=W0212
+ )
+ for operand in operands
+ ]
+
+ # keep track of the `partition` and `system_input` of the network
+ _, contracrtion_list = np.einsum_path(
+ subscripts, *tensors, optimize=False, einsum_call=True
+ )
+
+ inds, idx_rm, einsum_str, _, _ = contracrtion_list[0]
+ input_str, results_index = einsum_str.split("->")
+ inputs = input_str.split(",")
+
+ # Warning if the same index connects two input or two output systems
+ if not surpress_warning:
+ for ind in idx_rm:
+ found = 0
+ for i, script in enumerate(inputs):
+ index = script.find(ind)
+ if index < 0:
+ continue
+ found += 1
+ if found > 1 and is_input == operands[inds[i]].system_input[index]:
+ warning(
+ f"Index {ind} connects two {'input' if is_input else 'output'} systems."
+ )
+ is_input = operands[inds[i]].system_input[index]
+ if found > 2:
+ warning(
+ f"Index {ind} appears multiple times in the input subscripts {input_str}."
+ )
+
+ # set correct order of the `partition` and `system_input`
+ partition = []
+ system_input = []
+ for ind in results_index:
+ for i, script in enumerate(inputs):
+ index = script.find(ind)
+ if index < 0:
+ continue
+
+ partition.append(operands[inds[i]].partition[index])
+ system_input.append(operands[inds[i]].system_input[index])
+
+ new_tensor = np.einsum(subscripts, *tensors)
+
+ return QuantumNetwork(new_tensor, partition, system_input, backend=backend)
+
+
+class IdentityChannel(QuantumChannel):
+ """The Identity channel with the given dimension.
+
+ Args:
+ dim (int): Dimension of the Identity operator.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+
+ def __init__(self, dim: int, backend=None):
+
+ identity = np.eye(dim, dtype=complex)
+ identity = backend.cast(identity, dtype=identity.dtype)
+ super().__init__(identity, [dim, dim], pure=True, backend=backend)
+
+
+class TraceOperation(QuantumNetwork):
+ """The Trace operator with the given dimension.
+
+ Args:
+ dim (int): Dimension of the Trace operator.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): Backend to be used in
+ calculations. If ``None``, defaults to :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+
+ def __init__(self, dim: int, backend=None):
+
+ identity = np.eye(dim, dtype=complex)
+ identity = backend.cast(identity, dtype=identity.dtype)
+ super().__init__(identity, [dim], [True], pure=False, backend=backend)
diff --git a/src/qibo/quantum_info/random_ensembles.py b/src/qibo/quantum_info/random_ensembles.py
new file mode 100644
index 000000000..59efb49b7
--- /dev/null
+++ b/src/qibo/quantum_info/random_ensembles.py
@@ -0,0 +1,1235 @@
+"""Module with functions that create random quantum and classical objects."""
+
+import math
+import warnings
+from functools import cache
+from typing import Optional, Union
+
+import numpy as np
+from scipy.stats import rv_continuous
+
+from qibo import Circuit, gates, matrices
+from qibo.backends import NumpyBackend, _check_backend_and_local_state
+from qibo.config import MAX_ITERATIONS, PRECISION_TOL, raise_error
+from qibo.quantum_info.basis import comp_basis_to_pauli
+from qibo.quantum_info.superoperator_transformations import (
+ choi_to_chi,
+ choi_to_kraus,
+ choi_to_liouville,
+ choi_to_pauli,
+ choi_to_stinespring,
+ vectorization,
+)
+
+
+class _ProbabilityDistributionGaussianLoader(rv_continuous):
+ """Probability density function for sampling phases of
+ the RBS gates as a function of circuit depth."""
+
+ def _pdf(self, theta: float, depth: int):
+ amplitude = 2 * math.gamma(2 ** (depth - 1)) / math.gamma(2 ** (depth - 2)) ** 2
+
+ probability = abs(math.sin(theta) * math.cos(theta)) ** (2 ** (depth - 1) - 1)
+
+ return amplitude * probability / 4
+
+
+class _probability_distribution_sin(rv_continuous): # pragma: no cover
+ def _pdf(self, theta: float):
+ return 0.5 * np.sin(theta)
+
+ def _cdf(self, theta: float):
+ return np.sin(theta / 2) ** 2
+
+ def _ppf(self, theta: float):
+ return 2 * np.arcsin(np.sqrt(theta))
+
+
+def uniform_sampling_U3(ngates: int, seed=None, backend=None):
+ """Samples parameters for Haar-random :class:`qibo.gates.U3`.
+
+ Args:
+ ngates (int): Total number of :math:`U_{3}` gates to be sampled.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: array of shape (``ngates``, :math:`3`).
+ """
+ if not isinstance(ngates, int):
+ raise_error(
+ TypeError, f"ngates must be type int, but it is type {type(ngates)}."
+ )
+ elif ngates <= 0:
+ raise_error(ValueError, f"ngates must be non-negative, but it is {ngates}.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ sampler = _probability_distribution_sin(a=0, b=np.pi, seed=local_state)
+ phases = local_state.random((ngates, 3))
+ phases[:, 0] = sampler.rvs(size=len(phases[:, 0]))
+ phases[:, 1] = phases[:, 1] * 2 * np.pi
+ phases[:, 2] = phases[:, 2] * 2 * np.pi
+
+ phases = backend.cast(phases, dtype=phases.dtype)
+
+ return phases
+
+
+def random_gaussian_matrix(
+ dims: int,
+ rank: Optional[int] = None,
+ mean: float = 0,
+ stddev: float = 1,
+ seed=None,
+ backend=None,
+):
+ """Generates a random Gaussian Matrix.
+
+ Gaussian matrices are matrices where each entry is
+ sampled from a Gaussian probability distribution
+
+ .. math::"haar",
+ p(x) = \\frac{1}{\\sqrt{2 \\, \\pi} \\, \\sigma} \\,
+ \\exp{\\left(-\\frac{(x - \\mu)^{2}}{2\\,\\sigma^{2}}\\right)}
+
+ with mean :math:`\\mu` and standard deviation :math:`\\sigma`.
+
+ Args:
+ dims (int): dimension of the matrix.
+ rank (int, optional): rank of the matrix. If ``None``, then
+ ``rank == dims``. Default: ``None``.
+ mean (float, optional): mean of the Gaussian distribution. Defaults to 0.
+ stddev (float, optional): standard deviation of the Gaussian distribution.
+ Defaults to ``1``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random
+ numbers or a fixed seed to initialize a generator. If ``None``, initializes
+ a generator with a random seed. Default: ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Random Gaussian matrix with dimensions ``(dims, rank)``.
+ """
+
+ if dims <= 0:
+ raise_error(ValueError, "dims must be type int and positive.")
+
+ if rank is None:
+ rank = dims
+ else:
+ if rank > dims:
+ raise_error(
+ ValueError, f"rank ({rank}) cannot be greater than dims ({dims})."
+ )
+ elif rank <= 0:
+ raise_error(ValueError, f"rank ({rank}) must be an int between 1 and dims.")
+
+ if stddev is not None and stddev <= 0.0:
+ raise_error(ValueError, "stddev must be a positive float.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ dims = (dims, rank)
+
+ matrix = 1.0j * local_state.normal(loc=mean, scale=stddev, size=dims)
+ matrix += local_state.normal(loc=mean, scale=stddev, size=dims)
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+
+ return matrix
+
+
+def random_hermitian(
+ dims: int,
+ semidefinite: bool = False,
+ normalize: bool = False,
+ seed=None,
+ backend=None,
+):
+ """Generates a random Hermitian matrix :math:`H`, i.e.
+ a random matrix such that :math:`H = H^{\\dagger}.`
+
+ Args:
+ dims (int): dimension of the matrix.
+ semidefinite (bool, optional): if ``True``, returns a Hermitian matrix that
+ is also positive semidefinite. Defaults to ``False``.
+ normalize (bool, optional): if ``True`` and ``semidefinite=False``, returns
+ a Hermitian matrix with eigenvalues in the interval
+ :math:`[-1, \\, 1]`. If ``True`` and ``semidefinite=True``,
+ interval is :math:`[0, \\, 1]`. Defaults to ``False``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Hermitian matrix :math:`H` with dimensions ``(dims, dims)``.
+ """
+
+ if dims <= 0:
+ raise_error(ValueError, f"dims ({dims}) must be type int and positive.")
+
+ if not isinstance(semidefinite, bool) or not isinstance(normalize, bool):
+ raise_error(TypeError, "semidefinite and normalize must be type bool.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ matrix = random_gaussian_matrix(dims, dims, seed=local_state, backend=backend)
+
+ if semidefinite:
+ matrix = backend.np.matmul(backend.np.conj(matrix).T, matrix)
+ else:
+ matrix = (matrix + backend.np.conj(matrix).T) / 2
+
+ if normalize:
+ matrix = matrix / np.linalg.norm(backend.to_numpy(matrix))
+
+ return matrix
+
+
+def random_unitary(dims: int, measure: Optional[str] = None, seed=None, backend=None):
+ """Returns a random Unitary operator :math:`U`, i.e.
+ a random operator such that :math:`U^{-1} = U^{\\dagger}`.
+
+ Args:
+ dims (int): dimension of the matrix.
+ measure (str, optional): probability measure in which to sample the unitary
+ from. If ``None``, functions returns :math:`\\exp{(-i \\, H)}`, where
+ :math:`H` is a Hermitian operator. If ``"haar"``, returns an Unitary
+ matrix sampled from the Haar measure. Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Unitary matrix :math:`U` with dimensions ``(dims, dims)``.
+ """
+
+ if dims <= 0:
+ raise_error(ValueError, "dims must be type int and positive.")
+
+ if measure is not None:
+ if not isinstance(measure, str):
+ raise_error(
+ TypeError, f"measure must be type str but it is type {type(measure)}."
+ )
+ if measure != "haar":
+ raise_error(ValueError, f"measure {measure} not implemented.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if measure == "haar":
+ unitary = random_gaussian_matrix(dims, dims, seed=local_state, backend=backend)
+ # Tensorflow experi
+ Q, R = backend.np.linalg.qr(unitary)
+ D = backend.np.diag(R)
+ D = D / backend.np.abs(D)
+ R = backend.np.diag(D)
+ unitary = backend.np.matmul(Q, R)
+ elif measure is None:
+ from scipy.linalg import expm
+
+ H = random_hermitian(dims, seed=seed, backend=NumpyBackend())
+ unitary = expm(-1.0j * H / 2)
+ unitary = backend.cast(unitary, dtype=unitary.dtype)
+
+ return unitary
+
+
+def random_quantum_channel(
+ dims: int,
+ representation: str = "liouville",
+ measure: Optional[str] = None,
+ rank: Optional[int] = None,
+ order: str = "row",
+ normalize: bool = False,
+ precision_tol: Optional[float] = None,
+ validate_cp: bool = True,
+ nqubits: Optional[int] = None,
+ initial_state_env=None,
+ seed=None,
+ backend=None,
+):
+ """Creates a random superoperator from an unitary operator in one of the
+ supported superoperator representations.
+
+ Args:
+ dims (int): dimension of the :math:`n`-qubit operator, i.e. :math:`\\text{dims}=2^{n}`.
+ representation (str, optional): If ``"chi"``, returns a random channel in the
+ Chi representation. If ``"choi"``, returns channel in Choi representation.
+ If ``"kraus"``, returns Kraus representation of channel. If ``"liouville"``,
+ returns Liouville representation. If ``"pauli"``, returns Pauli-Liouville
+ representation. If "pauli-" or "chi-", (e.g. "pauli-IZXY"),
+ returns it in the Pauli basis with the corresponding order of single-qubit Pauli elements
+ (see :func:`qibo.quantum_info.pauli_basis`). If ``"stinespring"``,
+ returns random channel in the Stinespring representation. Defaults to ``"liouville"``.
+ measure (str, optional): probability measure in which to sample the unitary
+ from. If ``None``, functions returns :math:`\\exp{(-i \\, H)}`, where
+ :math:`H` is a Hermitian operator. If ``"haar"``, returns an Unitary
+ matrix sampled from the Haar measure. If ``"bcsz"``, it samples an unitary
+ from the BCSZ distribution with Kraus ``rank``. Defaults to ``None``.
+ rank (int, optional): used when ``measure=="bcsz"``. Rank of the matrix.
+ If ``None``, then ``rank==dims``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, vectorization is performed row-wise.
+ If ``"column"``, vectorization is performed column-wise. If ``"system"``,
+ a block-vectorization is performed. Defaults to ``"row"``.
+ normalize (bool, optional): used when ``representation="chi"`` or
+ ``representation="pauli"``. If ``True`` assumes the normalized Pauli basis.
+ If ``False``, it assumes unnormalized Pauli basis. Defaults to ``False``.
+ precision_tol (float, optional): if ``representation="kraus"``, it is the
+ precision tolerance for eigenvalues found in the spectral decomposition
+ problem. Any eigenvalue :math:`\\lambda <` ``precision_tol`` is set
+ to 0 (zero). If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ validate_cp (bool, optional): used when ``representation="stinespring"``.
+ If ``True``, checks if the Choi representation of superoperator
+ used as intermediate step is a completely positive map.
+ If ``False``, it assumes that it is completely positive (and Hermitian).
+ Defaults to ``True``.
+ nqubits (int, optional): used when ``representation="stinespring"``.
+ Total number of qubits in the system that is interacting with
+ the environment. Must be equal or greater than the number of
+ qubits that Kraus representation of the system superoperator acts on.
+ If ``None``, defaults to the number of qubits in the Kraus operators.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): used when ``representation="stinespring"``.
+ Statevector representing the initial state of the enviroment.
+ If ``None``, it assumes the environment in its ground state.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Superoperator representation of a random unitary gate.
+ """
+ if not isinstance(representation, str):
+ raise_error(
+ TypeError,
+ f"representation must be type str, but it is type {type(representation)}",
+ )
+
+ if representation not in [
+ "chi",
+ "choi",
+ "kraus",
+ "liouville",
+ "pauli",
+ "stinespring",
+ ]:
+ if (
+ ("chi-" not in representation and "pauli-" not in representation)
+ or len(representation.split("-")) != 2
+ or set(representation.split("-")[1]) != {"I", "X", "Y", "Z"}
+ ):
+ raise_error(ValueError, f"representation {representation} not implemented.")
+
+ if measure == "bcsz" and order not in ["row", "column"]:
+ raise_error(
+ NotImplementedError, f"order {order} not implemented for measure {measure}."
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if measure == "bcsz":
+ super_op = _super_op_from_bcsz_measure(
+ dims=dims, rank=rank, order=order, seed=local_state, backend=backend
+ )
+ else:
+ super_op = random_unitary(dims, measure, local_state, backend)
+ super_op = vectorization(super_op, order=order, backend=backend)
+ super_op = backend.np.outer(super_op, backend.np.conj(super_op))
+
+ if "chi" in representation:
+ pauli_order = "IXYZ"
+ if "-" in representation:
+ pauli_order = representation.split("-")[1]
+ super_op = choi_to_chi(
+ super_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ elif representation == "kraus":
+ super_op = choi_to_kraus(
+ super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=False,
+ backend=backend,
+ )
+ elif representation == "liouville":
+ super_op = choi_to_liouville(super_op, order=order, backend=backend)
+ elif "pauli" in representation:
+ pauli_order = "IXYZ"
+ if "-" in representation:
+ pauli_order = representation.split("-")[1]
+ super_op = choi_to_pauli(
+ super_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ elif representation == "stinespring":
+ super_op = choi_to_stinespring(
+ super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+
+ return super_op
+
+
+def random_statevector(dims: int, seed=None, backend=None):
+ """Creates a random statevector :math:`\\ket{\\psi}`.
+
+ .. math::
+ \\ket{\\psi} = \\sum_{k = 0}^{d - 1} \\, \\sqrt{p_{k}} \\,
+ e^{i \\phi_{k}} \\, \\ket{k} \\, ,
+
+ where :math:`d` is ``dims``, and :math:`p_{k}` and :math:`\\phi_{k}` are, respectively,
+ the probability and phase corresponding to the computational basis state :math:`\\ket{k}`.
+
+ Args:
+ dims (int): dimension of the matrix.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Random statevector :math:`\\ket{\\psi}`.
+ """
+
+ if dims <= 0:
+ raise_error(ValueError, "dim must be of type int and >= 1")
+
+ if (
+ seed is not None
+ and not isinstance(seed, int)
+ and not isinstance(seed, np.random.Generator)
+ ):
+ raise_error(
+ TypeError, "seed must be either type int or numpy.random.Generator."
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ state = backend.cast(local_state.standard_normal(dims).astype(complex))
+ state = state + 1.0j * backend.cast(local_state.standard_normal(dims))
+ state = state / backend.np.linalg.norm(state)
+
+ return state
+
+
+def random_density_matrix(
+ dims: int,
+ rank: Optional[int] = None,
+ pure: bool = False,
+ metric: str = "hilbert-schmidt",
+ basis: Optional[str] = None,
+ normalize: bool = False,
+ order: str = "row",
+ seed=None,
+ backend=None,
+):
+ """Creates a random density matrix :math:`\\rho`. If ``pure=True``,
+
+ .. math::
+ \\rho = \\ketbra{\\psi}{\\psi} \\, ,
+
+ where :math:`\\ket{\\psi}` is a :func:`qibo.quantum_info.random_statevector`.
+ If ``pure=False``, then
+
+ .. math::
+ \\rho = \\sum_{k} \\, p_{k} \\, \\ketbra{\\psi_{k}}{\\psi_{k}} \\, .
+
+ is a mixed state.
+
+ Args:
+ dims (int): dimension of the matrix.
+ rank (int, optional): rank of the matrix. If ``None``, then ``rank == dims``.
+ Defaults to ``None``.
+ pure (bool, optional): if ``True``, returns a pure state. Defaults to ``False``.
+ metric (str, optional): metric to sample the density matrix from. Options:
+ ``"hilbert-schmidt"``, ``"ginibre"``, and ``"bures"``.
+ Note that, by definition, ``rank`` defaults to ``None``
+ when ``metric=="hilbert-schmidt"``. Defaults to ``"hilbert-schmidt"``.
+ basis (str, optional): if ``None``, returns random density matrix in the
+ computational basis. If ``"pauli-"``, (e.g. ``"pauli-IZXY"``),
+ returns it in the Pauli basis with the corresponding order of single-qubit
+ Pauli elements (see :func:`qibo.quantum_info.pauli_basis`).
+ Defaults to ``None``.
+ normalize(bool, optional): used when ``basis="pauli-"``. If ``True``
+ returns random density matrix in the normalized Pauli basis. If ``False``,
+ returns state in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): used when ``basis="pauli-"``. If ``"row"``,
+ vectorization of Pauli basis is performed row-wise. If ``"column"``,
+ vectorization is performed column-wise. If ``"system"``, system-wise
+ vectorization is performed. Default is ``"row"``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: Random density matrix :math:`\\rho`.
+ """
+
+ if dims <= 0:
+ raise_error(ValueError, "dims must be type int and positive.")
+
+ if rank is not None and rank > dims:
+ raise_error(ValueError, f"rank ({rank}) cannot be greater than dims ({dims}).")
+
+ if rank is not None and rank <= 0:
+ raise_error(ValueError, f"rank ({rank}) must be an int between 1 and dims.")
+
+ if rank is not None and not isinstance(rank, int):
+ raise_error(TypeError, f"rank must be type int, but it is type {type(rank)}.")
+
+ if not isinstance(pure, bool):
+ raise_error(TypeError, f"pure must be type bool, but it is type {type(pure)}.")
+
+ if not isinstance(metric, str):
+ raise_error(
+ TypeError, f"metric must be type str, but it is type {type(metric)}."
+ )
+ if metric not in ["hilbert-schmidt", "ginibre", "bures"]:
+ raise_error(ValueError, f"metric {metric} not implemented.")
+
+ if basis is not None and not isinstance(basis, str):
+ raise_error(TypeError, f"basis must be type str, but it is type {type(basis)}.")
+ elif basis is not None and basis not in ["pauli"]:
+ if (
+ "pauli-" not in basis
+ or len(basis.split("-")) != 2
+ or set(basis.split("-")[1]) != {"I", "X", "Y", "Z"}
+ ):
+ raise_error(ValueError, f"basis {basis} nor recognized.")
+
+ if not isinstance(normalize, bool):
+ raise_error(
+ TypeError, f"normalize must be type bool, but it is type {type(normalize)}."
+ )
+ elif normalize is True and basis is None:
+ raise_error(ValueError, "normalize cannot be True when basis=None.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if metric == "hilbert-schmidt":
+ rank = None
+
+ if pure:
+ state = random_statevector(dims, seed=local_state, backend=backend)
+ state = backend.np.outer(state, backend.np.conj(state).T)
+ else:
+ if metric in ["hilbert-schmidt", "ginibre"]:
+ state = random_gaussian_matrix(
+ dims, rank, mean=0, stddev=1, seed=local_state, backend=backend
+ )
+ state = backend.np.matmul(
+ state, backend.np.transpose(backend.np.conj(state), (1, 0))
+ )
+ state = state / backend.np.trace(state)
+ else:
+ nqubits = int(np.log2(dims))
+ state = backend.identity_density_matrix(nqubits, normalize=False)
+ state += random_unitary(dims, seed=local_state, backend=backend)
+ state = backend.np.matmul(
+ state,
+ random_gaussian_matrix(dims, rank, seed=local_state, backend=backend),
+ )
+ state = backend.np.matmul(
+ state, backend.np.transpose(backend.np.conj(state), (1, 0))
+ )
+ state /= backend.np.trace(state)
+
+ state = backend.cast(state, dtype=state.dtype)
+
+ if basis is not None:
+ pauli_order = basis.split("-")[1]
+ unitary = comp_basis_to_pauli(
+ int(np.log2(dims)),
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ state = unitary @ vectorization(state, order=order, backend=backend)
+
+ return state
+
+
+def random_clifford(
+ nqubits: int,
+ return_circuit: bool = True,
+ density_matrix: bool = False,
+ seed=None,
+ backend=None,
+):
+ """Generates a random :math:`n`-qubit Clifford operator, where :math:`n` is ``nqubits``.
+ For the mathematical details, see Reference [1].
+
+ Args:
+ nqubits (int): number of qubits.
+ return_circuit (bool, optional): if ``True``, returns a :class:`qibo.models.Circuit`
+ object. If ``False``, returns an ``ndarray`` object. Defaults to ``True``.
+ density_matrix (bool, optional): used when ``return_circuit=True``. If `True`,
+ the circuit would evolve density matrices. Defaults to ``False``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ (ndarray or :class:`qibo.models.Circuit`): Random Clifford operator.
+
+ Reference:
+ 1. S. Bravyi and D. Maslov, *Hadamard-free circuits expose the
+ structure of the Clifford group*.
+ `arXiv:2003.09412 [quant-ph] `_.
+ """
+
+ if isinstance(nqubits, int) is False:
+ raise_error(
+ TypeError,
+ f"nqubits must be type int, but it is type {type(nqubits)}.",
+ )
+
+ if nqubits <= 0:
+ raise_error(ValueError, "nqubits must be a positive integer.")
+
+ if not isinstance(return_circuit, bool):
+ raise_error(
+ TypeError,
+ f"return_circuit must be type bool, but it is type {type(return_circuit)}.",
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ hadamards, permutations = _sample_from_quantum_mallows_distribution(
+ nqubits, local_state=local_state
+ )
+
+ delta_matrix = np.eye(nqubits, dtype=int)
+ delta_matrix_prime = np.copy(delta_matrix)
+
+ gamma_matrix_prime = local_state.integers(0, 2, size=nqubits)
+ gamma_matrix_prime = np.diag(gamma_matrix_prime)
+
+ gamma_matrix = local_state.integers(0, 2, size=nqubits)
+ gamma_matrix = hadamards * gamma_matrix
+ gamma_matrix = np.diag(gamma_matrix)
+
+ # filling off-diagonal elements of gammas and deltas matrices
+ for j in range(nqubits):
+ for k in range(j + 1, nqubits):
+ b = local_state.integers(0, 2)
+ gamma_matrix_prime[k, j] = b
+ gamma_matrix_prime[j, k] = b
+
+ b = local_state.integers(0, 2)
+ delta_matrix_prime[k, j] = b
+
+ if hadamards[k] == 1 and hadamards[j] == 1: # pragma: no cover
+ b = local_state.integers(0, 2)
+ gamma_matrix[k, j] = b
+ gamma_matrix[j, k] = b
+ if permutations[k] > permutations[j]:
+ b = local_state.integers(0, 2)
+ delta_matrix[k, j] = b
+
+ if hadamards[k] == 0 and hadamards[j] == 1:
+ b = local_state.integers(0, 2)
+ delta_matrix[k, j] = b
+ if permutations[k] > permutations[j]:
+ b = local_state.integers(0, 2)
+ gamma_matrix[k, j] = b
+ gamma_matrix[j, k] = b
+
+ if (
+ hadamards[k] == 1
+ and hadamards[j] == 0
+ and permutations[k] < permutations[j]
+ ): # pragma: no cover
+ b = local_state.integers(0, 2)
+ gamma_matrix[k, j] = b
+ gamma_matrix[j, k] = b
+
+ if (
+ hadamards[k] == 0
+ and hadamards[j] == 0
+ and permutations[k] < permutations[j]
+ ): # pragma: no cover
+ b = local_state.integers(0, 2)
+ delta_matrix[k, j] = b
+
+ # get first element of the Borel group
+ clifford_circuit = _operator_from_hadamard_free_group(
+ gamma_matrix, delta_matrix, density_matrix
+ )
+
+ # Apply permutated Hadamard layer
+ for qubit, had in enumerate(hadamards):
+ if had == 1:
+ clifford_circuit.add(gates.H(int(permutations[qubit])))
+
+ # get second element of the Borel group
+ clifford_circuit += _operator_from_hadamard_free_group(
+ gamma_matrix_prime,
+ delta_matrix_prime,
+ density_matrix,
+ random_pauli(
+ nqubits,
+ depth=1,
+ return_circuit=True,
+ density_matrix=density_matrix,
+ seed=local_state,
+ backend=backend,
+ ),
+ )
+
+ if return_circuit is False:
+ clifford_circuit = clifford_circuit.unitary(backend=backend)
+
+ return clifford_circuit
+
+
+def random_pauli(
+ qubits,
+ depth: int,
+ max_qubits: Optional[int] = None,
+ subset: Optional[list] = None,
+ return_circuit: bool = True,
+ density_matrix: bool = False,
+ seed=None,
+ backend=None,
+):
+ """Creates random Pauli operator(s).
+
+ Pauli operators are sampled from the single-qubit Pauli set
+ :math:`\\{I, \\, X, \\, Y, \\, Z\\}`.
+
+ Args:
+ qubits (int or list or ndarray): if ``int`` and ``max_qubits=None``, the
+ number of qubits. If ``int`` and ``max_qubits != None``, qubit index
+ in which the Pauli sequence will act. If ``list`` or ``ndarray``,
+ indexes of the qubits for the Pauli sequence to act.
+ depth (int): length of the sequence of Pauli gates.
+ max_qubits (int, optional): total number of qubits in the circuit.
+ If ``None``, ``max_qubits = max(qubits)``. Defaults to ``None``.
+ subset (list, optional): list containing a subset of the 4 single-qubit
+ Pauli operators. If ``None``, defaults to the complete set.
+ Defaults to ``None``.
+ return_circuit (bool, optional): if ``True``, returns a :class:`qibo.models.Circuit`
+ object. If ``False``, returns an ``ndarray`` with shape (qubits, depth, 2, 2)
+ that contains all Pauli matrices that were sampled. Defaults to ``True``.
+ density_matrix (bool, optional): used when ``return_circuit=True``. If `True`,
+ the circuit would evolve density matrices. Defaults to ``False``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ (ndarray or :class:`qibo.models.Circuit`): all sampled Pauli operators.
+
+ """
+
+ if (
+ not isinstance(qubits, int)
+ and not isinstance(qubits, list)
+ and not isinstance(qubits, np.ndarray)
+ ):
+ raise_error(
+ TypeError,
+ f"qubits must be either type int, list or ndarray, but it is type {type(qubits)}.",
+ )
+
+ if isinstance(qubits, int) and qubits < 0:
+ raise_error(ValueError, "qubits must be a non-negative integer.")
+
+ if isinstance(qubits, int) is False and any(q < 0 for q in qubits):
+ raise_error(ValueError, "qubit indexes must be non-negative integers.")
+
+ if isinstance(depth, int) and depth <= 0:
+ raise_error(ValueError, "depth must be a positive integer.")
+
+ if isinstance(max_qubits, int) and max_qubits <= 0:
+ raise_error(ValueError, "max_qubits must be a positive integer.")
+
+ if max_qubits is not None:
+ if isinstance(qubits, int) and qubits >= max_qubits:
+ raise_error(
+ ValueError,
+ f"qubit index ({qubits}) must be < max_qubits ({max_qubits}).",
+ )
+ elif not isinstance(qubits, int) and any(q >= max_qubits for q in qubits):
+ raise_error(ValueError, "all qubit indexes must be < max_qubits.")
+
+ if not isinstance(return_circuit, bool):
+ raise_error(
+ TypeError,
+ f"return_circuit must be type bool, but it is type {type(return_circuit)}.",
+ )
+
+ if subset is not None and not isinstance(subset, list):
+ raise_error(
+ TypeError, f"subset must be type list, but it is type {type(subset)}."
+ )
+
+ if subset is not None and any(isinstance(item, str) is False for item in subset):
+ raise_error(
+ TypeError,
+ "subset argument must be a subset of strings in the set ['I', 'X', 'Y', 'Z'].",
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ complete_set = (
+ {"I": gates.I, "X": gates.X, "Y": gates.Y, "Z": gates.Z}
+ if return_circuit
+ else {"I": matrices.I, "X": matrices.X, "Y": matrices.Y, "Z": matrices.Z}
+ )
+
+ if subset is None:
+ subset = complete_set
+ else:
+ subset = {key: complete_set[key] for key in subset}
+
+ keys = list(subset.keys())
+
+ if max_qubits is None:
+ if isinstance(qubits, int):
+ max_qubits = qubits
+ qubits = range(qubits)
+ else:
+ max_qubits = int(max(qubits)) + 1
+ else:
+ if isinstance(qubits, int):
+ qubits = [qubits]
+
+ indexes = local_state.integers(0, len(subset), size=(len(qubits), depth))
+ indexes = [[keys[item] for item in row] for row in indexes]
+
+ if return_circuit:
+ gate_grid = Circuit(max_qubits, density_matrix=density_matrix)
+ for qubit, row in zip(qubits, indexes):
+ for column_item in row:
+ if subset[column_item] != gates.I:
+ gate_grid.add(subset[column_item](qubit))
+ else:
+ gate_grid = backend.cast(
+ [[subset[column_item] for column_item in row] for row in indexes]
+ )
+
+ return gate_grid
+
+
+def random_pauli_hamiltonian(
+ nqubits: int,
+ max_eigenvalue: Optional[Union[int, float]] = None,
+ normalize: bool = False,
+ pauli_order: str = "IXYZ",
+ seed=None,
+ backend=None,
+):
+ """Generates a random Hamiltonian in the Pauli basis.
+
+ Args:
+ nqubits (int): number of qubits.
+ max_eigenvalue (int or float, optional): fixes the value of the
+ largest eigenvalue. Defaults to ``None``.
+ normalize (bool, optional): If ``True``, fixes the gap of the
+ Hamiltonian as ``1.0``. Moreover, if ``True``, then ``max_eigenvalue``
+ must be ``> 1.0``. Defaults to ``False``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the basis. Defaults to "IXYZ".
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ tuple(ndarray, ndarray): Hamiltonian in the Pauli basis and its corresponding eigenvalues.
+ """
+ if isinstance(nqubits, int) is False:
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+ elif nqubits <= 0:
+ raise_error(ValueError, "nqubits must be a positive int.")
+
+ if isinstance(max_eigenvalue, (int, float)) is False and normalize is True:
+ raise_error(
+ TypeError,
+ f"when normalize=True, max_eigenvalue must be type float, "
+ + f"but it is {type(max_eigenvalue)}.",
+ )
+ elif (
+ isinstance(max_eigenvalue, (int, float)) is False and max_eigenvalue is not None
+ ):
+ raise_error(
+ TypeError,
+ f"max_eigenvalue must be type float, but it is {type(max_eigenvalue)}.",
+ )
+
+ if isinstance(normalize, bool) is False:
+ raise_error(
+ TypeError,
+ f"normalize must be type bool, but it is type {type(normalize)}.",
+ )
+ elif normalize is True and max_eigenvalue <= 1.0:
+ raise_error(
+ ValueError,
+ "when normalize=True, gap is = 1, thus max_eigenvalue must be > 1.",
+ )
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ d = 2**nqubits
+
+ hamiltonian = random_hermitian(d, normalize=True, seed=local_state, backend=backend)
+
+ eigenvalues, eigenvectors = backend.calculate_eigenvectors(hamiltonian)
+ if backend.name == "tensorflow":
+ eigenvalues = backend.to_numpy(eigenvalues)
+ eigenvectors = backend.to_numpy(eigenvectors)
+
+ if normalize is True:
+ eigenvalues = eigenvalues - eigenvalues[0]
+
+ eigenvalues = eigenvalues / eigenvalues[1]
+
+ shift = 2
+ eigenvectors[:, shift:] = (
+ eigenvectors[:, shift:] * max_eigenvalue / eigenvalues[-1]
+ )
+ eigenvalues[shift:] = eigenvalues[shift:] * max_eigenvalue / eigenvalues[-1]
+
+ hamiltonian = np.zeros((d, d), dtype=complex)
+ hamiltonian = backend.cast(hamiltonian, dtype=hamiltonian.dtype)
+ # excluding the first eigenvector because first eigenvalue is zero
+ for eigenvalue, eigenvector in zip(
+ eigenvalues[1:], backend.np.transpose(eigenvectors, (1, 0))[1:]
+ ):
+ hamiltonian = hamiltonian + eigenvalue * backend.np.outer(
+ eigenvector, backend.np.conj(eigenvector)
+ )
+
+ U = comp_basis_to_pauli(
+ nqubits, normalize=True, pauli_order=pauli_order, backend=backend
+ )
+
+ hamiltonian = backend.np.real(U @ vectorization(hamiltonian, backend=backend))
+
+ return hamiltonian, eigenvalues
+
+
+def random_stochastic_matrix(
+ dims: int,
+ bistochastic: bool = False,
+ diagonally_dominant: bool = False,
+ precision_tol: Optional[float] = None,
+ max_iterations: Optional[int] = None,
+ seed=None,
+ backend=None,
+):
+ """Creates a random stochastic matrix.
+
+ Args:
+ dims (int): dimension of the matrix.
+ bistochastic (bool, optional): if ``True``, matrix is row- and column-stochastic.
+ If ``False``, matrix is row-stochastic. Defaults to ``False``.
+ diagonally_dominant (bool, optional): if ``True``, matrix is strictly diagonally
+ dominant. Defaults to ``False``.
+ precision_tol (float, optional): tolerance level for how much each probability
+ distribution can deviate from summing up to ``1.0``. If ``None``,
+ it defaults to ``qibo.config.PRECISION_TOL``. Defaults to ``None``.
+ max_iterations (int, optional): when ``bistochastic=True``, maximum number
+ of iterations used to normalize all rows and columns simultaneously.
+ If ``None``, defaults to ``qibo.config.MAX_ITERATIONS``.
+ Defaults to ``None``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a statevectorgenerator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: a random stochastic matrix.
+
+ """
+ if dims <= 0:
+ raise_error(ValueError, "dims must be type int and positive.")
+
+ if not isinstance(bistochastic, bool):
+ raise_error(
+ TypeError,
+ f"bistochastic must be type bool, but it is type {type(bistochastic)}.",
+ )
+
+ if not isinstance(diagonally_dominant, bool):
+ raise_error(
+ TypeError,
+ f"diagonally_dominant must be type bool, but it is type {type(diagonally_dominant)}.",
+ )
+
+ if precision_tol is not None:
+ if not isinstance(precision_tol, float):
+ raise_error(
+ TypeError,
+ f"precision_tol must be type float, but it is type {type(precision_tol)}.",
+ )
+ if precision_tol < 0.0:
+ raise_error(ValueError, "precision_tol must be non-negative.")
+
+ if max_iterations is not None:
+ if not isinstance(max_iterations, int):
+ raise_error(
+ TypeError,
+ f"max_iterations must be type int, but it is type {type(precision_tol)}.",
+ )
+ if max_iterations <= 0.0:
+ raise_error(ValueError, "max_iterations must be a positive int.")
+
+ backend, local_state = _check_backend_and_local_state(seed, backend)
+
+ if precision_tol is None:
+ precision_tol = PRECISION_TOL
+ if max_iterations is None:
+ max_iterations = MAX_ITERATIONS
+
+ matrix = local_state.random(size=(dims, dims))
+ if diagonally_dominant:
+ matrix /= dims**2
+ for k, row in enumerate(matrix):
+ row = np.delete(row, obj=k)
+ matrix[k, k] = 1 - np.sum(row)
+ row_sum = np.sum(matrix, axis=1)
+
+ row_sum = matrix.sum(axis=1)
+
+ if bistochastic:
+ column_sum = matrix.sum(axis=0)
+ count = 0
+ while count <= max_iterations - 1 and (
+ (
+ np.any(row_sum >= 1 + precision_tol)
+ or np.any(row_sum <= 1 - precision_tol)
+ )
+ or (
+ np.any(column_sum >= 1 + precision_tol)
+ or np.any(column_sum <= 1 - precision_tol)
+ )
+ ):
+ matrix = matrix / matrix.sum(axis=0)
+ matrix = matrix / matrix.sum(axis=1)[:, np.newaxis]
+ row_sum = matrix.sum(axis=1)
+ column_sum = matrix.sum(axis=0)
+ count += 1
+ if count == max_iterations:
+ warnings.warn("Reached max iterations.", RuntimeWarning)
+ else:
+ matrix = matrix / np.outer(row_sum, [1] * dims)
+
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+
+ return matrix
+
+
+def _sample_from_quantum_mallows_distribution(nqubits: int, local_state):
+ """Using the quantum Mallows distribution, samples a binary array
+ representing a layer of Hadamard gates as well as an array with permutated
+ qubit indexes. For more details, see Reference [1].
+
+ Args:
+ nqubits (int): number of qubits.
+ local_state (:class:`numpy.random.Generator`): a generator of
+ random numbers
+
+ Returns:
+ (``ndarray``, ``ndarray`): tuple of binary ``ndarray`` and ``ndarray`` of indexes.
+
+ Reference:
+ 1. S. Bravyi and D. Maslov, *Hadamard-free circuits expose the
+ structure of the Clifford group*.
+ `arXiv:2003.09412 [quant-ph] `_.
+
+ """
+ mute_index = list(range(nqubits))
+
+ exponents = np.arange(nqubits, 0, -1, dtype=np.int64)
+ powers = 4**exponents
+ powers[powers == 0] = np.iinfo(np.int64).max
+
+ r = local_state.uniform(0, 1, size=nqubits)
+
+ indexes = -1 * (np.ceil(np.log2(r + (1 - r) / powers)))
+
+ hadamards = 1 * (indexes < exponents)
+
+ permutations = np.zeros(nqubits, dtype=int)
+ for l, (index, m) in enumerate(zip(indexes, exponents)):
+ k = index if index < m else 2 * m - index - 1
+ k = int(k)
+ permutations[l] = mute_index[k]
+ del mute_index[k]
+
+ return hadamards, permutations
+
+
+@cache
+def _create_S(q):
+ return gates.S(int(q))
+
+
+@cache
+def _create_CZ(cq, tq):
+ return gates.CZ(int(cq), int(tq))
+
+
+@cache
+def _create_CNOT(cq, tq):
+ return gates.CNOT(int(cq), int(tq))
+
+
+def _operator_from_hadamard_free_group(
+ gamma_matrix, delta_matrix, density_matrix: bool = False, pauli_operator=None
+):
+ """Calculates an element :math:`F` of the Hadamard-free group :math:`\\mathcal{F}_{n}`,
+ where :math:`n` is the number of qubits ``nqubits``. For more details,
+ see Reference [1].
+
+ Args:
+ gamma_matrix (ndarray): :math:`\\, n \\times n \\,` binary matrix.
+ delta_matrix (ndarray): :math:`\\, n \\times n \\,` binary matrix.
+ density_matrix (bool, optional): used when ``return_circuit=True``. If `True`,
+ the circuit would evolve density matrices. Defaults to ``False``.
+ pauli_operator (:class:`qibo.models.Circuit`, optional): a :math:`n`-qubit
+ Pauli operator. If ``None``, it is assumed to be the Identity.
+ Defaults to ``None``.
+
+ Returns:
+ :class:`qibo.models.Circuit`: element of the Hadamard-free group.
+
+ Reference:
+ 1. S. Bravyi and D. Maslov, *Hadamard-free circuits expose the
+ structure of the Clifford group*.
+ `arXiv:2003.09412 [quant-ph] `_.
+ """
+ if gamma_matrix.shape != delta_matrix.shape: # pragma: no cover
+ raise_error(
+ ValueError,
+ "gamma_matrix and delta_matrix must have shape (nqubits, nqubits), "
+ + f"but {gamma_matrix.shape} != {delta_matrix.shape}",
+ )
+
+ nqubits = len(gamma_matrix)
+ circuit = Circuit(nqubits, density_matrix=density_matrix)
+
+ if pauli_operator is not None:
+ circuit += pauli_operator
+
+ idx = np.tril_indices(nqubits, k=-1)
+ gamma_ones = gamma_matrix[idx].nonzero()[0]
+ delta_ones = delta_matrix[idx].nonzero()[0]
+
+ S_gates = [_create_S(q) for q in np.diag(gamma_matrix).nonzero()[0]]
+ CZ_gates = [
+ _create_CZ(cq, tq) for cq, tq in zip(idx[1][gamma_ones], idx[0][gamma_ones])
+ ]
+ CNOT_gates = [
+ _create_CNOT(cq, tq) for cq, tq in zip(idx[1][delta_ones], idx[0][delta_ones])
+ ]
+
+ circuit.add(S_gates + CZ_gates + CNOT_gates)
+
+ return circuit
+
+
+def _super_op_from_bcsz_measure(dims: int, rank: int, order: str, seed, backend):
+ """Helper function for :func:qibo.quantum_info.random_ensembles.random_quantum_channel.
+ Generates a channel from the BCSZ measure.
+
+ Args:
+ dims (int): dimension of the :math:`n`-qubit operator, i.e. :math:`\\text{dims}=2^{n}`.
+ rank (int, optional): used when ``measure=="bcsz"``. Rank of the matrix.
+ If ``None``, then ``rank==dims``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, vectorization is performed row-wise.
+ If ``"column"``, vectorization is performed column-wise. Defaults to ``"row"``.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+ """
+ nqubits = int(np.log2(dims))
+
+ super_op = random_gaussian_matrix(
+ dims**2, rank=rank, mean=0, stddev=1, seed=seed, backend=backend
+ )
+ super_op = super_op @ backend.np.conj(super_op).T
+
+ # partial trace implemented with einsum
+ super_op_reduced = np.einsum(
+ "ijik->jk", np.reshape(backend.to_numpy(super_op), (dims,) * 4)
+ )
+
+ eigenvalues, eigenvectors = np.linalg.eigh(super_op_reduced)
+
+ eigenvalues = np.sqrt(1.0 / eigenvalues)
+
+ operator = np.zeros((dims, dims), dtype=complex)
+ operator = backend.cast(operator, dtype=operator.dtype)
+ for eigenvalue, eigenvector in zip(
+ backend.cast(eigenvalues), backend.cast(eigenvectors).T
+ ):
+ operator = operator + eigenvalue * backend.np.outer(
+ eigenvector, backend.np.conj(eigenvector)
+ )
+
+ if order == "row":
+ operator = backend.np.kron(
+ backend.identity_density_matrix(nqubits, normalize=False), operator
+ )
+ if order == "column":
+ operator = backend.np.kron(
+ operator, backend.identity_density_matrix(nqubits, normalize=False)
+ )
+
+ super_op = operator @ super_op @ operator
+
+ return super_op
diff --git a/src/qibo/quantum_info/superoperator_transformations.py b/src/qibo/quantum_info/superoperator_transformations.py
new file mode 100644
index 000000000..73bce85f9
--- /dev/null
+++ b/src/qibo/quantum_info/superoperator_transformations.py
@@ -0,0 +1,2230 @@
+"""Module with the most commom superoperator transformations."""
+
+import warnings
+from typing import Optional
+
+import numpy as np
+from scipy.optimize import minimize
+
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+from qibo.gates.abstract import Gate
+from qibo.gates.gates import Unitary
+from qibo.gates.special import FusedGate
+
+
+def vectorization(state, order: str = "row", backend=None):
+ """Returns state :math:`\\rho` in its Liouville
+ representation :math:`|\\rho\\rangle\\rangle`.
+
+ If ``order="row"``, then:
+
+ .. math::
+ |\\rho\\rangle\\rangle = \\sum_{k, l} \\, \\rho_{kl} \\, \\ket{k} \\otimes \\ket{l}
+
+ If ``order="column"``, then:
+
+ .. math::
+ |\\rho\\rangle\\rangle = \\sum_{k, l} \\, \\rho_{kl} \\, \\ket{l} \\otimes \\ket{k}
+
+ Args:
+ state: state vector or density matrix.
+ order (str, optional): If ``"row"``, vectorization is performed
+ row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, a block-vectorization is
+ performed. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Liouville representation of ``state``.
+ """
+ if (
+ (len(state.shape) >= 3)
+ or (len(state) == 0)
+ or (len(state.shape) == 2 and state.shape[0] != state.shape[1])
+ ):
+ raise_error(
+ TypeError,
+ f"Object must have dims either (k,) or (k,k), but have dims {state.shape}.",
+ )
+
+ if not isinstance(order, str):
+ raise_error(
+ TypeError, f"order must be type str, but it is type {type(order)} instead."
+ )
+ else:
+ if order not in ["row", "column", "system"]:
+ raise_error(
+ ValueError,
+ f"order must be either 'row' or 'column' or 'system', but it is {order}.",
+ )
+
+ backend = _check_backend(backend)
+
+ if len(state.shape) == 1:
+ state = backend.np.outer(state, backend.np.conj(state))
+
+ if order == "row":
+ state = backend.np.reshape(state, (1, -1))[0]
+ elif order == "column":
+ state = state.T
+ state = backend.np.reshape(state, (1, -1))[0]
+ else:
+ dim = len(state)
+ nqubits = int(np.log2(dim))
+
+ new_axis = []
+ for qubit in range(nqubits):
+ new_axis += [qubit + nqubits, qubit]
+
+ state = backend.np.reshape(state, [2] * 2 * nqubits)
+ state = backend.np.transpose(state, new_axis)
+ state = backend.np.reshape(state, (-1,))
+
+ return state
+
+
+def unvectorization(state, order: str = "row", backend=None):
+ """Returns state :math:`\\rho` from its Liouville
+ representation :math:`|\\rho\\rangle\\rangle`. This operation is
+ the inverse function of :func:`vectorization`, i.e.
+
+ .. math::
+ \\begin{align}
+ \\rho &= \\text{unvectorization}(|\\rho\\rangle\\rangle) \\nonumber \\\\
+ &= \\text{unvectorization}(\\text{vectorization}(\\rho)) \\nonumber
+ \\end{align}
+
+ Args:
+ state: quantum state in Liouville representation.
+ order (str, optional): If ``"row"``, unvectorization is performed
+ row-wise. If ``"column"``, unvectorization is performed
+ column-wise. If ``"system"``, system-wise vectorization is
+ performed. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Density matrix of ``state``.
+ """
+
+ if len(state.shape) != 1:
+ raise_error(
+ TypeError,
+ f"Object must have dims (k,), but have dims {state.shape}.",
+ )
+
+ if not isinstance(order, str):
+ raise_error(
+ TypeError, f"order must be type str, but it is type {type(order)} instead."
+ )
+ else:
+ if order not in ["row", "column", "system"]:
+ raise_error(
+ ValueError,
+ f"order must be either 'row' or 'column' or 'system', but it is {order}.",
+ )
+
+ backend = _check_backend(backend)
+ state = backend.cast(state)
+
+ dim = int(np.sqrt(len(state)))
+
+ if order in ["row", "column"]:
+ order = "C" if order == "row" else "F"
+ state = backend.cast(
+ np.reshape(backend.to_numpy(state), (dim, dim), order=order)
+ )
+ else:
+ nqubits = int(np.log2(dim))
+ axes_old = list(np.arange(0, 2 * nqubits))
+ state = backend.np.reshape(state, [2] * 2 * nqubits)
+ state = backend.np.transpose(state, axes_old[1::2] + axes_old[0::2])
+ state = backend.np.reshape(state, [2**nqubits] * 2)
+
+ return state
+
+
+def to_choi(channel, order: str = "row", backend=None):
+ """Converts quantum ``channel`` :math:`U` to its Choi representation :math:`\\Lambda`.
+
+ .. math::
+ \\Lambda = | U \\rangle\\rangle \\langle\\langle U | \\, ,
+
+ where :math:`| \\cdot \\rangle\\rangle` is the :func:`qibo.quantum_info.vectorization`
+ operation.
+
+ Args:
+ channel (ndarray): quantum channel.
+ order (str, optional): If ``"row"``, vectorization is performed
+ row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, a block-vectorization is
+ performed. Default is ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: quantum channel in its Choi representation.
+ """
+ backend = _check_backend(backend)
+
+ channel = vectorization(channel, order=order, backend=backend)
+ channel = backend.np.outer(channel, backend.np.conj(channel))
+
+ return channel
+
+
+def to_liouville(channel, order: str = "row", backend=None):
+ """Converts quantum ``channel`` :math:`U` to its Liouville representation
+ :math:`\\mathcal{E}`. It uses the Choi representation as an
+ intermediate step.
+
+ Args:
+ channel (ndarray): quantum channel.
+ order (str, optional): If ``"row"``, vectorization is performed
+ row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, a block-vectorization is
+ performed. Default is ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: quantum channel in its Liouville representation.
+ """
+ backend = _check_backend(backend)
+
+ channel = to_choi(channel, order=order, backend=backend)
+ channel = _reshuffling(channel, order=order, backend=backend)
+
+ return channel
+
+
+def to_pauli_liouville(
+ channel,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts quantum ``channel`` :math:`U` to its Pauli-Liouville
+ representation :math:`\\mathcal{E}`. It uses the Liouville representation
+ as an intermediate step.
+
+ Args:
+ channel (ndarray): quantum channel.
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, vectorization is performed
+ row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, a block-vectorization is
+ performed. Default is ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: quantum channel in its Pauli-Liouville representation.
+ """
+ from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415
+
+ backend = _check_backend(backend)
+
+ nqubits = int(np.log2(channel.shape[0]))
+
+ channel = to_liouville(channel, order=order, backend=backend)
+
+ unitary = comp_basis_to_pauli(
+ nqubits, normalize, pauli_order=pauli_order, backend=backend
+ )
+
+ channel = unitary @ channel @ backend.np.conj(unitary).T
+
+ return channel
+
+
+def to_chi(
+ channel,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts quantum ``channel`` :math:`U` to its :math:`\\chi`-representation.
+
+ Args:
+ channel (ndarray): quantum channel.
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, vectorization is performed
+ row-wise. If ``"column"``, vectorization is performed
+ column-wise. If ``"system"``, a block-vectorization is
+ performed. Default is ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Default is "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: quantum channel in its :math:`\\chi`-representation.
+ """
+ channel = to_choi(channel, order=order, backend=backend)
+ channel = liouville_to_pauli(
+ channel,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return channel
+
+
+def choi_to_liouville(choi_super_op, order: str = "row", backend=None):
+ """Converts Choi representation :math:`\\Lambda` of quantum channel
+ to its Liouville representation :math:`\\mathcal{E}`.
+
+
+ If ``order="row"``, then:
+
+ .. math::
+ \\Lambda_{\\alpha\\beta, \\, \\gamma\\delta} \\mapsto
+ \\Lambda_{\\alpha\\gamma, \\, \\beta\\delta} \\equiv \\mathcal{E}
+
+ If ``order="column"``, then:
+
+ .. math::
+ \\Lambda_{\\alpha\\beta, \\, \\gamma\\delta} \\mapsto
+ \\Lambda_{\\delta\\beta, \\, \\gamma\\alpha} \\equiv \\mathcal{E}
+
+
+ Args:
+ choi_super_op: Choi representation of quantum channel.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Liouville representation of quantum channel.
+ """
+
+ return _reshuffling(choi_super_op, order=order, backend=backend)
+
+
+def choi_to_pauli(
+ choi_super_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Choi representation :math:`\\Lambda` of a quantum channel
+ to its Pauli-Liouville representation.
+
+ Args:
+ choi_super_op (ndarray): superoperator in the Choi representation.
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, it assumes ``choi_super_op`` is in
+ row-vectorization. If ``"column"``, it assumes column-vectorization.
+ Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: superoperator in the Pauli-Liouville representation.
+ """
+ super_op = choi_to_liouville(choi_super_op, order, backend=backend)
+ super_op = liouville_to_pauli(
+ super_op, normalize, order, pauli_order, backend=backend
+ )
+
+ return super_op
+
+
+def choi_to_kraus(
+ choi_super_op,
+ precision_tol: Optional[float] = None,
+ order: str = "row",
+ validate_cp: bool = True,
+ backend=None,
+):
+ """Converts Choi representation :math:`\\Lambda` of a quantum channel :math:`\\mathcal{E}`
+ into Kraus operators :math:`\\{ K_{\\alpha} \\}_{\\alpha}`.
+
+ If :math:`\\mathcal{E}` is a completely positive (CP) map, then
+
+ .. math::
+ \\Lambda = \\sum_{\\alpha} \\, \\lambda_{\\alpha}^{2} \\,
+ |\\tilde{K}_{\\alpha}\\rangle\\rangle \\langle\\langle \\tilde{K}_{\\alpha}| \\, .
+
+ This is the spectral decomposition of :math:`\\Lambda`, Hence, the set
+ :math:`\\{\\lambda_{\\alpha}, \\, \\tilde{K}_{\\alpha}\\}_{\\alpha}`
+ is found by diagonalization of :math:`\\Lambda`. The Kraus operators
+ :math:`\\{K_{\\alpha}\\}_{\\alpha}` are defined as
+
+ .. math::
+ K_{\\alpha} = \\lambda_{\\alpha} \\,
+ \\text{unvectorization}(|\\tilde{K}_{\\alpha}\\rangle\\rangle) \\, .
+
+ If :math:`\\mathcal{E}` is not CP, then spectral composition is replaced by
+ a singular value decomposition (SVD), i.e.
+
+ .. math::
+ \\Lambda = U \\, S \\, V^{\\dagger} \\, ,
+
+ where :math:`U` is a :math:`d^{2} \\times d^{2}` unitary matrix, :math:`S` is a
+ :math:`d^{2} \\times d^{2}` positive diagonal matrix containing the singular values
+ of :math:`\\Lambda`, and :math:`V` is a :math:`d^{2} \\times d^{2}` unitary matrix.
+ The Kraus coefficients are replaced by the square root of the singular values, and
+ :math:`U` (:math:`V`) determine the left-generalized (right-generalized) Kraus
+ operators.
+
+ Args:
+ choi_super_op: Choi representation of a quantum channel.
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ tuple(ndarray, ndarray): The set
+ :math:`\\{K_{\\alpha}, \\, \\lambda_{\\alpha} \\}_{\\alpha}`
+ of Kraus operators representing the quantum channel and their respective coefficients.
+ If map is non-CP, then function returns the set
+ :math:`\\{ \\{K_{L}, \\, K_{R}\\}_{\\alpha}, \\, \\lambda_{\\alpha} \\}_{\\alpha}`,
+ with the left- and right-generalized Kraus operators as well as the square root of
+ their corresponding singular values.
+ """
+
+ if precision_tol is not None and not isinstance(precision_tol, float):
+ raise_error(
+ TypeError,
+ f"precision_tol must be type float, but it is type {type(precision_tol)}",
+ )
+
+ if precision_tol is not None and precision_tol < 0:
+ raise_error(
+ ValueError,
+ f"precision_tol must be a non-negative float, but it is {precision_tol}.",
+ )
+
+ if precision_tol is None: # pragma: no cover
+ precision_tol = PRECISION_TOL
+
+ if not isinstance(validate_cp, bool):
+ raise_error(
+ TypeError,
+ f"validate_cp must be type bool, but it is type {type(validate_cp)}.",
+ )
+
+ backend = _check_backend(backend)
+ choi_super_op = backend.cast(choi_super_op)
+
+ if validate_cp:
+ norm = float(
+ backend.calculate_norm_density_matrix(
+ choi_super_op - backend.np.conj(choi_super_op).T, order=2
+ )
+ )
+ if norm > PRECISION_TOL:
+ non_cp = True
+ else:
+ # using eigh because, in this case, choi_super_op is
+ # *already confirmed* to be Hermitian
+ eigenvalues, eigenvectors = backend.calculate_eigenvectors(choi_super_op)
+ eigenvectors = eigenvectors.T
+
+ non_cp = bool(any(backend.np.real(eigenvalues) < -PRECISION_TOL))
+ else:
+ non_cp = False
+ # using eigh because, in this case, choi_super_op is
+ # *assumed* to be Hermitian
+ eigenvalues, eigenvectors = backend.calculate_eigenvectors(choi_super_op)
+ eigenvectors = eigenvectors.T
+
+ if non_cp:
+ warnings.warn("Input choi_super_op is a non-completely positive map.")
+
+ # using singular value decomposition because choi_super_op is non-CP
+ U, coefficients, V = np.linalg.svd(backend.to_numpy(choi_super_op))
+ U = np.transpose(U)
+ coefficients = np.sqrt(coefficients)
+ V = np.conj(V)
+
+ kraus_left, kraus_right = [], []
+ for coeff, eigenvector_left, eigenvector_right in zip(coefficients, U, V):
+ kraus_left.append(
+ coeff * unvectorization(eigenvector_left, order=order, backend=backend)
+ )
+ kraus_right.append(
+ coeff * unvectorization(eigenvector_right, order=order, backend=backend)
+ )
+ kraus_left = backend.cast(kraus_left)
+ kraus_right = backend.cast(kraus_right)
+ kraus_ops = backend.cast([kraus_left, kraus_right])
+ else:
+ # when choi_super_op is CP
+ kraus_ops, coefficients = [], []
+ for eig, kraus in zip(eigenvalues, eigenvectors):
+ if backend.np.abs(eig) > precision_tol:
+ eig = backend.np.sqrt(eig)
+ kraus_ops.append(
+ eig * unvectorization(kraus, order=order, backend=backend)
+ )
+ coefficients.append(eig)
+
+ kraus_ops = backend.cast(kraus_ops)
+ coefficients = backend.cast(coefficients)
+
+ return kraus_ops, coefficients
+
+
+def choi_to_chi(
+ choi_super_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Choi representation :math:`\\Lambda` of quantum channel
+ to its :math:`\\chi`-matrix representation.
+
+ .. math::
+ \\chi = \\text{liouville_to_pauli}(\\Lambda)
+
+ Args:
+ choi_super_op: Choi representation of a quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4
+ single-qubit Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+ Returns:
+ ndarray: Chi-matrix representation of the quantum channel.
+ """
+ process_matrix = liouville_to_pauli(
+ choi_super_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return process_matrix
+
+
+def choi_to_stinespring(
+ choi_super_op,
+ precision_tol: Optional[float] = None,
+ order: str = "row",
+ validate_cp: bool = True,
+ nqubits: Optional[int] = None,
+ initial_state_env=None,
+ backend=None,
+):
+ """Converts Choi representation :math:`\\Lambda` of quantum channel
+ to its Stinespring representation :math:`U_{0}`.
+ It uses the Kraus representation as an intermediate step.
+
+ Args:
+ choi_super_op: Choi representation of a quantum channel.
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ nqubits (int, optional): total number of qubits in the system that is
+ interacting with the environment. Must be equal or greater than
+ the number of qubits ``kraus_ops`` acts on. If ``None``,
+ defaults to the number of qubits in ``kraus_ops``.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of quantum channel.
+ """
+ kraus_ops, _ = choi_to_kraus(
+ choi_super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=validate_cp,
+ backend=backend,
+ )
+
+ if validate_cp is True and len(kraus_ops.shape) != 3:
+ raise_error(
+ NotImplementedError,
+ "Stinespring representation not implemented for non-completely positive maps.",
+ )
+
+ if nqubits is None:
+ nqubits = int(np.log2(kraus_ops[0].shape[0]))
+
+ nqubits_list = [tuple(range(nqubits)) for _ in range(len(kraus_ops))]
+
+ kraus_ops = list(zip(nqubits_list, kraus_ops))
+
+ stinespring = kraus_to_stinespring(
+ kraus_ops, nqubits=nqubits, initial_state_env=initial_state_env, backend=backend
+ )
+
+ return stinespring
+
+
+def kraus_to_choi(kraus_ops, order: str = "row", backend=None):
+ """Converts Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`
+ of quantum channel to its Choi representation :math:`\\Lambda`.
+
+ .. math::
+ \\Lambda = \\sum_{\\alpha} \\, |K_{\\alpha}\\rangle\\rangle \\langle\\langle K_{\\alpha}|
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of the Kraus channel.
+ """
+ backend = _check_backend(backend)
+
+ gates, target_qubits = _set_gate_and_target_qubits(kraus_ops)
+ nqubits = 1 + max(target_qubits)
+ dim = 2**nqubits
+
+ super_op = np.zeros((dim**2, dim**2), dtype=complex)
+ super_op = backend.cast(super_op, dtype=super_op.dtype)
+ for gate in gates:
+ kraus_op = FusedGate(*range(nqubits))
+ kraus_op.append(gate)
+ kraus_op = kraus_op.matrix(backend)
+ kraus_op = vectorization(kraus_op, order=order, backend=backend)
+ super_op = super_op + backend.np.outer(kraus_op, backend.np.conj(kraus_op))
+ del kraus_op
+
+ return super_op
+
+
+def kraus_to_liouville(kraus_ops, order: str = "row", backend=None):
+ """Converts from Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`
+ of quantum channel to its Liouville representation :math:`\\mathcal{E}`.
+ It uses the Choi representation as an intermediate step.
+
+ .. math::
+ \\mathcal{E} = \\text{choi_to_liouville}(\\text{kraus_to_choi}
+ (\\{K_{\\alpha}\\}_{\\alpha}))
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Liouville representation of quantum channel.
+ """
+ super_op = kraus_to_choi(kraus_ops, order=order, backend=backend)
+ super_op = choi_to_liouville(super_op, order=order, backend=backend)
+
+ return super_op
+
+
+def kraus_to_pauli(
+ kraus_ops,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`
+ of a quantum channel to its Pauli-Liouville representation.
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, intermediate step for Choi
+ representation is done in row-vectorization. If ``"column"``,
+ step is done in column-vectorization. If ``"system"``,
+ block-vectorization is performed. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4
+ single-qubit Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: superoperator in the Pauli-Liouville representation.
+ """
+ super_op = kraus_to_choi(kraus_ops, order, backend=backend)
+ super_op = choi_to_pauli(super_op, normalize, order, pauli_order, backend=backend)
+
+ return super_op
+
+
+def kraus_to_chi(
+ kraus_ops,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Convert Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`
+ of quantum channel to its :math:`\\chi`-matrix representation.
+
+ .. math::
+ \\chi = \\sum_{\\alpha} \\, |c_{\\alpha}\\rangle\\rangle \\langle\\langle c_{\\alpha}|,
+
+ where :math:`|c_{\\alpha}\\rangle\\rangle \\cong |K_{\\alpha}\\rangle\\rangle`
+ in Pauli-Liouville basis.
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the Pauli basis. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Chi-matrix representation of the Kraus channel.
+ """
+ from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415
+
+ backend = _check_backend(backend)
+
+ gates, target_qubits = _set_gate_and_target_qubits(kraus_ops)
+ nqubits = 1 + max(target_qubits)
+ dim = 2**nqubits
+
+ comp_to_pauli = comp_basis_to_pauli(
+ int(nqubits),
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ super_op = np.zeros((dim**2, dim**2), dtype=complex)
+ super_op = backend.cast(super_op, dtype=super_op.dtype)
+ for gate in gates:
+ kraus_op = FusedGate(*range(nqubits))
+ kraus_op.append(gate)
+ kraus_op = kraus_op.matrix(backend)
+ kraus_op = vectorization(kraus_op, order=order, backend=backend)
+ kraus_op = comp_to_pauli @ kraus_op
+ super_op = super_op + backend.np.outer(kraus_op, backend.np.conj(kraus_op))
+ del kraus_op
+
+ return super_op
+
+
+def kraus_to_stinespring(
+ kraus_ops, nqubits: Optional[int] = None, initial_state_env=None, backend=None
+):
+ """Converts Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`
+ of quantum channel to its Stinespring representation :math:`U_{0}`, i.e.
+
+ .. math::
+ U_{0} = \\sum_{\\alpha} \\, K_{\\alpha} \\otimes \\ketbra{\\alpha}{v_{0}} \\, ,
+
+ where :math:`\\ket{v_{0}}` is the initial state of the environment
+ (``initial_state_env``), :math:`D` is the dimension of the environment's
+ Hilbert space, and
+ :math:`\\{\\ket{\\alpha} \\, : \\, \\alpha = 0, 1, \\cdots, D - 1 \\}`
+ is an orthonormal basis for the environment's space.
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ nqubits (int, optional): total number of qubits in the system that is
+ interacting with the environment. Must be equal or greater than
+ the number of qubits ``kraus_ops`` acts on. If ``None``,
+ defaults to the number of qubits in ``kraus_ops``.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Stinespring representation (restricted unitary) of the Kraus channel.
+ """
+ backend = _check_backend(backend)
+
+ if initial_state_env is not None:
+ if len(initial_state_env) != len(kraus_ops):
+ raise_error(
+ ValueError,
+ "dim of initial_state_env must be equal to the number of Kraus operators.",
+ )
+
+ if len(initial_state_env.shape) != 1:
+ raise_error(ValueError, "initial_state_env must be a statevector.")
+
+ gates, target_qubits = _set_gate_and_target_qubits(kraus_ops)
+
+ if nqubits is None:
+ nqubits = 1 + max(target_qubits)
+
+ dim = 2**nqubits
+ dim_env = len(kraus_ops)
+ dim_stinespring = dim * dim_env
+
+ if initial_state_env is None:
+ initial_state_env = np.zeros(dim_env, dtype=complex)
+ initial_state_env[0] = 1.0
+ initial_state_env = backend.cast(
+ initial_state_env, dtype=initial_state_env.dtype
+ )
+
+ # only utility is for outer product,
+ # so np.conj here to only do it once
+ initial_state_env = backend.np.conj(initial_state_env)
+
+ stinespring = np.zeros((dim_stinespring, dim_stinespring), dtype=complex)
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+ for alpha, gate in enumerate(gates):
+ vector_alpha = np.zeros(dim_env, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ kraus_op = FusedGate(*range(nqubits))
+ kraus_op.append(gate)
+ kraus_op = kraus_op.matrix(backend)
+ kraus_op = backend.cast(kraus_op, dtype=kraus_op.dtype)
+ stinespring = stinespring + backend.np.kron(
+ kraus_op,
+ backend.np.outer(vector_alpha, initial_state_env),
+ )
+ del kraus_op, vector_alpha
+
+ return stinespring
+
+
+def liouville_to_choi(super_op, order: str = "row", backend=None):
+ """Converts Liouville representation of quantum channel :math:`\\mathcal{E}`
+ to its Choi representation :math:`\\Lambda`. Indexing :math:`\\mathcal{E}` as
+ :math:`\\mathcal{E}_{\\alpha\\beta, \\, \\gamma\\delta} \\,\\,`, then
+
+ If ``order="row"``:
+
+ .. math::
+ \\Lambda = \\sum_{k, l} \\, \\ketbra{k}{l} \\otimes \\mathcal{E}(\\ketbra{k}{l})
+ \\equiv \\mathcal{E}_{\\alpha\\gamma, \\, \\beta\\delta}
+
+ If ``order="column"``, then:
+
+ .. math::
+ \\Lambda = \\sum_{k, l} \\, \\mathcal{E}(\\ketbra{k}{l}) \\otimes \\ketbra{k}{l}
+ \\equiv \\mathcal{E}_{\\delta\\beta, \\, \\gamma\\alpha}
+
+ Args:
+ super_op: Liouville representation of quantum channel.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of quantum channel.
+ """
+
+ return _reshuffling(super_op, order=order, backend=backend)
+
+
+def liouville_to_pauli(
+ super_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Liouville representation :math:`\\mathcal{E}` of a
+ quantum channel to its Pauli-Liouville representation.
+
+ Args:
+ super_op (ndarray): superoperator in the Liouville representation._
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, it assumes ``super_op`` is in
+ row-vectorization. If ``"column"``, it assumes column-vectorization.
+ If ``"system"``, it assumes block-vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the basis. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: superoperator in the Pauli-Liouville representation.
+ """
+ from qibo.quantum_info.basis import comp_basis_to_pauli
+
+ backend = _check_backend(backend)
+
+ dim = int(np.sqrt(len(super_op)))
+ nqubits = int(np.log2(dim))
+
+ if (
+ super_op.shape[0] != super_op.shape[1]
+ or np.mod(dim, 1) != 0
+ or np.mod(nqubits, 1) != 0
+ ):
+ raise_error(ValueError, "super_op must be of shape (4^n, 4^n)")
+
+ comp_to_pauli = comp_basis_to_pauli(
+ nqubits,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return comp_to_pauli @ super_op @ backend.np.conj(comp_to_pauli.T)
+
+
+def liouville_to_kraus(
+ super_op, precision_tol: Optional[float] = None, order: str = "row", backend=None
+):
+ """Converts Liouville representation :math:`\\mathcal{E}` of a quantum
+ channel to its Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`.
+ It uses the Choi representation as an intermediate step.
+
+ .. math::
+ \\{K_{\\alpha}, \\, \\lambda_{\\alpha}\\}_{\\alpha} =
+ \\text{choi_to_kraus}(\\text{liouville_to_choi}(\\mathcal{E}))
+
+ Args:
+ super_op (ndarray): Liouville representation of quantum channel.
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda < \\text{precision_tol}` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to None.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (ndarray, ndarray): Kraus operators of quantum channel and their respective coefficients.
+ """
+ choi_super_op = liouville_to_choi(super_op, order=order, backend=backend)
+ kraus_ops, coefficients = choi_to_kraus(
+ choi_super_op, precision_tol, order=order, backend=backend
+ )
+
+ return kraus_ops, coefficients
+
+
+def liouville_to_chi(
+ super_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Liouville representation of quantum channel :math:`\\mathcal{E}`
+ to its :math:`\\chi`-matrix representation.
+ It uses the Choi representation as an intermediate step.
+
+ .. math::
+ \\chi = \\text{liouville_to_pauli}(\\text{liouville_to_choi}(\\mathcal{E}))
+
+ Args:
+ super_op: Liouville representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the basis. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Chi-matrix representation of quantum channel.
+ """
+
+ choi_super_op = liouville_to_choi(super_op, order=order, backend=backend)
+ process_matrix = liouville_to_pauli(
+ choi_super_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return process_matrix
+
+
+def liouville_to_stinespring(
+ super_op,
+ order: str = "row",
+ precision_tol: Optional[float] = None,
+ validate_cp: bool = True,
+ nqubits: Optional[int] = None,
+ initial_state_env=None,
+ backend=None,
+):
+ """Converts Liouville representation :math:`\\mathcal{E}` of quantum channel
+ to its Stinespring representation :math:`U_{0}`.
+ It uses the Choi representation :math:`\\Lambda` as intermediate step.
+
+ Args:
+ super_op: Liouville representation of quantum channel.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ nqubits (int, optional): total number of qubits in the system that is
+ interacting with the environment. Must be equal or greater than
+ the number of qubits ``kraus_ops`` acts on. If ``None``,
+ defaults to the number of qubits in ``kraus_ops``.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Stinespring representation of quantum channel.
+ """
+ choi_super_op = liouville_to_choi(super_op, order=order, backend=backend)
+ stinespring = choi_to_stinespring(
+ choi_super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+
+ return stinespring
+
+
+def pauli_to_liouville(
+ pauli_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Pauli-Liouville representation of a quantum channel to its
+ Liouville representation :math:`\\mathcal{E}`.
+
+ Args:
+ pauli_op (ndarray): Pauli-Liouville representation of a quantum channel.
+ normalize (bool, optional): If ``True`` assumes ``pauli_op`` is represented
+ in the normalized Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, returns Liouville representation in
+ row-vectorization. If ``"column"``, returns column-vectorized
+ superoperator. If ``"system"``, superoperator will be in
+ block-vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: superoperator in the Liouville representation.
+ """
+ from qibo.quantum_info.basis import pauli_to_comp_basis # pylint: disable=C0415
+
+ backend = _check_backend(backend)
+
+ dim = int(np.sqrt(len(pauli_op)))
+ nqubits = int(np.log2(dim))
+
+ if (
+ pauli_op.shape[0] != pauli_op.shape[1]
+ or np.mod(dim, 1) != 0
+ or np.mod(nqubits, 1) != 0
+ ):
+ raise_error(ValueError, "pauli_op must be of shape (4^n, 4^n)")
+
+ pauli_to_comp = pauli_to_comp_basis(
+ nqubits,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return pauli_to_comp @ pauli_op @ backend.np.conj(pauli_to_comp).T
+
+
+def pauli_to_choi(
+ pauli_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Pauli-Liouville representation of a quantum channel
+ to its Choi representation :math:`\\Lambda`.
+
+ Args:
+ pauli_op (ndarray): superoperator in the Pauli-Liouville representation.
+ normalize (bool, optional): If ``True`` assumes ``pauli_op`` is represented
+ in the normalized Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, returns Choi representation in
+ row-vectorization. If ``"column"``, returns column-vectorized
+ superoperator. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of the superoperator.
+ """
+ super_op = pauli_to_liouville(
+ pauli_op, normalize, order, pauli_order, backend=backend
+ )
+ super_op = liouville_to_choi(super_op, order, backend=backend)
+
+ return super_op
+
+
+def pauli_to_kraus(
+ pauli_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ precision_tol: Optional[float] = None,
+ backend=None,
+):
+ """Converts Pauli-Liouville representation of a quantum channel
+ to its Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`.
+
+ Args:
+ pauli_op (ndarray): superoperator in the Pauli-Liouville representation.
+ normalize (bool, optional): If ``True`` assumes ``pauli_op`` is represented
+ in the normalized Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): vectorization order of the Liouville and Choi
+ intermediate steps. If ``"row"``, row-vectorizationcis used for both
+ representations. If ``"column"``, column-vectorization is used.
+ Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (ndarray, ndarray): Kraus operators and their coefficients.
+ """
+ super_op = pauli_to_liouville(
+ pauli_op, normalize, order, pauli_order, backend=backend
+ )
+ super_op = liouville_to_kraus(super_op, precision_tol, order, backend=backend)
+
+ return super_op
+
+
+def pauli_to_chi(
+ pauli_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Pauli-Liouville representation of a quantum channel
+ to its :math:`\\chi`-matrix representation.
+
+ Args:
+ pauli_op (ndarray): superoperator in the Pauli-Liouville representation.
+ normalize (bool, optional): If ``True`` assumes ``pauli_op`` is represented
+ in the normalized Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, returns Choi representation in
+ row-vectorization. If ``"column"``, returns column-vectorized
+ superoperator. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Chi-matrix representation of the quantum channel.
+ """
+ super_op = pauli_to_liouville(
+ pauli_op, normalize, order, pauli_order, backend=backend
+ )
+ super_op = liouville_to_chi(
+ super_op, normalize, order, pauli_order, backend=backend
+ )
+
+ return super_op
+
+
+def pauli_to_stinespring(
+ pauli_op,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ precision_tol: Optional[float] = None,
+ validate_cp: bool = True,
+ nqubits: Optional[int] = None,
+ initial_state_env=None,
+ backend=None,
+):
+ """Converts Pauli-Liouville representation :math:`\\mathcal{E}_{P}` of quantum channel
+ to its Stinespring representation :math:`U_{0}`.
+ It uses the Liouville representation :math:`\\mathcal{E}` as intermediate step.
+
+ Args:
+ pauli_op (ndarray): Pauli-Liouville representation of a quantum channel.
+ normalize (bool, optional): If ``True`` assumes ``pauli_op`` is represented
+ in the normalized Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, returns Liouville representation in
+ row-vectorization. If ``"column"``, returns column-vectorized
+ superoperator. If ``"system"``, superoperator will be in
+ block-vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ nqubits (int, optional): total number of qubits in the system that is
+ interacting with the environment. Must be equal or greater than
+ the number of qubits ``kraus_ops`` acts on. If ``None``,
+ defaults to the number of qubits in ``kraus_ops``.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Stinestring representation of quantum channel.
+ """
+ super_op = pauli_to_liouville(
+ pauli_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ stinespring = liouville_to_stinespring(
+ super_op,
+ order=order,
+ precision_tol=precision_tol,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+
+ return stinespring
+
+
+def chi_to_choi(
+ chi_matrix,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Convert the :math:`\\chi`-matrix representation of a quantum channel
+ to its Choi representation :math:`\\Lambda`.
+
+ .. math::
+ \\Lambda = \\text{pauli_to_liouville}(\\chi)
+
+ Args:
+ chi_matrix: Chi-matrix representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of quantum channel.
+ """
+ choi_super_op = pauli_to_liouville(
+ chi_matrix,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return choi_super_op
+
+
+def chi_to_liouville(
+ chi_matrix,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts the :math:`\\chi`-matrix representation of a quantum channel
+ to its Liouville representation :math:`\\mathcal{E}`.
+
+ .. math::
+ \\mathcal{E} = \\text{pauli_to_liouville}(\\text{choi_to_liouville}(\\chi))
+
+ Args:
+ chi_matrix: Chi-matrix representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Liouville representation of quantum channel.
+ """
+ choi_super_op = pauli_to_liouville(
+ chi_matrix,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ super_op = choi_to_liouville(choi_super_op, order=order, backend=backend)
+
+ return super_op
+
+
+def chi_to_pauli(
+ chi_matrix,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Convert :math:`\\chi`-matrix representation of a quantum channel
+ to its Pauli-Liouville representation :math:`\\mathcal{E}_P`.
+
+ .. math::
+ \\mathcal{E}_P = \\text{choi_to_pauli}(\\text{chi_to_choi}(\\chi))
+
+ Args:
+ chi_matrix: Chi-matrix representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: superoperator in the Pauli-Liouville representation.
+ """
+ choi_super_op = pauli_to_liouville(
+ chi_matrix,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ super_op = choi_to_pauli(
+ choi_super_op,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return super_op
+
+
+def chi_to_kraus(
+ chi_matrix,
+ normalize: bool = False,
+ precision_tol: Optional[float] = None,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ validate_cp: bool = True,
+ backend=None,
+):
+ """Converts the :math:`\\chi`-matrix representation of a quantum channel
+ to its Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`.
+
+ .. math::
+ \\mathcal{E}_P = \\text{choi_to_kraus}(\\text{chi_to_choi}(\\chi))
+
+ Args:
+ chi_matrix: Chi-matrix representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (ndarray, ndarray): Kraus operators and their coefficients.
+ """
+ choi_super_op = pauli_to_liouville(
+ chi_matrix,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ kraus_ops, coefficients = choi_to_kraus(
+ choi_super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=validate_cp,
+ backend=backend,
+ )
+
+ return kraus_ops, coefficients
+
+
+def chi_to_stinespring(
+ chi_matrix,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ precision_tol: Optional[float] = None,
+ validate_cp: bool = True,
+ nqubits: Optional[int] = None,
+ initial_state_env=None,
+ backend=None,
+):
+ """Converts :math:`\\chi`-representation of quantum channel
+ to its Stinespring representation :math:`U_{0}`.
+ It uses the Choi representation :math:`\\Lambda` as intermediate step.
+
+ Args:
+ chi_matrix: Chi-matrix representation of quantum channel.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements. Defaults to "IXYZ".
+ precision_tol (float, optional): Precision tolerance for eigenvalues
+ found in the spectral decomposition problem. Any eigenvalue
+ :math:`\\lambda <` ``precision_tol`` is set to 0 (zero).
+ If ``None``, ``precision_tol`` defaults to
+ ``qibo.config.PRECISION_TOL=1e-8``. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ validate_cp (bool, optional): If ``True``, checks if ``choi_super_op``
+ is a completely positive map. If ``False``, it assumes that
+ ``choi_super_op`` is completely positive (and Hermitian).
+ Defaults to ``True``.
+ nqubits (int, optional): total number of qubits in the system that is
+ interacting with the environment. Must be equal or greater than
+ the number of qubits ``kraus_ops`` acts on. If ``None``,
+ defaults to the number of qubits in ``kraus_ops``.
+ Defauts to ``None``.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Stinespring representation of quantum channel.
+ """
+ choi_super_op = chi_to_choi(
+ chi_matrix,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ stinespring = choi_to_stinespring(
+ choi_super_op,
+ precision_tol=precision_tol,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+
+ return stinespring
+
+
+def stinespring_to_choi(
+ stinespring,
+ dim_env: int,
+ initial_state_env=None,
+ nqubits: Optional[int] = None,
+ order: str = "row",
+ backend=None,
+):
+ """Converts Stinespring representation :math:`U_{0}` of quantum channel
+ to its Choi representation :math:`\\Lambda`.
+
+ .. math::
+ \\Lambda = \\text{kraus_to_choi}(\\text{stinespring_to_kraus}(U_{0}))
+
+ Args:
+ stinespring (ndarray): quantum channel in the Stinespring representation.
+ dim_env (int): dimension of the Hilbert space of the environment.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ nqubits (int, optional): number of qubits in the system. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi representation of quantum channel.
+ """
+ kraus_ops = stinespring_to_kraus(
+ stinespring,
+ dim_env,
+ initial_state_env=initial_state_env,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ if nqubits is None:
+ nqubits = int(np.log2(kraus_ops[0].shape[0]))
+
+ nqubits = [tuple(range(nqubits)) for _ in range(len(kraus_ops))]
+
+ kraus_ops = list(zip(nqubits, kraus_ops))
+
+ choi_super_op = kraus_to_choi(kraus_ops, order=order, backend=backend)
+
+ return choi_super_op
+
+
+def stinespring_to_liouville(
+ stinespring,
+ dim_env: int,
+ initial_state_env=None,
+ nqubits: Optional[int] = None,
+ order: str = "row",
+ backend=None,
+):
+ """Converts Stinespring representation :math:`U_{0}` of quantum channel
+ to its Liouville representation :math:`\\mathcal{E}` via Stinespring Dilation,
+ i.e.
+
+ .. math::
+ \\mathcal{E} = \\text{kraus_to_liouville}(\\text{stinespring_to_kraus}(U_{0}))
+
+ Args:
+ stinespring (ndarray): quantum channel in the Stinespring representation.
+ dim_env (int): dimension of the Hilbert space of the environment.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ nqubits (int, optional): number of qubits in the system. Defaults to ``None``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Liouville representation of quantum channel.
+ """
+ kraus_ops = stinespring_to_kraus(
+ stinespring,
+ dim_env,
+ initial_state_env=initial_state_env,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ if nqubits is None:
+ nqubits = int(np.log2(kraus_ops[0].shape[0]))
+
+ nqubits = [tuple(range(nqubits)) for _ in range(len(kraus_ops))]
+
+ kraus_ops = list(zip(nqubits, kraus_ops))
+
+ super_op = kraus_to_liouville(kraus_ops, order=order, backend=backend)
+
+ return super_op
+
+
+def stinespring_to_pauli(
+ stinespring,
+ dim_env: int,
+ initial_state_env=None,
+ nqubits: Optional[int] = None,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Stinespring representation :math:`U_{0}` of quantum channel
+ to its Pauli-Liouville representation :math:`\\mathcal{E}_{P}` via
+ Stinespring Dilation, i.e.
+
+ .. math::
+ \\mathcal{E}_{P} = \\text{kraus_to_pauli}(\\text{stinespring_to_kraus}(U_{0}))
+
+ Args:
+ stinespring (ndarray): quantum channel in the Stinespring representation.
+ dim_env (int): dimension of the Hilbert space of the environment.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ nqubits (int, optional): number of qubits in the system. Defaults to ``None``.
+ normalize (bool, optional): If ``True`` superoperator is returned
+ in the normalized Pauli basis. If ``False``, it is returned
+ in the unnormalized Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, intermediate step for Choi
+ representation is done in row-vectorization. If ``"column"``,
+ step is done in column-vectorization. If ``"system"``,
+ block-vectorization is performed. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4
+ single-qubit Pauli elements. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Pauli-Liouville representation of quantum channel.
+ """
+ kraus_ops = stinespring_to_kraus(
+ stinespring,
+ dim_env,
+ initial_state_env=initial_state_env,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ if nqubits is None:
+ nqubits = int(np.log2(kraus_ops[0].shape[0]))
+
+ nqubits = [tuple(range(nqubits)) for _ in range(len(kraus_ops))]
+
+ kraus_ops = list(zip(nqubits, kraus_ops))
+
+ super_op_pauli = kraus_to_pauli(
+ kraus_ops,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return super_op_pauli
+
+
+def stinespring_to_kraus(
+ stinespring,
+ dim_env: int,
+ initial_state_env=None,
+ nqubits: Optional[int] = None,
+ backend=None,
+):
+ """Converts the Stinespring representation :math:`U_{0}` of quantum channel
+ to its Kraus representation :math:`\\{K_{\\alpha}\\}_{\\alpha}`, i.e.
+
+ .. math::
+ K_{\\alpha} := \\bra{\\alpha} \\, U_{0} \\, \\ket{v_{0}} \\, ,
+
+ where :math:`\\ket{v_{0}}` is the initial state of the environment
+ (``initial_state_env``), :math:`D` is the dimension of the environment's
+ Hilbert space, and
+ :math:`\\{\\ket{\\alpha} \\, : \\, \\alpha = 0, 1, \\cdots, D - 1 \\}`
+ is an orthonormal basis for the environment's Hilbert space.
+ Note that :math:`\\text{dim}(\\ket{\\alpha}) = \\text{dim}(\\ket{v_{0}}) = D`,
+ while :math:`\\text{dim}(U) = 2^{n} \\, D`, where :math:`n` is `nqubits`.
+
+ Args:
+ stinespring (ndarray): quantum channel in the Stinespring representation.
+ dim_env (int): dimension of the Hilbert space of the environment.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ nqubits (int, optional): number of qubits in the system. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Kraus operators.
+ """
+ backend = _check_backend(backend)
+
+ if isinstance(dim_env, int) is False:
+ raise_error(
+ TypeError, f"dim_env must be type int, but it is type {type(dim_env)}."
+ )
+
+ if dim_env <= 0:
+ raise_error(ValueError, "dim_env must be a positive integer.")
+
+ if initial_state_env is not None and len(initial_state_env.shape) != 1:
+ raise_error(ValueError, "initial_state_env must be a statevector.")
+
+ if nqubits is not None:
+ if isinstance(nqubits, int) is False:
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+ if nqubits <= 0:
+ raise_error(ValueError, "nqubits must be a positive integer.")
+
+ dim_stinespring = stinespring.shape[0]
+
+ if nqubits is None:
+ nqubits = int(np.log2(dim_stinespring / dim_env))
+
+ dim = 2**nqubits
+
+ if dim * dim_env != dim_stinespring:
+ raise_error(
+ ValueError,
+ "Dimensions do not match. dim(`stinespring`) must be equal to `dim_env` * 2**nqubits.",
+ )
+
+ if initial_state_env is None:
+ initial_state_env = np.zeros(dim_env, dtype=complex)
+ initial_state_env[0] = 1.0
+ initial_state_env = backend.cast(
+ initial_state_env, dtype=initial_state_env.dtype
+ )
+
+ stinespring = backend.np.reshape(stinespring, (dim, dim_env, dim, dim_env))
+ stinespring = backend.np.swapaxes(stinespring, 1, 2)
+
+ kraus_ops = []
+ for alpha in range(dim_env):
+ vector_alpha = np.zeros(dim_env, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ kraus = backend.np.conj(vector_alpha) @ stinespring @ initial_state_env
+ kraus_ops.append(kraus)
+
+ return kraus_ops
+
+
+def stinespring_to_chi(
+ stinespring,
+ dim_env: int,
+ initial_state_env=None,
+ nqubits: Optional[int] = None,
+ normalize: bool = False,
+ order: str = "row",
+ pauli_order: str = "IXYZ",
+ backend=None,
+):
+ """Converts Stinespring representation :math:`U_{0}` of quantum channel
+ to its :math:`\\chi`-matrix representation via Stinespring Dilation, i.e.
+
+ .. math::
+ \\chi = \\text{kraus_to_chi}(\\text{stinespring_to_kraus}(U_{0}))
+
+ Args:
+ stinespring (ndarray): quantum channel in the Stinespring representation.
+ dim_env (int): dimension of the Hilbert space of the environment.
+ initial_state_env (ndarray, optional): statevector representing the
+ initial state of the enviroment. If ``None``, it assumes the
+ environment in its ground state. Defaults to ``None``.
+ nqubits (int, optional): number of qubits in the system. Defaults to ``None``.
+ normalize (bool, optional): If ``True`` assumes the normalized
+ Pauli basis. If ``False``, it assumes unnormalized
+ Pauli basis. Defaults to ``False``.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ pauli_order (str, optional): corresponds to the order of 4 single-qubit
+ Pauli elements in the Pauli basis. Defaults to "IXYZ".
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: :math:`\\chi`-representation of quantum channel.
+ """
+ kraus_ops = stinespring_to_kraus(
+ stinespring,
+ dim_env,
+ initial_state_env=initial_state_env,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ if nqubits is None:
+ nqubits = int(np.log2(kraus_ops[0].shape[0]))
+
+ nqubits = [tuple(range(nqubits)) for _ in range(len(kraus_ops))]
+
+ kraus_ops = list(zip(nqubits, kraus_ops))
+
+ chi_super_op = kraus_to_chi(
+ kraus_ops,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ return chi_super_op
+
+
+def kraus_to_unitaries(
+ kraus_ops, order: str = "row", precision_tol: Optional[float] = None, backend=None
+):
+ """Tries to convert Kraus operators into a probabilistc sum of unitaries.
+
+ Given a set of Kraus operators :math:`\\{K_{\\alpha}\\}_{\\alpha}`,
+ returns an ensemble :math:`\\{U_{\\alpha}, p_{\\alpha}\\}` that defines
+ an :class:`qibo.gates.channels.UnitaryChannel` that approximates the original
+ channel up to a precision tolerance in Frobenius norm.
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+ order (str, optional): _description_. Defaults to "row".
+ precision_tol (float, optional): Precision tolerance for the minimization
+ of the Frobenius norm :math:`\\| \\mathcal{E}_{K} - \\mathcal{E}_{U} \\|_{F}`,
+ where :math:`\\mathcal{E}_{K}` is the Liouville representation of the Kraus
+ channel :math:`\\{K_{\\alpha}\\}_{\\alpha}`, and :math:`\\mathcal{E}_{U}`
+ is the Liouville representaton of the :class:`qibo.gates.channels.UnitaryChannel`
+ that best approximates the original channel. If ``None``, ``precision_tol``
+ defaults to ``1e-7``. Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (ndarray, ndarray): Unitary operators and their associated probabilities.
+ """
+
+ if precision_tol is None:
+ precision_tol = 10 * PRECISION_TOL
+ else:
+ if not isinstance(precision_tol, float):
+ raise_error(
+ TypeError,
+ f"precision_tol must be type float, but it is type {type(precision_tol)}.",
+ )
+ if precision_tol < 0.0:
+ raise_error(ValueError, "precision_tol must be non-negative.")
+
+ backend = _check_backend(backend)
+
+ target_qubits = [q for q, _ in kraus_ops]
+ nqubits = 1 + np.max(target_qubits)
+ dim = 2**nqubits
+
+ target = kraus_to_liouville(kraus_ops, order=order, backend=backend)
+
+ # QR decomposition
+ unitaries = []
+ for _, kraus in kraus_ops:
+ Q, _ = np.linalg.qr(kraus)
+ unitaries.append(Q)
+ # unitaries = np.array(unitaries)
+ # unitaries = backend.cast(unitaries)
+
+ # unitaries in Liouville representation
+ unitaries_liouville = _individual_kraus_to_liouville(
+ list(zip(target_qubits, unitaries)), backend=backend
+ )
+
+ # function to minimize
+ def function(x0, operators):
+ operator = (1 - np.sum(x0)) * np.eye(dim**2, dtype=complex)
+ operator = backend.cast(operator, dtype=operator.dtype)
+ for prob, oper in zip(x0, operators):
+ operator = operator + prob * oper
+
+ return float(backend.calculate_norm_density_matrix(target - operator, order=2))
+
+ # initial parameters as flat distribution
+ x0 = [1.0 / (len(kraus_ops) + 1)] * len(kraus_ops)
+
+ # final parameters
+ probabilities = minimize(
+ function,
+ x0,
+ args=(unitaries_liouville),
+ options={"return_all": True},
+ )["x"]
+
+ final_norm = function(probabilities, unitaries_liouville)
+ if final_norm > precision_tol:
+ warnings.warn(
+ f"precision in Frobenius norm of {final_norm} is greater then set "
+ + f"precision_tol of {precision_tol}.",
+ Warning,
+ )
+
+ return unitaries, probabilities
+
+
+def _reshuffling(super_op, order: str = "row", backend=None):
+ """Reshuffling operation used to convert Lioville representation
+ of quantum channels to their Choi representation (and vice-versa).
+
+ For an operator :math:`A` with dimensions :math:`d^{2} \\times d^{2}`,
+ the reshuffling operation consists of reshaping :math:`A` as a
+ 4-dimensional tensor, swapping two axes, and reshaping back to a
+ :math:`d^{2} \\times d^{2}` matrix.
+
+ If ``order="row"``, then:
+
+ .. math::
+ A_{\\alpha\\beta, \\, \\gamma\\delta} \\mapsto A_{\\alpha, \\, \\beta, \\,
+ \\gamma, \\, \\delta} \\mapsto A_{\\alpha, \\, \\gamma, \\, \\beta, \\, \\delta}
+ \\mapsto A_{\\alpha\\gamma, \\, \\beta\\delta}
+
+ If ``order="column"``, then:
+
+ .. math::
+ A_{\\alpha\\beta, \\, \\gamma\\delta} \\mapsto A_{\\alpha, \\, \\beta, \\,
+ \\gamma, \\, \\delta} \\mapsto A_{\\delta, \\, \\beta, \\, \\gamma, \\, \\alpha}
+ \\mapsto A_{\\delta\\beta, \\, \\gamma\\alpha}
+
+ Args:
+ super_op (ndarray): Liouville (Choi) representation of a
+ quantum channel.
+ order (str, optional): If ``"row"``, reshuffling is performed
+ with respect to row-wise vectorization. If ``"column"``,
+ reshuffling is performed with respect to column-wise
+ vectorization. If ``"system"``, operator is converted to
+ a representation based on row vectorization, reshuffled,
+ and then converted back to its representation with
+ respect to system-wise vectorization. Defaults to ``"row"``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Choi (Liouville) representation of the quantum channel.
+ """
+ super_op = backend.cast(super_op)
+
+ if not isinstance(order, str):
+ raise_error(TypeError, f"order must be type str, but it is type {type(order)}.")
+
+ orders = ["row", "column", "system"]
+ if order not in orders:
+ raise_error(
+ ValueError,
+ f"order must be either 'row' or 'column' or 'system', but it is {order}.",
+ )
+ del orders
+
+ if order == "system":
+ raise_error(
+ NotImplementedError, "reshuffling not implemented for system vectorization."
+ )
+
+ backend = _check_backend(backend)
+
+ dim = np.sqrt(super_op.shape[0])
+
+ if (
+ super_op.shape[0] != super_op.shape[1]
+ or np.mod(dim, 1) != 0
+ or np.mod(np.log2(int(dim)), 1) != 0
+ ):
+ raise_error(ValueError, "super_op must be of shape (4^n, 4^n)")
+
+ dim = int(dim)
+ super_op = backend.np.reshape(super_op, [dim] * 4)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ super_op = backend.np.swapaxes(super_op, *axes)
+
+ super_op = backend.np.reshape(super_op, [dim**2, dim**2])
+
+ return super_op
+
+
+def _set_gate_and_target_qubits(kraus_ops): # pragma: no cover
+ """Returns Kraus operators as a set of gates acting on
+ their respective ``target qubits``.
+
+ Args:
+ kraus_ops (list): List of Kraus operators as pairs ``(qubits, Ak)``
+ where ``qubits`` refers the qubit ids that :math:`A_k` acts on
+ and :math:`A_k` is the corresponding matrix as a ``np.ndarray``.
+
+ Returns:
+ (tuple, tuple): gates and their respective target qubits.
+ """
+ if isinstance(kraus_ops[0], Gate):
+ gates = tuple(kraus_ops)
+ target_qubits = tuple(
+ sorted({q for gate in kraus_ops for q in gate.target_qubits})
+ )
+ else:
+ gates, qubitset = [], set()
+ for qubits, matrix in kraus_ops:
+ rank = 2 ** len(qubits)
+ shape = tuple(matrix.shape)
+ if shape != (rank, rank):
+ raise_error(
+ ValueError,
+ f"Invalid Kraus operator shape {shape} for "
+ + f"acting on {len(qubits)} qubits.",
+ )
+ qubitset.update(qubits)
+ gates.append(Unitary(matrix, *list(qubits)))
+ gates = tuple(gates)
+ target_qubits = tuple(sorted(qubitset))
+
+ return gates, target_qubits
+
+
+def _individual_kraus_to_liouville(
+ kraus_ops, order: str = "row", backend=None
+): # pragma: no cover
+ """Auxiliary, modified version of :func:`qibo.quantum_info.kraus_to_choi`
+ to be used in :func:`qibo.quantum_info.kraus_to_unitaries`. In principle,
+ this should be not be accessible to users.
+ """
+ backend = _check_backend(backend)
+
+ gates, target_qubits = _set_gate_and_target_qubits(kraus_ops)
+ nqubits = 1 + max(target_qubits)
+
+ super_ops = []
+ for gate in gates:
+ kraus_op = FusedGate(*range(nqubits))
+ kraus_op.append(gate)
+ kraus_op = kraus_op.matrix(backend)
+ kraus_op = vectorization(kraus_op, order=order, backend=backend)
+ kraus_op = backend.np.outer(kraus_op, backend.np.conj(kraus_op))
+ super_ops.append(choi_to_liouville(kraus_op, order=order, backend=backend))
+
+ return super_ops
diff --git a/src/qibo/quantum_info/utils.py b/src/qibo/quantum_info/utils.py
new file mode 100644
index 000000000..ef21f2c97
--- /dev/null
+++ b/src/qibo/quantum_info/utils.py
@@ -0,0 +1,504 @@
+"""Utility functions for the Quantum Information module."""
+
+from functools import reduce
+from itertools import permutations
+from math import factorial
+from re import finditer
+from typing import Optional, Union
+
+import numpy as np
+
+from qibo import matrices
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+
+
+def hamming_weight(
+ bitstring: Union[int, str, list, tuple], return_indexes: bool = False
+):
+ """Calculates the Hamming weight of a bitstring.
+
+ The Hamming weight of a bistring is the number of :math:'1's that the bistring contains.
+
+ Args:
+ bitstring (int or str or tuple or list): bitstring to calculate the
+ weight, either in binary or integer representation.
+ return_indexes (bool, optional): If ``True``, returns the indexes of the
+ non-zero elements. Defaults to ``False``.
+
+ Returns:
+ (int or list): Hamming weight of bitstring or list of indexes of non-zero elements.
+ """
+ if not isinstance(return_indexes, bool):
+ raise_error(
+ TypeError,
+ f"return_indexes must be type bool, but it is type {type(return_indexes)}",
+ )
+
+ if not isinstance(bitstring, (int, str, list, tuple, np.ndarray)):
+ raise_error(
+ TypeError,
+ "bitstring must be either type int, list, tuple, or numpy.ndarray. "
+ f"However, it is type {type(bitstring)}.",
+ )
+
+ if isinstance(bitstring, int):
+ bitstring = f"{bitstring:b}"
+ elif isinstance(bitstring, (list, tuple, np.ndarray)):
+ bitstring = "".join([str(bit) for bit in bitstring])
+
+ indexes = [item.start() for item in finditer("1", bitstring)]
+
+ if return_indexes:
+ return indexes
+
+ return len(indexes)
+
+
+def hamming_distance(
+ bitstring_1: Union[int, str, list, tuple],
+ bitstring_2: Union[int, str, list, tuple],
+ return_indexes: bool = False,
+):
+ """Calculates the Hamming distance between two bistrings.
+
+ This is done by calculating the Hamming weight
+ (:func:`qibo.quantum_info.utils.hamming_weight`) of ``| bitstring_1 - bitstring_2 |``.
+
+ Args:
+ bitstring_1 (int or str or list or tuple): fisrt bistring.
+ bitstring_2 (int or str or list or tuple): second bitstring.
+ return_indexes (bool, optional): If ``True``, returns the indexes of the
+ non-zero elements. Defaults to ``False``.
+
+ Returns:
+ int or list: Hamming distance or list of indexes of non-zero elements.
+ """
+ if not isinstance(return_indexes, bool):
+ raise_error(
+ TypeError,
+ f"return_indexes must be type bool, but it is type {type(return_indexes)}",
+ )
+
+ if not isinstance(bitstring_1, (int, str, list, tuple)):
+ raise_error(
+ TypeError,
+ "bitstring_1 must be either type int, list, tuple, or numpy.ndarray. "
+ f"However, it is type {type(bitstring_1)}.",
+ )
+
+ if not isinstance(bitstring_2, (int, str, list, tuple)):
+ raise_error(
+ TypeError,
+ "bitstring_2 must be either type int, list, tuple, or numpy.ndarray. "
+ f"However, it is type {type(bitstring_2)}.",
+ )
+
+ if isinstance(bitstring_1, (list, tuple)):
+ bitstring_1 = "".join(bitstring_1)
+
+ if isinstance(bitstring_2, (list, tuple)):
+ bitstring_2 = "".join(bitstring_2)
+
+ nbits = max(len(bitstring_1), len(bitstring_2))
+
+ bitstring_1 = "0" * (nbits - len(bitstring_1)) + bitstring_1
+ bitstring_2 = "0" * (nbits - len(bitstring_2)) + bitstring_2
+
+ difference = np.array(list(bitstring_1), dtype=int) - np.array(
+ list(bitstring_2), dtype=int
+ )
+ difference = np.abs(difference)
+ difference = difference.astype(str)
+ difference = "".join(difference)
+
+ return hamming_weight(difference, return_indexes=return_indexes)
+
+
+def hadamard_transform(array, implementation: str = "fast", backend=None):
+ """Calculates the (fast) Hadamard Transform :math:`\\text{HT}` of a
+ :math:`2^{n}`-dimensional vector or :math:`2^{n} \\times 2^{n}` matrix :math:`A`,
+ where :math:`n` is the number of qubits in the system. If :math:`A` is a vector, then
+
+ .. math::
+ \\text{HT}(A) = \\frac{1}{2^{n / 2}} \\, H^{\\otimes n} \\, A \\,
+
+ where :math:`H` is the :class:`qibo.gates.H` gate. If :math:`A` is a matrix, then
+
+ .. math::
+ \\text{HT}(A) = \\frac{1}{2^{n}} \\, H^{\\otimes n} \\, A \\, H^{\\otimes n} \\, .
+
+ Args:
+ array (ndarray): array or matrix.
+ implementation (str, optional): if ``"regular"``, it uses the straightforward
+ implementation of the algorithm with computational complexity of
+ :math:`\\mathcal{O}(2^{2n})` for vectors and :math:`\\mathcal{O}(2^{3n})`
+ for matrices. If ``"fast"``, computational complexity is
+ :math:`\\mathcal{O}(n \\, 2^{n})` in both cases.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
+ in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`.
+ Defaults to ``None``.
+
+ Returns:
+ ndarray: (Fast) Hadamard Transform of ``array``.
+ """
+ backend = _check_backend(backend)
+
+ if (
+ len(array.shape) not in [1, 2]
+ or (len(array.shape) == 1 and np.log2(array.shape[0]).is_integer() is False)
+ or (
+ len(array.shape) == 2
+ and (
+ np.log2(array.shape[0]).is_integer() is False
+ or np.log2(array.shape[1]).is_integer() is False
+ )
+ )
+ ):
+ raise_error(
+ TypeError,
+ f"array must have shape (2**n,) or (2**n, 2**n), but it has shape {array.shape}.",
+ )
+
+ if isinstance(implementation, str) is False:
+ raise_error(
+ TypeError,
+ f"implementation must be type str, but it is type {type(implementation)}.",
+ )
+
+ if implementation not in ["fast", "regular"]:
+ raise_error(
+ ValueError,
+ f"implementation must be either `regular` or `fast`, but it is {implementation}.",
+ )
+
+ if implementation == "regular":
+ nqubits = int(np.log2(array.shape[0]))
+ hadamards = np.real(reduce(np.kron, [matrices.H] * nqubits))
+ hadamards /= 2 ** (nqubits / 2)
+ hadamards = backend.cast(hadamards, dtype=hadamards.dtype)
+
+ array = hadamards @ array
+
+ if len(array.shape) == 2:
+ array = array @ hadamards
+
+ return array
+
+ array = _hadamard_transform_1d(array, backend=backend)
+
+ if len(array.shape) == 2:
+ array = _hadamard_transform_1d(array.T, backend=backend).T
+
+ # needed for the tensorflow backend
+ array = backend.cast(array, dtype=array.dtype)
+
+ return array
+
+
+def hellinger_distance(prob_dist_p, prob_dist_q, validate: bool = False, backend=None):
+ """Calculates the Hellinger distance :math:`H` between two discrete probability distributions.
+
+ For probabilities :math:`\\mathbf{p}` and :math:`\\mathbf{q}`, it is defined as
+
+ .. math::
+ H(\\mathbf{p} \\, , \\, \\mathbf{q}) = \\frac{1}{\\sqrt{2}} \\, \\|
+ \\sqrt{\\mathbf{p}} - \\sqrt{\\mathbf{q}} \\|_{2}
+
+ where :math:`\\|\\cdot\\|_{2}` is the Euclidean norm.
+
+ Args:
+ prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+ prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+ validate (bool, optional): If ``True``, checks if :math:`p` and :math:`q` are proper
+ probability distributions. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (float): Hellinger distance :math:`H(p, q)`.
+ """
+ backend = _check_backend(backend)
+
+ if isinstance(prob_dist_p, list):
+ prob_dist_p = backend.cast(prob_dist_p, dtype=np.float64)
+ if isinstance(prob_dist_q, list):
+ prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64)
+
+ if (len(prob_dist_p.shape) != 1) or (len(prob_dist_q.shape) != 1):
+ raise_error(
+ TypeError,
+ "Probability arrays must have dims (k,) but have "
+ + f"dims {prob_dist_p.shape} and {prob_dist_q.shape}.",
+ )
+
+ if (len(prob_dist_p) == 0) or (len(prob_dist_q) == 0):
+ raise_error(TypeError, "At least one of the arrays is empty.")
+
+ if validate:
+ if (any(prob_dist_p < 0) or any(prob_dist_p > 1.0)) or (
+ any(prob_dist_q < 0) or any(prob_dist_q > 1.0)
+ ):
+ raise_error(
+ ValueError,
+ "All elements of the probability array must be between 0. and 1..",
+ )
+ if backend.np.abs(backend.np.sum(prob_dist_p) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "First probability array must sum to 1.")
+
+ if backend.np.abs(backend.np.sum(prob_dist_q) - 1.0) > PRECISION_TOL:
+ raise_error(ValueError, "Second probability array must sum to 1.")
+
+ distance = float(
+ backend.calculate_norm(
+ backend.np.sqrt(prob_dist_p) - backend.np.sqrt(prob_dist_q)
+ )
+ / np.sqrt(2)
+ )
+
+ return distance
+
+
+def hellinger_fidelity(prob_dist_p, prob_dist_q, validate: bool = False, backend=None):
+ """Calculates the Hellinger fidelity between two discrete probability distributions.
+
+ For probabilities :math:`p` and :math:`q`, the fidelity is defined as
+
+ .. math::
+ (1 - H^{2}(p, q))^{2} \\, ,
+
+ where :math:`H(p, q)` is the :func:`qibo.quantum_info.utils.hellinger_distance`.
+
+ Args:
+ prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+ prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+ validate (bool, optional): if ``True``, checks if :math:`p` and :math:`q` are proper
+ probability distributions. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (float): Hellinger fidelity.
+
+ """
+ backend = _check_backend(backend)
+
+ distance = hellinger_distance(prob_dist_p, prob_dist_q, validate, backend=backend)
+
+ return (1 - distance**2) ** 2
+
+
+def hellinger_shot_error(
+ prob_dist_p, prob_dist_q, nshots: int, validate: bool = False, backend=None
+):
+ """Calculates the Hellinger fidelity error between two discrete probability distributions estimated from finite statistics.
+
+ It is calculated propagating the probability error of each state of the system.
+ The complete formula is:
+
+ .. math::
+ \\frac{1 - H^{2}(p, q)}{\\sqrt{nshots}} \\, \\sum_{k} \\,
+ \\left(\\sqrt{p_{k} \\, (1 - q_{k})} + \\sqrt{q_{k} \\, (1 - p_{k})}\\right)
+
+ where :math:`H(p, q)` is the :func:`qibo.quantum_info.utils.hellinger_distance`,
+ and :math:`1 - H^{2}(p, q)` is the square root of the
+ :func:`qibo.quantum_info.utils.hellinger_fidelity`.
+
+ Args:
+ prob_dist_p (ndarray or list): discrete probability distribution :math:`p`.
+ prob_dist_q (ndarray or list): discrete probability distribution :math:`q`.
+ nshots (int): number of shots we used to run the circuit to obtain :math:`p` and :math:`q`.
+ validate (bool, optional): if ``True``, checks if :math:`p` and :math:`q` are proper
+ probability distributions. Defaults to ``False``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ (float): Hellinger fidelity error.
+
+ """
+ backend = _check_backend(backend)
+
+ if isinstance(prob_dist_p, list):
+ prob_dist_p = backend.cast(prob_dist_p, dtype=np.float64)
+
+ if isinstance(prob_dist_q, list):
+ prob_dist_q = backend.cast(prob_dist_q, dtype=np.float64)
+
+ hellinger_error = hellinger_fidelity(
+ prob_dist_p, prob_dist_q, validate=validate, backend=backend
+ )
+ hellinger_error = np.sqrt(hellinger_error / nshots) * backend.np.sum(
+ np.sqrt(prob_dist_q * (1 - prob_dist_p))
+ + np.sqrt(prob_dist_p * (1 - prob_dist_q))
+ )
+
+ return hellinger_error
+
+
+def haar_integral(
+ nqubits: int,
+ power_t: int,
+ samples: Optional[int] = None,
+ backend=None,
+):
+ """Returns the integral over pure states over the Haar measure.
+
+ .. math::
+ \\int_{\\text{Haar}} d\\psi \\, \\left(|\\psi\\rangle\\right.\\left.
+ \\langle\\psi|\\right)^{\\otimes t}
+
+ Args:
+ nqubits (int): Number of qubits.
+ power_t (int): power that defines the :math:`t`-design.
+ samples (int, optional): If ``None``, estimated the integral exactly.
+ Otherwise, number of samples to estimate the integral via sampling.
+ Defaults to ``None``.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ array: Estimation of the Haar integral.
+
+ .. note::
+ The ``exact=True`` method is implemented using Lemma 34 of
+ `Kliesch and Roth (2020) `_.
+ """
+
+ if isinstance(nqubits, int) is False:
+ raise_error(
+ TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
+ )
+
+ if isinstance(power_t, int) is False:
+ raise_error(
+ TypeError, f"power_t must be type int, but it is type {type(power_t)}."
+ )
+
+ if samples is not None and isinstance(samples, int) is False:
+ raise_error(
+ TypeError, f"samples must be type int, but it is type {type(samples)}."
+ )
+
+ backend = _check_backend(backend)
+
+ dim = 2**nqubits
+
+ if samples is not None:
+ from qibo.quantum_info.random_ensembles import ( # pylint: disable=C0415
+ random_statevector,
+ )
+
+ rand_unit_density = np.zeros((dim**power_t, dim**power_t), dtype=complex)
+ rand_unit_density = backend.cast(
+ rand_unit_density, dtype=rand_unit_density.dtype
+ )
+ for _ in range(samples):
+ haar_state = backend.np.reshape(
+ random_statevector(dim, backend=backend), (-1, 1)
+ )
+
+ rho = haar_state @ backend.np.conj(haar_state).T
+
+ rand_unit_density = rand_unit_density + reduce(
+ backend.np.kron, [rho] * power_t
+ )
+
+ integral = rand_unit_density / samples
+
+ return integral
+
+ normalization = factorial(dim - 1) / factorial(dim - 1 + power_t)
+
+ permutations_list = list(permutations(np.arange(power_t) + power_t))
+ permutations_list = [
+ tuple(np.arange(power_t)) + indices for indices in permutations_list
+ ]
+
+ identity = np.eye(dim**power_t, dtype=float)
+ identity = np.reshape(identity, (dim,) * (2 * power_t))
+ identity = backend.cast(identity, dtype=identity.dtype)
+
+ integral = np.zeros((dim**power_t, dim**power_t), dtype=float)
+ integral = backend.cast(integral, dtype=integral.dtype)
+ for indices in permutations_list:
+ integral = integral + backend.np.reshape(
+ backend.np.transpose(identity, indices), (-1, dim**power_t)
+ )
+ integral = integral * normalization
+
+ return integral
+
+
+def pqc_integral(circuit, power_t: int, samples: int, backend=None):
+ """Returns the integral over pure states generated by uniformly sampling
+ in the parameter space described by a parameterized circuit.
+
+ .. math::
+ \\int_{\\Theta} d\\psi \\, \\left(|\\psi_{\\theta}\\rangle\\right.\\left.
+ \\langle\\psi_{\\theta}|\\right)^{\\otimes t}
+
+ Args:
+ circuit (:class:`qibo.models.Circuit`): Parametrized circuit.
+ power_t (int): power that defines the :math:`t`-design.
+ samples (int): number of samples to estimate the integral.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be
+ used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: Estimation of the integral.
+ """
+
+ if isinstance(power_t, int) is False:
+ raise_error(
+ TypeError, f"power_t must be type int, but it is type {type(power_t)}."
+ )
+
+ if isinstance(samples, int) is False:
+ raise_error(
+ TypeError, f"samples must be type int, but it is type {type(samples)}."
+ )
+
+ backend = _check_backend(backend)
+
+ circuit.density_matrix = True
+ dim = 2**circuit.nqubits
+
+ rand_unit_density = np.zeros((dim**power_t, dim**power_t), dtype=complex)
+ rand_unit_density = backend.cast(rand_unit_density, dtype=rand_unit_density.dtype)
+ for _ in range(samples):
+ params = np.random.uniform(-np.pi, np.pi, circuit.trainable_gates.nparams)
+ circuit.set_parameters(params)
+
+ rho = backend.execute_circuit(circuit).state()
+
+ rand_unit_density = rand_unit_density + reduce(np.kron, [rho] * power_t)
+
+ integral = rand_unit_density / samples
+
+ return integral
+
+
+def _hadamard_transform_1d(array, backend=None):
+ # necessary because of tf.EagerTensor
+ # does not accept item assignment
+ backend = _check_backend(backend)
+ array_copied = backend.np.copy(array)
+
+ indexes = [2**k for k in range(int(np.log2(len(array_copied))))]
+ for index in indexes:
+ for k in range(0, len(array_copied), 2 * index):
+ for j in range(k, k + index):
+ # copy necessary because of cupy backend
+ elem_1 = backend.np.copy(array_copied[j])
+ elem_2 = backend.np.copy(array_copied[j + index])
+ array_copied[j] = elem_1 + elem_2
+ array_copied[j + index] = elem_1 - elem_2
+ array_copied /= 2.0
+
+ return array_copied
diff --git a/src/qibo/result.py b/src/qibo/result.py
new file mode 100644
index 000000000..905cbb8e4
--- /dev/null
+++ b/src/qibo/result.py
@@ -0,0 +1,541 @@
+import collections
+import warnings
+from typing import Optional, Union
+
+import numpy as np
+
+from qibo import __version__, backends, gates
+from qibo.config import raise_error
+from qibo.measurements import apply_bitflips, frequencies_to_binary
+
+
+def load_result(filename: str):
+ """Loads the results of a circuit execution saved to disk.
+
+ Args:
+ filename (str): Path to the file containing the results.
+
+ Returns:
+ :class:`qibo.result.QuantumState` or :class:`qibo.result.MeasurementOutcomes` or :class:`qibo.result.CircuitResult`: result of circuit execution saved to disk, depending on saved filed.
+ """
+ payload = np.load(filename, allow_pickle=True).item()
+ return globals()[payload.pop("dtype")].from_dict(payload)
+
+
+class QuantumState:
+ """Data structure to represent the final state after circuit execution.
+
+ Args:
+ state (np.ndarray): Input quantum state as np.ndarray.
+ backend (qibo.backends.AbstractBackend): Backend used for the calculations. If not provided the :class:`qibo.backends.GlobalBackend` is going to be used.
+ """
+
+ def __init__(self, state, backend=None):
+ from qibo.backends import _check_backend
+
+ self.backend = _check_backend(backend)
+ self.density_matrix = len(state.shape) == 2
+ self.nqubits = int(np.log2(state.shape[0]))
+ self._state = state
+
+ def symbolic(self, decimals: int = 5, cutoff: float = 1e-10, max_terms: int = 20):
+ """Dirac notation representation of the state in the computational basis.
+
+ Args:
+ decimals (int, optional): Number of decimals for the amplitudes.
+ Defaults to :math:`5`.
+ cutoff (float, optional): Amplitudes with absolute value smaller than the
+ cutoff are ignored from the representation. Defaults to ``1e-10``.
+ max_terms (int, optional): Maximum number of terms to print. If the state
+ contains more terms they will be ignored. Defaults to :math:`20`.
+
+ Returns:
+ (str): A string representing the state in the computational basis.
+ """
+ if self.density_matrix:
+ terms = self.backend.calculate_symbolic_density_matrix(
+ self._state, self.nqubits, decimals, cutoff, max_terms
+ )
+ else:
+ terms = self.backend.calculate_symbolic(
+ self._state, self.nqubits, decimals, cutoff, max_terms
+ )
+ return " + ".join(terms)
+
+ def state(self, numpy: bool = False):
+ """State's tensor representation as a backend tensor.
+
+ Args:
+ numpy (bool, optional): If ``True`` the returned tensor will be a ``numpy`` array,
+ otherwise it will follow the backend tensor type.
+ Defaults to ``False``.
+
+ Returns:
+ The state in the computational basis.
+ """
+ if numpy:
+ return np.array(self._state.tolist())
+
+ return self._state
+
+ def probabilities(self, qubits: Optional[Union[list, set]] = None):
+ """Calculates measurement probabilities by tracing out qubits.
+
+ When noisy model is applied to a circuit and `circuit.density_matrix=False`,
+ this method returns the average probability resulting from
+ repeated execution. This probability distribution approximates the
+ exact probability distribution obtained when `circuit.density_matrix=True`.
+
+ Args:
+ qubits (list or set, optional): Set of qubits that are measured.
+ If ``None``, ``qubits`` equates the total number of qubits.
+ Defauts to ``None``.
+ Returns:
+ (np.ndarray): Probabilities over the input qubits.
+ """
+
+ if qubits is None:
+ qubits = tuple(range(self.nqubits))
+
+ if self.density_matrix:
+ return self.backend.calculate_probabilities_density_matrix(
+ self._state, qubits, self.nqubits
+ )
+
+ return self.backend.calculate_probabilities(self._state, qubits, self.nqubits)
+
+ def __str__(self):
+ return self.symbolic()
+
+ def to_dict(self):
+ """Returns a dictonary containinig all the information needed to rebuild the ``QuantumState``"""
+ return {
+ "state": self.state(numpy=True),
+ "dtype": self.__class__.__name__,
+ "qibo": __version__,
+ }
+
+ def dump(self, filename: str):
+ """Writes to file the ``QuantumState`` for future reloading.
+
+ Args:
+ filename (str): Path to the file to write to.
+ """
+ with open(filename, "wb") as f:
+ np.save(f, self.to_dict())
+
+ @classmethod
+ def from_dict(cls, payload: dict):
+ """Builds a ``QuantumState`` object starting from a dictionary.
+
+ Args:
+ payload (dict): Dictionary containing all the information
+ to load the ``QuantumState`` object.
+
+ Returns:
+ :class:`qibo.result.QuantumState`: Quantum state object..
+ """
+ backend = backends.construct_backend("numpy")
+ return cls(payload.get("state"), backend=backend)
+
+ @classmethod
+ def load(cls, filename: str):
+ """Builds the ``QuantumState`` object stored in a file.
+
+ Args:
+ filename (str): Path to the file containing the ``QuantumState``.
+
+ Returns:
+ :class:`qibo.result.QuantumState`: Quantum state object.
+ """
+ payload = np.load(filename, allow_pickle=True).item()
+ return cls.from_dict(payload)
+
+
+class MeasurementOutcomes:
+ """Object to store the outcomes of measurements after circuit execution.
+
+ Args:
+ measurements (:class:`qibo.gates.M`): Measurement gates.
+ backend (:class:`qibo.backends.AbstractBackend`): Backend used for the calculations.
+ If ``None``, then :class:`qibo.backends.GlobalBackend` is used. Defaults to ``None``.
+ probabilities (np.ndarray): Use these probabilities to generate samples and frequencies.
+ samples (np.darray): Use these samples to generate probabilities and frequencies.
+ nshots (int): Number of shots used for samples, probabilities and frequencies generation.
+ """
+
+ def __init__(
+ self,
+ measurements,
+ backend=None,
+ probabilities=None,
+ samples: Optional[int] = None,
+ nshots: int = 1000,
+ ):
+ self.backend = backend
+ self.measurements = measurements
+ self.nshots = nshots
+
+ self._measurement_gate = None
+ self._probs = probabilities
+ self._samples = samples
+ self._frequencies = None
+ self._repeated_execution_frequencies = None
+
+ if samples is not None:
+ for m in measurements:
+ indices = [self.measurement_gate.qubits.index(q) for q in m.qubits]
+ m.result.register_samples(samples[:, indices])
+
+ def frequencies(self, binary: bool = True, registers: bool = False):
+ """Returns the frequencies of measured samples.
+
+ Args:
+ binary (bool, optional): Return frequency keys in binary or decimal form.
+ registers (bool, optional): Group frequencies according to registers.
+
+ Returns:
+ A `collections.Counter` where the keys are the observed values
+ and the values the corresponding frequencies, that is the number
+ of times each measured value/bitstring appears.
+
+ If ``binary`` is ``True``
+ the keys of the `Counter` are in binary form, as strings of
+ :math:`0`s and :math`1`s.
+ If ``binary`` is ``False``
+ the keys of the ``Counter`` are integers.
+ If ``registers`` is ``True``
+ a `dict` of `Counter` s is returned where keys are the name of
+ each register.
+ If ``registers`` is ``False``
+ a single ``Counter`` is returned which contains samples from all
+ the measured qubits, independently of their registers.
+ """
+ qubits = self.measurement_gate.qubits
+
+ if self._repeated_execution_frequencies is not None:
+ if binary:
+ return self._repeated_execution_frequencies
+
+ return collections.Counter(
+ {int(k, 2): v for k, v in self._repeated_execution_frequencies.items()}
+ )
+
+ if self._frequencies is None:
+ if self.measurement_gate.has_bitflip_noise() and not self.has_samples():
+ self._samples = self.samples()
+ if not self.has_samples():
+ # generate new frequencies
+ self._frequencies = self.backend.sample_frequencies(
+ self._probs, self.nshots
+ )
+ # register frequencies to individual gate ``MeasurementResult``
+ qubit_map = {q: i for i, q in enumerate(qubits)}
+ reg_frequencies = {}
+ binary_frequencies = frequencies_to_binary(
+ self._frequencies, len(qubits)
+ )
+ for gate in self.measurements:
+ rfreqs = collections.Counter()
+ for bitstring, freq in binary_frequencies.items():
+ idx = 0
+ rqubits = gate.target_qubits
+ for i, q in enumerate(rqubits):
+ if int(bitstring[qubit_map.get(q)]):
+ idx += 2 ** (len(rqubits) - i - 1)
+ rfreqs[idx] += freq
+ gate.result.register_frequencies(rfreqs, self.backend)
+ else:
+ self._frequencies = self.backend.calculate_frequencies(
+ self.samples(binary=False)
+ )
+
+ if registers:
+ return {
+ gate.register_name: gate.result.frequencies(binary)
+ for gate in self.measurements
+ }
+
+ if binary:
+ return frequencies_to_binary(self._frequencies, len(qubits))
+
+ return self._frequencies
+
+ def probabilities(self, qubits: Optional[Union[list, set]] = None):
+ """Calculate the probabilities as frequencies / nshots
+
+ Returns:
+ The array containing the probabilities of the measured qubits.
+ """
+ nqubits = len(self.measurement_gate.qubits)
+ if qubits is None:
+ qubits = range(nqubits)
+ else:
+ if not set(qubits).issubset(self.measurement_gate.qubits):
+ raise_error(
+ RuntimeError,
+ f"Asking probabilities for qubits {qubits}, but only qubits {self.measurement_gate.qubits} were measured.",
+ )
+ qubits = [self.measurement_gate.qubits.index(q) for q in qubits]
+
+ if self._probs is not None and not self.measurement_gate.has_bitflip_noise():
+ return self.backend.calculate_probabilities(
+ np.sqrt(self._probs), qubits, nqubits
+ )
+
+ probs = [0 for _ in range(2**nqubits)]
+ for state, freq in self.frequencies(binary=False).items():
+ probs[state] = freq / self.nshots
+ probs = self.backend.cast(probs)
+ self._probs = probs
+ return self.backend.calculate_probabilities(
+ self.backend.np.sqrt(probs), qubits, nqubits
+ )
+
+ def has_samples(self):
+ """Check whether the samples are available already.
+
+ Returns:
+ (bool): ``True`` if the samples are available, ``False`` otherwise.
+ """
+ return self.measurements[0].result.has_samples() or self._samples is not None
+
+ def samples(self, binary: bool = True, registers: bool = False):
+ """Returns raw measurement samples.
+
+ Args:
+ binary (bool, optional): Return samples in binary or decimal form.
+ registers (bool, optional): Group samples according to registers.
+
+ Returns:
+ If ``binary`` is ``True``
+ samples are returned in binary form as a tensor
+ of shape ``(nshots, n_measured_qubits)``.
+ If ``binary`` is ``False``
+ samples are returned in decimal form as a tensor
+ of shape ``(nshots,)``.
+ If ``registers`` is ``True``
+ samples are returned in a ``dict`` where the keys are the register
+ names and the values are the samples tensors for each register.
+ If ``registers`` is ``False``
+ a single tensor is returned which contains samples from all the
+ measured qubits, independently of their registers.
+ """
+ qubits = self.measurement_gate.target_qubits
+ if self._samples is None:
+ if self.measurements[0].result.has_samples():
+ self._samples = self.backend.np.concatenate(
+ [
+ self.backend.cast(gate.result.samples())
+ for gate in self.measurements
+ ],
+ axis=1,
+ )
+ else:
+ if self._frequencies is not None:
+ # generate samples that respect the existing frequencies
+ frequencies = self.frequencies(binary=False)
+ samples = np.concatenate(
+ [np.repeat(x, f) for x, f in frequencies.items()]
+ )
+ np.random.shuffle(samples)
+ else:
+ # generate new samples
+ samples = self.backend.sample_shots(self._probs, self.nshots)
+ samples = self.backend.samples_to_binary(samples, len(qubits))
+ if self.measurement_gate.has_bitflip_noise():
+ p0, p1 = self.measurement_gate.bitflip_map
+ bitflip_probabilities = [
+ [p0.get(q) for q in qubits],
+ [p1.get(q) for q in qubits],
+ ]
+ samples = self.backend.apply_bitflips(
+ samples, bitflip_probabilities
+ )
+ # register samples to individual gate ``MeasurementResult``
+ qubit_map = {
+ q: i for i, q in enumerate(self.measurement_gate.target_qubits)
+ }
+ self._samples = samples
+ for gate in self.measurements:
+ rqubits = tuple(qubit_map.get(q) for q in gate.target_qubits)
+ gate.result.register_samples(
+ self._samples[:, rqubits], self.backend
+ )
+
+ if registers:
+ return {
+ gate.register_name: gate.result.samples(binary)
+ for gate in self.measurements
+ }
+
+ if binary:
+ return self._samples
+
+ return self.backend.samples_to_decimal(self._samples, len(qubits))
+
+ @property
+ def measurement_gate(self):
+ """Single measurement gate containing all measured qubits.
+
+ Useful for sampling all measured qubits at once when simulating.
+ """
+ if self._measurement_gate is None:
+ for gate in self.measurements:
+ if self._measurement_gate is None:
+ self._measurement_gate = gates.M(
+ *gate.init_args, **gate.init_kwargs
+ )
+ else:
+ self._measurement_gate.add(gate)
+
+ return self._measurement_gate
+
+ def apply_bitflips(self, p0: float, p1: Optional[float] = None):
+ """Apply bitflips to the measurements with probabilities `p0` and `p1`
+
+ Args:
+ p0 (float): Probability of the 0->1 flip.
+ p1 (float): Probability of the 1->0 flip.
+ """
+ return apply_bitflips(self, p0, p1)
+
+ def expectation_from_samples(self, observable):
+ """Computes the real expectation value of a diagonal observable from frequencies.
+
+ Args:
+ observable (Hamiltonian/SymbolicHamiltonian): diagonal observable in the
+ computational basis.
+
+ Returns:
+ (float): expectation value from samples.
+ """
+ freq = self.frequencies(binary=True)
+ qubit_map = self.measurement_gate.qubits
+ return observable.expectation_from_samples(freq, qubit_map)
+
+ def to_dict(self):
+ """Returns a dictonary containinig all the information needed to rebuild the :class:`qibo.result.MeasurementOutcomes`."""
+ args = {
+ "measurements": [m.to_json() for m in self.measurements],
+ "probabilities": self._probs,
+ "samples": self._samples,
+ "nshots": self.nshots,
+ "dtype": self.__class__.__name__,
+ "qibo": __version__,
+ }
+ return args
+
+ def dump(self, filename: str):
+ """Writes to file the :class:`qibo.result.MeasurementOutcomes` for future reloading.
+
+ Args:
+ filename (str): Path to the file to write to.
+ """
+ with open(filename, "wb") as f:
+ np.save(f, self.to_dict())
+
+ @classmethod
+ def from_dict(cls, payload: dict):
+ """Builds a :class:`qibo.result.MeasurementOutcomes` object starting from a dictionary.
+
+ Args:
+ payload (dict): Dictionary containing all the information to load the :class:`qibo.result.MeasurementOutcomes` object.
+
+ Returns:
+ A :class:`qibo.result.MeasurementOutcomes` object.
+ """
+ from qibo.backends import construct_backend
+
+ if payload["probabilities"] is not None and payload["samples"] is not None:
+ warnings.warn(
+ "Both `probabilities` and `samples` found, discarding the `probabilities` and building out of the `samples`."
+ )
+ payload.pop("probabilities")
+ backend = construct_backend("numpy")
+ measurements = [gates.M.load(m) for m in payload.get("measurements")]
+ return cls(
+ measurements,
+ backend=backend,
+ probabilities=payload.get("probabilities"),
+ samples=payload.get("samples"),
+ nshots=payload.get("nshots"),
+ )
+
+ @classmethod
+ def load(cls, filename: str):
+ """Builds the :class:`qibo.result.MeasurementOutcomes` object stored in a file.
+
+ Args:
+ filename (str): Path to the file containing the :class:`qibo.result.MeasurementOutcomes`.
+
+ Returns:
+ A :class:`qibo.result.MeasurementOutcomes` object.
+ """
+ payload = np.load(filename, allow_pickle=True).item()
+ return cls.from_dict(payload)
+
+
+class CircuitResult(QuantumState, MeasurementOutcomes):
+ """Object to store both the outcomes of measurements and the final state after circuit execution.
+
+ Args:
+ final_state (np.ndarray): Input quantum state as np.ndarray.
+ measurements (qibo.gates.M): The measurement gates containing the measurements.
+ backend (qibo.backends.AbstractBackend): Backend used for the calculations. If not provided the :class:`qibo.backends.GlobalBackend` is going to be used.
+ probabilities (np.ndarray): Use these probabilities to generate samples and frequencies.
+ samples (np.darray): Use these samples to generate probabilities and frequencies.
+ nshots (int): Number of shots used for samples, probabilities and frequencies generation.
+ """
+
+ def __init__(
+ self, final_state, measurements, backend=None, samples=None, nshots=1000
+ ):
+ QuantumState.__init__(self, final_state, backend)
+ qubits = [q for m in measurements for q in m.target_qubits]
+ if len(qubits) == 0:
+ raise ValueError(
+ "Circuit does not contain measurements. Use a `QuantumState` instead."
+ )
+ probs = QuantumState.probabilities(self, qubits) if samples is None else None
+ MeasurementOutcomes.__init__(
+ self,
+ measurements,
+ backend=backend,
+ probabilities=probs,
+ samples=samples,
+ nshots=nshots,
+ )
+
+ def probabilities(self, qubits: Optional[Union[list, set]] = None):
+ if self.measurement_gate.has_bitflip_noise():
+ return MeasurementOutcomes.probabilities(self, qubits)
+ return QuantumState.probabilities(self, qubits)
+
+ def to_dict(self):
+ """Returns a dictonary containinig all the information needed to rebuild the ``CircuitResult``."""
+ args = MeasurementOutcomes.to_dict(self)
+ args.update(QuantumState.to_dict(self))
+ args.update({"dtype": self.__class__.__name__})
+ return args
+
+ @classmethod
+ def from_dict(cls, payload: dict):
+ """Builds a ``CircuitResult`` object starting from a dictionary.
+
+ Args:
+ payload (dict): Dictionary containing all the information to load the ``CircuitResult`` object.
+
+ Returns:
+ :class:`qibo.result.CircuitResult`: circuit result object.
+ """
+ state_load = {"state": payload.pop("state")}
+ state = QuantumState.from_dict(state_load)
+ measurements = MeasurementOutcomes.from_dict(payload)
+ return cls(
+ state.state(),
+ measurements.measurements,
+ backend=state.backend,
+ samples=measurements.samples(),
+ nshots=measurements.nshots,
+ )
diff --git a/src/qibo/solvers.py b/src/qibo/solvers.py
new file mode 100644
index 000000000..69ab0d362
--- /dev/null
+++ b/src/qibo/solvers.py
@@ -0,0 +1,156 @@
+from qibo.config import raise_error
+from qibo.hamiltonians.abstract import AbstractHamiltonian
+from qibo.hamiltonians.adiabatic import BaseAdiabaticHamiltonian
+from qibo.hamiltonians.hamiltonians import SymbolicHamiltonian
+
+
+class BaseSolver:
+ """Basic solver that should be inherited by all solvers.
+
+ Args:
+ dt (float): Time step size.
+ hamiltonian (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Hamiltonian object
+ that the state evolves under.
+ """
+
+ def __init__(self, dt, hamiltonian):
+ self.dt = dt
+ if isinstance(hamiltonian, AbstractHamiltonian):
+ self.backend = hamiltonian.backend
+ self.hamiltonian = lambda t: hamiltonian
+ else:
+ self.backend = hamiltonian(0).backend
+ self.hamiltonian = hamiltonian
+ self.t = 0
+
+ @property
+ def t(self):
+ """Solver's current time."""
+ return self._t
+
+ @t.setter
+ def t(self, new_t):
+ """Updates solver's current time."""
+ self._t = new_t
+ self.current_hamiltonian = self.hamiltonian(self.t)
+
+ def __call__(self, state): # pragma: no cover
+ # abstract method
+ raise_error(NotImplementedError)
+
+
+class TrotterizedExponential(BaseSolver):
+ """Solver that uses Trotterized exponentials.
+
+ Created automatically from the :class:`qibo.solvers.Exponential` if the
+ given Hamiltonian object is a
+ :class:`qibo.hamiltonians.hamiltonians.TrotterHamiltonian`.
+ """
+
+ def __init__(self, dt, hamiltonian):
+ super().__init__(dt, hamiltonian)
+ if isinstance(self.hamiltonian, BaseAdiabaticHamiltonian):
+ self.circuit = lambda t, dt: self.hamiltonian.circuit(self.dt, t=self.t)
+ else:
+ self.circuit = lambda t, dt: self.hamiltonian(self.t).circuit(self.dt)
+
+ def __call__(self, state):
+ circuit = self.circuit(self.t, self.dt)
+ self.t += self.dt
+ result = self.backend.execute_circuit(circuit, initial_state=state)
+ return result.state()
+
+
+class Exponential(BaseSolver):
+ """Solver that uses the matrix exponential of the Hamiltonian:
+
+ .. math::
+ U(t) = e^{-i H(t) \\delta t}
+
+ Calculates the evolution operator in every step and thus is compatible with
+ time-dependent Hamiltonians.
+ """
+
+ def __call__(self, state):
+ propagator = self.current_hamiltonian.exp(self.dt)
+ self.t += self.dt
+ return (propagator @ state[:, None])[:, 0]
+
+
+class RungeKutta4(BaseSolver):
+ """Solver based on the 4th order Runge-Kutta method."""
+
+ def __call__(self, state):
+ ham1 = self.current_hamiltonian
+ ham2 = self.hamiltonian(self.t + self.dt / 2.0)
+ ham3 = self.hamiltonian(self.t + self.dt)
+ k1 = ham1 @ state
+ k2 = ham2 @ (state + self.dt * k1 / 2.0)
+ k3 = ham2 @ (state + self.dt * k2 / 2.0)
+ k4 = ham3 @ (state + self.dt * k3)
+ self.t += self.dt
+ return state - 1j * self.dt * (k1 + 2 * k2 + 2 * k3 + k4) / 6.0
+
+
+class RungeKutta45(BaseSolver):
+ """Solver based on the 5th order Runge-Kutta method."""
+
+ def __call__(self, state):
+ ham1 = self.current_hamiltonian
+ ham2 = self.hamiltonian(self.t + self.dt / 4.0)
+ ham3 = self.hamiltonian(self.t + 3 * self.dt / 8.0)
+ ham4 = self.hamiltonian(self.t + 12 * self.dt / 13.0)
+ ham5 = self.hamiltonian(self.t + self.dt)
+ ham6 = self.hamiltonian(self.t + self.dt / 2.0)
+ k1 = ham1 @ state
+ k2 = ham2 @ (state + self.dt * k1 / 4.0)
+ k3 = ham3 @ (state + self.dt * (3 * k1 + 9 * k2) / 32.0)
+ k4 = ham4 @ (state + self.dt * (1932 * k1 - 7200 * k2 + 7296 * k3) / 2197.0)
+ k5 = ham5 @ (
+ state
+ + self.dt
+ * (439 * k1 / 216.0 - 8 * k2 + 3680 * k3 / 513.0 - 845 * k4 / 4104.0)
+ )
+ k6 = ham6 @ (
+ state
+ + self.dt
+ * (
+ -8 * k1 / 27.0
+ + 2 * k2
+ - 3544 * k3 / 2565
+ + 1859 * k4 / 4104
+ - 11 * k5 / 40.0
+ )
+ )
+ self.t += self.dt
+ return state - 1j * self.dt * (
+ 16 * k1 / 135.0
+ + 6656 * k3 / 12825.0
+ + 28561 * k4 / 56430.0
+ - 9 * k5 / 50.0
+ + 2 * k6 / 55.0
+ )
+
+
+def get_solver(solver_name, dt, hamiltonian):
+ if solver_name == "exp":
+ if isinstance(hamiltonian, AbstractHamiltonian):
+ h0 = hamiltonian
+ elif isinstance(hamiltonian, BaseAdiabaticHamiltonian):
+ h0 = hamiltonian.h0
+ else:
+ h0 = hamiltonian(0)
+
+ if isinstance(h0, SymbolicHamiltonian):
+ return TrotterizedExponential(dt, hamiltonian)
+ else:
+ return Exponential(dt, hamiltonian)
+
+ elif solver_name == "rk4":
+ return RungeKutta4(dt, hamiltonian)
+
+ elif solver_name == "rk45":
+ return RungeKutta45(dt, hamiltonian)
+
+ else: # pragma: no cover
+ raise_error(ValueError, f"Unknown solver {solver_name}.")
diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py
new file mode 100644
index 000000000..9aa12a1dc
--- /dev/null
+++ b/src/qibo/symbols.py
@@ -0,0 +1,163 @@
+import numpy as np
+import sympy
+
+from qibo import gates
+from qibo.backends import matrices
+from qibo.config import raise_error
+
+
+class Symbol(sympy.Symbol):
+ """Qibo specialization for ``sympy`` symbols.
+
+ These symbols can be used to create :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian`.
+ See :ref:`How to define custom Hamiltonians using symbols? `
+ for more details.
+
+ Example:
+ .. testcode::
+
+ from qibo import hamiltonians
+ from qibo.symbols import X, Y, Z
+ # construct a XYZ Hamiltonian on two qubits using Qibo symbols
+ form = X(0) * X(1) + Y(0) * Y(1) + Z(0) * Z(1)
+ ham = hamiltonians.SymbolicHamiltonian(form)
+
+ Args:
+ q (int): Target qubit id.
+ matrix (np.ndarray): 2x2 matrix represented by this symbol.
+ name (str): Name of the symbol which defines how it is represented in
+ symbolic expressions.
+ commutative (bool): If ``True`` the constructed symbols commute with
+ each other. Default is ``False``.
+ This argument should be used with caution because quantum operators
+ are not commutative objects and therefore switching this to ``True``
+ may lead to wrong results. It is useful for improving performance
+ in symbolic calculations in cases where the user is sure that
+ the operators participating in the Hamiltonian form are commuting
+ (for example when the Hamiltonian consists of Z terms only).
+ """
+
+ def __new__(cls, q, matrix=None, name="Symbol", commutative=False, **assumptions):
+ name = f"{name}{q}"
+ assumptions["commutative"] = commutative
+ return super().__new__(cls=cls, name=name, **assumptions)
+
+ def __init__(self, q, matrix=None, name="Symbol", commutative=False):
+ self.target_qubit = q
+ self._gate = None
+ if not (
+ matrix is None
+ or isinstance(matrix, np.ndarray)
+ or isinstance(
+ matrix,
+ (
+ int,
+ float,
+ complex,
+ np.int32,
+ np.int64,
+ np.float32,
+ np.float64,
+ np.complex64,
+ np.complex128,
+ ),
+ )
+ ):
+ raise_error(TypeError, f"Invalid type {type(matrix)} of symbol matrix.")
+ self.matrix = matrix
+
+ def __getstate__(self):
+ return {
+ "target_qubit": self.target_qubit,
+ "matrix": self.matrix,
+ "name": self.name,
+ }
+
+ def __setstate__(self, data):
+ self.target_qubit = data.get("target_qubit")
+ self.matrix = data.get("matrix")
+ self.name = data.get("name")
+ self._gate = None
+
+ @property
+ def gate(self):
+ """Qibo gate that implements the action of the symbol on states."""
+ if self._gate is None:
+ self._gate = self.calculate_gate()
+ return self._gate
+
+ def calculate_gate(self): # pragma: no cover
+ return gates.Unitary(self.matrix, self.target_qubit)
+
+ def full_matrix(self, nqubits):
+ """Calculates the full dense matrix corresponding to the symbol as part of a bigger system.
+
+ Args:
+ nqubits (int): Total number of qubits in the system.
+
+ Returns:
+ Matrix of dimension (2^nqubits, 2^nqubits) composed of the Kronecker
+ product between identities and the symbol's single-qubit matrix.
+ """
+ from qibo.hamiltonians.models import multikron
+
+ matrix_list = self.target_qubit * [matrices.I]
+ matrix_list.append(self.matrix)
+ n = nqubits - self.target_qubit - 1
+ matrix_list.extend(matrices.I for _ in range(n))
+ return multikron(matrix_list)
+
+
+class PauliSymbol(Symbol):
+ def __new__(cls, q, commutative=False, **assumptions):
+ matrix = getattr(matrices, cls.__name__)
+ return super().__new__(cls, q, matrix, cls.__name__, commutative, **assumptions)
+
+ def __init__(self, q, commutative=False):
+ name = self.__class__.__name__
+ matrix = getattr(matrices, name)
+ super().__init__(q, matrix, name, commutative)
+
+ def calculate_gate(self):
+ name = self.__class__.__name__
+ return getattr(gates, name)(self.target_qubit)
+
+
+class I(PauliSymbol):
+ """Qibo symbol for the identity operator.
+
+ Args:
+ q (int): Target qubit id.
+ """
+
+ pass
+
+
+class X(PauliSymbol):
+ """Qibo symbol for the Pauli-X operator.
+
+ Args:
+ q (int): Target qubit id.
+ """
+
+ pass
+
+
+class Y(PauliSymbol):
+ """Qibo symbol for the Pauli-X operator.
+
+ Args:
+ q (int): Target qubit id.
+ """
+
+ pass
+
+
+class Z(PauliSymbol):
+ """Qibo symbol for the Pauli-X operator.
+
+ Args:
+ q (int): Target qubit id.
+ """
+
+ pass
diff --git a/src/qibo/tomography/__init__.py b/src/qibo/tomography/__init__.py
new file mode 100644
index 000000000..c6a0c1862
--- /dev/null
+++ b/src/qibo/tomography/__init__.py
@@ -0,0 +1 @@
+from qibo.tomography.gate_set_tomography import *
diff --git a/src/qibo/tomography/gate_set_tomography.py b/src/qibo/tomography/gate_set_tomography.py
new file mode 100644
index 000000000..127350953
--- /dev/null
+++ b/src/qibo/tomography/gate_set_tomography.py
@@ -0,0 +1,338 @@
+from functools import cache
+from inspect import signature
+from itertools import product
+from random import Random
+from typing import List, Union
+
+import numpy as np
+from sympy import S
+
+from qibo import Circuit, gates, symbols
+from qibo.backends import _check_backend
+from qibo.config import raise_error
+from qibo.hamiltonians import SymbolicHamiltonian
+from qibo.transpiler.optimizer import Preprocessing
+from qibo.transpiler.pipeline import Passes
+from qibo.transpiler.placer import Random
+from qibo.transpiler.router import Sabre
+from qibo.transpiler.unroller import NativeGates, Unroller
+
+SUPPORTED_NQUBITS = [1, 2]
+"""Supported nqubits for GST."""
+
+
+def _check_nqubits(nqubits):
+ if nqubits not in SUPPORTED_NQUBITS:
+ raise_error(
+ ValueError,
+ f"nqubits given as {nqubits}. nqubits needs to be either 1 or 2.",
+ )
+
+
+@cache
+def _gates(nqubits) -> List:
+ """Gates implementing all the GST state preparations.
+
+ Args:
+ nqubits (int): Number of qubits for the circuit.
+ Returns:
+ List(:class:`qibo.gates.Gate`): gates used to prepare the possible states.
+ """
+
+ return list(
+ product(
+ [(gates.I,), (gates.X,), (gates.H,), (gates.H, gates.S)], repeat=nqubits
+ )
+ )
+
+
+@cache
+def _measurements(nqubits: int) -> List:
+ """Measurement gates implementing all the GST measurement bases.
+
+ Args:
+ nqubits (int): Number of qubits for the circuit.
+ Returns:
+ List(:class:`qibo.gates.Gate`): gates implementing the possible measurement bases.
+ """
+
+ return list(product([gates.Z, gates.X, gates.Y, gates.Z], repeat=nqubits))
+
+
+@cache
+def _observables(nqubits: int) -> List:
+ """All the observables measured in the GST protocol.
+
+ Args:
+ nqubits (int): number of qubits for the circuit.
+
+ Returns:
+ List[:class:`qibo.symbols.Symbol`]: all possible observables to be measured.
+ """
+
+ return list(product([symbols.I, symbols.Z, symbols.Z, symbols.Z], repeat=nqubits))
+
+
+@cache
+def _get_observable(j: int, nqubits: int):
+ """Returns the :math:`j`-th observable. The :math:`j`-th observable is expressed as a base-4 indexing and is given by
+
+ .. math::
+ j \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ I, X, Y, Z\\}^{\\otimes n}.
+
+ Args:
+ j (int): index of the measurement basis (in base-4)
+ nqubits (int): number of qubits.
+
+ Returns:
+ List[:class:`qibo.hamiltonians.SymbolicHamiltonian`]: observables represented by symbolic Hamiltonians.
+ """
+
+ if j == 0:
+ _check_nqubits(nqubits)
+ observables = _observables(nqubits)[j]
+ observable = S(1)
+ for q, obs in enumerate(observables):
+ if obs is not symbols.I:
+ observable *= obs(q)
+ return SymbolicHamiltonian(observable, nqubits=nqubits)
+
+
+@cache
+def _prepare_state(k, nqubits):
+ """Prepares the :math:`k`-th state for an :math:`n`-qubits (`nqubits`) circuit.
+ Using base-4 indexing for :math:`k`,
+
+ .. math::
+ k \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ 0\\rangle\\langle0|, |1\\rangle\\langle1|,
+ |+\\rangle\\langle +|, |y+\\rangle\\langle y+|\\}^{\\otimes n}.
+
+ Args:
+ k (int): index of the state to be prepared.
+ nqubits (int): Number of qubits.
+
+ Returns:
+ list(:class:`qibo.gates.Gate`): gates that prepare the :math:`k`-th state.
+ """
+
+ _check_nqubits(nqubits)
+ gates = _gates(nqubits)[k]
+ return [gate(q) for q in range(len(gates)) for gate in gates[q]]
+
+
+@cache
+def _measurement_basis(j, nqubits):
+ """Constructs the :math:`j`-th measurement basis element for an :math:`n`-qubits (`nqubits`) circuit.
+ Base-4 indexing is used for the :math:`j`-th measurement basis and is given by
+
+ .. math::
+ j \\in \\{0, 1, 2, 3\\}^{\\otimes n} \\equiv \\{ I, X, Y, Z\\}^{\\otimes n}.
+
+ Args:
+ j (int): index of the measurement basis element.
+ nqubits (int): number of qubits.
+
+ Returns:
+ List[:class:`qibo.gates.Gate`]: gates forming the :math:`j`-th element
+ of the Pauli measurement basis.
+ """
+
+ _check_nqubits(nqubits)
+ measurements = _measurements(nqubits)[j]
+ return [gates.M(q, basis=measurements[q]) for q in range(len(measurements))]
+
+
+def _gate_tomography(
+ nqubits: int,
+ gate: gates.Gate = None,
+ nshots: int = int(1e4),
+ noise_model=None,
+ backend=None,
+ transpiler=None,
+):
+ """Runs gate tomography for a 1 or 2 qubit gate.
+
+ It obtains a :math:`4^{n} \\times 4^{n}` matrix, where :math:`n` is the number of qubits.
+ This matrix needs to be post-processed to get the Pauli-Liouville representation of the gate.
+ The matrix has elements :math:`\\text{tr}(M_{j} \\, \\rho_{k})` or
+ :math:`\\text{tr}(M_{j} \\, O_{l} \\rho_{k})`, depending on whether the gate
+ :math:`O_{l}` is present or not.
+
+ Args:
+ nqubits (int): number of qubits of the gate.
+ gate (:class:`qibo.gates.Gate`, optional): gate to perform gate tomography on.
+ If ``None``, then gate tomography will be performed for an empty circuit.
+ Defaults to ``None``.
+ nshots (int, optional): number of shots used.
+ noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model applied to simulate
+ noisy computations.
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+ Returns:
+ ndarray: matrix approximating the input gate.
+ """
+
+ # Check if gate is 1 or 2 qubit gate.
+ _check_nqubits(nqubits)
+
+ backend = _check_backend(backend)
+
+ if gate is not None:
+ if nqubits != len(gate.qubits):
+ raise_error(
+ ValueError,
+ f"Mismatched inputs: nqubits given as {nqubits}. {gate} is a {len(gate.qubits)}-qubit gate.",
+ )
+ gate = gate.__class__(*gate.qubits, **gate.init_kwargs)
+
+ # GST for empty circuit or with gates
+ matrix_jk = np.zeros((4**nqubits, 4**nqubits))
+ for k in range(4**nqubits):
+ circ = Circuit(nqubits, density_matrix=True)
+ circ.add(_prepare_state(k, nqubits))
+
+ if gate is not None:
+ circ.add(gate)
+
+ for j in range(4**nqubits):
+ if j == 0:
+ exp_val = 1.0
+ else:
+ new_circ = circ.copy()
+ measurements = _measurement_basis(j, nqubits)
+ new_circ.add(measurements)
+ observable = _get_observable(j, nqubits)
+ if noise_model is not None and backend.name != "qibolab":
+ new_circ = noise_model.apply(new_circ)
+ if transpiler is not None:
+ new_circ, _ = transpiler(new_circ)
+ exp_val = observable.expectation_from_samples(
+ backend.execute_circuit(new_circ, nshots=nshots).frequencies()
+ )
+ matrix_jk[j, k] = exp_val
+ return matrix_jk
+
+
+def GST(
+ gate_set: Union[tuple, set, list],
+ nshots=int(1e4),
+ noise_model=None,
+ include_empty=False,
+ pauli_liouville=False,
+ gauge_matrix=None,
+ backend=None,
+ transpiler=None,
+):
+ """Runs Gate Set Tomography on the input ``gate_set``.
+
+ Args:
+ gate_set (tuple or set or list): set of :class:`qibo.gates.Gate` to run GST on.
+ nshots (int, optional): number of shots used in Gate Set Tomography per gate.
+ Defaults to :math:`10^{4}`.
+ noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model applied to simulate
+ noisy computations.
+ include_empty (bool, optional): if ``True``, additionally performs gate set tomography
+ for :math:`1`- and :math:`2`-qubit empty circuits, returning the corresponding empty
+ matrices in the first and second position of the ouput list.
+ pauli_liouville (bool, optional): if ``True``, returns the matrices in the
+ Pauli-Liouville representation. Defaults to ``False``.
+ gauge_matrix (ndarray, optional): gauge matrix transformation to the Pauli-Liouville
+ representation. Defaults to
+
+ .. math::
+ \\begin{pmatrix}
+ 1 & 1 & 1 & 1 \\\\
+ 0 & 0 & 1 & 0 \\\\
+ 0 & 0 & 0 & 1 \\\\
+ 1 & -1 & 0 & 0 \\\\
+ \\end{pmatrix}
+
+ backend (:class:`qibo.backends.abstract.Backend`, optional): backend
+ to be used in the execution. If ``None``, it uses
+ :class:`qibo.backends.GlobalBackend`. Defaults to ``None``.
+
+
+ Returns:
+ List(ndarray): input ``gate_set`` represented by matrices estimaded via GST.
+ """
+
+ backend = _check_backend(backend)
+
+ if backend.name == "qibolab" and transpiler is None: # pragma: no cover
+ transpiler = Passes(
+ connectivity=backend.platform.topology,
+ passes=[
+ Preprocessing(backend.platform.topology),
+ Random(backend.platform.topology),
+ Sabre(backend.platform.topology),
+ Unroller(NativeGates.default()),
+ ],
+ )
+
+ matrices = []
+ empty_matrices = []
+ if include_empty or pauli_liouville:
+ for nqubits in SUPPORTED_NQUBITS:
+ empty_matrix = _gate_tomography(
+ nqubits=nqubits,
+ gate=None,
+ nshots=nshots,
+ noise_model=noise_model,
+ backend=backend,
+ transpiler=transpiler,
+ )
+ empty_matrices.append(empty_matrix)
+
+ for gate in gate_set:
+ if gate is not None:
+ init_args = signature(gate).parameters
+ if "q" in init_args:
+ nqubits = 1
+ elif "q0" in init_args and "q1" in init_args and "q2" not in init_args:
+ nqubits = 2
+ else:
+ raise_error(
+ RuntimeError,
+ f"Gate {gate} is not supported for `GST`, only 1- and 2-qubits gates are supported.",
+ )
+ gate = gate(*range(nqubits))
+
+ matrices.append(
+ _gate_tomography(
+ nqubits=nqubits,
+ gate=gate,
+ nshots=nshots,
+ noise_model=noise_model,
+ backend=backend,
+ transpiler=transpiler,
+ )
+ )
+
+ if pauli_liouville:
+ if gauge_matrix is not None:
+ if np.linalg.det(gauge_matrix) == 0:
+ raise_error(ValueError, "Matrix is not invertible")
+ else:
+ gauge_matrix = np.array(
+ [[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]]
+ )
+ PL_matrices = []
+ gauge_matrix_1q = gauge_matrix
+ gauge_matrix_2q = np.kron(gauge_matrix, gauge_matrix)
+ for matrix in matrices:
+ gauge_matrix = gauge_matrix_1q if matrix.shape[0] == 4 else gauge_matrix_2q
+ empty = empty_matrices[0] if matrix.shape[0] == 4 else empty_matrices[1]
+ PL_matrices.append(
+ gauge_matrix
+ @ np.linalg.inv(empty)
+ @ matrix
+ @ np.linalg.inv(gauge_matrix)
+ )
+ matrices = PL_matrices
+
+ if include_empty:
+ matrices = empty_matrices + matrices
+
+ return matrices
diff --git a/src/qibo/transpiler/__init__.py b/src/qibo/transpiler/__init__.py
new file mode 100644
index 000000000..bfe576776
--- /dev/null
+++ b/src/qibo/transpiler/__init__.py
@@ -0,0 +1,12 @@
+from qibo.transpiler.optimizer import Preprocessing, Rearrange
+from qibo.transpiler.pipeline import Passes
+from qibo.transpiler.placer import (
+ Custom,
+ Random,
+ ReverseTraversal,
+ StarConnectivityPlacer,
+ Subgraph,
+ Trivial,
+)
+from qibo.transpiler.router import Sabre, ShortestPaths, StarConnectivityRouter
+from qibo.transpiler.unroller import NativeGates
diff --git a/src/qibo/transpiler/_exceptions.py b/src/qibo/transpiler/_exceptions.py
new file mode 100644
index 000000000..2d81c81dc
--- /dev/null
+++ b/src/qibo/transpiler/_exceptions.py
@@ -0,0 +1,22 @@
+"""Custom exceptions raised in transpiler routines."""
+
+
+class BlockingError(Exception):
+ """Raise when an error occurs in the blocking procedure"""
+
+
+class ConnectivityError(Exception):
+ """Raise for an error in the connectivity"""
+
+
+class DecompositionError(Exception):
+ """A decomposition error is raised when, during transpiling,
+ gates are not correctly decomposed in native gates"""
+
+
+class PlacementError(Exception):
+ """Raise for an error in the initial qubit placement"""
+
+
+class TranspilerPipelineError(Exception):
+ """Raise when an error occurs in the transpiler pipeline"""
diff --git a/src/qibo/transpiler/abstract.py b/src/qibo/transpiler/abstract.py
new file mode 100644
index 000000000..439045e72
--- /dev/null
+++ b/src/qibo/transpiler/abstract.py
@@ -0,0 +1,58 @@
+from abc import ABC, abstractmethod
+from typing import Tuple
+
+import networkx as nx
+
+from qibo.models import Circuit
+
+
+class Placer(ABC):
+ @abstractmethod
+ def __init__(self, connectivity: nx.Graph, *args):
+ """A placer implements the initial logical-physical qubit mapping"""
+
+ @abstractmethod
+ def __call__(self, circuit: Circuit, *args) -> dict:
+ """Find initial qubit mapping
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be mapped.
+
+ Returns:
+ (dict): dictionary containing the initial logical to physical qubit mapping.
+ """
+
+
+class Router(ABC):
+ @abstractmethod
+ def __init__(self, connectivity: nx.Graph, *args):
+ """A router implements the mapping of a circuit on a specific hardware."""
+
+ @abstractmethod
+ def __call__(
+ self, circuit: Circuit, initial_layout: dict, *args
+ ) -> Tuple[Circuit, dict]:
+ """Match circuit to hardware connectivity.
+
+ Args:
+ circuit (qibo.models.Circuit): circuit to be routed.
+ initial_layout (dict): dictionary containing the initial logical to physical qubit mapping.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`, dict): routed circuit and dictionary containing the final logical to physical qubit mapping.
+ """
+
+
+class Optimizer(ABC):
+ """An optimizer tries to reduce the number of gates during transpilation."""
+
+ @abstractmethod
+ def __call__(self, circuit: Circuit, *args) -> Circuit:
+ """Optimize transpiled circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be optimized
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`): circuit with optimized number of gates.
+ """
diff --git a/src/qibo/transpiler/blocks.py b/src/qibo/transpiler/blocks.py
new file mode 100644
index 000000000..f08597569
--- /dev/null
+++ b/src/qibo/transpiler/blocks.py
@@ -0,0 +1,351 @@
+from typing import Optional, Union
+
+from qibo import Circuit, gates
+from qibo.config import raise_error
+from qibo.gates import Gate
+from qibo.transpiler._exceptions import BlockingError
+
+
+class Block:
+ """A block contains a subset of gates acting on two qubits.
+
+ Args:
+ qubits (tuple): qubits where the block is acting.
+ gates (list): list of gates that compose the block.
+ name (str or int, optional): name of the block. Defaults to ``None``.
+ """
+
+ def __init__(
+ self, qubits: tuple, gates: list, name: Optional[Union[str, int]] = None
+ ):
+ self.qubits = qubits
+ self.gates = gates
+ self.name = name
+
+ @property
+ def entangled(self):
+ """Returns ``True`` if the block contains two-qubit gates."""
+ return self._count_2q_gates() > 0
+
+ def add_gate(self, gate: Gate):
+ """Add a new gate to the block.
+
+ Args:
+ gate (:class:`qibo.gates.abstract.Gate`): gate to be added.
+ """
+ if not set(gate.qubits).issubset(self.qubits):
+ raise_error(
+ BlockingError,
+ f"Gate acting on qubits {gate.qubits} can't be added "
+ + f"to block acting on qubits {self._qubits}.",
+ )
+ self.gates.append(gate)
+
+ def _count_2q_gates(self):
+ """Return the number of two qubit gates in the block."""
+ return _count_2q_gates(self.gates)
+
+ @property
+ def qubits(self):
+ """Returns a sorted tuple with qubits of the block."""
+ return tuple(sorted(self._qubits))
+
+ @qubits.setter
+ def qubits(self, qubits):
+ self._qubits = qubits
+
+ def fuse(self, block, name: Optional[str] = None):
+ """Fuses the current block with a new one, the qubits they are acting on must coincide.
+
+ Args:
+ block (:class:`qibo.transpiler.blocks.Block`): block to fuse.
+ name (str, optional): name of the fused block. Defaults to ``None``.
+
+ Return:
+ (:class:`qibo.transpiler.blocks.Block`): fusion of the two input blocks.
+ """
+ if not self.qubits == block.qubits:
+ raise_error(
+ BlockingError, "In order to fuse two blocks their qubits must coincide."
+ )
+ return Block(qubits=self.qubits, gates=self.gates + block.gates, name=name)
+
+ def on_qubits(self, new_qubits: tuple):
+ """Return a new block acting on the new qubits.
+
+ Args:
+ new_qubits (tuple): new qubits where the block is acting.
+ """
+ qubits_dict = dict(zip(self.qubits, new_qubits))
+ new_gates = [gate.on_qubits(qubits_dict) for gate in self.gates]
+ return Block(qubits=new_qubits, gates=new_gates, name=self.name)
+
+ # TODO: use real QM properties to check commutation
+ def commute(self, block):
+ """Check if a block commutes with the current one.
+
+ Args:
+ block (:class:`qibo.transpiler.blocks.Block`): block to check commutation.
+
+ Return:
+ True if the two blocks don't share any qubit.
+ False otherwise.
+ """
+ if len(set(self.qubits).intersection(block.qubits)) > 0:
+ return False
+ return True
+
+ # TODO
+ def kak_decompose(self): # pragma: no cover
+ """Return KAK decomposition of the block.
+ This should be done only if the block is entangled and the number of
+ two qubit gates is higher than the number after the decomposition.
+ """
+ raise_error(NotImplementedError, "KAK decomposition is not available yet.")
+
+
+class CircuitBlocks:
+ """A CircuitBlocks contains a quantum circuit decomposed in two qubits blocks.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be decomposed.
+ index_names (bool, optional): assign names to the blocks. Defaults to ``False``.
+ """
+
+ def __init__(self, circuit: Circuit, index_names: bool = False):
+ self.block_list = block_decomposition(circuit)
+ self._index_names = index_names
+ if index_names:
+ for index, block in enumerate(self.block_list):
+ block.name = index
+ self.qubits = circuit.nqubits
+
+ def __call__(self):
+ return self.block_list
+
+ def search_by_index(self, index: int):
+ """Find a block from its index, requires index_names to be ``True``."""
+ if not self._index_names:
+ raise_error(
+ BlockingError,
+ "You need to assign index names in order to use search_by_index.",
+ )
+ for block in self.block_list:
+ if block.name == index:
+ return block
+ raise_error(BlockingError, f"No block found with index {index}.")
+
+ def add_block(self, block: "Block"):
+ """Add a two qubits block."""
+ if not set(block.qubits).issubset(range(self.qubits)):
+ raise_error(
+ BlockingError,
+ "The block can't be added to the circuit because it acts on different qubits",
+ )
+ self.block_list.append(block)
+
+ def circuit(self, circuit_kwargs=None):
+ """Return the quantum circuit.
+
+ Args:
+ circuit_kwargs (dict): original circuit init_kwargs.
+ """
+ if circuit_kwargs is None:
+ circuit = Circuit(self.qubits)
+ else:
+ circuit = Circuit(**circuit_kwargs)
+ for block in self.block_list:
+ for gate in block.gates:
+ circuit.add(gate)
+ return circuit
+
+ def remove_block(self, block: "Block"):
+ """Remove a block from the circuit blocks."""
+ try:
+ self.block_list.remove(block)
+ except ValueError:
+ raise_error(
+ BlockingError,
+ "The block you are trying to remove is not present in the circuit blocks.",
+ )
+
+
+def block_decomposition(circuit: Circuit, fuse: bool = True):
+ """Decompose a circuit into blocks of gates acting on two qubits.
+ Break measurements on multiple qubits into measurements of single qubit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be decomposed.
+ fuse (bool, optional): fuse adjacent blocks acting on the same qubits.
+
+ Return:
+ (list): list of blocks that act on two qubits.
+ """
+ if circuit.nqubits < 2:
+ raise_error(
+ BlockingError,
+ "Only circuits with at least two qubits can be decomposed with block_decomposition.",
+ )
+
+ if _check_multi_qubit_measurements(circuit):
+ circuit = _split_multi_qubit_measurements(circuit)
+ initial_blocks = _initial_block_decomposition(circuit)
+
+ if not fuse:
+ return initial_blocks
+
+ blocks = []
+ while len(initial_blocks) > 0:
+ first_block = initial_blocks[0]
+ remove_list = [first_block]
+ if len(initial_blocks[1:]) > 0:
+ for second_block in initial_blocks[1:]:
+ if second_block.qubits == first_block.qubits:
+ first_block = first_block.fuse(second_block)
+ remove_list.append(second_block)
+ elif not first_block.commute(second_block):
+ break
+ blocks.append(first_block)
+ _remove_gates(initial_blocks, remove_list)
+
+ return blocks
+
+
+def _initial_block_decomposition(circuit: Circuit):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Decompose a circuit into blocks of gates acting on two qubits.
+ This decomposition is not minimal.
+
+ Args:
+ circuit (qibo.models.Circuit): circuit to be decomposed.
+
+ Return:
+ blocks (list): list of blocks that act on two qubits.
+ """
+ blocks = []
+ all_gates = list(circuit.queue)
+ while _count_multi_qubit_gates(all_gates) > 0:
+ for idx, gate in enumerate(all_gates):
+ if len(gate.qubits) == 2:
+ qubits = gate.qubits
+ block_gates = _find_previous_gates(all_gates[0:idx], qubits)
+ block_gates.append(gate)
+ block_gates.extend(_find_successive_gates(all_gates[idx + 1 :], qubits))
+ block = Block(qubits=qubits, gates=block_gates)
+ _remove_gates(all_gates, block_gates)
+ blocks.append(block)
+ break
+ elif len(gate.qubits) > 2:
+ raise_error(
+ BlockingError,
+ "Gates targeting more than 2 qubits are not supported.",
+ )
+
+ # Now we need to deal with the remaining spare single qubit gates
+ while len(all_gates) > 0:
+ first_qubit = all_gates[0].qubits[0]
+ block_gates = _gates_on_qubit(gatelist=all_gates, qubit=first_qubit)
+ _remove_gates(all_gates, block_gates)
+ # Add other single qubits if there are still single qubit gates
+ if len(all_gates) > 0:
+ second_qubit = all_gates[0].qubits[0]
+ second_qubit_block_gates = _gates_on_qubit(
+ gatelist=all_gates, qubit=second_qubit
+ )
+ block_gates += second_qubit_block_gates
+ _remove_gates(all_gates, second_qubit_block_gates)
+ block = Block(qubits=(first_qubit, second_qubit), gates=block_gates)
+ # In case there are no other spare single qubit gates create a block using a following qubit as placeholder
+ else:
+ block = Block(
+ qubits=(first_qubit, (first_qubit + 1) % circuit.nqubits),
+ gates=block_gates,
+ )
+ blocks.append(block)
+
+ return blocks
+
+
+def _check_multi_qubit_measurements(circuit: Circuit):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return True if the circuit contains measurements acting on multiple qubits."""
+ for gate in circuit.queue:
+ if isinstance(gate, gates.M) and len(gate.qubits) > 1:
+ return True
+ return False
+
+
+def _split_multi_qubit_measurements(circuit: Circuit):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return an equivalent circuit containinig measurements acting only on single qubits.
+ """
+ new_circuit = Circuit(circuit.nqubits)
+ for gate in circuit.queue:
+ if isinstance(gate, gates.M) and len(gate.qubits) > 1:
+ for qubit in gate.qubits:
+ new_circuit.add(gates.M(qubit))
+ else:
+ new_circuit.add(gate)
+ return new_circuit
+
+
+def _gates_on_qubit(gatelist, qubit):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return a list of all single qubit gates in gatelist acting on a specific qubit."""
+ selected_gates = []
+ for gate in gatelist:
+ if gate.qubits[0] == qubit:
+ selected_gates.append(gate)
+ return selected_gates
+
+
+def _remove_gates(gatelist, remove_list):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Remove all gates present in remove_list from gatelist."""
+ for gate in remove_list:
+ gatelist.remove(gate)
+
+
+def _count_2q_gates(gatelist: list):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return the number of two qubit gates in a list of gates."""
+ return len([gate for gate in gatelist if len(gate.qubits) == 2])
+
+
+def _count_multi_qubit_gates(gatelist: list):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return the number of multi qubit gates in a list of gates."""
+ return len([gate for gate in gatelist if len(gate.qubits) >= 2])
+
+
+def _find_successive_gates(gates_list: list, qubits: tuple):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return a list containing all gates acting on qubits until a new two qubit gate acting on qubits is found.
+ """
+ successive_gates = []
+ for qubit in qubits:
+ for gate in gates_list:
+ if (len(gate.qubits) == 1) and (gate.qubits[0] == qubit):
+ successive_gates.append(gate)
+ elif (len(gate.qubits) == 2) and (qubit in gate.qubits):
+ break
+ return successive_gates
+
+
+def _find_previous_gates(gates_list: list, qubits: tuple):
+ """Helper method for :meth:'qibo.transpiler.blocks.block_decomposition'.
+
+ Return a list containing all gates acting on qubits."""
+ previous_gates = []
+ for gate in gates_list:
+ if gate.qubits[0] in qubits:
+ previous_gates.append(gate)
+ return previous_gates
diff --git a/src/qibo/transpiler/decompositions.py b/src/qibo/transpiler/decompositions.py
new file mode 100644
index 000000000..3aa5d5649
--- /dev/null
+++ b/src/qibo/transpiler/decompositions.py
@@ -0,0 +1,504 @@
+import numpy as np
+
+from qibo import gates
+from qibo.backends import NumpyBackend
+from qibo.transpiler.unitary_decompositions import (
+ two_qubit_decomposition,
+ u3_decomposition,
+)
+
+backend = NumpyBackend()
+
+
+class GateDecompositions:
+ """Abstract data structure that holds decompositions of gates."""
+
+ def __init__(self):
+ self.decompositions = {}
+
+ def add(self, gate, decomposition):
+ """Register a decomposition for a gate."""
+ self.decompositions[gate] = decomposition
+
+ def count_2q(self, gate):
+ """Count the number of two-qubit gates in the decomposition of the given gate."""
+ if gate.parameters:
+ decomposition = self.decompositions[gate.__class__](gate)
+ else:
+ decomposition = self.decompositions[gate.__class__]
+ return len(tuple(g for g in decomposition if len(g.qubits) > 1))
+
+ def count_1q(self, gate):
+ """Count the number of single qubit gates in the decomposition of the given gate."""
+ if gate.parameters:
+ decomposition = self.decompositions[gate.__class__](gate)
+ else:
+ decomposition = self.decompositions[gate.__class__]
+ return len(tuple(g for g in decomposition if len(g.qubits) == 1))
+
+ def __call__(self, gate):
+ """Decompose a gate."""
+ decomposition = self.decompositions[gate.__class__]
+ if callable(decomposition):
+ decomposition = decomposition(gate)
+ return [
+ g.on_qubits({i: q for i, q in enumerate(gate.qubits)})
+ for g in decomposition
+ ]
+
+
+def _u3_to_gpi2(t, p, l):
+ """Decompose a U3 gate into GPI2 gates, the decomposition is optimized to use the minimum number of gates..
+
+ Args:
+ t (float): theta parameter of U3 gate.
+ p (float): phi parameter of U3 gate.
+ l (float): lambda parameter of U3 gate.
+
+ Returns:
+ decomposition (list): list of native gates that decompose the U3 gate.
+ """
+ decomposition = []
+ if l != 0.0:
+ decomposition.append(gates.RZ(0, l))
+ decomposition.append(gates.GPI2(0, 0))
+ if t != -np.pi:
+ decomposition.append(gates.RZ(0, t + np.pi))
+ decomposition.append(gates.GPI2(0, 0))
+ if p != -np.pi:
+ decomposition.append(gates.RZ(0, p + np.pi))
+ return decomposition
+
+
+# Decompose single qubit gates using GPI2 (more efficient on hardware)
+gpi2_dec = GateDecompositions()
+gpi2_dec.add(gates.H, [gates.Z(0), gates.GPI2(0, np.pi / 2)])
+gpi2_dec.add(gates.X, [gates.GPI2(0, np.pi / 2), gates.GPI2(0, np.pi / 2), gates.Z(0)])
+gpi2_dec.add(gates.Y, [gates.Z(0), gates.GPI2(0, 0), gates.GPI2(0, 0)])
+gpi2_dec.add(gates.Z, [gates.Z(0)])
+gpi2_dec.add(gates.S, [gates.RZ(0, np.pi / 2)])
+gpi2_dec.add(gates.SDG, [gates.RZ(0, -np.pi / 2)])
+gpi2_dec.add(gates.T, [gates.RZ(0, np.pi / 4)])
+gpi2_dec.add(gates.TDG, [gates.RZ(0, -np.pi / 4)])
+gpi2_dec.add(gates.SX, [gates.GPI2(0, 0)])
+gpi2_dec.add(
+ gates.RX,
+ lambda gate: [
+ gates.Z(0),
+ gates.GPI2(0, np.pi / 2),
+ gates.RZ(0, gate.parameters[0] + np.pi),
+ gates.GPI2(0, np.pi / 2),
+ ],
+)
+gpi2_dec.add(
+ gates.RY,
+ lambda gate: [
+ gates.GPI2(0, 0),
+ gates.RZ(0, gate.parameters[0] + np.pi),
+ gates.GPI2(0, 0),
+ gates.Z(0),
+ ],
+)
+gpi2_dec.add(gates.RZ, lambda gate: [gates.RZ(0, gate.parameters[0])])
+gpi2_dec.add(gates.GPI2, lambda gate: [gates.GPI2(0, gate.parameters[0])])
+gpi2_dec.add(gates.U1, lambda gate: [gates.RZ(0, gate.parameters[0])])
+gpi2_dec.add(
+ gates.U2,
+ lambda gate: _u3_to_gpi2(np.pi / 2, gate.parameters[0], gate.parameters[1]),
+)
+gpi2_dec.add(gates.U3, lambda gate: _u3_to_gpi2(*gate.parameters))
+gpi2_dec.add(
+ gates.Unitary, lambda gate: _u3_to_gpi2(*u3_decomposition(gate.parameters[0]))
+)
+gpi2_dec.add(
+ gates.FusedGate, lambda gate: _u3_to_gpi2(*u3_decomposition(gate.matrix(backend)))
+)
+
+# Decompose single qubit gates using U3
+u3_dec = GateDecompositions()
+u3_dec.add(gates.H, [gates.U3(0, -np.pi / 2, np.pi, 0)])
+u3_dec.add(gates.X, [gates.U3(0, np.pi, 0, np.pi)])
+u3_dec.add(gates.Y, [gates.U3(0, np.pi, 0, 0)])
+u3_dec.add(gates.Z, [gates.Z(0)])
+u3_dec.add(gates.S, [gates.RZ(0, np.pi / 2)])
+u3_dec.add(gates.SDG, [gates.RZ(0, -np.pi / 2)])
+u3_dec.add(gates.T, [gates.RZ(0, np.pi / 4)])
+u3_dec.add(gates.TDG, [gates.RZ(0, -np.pi / 4)])
+u3_dec.add(gates.SX, [gates.U3(0, np.pi / 2, -np.pi / 2, np.pi / 2)])
+u3_dec.add(
+ gates.RX, lambda gate: [gates.U3(0, gate.parameters[0], -np.pi / 2, np.pi / 2)]
+)
+u3_dec.add(gates.RY, lambda gate: [gates.U3(0, gate.parameters[0], 0, 0)])
+u3_dec.add(gates.RZ, lambda gate: [gates.RZ(0, gate.parameters[0])])
+u3_dec.add(
+ gates.PRX,
+ lambda gate: [
+ gates.RZ(0, gate.parameters[1] - np.pi / 2),
+ gates.RY(0, -gate.parameters[0]),
+ gates.RZ(0, gate.parameters[1] + np.pi / 2),
+ ],
+)
+u3_dec.add(
+ gates.GPI2, lambda gate: [gates.U3(0, *u3_decomposition(gate.matrix(backend)))]
+)
+u3_dec.add(gates.U1, lambda gate: [gates.RZ(0, gate.parameters[0])])
+u3_dec.add(
+ gates.U2,
+ lambda gate: [gates.U3(0, np.pi / 2, gate.parameters[0], gate.parameters[1])],
+)
+u3_dec.add(
+ gates.U3,
+ lambda gate: [
+ gates.U3(0, gate.parameters[0], gate.parameters[1], gate.parameters[2])
+ ],
+)
+u3_dec.add(
+ gates.Unitary,
+ lambda gate: [gates.U3(0, *u3_decomposition(gate.parameters[0]))],
+)
+u3_dec.add(
+ gates.FusedGate,
+ lambda gate: [gates.U3(0, *u3_decomposition(gate.matrix(backend)))],
+)
+
+# register the iSWAP decompositions
+iswap_dec = GateDecompositions()
+iswap_dec.add(
+ gates.CNOT,
+ [
+ gates.U3(0, 3 * np.pi / 2, np.pi, 0),
+ gates.U3(1, np.pi / 2, np.pi, np.pi),
+ gates.iSWAP(0, 1),
+ gates.U3(0, np.pi, 0, np.pi),
+ gates.U3(1, np.pi / 2, np.pi, np.pi),
+ gates.iSWAP(0, 1),
+ gates.U3(0, np.pi / 2, np.pi / 2, np.pi),
+ gates.U3(1, np.pi / 2, np.pi, -np.pi / 2),
+ ],
+)
+iswap_dec.add(
+ gates.CZ,
+ [
+ gates.U3(0, -np.pi / 2, np.pi, 0),
+ gates.U3(1, -np.pi / 2, np.pi, 0),
+ gates.U3(1, np.pi / 2, np.pi, np.pi),
+ gates.iSWAP(0, 1),
+ gates.U3(0, np.pi, 0, np.pi),
+ gates.U3(1, np.pi / 2, np.pi, np.pi),
+ gates.iSWAP(0, 1),
+ gates.U3(0, np.pi / 2, np.pi / 2, np.pi),
+ gates.U3(1, np.pi / 2, np.pi, -np.pi / 2),
+ gates.U3(1, -np.pi / 2, np.pi, 0),
+ ],
+)
+iswap_dec.add(
+ gates.SWAP,
+ [
+ gates.iSWAP(0, 1),
+ gates.U3(1, np.pi / 2, -np.pi / 2, np.pi / 2),
+ gates.iSWAP(0, 1),
+ gates.U3(0, np.pi / 2, -np.pi / 2, np.pi / 2),
+ gates.iSWAP(0, 1),
+ gates.U3(1, np.pi / 2, -np.pi / 2, np.pi / 2),
+ ],
+)
+iswap_dec.add(gates.iSWAP, [gates.iSWAP(0, 1)])
+
+# register CZ decompositions
+cz_dec = GateDecompositions()
+cz_dec.add(gates.CNOT, [gates.H(1), gates.CZ(0, 1), gates.H(1)])
+cz_dec.add(gates.CZ, [gates.CZ(0, 1)])
+cz_dec.add(
+ gates.SWAP,
+ [
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.H(0),
+ gates.CZ(1, 0),
+ gates.H(0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ ],
+)
+cz_dec.add(
+ gates.iSWAP,
+ [
+ gates.U3(0, np.pi / 2.0, 0, -np.pi / 2.0),
+ gates.U3(1, np.pi / 2.0, 0, -np.pi / 2.0),
+ gates.CZ(0, 1),
+ gates.H(0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(0),
+ gates.H(1),
+ ],
+)
+cz_dec.add(
+ gates.CRX,
+ lambda gate: [
+ gates.RX(1, gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ gates.RX(1, -gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ ],
+)
+cz_dec.add(
+ gates.CRY,
+ lambda gate: [
+ gates.RY(1, gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ gates.RY(1, -gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ ],
+)
+cz_dec.add(
+ gates.CRZ,
+ lambda gate: [
+ gates.RZ(1, gate.parameters[0] / 2.0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.RX(1, -gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ gates.H(1),
+ ],
+)
+cz_dec.add(
+ gates.CU1,
+ lambda gate: [
+ gates.RZ(0, gate.parameters[0] / 2.0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.RX(1, -gate.parameters[0] / 2.0),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.RZ(1, gate.parameters[0] / 2.0),
+ ],
+)
+cz_dec.add(
+ gates.CU2,
+ lambda gate: [
+ gates.RZ(1, (gate.parameters[1] - gate.parameters[0]) / 2.0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.U3(1, -np.pi / 4, 0, -(gate.parameters[1] + gate.parameters[0]) / 2.0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.U3(1, np.pi / 4, gate.parameters[0], 0),
+ ],
+)
+cz_dec.add(
+ gates.CU3,
+ lambda gate: [
+ gates.RZ(1, (gate.parameters[2] - gate.parameters[1]) / 2.0),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.U3(
+ 1,
+ -gate.parameters[0] / 2.0,
+ 0,
+ -(gate.parameters[2] + gate.parameters[1]) / 2.0,
+ ),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ gates.U3(1, gate.parameters[0] / 2.0, gate.parameters[1], 0),
+ ],
+)
+cz_dec.add(
+ gates.FSWAP,
+ [
+ gates.U3(0, np.pi / 2, -np.pi / 2, np.pi),
+ gates.U3(1, np.pi / 2, np.pi / 2, np.pi / 2),
+ gates.CZ(0, 1),
+ gates.U3(0, np.pi / 2, 0, -np.pi / 2),
+ gates.U3(1, np.pi / 2, 0, np.pi / 2),
+ gates.CZ(0, 1),
+ gates.U3(0, np.pi / 2, np.pi / 2, np.pi),
+ gates.U3(1, np.pi / 2, 0, np.pi),
+ ],
+)
+cz_dec.add(
+ gates.RXX,
+ lambda gate: [
+ gates.H(0),
+ gates.CZ(0, 1),
+ gates.RX(1, gate.parameters[0]),
+ gates.CZ(0, 1),
+ gates.H(0),
+ ],
+)
+cz_dec.add(
+ gates.RYY,
+ lambda gate: [
+ gates.RX(0, np.pi / 2),
+ gates.U3(1, np.pi / 2, np.pi / 2, np.pi),
+ gates.CZ(0, 1),
+ gates.RX(1, gate.parameters[0]),
+ gates.CZ(0, 1),
+ gates.RX(0, -np.pi / 2),
+ gates.U3(1, np.pi / 2, 0, np.pi / 2),
+ ],
+)
+cz_dec.add(
+ gates.RZZ,
+ lambda gate: [
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.RX(1, gate.parameters[0]),
+ gates.CZ(0, 1),
+ gates.H(1),
+ ],
+)
+cz_dec.add(
+ gates.TOFFOLI,
+ [
+ gates.CZ(1, 2),
+ gates.RX(2, -np.pi / 4),
+ gates.CZ(0, 2),
+ gates.RX(2, np.pi / 4),
+ gates.CZ(1, 2),
+ gates.RX(2, -np.pi / 4),
+ gates.CZ(0, 2),
+ gates.RX(2, np.pi / 4),
+ gates.RZ(1, np.pi / 4),
+ gates.H(1),
+ gates.CZ(0, 1),
+ gates.RZ(0, np.pi / 4),
+ gates.RX(1, -np.pi / 4),
+ gates.CZ(0, 1),
+ gates.H(1),
+ ],
+)
+cz_dec.add(
+ gates.Unitary,
+ lambda gate: two_qubit_decomposition(0, 1, gate.parameters[0], backend=backend),
+)
+cz_dec.add(
+ gates.fSim,
+ lambda gate: two_qubit_decomposition(0, 1, gate.matrix(backend), backend=backend),
+)
+cz_dec.add(
+ gates.GeneralizedfSim,
+ lambda gate: two_qubit_decomposition(0, 1, gate.matrix(backend), backend=backend),
+)
+
+# register other optimized gate decompositions
+opt_dec = GateDecompositions()
+opt_dec.add(
+ gates.SWAP,
+ [
+ gates.H(0),
+ gates.SDG(0),
+ gates.SDG(1),
+ gates.iSWAP(0, 1),
+ gates.CZ(0, 1),
+ gates.H(1),
+ ],
+)
+
+
+# standard gate decompositions used by :meth:`qibo.gates.gates.Gate.decompose`
+standard_decompositions = GateDecompositions()
+standard_decompositions.add(gates.SX, [gates.RX(0, np.pi / 2, trainable=False)])
+standard_decompositions.add(gates.SXDG, [gates.RX(0, -np.pi / 2, trainable=False)])
+standard_decompositions.add(
+ gates.PRX,
+ lambda gate: [
+ gates.RZ(0, -gate.parameters[1] - np.pi / 2),
+ gates.RY(0, -gate.parameters[0]),
+ gates.RZ(0, gate.parameters[1] + np.pi / 2),
+ ],
+)
+standard_decompositions.add(
+ gates.U3,
+ lambda gate: [
+ gates.RZ(0, gate.parameters[2]),
+ gates.SX(0),
+ gates.RZ(0, gate.parameters[0] + np.pi),
+ gates.SX(0),
+ gates.RZ(0, gate.parameters[1] + np.pi),
+ ],
+)
+standard_decompositions.add(gates.CY, [gates.SDG(1), gates.CNOT(0, 1), gates.S(1)])
+standard_decompositions.add(gates.CZ, [gates.H(1), gates.CNOT(0, 1), gates.H(1)])
+standard_decompositions.add(
+ gates.CSX, [gates.H(1), gates.CU1(0, 1, np.pi / 2), gates.H(1)]
+)
+standard_decompositions.add(
+ gates.CSXDG, [gates.H(1), gates.CU1(0, 1, -np.pi / 2), gates.H(1)]
+)
+standard_decompositions.add(
+ gates.RZX,
+ lambda gate: [
+ gates.H(1),
+ gates.CNOT(0, 1),
+ gates.RZ(1, gate.parameters[0]),
+ gates.CNOT(0, 1),
+ gates.H(1),
+ ],
+)
+standard_decompositions.add(
+ gates.RXXYY,
+ lambda gate: [
+ gates.RZ(1, -np.pi / 2),
+ gates.S(0),
+ gates.SX(1),
+ gates.RZ(1, np.pi / 2),
+ gates.CNOT(1, 0),
+ gates.RY(0, -gate.parameters[0] / 2),
+ gates.RY(1, -gate.parameters[0] / 2),
+ gates.CNOT(1, 0),
+ gates.SDG(0),
+ gates.RZ(1, -np.pi / 2),
+ gates.SX(1).dagger(),
+ gates.RZ(1, np.pi / 2),
+ ],
+)
+standard_decompositions.add(
+ gates.RBS,
+ lambda gate: [
+ gates.H(0),
+ gates.CNOT(0, 1),
+ gates.H(1),
+ gates.RY(0, gate.parameters[0]),
+ gates.RY(1, -gate.parameters[0]),
+ gates.H(1),
+ gates.CNOT(0, 1),
+ gates.H(0),
+ ],
+)
+standard_decompositions.add(
+ gates.GIVENS, lambda gate: gates.RBS(0, 1, -gate.parameters[0]).decompose()
+)
+standard_decompositions.add(
+ gates.FSWAP, [gates.X(1)] + gates.GIVENS(0, 1, np.pi / 2).decompose() + [gates.X(0)]
+)
+standard_decompositions.add(
+ gates.ECR, [gates.S(0), gates.SX(1), gates.CNOT(0, 1), gates.X(0)]
+)
+standard_decompositions.add(gates.CCZ, [gates.H(2), gates.TOFFOLI(0, 1, 2), gates.H(2)])
+standard_decompositions.add(
+ gates.TOFFOLI,
+ [
+ gates.H(2),
+ gates.CNOT(1, 2),
+ gates.TDG(2),
+ gates.CNOT(0, 2),
+ gates.T(2),
+ gates.CNOT(1, 2),
+ gates.T(1),
+ gates.TDG(2),
+ gates.CNOT(0, 2),
+ gates.CNOT(0, 1),
+ gates.T(2),
+ gates.T(0),
+ gates.TDG(1),
+ gates.H(2),
+ gates.CNOT(0, 1),
+ ],
+)
diff --git a/src/qibo/transpiler/optimizer.py b/src/qibo/transpiler/optimizer.py
new file mode 100644
index 000000000..700895762
--- /dev/null
+++ b/src/qibo/transpiler/optimizer.py
@@ -0,0 +1,57 @@
+import networkx as nx
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.models import Circuit
+from qibo.transpiler.abstract import Optimizer
+
+
+class Preprocessing(Optimizer):
+ """Match the number of qubits of the circuit with the number of qubits of the chip if possible.
+
+ Args:
+ connectivity (:class:`networkx.Graph`): hardware chip connectivity.
+ """
+
+ def __init__(self, connectivity: nx.Graph):
+ self.connectivity = connectivity
+
+ def __call__(self, circuit: Circuit) -> Circuit:
+ physical_qubits = self.connectivity.number_of_nodes()
+ logical_qubits = circuit.nqubits
+ if logical_qubits > physical_qubits:
+ raise_error(
+ ValueError,
+ "The number of qubits in the circuit can't be greater "
+ + "than the number of physical qubits.",
+ )
+ if logical_qubits == physical_qubits:
+ return circuit
+ new_circuit = Circuit(physical_qubits)
+ for gate in circuit.queue:
+ new_circuit.add(gate)
+ return new_circuit
+
+
+class Rearrange(Optimizer):
+ """Rearranges gates using qibo's fusion algorithm.
+ May reduce number of SWAPs when fixing for connectivity
+ but this has not been tested.
+
+ Args:
+ max_qubits (int, optional): maximum number of qubits to fuse gates.
+ Defaults to :math:`1`.
+ """
+
+ def __init__(self, max_qubits: int = 1):
+ self.max_qubits = max_qubits
+
+ def __call__(self, circuit: Circuit):
+ fused_circuit = circuit.fuse(max_qubits=self.max_qubits)
+ new = circuit.__class__(circuit.nqubits)
+ for fgate in fused_circuit.queue:
+ if isinstance(fgate, gates.FusedGate):
+ new.add(gates.Unitary(fgate.matrix(), *fgate.qubits))
+ else:
+ new.add(fgate)
+ return new
diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py
new file mode 100644
index 000000000..a6bf3a7b9
--- /dev/null
+++ b/src/qibo/transpiler/pipeline.py
@@ -0,0 +1,286 @@
+from typing import Optional
+
+import networkx as nx
+import numpy as np
+
+from qibo.backends import NumpyBackend
+from qibo.config import raise_error
+from qibo.models import Circuit
+from qibo.quantum_info.random_ensembles import random_statevector
+from qibo.transpiler._exceptions import TranspilerPipelineError
+from qibo.transpiler.abstract import Optimizer, Placer, Router
+from qibo.transpiler.optimizer import Preprocessing
+from qibo.transpiler.placer import StarConnectivityPlacer, Trivial, assert_placement
+from qibo.transpiler.router import (
+ ConnectivityError,
+ StarConnectivityRouter,
+ assert_connectivity,
+)
+from qibo.transpiler.unroller import (
+ DecompositionError,
+ NativeGates,
+ Unroller,
+ assert_decomposition,
+)
+
+
+def assert_circuit_equivalence(
+ original_circuit: Circuit,
+ transpiled_circuit: Circuit,
+ final_map: dict,
+ initial_map: Optional[dict] = None,
+ test_states: Optional[list] = None,
+ ntests: int = 3,
+):
+ """Checks that the transpiled circuit agrees with the original using simulation.
+
+ Args:
+ original_circuit (:class:`qibo.models.circuit.Circuit`): Original circuit.
+ transpiled_circuit (:class:`qibo.models.circuit.Circuit`): Transpiled circuit.
+ final_map (dict): logical-physical qubit mapping after routing.
+ initial_map (dict, optional): logical_physical qubit mapping before routing.
+ If ``None``, trivial initial map is used. Defauts to ``None``.
+ test_states (list, optional): states on which the test is performed.
+ If ``None``, ``ntests`` random states will be tested. Defauts to ``None``.
+ ntests (int, optional): number of random states tested. Defauts to :math:`3`.
+ """
+ backend = NumpyBackend()
+ ordering = np.argsort(np.array(list(final_map.values())))
+ if transpiled_circuit.nqubits != original_circuit.nqubits:
+ raise_error(
+ ValueError,
+ "Transpiled and original circuit do not have the same number of qubits.",
+ )
+
+ if test_states is None:
+ test_states = [
+ random_statevector(dims=2**original_circuit.nqubits, backend=backend)
+ for _ in range(ntests)
+ ]
+ if initial_map is not None:
+ reordered_test_states = []
+ initial_map = np.array(list(initial_map.values()))
+ reordered_test_states = [
+ _transpose_qubits(initial_state, initial_map)
+ for initial_state in test_states
+ ]
+ else:
+ reordered_test_states = test_states
+
+ for i in range(len(test_states)):
+ target_state = backend.execute_circuit(
+ original_circuit, initial_state=test_states[i]
+ ).state()
+ final_state = backend.execute_circuit(
+ transpiled_circuit, initial_state=reordered_test_states[i]
+ ).state()
+ final_state = _transpose_qubits(final_state, ordering)
+ fidelity = np.abs(np.dot(np.conj(target_state), final_state))
+ try:
+ np.testing.assert_allclose(fidelity, 1.0)
+ except AssertionError:
+ raise_error(TranspilerPipelineError, "Circuit equivalence not satisfied.")
+
+
+def _transpose_qubits(state: np.ndarray, qubits_ordering: np.ndarray):
+ """Reorders qubits of a given state vector.
+
+ Args:
+ state (np.ndarray): final state of the circuit.
+ qubits_ordering (np.ndarray): final qubit ordering.
+ """
+ original_shape = state.shape
+ state = np.reshape(state, len(qubits_ordering) * (2,))
+ state = np.transpose(state, qubits_ordering)
+ return np.reshape(state, original_shape)
+
+
+def assert_transpiling(
+ original_circuit: Circuit,
+ transpiled_circuit: Circuit,
+ connectivity: nx.Graph,
+ initial_layout: dict,
+ final_layout: dict,
+ native_gates: NativeGates = NativeGates.default(),
+ check_circuit_equivalence=True,
+):
+ """Check that all transpiler passes have been executed correctly.
+
+ Args:
+ original_circuit (qibo.models.Circuit): circuit before transpiling.
+ transpiled_circuit (qibo.models.Circuit): circuit after transpiling.
+ connectivity (networkx.Graph): chip qubits connectivity.
+ initial_layout (dict): initial physical-logical qubit mapping.
+ final_layout (dict): final physical-logical qubit mapping.
+ native_gates (NativeGates): native gates supported by the hardware.
+ check_circuit_equivalence (Bool): use simulations to check if the transpiled circuit is the same as the original.
+ """
+ assert_connectivity(circuit=transpiled_circuit, connectivity=connectivity)
+ assert_decomposition(
+ circuit=transpiled_circuit,
+ native_gates=native_gates,
+ )
+ if original_circuit.nqubits != transpiled_circuit.nqubits:
+ qubit_matcher = Preprocessing(connectivity=connectivity)
+ original_circuit = qubit_matcher(circuit=original_circuit)
+ assert_placement(
+ circuit=original_circuit, layout=initial_layout, connectivity=connectivity
+ )
+ assert_placement(
+ circuit=transpiled_circuit, layout=final_layout, connectivity=connectivity
+ )
+ if check_circuit_equivalence:
+ assert_circuit_equivalence(
+ original_circuit=original_circuit,
+ transpiled_circuit=transpiled_circuit,
+ initial_map=initial_layout,
+ final_map=final_layout,
+ )
+
+
+def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list):
+ """Restrict the connectivity to selected qubits.
+
+ Args:
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ qubits (list): list of physical qubits to be used.
+
+ Returns:
+ (:class:`networkx.Graph`): restricted connectivity.
+ """
+ if not set(qubits).issubset(set(connectivity.nodes)):
+ raise_error(
+ ConnectivityError, "Some qubits are not in the original connectivity."
+ )
+
+ new_connectivity = nx.Graph()
+ new_connectivity.add_nodes_from(qubits)
+ new_edges = [
+ edge for edge in connectivity.edges if edge[0] in qubits and edge[1] in qubits
+ ]
+ new_connectivity.add_edges_from(new_edges)
+
+ if not nx.is_connected(new_connectivity):
+ raise_error(ConnectivityError, "New connectivity graph is not connected.")
+
+ return new_connectivity
+
+
+class Passes:
+ """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially:
+
+ Args:
+ passes (list, optional): list of passes to be applied sequentially.
+ If ``None``, default transpiler will be used.
+ Defaults to ``None``.
+ connectivity (:class:`networkx.Graph`, optional): physical qubits connectivity.
+ If ``None``, :class:`` is used.
+ Defaults to ``None``.
+ native_gates (:class:`qibo.transpiler.unroller.NativeGates`, optional): native gates.
+ Defaults to :math:`qibo.transpiler.unroller.NativeGates.default`.
+ on_qubits (list, optional): list of physical qubits to be used.
+ If "None" all qubits are used. Defaults to ``None``.
+ int_qubit_name (bool, optional): if `True` the `final_layout` keys are
+ cast to integers.
+ """
+
+ def __init__(
+ self,
+ passes: list = None,
+ connectivity: nx.Graph = None,
+ native_gates: NativeGates = NativeGates.default(),
+ on_qubits: list = None,
+ int_qubit_names: bool = False,
+ ):
+ if on_qubits is not None:
+ connectivity = restrict_connectivity_qubits(connectivity, on_qubits)
+ self.connectivity = connectivity
+ self.native_gates = native_gates
+ self.passes = self.default() if passes is None else passes
+ self.initial_layout = None
+ self.int_qubit_names = int_qubit_names
+
+ def default(self):
+ """Return the default transpiler pipeline for the required hardware connectivity."""
+ if not isinstance(self.connectivity, nx.Graph):
+ raise_error(
+ TranspilerPipelineError,
+ "Define the hardware chip connectivity to use default transpiler",
+ )
+ default_passes = []
+ # preprocessing
+ default_passes.append(Preprocessing(connectivity=self.connectivity))
+ # default placer pass
+ default_passes.append(StarConnectivityPlacer())
+ # default router pass
+ default_passes.append(StarConnectivityRouter())
+ # default unroller pass
+ default_passes.append(Unroller(native_gates=self.native_gates))
+
+ return default_passes
+
+ def __call__(self, circuit):
+ """
+ This function returns the compiled circuits and the dictionary mapping
+ physical (keys) to logical (values) qubit. If `int_qubit_name` is `True`
+ each key `i` correspond to the `i-th` qubit in the graph.
+ """
+ final_layout = self.initial_layout = None
+ for transpiler_pass in self.passes:
+ if isinstance(transpiler_pass, Optimizer):
+ transpiler_pass.connectivity = self.connectivity
+ circuit = transpiler_pass(circuit)
+ elif isinstance(transpiler_pass, Placer):
+ transpiler_pass.connectivity = self.connectivity
+ if self.initial_layout is None:
+ self.initial_layout = transpiler_pass(circuit)
+ final_layout = (
+ self.initial_layout
+ ) # This way the final layout will be the same as the initial layout if no router is used
+ else:
+ raise_error(
+ TranspilerPipelineError,
+ "You are defining more than one placer pass.",
+ )
+ elif isinstance(transpiler_pass, Router):
+ transpiler_pass.connectivity = self.connectivity
+ if self.initial_layout is not None:
+ circuit, final_layout = transpiler_pass(
+ circuit, self.initial_layout
+ )
+ else:
+ raise_error(
+ TranspilerPipelineError, "Use a placement pass before routing."
+ )
+ elif isinstance(transpiler_pass, Unroller):
+ circuit = transpiler_pass(circuit)
+ else:
+ raise_error(
+ TranspilerPipelineError,
+ f"Unrecognised transpiler pass: {transpiler_pass}",
+ )
+ if self.int_qubit_names and final_layout is not None:
+ final_layout = {int(key[1:]): value for key, value in final_layout.items()}
+ return circuit, final_layout
+
+ def is_satisfied(self, circuit: Circuit):
+ """Returns ``True`` if the circuit respects the hardware connectivity and native gates, ``False`` otherwise.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be checked.
+
+ Returns:
+ (bool): satisfiability condition.
+ """
+ try:
+ assert_connectivity(circuit=circuit, connectivity=self.connectivity)
+ assert_decomposition(circuit=circuit, native_gates=self.native_gates)
+ return True
+ except ConnectivityError:
+ return False
+ except DecompositionError:
+ return False
+
+ def get_initial_layout(self):
+ """Return initial qubit layout"""
+ return self.initial_layout
diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py
new file mode 100644
index 000000000..543a8b888
--- /dev/null
+++ b/src/qibo/transpiler/placer.py
@@ -0,0 +1,479 @@
+from typing import Optional, Union
+
+import networkx as nx
+
+from qibo import gates
+from qibo.backends import _check_backend_and_local_state
+from qibo.config import log, raise_error
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import PlacementError
+from qibo.transpiler.abstract import Placer, Router
+from qibo.transpiler.router import _find_connected_qubit
+
+
+def assert_placement(
+ circuit: Circuit, layout: dict, connectivity: nx.Graph = None
+) -> bool:
+ """Check if layout is in the correct form and matches the number of qubits of the circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Circuit model to check.
+ layout (dict): physical to logical qubit mapping.
+ connectivity (:class:`networkx.Graph`, optional): Chip connectivity.
+ This argument is necessary if the layout is applied to a subset of
+ qubits of the original connectivity graph. Defaults to ``None``.
+ """
+ assert_mapping_consistency(layout=layout, connectivity=connectivity)
+ if circuit.nqubits > len(layout):
+ raise_error(
+ PlacementError,
+ "Layout can't be used on circuit. The circuit requires more qubits.",
+ )
+ if circuit.nqubits < len(layout):
+ raise_error(
+ PlacementError,
+ "Layout can't be used on circuit. "
+ + "Ancillary extra qubits need to be added to the circuit.",
+ )
+
+
+def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None):
+ """Check if layout is in the correct form.
+
+ Args:
+ layout (dict): physical to logical qubit mapping.
+ connectivity (:class:`networkx.Graph`, optional): Chip connectivity.
+ This argument is necessary if the layout is applied to a subset of
+ qubits of the original connectivity graph. Defaults to ``None``.
+ """
+ values = sorted(layout.values())
+ physical_qubits = list(layout)
+ nodes = (
+ list(range(len(values))) if connectivity is None else list(connectivity.nodes)
+ )
+ ref_keys = (
+ ["q" + str(i) for i in nodes] if isinstance(physical_qubits[0], str) else nodes
+ )
+ if physical_qubits != ref_keys:
+ raise_error(
+ PlacementError,
+ "Some physical qubits in the layout may be missing or duplicated.",
+ )
+ if values != list(range(len(values))):
+ raise_error(
+ PlacementError,
+ "Some logical qubits in the layout may be missing or duplicated.",
+ )
+
+
+def _find_gates_qubits_pairs(circuit: Circuit):
+ """Helper method for :meth:`qibo.transpiler.placer`.
+ Translate circuit into a list of pairs of qubits to be used by the router and placer.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (list): Pairs of qubits targeted by two qubits gates.
+ """
+ gates_qubits_pairs = []
+ for gate in circuit.queue:
+ if isinstance(gate, gates.M):
+ pass
+ elif len(gate.qubits) == 2:
+ gates_qubits_pairs.append(sorted(gate.qubits))
+ elif len(gate.qubits) >= 3:
+ raise_error(
+ ValueError, "Gates targeting more than 2 qubits are not supported"
+ )
+ return gates_qubits_pairs
+
+
+class StarConnectivityPlacer(Placer):
+ """Find an optimized qubit placement for the following connectivity:
+
+ q
+ |
+ q -- q -- q
+ |
+ q
+
+ Args:
+ connectivity (:class:`networkx.Graph`): chip connectivity, not used for this transpiler.
+ middle_qubit (int, optional): qubit id of the qubit that is in the middle of the star.
+ """
+
+ def __init__(self, connectivity=None, middle_qubit: int = 2):
+ self.middle_qubit = middle_qubit
+ if connectivity is not None: # pragma: no cover
+ log.warning(
+ "StarConnectivityRouter does not use the connectivity graph."
+ "The connectivity graph will be ignored."
+ )
+
+ def __call__(self, circuit: Circuit):
+ """Apply the transpiler transformation on a given circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): The original Qibo circuit to transform.
+ Only single qubit gates and two qubits gates are supported by the router.
+
+ Returns:
+ dict: physical to logical qubit mapping.
+ """
+
+ # find the number of qubits for hardware circuit
+ nqubits = max(circuit.nqubits, self.middle_qubit + 1)
+ hardware_qubits = list(range(nqubits))
+
+ for i, gate in enumerate(circuit.queue):
+ if len(gate.qubits) > 2:
+ raise_error(
+ PlacementError,
+ "Gates targeting more than 2 qubits are not supported",
+ )
+ if len(gate.qubits) == 2:
+ if self.middle_qubit not in gate.qubits:
+ new_middle = _find_connected_qubit(
+ gate.qubits,
+ circuit.queue[i + 1 :],
+ hardware_qubits,
+ error=PlacementError,
+ )
+ hardware_qubits[self.middle_qubit], hardware_qubits[new_middle] = (
+ new_middle,
+ self.middle_qubit,
+ )
+ break
+
+ return dict(zip(["q" + str(i) for i in range(nqubits)], hardware_qubits))
+
+
+class Trivial(Placer):
+ """Place qubits according to the following notation:
+
+ .. math::
+ \\{\\textup{"q0"} : 0, \\textup{"q1"} : 1, ..., \\textup{"qn"} : n}.
+
+ Args:
+ connectivity (networkx.Graph, optional): chip connectivity.
+ """
+
+ def __init__(self, connectivity: nx.Graph = None):
+ self.connectivity = connectivity
+
+ def __call__(self, circuit: Circuit):
+ """Find the trivial placement for the circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (dict): physical to logical qubit mapping.
+ """
+ if self.connectivity is not None:
+ if self.connectivity.number_of_nodes() != circuit.nqubits:
+ raise_error(
+ PlacementError,
+ "The number of nodes of the connectivity graph must match "
+ + "the number of qubits in the circuit",
+ )
+ trivial_layout = dict(
+ zip(
+ ["q" + str(i) for i in list(self.connectivity.nodes())],
+ range(circuit.nqubits),
+ )
+ )
+ else:
+ trivial_layout = dict(
+ zip(
+ ["q" + str(i) for i in range(circuit.nqubits)],
+ range(circuit.nqubits),
+ )
+ )
+ return trivial_layout
+
+
+class Custom(Placer):
+ """Define a custom initial qubit mapping.
+
+ Args:
+ map (list or dict): physical to logical qubit mapping.
+ Examples: :math:`[1,2,0]` or
+ :math:`{\\textup{"q0"}: 1, \\textup{"q1"}: 2, \\textup{"q2"}:0}`
+ to assign the physical qubits :math:`\\{0, 1, 2\\}`
+ to the logical qubits :math:`[1, 2, 0]`.
+ connectivity (:class:`networkx.Graph`, optional): chip connectivity.
+ This argument is necessary if the layout applied to a subset of
+ qubits of the original connectivity graph. Defaults to ``None``.
+ """
+
+ def __init__(self, initial_map: Union[list, dict], connectivity: nx.Graph = None):
+ self.connectivity = connectivity
+ self.initial_map = initial_map
+
+ def __call__(self, circuit=None):
+ """Return the custom placement if it can be applied to the given circuit (if given).
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (dict): physical to logical qubit mapping.
+ """
+ if isinstance(self.initial_map, dict):
+ pass
+ elif isinstance(self.initial_map, list):
+ if self.connectivity is not None:
+ self.initial_map = dict(
+ zip(
+ ["q" + str(i) for i in self.connectivity.nodes()],
+ self.initial_map,
+ )
+ )
+ else:
+ self.initial_map = dict(
+ zip(
+ ["q" + str(i) for i in range(len(self.initial_map))],
+ self.initial_map,
+ )
+ )
+ else:
+ raise_error(TypeError, "Use dict or list to define mapping.")
+ if circuit is not None:
+ assert_placement(circuit, self.initial_map, connectivity=self.connectivity)
+ else:
+ assert_mapping_consistency(self.initial_map, connectivity=self.connectivity)
+ return self.initial_map
+
+
+class Subgraph(Placer):
+ """
+ Subgraph isomorphism qubit placer.
+
+ Since it is a :math:`NP`-complete problem, it can take exponential time for large circuits.
+ This initialization method may fail for very short circuits.
+
+ Attributes:
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ """
+
+ def __init__(self, connectivity: nx.Graph):
+ self.connectivity = connectivity
+
+ def __call__(self, circuit: Circuit):
+ """Find the initial layout of the given circuit using subgraph isomorphism.
+ Circuit must contain at least two two-qubit gates to implement subgraph placement.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (dict): physical to logical qubit mapping.
+ """
+ gates_qubits_pairs = _find_gates_qubits_pairs(circuit)
+ if len(gates_qubits_pairs) < 3:
+ raise_error(
+ ValueError,
+ "Circuit must contain at least two two-qubit gates "
+ + "to implement subgraph placement.",
+ )
+ circuit_subgraph = nx.Graph()
+ circuit_subgraph.add_nodes_from(list(range(circuit.nqubits)))
+ matcher = nx.algorithms.isomorphism.GraphMatcher(
+ self.connectivity, circuit_subgraph
+ )
+ i = 0
+ circuit_subgraph.add_edge(gates_qubits_pairs[i][0], gates_qubits_pairs[i][1])
+ while matcher.subgraph_is_monomorphic():
+ result = matcher
+ i += 1
+ circuit_subgraph.add_edge(
+ gates_qubits_pairs[i][0], gates_qubits_pairs[i][1]
+ )
+ matcher = nx.algorithms.isomorphism.GraphMatcher(
+ self.connectivity, circuit_subgraph
+ )
+ if (
+ self.connectivity.number_of_edges()
+ == circuit_subgraph.number_of_edges()
+ or i == len(gates_qubits_pairs) - 1
+ ):
+ break
+
+ sorted_result = dict(sorted(result.mapping.items()))
+
+ return {"q" + str(k): v for k, v in sorted_result.items()}
+
+
+class Random(Placer):
+ """
+ Random initialization with greedy policy, let a maximum number of 2-qubit
+ gates can be applied without introducing any SWAP gate.
+
+ Attributes:
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ samples (int, optional): number of initial random layouts tested.
+ Defaults to :math:`100`.
+ seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
+ random numbers or a fixed seed to initialize a generator. If ``None``,
+ initializes a generator with a random seed. Defaults to ``None``.
+ """
+
+ def __init__(self, connectivity, samples: int = 100, seed=None):
+ self.connectivity = connectivity
+ self.samples = samples
+ self.seed = seed
+
+ def __call__(self, circuit):
+ """Find an initial layout of the given circuit using random greedy algorithm.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): Circuit to be transpiled.
+
+ Returns:
+ (dict): physical-to-logical qubit mapping.
+ """
+ _, local_state = _check_backend_and_local_state(self.seed, backend=None)
+ gates_qubits_pairs = _find_gates_qubits_pairs(circuit)
+ nodes = self.connectivity.number_of_nodes()
+ keys = list(self.connectivity.nodes())
+ dict_keys = ["q" + str(i) for i in keys]
+
+ final_mapping = dict(zip(keys, range(nodes)))
+ final_graph = nx.relabel_nodes(self.connectivity, final_mapping)
+ final_cost = self._cost(final_graph, gates_qubits_pairs)
+ for _ in range(self.samples):
+ mapping = dict(
+ zip(keys, local_state.choice(range(nodes), nodes, replace=False))
+ )
+ graph = nx.relabel_nodes(self.connectivity, mapping)
+ cost = self._cost(graph, gates_qubits_pairs)
+
+ if cost == 0:
+ final_layout = dict(zip(dict_keys, list(mapping.values())))
+ return dict(sorted(final_layout.items()))
+
+ if cost < final_cost:
+ final_graph = graph
+ final_mapping = mapping
+ final_cost = cost
+
+ final_layout = dict(zip(dict_keys, list(final_mapping.values())))
+ return dict(sorted(final_layout.items()))
+
+ def _cost(self, graph: nx.Graph, gates_qubits_pairs: list):
+ """
+ Compute the cost associated to an initial layout as the lengh of the reduced circuit.
+
+ Args:
+ graph (:class:`networkx.Graph`): current hardware qubit mapping.
+ gates_qubits_pairs (list): circuit representation.
+
+ Returns:
+ (int): lengh of the reduced circuit.
+ """
+ for allowed, gate in enumerate(gates_qubits_pairs):
+ if gate not in graph.edges():
+ return len(gates_qubits_pairs) - allowed - 1
+
+ return 0
+
+
+class ReverseTraversal(Placer):
+ """
+ Places qubits based on the algorithm proposed in Reference [1].
+
+ Compatible with all the available ``Router``s.
+
+ Args:
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ routing_algorithm (:class:`qibo.transpiler.abstract.Router`): routing algorithm.
+ depth (int, optional): number of two-qubit gates considered before finding initial layout.
+ If ``None`` just one backward step will be implemented.
+ If depth is greater than the number of two-qubit gates in the circuit,
+ the circuit will be routed more than once.
+ Example: on a circuit with four two-qubit gates :math:`A-B-C-D`
+ using depth :math:`d = 6`, the routing will be performed
+ on the circuit :math:`C-D-D-C-B-A`.
+
+ References:
+ 1. G. Li, Y. Ding, and Y. Xie,
+ *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*.
+ `arXiv:1809.02573 [cs.ET] `_.
+ """
+
+ def __init__(
+ self,
+ connectivity: nx.Graph,
+ routing_algorithm: Router,
+ depth: Optional[int] = None,
+ ):
+ self.connectivity = connectivity
+ self.routing_algorithm = routing_algorithm
+ self.depth = depth
+
+ def __call__(self, circuit: Circuit):
+ """Find the initial layout of the given circuit using Reverse Traversal placement.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (dict): physical to logical qubit mapping.
+ """
+ initial_placer = Trivial(self.connectivity)
+ initial_placement = initial_placer(circuit=circuit)
+ new_circuit = self._assemble_circuit(circuit)
+ final_placement = self._routing_step(initial_placement, new_circuit)
+
+ return final_placement
+
+ def _assemble_circuit(self, circuit: Circuit):
+ """Assemble a single circuit to apply Reverse Traversal placement based on depth.
+
+ Example: for a circuit with four two-qubit gates :math:`A-B-C-D`
+ using depth :math:`d = 6`, the function will return the circuit :math:`C-D-D-C-B-A`.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`): assembled circuit to perform
+ Reverse Traversal placement.
+ """
+
+ if self.depth is None:
+ return circuit.invert()
+
+ gates_qubits_pairs = _find_gates_qubits_pairs(circuit)
+ circuit_gates = len(gates_qubits_pairs)
+ if circuit_gates == 0:
+ raise_error(
+ ValueError, "The circuit must contain at least a two-qubit gate."
+ )
+ repetitions, remainder = divmod(self.depth, circuit_gates)
+
+ assembled_gates_qubits_pairs = []
+ for _ in range(repetitions):
+ assembled_gates_qubits_pairs += gates_qubits_pairs[:]
+ gates_qubits_pairs.reverse()
+ assembled_gates_qubits_pairs += gates_qubits_pairs[0:remainder]
+
+ new_circuit = Circuit(circuit.nqubits)
+ for qubits in assembled_gates_qubits_pairs:
+ # As only the connectivity is important here we can replace everything with CZ gates
+ new_circuit.add(gates.CZ(qubits[0], qubits[1]))
+
+ return new_circuit.invert()
+
+ def _routing_step(self, layout: dict, circuit: Circuit):
+ """Perform routing of the circuit.
+
+ Args:
+ layout (dict): intial qubit layout.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed.
+ """
+ _, final_mapping = self.routing_algorithm(circuit, layout)
+
+ return final_mapping
diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py
new file mode 100644
index 000000000..6d138fbc7
--- /dev/null
+++ b/src/qibo/transpiler/router.py
@@ -0,0 +1,980 @@
+import random
+from copy import deepcopy
+from typing import Optional, Union
+
+import networkx as nx
+import numpy as np
+
+from qibo import gates
+from qibo.config import log, raise_error
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import ConnectivityError
+from qibo.transpiler.abstract import Router
+from qibo.transpiler.blocks import Block, CircuitBlocks
+
+
+def assert_connectivity(connectivity: nx.Graph, circuit: Circuit):
+ """Assert if a circuit can be executed on Hardware.
+
+ No gates acting on more than two qubits.
+ All two-qubit operations can be performed on hardware.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit model to check.
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ """
+ if list(connectivity.nodes) != list(range(connectivity.number_of_nodes())):
+ node_mapping = {node: i for i, node in enumerate(connectivity.nodes)}
+ new_connectivity = nx.Graph()
+ new_connectivity.add_edges_from(
+ [(node_mapping[u], node_mapping[v]) for u, v in connectivity.edges]
+ )
+ connectivity = new_connectivity
+ for gate in circuit.queue:
+ if len(gate.qubits) > 2 and not isinstance(gate, gates.M):
+ raise_error(ConnectivityError, f"{gate.name} acts on more than two qubits.")
+ if len(gate.qubits) == 2:
+ if (gate.qubits[0], gate.qubits[1]) not in connectivity.edges:
+ raise_error(
+ ConnectivityError,
+ f"Circuit does not respect connectivity. {gate.name} acts on {gate.qubits}.",
+ )
+
+
+class StarConnectivityRouter(Router):
+ """Transforms an arbitrary circuit to one that can be executed on hardware.
+
+ This transpiler produces a circuit that respects the following connectivity:
+
+ q
+ |
+ q -- q -- q
+ |
+ q
+
+ by adding SWAP gates when needed.
+
+ Args:
+ connectivity (:class:`networkx.Graph`): chip connectivity, not used for this transpiler.
+ middle_qubit (int, optional): qubit id of the qubit that is in the middle of the star.
+ """
+
+ def __init__(self, connectivity=None, middle_qubit: int = 2):
+ self.middle_qubit = middle_qubit
+ if connectivity is not None: # pragma: no cover
+ log.warning(
+ "StarConnectivityRouter does not use the connectivity graph."
+ "The connectivity graph will be ignored."
+ )
+
+ def __call__(self, circuit: Circuit, initial_layout: dict):
+ """Apply the transpiler transformation on a given circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): The original Qibo circuit to transform.
+ Only single qubit gates and two qubits gates are supported by the router.
+ initial_layout (dict): initial physical-to-logical qubit mapping,
+ use `qibo.transpiler.placer.StarConnectivityPlacer` for better performance.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`, list): circuit that performs the same operation
+ as the original but respects the hardware connectivity,
+ and list that maps logical to hardware qubits.
+ """
+
+ middle_qubit = self.middle_qubit
+ nqubits = max(circuit.nqubits, middle_qubit + 1)
+ # new circuit object that will be compatible with hardware connectivity
+ new = Circuit(nqubits)
+ # list to maps logical to hardware qubits
+ hardware_qubits = list(initial_layout.values())
+
+ for i, gate in enumerate(circuit.queue):
+ # map gate qubits to hardware
+ qubits = tuple(hardware_qubits.index(q) for q in gate.qubits)
+ if isinstance(gate, gates.M):
+ new_gate = gates.M(*qubits, **gate.init_kwargs)
+ new_gate.result = gate.result
+ new.add(new_gate)
+ continue
+
+ if len(qubits) > 2:
+ raise_error(
+ ConnectivityError,
+ "Gates targeting more than two qubits are not supported.",
+ )
+
+ if len(qubits) == 2 and middle_qubit not in qubits:
+ # find which qubit should be moved
+ new_middle = _find_connected_qubit(
+ qubits,
+ circuit.queue[i + 1 :],
+ hardware_qubits,
+ error=ConnectivityError,
+ )
+ # update hardware qubits according to the swap
+ hardware_qubits[middle_qubit], hardware_qubits[new_middle] = (
+ hardware_qubits[new_middle],
+ hardware_qubits[middle_qubit],
+ )
+ new.add(gates.SWAP(middle_qubit, new_middle))
+ # update gate qubits according to the new swap
+ qubits = tuple(hardware_qubits.index(q) for q in gate.qubits)
+
+ # add gate to the hardware circuit
+ if isinstance(gate, gates.Unitary):
+ # gates.Unitary requires matrix as first argument
+ matrix = gate.init_args[0]
+ new.add(gate.__class__(matrix, *qubits, **gate.init_kwargs))
+ else:
+ new.add(gate.__class__(*qubits, **gate.init_kwargs))
+ hardware_qubits_keys = ["q" + str(i) for i in range(5)]
+ return new, dict(zip(hardware_qubits_keys, hardware_qubits))
+
+
+def _find_connected_qubit(qubits, queue, hardware_qubits, error):
+ """Helper method for :meth:`qibo.transpiler.router.StarConnectivityRouter`
+ and :meth:`qibo.transpiler.router.StarConnectivityPlacer`.
+
+ Finds which qubit should be mapped to hardware middle qubit
+ by looking at the two-qubit gates that follow.
+ """
+ possible_qubits = set(qubits)
+ for next_gate in queue:
+ if len(next_gate.qubits) > 2:
+ raise_error(
+ error,
+ "Gates targeting more than 2 qubits are not supported",
+ )
+ if len(next_gate.qubits) == 2:
+ possible_qubits &= {hardware_qubits.index(q) for q in next_gate.qubits}
+
+ if not possible_qubits:
+ return qubits[0]
+
+ if len(possible_qubits) == 1:
+ return possible_qubits.pop()
+
+ return qubits[0]
+
+
+class CircuitMap:
+ """Class that stores the circuit and physical-logical mapping during routing.
+
+ Also implements the initial two-qubit block decompositions.
+
+ Args:
+ initial_layout (dict): initial logical-to-physical qubit mapping.
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed.
+ blocks (:class:`qibo.transpiler.blocks.CircuitBlocks`, optional): circuit
+ block representation. If ``None``, the blocks will be computed from the circuit.
+ Defaults to ``None``.
+ """
+
+ def __init__(
+ self,
+ initial_layout: dict,
+ circuit: Circuit,
+ blocks: Optional[CircuitBlocks] = None,
+ ):
+ if blocks is not None:
+ self.circuit_blocks = blocks
+ else:
+ self.circuit_blocks = CircuitBlocks(circuit, index_names=True)
+ # Order the initial layout based on the hardware qubit names
+ # to avoid problems in custom layouts
+ self.initial_layout = dict(sorted(initial_layout.items()))
+ self._graph_qubits_names = [int(key[1:]) for key in self.initial_layout.keys()]
+ self._circuit_logical = list(range(len(self.initial_layout)))
+ self._physical_logical = list(self.initial_layout.values())
+ self._routed_blocks = CircuitBlocks(Circuit(circuit.nqubits))
+ self._swaps = 0
+
+ def set_circuit_logical(self, circuit_logical_map: list):
+ """Sets the current circuit to logical qubit mapping.
+
+ Method works in-place.
+
+ Args:
+ circuit_logical_map (list): logical mapping.
+ """
+ self._circuit_logical = circuit_logical_map
+
+ def blocks_qubits_pairs(self):
+ """Returns a list containing the qubit pairs of each block."""
+ return [block.qubits for block in self.circuit_blocks()]
+
+ def execute_block(self, block: Block):
+ """Executes a block by removing it from the circuit representation
+ and adding it to the routed circuit.
+
+ Method works in-place.
+
+ Args:
+ block (:class:`qibo.transpiler.blocks.Block`): block to be removed.
+ """
+ self._routed_blocks.add_block(
+ block.on_qubits(self.get_physical_qubits(block, index=True))
+ )
+ self.circuit_blocks.remove_block(block)
+
+ def routed_circuit(self, circuit_kwargs: Optional[dict] = None):
+ """Returns the routed circuit.
+
+ Args:
+ circuit_kwargs (dict): original circuit ``init_kwargs``.
+
+ Returns:
+ :class:`qibo.models.circuit.Circuit`: Routed circuit.
+ """
+ return self._routed_blocks.circuit(circuit_kwargs=circuit_kwargs)
+
+ def final_layout(self):
+ """Returns the final physical-circuit qubits mapping."""
+ unsorted_dict = {
+ "q" + str(self.circuit_to_physical(i)): i
+ for i in range(len(self._circuit_logical))
+ }
+
+ return dict(sorted(unsorted_dict.items()))
+
+ def update(self, swap: tuple):
+ """Updates the logical-physical qubit mapping after applying a ``SWAP``
+
+ Adds the :class:`qibo.gates.gates.SWAP` gate to the routed blocks.
+ Method works in-place.
+
+ Args:
+ swap (tuple): tuple containing the logical qubits to be swapped.
+ """
+ physical_swap = self.logical_to_physical(swap, index=True)
+ self._routed_blocks.add_block(
+ Block(qubits=physical_swap, gates=[gates.SWAP(*physical_swap)])
+ )
+ self._swaps += 1
+ idx_0, idx_1 = self._circuit_logical.index(
+ swap[0]
+ ), self._circuit_logical.index(swap[1])
+ self._circuit_logical[idx_0], self._circuit_logical[idx_1] = swap[1], swap[0]
+
+ def get_logical_qubits(self, block: Block):
+ """Returns the current logical qubits where a block is acting on.
+
+ Args:
+ block (:class:`qibo.transpiler.blocks.Block`): block to be analysed.
+
+ Returns:
+ tuple: logical qubits where a block is acting on.
+ """
+ return self.circuit_to_logical(block.qubits)
+
+ def get_physical_qubits(self, block: Union[int, Block], index: bool = False):
+ """Returns the physical qubits where a block is acting on.
+
+ Args:
+ block (int or :class:`qibo.transpiler.blocks.Block`): block to be analysed.
+ index (bool, optional): If ``True``, qubits are returned as indices of
+ the connectivity nodes. Defaults to ``False``.
+
+ Returns:
+ tuple: physical qubits where a block is acting on.
+
+ """
+ if isinstance(block, int):
+ block = self.circuit_blocks.search_by_index(block)
+
+ return self.logical_to_physical(self.get_logical_qubits(block), index=index)
+
+ def logical_to_physical(self, logical_qubits: tuple, index: bool = False):
+ """Returns the physical qubits associated to the logical qubits.
+
+ Args:
+ logical_qubits (tuple): physical qubits.
+ index (bool, optional): If ``True``, qubits are returned as indices of
+ `the connectivity nodes. Defaults to ``False``.
+
+ Returns:
+ tuple: physical qubits associated to the logical qubits.
+ """
+ if not index:
+ return tuple(
+ self._graph_qubits_names[
+ self._physical_logical.index(logical_qubits[i])
+ ]
+ for i in range(2)
+ )
+
+ return tuple(self._physical_logical.index(logical_qubits[i]) for i in range(2))
+
+ def circuit_to_logical(self, circuit_qubits: tuple):
+ """Returns the current logical qubits associated to the initial circuit qubits.
+
+ Args:
+ circuit_qubits (tuple): circuit qubits.
+
+ Returns:
+ tuple: logical qubits.
+ """
+ return tuple(self._circuit_logical[circuit_qubits[i]] for i in range(2))
+
+ def circuit_to_physical(self, circuit_qubit: int):
+ """Returns the current physical qubit associated to an initial circuit qubit.
+
+ Args:
+ circuit_qubit (int): circuit qubit.
+
+ Returns:
+ int: physical qubit.
+ """
+ return self._graph_qubits_names[
+ self._physical_logical.index(self._circuit_logical[circuit_qubit])
+ ]
+
+ def physical_to_logical(self, physical_qubit: int):
+ """Returns current logical qubit associated to a physical qubit (connectivity graph node).
+
+ Args:
+ physical_qubit (int): physical qubit.
+
+ Returns:
+ int: logical qubit.
+ """
+ physical_qubit_index = self._graph_qubits_names.index(physical_qubit)
+
+ return self._physical_logical[physical_qubit_index]
+
+
+class ShortestPaths(Router):
+ """A class to perform initial qubit mapping and connectivity matching.
+
+ Args:
+ connectivity (:class:`networkx.Graph`): chip connectivity.
+ seed (int, optional): seed for the random number generator.
+ If ``None``, defaults to :math:`42`. Defaults to ``None``.
+ """
+
+ def __init__(self, connectivity: nx.Graph, seed: Optional[int] = None):
+ self.connectivity = connectivity
+ self._front_layer = None
+ self.circuit = None
+ self._dag = None
+ self._final_measurements = None
+ if seed is None:
+ seed = 42
+ random.seed(seed)
+
+ @property
+ def added_swaps(self):
+ """Returns the number of SWAP gates added to the circuit during routing."""
+ return self.circuit._swaps
+
+ def __call__(self, circuit: Circuit, initial_layout: dict):
+ """Circuit connectivity matching.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be matched
+ to hardware connectivity.
+ initial_layout (dict): initial physical-to-logical qubit mapping
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology,
+ and final physical-to-logical qubit mapping.
+ """
+ self._preprocessing(circuit=circuit, initial_layout=initial_layout)
+ while self._dag.number_of_nodes() != 0:
+ execute_block_list = self._check_execution()
+ if execute_block_list is not None:
+ self._execute_blocks(execute_block_list)
+ else:
+ self._find_new_mapping()
+
+ circuit_kwargs = circuit.init_kwargs
+ circuit_kwargs["wire_names"] = list(initial_layout.keys())
+ routed_circuit = self.circuit.routed_circuit(circuit_kwargs=circuit_kwargs)
+ if self._final_measurements is not None:
+ routed_circuit = self._append_final_measurements(
+ routed_circuit=routed_circuit
+ )
+
+ return routed_circuit, self.circuit.final_layout()
+
+ def _find_new_mapping(self):
+ """Find new qubit mapping. Mapping is found by looking for the shortest path.
+
+ Method works in-place.
+ """
+ candidates_evaluation = []
+ for candidate in self._candidates():
+ cost = self._compute_cost(candidate)
+ candidates_evaluation.append((candidate, cost))
+ best_cost = min(candidate[1] for candidate in candidates_evaluation)
+ best_candidates = [
+ candidate[0]
+ for candidate in candidates_evaluation
+ if candidate[1] == best_cost
+ ]
+ best_candidate = random.choice(best_candidates)
+ self._add_swaps(best_candidate, self.circuit)
+
+ def _candidates(self):
+ """Returns all possible shortest paths in a ``list`` that contains
+ the new mapping and a second ``list`` containing the path meeting point.
+ """
+ target_qubits = self.circuit.get_physical_qubits(self._front_layer[0])
+ path_list = list(
+ nx.all_shortest_paths(
+ self.connectivity, source=target_qubits[0], target=target_qubits[1]
+ )
+ )
+ all_candidates = []
+ for path in path_list:
+ for meeting_point in range(len(path) - 1):
+ all_candidates.append((path, meeting_point))
+
+ return all_candidates
+
+ @staticmethod
+ def _add_swaps(candidate: tuple, circuitmap: CircuitMap):
+ """Adds swaps to the circuit to move qubits.
+
+ Method works in-place.
+
+ Args:
+ candidate (tuple): contains path to move qubits and qubit meeting point in the path.
+ circuitmap (CircuitMap): representation of the circuit.
+ """
+ path = candidate[0]
+ meeting_point = candidate[1]
+ forward = path[0 : meeting_point + 1]
+ backward = list(reversed(path[meeting_point + 1 :]))
+ if len(forward) > 1:
+ for f1, f2 in zip(forward[:-1], forward[1:]):
+ circuitmap.update(
+ (
+ circuitmap.physical_to_logical(f1),
+ circuitmap.physical_to_logical(f2),
+ )
+ )
+ if len(backward) > 1:
+ for b1, b2 in zip(backward[:-1], backward[1:]):
+ circuitmap.update(
+ (
+ circuitmap.physical_to_logical(b1),
+ circuitmap.physical_to_logical(b2),
+ )
+ )
+
+ def _compute_cost(self, candidate: tuple):
+ """Greedy algorithm that decides which path to take and how qubits should be walked.
+
+ The cost is computed as minus the number of successive gates that can be executed.
+
+ Args:
+ candidate (tuple): contains path to move qubits and qubit meeting point in the path.
+
+ Returns:
+ (list, int): best path to move qubits and qubit meeting point in the path.
+ """
+ temporary_circuit = CircuitMap(
+ initial_layout=self.circuit.initial_layout,
+ circuit=Circuit(len(self.circuit.initial_layout)),
+ blocks=deepcopy(self.circuit.circuit_blocks),
+ )
+ temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical))
+ self._add_swaps(candidate, temporary_circuit)
+ temporary_dag = deepcopy(self._dag)
+ successive_executed_gates = 0
+ while temporary_dag.number_of_nodes() != 0:
+ for layer, nodes in enumerate(nx.topological_generations(temporary_dag)):
+ for node in nodes:
+ temporary_dag.nodes[node]["layer"] = layer
+ temporary_front_layer = [
+ node[0] for node in temporary_dag.nodes(data="layer") if node[1] == 0
+ ]
+ all_executed = True
+ for block in temporary_front_layer:
+ if (
+ temporary_circuit.get_physical_qubits(block)
+ in self.connectivity.edges
+ or not temporary_circuit.circuit_blocks.search_by_index(
+ block
+ ).entangled
+ ):
+ successive_executed_gates += 1
+ temporary_circuit.execute_block(
+ temporary_circuit.circuit_blocks.search_by_index(block)
+ )
+ temporary_dag.remove_node(block)
+ else:
+ all_executed = False
+ if not all_executed:
+ break
+
+ return -successive_executed_gates
+
+ def _check_execution(self):
+ """Checks if some blocks in the front layer can be executed in the current configuration.
+
+ Returns:
+ (list): executable blocks if there are, ``None`` otherwise.
+ """
+ executable_blocks = []
+ for block in self._front_layer:
+ if (
+ self.circuit.get_physical_qubits(block) in self.connectivity.edges
+ or not self.circuit.circuit_blocks.search_by_index(block).entangled
+ ):
+ executable_blocks.append(block)
+ if len(executable_blocks) == 0:
+ return None
+
+ return executable_blocks
+
+ def _execute_blocks(self, blocklist: list):
+ """Executes a list of blocks:
+ -Remove the correspondent nodes from the dag and circuit representation.
+ -Add the executed blocks to the routed circuit.
+ -Update the dag layers and front layer.
+
+ Method works in-place.
+
+ Args:
+ blocklist (list): list of blocks.
+ """
+ for block_id in blocklist:
+ block = self.circuit.circuit_blocks.search_by_index(block_id)
+ self.circuit.execute_block(block)
+ self._dag.remove_node(block_id)
+ self._update_front_layer()
+
+ def _update_front_layer(self):
+ """Updates the front layer of the dag.
+
+ Method works in-place.
+ """
+ for layer, nodes in enumerate(nx.topological_generations(self._dag)):
+ for node in nodes:
+ self._dag.nodes[node]["layer"] = layer
+ self._front_layer = [
+ node[0] for node in self._dag.nodes(data="layer") if node[1] == 0
+ ]
+
+ def _preprocessing(self, circuit: Circuit, initial_layout: dict):
+ """The following objects will be initialised:
+ - circuit: class to represent circuit and to perform logical-physical qubit mapping.
+ - _final_measurements: measurement gates at the end of the circuit.
+ - _front_layer: list containing the blocks to be executed.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed.
+ initial_layout (dict): initial physical-to-logical qubit mapping.
+ """
+ copied_circuit = circuit.copy(deep=True)
+ self._final_measurements = self._detach_final_measurements(copied_circuit)
+ self.circuit = CircuitMap(initial_layout, copied_circuit)
+ self._dag = _create_dag(self.circuit.blocks_qubits_pairs())
+ self._update_front_layer()
+
+ def _detach_final_measurements(self, circuit: Circuit):
+ """Detaches measurement gates at the end of the circuit for separate handling.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuits to be processed.
+
+ Returns:
+ (NoneType or list): list of measurements. If no measurements, returns ``None``.
+ """
+ final_measurements = []
+ for gate in circuit.queue[::-1]:
+ if isinstance(gate, gates.M):
+ final_measurements.append(gate)
+ circuit.queue.remove(gate)
+ else:
+ break
+ if not final_measurements:
+ return None
+
+ return final_measurements[::-1]
+
+ def _append_final_measurements(self, routed_circuit: Circuit):
+ """Appends the final measurment gates on the correct qubits
+ conserving the measurement register."""
+ for measurement in self._final_measurements:
+ original_qubits = measurement.qubits
+ routed_qubits = (
+ self.circuit.circuit_to_physical(qubit) for qubit in original_qubits
+ )
+ routed_circuit.add(
+ measurement.on_qubits(dict(zip(original_qubits, routed_qubits)))
+ )
+
+ return routed_circuit
+
+
+class Sabre(Router):
+ """Routing algorithm proposed in Ref [1].
+
+ Args:
+ connectivity (:class:`networkx.Graph`): hardware chip connectivity.
+ lookahead (int, optional): lookahead factor, how many dag layers will be considered
+ in computing the cost. Defaults to :math:`2`.
+ decay_lookahead (float, optional): value in interval :math:`[0, 1]`.
+ How the weight of the distance in the dag layers decays in computing the cost.
+ Defaults to :math:`0.6`.
+ delta (float, optional): defines the number of SWAPs vs depth trade-off by deciding
+ how the algorithm tends to select non-overlapping SWAPs.
+ Defaults to math:`10^{-3}`.
+ seed (int, optional): seed for the candidate random choice as tiebraker.
+ Defaults to ``None``.
+ swap_threshold (float, optional): limits the number of added SWAPs in every routing iteration.
+ This threshold is multiplied by the length of the longest path in the circuit connectivity.
+ If the number of added SWAPs exceeds the threshold before a gate is routed,
+ shortestpath routing is applied.
+ Defaults to :math:`1.5`.
+
+ References:
+ 1. G. Li, Y. Ding, and Y. Xie,
+ *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*.
+ `arXiv:1809.02573 [cs.ET] `_.
+ """
+
+ def __init__(
+ self,
+ connectivity: nx.Graph,
+ lookahead: int = 2,
+ decay_lookahead: float = 0.6,
+ delta: float = 0.001,
+ swap_threshold: float = 1.5,
+ seed: Optional[int] = None,
+ ):
+ self.connectivity = connectivity
+ self.lookahead = lookahead
+ self.decay = decay_lookahead
+ self.delta = delta
+ self.swap_threshold = swap_threshold
+ self._delta_register = None
+ self._dist_matrix = None
+ self._dag = None
+ self._front_layer = None
+ self.circuit = None
+ self._memory_map = None
+ self._final_measurements = None
+ self._temporary_added_swaps = 0
+ self._saved_circuit = None
+ random.seed(seed)
+
+ def __call__(self, circuit: Circuit, initial_layout: dict):
+ """Route the circuit.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed.
+ initial_layout (dict): initial physical to logical qubit mapping.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`, dict): routed circuit and final layout.
+ """
+ self._preprocessing(circuit=circuit, initial_layout=initial_layout)
+ self._saved_circuit = deepcopy(self.circuit)
+ longest_path = np.max(self._dist_matrix)
+
+ while self._dag.number_of_nodes() != 0:
+ execute_block_list = self._check_execution()
+ if execute_block_list is not None:
+ self._execute_blocks(execute_block_list)
+ else:
+ self._find_new_mapping()
+
+ # If the number of added swaps is too high, the algorithm is stuck.
+ # Reset the circuit to the last saved state and make the nearest gate executable by manually adding SWAPs.
+ if (
+ self._temporary_added_swaps > self.swap_threshold * longest_path
+ ): # threshold is arbitrary
+ self.circuit = deepcopy(self._saved_circuit)
+ self._shortest_path_routing()
+
+ circuit_kwargs = circuit.init_kwargs
+ circuit_kwargs["wire_names"] = list(initial_layout.keys())
+ routed_circuit = self.circuit.routed_circuit(circuit_kwargs=circuit_kwargs)
+ if self._final_measurements is not None:
+ routed_circuit = self._append_final_measurements(
+ routed_circuit=routed_circuit
+ )
+
+ return routed_circuit, self.circuit.final_layout()
+
+ @property
+ def added_swaps(self):
+ """Returns the number of SWAP gates added to the circuit during routing."""
+ return self.circuit._swaps
+
+ def _preprocessing(self, circuit: Circuit, initial_layout: dict):
+ """The following objects will be initialised:
+ - circuit: class to represent circuit and to perform logical-physical qubit mapping.
+ - _final_measurements: measurement gates at the end of the circuit.
+ - _dist_matrix: matrix reporting the shortest path lengh between all node pairs.
+ - _dag: direct acyclic graph of the circuit based on commutativity.
+ - _memory_map: list to remember previous SWAP moves.
+ - _front_layer: list containing the blocks to be executed.
+ - _delta_register: list containing the special weigh added to qubits
+ to prevent overlapping swaps.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed.
+ initial_layout (dict): initial physical-to-logical qubit mapping.
+ """
+ copied_circuit = circuit.copy(deep=True)
+ self._final_measurements = self._detach_final_measurements(copied_circuit)
+ self.circuit = CircuitMap(initial_layout, copied_circuit)
+ self._dist_matrix = nx.floyd_warshall_numpy(self.connectivity)
+ self._dag = _create_dag(self.circuit.blocks_qubits_pairs())
+ self._memory_map = []
+ self._update_dag_layers()
+ self._update_front_layer()
+ self._delta_register = [1.0 for _ in range(circuit.nqubits)]
+
+ def _detach_final_measurements(self, circuit: Circuit):
+ """Detach measurement gates at the end of the circuit for separate handling."""
+ final_measurements = []
+ for gate in circuit.queue[::-1]:
+ if isinstance(gate, gates.M):
+ final_measurements.append(gate)
+ circuit.queue.remove(gate)
+ else:
+ break
+ if not final_measurements:
+ return None
+ return final_measurements[::-1]
+
+ def _append_final_measurements(self, routed_circuit: Circuit):
+ """Appends final measurment gates on the correct qubits conserving the measurement register.
+
+ Args:
+ routed_circuit (:class:`qibo.models.circuit.Circuit`): original circuit.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`) routed circuit.
+ """
+ for measurement in self._final_measurements:
+ original_qubits = measurement.qubits
+ routed_qubits = list(
+ self.circuit.circuit_to_physical(qubit) for qubit in original_qubits
+ )
+ routed_circuit.add(
+ measurement.on_qubits(dict(zip(original_qubits, routed_qubits)))
+ )
+ return routed_circuit
+
+ def _update_dag_layers(self):
+ """Update dag layers and put them in topological order.
+
+ Method works in-place.
+ """
+ for layer, nodes in enumerate(nx.topological_generations(self._dag)):
+ for node in nodes:
+ self._dag.nodes[node]["layer"] = layer
+
+ def _update_front_layer(self):
+ """Update the front layer of the dag.
+
+ Method works in-place.
+ """
+ self._front_layer = self._get_dag_layer(0)
+
+ def _get_dag_layer(self, n_layer):
+ """Return the :math:`n`-topological layer of the dag."""
+ return [node[0] for node in self._dag.nodes(data="layer") if node[1] == n_layer]
+
+ def _find_new_mapping(self):
+ """Find the new best mapping by adding one swap."""
+ candidates_evaluation = {}
+ self._memory_map.append(deepcopy(self.circuit._circuit_logical))
+ for candidate in self._swap_candidates():
+ candidates_evaluation[candidate] = self._compute_cost(candidate)
+
+ best_cost = min(candidates_evaluation.values())
+ best_candidates = [
+ key for key, value in candidates_evaluation.items() if value == best_cost
+ ]
+ best_candidate = random.choice(best_candidates)
+
+ for qubit in self.circuit.logical_to_physical(best_candidate, index=True):
+ self._delta_register[qubit] += self.delta
+ self.circuit.update(best_candidate)
+ self._temporary_added_swaps += 1
+
+ def _compute_cost(self, candidate: int):
+ """Compute the cost associated to a possible SWAP candidate."""
+ temporary_circuit = CircuitMap(
+ initial_layout=self.circuit.initial_layout,
+ circuit=Circuit(len(self.circuit.initial_layout)),
+ blocks=self.circuit.circuit_blocks,
+ )
+ temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical))
+ temporary_circuit.update(candidate)
+
+ if temporary_circuit._circuit_logical in self._memory_map:
+ return float("inf")
+
+ tot_distance = 0.0
+ weight = 1.0
+ for layer in range(self.lookahead + 1):
+ layer_gates = self._get_dag_layer(layer)
+ avg_layer_distance = 0.0
+ for gate in layer_gates:
+ qubits = temporary_circuit.get_physical_qubits(gate, index=True)
+ avg_layer_distance += (
+ max(self._delta_register[i] for i in qubits)
+ * (self._dist_matrix[qubits[0], qubits[1]] - 1.0)
+ / len(layer_gates)
+ )
+ tot_distance += weight * avg_layer_distance
+ weight *= self.decay
+
+ return tot_distance
+
+ def _swap_candidates(self):
+ """Returns a list of possible candidate SWAPs to be applied on logical qubits directly.
+
+ The possible candidates are the ones sharing at least one qubit
+ with a block in the front layer.
+
+ Returns:
+ (list): list of candidates.
+ """
+ candidates = []
+ for block in self._front_layer:
+ for qubit in self.circuit.get_physical_qubits(block):
+ for connected in self.connectivity.neighbors(qubit):
+ candidate = tuple(
+ sorted(
+ (
+ self.circuit.physical_to_logical(qubit),
+ self.circuit.physical_to_logical(connected),
+ )
+ )
+ )
+ if candidate not in candidates:
+ candidates.append(candidate)
+
+ return candidates
+
+ def _check_execution(self):
+ """Check if some blocks in the front layer can be executed in the current configuration.
+
+ Returns:
+ (list): executable blocks if there are, ``None`` otherwise.
+ """
+ executable_blocks = []
+ for block in self._front_layer:
+ if (
+ self.circuit.get_physical_qubits(block) in self.connectivity.edges
+ or not self.circuit.circuit_blocks.search_by_index(block).entangled
+ ):
+ executable_blocks.append(block)
+
+ if len(executable_blocks) == 0:
+ return None
+
+ return executable_blocks
+
+ def _execute_blocks(self, blocklist: list):
+ """Executes a list of blocks:
+ -Remove the correspondent nodes from the dag and circuit representation.
+ -Add the executed blocks to the routed circuit.
+ -Update the dag layers and front layer.
+ -Reset the mapping memory.
+
+ Method works in-place.
+
+ Args:
+ blocklist (list): list of blocks.
+ """
+ for block_id in blocklist:
+ block = self.circuit.circuit_blocks.search_by_index(block_id)
+ self.circuit.execute_block(block)
+ self._dag.remove_node(block_id)
+ self._update_dag_layers()
+ self._update_front_layer()
+ self._memory_map = []
+ self._delta_register = [1.0 for _ in self._delta_register]
+ self._temporary_added_swaps = 0
+ self._saved_circuit = deepcopy(self.circuit)
+
+ def _shortest_path_routing(self):
+ """Route a gate in the front layer using the shortest path. This method is executed when the standard SABRE fails to find an optimized solution.
+
+ Method works in-place.
+ """
+
+ min_distance = float("inf")
+ shortest_path_qubits = None
+
+ for block in self._front_layer:
+ q1, q2 = self.circuit.get_physical_qubits(block)
+ distance = self._dist_matrix[q1, q2]
+
+ if distance < min_distance:
+ min_distance = distance
+ shortest_path_qubits = [q1, q2]
+
+ shortest_path = nx.bidirectional_shortest_path(
+ self.connectivity, shortest_path_qubits[0], shortest_path_qubits[1]
+ )
+
+ # Q1 is moved
+ shortest_path = [
+ self.circuit.physical_to_logical(q) for q in shortest_path[:-1]
+ ]
+ swaps = list(zip(shortest_path[:-1], shortest_path[1:]))
+
+ for swap in swaps:
+ self.circuit.update(swap)
+
+
+def _create_dag(gates_qubits_pairs: list):
+ """Helper method for :meth:`qibo.transpiler.router.Sabre`.
+
+ Create direct acyclic graph (dag) of the circuit based on two qubit gates
+ commutativity relations.
+
+ Args:
+ gates_qubits_pairs (list): list of qubits tuples where gates/blocks acts.
+
+ Returns:
+ (:class:`networkx.DiGraph`): adjoint of the circuit.
+ """
+ dag = nx.DiGraph()
+ dag.add_nodes_from(range(len(gates_qubits_pairs)))
+ # Find all successors
+ connectivity_list = []
+ for idx, gate in enumerate(gates_qubits_pairs):
+ saturated_qubits = []
+ for next_idx, next_gate in enumerate(gates_qubits_pairs[idx + 1 :]):
+ for qubit in gate:
+ if (qubit in next_gate) and (not qubit in saturated_qubits):
+ saturated_qubits.append(qubit)
+ connectivity_list.append((idx, next_idx + idx + 1))
+ if len(saturated_qubits) >= 2:
+ break
+ dag.add_edges_from(connectivity_list)
+
+ return _remove_redundant_connections(dag)
+
+
+def _remove_redundant_connections(dag: nx.DiGraph):
+ """Helper method for :func:`qibo.transpiler.router._create_dag`.
+
+ Remove redundant connection from a DAG using transitive reduction.
+
+ Args:
+ dag (:class:`networkx.DiGraph`): dag to be reduced.
+
+ Returns:
+ (:class:`networkx.DiGraph`): reduced dag.
+ """
+ new_dag = nx.DiGraph()
+ new_dag.add_nodes_from(range(dag.number_of_nodes()))
+ transitive_reduction = nx.transitive_reduction(dag)
+ new_dag.add_edges_from(transitive_reduction.edges)
+
+ return new_dag
diff --git a/src/qibo/transpiler/unitary_decompositions.py b/src/qibo/transpiler/unitary_decompositions.py
new file mode 100644
index 000000000..ad892db73
--- /dev/null
+++ b/src/qibo/transpiler/unitary_decompositions.py
@@ -0,0 +1,333 @@
+import numpy as np
+
+from qibo import gates, matrices
+from qibo.backends import _check_backend
+from qibo.config import PRECISION_TOL, raise_error
+
+magic_basis = np.array(
+ [[1, -1j, 0, 0], [0, 0, 1, -1j], [0, 0, -1, -1j], [1, 1j, 0, 0]]
+) / np.sqrt(2)
+
+bell_basis = np.array(
+ [[1, 1, 0, 0], [0, 0, 1, 1], [0, 0, 1, -1], [1, -1, 0, 0]]
+) / np.sqrt(2)
+
+H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+
+
+def u3_decomposition(unitary):
+ """Decomposes arbitrary one-qubit gates to U3.
+
+ Args:
+ unitary (ndarray): Unitary :math:`2 \\times 2` matrix to be decomposed.
+
+ Returns:
+ (float, float, float): parameters of U3 gate.
+ """
+ # https://github.com/Qiskit/qiskit-terra/blob/d2e3340adb79719f9154b665e8f6d8dc26b3e0aa/qiskit/quantum_info/synthesis/one_qubit_decompose.py#L221
+ su2 = unitary / np.sqrt(np.linalg.det(unitary))
+ theta = 2 * np.arctan2(abs(su2[1, 0]), abs(su2[0, 0]))
+ plus = np.angle(su2[1, 1])
+ minus = np.angle(su2[1, 0])
+ phi = plus + minus
+ lam = plus - minus
+
+ return theta, phi, lam
+
+
+def calculate_psi(unitary, magic_basis=magic_basis, backend=None):
+ """Solves the eigenvalue problem of :math:`U^{T} U`.
+
+ See step (1) of Appendix A in arXiv:quant-ph/0011050.
+
+ Args:
+ unitary (ndarray): Unitary matrix of the gate we are decomposing
+ in the computational basis.
+ magic_basis (ndarray, optional): basis in which to solve the eigenvalue problem.
+ Defaults to ``magic basis``.
+ backend (:class:`qibo.backends.abstract.Backend`): Backend to use for calculations.
+
+ Returns:
+ ndarray: Eigenvectors in the computational basis and eigenvalues of :math:`U^{T} U`.
+ """
+ backend = _check_backend(backend)
+
+ if backend.__class__.__name__ in [
+ "CupyBackend",
+ "CuQuantumBackend",
+ ]: # pragma: no cover
+ raise_error(
+ NotImplementedError,
+ f"{backend.__class__.__name__} does not support `linalg.eig.`",
+ )
+
+ magic_basis = backend.cast(magic_basis)
+ unitary = backend.cast(unitary)
+ # write unitary in magic basis
+ u_magic = (
+ backend.np.transpose(backend.np.conj(magic_basis), (1, 0))
+ @ unitary
+ @ magic_basis
+ )
+ # construct and diagonalize UT_U
+ ut_u = backend.np.transpose(u_magic, (1, 0)) @ u_magic
+ if backend.__class__.__name__ != "PyTorchBackend":
+ # eig seems to have a different behavior based on backend/hardware,
+ # use np.round to increase precision seems to fix the issue
+ eigvals, psi_magic = np.linalg.eig(np.round(ut_u, decimals=20))
+ else:
+ eigvals, psi_magic = backend.calculate_eigenvectors(ut_u, hermitian=False)
+ # orthogonalize eigenvectors in the case of degeneracy (Gram-Schmidt)
+ psi_magic, _ = backend.np.linalg.qr(psi_magic)
+ # write psi in computational basis
+ psi = backend.np.matmul(magic_basis, psi_magic)
+ return psi, eigvals
+
+
+def schmidt_decompose(state, backend=None):
+ """Decomposes a two-qubit product state to its single-qubit parts.
+
+ Args:
+ state (ndarray): product state to be decomposed.
+
+ Returns:
+ (ndarray, ndarray): decomposition
+
+ """
+ backend = _check_backend(backend)
+ # tf.linalg.svd has a different behaviour
+ if backend.__class__.__name__ == "TensorflowBackend":
+ u, d, v = np.linalg.svd(backend.np.reshape(state, (2, 2)))
+ else:
+ u, d, v = backend.np.linalg.svd(backend.np.reshape(state, (2, 2)))
+ if not np.allclose(backend.to_numpy(d), [1, 0]): # pragma: no cover
+ raise_error(
+ ValueError,
+ f"Unexpected singular values: {d}\nCan only decompose product states.",
+ )
+ return u[:, 0], v[0]
+
+
+def calculate_single_qubit_unitaries(psi, backend=None):
+ """Calculates local unitaries that maps a maximally entangled basis to the magic basis.
+
+ See Lemma 1 of Appendix A in arXiv:quant-ph/0011050.
+
+ Args:
+ psi (ndarray): Maximally entangled two-qubit states that define a basis.
+
+ Returns:
+ (ndarray, ndarray): Local unitaries UA and UB that map the given basis to the magic basis.
+ """
+ backend = _check_backend(backend)
+ psi_magic = backend.np.matmul(backend.np.conj(backend.cast(magic_basis)).T, psi)
+ if (
+ backend.np.real(
+ backend.calculate_norm_density_matrix(backend.np.imag(psi_magic))
+ )
+ > PRECISION_TOL
+ ): # pragma: no cover
+ raise_error(NotImplementedError, "Given state is not real in the magic basis.")
+ psi_bar = backend.cast(psi.T, copy=True)
+
+ # find e and f by inverting (A3), (A4)
+ ef = (psi_bar[0] + 1j * psi_bar[1]) / np.sqrt(2)
+ e_f_ = (psi_bar[0] - 1j * psi_bar[1]) / np.sqrt(2)
+ e, f = schmidt_decompose(ef, backend=backend)
+ e_, f_ = schmidt_decompose(e_f_, backend=backend)
+ # find exp(1j * delta) using (A5a)
+ ef_ = backend.np.kron(e, f_)
+ phase = (
+ 1j
+ * np.sqrt(2)
+ * backend.np.sum(backend.np.multiply(backend.np.conj(ef_), psi_bar[2]))
+ )
+ v0 = backend.cast(np.asarray([1, 0]))
+ v1 = backend.cast(np.asarray([0, 1]))
+ # construct unitaries UA, UB using (A6a), (A6b)
+ ua = backend.np.tensordot(v0, backend.np.conj(e), 0) + phase * backend.np.tensordot(
+ v1, backend.np.conj(e_), 0
+ )
+ ub = backend.np.tensordot(v0, backend.np.conj(f), 0) + backend.np.conj(
+ phase
+ ) * backend.np.tensordot(v1, backend.np.conj(f_), 0)
+ return ua, ub
+
+
+def calculate_diagonal(unitary, ua, ub, va, vb, backend=None):
+ """Calculates Ud matrix that can be written as exp(-iH).
+
+ See Eq. (A1) in arXiv:quant-ph/0011050.
+ Ud is diagonal in the magic and Bell basis.
+ """
+ backend = _check_backend(backend)
+ # normalize U_A, U_B, V_A, V_B so that detU_d = 1
+ # this is required so that sum(lambdas) = 0
+ # and Ud can be written as exp(-iH)
+ if backend.__class__.__name__ == "TensorflowBackend":
+ det = np.linalg.det(unitary) ** (1 / 16)
+ else:
+ det = backend.np.linalg.det(unitary) ** (1 / 16)
+ ua *= det
+ ub *= det
+ va *= det
+ vb *= det
+ u_dagger = backend.np.transpose(
+ backend.np.conj(
+ backend.np.kron(
+ ua,
+ ub,
+ )
+ ),
+ (1, 0),
+ )
+ v_dagger = backend.np.transpose(backend.np.conj(backend.np.kron(va, vb)), (1, 0))
+ ud = u_dagger @ unitary @ v_dagger
+ return ua, ub, ud, va, vb
+
+
+def magic_decomposition(unitary, backend=None):
+ """Decomposes an arbitrary unitary to (A1) from arXiv:quant-ph/0011050."""
+ backend = _check_backend(backend)
+ unitary = backend.cast(unitary)
+ psi, eigvals = calculate_psi(unitary, backend=backend)
+ psi_tilde = backend.np.conj(backend.np.sqrt(eigvals)) * backend.np.matmul(
+ unitary, psi
+ )
+ va, vb = calculate_single_qubit_unitaries(psi, backend=backend)
+ ua_dagger, ub_dagger = calculate_single_qubit_unitaries(psi_tilde, backend=backend)
+ ua, ub = backend.np.transpose(
+ backend.np.conj(ua_dagger), (1, 0)
+ ), backend.np.transpose(backend.np.conj(ub_dagger), (1, 0))
+ return calculate_diagonal(unitary, ua, ub, va, vb, backend=backend)
+
+
+def to_bell_diagonal(ud, bell_basis=bell_basis, backend=None):
+ """Transforms a matrix to the Bell basis and checks if it is diagonal."""
+ backend = _check_backend(backend)
+
+ ud = backend.cast(ud)
+ bell_basis = backend.cast(bell_basis)
+
+ ud_bell = (
+ backend.np.transpose(backend.np.conj(bell_basis), (1, 0)) @ ud @ bell_basis
+ )
+ ud_diag = backend.np.diag(ud_bell)
+ if not backend.np.allclose(backend.np.diag(ud_diag), ud_bell): # pragma: no cover
+ return None
+ uprod = backend.np.prod(ud_diag)
+ if not np.allclose(backend.to_numpy(uprod), 1): # pragma: no cover
+ return None
+ return ud_diag
+
+
+def calculate_h_vector(ud_diag, backend=None):
+ """Finds h parameters corresponding to exp(-iH).
+
+ See Eq. (4)-(5) in arXiv:quant-ph/0307177.
+ """
+ backend = _check_backend(backend)
+ lambdas = -backend.np.angle(ud_diag)
+ hx = (lambdas[0] + lambdas[2]) / 2.0
+ hy = (lambdas[1] + lambdas[2]) / 2.0
+ hz = (lambdas[0] + lambdas[1]) / 2.0
+ return hx, hy, hz
+
+
+def cnot_decomposition(q0, q1, hx, hy, hz, backend=None):
+ """Performs decomposition (6) from arXiv:quant-ph/0307177."""
+ backend = _check_backend(backend)
+ h = backend.cast(H)
+ u3 = backend.cast(-1j * matrices.H)
+ # use corrected version from PRA paper (not arXiv)
+ u2 = -u3 @ gates.RX(0, 2 * hx - np.pi / 2).matrix(backend)
+ # add an extra exp(-i pi / 4) global phase to get exact match
+ v2 = np.exp(-1j * np.pi / 4) * gates.RZ(0, 2 * hz).matrix(backend)
+ v3 = gates.RZ(0, -2 * hy).matrix(backend)
+ w = backend.cast((matrices.I - 1j * matrices.X) / np.sqrt(2))
+ # change CNOT to CZ using Hadamard gates
+ return [
+ gates.H(q1),
+ gates.CZ(q0, q1),
+ gates.Unitary(u2, q0),
+ gates.Unitary(h @ v2 @ h, q1),
+ gates.CZ(q0, q1),
+ gates.Unitary(u3, q0),
+ gates.Unitary(h @ v3 @ h, q1),
+ gates.CZ(q0, q1),
+ gates.Unitary(w, q0),
+ gates.Unitary(backend.np.conj(w).T @ h, q1),
+ ]
+
+
+def cnot_decomposition_light(q0, q1, hx, hy, backend=None):
+ """Performs decomposition (24) from arXiv:quant-ph/0307177."""
+ backend = _check_backend(backend)
+ h = backend.cast(H)
+ w = backend.cast((matrices.I - 1j * matrices.X) / np.sqrt(2))
+ u2 = gates.RX(0, 2 * hx).matrix(backend)
+ v2 = gates.RZ(0, -2 * hy).matrix(backend)
+ # change CNOT to CZ using Hadamard gates
+ return [
+ gates.Unitary(backend.np.conj(w).T, q0),
+ gates.Unitary(h @ w, q1),
+ gates.CZ(q0, q1),
+ gates.Unitary(u2, q0),
+ gates.Unitary(h @ v2 @ h, q1),
+ gates.CZ(q0, q1),
+ gates.Unitary(w, q0),
+ gates.Unitary(backend.np.conj(w).T @ h, q1),
+ ]
+
+
+def two_qubit_decomposition(q0, q1, unitary, backend=None):
+ """Performs two qubit unitary gate decomposition (24) from arXiv:quant-ph/0307177.
+
+ Args:
+ q0 (int): index of the first qubit.
+ q1 (int): index of the second qubit.
+ unitary (ndarray): Unitary :math:`4 \\times 4` to be decomposed.
+
+ Returns:
+ (list): gates implementing decomposition (24) from arXiv:quant-ph/0307177
+ """
+ backend = _check_backend(backend)
+
+ ud_diag = to_bell_diagonal(unitary, backend=backend)
+ ud = None
+ if ud_diag is None:
+ u4, v4, ud, u1, v1 = magic_decomposition(unitary, backend=backend)
+ ud_diag = to_bell_diagonal(ud, backend=backend)
+
+ hx, hy, hz = calculate_h_vector(ud_diag, backend=backend)
+ hx, hy, hz = float(hx), float(hy), float(hz)
+ if np.allclose([hx, hy, hz], [0, 0, 0]):
+ u4, v4, ud, u1, v1 = magic_decomposition(unitary, backend=backend)
+ gatelist = [gates.Unitary(u4 @ u1, q0), gates.Unitary(v4 @ v1, q1)]
+ elif np.allclose(hz, 0):
+ gatelist = cnot_decomposition_light(q0, q1, hx, hy, backend=backend)
+ if ud is None:
+ return gatelist
+ g0, g1 = gatelist[:2]
+ gatelist[0] = gates.Unitary(backend.cast(g0.parameters[0]) @ u1, q0)
+ gatelist[1] = gates.Unitary(backend.cast(g1.parameters[0]) @ v1, q1)
+
+ g0, g1 = gatelist[-2:]
+ gatelist[-2] = gates.Unitary(u4 @ g0.parameters[0], q0)
+ gatelist[-1] = gates.Unitary(v4 @ g1.parameters[0], q1)
+
+ else:
+ cnot_dec = cnot_decomposition(q0, q1, hx, hy, hz, backend=backend)
+ if ud is None:
+ return cnot_dec
+
+ gatelist = [
+ gates.Unitary(u1, q0),
+ gates.Unitary(backend.cast(H) @ v1, q1),
+ ]
+ gatelist.extend(cnot_dec[1:])
+ g0, g1 = gatelist[-2:]
+ gatelist[-2] = gates.Unitary(u4 @ g0.parameters[0], q0)
+ gatelist[-1] = gates.Unitary(v4 @ g1.parameters[0], q1)
+
+ return gatelist
diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py
new file mode 100644
index 000000000..a14aca382
--- /dev/null
+++ b/src/qibo/transpiler/unroller.py
@@ -0,0 +1,245 @@
+from enum import Flag, auto
+
+from qibo import gates
+from qibo.config import raise_error
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import DecompositionError
+from qibo.transpiler.decompositions import cz_dec, gpi2_dec, iswap_dec, opt_dec, u3_dec
+
+
+class NativeGates(Flag):
+ """Define native gates supported by the unroller. A native gate set should contain at least
+ one two-qubit gate (:class:`qibo.gates.gates.CZ` or :class:`qibo.gates.gates.iSWAP`),
+ and at least one single-qubit gate
+ (:class:`qibo.gates.gates.GPI2` or :class:`qibo.gates.gates.U3`).
+
+ Possible gates are:
+ - :class:`qibo.gates.gates.I`
+ - :class:`qibo.gates.gates.Z`
+ - :class:`qibo.gates.gates.RZ`
+ - :class:`qibo.gates.gates.M`
+ - :class:`qibo.gates.gates.GPI2`
+ - :class:`qibo.gates.gates.U3`
+ - :class:`qibo.gates.gates.CZ`
+ - :class:`qibo.gates.gates.iSWAP`
+ """
+
+ I = auto()
+ Z = auto()
+ RZ = auto()
+ M = auto()
+ GPI2 = auto()
+ U3 = auto()
+ CZ = auto()
+ iSWAP = auto()
+
+ @classmethod
+ def default(cls):
+ """Return default native gates set."""
+ return cls.CZ | cls.GPI2 | cls.I | cls.Z | cls.RZ | cls.M
+
+ @classmethod
+ def from_gatelist(cls, gatelist: list):
+ """Create a NativeGates object containing all gates from a ``gatelist``."""
+ natives = cls(0)
+ for gate in gatelist:
+ natives |= cls.from_gate(gate)
+ return natives
+
+ @classmethod
+ def from_gate(cls, gate: gates.Gate):
+ """Create a :class:`qibo.transpiler.unroller.NativeGates`
+ object from a :class:`qibo.gates.gates.Gate`."""
+ if isinstance(gate, gates.Gate):
+ return cls.from_gate(gate.__class__)
+
+ try:
+ return getattr(cls, gate.__name__)
+ except AttributeError:
+ raise_error(ValueError, f"Gate {gate} cannot be used as native.")
+
+
+# TODO: Make setting single-qubit native gates more flexible
+class Unroller:
+ """Translates a circuit to native gates.
+
+ Args:
+ native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use
+ in the transpiled circuit.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates.
+ """
+
+ def __init__(
+ self,
+ native_gates: NativeGates,
+ ):
+ self.native_gates = native_gates
+
+ def __call__(self, circuit: Circuit):
+ """Decomposes a circuit into native gates.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit model to decompose.
+
+ Returns:
+ (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates.
+ """
+ translated_circuit = Circuit(**circuit.init_kwargs)
+ for gate in circuit.queue:
+ translated_circuit.add(
+ translate_gate(
+ gate,
+ self.native_gates,
+ )
+ )
+ return translated_circuit
+
+
+def assert_decomposition(
+ circuit: Circuit,
+ native_gates: NativeGates,
+):
+ """Checks if a circuit has been correctly decomposed into native gates.
+
+ Args:
+ circuit (:class:`qibo.models.circuit.Circuit`): circuit model to check.
+ native_gates (:class:`qibo.transpiler.unroller.NativeGates`):
+ native gates in the transpiled circuit.
+ """
+ for gate in circuit.queue:
+ if isinstance(gate, gates.M):
+ continue
+ if len(gate.qubits) <= 2:
+ try:
+ native_type_gate = NativeGates.from_gate(gate)
+ if not native_type_gate & native_gates:
+ raise_error(
+ DecompositionError,
+ f"{gate.name} is not a native gate.",
+ )
+ except ValueError:
+ raise_error(
+ DecompositionError,
+ f"{gate.name} is not a native gate.",
+ )
+ else:
+ raise_error(
+ DecompositionError, f"{gate.name} acts on more than two qubits."
+ )
+
+
+def translate_gate(
+ gate,
+ native_gates: NativeGates,
+):
+ """Maps gates to a hardware-native implementation.
+
+ Args:
+ gate (:class:`qibo.gates.abstract.Gate`): gate to be decomposed.
+ native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates
+ to use in the decomposition.
+
+ Returns:
+ list: List of native gates that decompose the input gate.
+ """
+ if isinstance(gate, (gates.I, gates.Align)):
+ return gate
+
+ if isinstance(gate, gates.M):
+ gate.basis_gates = len(gate.basis_gates) * [gates.Z]
+ gate.basis = []
+ return gate
+
+ if len(gate.qubits) == 1:
+ return _translate_single_qubit_gates(gate, native_gates)
+
+ decomposition_2q = _translate_two_qubit_gates(gate, native_gates)
+ final_decomposition = []
+ for decomposed_2q_gate in decomposition_2q:
+ if len(decomposed_2q_gate.qubits) == 1:
+ final_decomposition += _translate_single_qubit_gates(
+ decomposed_2q_gate, native_gates
+ )
+ else:
+ final_decomposition.append(decomposed_2q_gate)
+ return final_decomposition
+
+
+def _translate_single_qubit_gates(gate: gates.Gate, single_qubit_natives: NativeGates):
+ """Helper method for :meth:`translate_gate`.
+
+ Maps single qubit gates to a hardware-native implementation.
+
+ Args:
+ gate (:class:`qibo.gates.abstract.Gate`): gate to be decomposed.
+ single_qubit_natives (:class:`qibo.transpiler.unroller.NativeGates`): single
+ qubit native gates.
+
+ Returns:
+ list: List of native gates that decompose the input gate.
+ """
+ if NativeGates.U3 & single_qubit_natives:
+ return u3_dec(gate)
+
+ if NativeGates.GPI2 & single_qubit_natives:
+ return gpi2_dec(gate)
+
+ raise_error(DecompositionError, "Use U3 or GPI2 as single qubit native gates")
+
+
+def _translate_two_qubit_gates(gate: gates.Gate, native_gates: NativeGates):
+ """Helper method for :meth:`translate_gate`.
+
+ Maps two qubit gates to a hardware-native implementation.
+
+ Args:
+ gate (:class:`qibo.gates.abstract.Gate`): gate to be decomposed.
+ native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates
+ supported by the quantum hardware.
+
+ Returns:
+ list: List of native gates that decompose the input gate.
+ """
+ if (
+ native_gates & (NativeGates.CZ | NativeGates.iSWAP)
+ ) is NativeGates.CZ | NativeGates.iSWAP:
+ # Check for a special optimized decomposition.
+ if gate.__class__ in opt_dec.decompositions:
+ return opt_dec(gate)
+ # Check if the gate has a CZ decomposition
+ if not gate.__class__ in iswap_dec.decompositions:
+ return cz_dec(gate)
+ # Check the decomposition with less 2 qubit gates.
+
+ if cz_dec.count_2q(gate) < iswap_dec.count_2q(gate):
+ return cz_dec(gate)
+ if cz_dec.count_2q(gate) > iswap_dec.count_2q(gate):
+ return iswap_dec(gate)
+ # If equal check the decomposition with less 1 qubit gates.
+ # This is never used for now but may be useful for future generalization
+ if cz_dec.count_1q(gate) < iswap_dec.count_1q(gate): # pragma: no cover
+ return cz_dec(gate)
+ return iswap_dec(gate) # pragma: no cover
+
+ if native_gates & NativeGates.CZ:
+ return cz_dec(gate)
+
+ if native_gates & NativeGates.iSWAP:
+ if gate.__class__ in iswap_dec.decompositions:
+ return iswap_dec(gate)
+
+ # First decompose into CZ
+ cz_decomposed = cz_dec(gate)
+ # Then CZ are decomposed into iSWAP
+ iswap_decomposed = []
+ for g in cz_decomposed:
+ # Need recursive function as gates.Unitary is not in iswap_dec
+ for g_translated in translate_gate(g, native_gates=native_gates):
+ iswap_decomposed.append(g_translated)
+ return iswap_decomposed
+
+ raise_error(
+ DecompositionError, "Use only CZ and/or iSWAP as native gates"
+ ) # pragma: no cover
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 000000000..87ac46a01
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,96 @@
+"""conftest.py.
+
+Pytest fixtures.
+"""
+
+import sys
+
+import pytest
+
+from qibo.backends import construct_backend
+
+# backends to be tested
+BACKENDS = [
+ "numpy",
+ "tensorflow",
+ "pytorch",
+ "qibojit-numba",
+ "qibojit-cupy",
+ "qibojit-cuquantum",
+]
+# multigpu configurations to be tested (only with qibojit-cupy)
+ACCELERATORS = [
+ {"/GPU:0": 1, "/GPU:1": 1},
+ {"/GPU:0": 2, "/GPU:1": 2},
+ {"/GPU:0": 1, "/GPU:1": 1, "/GPU:2": 1, "/GPU:3": 1},
+]
+
+
+def get_backend(backend_name):
+ if "-" in backend_name:
+ name, platform = backend_name.split("-")
+ else:
+ name, platform = backend_name, None
+ return construct_backend(name, platform=platform)
+
+
+# ignore backends that are not available in the current testing environment
+AVAILABLE_BACKENDS = []
+MULTIGPU_BACKENDS = []
+for backend_name in BACKENDS:
+ try:
+ _backend = get_backend(backend_name)
+ AVAILABLE_BACKENDS.append(backend_name)
+ if _backend.supports_multigpu: # pragma: no cover
+ MULTIGPU_BACKENDS.append(backend_name)
+ except (ModuleNotFoundError, ImportError):
+ pass
+
+try:
+ get_backend("qulacs")
+ QULACS_INSTALLED = True
+except ModuleNotFoundError:
+ QULACS_INSTALLED = False
+
+
+def pytest_runtest_setup(item):
+ ALL = {"darwin", "linux"}
+ supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
+ plat = sys.platform
+ if supported_platforms and plat not in supported_platforms: # pragma: no cover
+ # case not covered by workflows
+ pytest.skip(f"Cannot run test on platform {plat}.")
+ elif not QULACS_INSTALLED and item.fspath.purebasename == "test_backends_qulacs":
+ # case not covered by workflows
+ pytest.skip(f"Cannot test `qulacs` on platform {plat}.")
+
+
+def pytest_configure(config):
+ config.addinivalue_line("markers", "linux: mark test to run only on linux")
+
+
+@pytest.fixture
+def backend(backend_name):
+ yield get_backend(backend_name)
+
+
+def pytest_generate_tests(metafunc):
+ module_name = metafunc.module.__name__
+
+ if module_name == "tests.test_models_distcircuit_execution":
+ config = [(bk, acc) for acc in ACCELERATORS for bk in MULTIGPU_BACKENDS]
+ metafunc.parametrize("backend_name,accelerators", config)
+
+ else:
+ if "backend_name" in metafunc.fixturenames:
+ if "accelerators" in metafunc.fixturenames:
+ config = [(backend, None) for backend in AVAILABLE_BACKENDS]
+ config.extend(
+ (bk, acc) for acc in ACCELERATORS for bk in MULTIGPU_BACKENDS
+ )
+ metafunc.parametrize("backend_name,accelerators", config)
+ else:
+ metafunc.parametrize("backend_name", AVAILABLE_BACKENDS)
+
+ elif "accelerators" in metafunc.fixturenames:
+ metafunc.parametrize("accelerators", ACCELERATORS)
diff --git a/tests/regressions/__init__.py b/tests/regressions/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/regressions/aavqe_bfgs.out b/tests/regressions/aavqe_bfgs.out
new file mode 100644
index 000000000..632a010dd
--- /dev/null
+++ b/tests/regressions/aavqe_bfgs.out
@@ -0,0 +1,12 @@
+2.415803926183727857e+00
+3.957758339873970854e+00
+4.195044865280936897e+00
+3.646499497404236845e+00
+7.839134923521597909e-01
+4.875292985270842649e+00
+2.827505760791610978e+00
+4.769898317623221473e+00
+7.407514603417983956e+00
+9.659241726629221869e-01
+5.644780051708082169e+00
+2.633827725025035260e+00
diff --git a/tests/regressions/adiabatic_bfgs.out b/tests/regressions/adiabatic_bfgs.out
new file mode 100644
index 000000000..1b7df530b
--- /dev/null
+++ b/tests/regressions/adiabatic_bfgs.out
@@ -0,0 +1,2 @@
+5.149030536967988958e-01
+2.009890043019788575e+00
diff --git a/tests/regressions/falqon1.out b/tests/regressions/falqon1.out
new file mode 100644
index 000000000..b510817b6
--- /dev/null
+++ b/tests/regressions/falqon1.out
@@ -0,0 +1,12 @@
+1.000000000000000056e-01
+0.000000000000000000e+00
+1.000000000000000056e-01
+5.960079923851835382e-02
+1.000000000000000056e-01
+1.163174774926115235e-01
+1.000000000000000056e-01
+1.526867293265563030e-01
+1.000000000000000056e-01
+1.367890776526200225e-01
+1.000000000000000056e-01
+8.903718462471775508e-02
diff --git a/tests/regressions/falqon2.out b/tests/regressions/falqon2.out
new file mode 100644
index 000000000..bc05940bf
--- /dev/null
+++ b/tests/regressions/falqon2.out
@@ -0,0 +1,6 @@
+1.000000000000000021e-02
+0.000000000000000000e+00
+1.000000000000000021e-02
+5.999600007999924189e-04
+1.000000000000000021e-02
+1.199705365195721882e-03
diff --git a/tests/regressions/falqon3.out b/tests/regressions/falqon3.out
new file mode 100644
index 000000000..bc05940bf
--- /dev/null
+++ b/tests/regressions/falqon3.out
@@ -0,0 +1,6 @@
+1.000000000000000021e-02
+0.000000000000000000e+00
+1.000000000000000021e-02
+5.999600007999924189e-04
+1.000000000000000021e-02
+1.199705365195721882e-03
diff --git a/tests/regressions/falqon4.out b/tests/regressions/falqon4.out
new file mode 100644
index 000000000..2c96fe3e3
--- /dev/null
+++ b/tests/regressions/falqon4.out
@@ -0,0 +1,4 @@
+1.000000000000000021e-02
+0.000000000000000000e+00
+1.000000000000000021e-02
+5.999600007999925273e-04
diff --git a/tests/regressions/heisenberg_N3delta0.0.out b/tests/regressions/heisenberg_N3delta0.0.out
new file mode 100644
index 000000000..42078501d
--- /dev/null
+++ b/tests/regressions/heisenberg_N3delta0.0.out
@@ -0,0 +1,64 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/heisenberg_N3delta0.5.out b/tests/regressions/heisenberg_N3delta0.5.out
new file mode 100644
index 000000000..5a4cc8213
--- /dev/null
+++ b/tests/regressions/heisenberg_N3delta0.5.out
@@ -0,0 +1,64 @@
+1.500000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-5.000000000000000000e-01
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+-5.000000000000000000e-01
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-5.000000000000000000e-01
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+-5.000000000000000000e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+-5.000000000000000000e-01
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+-5.000000000000000000e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.500000000000000000e+00
diff --git a/tests/regressions/heisenberg_N3delta1.0.out b/tests/regressions/heisenberg_N3delta1.0.out
new file mode 100644
index 000000000..b52f4f6a2
--- /dev/null
+++ b/tests/regressions/heisenberg_N3delta1.0.out
@@ -0,0 +1,64 @@
+3.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+-1.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+-1.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+-1.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+0.000000000000000000e+00
+2.000000000000000000e+00
+-1.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+3.000000000000000000e+00
diff --git a/tests/regressions/maxcut_N3.out b/tests/regressions/maxcut_N3.out
new file mode 100644
index 000000000..3eca5caa8
--- /dev/null
+++ b/tests/regressions/maxcut_N3.out
@@ -0,0 +1,64 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/maxcut_N4.out b/tests/regressions/maxcut_N4.out
new file mode 100644
index 000000000..85bf53987
--- /dev/null
+++ b/tests/regressions/maxcut_N4.out
@@ -0,0 +1,256 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/maxcut_N5.out b/tests/regressions/maxcut_N5.out
new file mode 100644
index 000000000..5487761f7
--- /dev/null
+++ b/tests/regressions/maxcut_N5.out
@@ -0,0 +1,1024 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.200000000000000000e+01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-8.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/qaoa_bfgs.out b/tests/regressions/qaoa_bfgs.out
new file mode 100644
index 000000000..220229957
--- /dev/null
+++ b/tests/regressions/qaoa_bfgs.out
@@ -0,0 +1,4 @@
+5.669456681101249229e-01
+2.293889145170547872e-01
+3.733402610400745059e-01
+5.071904237683148020e-01
diff --git a/tests/regressions/tfim_N3h0.0.out b/tests/regressions/tfim_N3h0.0.out
new file mode 100644
index 000000000..ea4543849
--- /dev/null
+++ b/tests/regressions/tfim_N3h0.0.out
@@ -0,0 +1,64 @@
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
diff --git a/tests/regressions/tfim_N3h0.5.out b/tests/regressions/tfim_N3h0.5.out
new file mode 100644
index 000000000..3f93a9939
--- /dev/null
+++ b/tests/regressions/tfim_N3h0.5.out
@@ -0,0 +1,64 @@
+-3.000000000000000000e+00
+-5.000000000000000000e-01
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-5.000000000000000000e-01
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-5.000000000000000000e-01
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-0.000000000000000000e+00
+-5.000000000000000000e-01
+-5.000000000000000000e-01
+-3.000000000000000000e+00
diff --git a/tests/regressions/tfim_N3h1.0.out b/tests/regressions/tfim_N3h1.0.out
new file mode 100644
index 000000000..e02df72c7
--- /dev/null
+++ b/tests/regressions/tfim_N3h1.0.out
@@ -0,0 +1,64 @@
+-3.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-3.000000000000000000e+00
diff --git a/tests/regressions/trotter_adiabatic_bfgs.out b/tests/regressions/trotter_adiabatic_bfgs.out
new file mode 100644
index 000000000..994f95cad
--- /dev/null
+++ b/tests/regressions/trotter_adiabatic_bfgs.out
@@ -0,0 +1,2 @@
+5.158789747253303970e-01
+2.009875169593585476e+00
diff --git a/tests/regressions/trotter_qaoa_bfgs.out b/tests/regressions/trotter_qaoa_bfgs.out
new file mode 100644
index 000000000..043ce6ca7
--- /dev/null
+++ b/tests/regressions/trotter_qaoa_bfgs.out
@@ -0,0 +1,4 @@
+7.412847784321033862e-01
+2.865912623362547040e-01
+4.754604315219064237e-01
+6.513874284677686477e-01
diff --git a/tests/regressions/trotter_qaoa_powell.out b/tests/regressions/trotter_qaoa_powell.out
new file mode 100644
index 000000000..588dbee64
--- /dev/null
+++ b/tests/regressions/trotter_qaoa_powell.out
@@ -0,0 +1,4 @@
+1.625545998289206651e+00
+1.203642228486790700e+00
+1.183733122429738938e+00
+3.705732513178541798e-02
diff --git a/tests/regressions/tsp_layer2_imag.out b/tests/regressions/tsp_layer2_imag.out
new file mode 100644
index 000000000..754d67be3
--- /dev/null
+++ b/tests/regressions/tsp_layer2_imag.out
@@ -0,0 +1,512 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.034267702652136951e-07
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.125407363285722828e-08
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+8.175715411600245706e-04
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-5.513468058776938713e-05
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+7.347588697741100523e-03
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.097722566296985974e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/tsp_layer2_real.out b/tests/regressions/tsp_layer2_real.out
new file mode 100644
index 000000000..a6258ded5
--- /dev/null
+++ b/tests/regressions/tsp_layer2_real.out
@@ -0,0 +1,512 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.142305492602130386e-08
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.018967776766042943e-07
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-7.402466722830055267e-03
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-6.089395260702398522e-06
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+8.115104924275230074e-04
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+9.939013723965425262e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/tsp_layer4_imag.out b/tests/regressions/tsp_layer4_imag.out
new file mode 100644
index 000000000..21ac4437e
--- /dev/null
+++ b/tests/regressions/tsp_layer4_imag.out
@@ -0,0 +1,512 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.453194996748716108e-09
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+6.283711317120813100e-12
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+8.738304561841873428e-06
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-1.012061297980901969e-07
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+2.668345483198655485e-03
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-2.182302544452734694e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/tsp_layer4_real.out b/tests/regressions/tsp_layer4_real.out
new file mode 100644
index 000000000..6143ea496
--- /dev/null
+++ b/tests/regressions/tsp_layer4_real.out
@@ -0,0 +1,512 @@
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+6.017462797455740175e-10
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-2.430347448897224316e-11
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-3.907637459246790111e-05
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+-4.249523260157431898e-08
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+1.120417515571634935e-03
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+9.758930161848613505e-01
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
+0.000000000000000000e+00
diff --git a/tests/regressions/vqc_bfgs.out b/tests/regressions/vqc_bfgs.out
new file mode 100644
index 000000000..277983f83
--- /dev/null
+++ b/tests/regressions/vqc_bfgs.out
@@ -0,0 +1,27 @@
+3.543129794221170581e+00
+4.375214920072721725e+00
+3.650506684800013169e+00
+3.539864702952411513e+00
+2.817149041450181191e+00
+3.921505097774900861e+00
+2.322887353548705569e+00
+5.758422446781272797e+00
+5.837572222692092083e+00
+2.761178542829372695e+00
+4.834115746237728217e+00
+3.105845328373380010e+00
+3.683596864339066190e+00
+5.675255817725797236e+00
+4.010838198610574001e-01
+7.173603215005160960e-01
+1.735057201195332188e-02
+5.186255882725080646e+00
+4.811737444685575404e+00
+5.356762188564393234e+00
+6.361857082105134786e+00
+5.365899689428245800e+00
+2.641383784307568927e+00
+5.117226145119830427e+00
+9.410630323543563991e-01
+3.762565780089694556e+00
+8.912530706025829641e-01
diff --git a/tests/regressions/vqc_powell.out b/tests/regressions/vqc_powell.out
new file mode 100644
index 000000000..3c5f76e16
--- /dev/null
+++ b/tests/regressions/vqc_powell.out
@@ -0,0 +1,27 @@
+4.134476763937597887e+00
+4.042679974947890997e+00
+2.412349946507370113e+00
+2.959040551053622625e+00
+3.049527995451426943e+00
+3.856175105221426147e+00
+2.462919031186278307e+00
+5.592589704482441526e+00
+6.641295534614007678e+00
+3.409234117248751250e+00
+4.918855038355289366e+00
+3.726101585787755965e+00
+4.058362455701950644e+00
+5.418627963950028992e+00
+8.911662560447441983e-02
+9.332351957195673675e-01
+-4.909980371122281140e-01
+4.866854668448537424e+00
+4.914675769921053039e+00
+5.206525238579259529e+00
+6.172337376099942929e+00
+4.871815669864262510e+00
+2.741151666624195293e+00
+4.840171980847915556e+00
+8.917683875738773303e-01
+3.997869399785497269e+00
+1.114699423250099519e+00
diff --git a/tests/regressions/vqe_bfgs.out b/tests/regressions/vqe_bfgs.out
new file mode 100644
index 000000000..5d1f4ca05
--- /dev/null
+++ b/tests/regressions/vqe_bfgs.out
@@ -0,0 +1,27 @@
+3.705548771113760242e+00
+4.570044723473086101e+00
+3.609288285246643024e+00
+3.211140468209619847e+00
+2.540640910662608665e+00
+3.880286696220767162e+00
+2.739419079661859602e+00
+5.481914331999808709e+00
+5.883015260209523589e+00
+2.478529118951129817e+00
+4.982313429205010635e+00
+3.151288354688047910e+00
+3.555053136686849857e+00
+5.823453514698424982e+00
+4.236483912618405068e-01
+9.080965287180534640e-01
+-2.841588102501482194e-01
+5.208820457627200362e+00
+4.655628432561794483e+00
+5.055252810954823595e+00
+6.179253091816462984e+00
+5.202017633729099977e+00
+2.822808830356239529e+00
+4.934622159332876734e+00
+5.306138387942347379e-01
+3.943990833839792653e+00
+1.262044372532761916e+00
diff --git a/tests/regressions/vqe_powell.out b/tests/regressions/vqe_powell.out
new file mode 100644
index 000000000..9521252ab
--- /dev/null
+++ b/tests/regressions/vqe_powell.out
@@ -0,0 +1,27 @@
+5.626210397255835716e+00
+4.963028723398160480e+00
+4.368275433948578979e+00
+2.792986132388808773e+00
+2.608189804106265441e+00
+4.113757308114370126e+00
+3.181650470854141766e+00
+5.760220717821760594e+00
+6.096696367647056469e+00
+2.321800921030930720e+00
+4.965422825607179291e+00
+3.330802781571029048e+00
+3.559687220742407732e+00
+5.822991696415676266e+00
+4.772934514055064970e-01
+5.312291963712059140e-01
+8.761030762215596401e-02
+5.247720650366180628e+00
+4.862120637129929257e+00
+5.478008761221524026e+00
+6.161952204896642193e+00
+5.039126618313415129e+00
+2.895348893679761115e+00
+4.904940026367345496e+00
+7.388286098979613969e-01
+4.015630571918912928e+00
+9.306582041735210753e-01
diff --git a/tests/regressions/x_N3.out b/tests/regressions/x_N3.out
new file mode 100644
index 000000000..05702bcab
--- /dev/null
+++ b/tests/regressions/x_N3.out
@@ -0,0 +1,64 @@
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
diff --git a/tests/regressions/y_N4.out b/tests/regressions/y_N4.out
new file mode 100644
index 000000000..31ac34296
--- /dev/null
+++ b/tests/regressions/y_N4.out
@@ -0,0 +1,256 @@
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
diff --git a/tests/regressions/z_N5.out b/tests/regressions/z_N5.out
new file mode 100644
index 000000000..e7ee535bf
--- /dev/null
+++ b/tests/regressions/z_N5.out
@@ -0,0 +1,1024 @@
+-5.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+1.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+3.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+-0.000000000000000000e+00
+5.000000000000000000e+00
diff --git a/tests/test_backends.py b/tests/test_backends.py
new file mode 100644
index 000000000..1be00e6d2
--- /dev/null
+++ b/tests/test_backends.py
@@ -0,0 +1,148 @@
+import platform
+import sys
+
+import numpy as np
+import pytest
+
+from qibo import construct_backend, gates, list_available_backends, set_backend
+from qibo.backends import MetaBackend
+
+####################### Test `matrix` #######################
+GATES = [
+ ("H", (0,), np.array([[1, 1], [1, -1]]) / np.sqrt(2)),
+ ("X", (0,), np.array([[0, 1], [1, 0]])),
+ ("Y", (0,), np.array([[0, -1j], [1j, 0]])),
+ ("Z", (1,), np.array([[1, 0], [0, -1]])),
+ ("S", (2,), np.array([[1, 0], [0, 1j]])),
+ ("T", (2,), np.array([[1, 0], [0, np.exp(1j * np.pi / 4.0)]])),
+ (
+ "CNOT",
+ (0, 1),
+ np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]),
+ ),
+ ("CZ", (1, 3), np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]])),
+ (
+ "SWAP",
+ (2, 4),
+ np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]),
+ ),
+ (
+ "FSWAP",
+ (2, 4),
+ np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, -1]]),
+ ),
+ (
+ "TOFFOLI",
+ (1, 2, 3),
+ np.array(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 1],
+ [0, 0, 0, 0, 0, 0, 1, 0],
+ ]
+ ),
+ ),
+]
+
+
+@pytest.mark.parametrize("gate,qubits,target_matrix", GATES)
+def test_matrix(backend, gate, qubits, target_matrix):
+ gate = getattr(gates, gate)(*qubits)
+ backend.assert_allclose(gate.matrix(backend), target_matrix)
+
+
+GATES = [
+ (
+ "RX",
+ lambda x: np.array(
+ [
+ [np.cos(x / 2.0), -1j * np.sin(x / 2.0)],
+ [-1j * np.sin(x / 2.0), np.cos(x / 2.0)],
+ ]
+ ),
+ ),
+ (
+ "RY",
+ lambda x: np.array(
+ [[np.cos(x / 2.0), -np.sin(x / 2.0)], [np.sin(x / 2.0), np.cos(x / 2.0)]]
+ ),
+ ),
+ ("RZ", lambda x: np.diag([np.exp(-1j * x / 2.0), np.exp(1j * x / 2.0)])),
+ ("U1", lambda x: np.diag([1, np.exp(1j * x)])),
+ ("CU1", lambda x: np.diag([1, 1, 1, np.exp(1j * x)])),
+]
+
+
+@pytest.mark.parametrize("gate,target_matrix", GATES)
+def test_matrix_rotations(backend, gate, target_matrix):
+ """Check that `_construct_unitary` method constructs the proper matrix."""
+ theta = 0.1234
+ if gate == "CU1":
+ gate = getattr(gates, gate)(0, 1, theta)
+ else:
+ gate = getattr(gates, gate)(0, theta)
+ backend.assert_allclose(gate.matrix(backend), target_matrix(theta))
+ backend.assert_allclose(gate.matrix(backend), target_matrix(theta))
+
+
+def test_plus_density_matrix(backend):
+ matrix = backend.plus_density_matrix(4)
+ target_matrix = np.ones((16, 16)) / 16
+ backend.assert_allclose(matrix, target_matrix)
+
+
+def test_set_backend_error():
+ with pytest.raises(ValueError):
+ set_backend("non-existing-backend")
+
+
+def test_metabackend_load_error():
+ with pytest.raises(ValueError):
+ MetaBackend.load("non-existing-backend")
+
+
+def test_construct_backend(backend):
+ assert isinstance(
+ construct_backend(backend.name, platform=backend.platform), backend.__class__
+ )
+
+
+def test_list_available_backends():
+ tensorflow = False if platform.system() == "Windows" else True
+ qulacs = (
+ False if platform.system() == "Darwin" and sys.version_info[1] == 9 else True
+ )
+ available_backends = {
+ "numpy": True,
+ "tensorflow": tensorflow,
+ "pytorch": True,
+ "qulacs": qulacs,
+ "qibojit": {"numba": True, "cupy": False, "cuquantum": False},
+ "qibolab": False,
+ "qibo-cloud-backends": False,
+ "qibotn": {"cutensornet": False, "qutensornet": True},
+ }
+ assert available_backends == list_available_backends(
+ "qibojit", "qibolab", "qibo-cloud-backends", "qibotn"
+ )
+
+
+def test_gradients_pytorch():
+ from qibo.backends import PyTorchBackend # pylint: disable=import-outside-toplevel
+
+ backend = PyTorchBackend()
+ gate = gates.RX(0, 0.1)
+ matrix = gate.matrix(backend)
+ assert matrix.requires_grad
+ assert backend.gradients
+ backend.requires_grad(False)
+ gate = gates.RX(0, 0.1)
+ matrix = gate.matrix(backend)
+ assert not matrix.requires_grad
+ assert not backend.gradients
+ assert not backend.matrices.requires_grad
diff --git a/tests/test_backends_clifford.py b/tests/test_backends_clifford.py
new file mode 100644
index 000000000..5e4bae9d6
--- /dev/null
+++ b/tests/test_backends_clifford.py
@@ -0,0 +1,310 @@
+"""Tests for Clifford backend."""
+
+from itertools import product
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, set_backend
+from qibo.backends import (
+ CliffordBackend,
+ GlobalBackend,
+ NumpyBackend,
+ PyTorchBackend,
+ TensorflowBackend,
+)
+from qibo.backends.clifford import _get_engine_name
+from qibo.noise import DepolarizingError, NoiseModel, PauliError
+from qibo.quantum_info.random_ensembles import random_clifford
+
+numpy_bkd = NumpyBackend()
+
+
+def construct_clifford_backend(backend):
+ if (
+ isinstance(backend, (TensorflowBackend, PyTorchBackend))
+ or backend.__class__.__name__ == "CuQuantumBackend"
+ ):
+ with pytest.raises(NotImplementedError):
+ clifford_backend = CliffordBackend(backend.name)
+ pytest.skip("Clifford backend not defined for this engine.")
+
+ return CliffordBackend(_get_engine_name(backend))
+
+
+def test_set_backend(backend):
+ clifford_bkd = construct_clifford_backend(backend)
+ platform = _get_engine_name(backend)
+ set_backend("clifford", platform=platform)
+ assert isinstance(GlobalBackend(), CliffordBackend)
+ global_platform = GlobalBackend().platform
+ assert global_platform == platform
+
+
+def test_global_backend(backend):
+ construct_clifford_backend(backend)
+ set_backend(backend.name, platform=backend.platform)
+ clifford_bkd = CliffordBackend()
+ target = (
+ GlobalBackend().name if backend.name == "numpy" else GlobalBackend().platform
+ )
+ assert clifford_bkd.platform == target
+ set_backend("numpy")
+
+
+THETAS_1Q = [
+ th + 2 * i * np.pi for i in range(2) for th in [0, np.pi / 2, np.pi, 3 * np.pi / 2]
+]
+
+AXES = ["RX", "RY", "RZ"]
+
+
+@pytest.mark.parametrize("axis,theta", list(product(AXES, THETAS_1Q)))
+def test_rotations_1q(backend, theta, axis):
+ clifford_bkd = construct_clifford_backend(backend)
+ c = Circuit(3, density_matrix=True)
+ qubits = np.random.randint(3, size=2)
+ H_qubits = np.random.choice(range(3), size=2, replace=False)
+ c.add(gates.H(q) for q in H_qubits)
+ c.add(getattr(gates, axis)(qubits[0], theta=theta))
+ c.add(getattr(gates, axis)(qubits[1], theta=theta))
+ clifford_state = clifford_bkd.execute_circuit(c).state()
+ numpy_state = numpy_bkd.execute_circuit(c).state()
+ numpy_state = backend.cast(numpy_state)
+ backend.assert_allclose(clifford_state, numpy_state, atol=1e-8)
+
+
+THETAS_2Q = [i * np.pi for i in range(4)]
+
+
+@pytest.mark.parametrize("axis,theta", list(product(AXES, THETAS_2Q)))
+def test_rotations_2q(backend, theta, axis):
+ clifford_bkd = construct_clifford_backend(backend)
+ c = Circuit(3, density_matrix=True)
+ qubits_0 = np.random.choice(range(3), size=2, replace=False)
+ qubits_1 = np.random.choice(range(3), size=2, replace=False)
+ H_qubits = np.random.choice(range(3), size=2, replace=False)
+ c.add(gates.H(q) for q in H_qubits)
+ c.add(getattr(gates, f"C{axis}")(*qubits_0, theta=theta))
+ c.add(getattr(gates, f"C{axis}")(*qubits_1, theta=theta))
+ clifford_state = clifford_bkd.execute_circuit(c).state()
+ numpy_state = numpy_bkd.execute_circuit(c).state()
+ numpy_state = backend.cast(numpy_state)
+ backend.assert_allclose(clifford_state, numpy_state, atol=1e-8)
+
+
+SINGLE_QUBIT_CLIFFORDS = ["I", "H", "S", "Z", "X", "Y", "SX", "SDG", "SXDG"]
+
+
+@pytest.mark.parametrize("gate", SINGLE_QUBIT_CLIFFORDS)
+def test_single_qubit_gates(backend, gate):
+ clifford_bkd = construct_clifford_backend(backend)
+ c = Circuit(3, density_matrix=True)
+ qubits = np.random.randint(3, size=2)
+ H_qubits = np.random.choice(range(3), size=2, replace=False)
+ c.add(gates.H(q) for q in H_qubits)
+ c.add(getattr(gates, gate)(qubits[0]))
+ c.add(getattr(gates, gate)(qubits[1]))
+ clifford_state = clifford_bkd.execute_circuit(c).state()
+ numpy_state = numpy_bkd.execute_circuit(c).state()
+ numpy_state = backend.cast(numpy_state)
+ backend.assert_allclose(clifford_state, numpy_state, atol=1e-8)
+
+
+TWO_QUBITS_CLIFFORDS = ["CNOT", "CZ", "CY", "SWAP", "iSWAP", "FSWAP", "ECR"]
+
+
+@pytest.mark.parametrize("gate", TWO_QUBITS_CLIFFORDS)
+def test_two_qubits_gates(backend, gate):
+ clifford_bkd = construct_clifford_backend(backend)
+ c = Circuit(5, density_matrix=True)
+ qubits = np.random.choice(range(5), size=4, replace=False).reshape(2, 2)
+ H_qubits = np.random.choice(range(5), size=3, replace=False)
+ c.add(gates.H(q) for q in H_qubits)
+ c.add(getattr(gates, gate)(*qubits[0]))
+ c.add(getattr(gates, gate)(*qubits[1]))
+ clifford_state = clifford_bkd.execute_circuit(c).state()
+ numpy_state = numpy_bkd.execute_circuit(c).state()
+ numpy_state = backend.cast(numpy_state)
+ backend.assert_allclose(clifford_state, numpy_state, atol=1e-8)
+
+
+local_state = np.random.RandomState(2)
+MEASURED_QUBITS = sorted(local_state.choice(range(3), size=2, replace=False))
+
+
+@pytest.mark.parametrize("binary", [False, True])
+@pytest.mark.parametrize(
+ "prob_qubits",
+ [
+ range(3),
+ local_state.choice(MEASURED_QUBITS, size=2, replace=False),
+ [0],
+ [1],
+ [2],
+ ],
+)
+def test_random_clifford_circuit(backend, prob_qubits, binary):
+ backend.set_seed(2024)
+ nqubits, nshots = 3, 200
+ clifford_bkd = construct_clifford_backend(backend)
+
+ c = random_clifford(nqubits, seed=1, backend=backend)
+ c.density_matrix = True
+ c_copy = c.copy()
+ c.add(gates.M(*MEASURED_QUBITS))
+ c_copy.add(gates.M(*MEASURED_QUBITS))
+
+ numpy_bkd.set_seed(2024)
+ numpy_result = numpy_bkd.execute_circuit(c, nshots=nshots)
+
+ clifford_bkd.set_seed(2024)
+ clifford_result = clifford_bkd.execute_circuit(c_copy, nshots=nshots)
+
+ backend.assert_allclose(backend.cast(numpy_result.state()), clifford_result.state())
+
+ if not set(prob_qubits).issubset(set(MEASURED_QUBITS)):
+ with pytest.raises(RuntimeError) as excinfo:
+ numpy_bkd.assert_allclose(
+ numpy_result.probabilities(prob_qubits),
+ clifford_result.probabilities(prob_qubits),
+ )
+ assert (
+ str(excinfo.value)
+ == f"Asking probabilities for qubits {prob_qubits}, but only qubits {MEASURED_QUBITS} were measured."
+ )
+ else:
+ backend.assert_allclose(
+ backend.cast(numpy_result.probabilities(prob_qubits)),
+ clifford_result.probabilities(prob_qubits),
+ atol=1e-1,
+ )
+
+ numpy_freq = numpy_result.frequencies(binary)
+ clifford_freq = clifford_result.frequencies(binary)
+ clifford_freq = {state: clifford_freq[state] for state in numpy_freq.keys()}
+
+ assert len(numpy_freq) == len(clifford_freq)
+
+ for np_count, clif_count in zip(numpy_freq.values(), clifford_freq.values()):
+ backend.assert_allclose(np_count / nshots, clif_count / nshots, atol=1e-1)
+
+
+@pytest.mark.parametrize("seed", [2024])
+def test_collapsing_measurements(backend, seed):
+ backend.set_seed(2024)
+ clifford_bkd = construct_clifford_backend(backend)
+ gate_queue = random_clifford(
+ 3, density_matrix=True, seed=seed, backend=backend
+ ).queue
+ local_state = np.random.default_rng(seed)
+ measured_qubits = local_state.choice(range(3), size=2, replace=False)
+ c1 = Circuit(3)
+ c2 = Circuit(3, density_matrix=True)
+ for i, g in enumerate(gate_queue):
+ if i == int(len(gate_queue) / 2):
+ c1.add(gates.M(*measured_qubits))
+ c2.add(gates.M(*measured_qubits))
+ else:
+ c1.add(g)
+ c2.add(g)
+ c1.add(gates.M(*range(3)))
+ c2.add(gates.M(*range(3)))
+
+ clifford_bkd.set_seed(seed)
+ clifford_res = clifford_bkd.execute_circuit(c1, nshots=1000)
+
+ numpy_bkd.set_seed(seed)
+ numpy_res = numpy_bkd.execute_circuit(c2, nshots=1000)
+
+ backend.assert_allclose(
+ clifford_res.probabilities(), backend.cast(numpy_res.probabilities()), atol=1e-1
+ )
+
+
+def test_non_clifford_error(backend):
+ clifford_bkd = construct_clifford_backend(backend)
+ c = Circuit(1)
+ c.add(gates.T(0))
+ with pytest.raises(RuntimeError) as excinfo:
+ clifford_bkd.execute_circuit(c)
+ assert str(excinfo.value) == "Circuit contains non-Clifford gates."
+
+
+def test_initial_state(backend):
+ clifford_bkd = construct_clifford_backend(backend)
+ state = random_clifford(3, backend=numpy_bkd)
+ tmp = clifford_bkd.execute_circuit(state)
+ initial_symplectic_matrix = tmp.symplectic_matrix
+ initial_state = numpy_bkd.execute_circuit(state).state()
+ initial_state = np.outer(initial_state, np.transpose(np.conj(initial_state)))
+ c = random_clifford(3, density_matrix=True, backend=backend)
+ numpy_state = numpy_bkd.execute_circuit(c, initial_state=initial_state).state()
+ clifford_state = clifford_bkd.execute_circuit(
+ c, initial_state=initial_symplectic_matrix
+ ).state()
+ backend.assert_allclose(numpy_state, clifford_state)
+
+
+@pytest.mark.parametrize("seed", [10])
+def test_bitflip_noise(backend, seed):
+ rng = np.random.default_rng(seed)
+ clifford_bkd = construct_clifford_backend(backend)
+ c = random_clifford(5, seed=rng, backend=backend)
+ c_copy = c.copy()
+ qubits = rng.choice(range(3), size=2, replace=False)
+ c.add(gates.M(*qubits, p0=0.1, p1=0.5))
+ c_copy.add(gates.M(*qubits, p0=0.1, p1=0.5))
+ numpy_res = numpy_bkd.execute_circuit(c_copy)
+ clifford_res = clifford_bkd.execute_circuit(c)
+ backend.assert_allclose(
+ clifford_res.probabilities(qubits), numpy_res.probabilities(qubits), atol=1e-1
+ )
+
+
+@pytest.mark.parametrize("seed", [2024])
+def test_noise_channels(backend, seed):
+ backend.set_seed(seed)
+
+ clifford_bkd = construct_clifford_backend(backend)
+ clifford_bkd.set_seed(seed)
+
+ nqubits = 3
+
+ c = random_clifford(nqubits, density_matrix=True, seed=seed, backend=backend)
+
+ noise = NoiseModel()
+ noisy_gates = np.random.choice(c.queue, size=1, replace=False)
+ noise.add(PauliError([("X", 0.3)]), gates.H)
+ noise.add(DepolarizingError(0.3), noisy_gates[0].__class__)
+
+ c.add(gates.M(*range(nqubits)))
+ c_copy = c.copy()
+
+ c = noise.apply(c)
+ c_copy = noise.apply(c_copy)
+
+ numpy_bkd.set_seed(2024)
+ numpy_result = numpy_bkd.execute_circuit(c)
+ clifford_result = clifford_bkd.execute_circuit(c_copy)
+
+ backend.assert_allclose(
+ backend.cast(numpy_result.probabilities()),
+ clifford_result.probabilities(),
+ atol=1e-1,
+ )
+
+
+def test_stim(backend):
+ clifford_bkd = construct_clifford_backend(backend)
+ clifford_stim = CliffordBackend(engine="stim")
+
+ nqubits = 3
+ circuit = random_clifford(nqubits, backend=backend)
+
+ result_qibo = clifford_bkd.execute_circuit(circuit)
+ result_stim = clifford_stim.execute_circuit(circuit)
+
+ backend.assert_allclose(
+ result_stim.symplectic_matrix, result_qibo.symplectic_matrix
+ )
diff --git a/tests/test_backends_global.py b/tests/test_backends_global.py
new file mode 100644
index 000000000..9ad3a4b2f
--- /dev/null
+++ b/tests/test_backends_global.py
@@ -0,0 +1,110 @@
+import pytest
+
+import qibo
+from qibo import matrices
+
+
+def test_set_backend():
+ from qibo.backends import GlobalBackend
+
+ backend = GlobalBackend()
+ qibo.set_backend("numpy")
+ assert qibo.get_backend() == "numpy"
+ assert GlobalBackend().name == "numpy"
+
+
+def test_set_precision():
+ import numpy as np
+
+ assert qibo.get_precision() == "double"
+ qibo.set_precision("single")
+ assert matrices.I.dtype == np.complex64
+ assert qibo.get_precision() == "single"
+ qibo.set_precision("double")
+ assert matrices.I.dtype == np.complex128
+ assert qibo.get_precision() == "double"
+ with pytest.raises(ValueError):
+ qibo.set_precision("test")
+
+
+def test_set_device():
+ qibo.set_backend("numpy")
+ qibo.set_device("/CPU:0")
+ assert qibo.get_device() == "/CPU:0"
+ with pytest.raises(ValueError):
+ qibo.set_device("test")
+ with pytest.raises(ValueError):
+ qibo.set_device("/GPU:0")
+
+
+def test_set_threads():
+ with pytest.raises(ValueError):
+ qibo.set_threads(-2)
+ with pytest.raises(TypeError):
+ qibo.set_threads("test")
+
+ qibo.set_backend("numpy")
+ assert qibo.get_threads() == 1
+ with pytest.raises(ValueError):
+ qibo.set_threads(10)
+
+
+def test_set_shot_batch_size():
+ original_batch_size = qibo.get_batch_size()
+ qibo.set_batch_size(1024)
+ assert qibo.get_batch_size() == 1024
+ from qibo.config import SHOT_BATCH_SIZE
+
+ assert SHOT_BATCH_SIZE == 1024
+ with pytest.raises(TypeError):
+ qibo.set_batch_size("test")
+ with pytest.raises(ValueError):
+ qibo.set_batch_size(-10)
+ with pytest.raises(ValueError):
+ qibo.set_batch_size(2**35)
+ qibo.set_batch_size(original_batch_size)
+
+
+def test_set_metropolis_threshold():
+ original_threshold = qibo.get_metropolis_threshold()
+ qibo.set_metropolis_threshold(100)
+ assert qibo.get_metropolis_threshold() == 100
+ from qibo.config import SHOT_METROPOLIS_THRESHOLD
+
+ assert SHOT_METROPOLIS_THRESHOLD == 100
+ with pytest.raises(TypeError):
+ qibo.set_metropolis_threshold("test")
+ with pytest.raises(ValueError):
+ qibo.set_metropolis_threshold(-10)
+ qibo.set_metropolis_threshold(original_threshold)
+
+
+def test_circuit_execution():
+ qibo.set_backend("numpy")
+ c = qibo.models.Circuit(2)
+ c.add(qibo.gates.H(0))
+ result = c()
+ unitary = c.unitary()
+
+
+def test_gate_matrix():
+ qibo.set_backend("numpy")
+ gate = qibo.gates.H(0)
+ matrix = gate.matrix
+
+
+def test_check_backend(backend):
+ # testing when backend is not None
+ test = qibo.backends._check_backend(backend)
+
+ assert test.name == backend.name
+ assert test.__class__ == backend.__class__
+
+ # testing when backend is None
+ test = None
+ test = qibo.backends._check_backend(test)
+
+ target = qibo.backends.GlobalBackend()
+
+ assert test.name == target.name
+ assert test.__class__ == target.__class__
diff --git a/tests/test_backends_qibotn.py b/tests/test_backends_qibotn.py
new file mode 100644
index 000000000..ae850afef
--- /dev/null
+++ b/tests/test_backends_qibotn.py
@@ -0,0 +1,16 @@
+import os
+
+import qibo
+
+# Force quimb to use qibojit default number of threads.
+os.environ["NUMBA_NUM_THREADS"] = f"{qibo.get_threads()}"
+from qibotn.backends.quimb import QuimbBackend
+
+from qibo.backends import GlobalBackend
+
+
+def test_backend_qibotn():
+ qibo.set_backend(backend="qibotn", platform="qutensornet", runcard=None)
+ assert isinstance(GlobalBackend(), QuimbBackend)
+
+ qibo.set_backend("numpy")
diff --git a/tests/test_backends_qulacs.py b/tests/test_backends_qulacs.py
new file mode 100644
index 000000000..48f7c6d65
--- /dev/null
+++ b/tests/test_backends_qulacs.py
@@ -0,0 +1,43 @@
+import random
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.backends import GlobalBackend, MetaBackend, NumpyBackend, set_backend
+from qibo.quantum_info import random_clifford, random_density_matrix, random_statevector
+
+numpy_bkd = NumpyBackend()
+
+
+@pytest.mark.parametrize("density_matrix", [True, False])
+@pytest.mark.parametrize("with_measurements", [True, False])
+def test_qulacs(density_matrix, with_measurements):
+ c = random_clifford(3, backend=numpy_bkd, density_matrix=density_matrix)
+ if with_measurements:
+ measured_qubits = random.sample([0, 1, 2], 2)
+ c.add(gates.M(*measured_qubits))
+ qulacs_bkd = MetaBackend.load("qulacs")
+ nshots = 1000
+ qulacs_res = qulacs_bkd.execute_circuit(c, nshots=nshots)
+ numpy_res = numpy_bkd.execute_circuit(c, nshots=nshots)
+ numpy_bkd.assert_allclose(numpy_res.probabilities(), qulacs_res.probabilities())
+ if with_measurements:
+ numpy_freq = numpy_res.frequencies(binary=True)
+ qulacs_freq = qulacs_res.frequencies(binary=True)
+ numpy_freq = [numpy_freq.get(state, 0) / nshots for state in range(8)]
+ qulacs_freq = [qulacs_freq.get(state, 0) / nshots for state in range(8)]
+ numpy_bkd.assert_allclose(numpy_freq, qulacs_freq, atol=1e-1)
+
+
+def test_initial_state_error():
+ c = Circuit(1)
+ qulacs_bkd = MetaBackend.load("qulacs")
+ initial_state = np.array([0.0, 1.0])
+ with pytest.raises(NotImplementedError):
+ qulacs_bkd.execute_circuit(c, initial_state=initial_state)
+
+
+def test_set_backend():
+ set_backend("qulacs")
+ assert GlobalBackend().name == "qulacs"
diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py
new file mode 100644
index 000000000..b599a904f
--- /dev/null
+++ b/tests/test_callbacks.py
@@ -0,0 +1,403 @@
+"""Test methods defined in `qibo/core/callbacks.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import callbacks, gates, hamiltonians
+
+# Absolute testing tolerance for the cases of zero entanglement entropy
+from qibo.config import PRECISION_TOL
+from qibo.models import AdiabaticEvolution, Circuit
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+
+
+def test_abstract_callback_properties():
+ callback = callbacks.Callback()
+ callback.nqubits = 5
+ callback.append(1)
+ callback.extend([2, 3])
+ assert callback.nqubits == 5
+ assert callback.results == [1, 2, 3]
+
+
+def test_creating_callbacks():
+ callback = callbacks.EntanglementEntropy()
+ callback = callbacks.EntanglementEntropy([1, 2], compute_spectrum=True)
+ callback = callbacks.Norm()
+ callback = callbacks.Overlap(0)
+ callback = callbacks.Energy("test")
+ callback = callbacks.Gap()
+ callback = callbacks.Gap(2)
+ with pytest.raises(ValueError):
+ callback = callbacks.Gap("test")
+ with pytest.raises(TypeError):
+ callback = callbacks.Gap(1.0)
+
+
+def test_getitem_bad_indexing(backend):
+ entropy = callbacks.EntanglementEntropy([0])
+ c = Circuit(2)
+ c.add(gates.RY(0, 0.1234))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ final_state = backend.execute_circuit(c)
+ entropy[0]
+ with pytest.raises(IndexError):
+ entropy[1]
+ with pytest.raises(IndexError):
+ entropy["a"]
+
+
+def test_entropy_product_state(backend):
+ """Check that the |++> state has zero entropy."""
+ entropy = callbacks.EntanglementEntropy()
+ entropy.nqubits = 2
+ state = np.ones(4) / 2.0
+ result = entropy.apply(backend, state)
+ backend.assert_allclose(result, 0, atol=PRECISION_TOL)
+
+
+def test_entropy_singlet_state(backend):
+ """Check that the singlet state has maximum entropy."""
+ entropy = callbacks.EntanglementEntropy([0])
+ entropy.nqubits = 2
+ state = np.zeros(4)
+ state[0], state[-1] = 1, 1
+ state = state / np.sqrt(2)
+ result = entropy.apply(backend, state)
+ backend.assert_allclose(result, 1.0)
+
+
+def test_entropy_switch_partition(backend):
+ """Check that partition is switched to the largest counterpart."""
+ entropy = callbacks.EntanglementEntropy([0])
+ entropy.nqubits = 5
+ # Prepare ghz state of 5 qubits
+ state = np.zeros(2**5)
+ state[0], state[-1] = 1, 1
+ state = state / np.sqrt(2)
+ result = entropy.apply(backend, state)
+ backend.assert_allclose(result, 1.0)
+
+
+@pytest.mark.parametrize("base", [2, np.e, 10])
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_entropy_in_circuit(backend, density_matrix, base):
+ """Check that entropy calculation works in circuit."""
+ entropy = callbacks.EntanglementEntropy([0], compute_spectrum=True, base=base)
+ c = Circuit(2, density_matrix=density_matrix)
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.H(0))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ state = backend.execute_circuit(c)
+
+ target = [0, 0, np.log(2)] / np.log(base)
+ values = [backend.to_numpy(x) for x in entropy]
+ backend.assert_allclose(values, target, atol=PRECISION_TOL)
+
+ target_spectrum = [0.0] + list([0, 0, np.log(2), np.log(2)] / np.log(base))
+ entropy_spectrum = backend.np.ravel(
+ backend.np.concatenate(entropy.spectrum)
+ ).tolist()
+ backend.assert_allclose(entropy_spectrum, target_spectrum, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize(
+ "gateconf,target_entropy",
+ [
+ (["H", "CNOT", "entropy"], [1.0]),
+ (["H", "entropy", "CNOT"], [0.0]),
+ (["entropy", "H", "CNOT"], [0.0]),
+ (["entropy", "H", "CNOT", "entropy"], [0.0, 1.0]),
+ (["H", "entropy", "CNOT", "entropy"], [0.0, 1.0]),
+ (["entropy", "H", "entropy", "CNOT"], [0.0, 0.0]),
+ ],
+)
+def test_entropy_in_distributed_circuit(
+ backend, accelerators, gateconf, target_entropy
+):
+ """Check that various entropy configurations work in distributed circuit."""
+ target_c = Circuit(4)
+ target_c.add([gates.H(0), gates.CNOT(0, 1)])
+ target_state = backend.execute_circuit(target_c)
+
+ entropy = callbacks.EntanglementEntropy([0])
+ c = Circuit(4, accelerators)
+ for gate in gateconf:
+ if gate == "H":
+ c.add(gates.H(0))
+ elif gate == "CNOT":
+ c.add(gates.CNOT(0, 1))
+ elif gate == "entropy":
+ c.add(gates.CallbackGate(entropy))
+ final_state = backend.execute_circuit(c)
+ backend.assert_allclose(final_state, target_state)
+ values = [backend.to_numpy(x) for x in entropy[:]]
+ backend.assert_allclose(values, target_entropy, atol=PRECISION_TOL)
+
+
+def test_entropy_multiple_executions(backend, accelerators):
+ """Check entropy calculation when the callback is used in multiple executions."""
+ target_c = Circuit(4)
+ target_c.add([gates.RY(0, 0.1234), gates.CNOT(0, 1)])
+ target_state = backend.execute_circuit(target_c)
+
+ entropy = callbacks.EntanglementEntropy([0])
+ c = Circuit(4, accelerators)
+ c.add(gates.RY(0, 0.1234))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ state = backend.execute_circuit(c)
+ backend.assert_allclose(state, target_state)
+
+ target_c = Circuit(4)
+ target_c.add([gates.RY(0, 0.4321), gates.CNOT(0, 1)])
+ target_state = backend.execute_circuit(target_c)
+
+ c = Circuit(4, accelerators)
+ c.add(gates.RY(0, 0.4321))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ state = backend.execute_circuit(c)
+ backend.assert_allclose(state, target_state)
+
+ def target_entropy(t):
+ cos = np.cos(t / 2.0) ** 2
+ sin = np.sin(t / 2.0) ** 2
+ return -cos * np.log2(cos) - sin * np.log2(sin)
+
+ target = [0, target_entropy(0.1234), 0, target_entropy(0.4321)]
+ values = [backend.to_numpy(x) for x in entropy[:]]
+ backend.assert_allclose(values, target, atol=1e-6)
+
+ c = Circuit(8, accelerators)
+ with pytest.raises(RuntimeError):
+ c.add(gates.CallbackGate(entropy))
+ state = backend.execute_circuit(c)
+
+
+def test_entropy_large_circuit(backend, accelerators):
+ """Check that entropy calculation works for variational like circuit."""
+ thetas = np.pi * np.random.random((3, 8))
+ target_entropy = callbacks.EntanglementEntropy([0, 2, 4, 5])
+ target_entropy.nqubits = 8
+ c1 = Circuit(8)
+ c1.add(gates.RY(i, thetas[0, i]) for i in range(8))
+ c1.add(gates.CZ(i, i + 1) for i in range(0, 7, 2))
+ state1 = backend.execute_circuit(c1).state()
+ e1 = target_entropy.apply(backend, state1)
+
+ c2 = Circuit(8)
+ c2.add(gates.RY(i, thetas[1, i]) for i in range(8))
+ c2.add(gates.CZ(i, i + 1) for i in range(1, 7, 2))
+ c2.add(gates.CZ(0, 7))
+ state2 = backend.execute_circuit(c1 + c2).state()
+ e2 = target_entropy.apply(backend, state2)
+
+ c3 = Circuit(8)
+ c3.add(gates.RY(i, thetas[2, i]) for i in range(8))
+ c3.add(gates.CZ(i, i + 1) for i in range(0, 7, 2))
+ state3 = backend.execute_circuit(c1 + c2 + c3).state()
+ e3 = target_entropy.apply(backend, state3)
+
+ entropy = callbacks.EntanglementEntropy([0, 2, 4, 5])
+ c = Circuit(8, accelerators)
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.RY(i, thetas[0, i]) for i in range(8))
+ c.add(gates.CZ(i, i + 1) for i in range(0, 7, 2))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.RY(i, thetas[1, i]) for i in range(8))
+ c.add(gates.CZ(i, i + 1) for i in range(1, 7, 2))
+ c.add(gates.CZ(0, 7))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.RY(i, thetas[2, i]) for i in range(8))
+ c.add(gates.CZ(i, i + 1) for i in range(0, 7, 2))
+ c.add(gates.CallbackGate(entropy))
+ state = backend.execute_circuit(c)
+
+ backend.assert_allclose(state3, state)
+ values = [backend.to_numpy(x) for x in entropy[:]]
+ targets = [backend.to_numpy(x) for x in [0, e1, e2, e3]]
+ backend.assert_allclose(values, targets)
+
+
+def test_entropy_density_matrix(backend):
+ from qibo.quantum_info import random_density_matrix
+
+ rho = random_density_matrix(2**4, backend=backend)
+ # this rho is not always positive. Make rho positive for this application
+ _, u = np.linalg.eigh(backend.to_numpy(rho))
+ u = backend.cast(u, dtype=u.dtype)
+ matrix = np.random.random(u.shape[0])
+ matrix = backend.cast(matrix, dtype=u.dtype)
+ rho = backend.np.matmul(
+ backend.np.matmul(u, backend.np.diag(5 * matrix)),
+ backend.np.conj(backend.np.transpose(u, (1, 0))),
+ )
+ # this is a positive rho
+
+ entropy = callbacks.EntanglementEntropy([1, 3])
+ entropy.nqubits = 4
+ final_ent = entropy.apply(backend, rho)
+
+ rho = backend.to_numpy(rho.reshape(8 * (2,)))
+ reduced_rho = np.einsum("abcdafch->bdfh", rho).reshape((4, 4))
+ eigvals = np.linalg.eigvalsh(reduced_rho).real
+ # assert that all eigenvalues are non-negative
+ assert (eigvals >= 0).prod()
+ mask = eigvals > 0
+ target_ent = -(eigvals[mask] * np.log2(eigvals[mask])).sum()
+ backend.assert_allclose(final_ent, target_ent)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("copy", [False, True])
+def test_state_callback(backend, density_matrix, copy):
+ statec = callbacks.State(copy=copy)
+ c = Circuit(2, density_matrix=density_matrix)
+ c.add(gates.H(0))
+ c.add(gates.CallbackGate(statec))
+ c.add(gates.H(1))
+ c.add(gates.CallbackGate(statec))
+ final_state = backend.execute_circuit(c)
+
+ target_state0 = np.array([1, 0, 1, 0]) / np.sqrt(2)
+ target_state1 = np.ones(4) / 2.0
+ if not copy and backend.name == "qibojit":
+ # when copy is disabled in the callback and in-place updates are used
+ target_state0 = target_state1
+ if density_matrix:
+ target_state0 = np.tensordot(target_state0, target_state0, axes=0)
+ target_state1 = np.tensordot(target_state1, target_state1, axes=0)
+ backend.assert_allclose(statec[0], target_state0)
+ backend.assert_allclose(statec[1], target_state1)
+
+
+@pytest.mark.parametrize("seed", list(range(1, 5 + 1)))
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_norm(backend, density_matrix, seed):
+ norm = callbacks.Norm()
+ if density_matrix:
+ norm.nqubits = 1
+ state = random_density_matrix(2**norm.nqubits, seed=seed, backend=backend)
+ target_norm = backend.np.trace(state)
+ final_norm = norm.apply_density_matrix(backend, state)
+ else:
+ norm.nqubits = 2
+ state = random_statevector(2**norm.nqubits, seed=seed, backend=backend)
+ target_norm = np.sqrt((np.abs(backend.to_numpy(state)) ** 2).sum())
+ final_norm = norm.apply(backend, state)
+
+ backend.assert_allclose(final_norm, target_norm)
+
+
+@pytest.mark.parametrize("seed", list(range(1, 5 + 1)))
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("nqubits", list(range(2, 6 + 1, 2)))
+def test_overlap(backend, nqubits, density_matrix, seed):
+ dims = 2**nqubits
+ if density_matrix:
+ state0 = random_density_matrix(dims, seed=seed, backend=backend)
+ state1 = random_density_matrix(dims, seed=seed + 1, backend=backend)
+ else:
+ state0 = random_statevector(dims, seed=seed, backend=backend)
+ state1 = random_statevector(dims, seed=seed + 1, backend=backend)
+
+ overlap = callbacks.Overlap(state0)
+ overlap.nqubits = nqubits
+
+ if density_matrix:
+ final_overlap = overlap.apply_density_matrix(backend, state1)
+ target_overlap = np.trace(
+ np.transpose(np.conj(backend.to_numpy(state0))) @ backend.to_numpy(state1)
+ )
+ else:
+ final_overlap = overlap.apply(backend, state1)
+ target_overlap = np.abs(
+ np.sum(np.conj(backend.to_numpy(state0)) * backend.to_numpy(state1))
+ )
+
+ backend.assert_allclose(final_overlap, target_overlap)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_energy(backend, density_matrix):
+ from qibo import hamiltonians
+
+ ham = hamiltonians.TFIM(4, h=1.0, backend=backend)
+ energy = callbacks.Energy(ham)
+ matrix = backend.to_numpy(ham.matrix)
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ if density_matrix:
+ from qibo.quantum_info import random_density_matrix
+
+ state = random_density_matrix(2**4, backend=backend)
+ target_energy = backend.np.trace(backend.np.matmul(matrix, state))
+ final_energy = energy.apply_density_matrix(backend, state)
+ else:
+ from qibo.quantum_info import random_statevector
+
+ state = random_statevector(2**4, backend=backend)
+ target_energy = np.matmul(
+ np.conj(backend.to_numpy(state)),
+ np.matmul(backend.to_numpy(matrix), backend.to_numpy(state)),
+ )
+ final_energy = energy.apply(backend, state)
+ backend.assert_allclose(final_energy, target_energy)
+
+
+@pytest.mark.parametrize("dense", [False, True])
+@pytest.mark.parametrize("check_degenerate", [False, True])
+def test_gap(backend, dense, check_degenerate):
+ h0 = hamiltonians.X(4, dense=dense, backend=backend)
+ h = 0 if check_degenerate else 1
+ h1 = hamiltonians.TFIM(4, h=h, dense=dense, backend=backend)
+
+ ham = lambda t: (1 - t) * h0.matrix + t * h1.matrix
+ targets = {"ground": [], "excited": [], "gap": []}
+ for t in np.linspace(0, 1, 11):
+ eigvals = np.real(np.linalg.eigvalsh(backend.to_numpy(ham(t))))
+ targets["ground"].append(eigvals[0])
+ targets["excited"].append(eigvals[1])
+ targets["gap"].append(eigvals[1] - eigvals[0])
+ if check_degenerate:
+ targets["gap"][-1] = eigvals[3] - eigvals[0]
+
+ gap = callbacks.Gap(check_degenerate=check_degenerate)
+ ground = callbacks.Gap(0)
+ excited = callbacks.Gap(1)
+ evolution = AdiabaticEvolution(
+ h0, h1, lambda t: t, dt=1e-1, callbacks=[gap, ground, excited]
+ )
+ final_state = evolution(final_time=1.0)
+ targets = {k: np.stack(v) for k, v in targets.items()}
+
+ values = {
+ "ground": np.array([backend.to_numpy(x) for x in ground]),
+ "excited": np.array([backend.to_numpy(x) for x in excited]),
+ "gap": np.array([backend.to_numpy(x) for x in gap]),
+ }
+ for k, v in values.items():
+ backend.assert_allclose(v, targets.get(k))
+
+
+def test_gap_errors():
+ """Check errors in gap callback instantiation."""
+ # invalid string ``mode``
+ with pytest.raises(ValueError):
+ gap = callbacks.Gap("test")
+ # invalid ``mode`` type
+ with pytest.raises(TypeError):
+ gap = callbacks.Gap([])
+
+ gap = callbacks.Gap()
+ # call before setting evolution model
+ with pytest.raises(RuntimeError):
+ gap.apply(None, np.ones(4))
+ # not implemented for density matrices
+ with pytest.raises(NotImplementedError):
+ gap.apply_density_matrix(None, np.zeros(8))
diff --git a/tests/test_cirq.py b/tests/test_cirq.py
new file mode 100644
index 000000000..8ff8498d7
--- /dev/null
+++ b/tests/test_cirq.py
@@ -0,0 +1,318 @@
+"""Test that Qibo gate execution agrees with Cirq."""
+
+import cirq
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.backends import NumpyBackend
+from qibo.models import QFT
+from qibo.quantum_info import random_statevector, random_unitary
+
+numpy_backend = NumpyBackend()
+
+
+def random_active_qubits(nqubits, nmin=None, nactive=None):
+ """Generates random list of target and control qubits."""
+ all_qubits = np.arange(nqubits)
+ np.random.shuffle(all_qubits)
+ if nactive is None:
+ nactive = np.random.randint(nmin + 1, nqubits)
+ return list(all_qubits[:nactive])
+
+
+def execute_cirq(cirq_gates, nqubits, initial_state=None):
+ """Executes a Cirq circuit with the given list of gates."""
+ c = cirq.Circuit()
+ q = [cirq.LineQubit(i) for i in range(nqubits)]
+ # apply identity gates to all qubits so that they become part of the circuit
+ c.append([cirq.I(qi) for qi in q])
+ for gate, targets in cirq_gates:
+ c.append(gate(*[q[i] for i in targets]))
+ result = cirq.Simulator().simulate(
+ c, initial_state=initial_state
+ ) # pylint: disable=no-member
+ depth = len(cirq.Circuit(c.all_operations()))
+ return result.final_state_vector, depth - 1
+
+
+def assert_gates_equivalent(
+ backend, qibo_gate, cirq_gates, nqubits, ndevices=None, atol=1e-7
+):
+ """Asserts that QIBO and Cirq gates have equivalent action on a random state.
+
+ Args:
+ qibo_gate: QIBO gate.
+ cirq_gates: List of tuples (cirq gate, target qubit IDs).
+ nqubits: Total number of qubits in the circuit.
+ atol: Absolute tolerance in state vector comparsion.
+ """
+ initial_state = random_statevector(2**nqubits, backend=numpy_backend)
+ copy = numpy_backend.cast(initial_state, copy=True)
+ target_state, target_depth = execute_cirq(cirq_gates, nqubits, copy)
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ accelerators = None
+ if ndevices is not None:
+ accelerators = {"/GPU:0": ndevices}
+
+ c = Circuit(nqubits, accelerators)
+ c.add(qibo_gate)
+ assert c.depth == target_depth
+ if accelerators and not backend.supports_multigpu:
+ with pytest.raises(NotImplementedError):
+ final_state = backend.execute_circuit(c, np.copy(initial_state)).state()
+ else:
+ final_state = backend.execute_circuit(c, np.copy(initial_state)).state()
+ backend.assert_allclose(final_state, target_state, atol=atol)
+
+
+def assert_cirq_gates_equivalent(qibo_gate, cirq_gate):
+ """Asserts that qibo gate is equivalent to cirq gate.
+
+ Checks that:
+ * Gate type agrees.
+ * Target and control qubits agree.
+ * Parameter (if applicable) agrees.
+ Cirq gate parameters are extracted by parsing the gate string.
+ """
+ import re
+
+ # Fix for cirq >= 0.15.0
+ chars = iter(str(cirq_gate))
+ clean_gate = []
+ open = False
+ for char in chars:
+ if char == "q":
+ open = True
+ next(chars)
+ elif open and char == ")":
+ open = False
+ else:
+ clean_gate.append(char)
+
+ pieces = [x for x in re.split("[()]", "".join(clean_gate)) if x]
+ if len(pieces) == 2:
+ gatename, targets = pieces
+ theta = None
+ elif len(pieces) == 3:
+ gatename, theta, targets = pieces
+ else: # pragma: no cover
+ # case not tested because it fails
+ raise RuntimeError(f"Cirq gate parsing failed with {pieces}.")
+
+ qubits = list(int(x) for x in targets.replace(" ", "").split(","))
+ targets = (qubits.pop(),)
+ controls = set(qubits)
+
+ qibo_to_cirq = {"CNOT": "CNOT", "RY": "Ry", "TOFFOLI": "TOFFOLI"}
+ assert qibo_to_cirq[qibo_gate.__class__.__name__] == gatename
+ assert qibo_gate.target_qubits == targets
+ assert set(qibo_gate.control_qubits) == controls
+ if theta is not None:
+ if "π" in theta:
+ theta = np.pi * float(theta.replace("π", ""))
+ else: # pragma: no cover
+ # case doesn't happen in tests (could remove)
+ theta = float(theta)
+ np.testing.assert_allclose(theta, qibo_gate.parameters)
+
+
+@pytest.mark.parametrize(
+ ("target", "controls", "free"),
+ [
+ (0, (1,), ()),
+ (2, (0, 1), ()),
+ (3, (0, 1, 4), (2, 5)),
+ (7, (0, 1, 2, 3, 4), (5, 6)),
+ ],
+)
+def test_x_decompose_with_cirq(target, controls, free):
+ """Check that decomposition of multi-control ``X`` agrees with Cirq."""
+ gate = gates.X(target).controlled_by(*controls)
+ qibo_decomp = gate.decompose(*free, use_toffolis=False)
+
+ # Calculate the decomposition using Cirq.
+ nqubits = max((target,) + controls + free) + 1
+ qubits = [cirq.LineQubit(i) for i in range(nqubits)]
+ controls = [qubits[i] for i in controls]
+ free = [qubits[i] for i in free]
+ cirq_decomp = cirq.decompose_multi_controlled_x(controls, qubits[target], free)
+ assert len(qibo_decomp) == len(cirq_decomp)
+ for qibo_gate, cirq_gate in zip(qibo_decomp, cirq_decomp):
+ assert_cirq_gates_equivalent(qibo_gate, cirq_gate)
+
+
+@pytest.mark.parametrize(
+ ("gate_name", "nqubits", "ndevices"),
+ [
+ ("H", 3, None),
+ ("H", 3, 2),
+ ("X", 2, None),
+ ("X", 2, 2),
+ ("Y", 1, None),
+ ("Z", 1, None),
+ ("S", 4, None),
+ ("T", 4, None),
+ ],
+)
+def test_one_qubit_gates(backend, gate_name, nqubits, ndevices):
+ """Check simple one-qubit gates."""
+ targets = random_active_qubits(nqubits, nactive=1)
+ qibo_gate = getattr(gates, gate_name)(*targets)
+ cirq_gate = [(getattr(cirq, gate_name), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(
+ ("gate_name", "nqubits", "ndevices"),
+ [("RX", 3, None), ("RX", 3, 4), ("RY", 2, None), ("RY", 2, 2), ("RZ", 1, None)],
+)
+def test_one_qubit_parametrized_gates(backend, gate_name, nqubits, ndevices):
+ """Check parametrized one-qubit rotations."""
+ theta = 0.1234
+ targets = random_active_qubits(nqubits, nactive=1)
+ qibo_gate = getattr(gates, gate_name)(*targets, theta)
+ cirq_gate = [(getattr(cirq, gate_name.lower())(theta), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(("nqubits", "ndevices"), [(2, None), (3, 4), (2, 2)])
+def test_u1_gate(backend, nqubits, ndevices):
+ """Check U1 gate."""
+ theta = 0.1234
+ targets = random_active_qubits(nqubits, nactive=1)
+ qibo_gate = gates.U1(*targets, theta)
+ cirq_gate = [(cirq.ZPowGate(exponent=theta / np.pi), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize("gate_name", ["CNOT", "SWAP", "CZ"])
+@pytest.mark.parametrize("nqubits", [3, 4, 5])
+@pytest.mark.parametrize("ndevices", [None, 2])
+def test_two_qubit_gates(backend, gate_name, nqubits, ndevices):
+ """Check two-qubit gates."""
+ targets = random_active_qubits(nqubits, nactive=2)
+ qibo_gate = getattr(gates, gate_name)(*targets)
+ cirq_gate = [(getattr(cirq, gate_name), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(
+ ("nqubits", "ndevices"), [(2, None), (6, None), (6, 2), (7, None), (7, 4)]
+)
+def test_two_qubit_parametrized_gates(backend, nqubits, ndevices):
+ """Check ``CU1`` and ``fSim`` gate."""
+ theta = 0.1234
+ phi = 0.4321
+
+ targets = random_active_qubits(nqubits, nactive=2)
+ qibo_gate = gates.CU1(*targets, np.pi * theta)
+ cirq_gate = [(cirq.CZPowGate(exponent=theta), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits)
+
+ targets = random_active_qubits(nqubits, nactive=2)
+ qibo_gate = gates.fSim(*targets, theta, phi)
+ cirq_gate = [(cirq.FSimGate(theta=theta, phi=phi), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(
+ ("nqubits", "ndevices"), [(5, None), (6, None), (9, None), (5, 2), (6, 4), (9, 8)]
+)
+def test_unitary_matrix_gate(backend, nqubits, ndevices):
+ """Check arbitrary unitary gate."""
+ matrix = random_unitary(2**1, backend=numpy_backend)
+ targets = random_active_qubits(nqubits, nactive=1)
+ qibo_gate = gates.Unitary(matrix, *targets)
+ cirq_gate = [(cirq.MatrixGate(matrix), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits)
+
+ for _ in range(10):
+ matrix = random_unitary(2**2, backend=numpy_backend)
+ targets = random_active_qubits(nqubits, nactive=2)
+ qibo_gate = gates.Unitary(matrix, *targets)
+ cirq_gate = [(cirq.MatrixGate(matrix), targets)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(
+ ("gate_name", "nqubits", "ndevices"),
+ [
+ ("H", 3, None),
+ ("Z", 4, None),
+ ("Y", 5, 4),
+ ("X", 6, None),
+ ("H", 7, 2),
+ ("Z", 8, 8),
+ ("Y", 12, 16),
+ ("S", 13, 4),
+ ("T", 9, 2),
+ ],
+)
+def test_one_qubit_gates_controlled_by(backend, gate_name, nqubits, ndevices):
+ """Check one-qubit gates controlled on arbitrary number of qubits."""
+ for _ in range(5):
+ activeq = random_active_qubits(nqubits, nmin=1)
+ qibo_gate = getattr(gates, gate_name)(activeq[-1]).controlled_by(*activeq[:-1])
+ cirq_gate = [(getattr(cirq, gate_name).controlled(len(activeq) - 1), activeq)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize(
+ ("nqubits", "ndevices"),
+ [
+ (4, None),
+ (5, None),
+ (8, None),
+ (12, None),
+ (15, None),
+ (17, None),
+ (6, 2),
+ (9, 2),
+ (11, 4),
+ (13, 8),
+ (14, 16),
+ ],
+)
+def test_two_qubit_gates_controlled_by(backend, nqubits, ndevices):
+ """Check ``SWAP`` and ``fSim`` gates controlled on arbitrary number of qubits."""
+ for _ in range(5):
+ activeq = random_active_qubits(nqubits, nmin=2)
+ qibo_gate = gates.SWAP(*activeq[-2:]).controlled_by(*activeq[:-2])
+ cirq_gate = [(cirq.SWAP.controlled(len(activeq) - 2), activeq)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+ theta = np.random.random()
+ phi = np.random.random()
+ qibo_gate = gates.fSim(*activeq[-2:], theta, phi).controlled_by(*activeq[:-2])
+ cirq_gate = [(cirq.FSimGate(theta, phi).controlled(len(activeq) - 2), activeq)]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize("nqubits", [5, 12, 13, 14])
+@pytest.mark.parametrize("ntargets", [1, 2])
+@pytest.mark.parametrize("ndevices", [None, 2, 8])
+def test_unitary_matrix_gate_controlled_by(backend, nqubits, ntargets, ndevices):
+ """Check arbitrary unitary gate controlled on arbitrary number of qubits."""
+ for _ in range(10):
+ activeq = random_active_qubits(nqubits, nactive=5)
+ matrix = random_unitary(2**ntargets, backend=numpy_backend)
+ qibo_gate = gates.Unitary(matrix, *activeq[-ntargets:]).controlled_by(
+ *activeq[:-ntargets]
+ )
+ cirq_gate = [
+ (cirq.MatrixGate(matrix).controlled(len(activeq) - ntargets), activeq)
+ ]
+ assert_gates_equivalent(backend, qibo_gate, cirq_gate, nqubits, ndevices)
+
+
+@pytest.mark.parametrize("nqubits", [5, 6, 7, 11, 12])
+def test_qft(backend, accelerators, nqubits):
+ c = QFT(nqubits, accelerators=accelerators)
+ initial_state = random_statevector(2**nqubits, backend=numpy_backend)
+ final_state = backend.execute_circuit(c, np.copy(initial_state)).state()
+ final_state = backend.cast(final_state, dtype=final_state.dtype)
+ cirq_gates = [(cirq.qft, list(range(nqubits)))]
+ target_state, _ = execute_cirq(cirq_gates, nqubits, np.copy(initial_state))
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ backend.assert_allclose(target_state, final_state, atol=1e-6)
diff --git a/tests/test_derivative.py b/tests/test_derivative.py
new file mode 100644
index 000000000..8a39efcee
--- /dev/null
+++ b/tests/test_derivative.py
@@ -0,0 +1,115 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, hamiltonians
+from qibo.derivative import finite_differences, parameter_shift
+from qibo.symbols import Z
+
+
+# defining an observable
+def hamiltonian(nqubits, backend):
+ return hamiltonians.hamiltonians.SymbolicHamiltonian(
+ np.prod([Z(i) for i in range(nqubits)]), backend=backend
+ )
+
+
+# defining a dummy circuit
+def circuit(nqubits=1):
+ c = Circuit(nqubits)
+ # all gates for which generator eigenvalue is implemented
+ c.add(gates.RX(q=0, theta=0))
+ c.add(gates.RY(q=0, theta=0))
+ c.add(gates.RZ(q=0, theta=0))
+ c.add(gates.M(0))
+
+ return c
+
+
+@pytest.mark.parametrize("nshots, atol", [(None, 1e-8), (100000, 1e-2)])
+@pytest.mark.parametrize(
+ "scale_factor, grads",
+ [(1, [-8.51104358e-02, -5.20075970e-01, 0]), (0.5, [-0.02405061, -0.13560379, 0])],
+)
+def test_standard_parameter_shift(backend, nshots, atol, scale_factor, grads):
+
+ # initializing the circuit
+ c = circuit(nqubits=1)
+ backend.set_seed(42)
+
+ # some parameters
+ # we know the derivative's values with these params
+ test_params = np.linspace(0.1, 1, 3)
+ test_params *= scale_factor
+ c.set_parameters(test_params)
+
+ test_hamiltonian = hamiltonian(nqubits=1, backend=backend)
+
+ # testing parameter out of bounds
+ with pytest.raises(ValueError):
+ grad = parameter_shift(
+ circuit=c, hamiltonian=test_hamiltonian, parameter_index=5
+ )
+
+ # testing hamiltonian type
+ with pytest.raises(TypeError):
+ grad = parameter_shift(
+ circuit=c, hamiltonian=c, parameter_index=0, nshots=nshots
+ )
+
+ # executing all the procedure
+ grad = [
+ parameter_shift(
+ circuit=c,
+ hamiltonian=test_hamiltonian,
+ parameter_index=i,
+ scale_factor=scale_factor,
+ nshots=nshots,
+ )
+ for i in range(3)
+ ]
+
+ # check of known values
+ for i in range(3):
+ backend.assert_allclose(grad[i], grads[i], atol=atol)
+
+
+@pytest.mark.parametrize("step_size", [10**-i for i in range(5, 10, 1)])
+def test_finite_differences(backend, step_size):
+ # initializing the circuit
+ c = circuit(nqubits=1)
+ backend.set_seed(42)
+
+ # some parameters
+ test_params = np.linspace(0.1, 1, 3)
+ grads = [-8.51104358e-02, -5.20075970e-01, 0]
+ atol = 1e-6
+ c.set_parameters(test_params)
+
+ test_hamiltonian = hamiltonian(nqubits=1, backend=backend)
+
+ # testing parameter out of bounds
+ with pytest.raises(ValueError):
+ grad_0 = finite_differences(
+ circuit=c, hamiltonian=test_hamiltonian, parameter_index=5
+ )
+
+ # testing hamiltonian type
+ with pytest.raises(TypeError):
+ grad_0 = finite_differences(circuit=c, hamiltonian=c, parameter_index=0)
+
+ # executing all the procedure
+ grad_0 = finite_differences(
+ circuit=c, hamiltonian=test_hamiltonian, parameter_index=0, step_size=step_size
+ )
+ grad_1 = finite_differences(
+ circuit=c, hamiltonian=test_hamiltonian, parameter_index=1, step_size=step_size
+ )
+ grad_2 = finite_differences(
+ circuit=c, hamiltonian=test_hamiltonian, parameter_index=2, step_size=step_size
+ )
+
+ # check of known values
+ # calculated using tf.GradientTape
+ backend.assert_allclose(grad_0, grads[0], atol=atol)
+ backend.assert_allclose(grad_1, grads[1], atol=atol)
+ backend.assert_allclose(grad_2, grads[2], atol=atol)
diff --git a/tests/test_gates_abstract.py b/tests/test_gates_abstract.py
new file mode 100644
index 000000000..c3e4fabbd
--- /dev/null
+++ b/tests/test_gates_abstract.py
@@ -0,0 +1,471 @@
+"""Tests methods defined in `qibo/gates/abstract.py` and
+`qibo/gates/gates.py`."""
+
+import json
+from typing import Optional
+
+import pytest
+
+from qibo import gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.gates import abstract
+
+
+@pytest.mark.parametrize(
+ "gatename", ["H", "X", "Y", "Z", "S", "SDG", "T", "TDG", "I", "Align"]
+)
+def test_one_qubit_gates_init(gatename):
+ gate = getattr(gates, gatename)(0)
+ assert gate.target_qubits == (0,)
+
+
+def gate_from_json(gatename: str, control: Optional[list] = None):
+ gate = getattr(gates, gatename)(0)
+
+ control = [] if control is None else control
+
+ json_general = f"""
+ {{
+ "name": {{}},
+ "init_args": [0],
+ "init_kwargs": {{}},
+ "_target_qubits": [0],
+ "_control_qubits": {control}
+ }}
+ """
+
+ json_gate = json.loads(json_general)
+ json_gate["name"] = gate.name
+
+ return gate, json_gate
+
+
+@pytest.mark.parametrize(
+ "gatename", ["H", "X", "Y", "Z", "S", "SDG", "T", "TDG", "I", "Align"]
+)
+def test_one_qubit_gates_serialization(gatename):
+ gate, json_gate = gate_from_json(gatename)
+ raw = gate.raw
+
+ assert isinstance(raw, dict)
+ assert gate.to_json() == json.dumps(raw)
+
+ del raw["_class"]
+
+ # raw may contain some objects later converted by JSON (e.g. tuples)
+ assert json.loads(json.dumps(raw)) == json_gate
+ assert gates.Gate.from_dict(gate.raw).raw == gate.raw
+
+
+@pytest.mark.parametrize(
+ "gatename", ["H", "X", "Y", "Z", "S", "SDG", "T", "TDG", "I", "Align"]
+)
+def test_controlled_gates_serialization(gatename):
+ gate, _ = gate_from_json(gatename, control=[1, 4])
+
+ assert isinstance(gate.raw, dict)
+ assert gates.Gate.from_dict(gate.raw).raw == gate.raw
+
+
+def test_gates_serialization_errors(monkeypatch):
+ with pytest.raises(ValueError, match="Unknown"):
+ _ = abstract.Gate.from_dict({"_class": "Ciao"})
+
+ error_tag = "not-control-error"
+
+ def mock_controlled_by(*args, **kwargs):
+ raise RuntimeError(error_tag)
+
+ monkeypatch.setattr(abstract.Gate, "controlled_by", mock_controlled_by)
+
+ with pytest.raises(RuntimeError, match=error_tag):
+ _ = abstract.Gate.from_dict(
+ {"_class": "H", "init_args": (0,), "init_kwargs": {}, "_control_qubits": ()}
+ )
+
+
+@pytest.mark.parametrize(
+ "controls,instance", [((1,), "CNOT"), ((1, 2), "TOFFOLI"), ((1, 2, 4), "X")]
+)
+def test_x_controlled_by(controls, instance):
+ gate = gates.X(0).controlled_by(*controls)
+ assert gate.target_qubits == (0,)
+ assert gate.control_qubits == controls
+ assert isinstance(gate, getattr(gates, instance))
+
+
+def test_x_decompose_with_few_controls():
+ """Check ``X`` decomposition with less than three controls."""
+ gate = gates.X(0)
+ decomp = gate.decompose(1, 2)
+ assert len(decomp) == 1
+ assert isinstance(decomp[0], gates.X)
+
+
+@pytest.mark.parametrize("use_toffolis", [True, False])
+def test_x_decomposition_errors(use_toffolis):
+ """Check ``X`` decomposition errors."""
+ gate = gates.X(0).controlled_by(1, 2, 3, 4)
+ with pytest.raises(ValueError):
+ _ = gate.decompose(2, 3, use_toffolis=use_toffolis)
+
+
+@pytest.mark.parametrize("controls,instance", [((1,), "CZ"), ((1, 2), "Z")])
+def test_z_controlled_by(controls, instance):
+ gate = gates.Z(0).controlled_by(*controls)
+ assert gate.target_qubits == (0,)
+ assert gate.control_qubits == controls
+ assert isinstance(gate, getattr(gates, instance))
+
+
+@pytest.mark.parametrize(
+ "targets,p0,p1",
+ [((0,), None, None), ((0, 1, 2), None, None), ((0, 3, 2), 0.2, 0.1)],
+)
+def test_measurement_init(targets, p0, p1):
+ # also tests `_get_bitflip_map`
+ gate = gates.M(*targets, p0=p0, p1=p1)
+ assert gate.target_qubits == targets
+ p0map = {q: 0 if p0 is None else p0 for q in targets}
+ p1map = {q: 0 if p1 is None else p1 for q in targets}
+ assert gate.bitflip_map == (p0map, p1map)
+
+
+def test_measurement_add():
+ gate = gates.M(0, 2)
+ assert gate.target_qubits == (0, 2)
+ assert gate.bitflip_map == 2 * ({0: 0, 2: 0},)
+ gate.add(gates.M(1, 3, p0=0.3, p1=0.0))
+ assert gate.target_qubits == (0, 2, 1, 3)
+ assert gate.bitflip_map == ({0: 0, 1: 0.3, 2: 0, 3: 0.3}, {0: 0, 1: 0, 2: 0, 3: 0})
+
+
+def test_measurement_errors():
+ gate = gates.M(0)
+ with pytest.raises(NotImplementedError):
+ gate.controlled_by(1)
+
+
+@pytest.mark.parametrize(
+ "gatename,params",
+ [
+ ("RX", (0.1234,)),
+ ("RY", (0.1234,)),
+ ("RZ", (0.1234,)),
+ ("U1", (0.1234,)),
+ ("U2", (0.1234, 0.4321)),
+ ("U3", (0.1234, 0.4321, 0.5678)),
+ ],
+)
+def test_one_qubit_rotations_init(gatename, params):
+ gate = getattr(gates, gatename)(0, *params)
+ assert gate.target_qubits == (0,)
+ assert gate.parameters == params
+
+
+@pytest.mark.parametrize(
+ "gatename,params",
+ [
+ ("RX", (0.1234,)),
+ ("RY", (0.1234,)),
+ ("RZ", (0.1234,)),
+ ("U1", (0.1234,)),
+ ("U2", (0.1234, 0.4321)),
+ ("U3", (0.1234, 0.4321, 0.5678)),
+ ],
+)
+def test_one_qubit_rotations_serialization(gatename, params):
+ gate = getattr(gates, gatename)(0, *params)
+
+ json_general = """
+ {
+ "name": {},
+ "init_args": [0],
+ "init_kwargs": {},
+ "_target_qubits": [0],
+ "_control_qubits": []
+ }
+ """
+
+ json_gate = json.loads(json_general)
+ json_gate["name"] = gate.name
+ json_gate["init_kwargs"] = gate.init_kwargs
+ del json_gate["init_kwargs"]["trainable"]
+
+ raw = gate.raw
+ del raw["_class"]
+
+ raw = gate.raw
+
+ assert isinstance(raw, dict)
+ assert gate.to_json() == json.dumps(raw)
+
+ del raw["_class"]
+
+ # raw may contain some objects later converted by JSON (e.g. tuples)
+ assert json.loads(json.dumps(raw)) == json_gate
+ assert gates.Gate.from_dict(gate.raw).raw == gate.raw
+
+
+@pytest.mark.parametrize(
+ "gatename,params",
+ [
+ ("RX", (0.1234,)),
+ ("RY", (0.1234,)),
+ ("RZ", (0.1234,)),
+ ("U1", (0.1234,)),
+ ("U2", (0.1234, 0.4321)),
+ ("U3", (0.1234, 0.4321, 0.5678)),
+ ],
+)
+def test_one_qubit_rotations_controlled_by(gatename, params):
+ gate = getattr(gates, gatename)(0, *params).controlled_by(1)
+ assert gate.target_qubits == (0,)
+ assert gate.control_qubits == (1,)
+ assert isinstance(gate, getattr(gates, f"C{gatename}"))
+ gate = getattr(gates, gatename)(1, *params).controlled_by(0, 3)
+ assert gate.target_qubits == (1,)
+ assert gate.control_qubits == (0, 3)
+ assert gate.parameters == params
+
+
+def test_cnot_and_cy_and_cz_init():
+ gate = gates.CNOT(0, 1)
+ assert gate.target_qubits == (1,)
+ assert gate.control_qubits == (0,)
+ gate = gates.CY(4, 7)
+ assert gate.target_qubits == (7,)
+ assert gate.control_qubits == (4,)
+ gate = gates.CZ(3, 2)
+ assert gate.target_qubits == (2,)
+ assert gate.control_qubits == (3,)
+
+
+# :meth:`qibo.gates.CNOT.decompose` is tested in
+# ``test_x_decompose_with_cirq`` above
+
+
+@pytest.mark.parametrize(
+ "gatename,params",
+ [
+ ("CRX", (0.1234,)),
+ ("CRY", (0.1234,)),
+ ("CRZ", (0.1234,)),
+ ("CU1", (0.1234,)),
+ ("CU2", (0.1234, 0.4321)),
+ ("CU3", (0.1234, 0.4321, 0.5678)),
+ ],
+)
+def test_two_qubit_controlled_rotations_init(gatename, params):
+ gate = getattr(gates, gatename)(0, 2, *params)
+ assert gate.target_qubits == (2,)
+ assert gate.control_qubits == (0,)
+
+
+def test_swap_init():
+ gate = gates.SWAP(4, 3)
+ assert gate.target_qubits == (4, 3)
+
+
+def test_fsim_init():
+ import numpy as np
+
+ gate = gates.fSim(0, 1, 0.1234, 0.4321)
+ assert gate.target_qubits == (0, 1)
+ matrix = np.random.random((2, 2))
+ gate = gates.GeneralizedfSim(0, 1, matrix, 0.4321)
+ assert gate.target_qubits == (0, 1)
+ assert gate.parameters == (matrix, 0.4321)
+ matrix = np.random.random((3, 3))
+ with pytest.raises(ValueError):
+ gate = gates.GeneralizedfSim(0, 1, matrix, 0.4321)
+
+
+def test_toffoli_init():
+ gate = gates.TOFFOLI(0, 2, 1)
+ assert gate.target_qubits == (1,)
+ assert gate.control_qubits == (0, 2)
+
+
+# :meth:`qibo.gates.TOFFOLI.decompose` and
+# :meth:`qibo.gates.TOFFOLI.congruent`
+# are tested in `test_x_decompose_with_cirq`
+
+
+@pytest.mark.parametrize("targets", [(0,), (2, 0), (1, 3, 2)])
+def test_unitary_init(targets):
+ import numpy as np
+
+ matrix = np.random.random(2 * (2 ** len(targets),))
+ gate = gates.Unitary(matrix, *targets)
+ assert gate.target_qubits == targets
+ assert gate.nparams == 4 ** len(targets)
+
+
+def test_kraus_channel_init():
+ import numpy as np
+
+ qubits = [(0,), (0, 1), (0, 2), (3,)]
+ ops = [np.random.random((2 ** len(q), 2 ** len(q))) for q in qubits]
+ gate = gates.KrausChannel(qubits, ops)
+ gate.target_qubits == (0, 1, 2, 3)
+ for g in gate.gates:
+ assert isinstance(g, gates.Unitary)
+ qubits.append((4,))
+ ops.append(np.random.random((4, 4)))
+ with pytest.raises(ValueError):
+ gate = gates.KrausChannel(qubits, ops)
+
+
+def test_unitary_channel_init():
+ import numpy as np
+
+ qubits = [(0,), (0, 1), (0, 2), (3,)]
+ ops = [(0.1, np.random.random((2 ** len(q), 2 ** len(q)))) for q in qubits]
+ gate = gates.UnitaryChannel(qubits, ops)
+ gate.target_qubits == (0, 1, 2, 3)
+ for g in gate.gates:
+ assert isinstance(g, gates.Unitary)
+
+ with pytest.raises(ValueError):
+ gate = gates.UnitaryChannel(qubits[:2], ops)
+ with pytest.raises(ValueError):
+ ops[0] = (-0.1, np.random.random((2, 2)))
+ gate = gates.UnitaryChannel(qubits, ops)
+
+
+def test_pauli_noise_channel_init(backend):
+ gate = gates.PauliNoiseChannel(0, list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ assert gate.target_qubits == (0,)
+ for g, p in zip(gate.gates, [matrices.X, matrices.Y, matrices.Z]):
+ p = backend.cast(p, dtype=p.dtype)
+ backend.assert_allclose(g.matrix(backend), p, atol=PRECISION_TOL)
+
+
+def test_reset_channel_init():
+ gate = gates.ResetChannel(0, [0.1, 0.2])
+ assert gate.target_qubits == (0,)
+
+
+def test_qubit_getter_and_setter():
+ from qibo.gates.abstract import Gate
+
+ gate = Gate()
+ gate.target_qubits = (0, 3)
+ gate.control_qubits = (1, 4, 2)
+ assert gate.qubits == (1, 2, 4, 0, 3)
+
+ gate = Gate()
+ with pytest.raises(ValueError):
+ gate.target_qubits = (1, 1)
+ gate = Gate()
+ with pytest.raises(ValueError):
+ gate.control_qubits = (1, 1)
+ gate = Gate()
+ gate.target_qubits = (0, 1)
+ with pytest.raises(ValueError):
+ gate.control_qubits = (1,)
+
+
+def test_density_matrix_getter_and_setter():
+ from qibo.gates.abstract import Gate
+
+ gate = Gate()
+ gate.target_qubits = (0, 1)
+ gate.control_qubits = (2,)
+ gate.density_matrix = True
+
+
+def test_gates_commute():
+ assert gates.H(0).commutes(gates.X(1))
+ assert gates.H(0).commutes(gates.H(1))
+ assert gates.H(0).commutes(gates.H(0))
+ assert not gates.H(0).commutes(gates.Y(0))
+ assert not gates.CNOT(0, 1).commutes(gates.SWAP(1, 2))
+ assert not gates.CNOT(0, 1).commutes(gates.H(1))
+ assert not gates.CNOT(0, 1).commutes(gates.Y(0).controlled_by(2))
+ assert not gates.CNOT(2, 3).commutes(gates.CNOT(3, 0))
+ assert gates.CNOT(0, 1).commutes(gates.Y(2).controlled_by(0))
+
+
+def test_on_qubits():
+ gate = gates.CNOT(0, 1).on_qubits({0: 2, 1: 3})
+ assert gate.target_qubits == (3,)
+ assert gate.control_qubits == (2,)
+ assert isinstance(gate, gates.CNOT)
+
+
+def test_controlled_by():
+ gate = gates.RX(0, 0.1234).controlled_by(1, 2, 3)
+ assert gate.target_qubits == (0,)
+ assert gate.control_qubits == (1, 2, 3)
+ assert gate.is_controlled_by
+ assert isinstance(gate, gates.RX)
+ with pytest.raises(RuntimeError):
+ gate = gates.CNOT(0, 1).controlled_by(2)
+
+
+def test_on_qubits_controlled_by():
+ gate = gates.H(0).controlled_by(1, 2)
+ gate = gate.on_qubits({0: 5, 1: 4, 2: 6})
+ assert gate.target_qubits == (5,)
+ assert gate.control_qubits == (4, 6)
+ assert isinstance(gate, gates.H)
+ assert gate.is_controlled_by
+
+
+def test_decompose():
+ decomp_gates = gates.H(0).decompose(1)
+ assert len(decomp_gates) == 1
+ assert isinstance(decomp_gates[0], gates.H)
+
+
+def test_special_gate():
+ from qibo.gates.abstract import SpecialGate
+
+ gate = SpecialGate()
+ assert not gate.commutes(gates.H(0))
+ with pytest.raises(NotImplementedError):
+ gate.on_qubits({0: 0})
+
+
+def test_fused_gate():
+ gate = gates.FusedGate(0, 1)
+ gate.append(gates.H(0))
+ gate.append(gates.CNOT(0, 1))
+ assert len(gate.gates) == 2
+ gate.prepend(gates.TOFFOLI(0, 1, 2))
+ assert gate.qubits == (0, 1, 2)
+ assert len(gate.gates) == 3
+ assert isinstance(gate.gates[0], gates.TOFFOLI)
+
+
+def test_generator_eigenvalue():
+ gate = gates.H(0)
+ with pytest.raises(NotImplementedError):
+ gate.generator_eigenvalue()
+
+
+def test_gate_set_parameters():
+ gate = gates.RX(0, theta=0)
+ assert gate.parameters == (0,)
+ gate.parameters = 0.5
+ gate2 = gate.__class__(*gate.init_args, **gate.init_kwargs)
+ assert gate.parameters == (0.5,)
+ assert gate2.parameters == (0.5,)
+
+
+def test_generalizedfsim_set_parameters():
+ import numpy as np
+
+ gate = gates.GeneralizedfSim(0, 1, unitary=np.eye(2), phi=0)
+ np.testing.assert_allclose(gate.parameters[0], np.eye(2))
+ assert gate.parameters[1] == 0
+
+ new_unitary = np.random.random((2, 2))
+ gate.parameters = (new_unitary, 0.5)
+ gate2 = gate.__class__(*gate.init_args, **gate.init_kwargs)
+ np.testing.assert_allclose(gate.parameters[0], new_unitary)
+ np.testing.assert_allclose(gate2.parameters[0], new_unitary)
+ assert gate.parameters[1] == 0.5
+ assert gate2.parameters[1] == 0.5
diff --git a/tests/test_gates_channels.py b/tests/test_gates_channels.py
new file mode 100644
index 000000000..a1a797454
--- /dev/null
+++ b/tests/test_gates_channels.py
@@ -0,0 +1,462 @@
+"""Test channels defined in `qibo/gates.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info import (
+ partial_trace,
+ random_density_matrix,
+ random_statevector,
+ random_stochastic_matrix,
+)
+
+
+def test_general_channel(backend):
+ """"""
+ a_1 = backend.cast(np.sqrt(0.4) * matrices.X)
+ a_2 = backend.cast(
+ np.sqrt(0.6)
+ * np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ )
+ initial_state = random_density_matrix(2**2, backend=backend)
+ m_1 = backend.np.kron(backend.identity_density_matrix(1, normalize=False), a_1)
+ m_1 = backend.cast(m_1, dtype=m_1.dtype)
+ m_2 = backend.cast(a_2, dtype=a_2.dtype)
+ target_state = backend.np.matmul(
+ backend.np.matmul(m_1, initial_state),
+ backend.np.transpose(backend.np.conj(m_1), (1, 0)),
+ )
+ target_state = target_state + backend.np.matmul(
+ backend.np.matmul(m_2, initial_state),
+ backend.np.transpose(backend.np.conj(m_2), (1, 0)),
+ )
+
+ channel1 = gates.KrausChannel([(1,), (0, 1)], [a_1, a_2])
+ assert channel1.target_qubits == (0, 1)
+ final_state = backend.apply_channel_density_matrix(
+ channel1, backend.np.copy(initial_state), 2
+ )
+ backend.assert_allclose(final_state, target_state)
+
+ a_1 = gates.Unitary(a_1, 1)
+ a_2 = gates.Unitary(a_2, 0, 1)
+ channel2 = gates.KrausChannel([(1,), (0, 1)], [a_1, a_2])
+ assert channel2.target_qubits == (0, 1)
+ final_state = backend.apply_channel_density_matrix(
+ channel2, backend.np.copy(initial_state), 2
+ )
+ backend.assert_allclose(final_state, target_state)
+
+ with pytest.raises(NotImplementedError):
+ channel1.on_qubits({})
+ with pytest.raises(NotImplementedError):
+ state = random_statevector(2**2, backend=backend)
+ channel1.apply(backend, state, 2)
+ with pytest.raises(NotImplementedError):
+ channel1.matrix(backend)
+
+
+def test_controlled_by_channel_error():
+ """"""
+ with pytest.raises(ValueError):
+ gates.PauliNoiseChannel(0, [("X", 0.5)]).controlled_by(1)
+
+ a_1 = np.sqrt(0.4) * matrices.X
+ a_2 = np.sqrt(0.6) * np.array(
+ [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]
+ )
+ config = ([(1,), (0, 1)], [a_1, a_2])
+ with pytest.raises(ValueError):
+ gates.KrausChannel(*config).controlled_by(1)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+def test_kraus_channel(backend, pauli_order):
+ """"""
+ a_1 = np.sqrt(0.4) * matrices.X
+ a_2 = np.sqrt(0.6) * matrices.Z
+
+ with pytest.raises(TypeError):
+ gates.KrausChannel("0", [a_1])
+ with pytest.raises(TypeError):
+ gates.KrausChannel([0, 1], [a_1, a_2])
+ with pytest.raises(ValueError):
+ gates.KrausChannel((0, 1), [a_1])
+
+ test_superop = np.array(
+ [
+ [0.6 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.4 + 0.0j],
+ [0.0 + 0.0j, -0.6 + 0.0j, 0.4 + 0.0j, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.4 + 0.0j, -0.6 + 0.0j, 0.0 + 0.0j],
+ [0.4 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.6 + 0.0j],
+ ]
+ )
+ test_choi = backend.cast(
+ np.reshape(test_superop, [2] * 4).swapaxes(0, 3).reshape([4, 4])
+ )
+
+ pauli_elements = {"I": 2.0, "X": -0.4, "Y": -2.0, "Z": 0.4}
+ test_pauli = backend.cast(np.diag([pauli_elements[p] for p in pauli_order]))
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+ test_choi = backend.cast(test_choi, dtype=test_choi.dtype)
+ test_pauli = backend.cast(test_pauli, dtype=test_pauli.dtype)
+
+ channel = gates.KrausChannel(0, [a_1, a_2])
+
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ channel.to_liouville(backend=backend) - test_superop, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ channel.to_choi(backend=backend) - test_choi, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm(
+ channel.to_pauli_liouville(pauli_order=pauli_order, backend=backend)
+ - test_pauli
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+
+ gates.DepolarizingChannel((0, 1), 0.98).to_choi()
+
+
+def test_unitary_channel(backend):
+ """"""
+ a_1 = backend.cast(matrices.X)
+ a_2 = backend.cast(
+ np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ )
+
+ qubits = [(0,), (2, 3)]
+ probabilities = [0.4, 0.3]
+ matrices_ = list(zip(probabilities, [a_1, a_2]))
+
+ channel = gates.UnitaryChannel(qubits, matrices_)
+ initial_state = random_density_matrix(2**4, backend=backend)
+ final_state = backend.apply_channel_density_matrix(
+ channel, backend.np.copy(initial_state), 4
+ )
+
+ eye = backend.identity_density_matrix(1, normalize=False)
+ ma_1 = backend.np.kron(backend.np.kron(a_1, eye), backend.np.kron(eye, eye))
+ ma_2 = backend.np.kron(backend.np.kron(eye, eye), a_2)
+ ma_1 = backend.cast(ma_1, dtype=ma_1.dtype)
+ ma_2 = backend.cast(ma_2, dtype=ma_2.dtype)
+ target_state = (
+ 0.3 * initial_state
+ + 0.4 * backend.np.matmul(ma_1, backend.np.matmul(initial_state, ma_1))
+ + 0.3 * backend.np.matmul(ma_2, backend.np.matmul(initial_state, ma_2))
+ )
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_unitary_channel_probability_tolerance(backend):
+ """Create ``UnitaryChannel`` with probability sum within tolerance (see #562)."""
+ nqubits = 2
+ param = 0.006
+ num_terms = 2 ** (2 * nqubits)
+ max_param = num_terms / (num_terms - 1)
+ prob_identity = 1 - param / max_param
+ prob_pauli = param / num_terms
+ qubits = (0, 1)
+ probs = [prob_identity] + [prob_pauli] * (num_terms - 1)
+ probs = np.array(probs, dtype="float64")
+ matrices_ = [(p, np.random.random((4, 4))) for p in probs]
+ gates.UnitaryChannel(qubits, matrices_)
+
+ probs = np.zeros_like(probs)
+ matrices_ = [(p, np.random.random((4, 4))) for p in probs]
+ identity_channel = gates.UnitaryChannel(qubits, matrices_)
+ backend.assert_allclose(
+ identity_channel.to_liouville(backend=backend), backend.np.eye(num_terms)
+ )
+
+
+def test_unitary_channel_errors():
+ """Check errors raised by ``gates.UnitaryChannel``."""
+ a_1 = matrices.X
+ a_2 = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+
+ qubits = [(0,), (2, 3)]
+ # Invalid ops
+ with pytest.raises(TypeError):
+ gates.UnitaryChannel(qubits, [a_1, (0.1, a_2)])
+ # Invalid qubit length
+ with pytest.raises(ValueError):
+ gates.UnitaryChannel(qubits + [(0,)], [(0.4, a_1), (0.3, a_2)])
+ # Probability > 1
+ with pytest.raises(ValueError):
+ gates.UnitaryChannel(qubits, [(0.4, a_1), (1.1, a_2)])
+ # Probability sum > 1
+ with pytest.raises(ValueError):
+ gates.UnitaryChannel(qubits, [(0.5, a_1), (0.6, a_2)])
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+def test_pauli_noise_channel(backend, pauli_order):
+ """"""
+ initial_state = random_density_matrix(2**2, backend=backend)
+ qubits = (1,)
+ channel = gates.PauliNoiseChannel(qubits, [("X", 0.3)])
+ final_state = backend.apply_channel_density_matrix(
+ channel, backend.np.copy(initial_state), 2
+ )
+ gate = gates.X(1)
+ target_state = backend.apply_gate_density_matrix(
+ gate, backend.np.copy(initial_state), 2
+ )
+ target_state = 0.3 * target_state + 0.7 * initial_state
+ backend.assert_allclose(final_state, target_state)
+
+ basis = ["X", "Y", "Z"]
+ pnp = np.array([0.1, 0.02, 0.05])
+ noise_elements = {
+ "I": 1,
+ "X": 1 - 2 * pnp[1] - 2 * pnp[2],
+ "Y": 1 - 2 * pnp[0] - 2 * pnp[2],
+ "Z": 1 - 2 * pnp[0] - 2 * pnp[1],
+ }
+ test_representation = np.diag([noise_elements[p] for p in pauli_order])
+
+ liouville = gates.PauliNoiseChannel(0, list(zip(basis, pnp))).to_pauli_liouville(
+ normalize=True, pauli_order=pauli_order, backend=backend
+ )
+ norm = float(
+ backend.calculate_norm_density_matrix(
+ backend.to_numpy(liouville) - test_representation, order=2
+ )
+ )
+
+ assert norm < PRECISION_TOL
+
+
+def test_depolarizing_channel_errors():
+ """"""
+ with pytest.raises(ValueError):
+ gates.DepolarizingChannel(0, 1.5)
+ with pytest.raises(ValueError):
+ gates.DepolarizingChannel([0, 1], 0.1).to_choi(nqubits=1)
+
+
+def test_depolarizing_channel(backend):
+ """"""
+ lam = 0.3
+ initial_state = random_density_matrix(2**3, backend=backend)
+ initial_state_r = partial_trace(initial_state, (2,), backend=backend)
+ channel = gates.DepolarizingChannel((0, 1), lam)
+ final_state = channel.apply_density_matrix(
+ backend, backend.np.copy(initial_state), 3
+ )
+ final_state_r = partial_trace(final_state, (2,), backend=backend)
+ target_state_r = (1 - lam) * initial_state_r + lam * backend.cast(
+ np.identity(4)
+ ) / 4
+ backend.assert_allclose(final_state_r, target_state_r)
+
+
+def test_amplitude_damping_channel(backend):
+ """"""
+ with pytest.raises(TypeError):
+ gates.AmplitudeDampingChannel(0, "0.1")
+ with pytest.raises(ValueError):
+ gates.AmplitudeDampingChannel(0, 1.1)
+
+ gamma = np.random.rand()
+ kraus_0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex)
+ kraus_1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex)
+ kraus_0 = backend.cast(kraus_0, dtype=kraus_0.dtype)
+ kraus_1 = backend.cast(kraus_1, dtype=kraus_1.dtype)
+
+ channel = gates.AmplitudeDampingChannel(0, gamma)
+
+ initial_state = random_density_matrix(2**1, backend=backend)
+ final_state = channel.apply_density_matrix(
+ backend, backend.np.copy(initial_state), 1
+ )
+ target_state = kraus_0 @ initial_state @ backend.np.transpose(
+ backend.np.conj(kraus_0), (1, 0)
+ ) + kraus_1 @ initial_state @ backend.np.transpose(backend.np.conj(kraus_1), (1, 0))
+
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_phase_damping_channel(backend):
+ """"""
+ with pytest.raises(TypeError):
+ gates.PhaseDampingChannel(0, "0.1")
+ with pytest.raises(ValueError):
+ gates.PhaseDampingChannel(0, 1.1)
+
+ gamma = np.random.rand()
+ kraus_0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex)
+ kraus_1 = np.array([[0, 0], [0, np.sqrt(gamma)]], dtype=complex)
+ kraus_0 = backend.cast(kraus_0, dtype=kraus_0.dtype)
+ kraus_1 = backend.cast(kraus_1, dtype=kraus_1.dtype)
+
+ channel = gates.PhaseDampingChannel(0, gamma)
+
+ initial_state = random_density_matrix(2**1, backend=backend)
+ final_state = channel.apply_density_matrix(
+ backend, backend.np.copy(initial_state), 1
+ )
+ target_state = kraus_0 @ initial_state @ backend.np.transpose(
+ backend.np.conj(kraus_0), (1, 0)
+ ) + kraus_1 @ initial_state @ backend.np.transpose(backend.np.conj(kraus_1), (1, 0))
+
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.parametrize(
+ "t_1,t_2,time,excpop", [(0.8, 0.5, 1.0, 0.4), (0.5, 0.8, 1.0, 0.4)]
+)
+def test_thermal_relaxation_channel(backend, t_1, t_2, time, excpop):
+ """Check ``gates.ThermalRelaxationChannel`` on a 3-qubit random density matrix."""
+ initial_state = random_density_matrix(2**3, backend=backend)
+ gate = gates.ThermalRelaxationChannel(0, [t_1, t_2, time, excpop])
+ final_state = gate.apply_density_matrix(backend, backend.np.copy(initial_state), 3)
+
+ if t_2 > t_1:
+ p_0, p_1, exp = (
+ gate.init_kwargs["p_0"],
+ gate.init_kwargs["p_1"],
+ gate.init_kwargs["e_t2"],
+ )
+ matrix = np.diag([1 - p_1, p_1, p_0, 1 - p_0])
+ matrix[0, -1], matrix[-1, 0] = exp, exp
+ matrix = matrix.reshape(4 * (2,))
+ # Apply matrix using Eq. (3.28) from arXiv:1111.6950
+ target_state = backend.np.copy(initial_state).reshape(6 * (2,))
+ target_state = np.einsum(
+ "abcd,aJKcjk->bJKdjk", matrix, backend.to_numpy(target_state)
+ )
+ target_state = target_state.reshape(initial_state.shape)
+ else:
+ p_0, p_1, p_z = (
+ gate.init_kwargs["p_0"],
+ gate.init_kwargs["p_1"],
+ gate.init_kwargs["p_z"],
+ )
+ m_z = backend.np.kron(
+ backend.cast(matrices.Z),
+ backend.np.kron(backend.cast(matrices.I), backend.cast(matrices.I)),
+ )
+ m_z = backend.cast(m_z, dtype=m_z.dtype)
+ z_rho = m_z @ initial_state @ m_z
+
+ trace = backend.to_numpy(partial_trace(initial_state, (0,), backend=backend))
+ trace = np.reshape(trace, 4 * (2,))
+ zeros = np.tensordot(
+ trace, np.array([[1, 0], [0, 0]], dtype=trace.dtype), axes=0
+ )
+ ones = np.tensordot(
+ trace, np.array([[0, 0], [0, 1]], dtype=trace.dtype), axes=0
+ )
+ zeros = np.transpose(zeros, [4, 0, 1, 5, 2, 3])
+ ones = np.transpose(ones, [4, 0, 1, 5, 2, 3])
+
+ zeros = backend.cast(zeros, dtype=zeros.dtype)
+ ones = backend.cast(ones, dtype=ones.dtype)
+
+ target_state = (1 - p_0 - p_1 - p_z) * initial_state + p_z * z_rho
+ target_state += backend.np.reshape(
+ p_0 * zeros + p_1 * ones, initial_state.shape
+ )
+
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(final_state - target_state, order=2)
+ )
+ < PRECISION_TOL,
+ True,
+ )
+
+
+@pytest.mark.parametrize(
+ "params",
+ [
+ [0.5],
+ [0.5, 0.5, 0.5, 0.5, 0.5],
+ [1.0, 0.5, 1.5, 1.5],
+ [1.0, 0.5, -0.5, 0.5],
+ [1.0, -0.5, 1.5, 0.5],
+ [-1.0, 0.5, 1.5, 0.5],
+ [1.0, 3.0, 1.5, 0.5],
+ ],
+)
+def test_thermal_relaxation_channel_errors(params):
+ """"""
+ with pytest.raises(ValueError):
+ gates.ThermalRelaxationChannel(0, params)
+
+
+def test_readout_error_channel(backend):
+ """"""
+ with pytest.raises(ValueError):
+ gates.ReadoutErrorChannel(0, np.array([[1.1, 0], [0.5, 0.5]]))
+
+ nqubits = 1
+ dim = 2**nqubits
+
+ rho = random_density_matrix(dim, seed=1, backend=backend)
+ stochastic_noise = random_stochastic_matrix(dim, seed=1, backend=backend)
+
+ probability_sum = gates.ReadoutErrorChannel(
+ 0, stochastic_noise
+ ).apply_density_matrix(backend, rho, 1)
+ probability_sum = np.diag(backend.to_numpy(probability_sum)).sum().real
+
+ backend.assert_allclose(probability_sum - 1 < PRECISION_TOL, True)
+
+
+def test_reset_channel(backend):
+ """"""
+ initial_state = random_density_matrix(2**3, backend=backend)
+ gate = gates.ResetChannel(0, [0.2, 0.2])
+ final_state = backend.reset_error_density_matrix(
+ gate, backend.np.copy(initial_state), 3
+ )
+
+ trace = backend.to_numpy(partial_trace(initial_state, (0,), backend=backend))
+ trace = np.reshape(trace, 4 * (2,))
+
+ zeros = np.tensordot(trace, np.array([[1, 0], [0, 0]], dtype=trace.dtype), axes=0)
+ ones = np.tensordot(trace, np.array([[0, 0], [0, 1]], dtype=trace.dtype), axes=0)
+ zeros = np.transpose(zeros, [4, 0, 1, 5, 2, 3])
+ ones = np.transpose(ones, [4, 0, 1, 5, 2, 3])
+ zeros = backend.cast(zeros, dtype=zeros.dtype)
+ ones = backend.cast(ones, dtype=ones.dtype)
+
+ target_state = 0.6 * initial_state + 0.2 * backend.np.reshape(
+ zeros + ones, initial_state.shape
+ )
+
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.parametrize("p_0,p_1", [(0, -0.1), (-0.1, 0), (0.5, 0.6), (0.8, 0.3)])
+def test_reset_channel_errors(p_0, p_1):
+ """"""
+ with pytest.raises(ValueError):
+ gates.ResetChannel(0, [p_0])
+ with pytest.raises(ValueError):
+ gates.ResetChannel(0, [p_0, p_1])
diff --git a/tests/test_gates_density_matrix.py b/tests/test_gates_density_matrix.py
new file mode 100644
index 000000000..ec4fde3e2
--- /dev/null
+++ b/tests/test_gates_density_matrix.py
@@ -0,0 +1,235 @@
+"""Test gates acting on density matrices."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info import random_density_matrix, random_statevector
+
+
+def apply_gates(backend, gatelist, nqubits=None, initial_state=None):
+ state = backend.cast(backend.np.copy(initial_state))
+ for gate in gatelist:
+ state = backend.apply_gate_density_matrix(gate, state, nqubits)
+ return backend.to_numpy(state)
+
+
+def test_hgate_density_matrix(backend):
+ nqubits = 2
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = gates.H(1)
+ final_rho = apply_gates(backend, [gate], 2, initial_rho)
+
+ matrix = np.array([[1 + 0j, 1], [1 + 0j, -1]]) / np.sqrt(2)
+ matrix = np.kron(np.eye(2), matrix)
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ target_rho = backend.np.matmul(backend.np.matmul(matrix, initial_rho), matrix)
+ backend.assert_allclose(final_rho, target_rho)
+
+
+def test_rygate_density_matrix(backend):
+ theta = 0.1234
+ nqubits = 1
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = gates.RY(0, theta=theta)
+ final_rho = apply_gates(backend, [gate], 1, initial_rho)
+
+ phase = np.exp(1j * theta / 2.0)
+ matrix = phase * np.array([[phase.real, -phase.imag], [phase.imag, phase.real]])
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ target_rho = backend.np.matmul(
+ backend.np.matmul(matrix, initial_rho), backend.np.conj(matrix).T
+ )
+
+ backend.assert_allclose(final_rho, target_rho, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize(
+ "gatename,gatekwargs",
+ [
+ ("H", {}),
+ ("X", {}),
+ ("Y", {}),
+ ("Z", {}),
+ ("S", {}),
+ ("SDG", {}),
+ ("T", {}),
+ ("TDG", {}),
+ ("I", {}),
+ ("Align", {}),
+ ("RX", {"theta": 0.123}),
+ ("RY", {"theta": 0.123}),
+ ("RZ", {"theta": 0.123}),
+ ("U1", {"theta": 0.123}),
+ ("U2", {"phi": 0.123, "lam": 0.321}),
+ ("U3", {"theta": 0.123, "phi": 0.321, "lam": 0.123}),
+ ],
+)
+def test_one_qubit_gates(backend, gatename, gatekwargs):
+ """Check applying one qubit gates to one qubit density matrix."""
+ nqubits = 1
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = getattr(gates, gatename)(0, **gatekwargs)
+ final_rho = apply_gates(backend, [gate], 1, initial_rho)
+
+ matrix = backend.to_numpy(gate.matrix(backend))
+ target_rho = np.einsum(
+ "ab,bc,cd->ad", matrix, backend.to_numpy(initial_rho), matrix.conj().T
+ )
+ backend.assert_allclose(final_rho, target_rho)
+
+
+@pytest.mark.parametrize("gatename", ["H", "X", "Y", "Z", "S", "SDG", "T", "TDG"])
+def test_controlled_by_one_qubit_gates(backend, gatename):
+ nqubits = 2
+ initial_rho = random_density_matrix(2**nqubits, seed=1, backend=backend)
+ gate = getattr(gates, gatename)(1).controlled_by(0)
+ final_rho = apply_gates(backend, [gate], 2, backend.np.copy(initial_rho))
+
+ matrix = backend.to_numpy(backend.matrix(getattr(gates, gatename)(1)))
+ cmatrix = np.eye(4, dtype=matrix.dtype)
+ cmatrix[2:, 2:] = matrix
+ target_rho = np.einsum(
+ "ab,bc,cd->ad",
+ cmatrix,
+ backend.to_numpy(initial_rho),
+ np.transpose(np.conj(cmatrix)),
+ )
+ backend.assert_allclose(final_rho, target_rho)
+
+
+@pytest.mark.parametrize(
+ "gatename,gatekwargs",
+ [
+ ("CNOT", {}),
+ ("CY", {}),
+ ("CZ", {}),
+ ("SWAP", {}),
+ ("CRX", {"theta": 0.123}),
+ ("CRY", {"theta": 0.123}),
+ ("CRZ", {"theta": 0.123}),
+ ("CU1", {"theta": 0.123}),
+ ("CU2", {"phi": 0.123, "lam": 0.321}),
+ ("CU3", {"theta": 0.123, "phi": 0.321, "lam": 0.123}),
+ ("fSim", {"theta": 0.123, "phi": 0.543}),
+ ],
+)
+def test_two_qubit_gates(backend, gatename, gatekwargs):
+ """Check applying two qubit gates to two qubit density matrix."""
+ nqubits = 2
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = getattr(gates, gatename)(0, 1, **gatekwargs)
+ final_rho = apply_gates(backend, [gate], 2, initial_rho)
+
+ matrix = backend.to_numpy(gate.matrix(backend))
+ target_rho = np.einsum(
+ "ab,bc,cd->ad", matrix, backend.to_numpy(initial_rho), np.conj(matrix).T
+ )
+ backend.assert_allclose(final_rho, target_rho, atol=PRECISION_TOL)
+
+
+def test_toffoli_gate(backend):
+ """Check applying Toffoli to three qubit density matrix."""
+ nqubits = 3
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = gates.TOFFOLI(0, 1, 2)
+ final_rho = apply_gates(backend, [gate], 3, initial_rho)
+
+ matrix = backend.to_numpy(gate.matrix(backend))
+ target_rho = np.einsum(
+ "ab,bc,cd->ad", matrix, backend.to_numpy(initial_rho), np.conj(matrix).T
+ )
+ backend.assert_allclose(final_rho, target_rho)
+
+
+@pytest.mark.parametrize("nqubits", [1, 2, 3])
+def test_unitary_gate(backend, nqubits):
+ """Check applying `gates.Unitary` to density matrix."""
+ shape = 2 * (2**nqubits,)
+ matrix = np.random.random(shape) + 1j * np.random.random(shape)
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = gates.Unitary(matrix, *range(nqubits))
+ final_rho = apply_gates(backend, [gate], nqubits, initial_rho)
+ target_rho = np.einsum(
+ "ab,bc,cd->ad", matrix, backend.to_numpy(initial_rho), np.conj(matrix).T
+ )
+ backend.assert_allclose(final_rho, target_rho)
+
+
+def test_cu1gate_application_twoqubit(backend):
+ """Check applying two qubit gate to three qubit density matrix."""
+ theta = 0.1234
+ nqubits = 3
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ gate = gates.CU1(0, 1, theta=theta)
+ final_rho = apply_gates(backend, [gate], nqubits, initial_rho)
+
+ matrix = np.eye(4, dtype=np.complex128)
+ matrix[3, 3] = np.exp(1j * theta)
+ matrix = np.kron(matrix, np.eye(2))
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ target_rho = backend.np.matmul(
+ backend.np.matmul(matrix, initial_rho), backend.np.conj(matrix).T
+ )
+ backend.assert_allclose(final_rho, target_rho)
+
+
+def test_controlled_by_no_effect(backend):
+ """Check controlled_by SWAP that should not be applied."""
+
+ initial_rho = np.zeros((16, 16))
+ initial_rho[0, 0] = 1
+
+ c = Circuit(4, density_matrix=True)
+ c.add(gates.X(0))
+ c.add(gates.SWAP(1, 3).controlled_by(0, 2))
+ final_rho = backend.execute_circuit(c, np.copy(initial_rho)).state()
+
+ c = Circuit(4, density_matrix=True)
+ c.add(gates.X(0))
+ target_rho = backend.execute_circuit(c, np.copy(initial_rho)).state()
+ backend.assert_allclose(final_rho, target_rho)
+
+
+def test_controlled_with_effect(backend):
+ """Check controlled_by SWAP that should be applied."""
+
+ initial_rho = np.zeros((16, 16))
+ initial_rho[0, 0] = 1
+
+ c = Circuit(4, density_matrix=True)
+ c.add(gates.X(0))
+ c.add(gates.X(2))
+ c.add(gates.SWAP(1, 3).controlled_by(0, 2))
+ final_rho = backend.execute_circuit(
+ c, backend.np.copy(backend.cast(initial_rho))
+ ).state()
+
+ c = Circuit(4, density_matrix=True)
+ c.add(gates.X(0))
+ c.add(gates.X(2))
+ c.add(gates.SWAP(1, 3))
+ target_rho = backend.execute_circuit(
+ c, backend.np.copy(backend.cast(initial_rho))
+ ).state()
+ backend.assert_allclose(final_rho, target_rho)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+def test_controlled_by_random(backend, nqubits):
+ """Check controlled_by method on gate."""
+
+ initial_psi = random_statevector(2**nqubits, backend=backend)
+ initial_rho = backend.np.outer(initial_psi, backend.np.conj(initial_psi))
+ c = Circuit(nqubits, density_matrix=True)
+ c.add(gates.RX(1, theta=0.789).controlled_by(2))
+ c.add(gates.fSim(0, 2, theta=0.123, phi=0.321).controlled_by(1, 3))
+ final_rho = backend.execute_circuit(c, backend.np.copy(initial_rho)).state()
+
+ c = Circuit(nqubits)
+ c.add(gates.RX(1, theta=0.789).controlled_by(2))
+ c.add(gates.fSim(0, 2, theta=0.123, phi=0.321).controlled_by(1, 3))
+ target_psi = backend.execute_circuit(c, backend.np.copy(initial_psi)).state()
+ target_rho = backend.np.outer(target_psi, backend.np.conj(target_psi))
+ backend.assert_allclose(final_rho, target_rho)
diff --git a/tests/test_gates_gates.py b/tests/test_gates_gates.py
new file mode 100644
index 000000000..27028e4a4
--- /dev/null
+++ b/tests/test_gates_gates.py
@@ -0,0 +1,1768 @@
+"""Test gates defined in `qibo/gates/gates.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.parameter import Parameter
+from qibo.quantum_info import random_hermitian, random_statevector, random_unitary
+from qibo.transpiler.decompositions import standard_decompositions
+
+
+def apply_gates(backend, gatelist, nqubits=None, initial_state=None):
+ if initial_state is None:
+ state = backend.zero_state(nqubits)
+ else:
+ state = backend.cast(initial_state, dtype=initial_state.dtype, copy=True)
+ if nqubits is None:
+ nqubits = int(np.log2(len(state)))
+ else: # pragma: no cover
+ assert nqubits == int(np.log2(len(state)))
+
+ for gate in gatelist:
+ state = backend.apply_gate(gate, state, nqubits)
+
+ return state
+
+
+def test_h(backend):
+ final_state = apply_gates(backend, [gates.H(0), gates.H(1)], nqubits=2)
+ target_state = np.ones_like(backend.to_numpy(final_state)) / 2
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.H(0).qasm_label == "h"
+ assert gates.H(0).clifford
+ assert gates.H(0).unitary
+
+
+def test_x(backend):
+ final_state = apply_gates(backend, [gates.X(0)], nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[2] = 1.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.X(0).qasm_label == "x"
+ assert gates.X(0).clifford
+ assert gates.X(0).unitary
+
+
+def test_y(backend):
+ final_state = apply_gates(backend, [gates.Y(1)], nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[1] = 1j
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.Y(0).qasm_label == "y"
+ assert gates.Y(0).clifford
+ assert gates.Y(0).unitary
+
+
+def test_z(backend):
+ final_state = apply_gates(backend, [gates.H(0), gates.H(1), gates.Z(0)], nqubits=2)
+ target_state = np.ones_like(backend.to_numpy(final_state)) / 2.0
+ target_state[2] *= -1.0
+ target_state[3] *= -1.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.Z(0).qasm_label == "z"
+ assert gates.Z(0).clifford
+ assert gates.Z(0).unitary
+
+
+def test_sx(backend):
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.SX(0)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.SX(0).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]]) / 2.0
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, backend=backend)
+ np_final_state_decompose = backend.to_numpy(final_state_decompose)
+ np_obs = backend.to_numpy(observable)
+ np_target_state = backend.to_numpy(target_state)
+ backend.assert_allclose(
+ np.conj(np_final_state_decompose).T @ np_obs @ np_final_state_decompose,
+ np.conj(np_target_state).T @ np_obs @ np_target_state,
+ atol=1e-6,
+ )
+
+ assert gates.SX(0).qasm_label == "sx"
+ assert gates.SX(0).clifford
+ assert gates.SX(0).unitary
+
+
+def test_sxdg(backend):
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.SXDG(0)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.SXDG(0).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array([[1 - 1j, 1 + 1j], [1 + 1j, 1 - 1j]]) / 2
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, backend=backend)
+ np_final_state_decompose = backend.to_numpy(final_state_decompose)
+ np_obs = backend.to_numpy(observable)
+ np_target_state = backend.to_numpy(target_state)
+ backend.assert_allclose(
+ np.conj(np_final_state_decompose).T @ np_obs @ np_final_state_decompose,
+ np.conj(np_target_state).T @ np_obs @ np_target_state,
+ atol=1e-6,
+ )
+
+ assert gates.SXDG(0).qasm_label == "sxdg"
+ assert gates.SXDG(0).clifford
+ assert gates.SXDG(0).unitary
+
+
+def test_s(backend):
+ final_state = apply_gates(backend, [gates.H(0), gates.H(1), gates.S(1)], nqubits=2)
+ target_state = np.array([0.5, 0.5j, 0.5, 0.5j])
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.S(0).qasm_label == "s"
+ assert gates.S(0).clifford
+ assert gates.S(0).unitary
+
+
+def test_sdg(backend):
+ final_state = apply_gates(
+ backend, [gates.H(0), gates.H(1), gates.SDG(1)], nqubits=2
+ )
+ target_state = np.array([0.5, -0.5j, 0.5, -0.5j])
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.SDG(0).qasm_label == "sdg"
+ assert gates.SDG(0).clifford
+ assert gates.SDG(0).unitary
+
+
+def test_t(backend):
+ final_state = apply_gates(backend, [gates.H(0), gates.H(1), gates.T(1)], nqubits=2)
+ target_state = np.array([0.5, (1 + 1j) / np.sqrt(8), 0.5, (1 + 1j) / np.sqrt(8)])
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.T(0).qasm_label == "t"
+ assert not gates.T(0).clifford
+ assert gates.T(0).unitary
+
+
+def test_tdg(backend):
+ final_state = apply_gates(
+ backend, [gates.H(0), gates.H(1), gates.TDG(1)], nqubits=2
+ )
+ target_state = np.array([0.5, (1 - 1j) / np.sqrt(8), 0.5, (1 - 1j) / np.sqrt(8)])
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.TDG(0).qasm_label == "tdg"
+ assert not gates.TDG(0).clifford
+ assert gates.TDG(0).unitary
+
+
+def test_identity(backend):
+ gatelist = [gates.H(0), gates.H(1), gates.I(0), gates.I(1)]
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ target_state = np.ones_like(backend.to_numpy(final_state)) / 2.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ gatelist = [gates.H(0), gates.H(1), gates.I(0, 1)]
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.I(0).qasm_label == "id"
+ assert gates.I(0).clifford
+ assert gates.I(0).unitary
+
+
+def test_align(backend):
+ with pytest.raises(TypeError):
+ gates.Align(0, delay="0.1")
+ with pytest.raises(ValueError):
+ gates.Align(0, delay=-1)
+
+ nqubits = 1
+
+ gate = gates.Align(0, 0)
+ gate_list = [gates.H(0), gate]
+
+ final_state = apply_gates(backend, gate_list, nqubits=nqubits)
+ target_state = backend.plus_state(nqubits)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ gate_matrix = gate.matrix(backend)
+ identity = backend.identity_density_matrix(nqubits, normalize=False)
+ backend.assert_allclose(gate_matrix, identity, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gate.qasm_label
+
+ assert not gates.Align(0, delay=0).clifford
+ assert not gates.Align(0, delay=0).unitary
+
+
+# :class:`qibo.core.cgates.M` is tested seperately in `test_measurement_gate.py`
+
+
+@pytest.mark.parametrize("theta", [np.random.rand(), np.pi / 2.0, -np.pi / 2.0, np.pi])
+def test_rx(backend, theta):
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.RX(0, theta=theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ phase = np.exp(1j * theta / 2.0)
+ gate = np.array([[phase.real, -1j * phase.imag], [-1j * phase.imag, phase.real]])
+ gate = backend.cast(gate)
+ target_state = gate @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RX(0, theta=theta).qasm_label == "rx"
+ assert gates.RX(0, theta=theta).unitary
+ if (theta % (np.pi / 2.0)).is_integer():
+ assert gates.RX(0, theta=theta).clifford
+ else:
+ assert not gates.RX(0, theta=theta).clifford
+
+ # test Parameter
+ assert (
+ gates.RX(
+ 0,
+ theta=Parameter(
+ lambda x, th1: 10 * th1 + x, trainable=[0.2], features=[40]
+ ),
+ ).init_kwargs["theta"]
+ == 42
+ )
+
+
+@pytest.mark.parametrize("theta", [np.random.rand(), np.pi / 2.0, -np.pi / 2.0, np.pi])
+def test_ry(backend, theta):
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.RY(0, theta=theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ phase = np.exp(1j * theta / 2.0)
+ gate = np.array([[phase.real, -phase.imag], [phase.imag, phase.real]])
+ gate = backend.cast(gate)
+ target_state = gate @ backend.cast(initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RY(0, theta=theta).qasm_label == "ry"
+ assert gates.RY(0, theta=theta).unitary
+ if (theta % (np.pi / 2.0)).is_integer():
+ assert gates.RY(0, theta=theta).clifford
+ else:
+ assert not gates.RY(0, theta=theta).clifford
+
+
+@pytest.mark.parametrize("apply_x", [True, False])
+@pytest.mark.parametrize("theta", [np.random.rand(), np.pi / 2.0, -np.pi / 2.0, np.pi])
+def test_rz(backend, theta, apply_x):
+ nqubits = 1
+ if apply_x:
+ gatelist = [gates.X(0)]
+ else:
+ gatelist = []
+ gatelist.append(gates.RZ(0, theta))
+ final_state = apply_gates(backend, gatelist, nqubits=nqubits)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ p = int(apply_x)
+ target_state[p] = np.exp((2 * p - 1) * 1j * theta / 2.0)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RZ(0, theta).qasm_label == "rz"
+ assert gates.RZ(0, theta=theta).unitary
+ if (theta % (np.pi / 2)).is_integer():
+ assert gates.RZ(0, theta=theta).clifford
+ else:
+ assert not gates.RZ(0, theta=theta).clifford
+
+
+def test_prx(backend):
+ theta = 0.52
+ phi = 0.24
+
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.PRX(0, theta, phi)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.PRX(0, theta, phi).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ cos = np.cos(theta / 2)
+ sin = np.sin(theta / 2)
+ exponent1 = -1.0j * np.exp(-1.0j * phi)
+ exponent2 = -1.0j * np.exp(1.0j * phi)
+ matrix = np.array([[cos, exponent1 * sin], [exponent2 * sin, cos]])
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state)
+ backend.assert_allclose(final_state_decompose, target_state)
+
+ assert gates.PRX(0, theta, phi).qasm_label == "prx"
+ assert not gates.PRX(0, theta, phi).clifford
+ assert gates.PRX(0, theta, phi).unitary
+
+
+def test_gpi(backend):
+ phi = 0.1234
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gates.GPI(0, phi)], initial_state=initial_state)
+
+ phase = np.exp(1.0j * phi)
+ matrix = np.array([[0, np.conj(phase)], [phase, 0]])
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.GPI(0, phi).qasm_label == "gpi"
+ assert not gates.GPI(0, phi).clifford
+ assert gates.GPI(0, phi).unitary
+
+
+def test_gpi2(backend):
+ phi = 0.1234
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend, [gates.GPI2(0, phi)], initial_state=initial_state
+ )
+
+ phase = np.exp(1.0j * phi)
+ matrix = np.array([[1, -1.0j * np.conj(phase)], [-1.0j * phase, 1]]) / np.sqrt(2)
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.GPI2(0, phi).qasm_label == "gpi2"
+ assert not gates.GPI2(0, phi).clifford
+ assert gates.GPI2(0, phi).unitary
+
+
+def test_u1(backend):
+ theta = 0.1234
+ final_state = apply_gates(backend, [gates.X(0), gates.U1(0, theta)], nqubits=1)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[1] = np.exp(1j * theta)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.U1(0, theta).qasm_label == "u1"
+ assert not gates.U1(0, theta).clifford
+ assert gates.U1(0, theta).unitary
+
+
+def test_u2(backend):
+ phi = 0.1234
+ lam = 0.4321
+ nqubits = 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend, [gates.U2(0, phi, lam)], initial_state=initial_state
+ )
+ matrix = np.array(
+ [
+ [np.exp(-1j * (phi + lam) / 2), -np.exp(-1j * (phi - lam) / 2)],
+ [np.exp(1j * (phi - lam) / 2), np.exp(1j * (phi + lam) / 2)],
+ ]
+ )
+ target_state = np.matmul(matrix, backend.to_numpy(initial_state)) / np.sqrt(2)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.U2(0, phi, lam).qasm_label == "u2"
+ assert not gates.U2(0, phi, lam).clifford
+ assert gates.U2(0, phi, lam).unitary
+
+
+@pytest.mark.parametrize("seed_observable", list(range(1, 10 + 1)))
+@pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
+def test_u3(backend, seed_state, seed_observable):
+ nqubits = 1
+ theta, phi, lam = np.random.rand(3)
+
+ initial_state = random_statevector(2**nqubits, seed=seed_state, backend=backend)
+ final_state = apply_gates(
+ backend, [gates.U3(0, theta, phi, lam)], initial_state=initial_state
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.U3(0, theta, phi, lam).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ cost, sint = np.cos(theta / 2), np.sin(theta / 2)
+ ep = np.exp(1j * (phi + lam) / 2)
+ em = np.exp(1j * (phi - lam) / 2)
+
+ matrix = np.array([[ep.conj() * cost, -em.conj() * sint], [em * sint, ep * cost]])
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, seed=seed_observable, backend=backend)
+ backend.assert_allclose(
+ backend.cast(backend.np.conj(final_state_decompose).T)
+ @ observable
+ @ final_state_decompose,
+ backend.cast(backend.np.conj(target_state).T)
+ @ observable
+ @ backend.cast(target_state),
+ atol=1e-6,
+ )
+ assert gates.U3(0, theta, phi, lam).qasm_label == "u3"
+ assert not gates.U3(0, theta, phi, lam).clifford
+ assert gates.U3(0, theta, phi, lam).unitary
+
+
+@pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
+def test_u1q(backend, seed_state):
+ nqubits = 1
+ theta, phi = np.random.rand(2)
+
+ initial_state = random_statevector(2**nqubits, seed=seed_state, backend=backend)
+ final_state = apply_gates(
+ backend, [gates.U1q(0, theta, phi)], initial_state=initial_state
+ )
+ cost, sint = np.cos(theta / 2), np.sin(theta / 2)
+
+ matrix = np.array(
+ [[cost, -1j * np.exp(-1j * phi) * sint], [-1j * np.exp(1j * phi) * sint, cost]]
+ )
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert not gates.U1q(0, theta, phi).clifford
+ assert gates.U1q(0, theta, phi).unitary
+
+
+@pytest.mark.parametrize("applyx", [False, True])
+def test_cnot(backend, applyx):
+ if applyx:
+ gatelist = [gates.X(0)]
+ else:
+ gatelist = []
+ gatelist.append(gates.CNOT(0, 1))
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[3 * int(applyx)] = 1.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.CNOT(0, 1).qasm_label == "cx"
+ assert gates.CNOT(0, 1).clifford
+ assert gates.CNOT(0, 1).unitary
+
+
+@pytest.mark.parametrize("seed_observable", list(range(1, 10 + 1)))
+@pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
+def test_cy(backend, seed_state, seed_observable):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, seed=seed_state, backend=backend)
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, 0, -1j],
+ [0, 0, 1j, 0],
+ ]
+ )
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.CY(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ gate = gates.CY(0, 1)
+
+ final_state = apply_gates(backend, [gate], initial_state=initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, seed=seed_observable, backend=backend)
+ backend.assert_allclose(
+ backend.cast(backend.np.conj(final_state_decompose).T)
+ @ observable
+ @ final_state_decompose,
+ backend.cast(backend.np.conj(target_state).T)
+ @ observable
+ @ backend.cast(target_state),
+ atol=1e-6,
+ )
+
+ assert gate.name == "cy"
+ assert gate.qasm_label == "cy"
+ assert gate.clifford
+ assert gate.unitary
+
+
+@pytest.mark.parametrize("seed_observable", list(range(1, 10 + 1)))
+@pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
+def test_cz(backend, seed_state, seed_observable):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, seed=seed_state, backend=backend)
+ matrix = np.eye(4)
+ matrix[3, 3] = -1
+ matrix = backend.cast(matrix)
+
+ target_state = backend.np.matmul(matrix, initial_state)
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.CZ(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ gate = gates.CZ(0, 1)
+
+ final_state = apply_gates(backend, [gate], initial_state=initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, seed=seed_observable, backend=backend)
+ backend.assert_allclose(
+ backend.cast(backend.np.conj(final_state_decompose).T)
+ @ observable
+ @ final_state_decompose,
+ backend.cast(backend.np.conj(target_state).T)
+ @ observable
+ @ backend.cast(target_state),
+ atol=1e-6,
+ )
+
+ assert gate.name == "cz"
+ assert gate.qasm_label == "cz"
+ assert gate.clifford
+ assert gate.unitary
+
+
+def test_csx(backend):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.CSX(0, 1)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.CSX(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, (1 + 1j) / 2, (1 - 1j) / 2],
+ [0, 0, (1 - 1j) / 2, (1 + 1j) / 2],
+ ]
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ assert gates.CSX(0, 1).qasm_label == "csx"
+ assert not gates.CSX(0, 1).clifford
+ assert gates.CSX(0, 1).unitary
+
+
+def test_csxdg(backend):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.CSXDG(0, 1)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.CSXDG(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, (1 - 1j) / 2, (1 + 1j) / 2],
+ [0, 0, (1 + 1j) / 2, (1 - 1j) / 2],
+ ]
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ assert gates.CSXDG(0, 1).qasm_label == "csxdg"
+ assert not gates.CSXDG(0, 1).clifford
+ assert gates.CSXDG(0, 1).unitary
+
+
+@pytest.mark.parametrize(
+ "name,params",
+ [
+ ("CRX", {"theta": 0.1}),
+ ("CRX", {"theta": np.random.randint(-5, 5) * np.pi / 2}),
+ ("CRY", {"theta": 0.2}),
+ ("CRY", {"theta": np.random.randint(-5, 5) * np.pi / 2}),
+ ("CRZ", {"theta": 0.3}),
+ ("CRZ", {"theta": np.random.randint(-5, 5) * np.pi / 2}),
+ ("CU1", {"theta": 0.1}),
+ ("CU2", {"phi": 0.1, "lam": 0.2}),
+ ("CU3", {"theta": 0.1, "phi": 0.2, "lam": 0.3}),
+ ],
+)
+def test_cun(backend, name, params):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+
+ gate = getattr(gates, name)(0, 1, **params)
+
+ assert gate.unitary
+
+ if name != "CU2":
+ assert gate.qasm_label == gate.name
+ else:
+ with pytest.raises(NotImplementedError):
+ gate.qasm_label
+
+ if name in ["CRX", "CRY", "CRZ"]:
+ theta = params["theta"]
+ if (theta % (np.pi / 2)).is_integer():
+ assert gate.clifford
+ else:
+ assert not gate.clifford
+
+ final_state = apply_gates(backend, [gate], initial_state=initial_state)
+
+ _matrix = gate.matrix(backend)
+ gate = backend.cast(_matrix, dtype=_matrix.dtype)
+
+ target_state = backend.np.matmul(gate, initial_state)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_swap(backend):
+ final_state = apply_gates(backend, [gates.X(1), gates.SWAP(0, 1)], nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[2] = 1.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.SWAP(0, 1).qasm_label == "swap"
+ assert gates.SWAP(0, 1).clifford
+ assert gates.SWAP(0, 1).unitary
+
+
+def test_iswap(backend):
+ final_state = apply_gates(backend, [gates.X(1), gates.iSWAP(0, 1)], nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[2] = 1.0j
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.iSWAP(0, 1).qasm_label == "iswap"
+ assert gates.iSWAP(0, 1).clifford
+ assert gates.iSWAP(0, 1).unitary
+
+
+def test_siswap(backend):
+ final_state = apply_gates(backend, [gates.X(1), gates.SiSWAP(0, 1)], nqubits=2)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[1] = 1.0 / np.sqrt(2)
+ target_state[2] = 1.0j / np.sqrt(2)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert not gates.SiSWAP(0, 1).clifford
+ assert gates.SiSWAP(0, 1).unitary
+
+
+def test_fswap(backend):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.FSWAP(0, 1)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.FSWAP(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, 0, 1, 0],
+ [0, 1, 0, 0],
+ [0, 0, 0, -1],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ assert gates.FSWAP(0, 1).qasm_label == "fswap"
+ assert gates.FSWAP(0, 1).clifford
+ assert gates.FSWAP(0, 1).unitary
+
+
+def test_multiple_swap(backend):
+ gatelist = [gates.X(0), gates.X(2), gates.SWAP(0, 1), gates.SWAP(2, 3)]
+ final_state = apply_gates(backend, gatelist, nqubits=4)
+ gatelist = [gates.X(1), gates.X(3)]
+ target_state = apply_gates(backend, gatelist, nqubits=4)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_fsim(backend):
+ theta = 0.1234
+ phi = 0.4321
+ gatelist = [gates.H(0), gates.H(1), gates.fSim(0, 1, theta, phi)]
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ target_state = np.ones_like(backend.to_numpy(final_state)) / 2.0
+ rotation = np.array(
+ [[np.cos(theta), -1j * np.sin(theta)], [-1j * np.sin(theta), np.cos(theta)]]
+ )
+ matrix = np.eye(4, dtype=target_state.dtype)
+ matrix[1:3, 1:3] = rotation
+ matrix[3, 3] = np.exp(-1j * phi)
+ matrix = backend.cast(matrix)
+ target_state = backend.np.matmul(matrix, backend.cast(target_state))
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.fSim(0, 1, theta, phi).qasm_label
+
+ assert not gates.fSim(0, 1, theta, phi).clifford
+ assert gates.fSim(0, 1, theta, phi).unitary
+
+
+def test_sycamore(backend):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.SYC(0, 1)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1 + 0j, 0, 0, 0],
+ [0, 0, -1j, 0],
+ [0, -1j, 0, 0],
+ [0, 0, 0, np.exp(-1j * np.pi / 6)],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.SYC(0, 1).qasm_label
+
+ assert not gates.SYC(0, 1).clifford
+ assert gates.SYC(0, 1).unitary
+
+
+def test_generalized_fsim(backend):
+ phi = np.random.random()
+ rotation = np.random.random((2, 2)) + 1j * np.random.random((2, 2))
+ gatelist = [gates.H(0), gates.H(1), gates.H(2)]
+ gatelist.append(gates.GeneralizedfSim(1, 2, rotation, phi))
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ target_state = np.ones(len(final_state), dtype=complex) / np.sqrt(8)
+ matrix = np.eye(4, dtype=target_state.dtype)
+ matrix[1:3, 1:3] = rotation
+ matrix[3, 3] = np.exp(-1j * phi)
+ # matrix = backend.cast(matrix, dtype=matrix.dtype)
+ target_state[:4] = np.matmul(matrix, target_state[:4])
+ target_state[4:] = np.matmul(matrix, target_state[4:])
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gatelist[-1].qasm_label
+
+ assert not gates.GeneralizedfSim(0, 1, rotation, phi).clifford
+ assert gates.GeneralizedfSim(0, 1, rotation, phi).unitary
+
+
+def test_generalized_fsim_parameter_setter(backend):
+ phi = np.random.random()
+ matrix = np.random.random((2, 2))
+ gate = gates.GeneralizedfSim(0, 1, matrix, phi)
+ backend.assert_allclose(gate.parameters[0], matrix, atol=1e-6)
+
+ assert gate.parameters[1] == phi
+
+ matrix = np.random.random((4, 4))
+
+ with pytest.raises(ValueError):
+ gates.GeneralizedfSim(0, 1, matrix, phi)
+
+ with pytest.raises(NotImplementedError):
+ gate.qasm_label
+
+ assert not gate.clifford
+ assert gate.unitary
+
+
+def test_rxx(backend):
+ theta = 0.1234
+ final_state = apply_gates(
+ backend, [gates.H(0), gates.H(1), gates.RXX(0, 1, theta=theta)], nqubits=2
+ )
+ phase = np.exp(1j * theta / 2.0)
+ gate = np.array(
+ [
+ [phase.real, 0, 0, -1j * phase.imag],
+ [0, phase.real, -1j * phase.imag, 0],
+ [0, -1j * phase.imag, phase.real, 0],
+ [-1j * phase.imag, 0, 0, phase.real],
+ ]
+ )
+ target_state = gate.dot(np.ones(4)) / 2.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RXX(0, 1, theta=theta).qasm_label == "rxx"
+ assert not gates.RXX(0, 1, theta).clifford
+ assert gates.RXX(0, 1, theta).unitary
+
+
+def test_ryy(backend):
+ theta = 0.1234
+ final_state = apply_gates(
+ backend, [gates.H(0), gates.H(1), gates.RYY(0, 1, theta=theta)], nqubits=2
+ )
+ phase = np.exp(1j * theta / 2.0)
+ gate = np.array(
+ [
+ [phase.real, 0, 0, 1j * phase.imag],
+ [0, phase.real, -1j * phase.imag, 0],
+ [0, -1j * phase.imag, phase.real, 0],
+ [1j * phase.imag, 0, 0, phase.real],
+ ]
+ )
+ target_state = gate.dot(np.ones(4)) / 2.0
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RYY(0, 1, theta=theta).qasm_label == "ryy"
+ assert not gates.RYY(0, 1, theta).clifford
+ assert gates.RYY(0, 1, theta).unitary
+
+
+def test_rzz(backend):
+ theta = 0.1234
+ final_state = apply_gates(
+ backend, [gates.X(0), gates.X(1), gates.RZZ(0, 1, theta=theta)], nqubits=2
+ )
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[3] = np.exp(-1j * theta / 2.0)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.RZZ(0, 1, theta=theta).qasm_label == "rzz"
+ assert not gates.RZZ(0, 1, theta).clifford
+ assert gates.RZZ(0, 1, theta).unitary
+
+
+def test_rzx(backend):
+ theta = 0.1234
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.RZX(0, 1, theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.RZX(0, 1, theta).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ cos, sin = np.cos(theta / 2), np.sin(theta / 2)
+ matrix = np.array(
+ [
+ [cos, -1j * sin, 0, 0],
+ [-1j * sin, cos, 0, 0],
+ [0, 0, cos, 1j * sin],
+ [0, 0, 1j * sin, cos],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.RZX(0, 1, theta).qasm_label
+
+ assert not gates.RZX(0, 1, theta).clifford
+ assert gates.RZX(0, 1, theta).unitary
+
+
+def test_rxxyy(backend):
+ theta = 0.1234
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.RXXYY(0, 1, theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.RXXYY(0, 1, theta).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ cos, sin = np.cos(theta / 2), np.sin(theta / 2)
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, cos, -1j * sin, 0],
+ [0, -1j * sin, cos, 0],
+ [0, 0, 0, 1],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+ target_state = matrix @ initial_state
+
+ observable = random_hermitian(2**nqubits, backend=backend)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ # testing random expectation value due to global phase difference
+ backend.assert_allclose(
+ backend.cast(backend.np.conj(final_state_decompose).T)
+ @ observable
+ @ final_state_decompose,
+ backend.cast(backend.np.conj(target_state).T) @ observable @ target_state,
+ atol=1e-6,
+ )
+
+ with pytest.raises(NotImplementedError):
+ gates.RXXYY(0, 1, theta).qasm_label
+
+ assert not gates.RXXYY(0, 1, theta).clifford
+ assert gates.RXXYY(0, 1, theta).unitary
+
+
+def test_ms(backend):
+ phi0 = 0.1234
+ phi1 = 0.4321
+ theta = np.pi / 2
+
+ with pytest.raises(ValueError):
+ gates.MS(0, 1, phi0=phi0, phi1=phi1, theta=np.pi)
+
+ final_state = apply_gates(
+ backend,
+ [gates.H(0), gates.H(1), gates.MS(0, 1, phi0=phi0, phi1=phi1)],
+ nqubits=2,
+ )
+ target_state = np.ones_like(backend.to_numpy(final_state)) / 2.0
+ plus = np.exp(1.0j * (phi0 + phi1))
+ minus = np.exp(1.0j * (phi0 - phi1))
+
+ matrix = np.eye(4, dtype=target_state.dtype)
+ matrix[3, 0] = -1.0j * plus
+ matrix[0, 3] = -1.0j * np.conj(plus)
+ matrix[2, 1] = -1.0j * minus
+ matrix[1, 2] = -1.0j * np.conj(minus)
+ matrix /= np.sqrt(2)
+ matrix = backend.cast(matrix)
+ target_state = backend.np.matmul(matrix, backend.cast(target_state))
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gates.MS(0, 1, phi0, phi1, theta).qasm_label == "ms"
+ assert not gates.MS(0, 1, phi0, phi1).clifford
+ assert gates.MS(0, 1, phi0, phi1).unitary
+
+
+def test_givens(backend):
+ theta = 0.1234
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.GIVENS(0, 1, theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.GIVENS(0, 1, theta).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, np.cos(theta), -np.sin(theta), 0],
+ [0, np.sin(theta), np.cos(theta), 0],
+ [0, 0, 0, 1],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+
+ target_state = matrix @ initial_state
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.GIVENS(0, 1, theta).qasm_label
+
+ assert not gates.GIVENS(0, 1, theta).clifford
+ assert gates.GIVENS(0, 1, theta).unitary
+
+
+def test_rbs(backend):
+ theta = 0.1234
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.RBS(0, 1, theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.RBS(0, 1, theta).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, np.cos(theta), np.sin(theta), 0],
+ [0, -np.sin(theta), np.cos(theta), 0],
+ [0, 0, 0, 1],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+
+ target_state = matrix @ initial_state
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ backend.assert_allclose(final_state_decompose, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.RBS(0, 1, theta).qasm_label
+
+ assert not gates.RBS(0, 1, theta).clifford
+ assert gates.RBS(0, 1, theta).unitary
+
+
+def test_ecr(backend):
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.ECR(0, 1)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+ # test decomposition
+ final_state_decompose = apply_gates(
+ backend,
+ gates.ECR(0, 1).decompose(),
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [0, 0, 1, 1j],
+ [0, 0, 1j, 1],
+ [1, -1j, 0, 0],
+ [-1j, 1, 0, 0],
+ ],
+ dtype=np.complex128,
+ ) / np.sqrt(2)
+ matrix = backend.cast(matrix)
+
+ target_state = matrix @ initial_state
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ # testing random expectation value due to global phase difference
+ observable = random_hermitian(2**nqubits, backend=backend)
+ backend.assert_allclose(
+ backend.cast(backend.np.conj(final_state_decompose).T)
+ @ observable
+ @ final_state_decompose,
+ backend.cast(backend.np.conj(target_state).T) @ observable @ target_state,
+ atol=1e-6,
+ )
+
+ with pytest.raises(NotImplementedError):
+ gates.ECR(0, 1).qasm_label
+
+ assert gates.ECR(0, 1).clifford
+ assert gates.ECR(0, 1).unitary
+
+
+@pytest.mark.parametrize("applyx", [False, True])
+def test_toffoli(backend, applyx):
+ if applyx:
+ gatelist = [gates.X(0), gates.X(1), gates.TOFFOLI(0, 1, 2)]
+ else:
+ gatelist = [gates.X(1), gates.TOFFOLI(0, 1, 2)]
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ if applyx:
+ target_state[-1] = 1
+ else:
+ target_state[2] = 1
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ assert gatelist[-1].qasm_label == "ccx"
+ assert not gates.TOFFOLI(0, 1, 2).clifford
+ assert gates.TOFFOLI(0, 1, 2).unitary
+
+ # test decomposition
+ decomposition = Circuit(3)
+ decomposition.add(standard_decompositions(gates.TOFFOLI(0, 1, 2)))
+ decomposition = decomposition.unitary(backend)
+
+ backend.assert_allclose(decomposition, backend.cast(matrices.TOFFOLI), atol=1e-10)
+
+
+def test_ccz(backend):
+ nqubits = 3
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.CCZ(0, 1, 2)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ matrix = np.array(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, 0, -1],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+
+ target_state = matrix @ initial_state
+ backend.assert_allclose(final_state, target_state)
+
+ assert gates.CCZ(0, 1, 2).qasm_label == "ccz"
+ assert not gates.CCZ(0, 1, 2).clifford
+ assert gates.CCZ(0, 1, 2).unitary
+
+ # test decomposition
+ decomposition = Circuit(3)
+ decomposition.add(gates.CCZ(0, 1, 2).decompose())
+ decomposition = decomposition.unitary(backend)
+
+ backend.assert_allclose(decomposition, backend.cast(matrices.CCZ), atol=1e-10)
+
+
+def test_deutsch(backend):
+ theta = 0.1234
+ nqubits = 3
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend,
+ [gates.DEUTSCH(0, 1, 2, theta)],
+ nqubits=nqubits,
+ initial_state=initial_state,
+ )
+
+ sin, cos = np.sin(theta), np.cos(theta)
+ matrix = np.array(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 0, 0, 0, 1j * cos, sin],
+ [0, 0, 0, 0, 0, 0, sin, 1j * cos],
+ ],
+ dtype=np.complex128,
+ )
+ matrix = backend.cast(matrix)
+
+ target_state = matrix @ initial_state
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.DEUTSCH(0, 1, 2, theta).qasm_label
+
+ assert not gates.DEUTSCH(0, 1, 2, theta).clifford
+ assert gates.DEUTSCH(0, 1, 2, theta).unitary
+
+
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_unitary(backend, nqubits):
+ initial_state = np.ones(2**nqubits) / np.sqrt(2**nqubits)
+ matrix = np.random.random(2 * (2 ** (nqubits - 1),))
+ target_state = np.kron(np.eye(2), matrix).dot(initial_state)
+ gatelist = [gates.H(i) for i in range(nqubits)]
+ gate = gates.Unitary(matrix, *range(1, nqubits), name="random")
+ gatelist.append(gate)
+ final_state = apply_gates(backend, gatelist, nqubits=nqubits)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_unitary_initialization(backend):
+
+ matrix = np.random.random((4, 4))
+ gate = gates.Unitary(matrix, 0, 1)
+ backend.assert_allclose(gate.parameters[0], matrix, atol=1e-6)
+
+ with pytest.raises(NotImplementedError):
+ gates.Unitary(matrix, 0, 1).qasm_label
+
+ assert not gates.Unitary(matrix, 0, 1).clifford
+ assert not gates.Unitary(matrix, 0, 1, check_unitary=False).unitary
+ assert gates.Unitary(random_unitary(2, backend=backend), 0).unitary
+
+
+def test_unitary_common_gates(backend):
+ target_state = apply_gates(backend, [gates.X(0), gates.H(1)], nqubits=2)
+ gatelist = [
+ gates.Unitary(np.array([[0, 1], [1, 0]]), 0),
+ gates.Unitary(np.array([[1, 1], [1, -1]]) / np.sqrt(2), 1),
+ ]
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ thetax = 0.1234
+ thetay = 0.4321
+ gatelist = [gates.RX(0, theta=thetax), gates.RY(1, theta=thetay), gates.CNOT(0, 1)]
+ target_state = apply_gates(backend, gatelist, nqubits=2)
+
+ rx = np.array(
+ [
+ [np.cos(thetax / 2), -1j * np.sin(thetax / 2)],
+ [-1j * np.sin(thetax / 2), np.cos(thetax / 2)],
+ ]
+ )
+ ry = np.array(
+ [
+ [np.cos(thetay / 2), -np.sin(thetay / 2)],
+ [np.sin(thetay / 2), np.cos(thetay / 2)],
+ ]
+ )
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ gatelist = [gates.Unitary(rx, 0), gates.Unitary(ry, 1), gates.Unitary(cnot, 0, 1)]
+ final_state = apply_gates(backend, gatelist, nqubits=2)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_unitary_multiqubit(backend):
+ gatelist = [gates.H(i) for i in range(4)]
+ gatelist.append(gates.CNOT(0, 1))
+ gatelist.append(gates.CNOT(2, 3))
+ gatelist.extend(gates.X(i) for i in range(4))
+
+ h = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ x = np.array([[0, 1], [1, 0]])
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ matrix = np.kron(np.kron(x, x), np.kron(x, x))
+ matrix = matrix @ np.kron(cnot, cnot)
+ matrix = matrix @ np.kron(np.kron(h, h), np.kron(h, h))
+ unitary = gates.Unitary(matrix, 0, 1, 2, 3)
+ final_state = apply_gates(backend, [unitary], nqubits=4)
+ target_state = apply_gates(backend, gatelist, nqubits=4)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+############################# Test ``controlled_by`` #############################
+
+
+def test_controlled_x(backend):
+ gatelist = [
+ gates.X(0),
+ gates.X(1),
+ gates.X(2),
+ gates.X(3).controlled_by(0, 1, 2),
+ gates.X(0),
+ gates.X(2),
+ ]
+ final_state = apply_gates(backend, gatelist, nqubits=4)
+ gatelist = [gates.X(1), gates.X(3)]
+ target_state = apply_gates(backend, gatelist, nqubits=4)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_x_vs_cnot(backend):
+ gatelist = [gates.X(0), gates.X(2).controlled_by(0)]
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ gatelist = [gates.X(0), gates.CNOT(0, 2)]
+ target_state = apply_gates(backend, gatelist, nqubits=3)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_x_vs_toffoli(backend):
+ gatelist = [gates.X(0), gates.X(2), gates.X(1).controlled_by(0, 2)]
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ gatelist = [gates.X(0), gates.X(2), gates.TOFFOLI(0, 2, 1)]
+ target_state = apply_gates(backend, gatelist, nqubits=3)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+@pytest.mark.parametrize("applyx", [False, True])
+def test_controlled_rx(backend, applyx):
+ theta = 0.1234
+ gatelist = [gates.X(0)]
+ if applyx:
+ gatelist.append(gates.X(1))
+ gatelist.append(gates.RX(2, theta).controlled_by(0, 1))
+ gatelist.append(gates.X(0))
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+
+ gatelist = []
+ if applyx:
+ gatelist.extend([gates.X(1), gates.RX(2, theta)])
+ target_state = apply_gates(backend, gatelist, nqubits=3)
+
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_u1(backend):
+ theta = 0.1234
+ gatelist = [gates.X(i) for i in range(3)]
+ gatelist.append(gates.U1(2, theta).controlled_by(0, 1))
+ gatelist.append(gates.X(0))
+ gatelist.append(gates.X(1))
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ target_state = np.zeros_like(backend.to_numpy(final_state))
+ target_state[1] = np.exp(1j * theta)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ gate = gates.U1(0, theta).controlled_by(1)
+ assert gate.__class__.__name__ == "CU1"
+
+
+def test_controlled_u2(backend):
+ phi = 0.1234
+ lam = 0.4321
+ gatelist = [gates.X(0), gates.X(1)]
+ gatelist.append(gates.U2(2, phi, lam).controlled_by(0, 1))
+ gatelist.extend([gates.X(0), gates.X(1)])
+ final_state = apply_gates(backend, gatelist, nqubits=3)
+ gatelist = [gates.X(0), gates.X(1), gates.U2(2, phi, lam), gates.X(0), gates.X(1)]
+ target_state = apply_gates(backend, gatelist, nqubits=3)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ # for coverage
+ gate = gates.CU2(0, 1, phi, lam)
+ assert gate.parameters == (phi, lam)
+
+
+def test_controlled_u3(backend):
+ theta, phi, lam = 0.1, 0.1234, 0.4321
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(
+ backend, [gates.U3(1, theta, phi, lam).controlled_by(0)], 2, initial_state
+ )
+ target_state = apply_gates(
+ backend, [gates.CU3(0, 1, theta, phi, lam)], 2, initial_state
+ )
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+ # for coverage
+ gate = gates.U3(0, theta, phi, lam)
+ assert gate.parameters == (theta, phi, lam)
+
+
+@pytest.mark.parametrize("applyx", [False, True])
+@pytest.mark.parametrize("free_qubit", [False, True])
+def test_controlled_swap(backend, applyx, free_qubit):
+ f = int(free_qubit)
+ gatelist = []
+ if applyx:
+ gatelist.append(gates.X(0))
+ gatelist.extend(
+ [
+ gates.RX(1 + f, theta=0.1234),
+ gates.RY(2 + f, theta=0.4321),
+ gates.SWAP(1 + f, 2 + f).controlled_by(0),
+ ]
+ )
+ final_state = apply_gates(backend, gatelist, 3 + f)
+ gatelist = [gates.RX(1 + f, theta=0.1234), gates.RY(2 + f, theta=0.4321)]
+ if applyx:
+ gatelist.extend([gates.X(0), gates.SWAP(1 + f, 2 + f)])
+ target_state = apply_gates(backend, gatelist, 3 + f)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+@pytest.mark.parametrize("applyx", [False, True])
+def test_controlled_swap_double(backend, applyx):
+ gatelist = [gates.X(0)]
+ if applyx:
+ gatelist.append(gates.X(3))
+ gatelist.append(gates.RX(1, theta=0.1234))
+ gatelist.append(gates.RY(2, theta=0.4321))
+ gatelist.append(gates.SWAP(1, 2).controlled_by(0, 3))
+ gatelist.append(gates.X(0))
+ final_state = apply_gates(backend, gatelist, 4)
+
+ gatelist = [gates.RX(1, theta=0.1234), gates.RY(2, theta=0.4321)]
+ if applyx:
+ gatelist.extend([gates.X(3), gates.SWAP(1, 2)])
+ target_state = apply_gates(backend, gatelist, 4)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_fsim(backend):
+ theta, phi = 0.1234, 0.4321
+ gatelist = [gates.H(i) for i in range(6)]
+ gatelist.append(gates.fSim(5, 3, theta, phi).controlled_by(0, 2, 1))
+ final_state = apply_gates(backend, gatelist, 6)
+
+ target_state = np.ones(len(final_state), dtype=complex) / np.sqrt(2**6)
+ rotation = np.array(
+ [[np.cos(theta), -1j * np.sin(theta)], [-1j * np.sin(theta), np.cos(theta)]]
+ )
+ matrix = np.eye(4, dtype=target_state.dtype)
+ matrix[1:3, 1:3] = rotation
+ matrix[3, 3] = np.exp(-1j * phi)
+ ids = [56, 57, 60, 61]
+ target_state[ids] = np.matmul(matrix, target_state[ids])
+ ids = [58, 59, 62, 63]
+ target_state[ids] = np.matmul(matrix, target_state[ids])
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_unitary(backend):
+ matrix = random_unitary(2**1, backend=backend)
+ gatelist = [gates.H(0), gates.H(1), gates.Unitary(matrix, 1).controlled_by(0)]
+ final_state = apply_gates(backend, gatelist, 2)
+ target_state = np.ones(len(final_state), dtype=complex) / 2.0
+ target_state[2:] = np.matmul(backend.to_numpy(matrix), target_state[2:])
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+ matrix = random_unitary(2**2, backend=backend)
+ gatelist = [gates.H(i) for i in range(4)]
+ gatelist.append(gates.Unitary(matrix, 1, 3).controlled_by(0, 2))
+ final_state = apply_gates(backend, gatelist, 4)
+ target_state = np.ones(len(final_state), dtype=complex) / 4.0
+ ids = [10, 11, 14, 15]
+ target_state[ids] = np.matmul(backend.to_numpy(matrix), target_state[ids])
+ target_state = backend.cast(target_state, dtype=target_state.dtype)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+###############################################################################
+
+################################# Test dagger #################################
+GATES = [
+ ("H", (0,)),
+ ("X", (0,)),
+ ("Y", (0,)),
+ ("Z", (0,)),
+ ("SX", (0,)),
+ ("SXDG", (0,)),
+ ("S", (0,)),
+ ("SDG", (0,)),
+ ("T", (0,)),
+ ("TDG", (0,)),
+ ("RX", (0, 0.1)),
+ ("RY", (0, 0.2)),
+ ("RZ", (0, 0.3)),
+ ("PRX", (0, 0.1, 0.2)),
+ ("GPI", (0, 0.1)),
+ ("GPI2", (0, 0.2)),
+ ("U1", (0, 0.1)),
+ ("U2", (0, 0.2, 0.3)),
+ ("U3", (0, 0.1, 0.2, 0.3)),
+ ("U1q", (0, 0.1, 0.2)),
+ ("CNOT", (0, 1)),
+ ("CZ", (0, 1)),
+ ("CSX", (0, 1)),
+ ("CSXDG", (0, 1)),
+ ("CRX", (0, 1, 0.1)),
+ ("CRZ", (0, 1, 0.3)),
+ ("CU1", (0, 1, 0.1)),
+ ("CU2", (0, 1, 0.2, 0.3)),
+ ("CU3", (0, 1, 0.1, 0.2, 0.3)),
+ ("fSim", (0, 1, 0.1, 0.2)),
+ ("SYC", (0, 1)),
+ ("RXX", (0, 1, 0.1)),
+ ("RYY", (0, 1, 0.2)),
+ ("RZZ", (0, 1, 0.3)),
+ ("RZX", (0, 1, 0.4)),
+ ("RXXYY", (0, 1, 0.5)),
+ ("MS", (0, 1, 0.1, 0.2, 0.3)),
+ ("GIVENS", (0, 1, 0.1)),
+ ("RBS", (0, 1, 0.2)),
+ ("ECR", (0, 1)),
+ ("SiSWAP", (0, 1)),
+ ("SiSWAPDG", (0, 1)),
+]
+
+
+@pytest.mark.parametrize("gate,args", GATES)
+def test_dagger(backend, gate, args):
+ gate = getattr(gates, gate)(*args)
+ nqubits = len(gate.qubits)
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gate, gate.dagger()], nqubits, initial_state)
+ backend.assert_allclose(final_state, initial_state, atol=1e-6)
+
+
+GATES = [
+ ("H", (3,)),
+ ("X", (3,)),
+ ("Y", (3,)),
+ ("S", (3,)),
+ ("SDG", (3,)),
+ ("T", (3,)),
+ ("TDG", (3,)),
+ ("RX", (3, 0.1)),
+ ("U1", (3, 0.1)),
+ ("U3", (3, 0.1, 0.2, 0.3)),
+]
+
+
+@pytest.mark.parametrize("gate,args", GATES)
+def test_controlled_dagger(backend, gate, args):
+ gate = getattr(gates, gate)(*args).controlled_by(0, 1, 2)
+ nqubits = 4
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gate, gate.dagger()], 4, initial_state)
+ backend.assert_allclose(final_state, initial_state, atol=1e-6)
+
+
+@pytest.mark.parametrize("gate_1,gate_2", [("S", "SDG"), ("T", "TDG")])
+@pytest.mark.parametrize("qubit", (0, 2, 4))
+def test_dagger_consistency(backend, gate_1, gate_2, qubit):
+ gate_1 = getattr(gates, gate_1)(qubit)
+ gate_2 = getattr(gates, gate_2)(qubit)
+ initial_state = random_statevector(2 ** (qubit + 1), backend=backend)
+ final_state = apply_gates(backend, [gate_1, gate_2], qubit + 1, initial_state)
+ backend.assert_allclose(final_state, initial_state, atol=1e-6)
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_unitary_dagger(backend, nqubits):
+ matrix = np.random.random((2**nqubits, 2**nqubits))
+ matrix = backend.cast(matrix)
+ gate = gates.Unitary(matrix, *range(nqubits))
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gate, gate.dagger()], nqubits, initial_state)
+ target_state = backend.np.matmul(matrix, initial_state)
+ target_state = backend.np.matmul(backend.np.conj(matrix).T, target_state)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_controlled_unitary_dagger(backend):
+ from scipy.linalg import expm
+
+ matrix = np.random.random((2, 2))
+ matrix = expm(1j * (matrix + matrix.T))
+ matrix = backend.cast(matrix)
+ gate = gates.Unitary(matrix, 0).controlled_by(1, 2, 3, 4)
+ nqubits = 5
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gate, gate.dagger()], 5, initial_state)
+ backend.assert_allclose(final_state, initial_state, atol=1e-6)
+
+
+def test_generalizedfsim_dagger(backend):
+ from scipy.linalg import expm
+
+ phi = 0.2
+ matrix = np.random.random((2, 2))
+ matrix = expm(1j * (matrix + matrix.T))
+ gate = gates.GeneralizedfSim(0, 1, matrix, phi)
+ nqubits = 2
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = apply_gates(backend, [gate, gate.dagger()], 2, initial_state)
+ backend.assert_allclose(final_state, initial_state, atol=1e-6)
+
+
+###############################################################################
+
+############################# Test basis rotation #############################
+
+
+def test_gate_basis_rotation(backend):
+ gate = gates.X(0).basis_rotation()
+ assert isinstance(gate, gates.H)
+ gate = gates.Y(0).basis_rotation()
+ assert isinstance(gate, gates.Unitary)
+ target_matrix = np.array([[1, -1j], [1j, -1]]) / np.sqrt(2)
+ backend.assert_allclose(gate.matrix(backend), target_matrix)
+ with pytest.raises(NotImplementedError):
+ gates.RX(0, np.pi / 2).basis_rotation()
+
+
+###############################################################################
+
+########################### Test gate decomposition ###########################
+
+
+@pytest.mark.parametrize(
+ ("target", "controls", "free"),
+ [
+ (0, (1,), ()),
+ (2, (0, 1), ()),
+ (3, (0, 1, 4), (2, 5)),
+ (7, (0, 1, 2, 3, 4), (5, 6)),
+ (5, (0, 2, 4, 6, 7), (1, 3)),
+ (8, (0, 2, 4, 6, 9), (3, 5, 7)),
+ ],
+)
+@pytest.mark.parametrize("use_toffolis", [True, False])
+def test_x_decomposition_execution(backend, target, controls, free, use_toffolis):
+ """Check that applying the decomposition is equivalent to applying the multi-control gate."""
+ gate = gates.X(target).controlled_by(*controls)
+ nqubits = max((target,) + controls + free) + 1
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ target_state = backend.apply_gate(gate, backend.np.copy(initial_state), nqubits)
+ dgates = gate.decompose(*free, use_toffolis=use_toffolis)
+ final_state = backend.np.copy(initial_state)
+ for gate in dgates:
+ final_state = backend.apply_gate(gate, final_state, nqubits)
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+###############################################################################
+
+####################### Test Clifford updates #################################
+
+
+@pytest.mark.parametrize(
+ "gate",
+ [
+ gates.RX(0, 0),
+ gates.RY(0, 0),
+ gates.RZ(0, 0),
+ gates.CRX(0, 1, 0),
+ gates.CRY(0, 1, 0),
+ gates.CRZ(0, 1, 0),
+ ],
+)
+def test_clifford_condition_update(backend, gate):
+ """Test clifford condition update if setting new angle into the rotations."""
+ assert gate.clifford
+ gate.parameters = 0.5
+ assert not gate.clifford
+ gate.parameters = np.pi
+ assert gate.clifford
+
+
+###############################################################################
diff --git a/tests/test_gates_special.py b/tests/test_gates_special.py
new file mode 100644
index 000000000..472d4e371
--- /dev/null
+++ b/tests/test_gates_special.py
@@ -0,0 +1,32 @@
+import numpy as np
+import pytest
+
+from qibo import gates
+
+
+def test_callback_gate_errors(backend):
+ from qibo import callbacks
+
+ entropy = callbacks.EntanglementEntropy([0])
+ gate = gates.CallbackGate(entropy)
+ with pytest.raises(NotImplementedError):
+ gate.on_qubits(2)
+ with pytest.raises(NotImplementedError):
+ gate.matrix(backend)
+
+
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_fused_gate_construct_unitary(backend, nqubits):
+ gate = gates.FusedGate(0, 1)
+ gate.append(gates.H(0))
+ gate.append(gates.H(1))
+ gate.append(gates.CZ(0, 1))
+ hmatrix = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ czmatrix = np.diag([1, 1, 1, -1])
+ target_matrix = czmatrix @ np.kron(hmatrix, hmatrix)
+ if nqubits > 2:
+ gate.append(gates.TOFFOLI(0, 1, 2))
+ toffoli = np.eye(8)
+ toffoli[-2:, -2:] = np.array([[0, 1], [1, 0]])
+ target_matrix = toffoli @ np.kron(target_matrix, np.eye(2))
+ backend.assert_allclose(gate.matrix(backend), target_matrix)
diff --git a/tests/test_hamiltonians.py b/tests/test_hamiltonians.py
new file mode 100644
index 000000000..5dce38618
--- /dev/null
+++ b/tests/test_hamiltonians.py
@@ -0,0 +1,460 @@
+"""Test methods in `qibo/core/hamiltonians.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, hamiltonians
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+from qibo.symbols import I, Z
+
+from .utils import random_sparse_matrix
+
+
+def test_hamiltonian_init(backend):
+ with pytest.raises(TypeError):
+ H = hamiltonians.Hamiltonian(2, "test", backend=backend)
+ with pytest.raises(ValueError):
+ H1 = hamiltonians.Hamiltonian(-2, np.eye(4), backend=backend)
+ with pytest.raises(RuntimeError):
+ H2 = hamiltonians.Hamiltonian(np.eye(2), np.eye(4), backend=backend)
+ with pytest.raises(ValueError):
+ H3 = hamiltonians.Hamiltonian(4, np.eye(10), backend=backend)
+
+
+@pytest.mark.parametrize(
+ "dtype",
+ [
+ int,
+ float,
+ complex,
+ np.int32,
+ np.int64,
+ np.float32,
+ np.float64,
+ np.complex64,
+ np.complex128,
+ ],
+)
+@pytest.mark.parametrize("sparse_type", [None, "coo", "csr", "csc", "dia"])
+def test_hamiltonian_algebraic_operations(backend, dtype, sparse_type):
+ """Test basic hamiltonian overloading."""
+
+ def transformation_a(a, b):
+ c1 = dtype(0.1)
+ return a + c1 * b
+
+ def transformation_b(a, b):
+ c1 = dtype(2)
+ c2 = dtype(3.5)
+ return c1 * a - b * c2
+
+ def transformation_c(a, b, use_eye=False):
+ c1 = dtype(4.5)
+ if use_eye:
+ return a + c1 * backend.to_numpy(backend.matrices.I(a.shape[0])) - b
+ else:
+ return a + c1 - b
+
+ def transformation_d(a, b, use_eye=False):
+ c1 = dtype(10.5)
+ c2 = dtype(2)
+ if use_eye:
+ return c1 * backend.to_numpy(backend.matrices.I(a.shape[0])) - a + c2 * b
+ else:
+ return c1 - a + c2 * b
+
+ if sparse_type is None:
+ H1 = hamiltonians.XXZ(nqubits=2, delta=0.5, backend=backend)
+ H2 = hamiltonians.XXZ(nqubits=2, delta=1, backend=backend)
+ mH1, mH2 = backend.to_numpy(H1.matrix), backend.to_numpy(H2.matrix)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+
+ mH1 = random_sparse_matrix(backend, 64, sparse_type=sparse_type)
+ mH2 = random_sparse_matrix(backend, 64, sparse_type=sparse_type)
+ H1 = hamiltonians.Hamiltonian(6, mH1, backend=backend)
+ H2 = hamiltonians.Hamiltonian(6, mH2, backend=backend)
+
+ hH1 = transformation_a(mH1, mH2)
+ hH2 = transformation_b(mH1, mH2)
+ hH3 = transformation_c(mH1, mH2, use_eye=True)
+ hH4 = transformation_d(mH1, mH2, use_eye=True)
+
+ HT1 = transformation_a(H1, H2)
+ HT2 = transformation_b(H1, H2)
+ HT3 = transformation_c(H1, H2)
+ HT4 = transformation_d(H1, H2)
+
+ backend.assert_allclose(hH1, HT1.matrix)
+ backend.assert_allclose(hH2, HT2.matrix)
+ backend.assert_allclose(hH3, HT3.matrix)
+ backend.assert_allclose(hH4, HT4.matrix)
+
+
+@pytest.mark.parametrize("sparse_type", [None, "coo", "csr", "csc", "dia"])
+def test_hamiltonian_addition(backend, sparse_type):
+ if sparse_type is None:
+ H1 = hamiltonians.Y(nqubits=3, backend=backend)
+ H2 = hamiltonians.TFIM(nqubits=3, h=1.0, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ H1 = hamiltonians.Hamiltonian(
+ 6,
+ random_sparse_matrix(backend, 64, sparse_type=sparse_type),
+ backend=backend,
+ )
+ H2 = hamiltonians.Hamiltonian(
+ 6,
+ random_sparse_matrix(backend, 64, sparse_type=sparse_type),
+ backend=backend,
+ )
+
+ H = H1 + H2
+ matrix = H1.matrix + H2.matrix
+ backend.assert_allclose(H.matrix, matrix)
+ H = H1 - 0.5 * H2
+ matrix = H1.matrix - 0.5 * H2.matrix
+ backend.assert_allclose(H.matrix, matrix)
+
+ H1 = hamiltonians.XXZ(nqubits=2, delta=0.5, backend=backend)
+ H2 = hamiltonians.XXZ(nqubits=3, delta=0.1, backend=backend)
+ with pytest.raises(RuntimeError):
+ R = H1 + H2
+ with pytest.raises(RuntimeError):
+ R = H1 - H2
+
+
+def test_hamiltonian_operation_errors(backend):
+ """Testing hamiltonian not implemented errors."""
+ H1 = hamiltonians.XXZ(nqubits=2, delta=0.5, backend=backend)
+ H2 = hamiltonians.XXZ(nqubits=2, delta=0.1, backend=backend)
+
+ with pytest.raises(NotImplementedError):
+ R = H1 * H2
+ with pytest.raises(NotImplementedError):
+ R = H1 + "a"
+ with pytest.raises(NotImplementedError):
+ R = H2 - (2,)
+ with pytest.raises(NotImplementedError):
+ R = [3] - H1
+
+
+@pytest.mark.parametrize("sparse_type", [None, "coo", "csr", "csc", "dia"])
+def test_hamiltonian_matmul(backend, sparse_type):
+ """Test matrix multiplication between Hamiltonians."""
+ if backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ if sparse_type is None:
+ nqubits = 3
+ H1 = hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ H2 = hamiltonians.Y(nqubits, backend=backend)
+ else:
+ nqubits = 5
+ nstates = 2**nqubits
+ H1 = hamiltonians.Hamiltonian(
+ nqubits,
+ random_sparse_matrix(backend, nstates, sparse_type),
+ backend=backend,
+ )
+ H2 = hamiltonians.Hamiltonian(
+ nqubits,
+ random_sparse_matrix(backend, nstates, sparse_type),
+ backend=backend,
+ )
+
+ m1 = backend.to_numpy(H1.matrix)
+ m2 = backend.to_numpy(H2.matrix)
+ if backend.name == "tensorflow" and sparse_type is not None:
+ with pytest.raises(NotImplementedError):
+ _ = H1 @ H2
+ else:
+ backend.assert_allclose((H1 @ H2).matrix, (m1 @ m2))
+ backend.assert_allclose((H2 @ H1).matrix, (m2 @ m1))
+
+ with pytest.raises(ValueError):
+ H1 @ np.zeros(3 * (2**nqubits,), dtype=m1.dtype)
+ with pytest.raises(NotImplementedError):
+ H1 @ 2
+
+
+@pytest.mark.parametrize("sparse_type", [None, "coo", "csr", "csc", "dia"])
+def test_hamiltonian_matmul_states(backend, sparse_type):
+ """Test matrix multiplication between Hamiltonian and states."""
+ if sparse_type is None:
+ nqubits = 3
+ H = hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ nqubits = 3
+ nstates = 2**nqubits
+ matrix = random_sparse_matrix(backend, nstates, sparse_type)
+ H = hamiltonians.Hamiltonian(nqubits, matrix, backend=backend)
+
+ hm = H.matrix
+ v = random_statevector(2**nqubits, backend=backend)
+ v = backend.cast(v, dtype=hm.dtype)
+ m = random_density_matrix(2**nqubits, backend=backend)
+ m = backend.cast(m, dtype=hm.dtype)
+ Hv = H @ backend.cast(v)
+ Hm = H @ backend.cast(m)
+ backend.assert_allclose(Hv, hm @ v) # needs atol for cuquantum
+ backend.assert_allclose(Hm, hm @ m)
+
+
+@pytest.mark.parametrize("density_matrix", [True, False])
+@pytest.mark.parametrize(
+ "sparse_type,dense",
+ [
+ (None, True),
+ (None, False),
+ ("coo", True),
+ ("csr", True),
+ ("csc", True),
+ ("dia", True),
+ ],
+)
+def test_hamiltonian_expectation(backend, dense, density_matrix, sparse_type):
+ """Test Hamiltonian expectation value calculation."""
+ if sparse_type is None:
+ h = hamiltonians.XXZ(nqubits=3, delta=0.5, dense=dense, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ h = hamiltonians.Hamiltonian(
+ 6, random_sparse_matrix(backend, 64, sparse_type), backend=backend
+ )
+
+ matrix = backend.to_numpy(h.matrix)
+ if density_matrix:
+ state = random_density_matrix(2**h.nqubits, backend=backend)
+ state = backend.to_numpy(state)
+ state = state + state.T.conj()
+ norm = np.trace(state)
+ target_ev = np.trace(matrix.dot(state)).real
+ else:
+ state = random_statevector(2**h.nqubits, backend=backend)
+ state = backend.to_numpy(state)
+ norm = np.sum(np.abs(state) ** 2)
+ target_ev = np.sum(state.conj() * matrix.dot(state)).real
+
+ backend.assert_allclose(h.expectation(state), target_ev)
+ backend.assert_allclose(h.expectation(state, True), target_ev / norm)
+
+
+def test_hamiltonian_expectation_errors(backend):
+ h = hamiltonians.XXZ(nqubits=3, delta=0.5, backend=backend)
+ state = np.random.rand(4, 4, 4) + 1j * np.random.rand(4, 4, 4)
+ with pytest.raises(ValueError):
+ h.expectation(state)
+ with pytest.raises(TypeError):
+ h.expectation("test")
+
+
+def test_hamiltonian_expectation_from_samples(backend):
+ """Test Hamiltonian expectation value calculation."""
+ backend.set_seed(12)
+ obs0 = 2 * Z(0) * Z(1) + Z(0) * Z(2)
+ obs1 = 2 * Z(0) * Z(1) + Z(0) * Z(2) * I(3)
+ h0 = hamiltonians.SymbolicHamiltonian(obs0, backend=backend)
+ h1 = hamiltonians.SymbolicHamiltonian(obs1, backend=backend)
+ matrix = backend.to_numpy(h0.matrix)
+ c = Circuit(4)
+ c.add(gates.RX(0, np.random.rand()))
+ c.add(gates.RX(1, np.random.rand()))
+ c.add(gates.RX(2, np.random.rand()))
+ c.add(gates.RX(3, np.random.rand()))
+ c.add(gates.M(0, 1, 2, 3))
+ nshots = 10**5
+ # result = c(nshots=nshots)
+ result = backend.execute_circuit(c, nshots=nshots)
+ freq = result.frequencies(binary=True)
+
+ Obs0 = hamiltonians.Hamiltonian(
+ 3, matrix, backend=backend
+ ).expectation_from_samples(freq, qubit_map=None)
+ Obs0 = backend.cast(Obs0, dtype=Obs0.dtype)
+
+ Obs1 = h1.expectation(result.state())
+ Obs1 = backend.cast(Obs1, dtype=Obs1.dtype)
+
+ backend.assert_allclose(Obs0, Obs1, atol=10 / np.sqrt(nshots))
+
+
+def test_hamiltonian_expectation_from_samples_errors(backend):
+ obs = random_density_matrix(4, backend=backend)
+ h = hamiltonians.Hamiltonian(2, obs, backend=backend)
+ with pytest.raises(NotImplementedError):
+ h.expectation_from_samples(None, qubit_map=None)
+
+
+@pytest.mark.parametrize("dtype", [np.complex128, np.complex64])
+@pytest.mark.parametrize(
+ "sparse_type,dense",
+ [
+ (None, True),
+ (None, False),
+ ("coo", True),
+ ("csr", True),
+ ("csc", True),
+ ("dia", True),
+ ],
+)
+def test_hamiltonian_eigenvalues(backend, dtype, sparse_type, dense):
+ """Testing hamiltonian eigenvalues scaling."""
+ if sparse_type is None:
+ H1 = hamiltonians.XXZ(nqubits=2, delta=0.5, dense=dense, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ from scipy import sparse
+
+ H1 = hamiltonians.XXZ(nqubits=5, delta=0.5, backend=backend)
+ m = getattr(sparse, f"{sparse_type}_matrix")(backend.to_numpy(H1.matrix))
+ H1 = hamiltonians.Hamiltonian(5, m, backend=backend)
+
+ H1_eigen = sorted(backend.to_numpy(H1.eigenvalues()))
+ hH1_eigen = sorted(backend.to_numpy(backend.calculate_eigenvalues(H1.matrix)))
+ backend.assert_allclose(sorted(H1_eigen), hH1_eigen)
+
+ c1 = dtype(2.5)
+ H2 = c1 * H1
+ H2_eigen = sorted(backend.to_numpy(H2._eigenvalues))
+ hH2_eigen = sorted(backend.to_numpy(backend.calculate_eigenvalues(c1 * H1.matrix)))
+ backend.assert_allclose(H2_eigen, hH2_eigen)
+
+ c2 = dtype(-11.1)
+ H3 = H1 * c2
+ if sparse_type is None:
+ H3_eigen = sorted(backend.to_numpy(H3._eigenvalues))
+ hH3_eigen = sorted(
+ backend.to_numpy(backend.calculate_eigenvalues(H1.matrix * c2))
+ )
+ backend.assert_allclose(H3_eigen, hH3_eigen)
+ else:
+ assert H3._eigenvalues is None
+
+
+@pytest.mark.parametrize("dtype", [np.complex128, np.complex64])
+@pytest.mark.parametrize("dense", [True, False])
+def test_hamiltonian_eigenvectors(backend, dtype, dense):
+ """Testing hamiltonian eigenvectors scaling."""
+ H1 = hamiltonians.XXZ(nqubits=2, delta=0.5, dense=dense, backend=backend)
+
+ V1 = backend.to_numpy(H1.eigenvectors())
+ U1 = backend.to_numpy(H1.eigenvalues())
+ backend.assert_allclose(H1.matrix, V1 @ np.diag(U1) @ V1.T)
+
+ c1 = dtype(2.5)
+ H2 = c1 * H1
+ V2 = backend.to_numpy(H2._eigenvectors)
+ U2 = backend.to_numpy(H2._eigenvalues)
+ backend.assert_allclose(H2.matrix, V2 @ np.diag(U2) @ V2.T)
+
+ c2 = dtype(-11.1)
+ H3 = H1 * c2
+ V3 = backend.to_numpy(H3.eigenvectors())
+ U3 = backend.to_numpy(H3._eigenvalues)
+ backend.assert_allclose(H3.matrix, V3 @ np.diag(U3) @ V3.T)
+
+ c3 = dtype(0)
+ H4 = c3 * H1
+ V4 = backend.to_numpy(H4._eigenvectors)
+ U4 = backend.to_numpy(H4._eigenvalues)
+ backend.assert_allclose(H4.matrix, V4 @ np.diag(U4) @ V4.T)
+
+
+@pytest.mark.parametrize(
+ "sparse_type,dense",
+ [
+ (None, True),
+ (None, False),
+ ("coo", True),
+ ("csr", True),
+ ("csc", True),
+ ("dia", True),
+ ],
+)
+def test_hamiltonian_ground_state(backend, sparse_type, dense):
+ """Test Hamiltonian ground state."""
+ if sparse_type is None:
+ H = hamiltonians.XXZ(nqubits=2, delta=0.5, dense=dense, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip("Tensorflow does not support operations with sparse matrices.")
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ from scipy import sparse
+
+ H = hamiltonians.XXZ(nqubits=5, delta=0.5, backend=backend)
+ m = getattr(sparse, f"{sparse_type}_matrix")(backend.to_numpy(H.matrix))
+ H = hamiltonians.Hamiltonian(5, m, backend=backend)
+ V = backend.to_numpy(H.eigenvectors())
+ backend.assert_allclose(H.ground_state(), V[:, 0])
+
+
+@pytest.mark.parametrize(
+ "sparse_type,dense",
+ [
+ (None, True),
+ (None, False),
+ ("coo", True),
+ ("csr", True),
+ ("csc", True),
+ ("dia", True),
+ ],
+)
+def test_hamiltonian_exponentiation(backend, sparse_type, dense):
+ """Test matrix exponentiation of Hamiltonians ``exp(1j * t * H)``."""
+ from scipy.linalg import expm
+
+ def construct_hamiltonian():
+ if sparse_type is None:
+ return hamiltonians.XXZ(nqubits=2, delta=0.5, dense=dense, backend=backend)
+ else:
+ if backend.name == "tensorflow":
+ pytest.skip(
+ "Tensorflow does not support operations with sparse matrices."
+ )
+ elif backend.name == "pytorch":
+ pytest.skip("Pytorch does not support operations with sparse matrices.")
+ from scipy import sparse
+
+ ham = hamiltonians.XXZ(nqubits=5, delta=0.5, backend=backend)
+ m = getattr(sparse, f"{sparse_type}_matrix")(backend.to_numpy(ham.matrix))
+ return hamiltonians.Hamiltonian(5, m, backend=backend)
+
+ H = construct_hamiltonian()
+ target_matrix = expm(-0.5j * backend.to_numpy(H.matrix))
+ H1 = construct_hamiltonian()
+ _ = H1.eigenvectors()
+
+ backend.assert_allclose(H.exp(0.5), target_matrix)
+ backend.assert_allclose(H1.exp(0.5), target_matrix)
+
+
+def test_hamiltonian_energy_fluctuation(backend):
+ """Test energy fluctuation."""
+ # define hamiltonian
+ ham = hamiltonians.XXZ(nqubits=2, backend=backend)
+ # take ground state and zero state
+ ground_state = ham.ground_state()
+ zero_state = backend.np.ones(2**2) / np.sqrt(2**2)
+ # collect energy fluctuations
+ gs_energy_fluctuation = ham.energy_fluctuation(ground_state)
+ zs_energy_fluctuation = ham.energy_fluctuation(zero_state)
+
+ assert np.isclose(backend.to_numpy(gs_energy_fluctuation), 0, atol=1e-5)
+ assert gs_energy_fluctuation < zs_energy_fluctuation
diff --git a/tests/test_hamiltonians_from_symbols.py b/tests/test_hamiltonians_from_symbols.py
new file mode 100644
index 000000000..4e45fec90
--- /dev/null
+++ b/tests/test_hamiltonians_from_symbols.py
@@ -0,0 +1,247 @@
+"""Test dense matrix of Hamiltonians constructed using symbols."""
+
+import pickle
+
+import numpy as np
+import pytest
+import sympy
+
+from qibo import hamiltonians, matrices
+from qibo.backends import NumpyBackend
+from qibo.quantum_info import random_hermitian
+from qibo.symbols import I, Symbol, X, Y, Z
+
+
+@pytest.mark.parametrize("symbol", [I, X, Y, Z])
+def test_symbols_pickling(symbol):
+ symbol = symbol(int(np.random.randint(4)))
+ dumped_symbol = pickle.dumps(symbol)
+ new_symbol = pickle.loads(dumped_symbol)
+ for attr in ("target_qubit", "name", "_gate"):
+ assert getattr(symbol, attr) == getattr(new_symbol, attr)
+ np.testing.assert_allclose(symbol.matrix, new_symbol.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+@pytest.mark.parametrize("hamtype", ["normal", "symbolic"])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_tfim_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms):
+ """Check creating TFIM Hamiltonian using sympy."""
+ if hamtype == "symbolic":
+ h = 0.5
+ symham = sum(Z(i) * Z(i + 1) for i in range(nqubits - 1))
+ symham += Z(0) * Z(nqubits - 1)
+ symham += h * sum(X(i) for i in range(nqubits))
+ ham = hamiltonians.SymbolicHamiltonian(-symham, backend=backend)
+ else:
+ h = 0.5
+ z_symbols = sympy.symbols(" ".join(f"Z{i}" for i in range(nqubits)))
+ x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(nqubits)))
+
+ symham = sum(z_symbols[i] * z_symbols[i + 1] for i in range(nqubits - 1))
+ symham += z_symbols[0] * z_symbols[-1]
+ symham += h * sum(x_symbols)
+ symmap = {z: (i, matrices.Z) for i, z in enumerate(z_symbols)}
+ symmap.update({x: (i, matrices.X) for i, x in enumerate(x_symbols)})
+ ham = hamiltonians.Hamiltonian.from_symbolic(-symham, symmap, backend=backend)
+
+ if calcterms:
+ _ = ham.terms
+ final_matrix = ham.matrix
+ target_matrix = hamiltonians.TFIM(nqubits, h=h, backend=backend).matrix
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("hamtype", ["normal", "symbolic"])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_from_symbolic_with_power(backend, hamtype, calcterms):
+ """Check ``from_symbolic`` when the expression contains powers."""
+ npbackend = NumpyBackend()
+ if hamtype == "symbolic":
+ matrix = random_hermitian(2, backend=npbackend)
+ symham = (
+ Symbol(0, matrix) ** 2
+ - Symbol(1, matrix) ** 2
+ + 3 * Symbol(1, matrix)
+ - 2 * Symbol(0, matrix) * Symbol(2, matrix)
+ + 1
+ )
+ ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+ else:
+ z = sympy.symbols(" ".join(f"Z{i}" for i in range(3)))
+ symham = z[0] ** 2 - z[1] ** 2 + 3 * z[1] - 2 * z[0] * z[2] + 1
+ matrix = random_hermitian(2, backend=npbackend)
+ symmap = {x: (i, matrix) for i, x in enumerate(z)}
+ ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend)
+
+ if calcterms:
+ _ = ham.terms
+
+ final_matrix = ham.matrix
+ matrix2 = matrix.dot(matrix)
+ eye = np.eye(2, dtype=matrix.dtype)
+ target_matrix = np.kron(np.kron(matrix2, eye), eye)
+ target_matrix -= np.kron(np.kron(eye, matrix2), eye)
+ target_matrix += 3 * np.kron(np.kron(eye, matrix), eye)
+ target_matrix -= 2 * np.kron(np.kron(matrix, eye), matrix)
+ target_matrix += np.eye(8, dtype=matrix.dtype)
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("hamtype", ["normal", "symbolic"])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_from_symbolic_with_complex_numbers(backend, hamtype, calcterms):
+ """Check ``from_symbolic`` when the expression contains imaginary unit."""
+ if hamtype == "symbolic":
+ symham = (
+ (1 + 2j) * X(0) * X(1)
+ + 2 * Y(0) * Y(1)
+ - 3j * X(0) * Y(1)
+ + 1j * Y(0) * X(1)
+ )
+ ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+ else:
+ x = sympy.symbols(" ".join(f"X{i}" for i in range(2)))
+ y = sympy.symbols(" ".join(f"Y{i}" for i in range(2)))
+ symham = (
+ (1 + 2j) * x[0] * x[1]
+ + 2 * y[0] * y[1]
+ - 3j * x[0] * y[1]
+ + 1j * y[0] * x[1]
+ )
+ symmap = {s: (i, matrices.X) for i, s in enumerate(x)}
+ symmap.update({s: (i, matrices.Y) for i, s in enumerate(y)})
+ ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend)
+
+ if calcterms:
+ _ = ham.terms
+ final_matrix = ham.matrix
+ target_matrix = (1 + 2j) * np.kron(matrices.X, matrices.X)
+ target_matrix += 2 * np.kron(matrices.Y, matrices.Y)
+ target_matrix -= 3j * np.kron(matrices.X, matrices.Y)
+ target_matrix += 1j * np.kron(matrices.Y, matrices.X)
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_from_symbolic_application_hamiltonian(backend, calcterms):
+ """Check ``from_symbolic`` for a specific four-qubit Hamiltonian."""
+ z1, z2, z3, z4 = sympy.symbols("z1 z2 z3 z4")
+ symmap = {z: (i, matrices.Z) for i, z in enumerate([z1, z2, z3, z4])}
+ symham = (
+ z1 * z2
+ - 0.5 * z1 * z3
+ + 2 * z2 * z3
+ + 0.35 * z2
+ + 0.25 * z3 * z4
+ + 0.5 * z3
+ + z4
+ - z1
+ )
+ # Check that Trotter dense matrix agrees will full Hamiltonian matrix
+ fham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend)
+ symham = (
+ Z(0) * Z(1)
+ - 0.5 * Z(0) * Z(2)
+ + 2 * Z(1) * Z(2)
+ + 0.35 * Z(1)
+ + 0.25 * Z(2) * Z(3)
+ + 0.5 * Z(2)
+ + Z(3)
+ - Z(0)
+ )
+ sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+ if calcterms:
+ _ = sham.terms
+ backend.assert_allclose(sham.matrix, fham.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+@pytest.mark.parametrize("hamtype", ["normal", "symbolic"])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_x_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms):
+ """Check creating sum(X) Hamiltonian using sympy."""
+ if hamtype == "symbolic":
+ symham = -sum(X(i) for i in range(nqubits))
+ ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+ else:
+ x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(nqubits)))
+ symham = -sum(x_symbols)
+ symmap = {x: (i, matrices.X) for i, x in enumerate(x_symbols)}
+ ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend)
+ if calcterms:
+ _ = ham.terms
+ final_matrix = ham.matrix
+ target_matrix = hamiltonians.X(nqubits, backend=backend).matrix
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("hamtype", ["normal", "symbolic"])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype, calcterms):
+ """Check creating Hamiltonian with three-qubit interaction using sympy."""
+ if hamtype == "symbolic":
+ symham = X(0) * Y(1) * Z(2) + 0.5 * Y(0) * Z(1) * X(3) + Z(0) * X(2)
+ symham += Y(2) + 1.5 * Z(1) - 2 - 3 * X(1) * Y(3)
+ ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+ else:
+ x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(4)))
+ y_symbols = sympy.symbols(" ".join(f"Y{i}" for i in range(4)))
+ z_symbols = sympy.symbols(" ".join(f"Z{i}" for i in range(4)))
+ symmap = {x: (i, matrices.X) for i, x in enumerate(x_symbols)}
+ symmap.update({x: (i, matrices.Y) for i, x in enumerate(y_symbols)})
+ symmap.update({x: (i, matrices.Z) for i, x in enumerate(z_symbols)})
+
+ symham = x_symbols[0] * y_symbols[1] * z_symbols[2]
+ symham += 0.5 * y_symbols[0] * z_symbols[1] * x_symbols[3]
+ symham += z_symbols[0] * x_symbols[2]
+ symham += -3 * x_symbols[1] * y_symbols[3]
+ symham += y_symbols[2]
+ symham += 1.5 * z_symbols[1]
+ symham -= 2
+ ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend)
+
+ if calcterms:
+ _ = ham.terms
+ final_matrix = ham.matrix
+ target_matrix = np.kron(
+ np.kron(matrices.X, matrices.Y), np.kron(matrices.Z, matrices.I)
+ )
+ target_matrix += 0.5 * np.kron(
+ np.kron(matrices.Y, matrices.Z), np.kron(matrices.I, matrices.X)
+ )
+ target_matrix += np.kron(
+ np.kron(matrices.Z, matrices.I), np.kron(matrices.X, matrices.I)
+ )
+ target_matrix += -3 * np.kron(
+ np.kron(matrices.I, matrices.X), np.kron(matrices.I, matrices.Y)
+ )
+ target_matrix += np.kron(
+ np.kron(matrices.I, matrices.I), np.kron(matrices.Y, matrices.I)
+ )
+ target_matrix += 1.5 * np.kron(
+ np.kron(matrices.I, matrices.Z), np.kron(matrices.I, matrices.I)
+ )
+ target_matrix -= 2 * np.eye(2**4, dtype=target_matrix.dtype)
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_hamiltonian_with_identity_symbol(backend, calcterms):
+ """Check creating Hamiltonian from expression which contains the identity symbol."""
+ symham = X(0) * I(1) * Z(2) + 0.5 * Y(0) * Z(1) * I(3) + Z(0) * I(1) * X(2)
+ ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend)
+
+ if calcterms:
+ _ = ham.terms
+ final_matrix = ham.matrix
+ target_matrix = np.kron(
+ np.kron(matrices.X, matrices.I), np.kron(matrices.Z, matrices.I)
+ )
+ target_matrix += 0.5 * np.kron(
+ np.kron(matrices.Y, matrices.Z), np.kron(matrices.I, matrices.I)
+ )
+ target_matrix += np.kron(
+ np.kron(matrices.Z, matrices.I), np.kron(matrices.X, matrices.I)
+ )
+ backend.assert_allclose(final_matrix, target_matrix)
diff --git a/tests/test_hamiltonians_models.py b/tests/test_hamiltonians_models.py
new file mode 100644
index 000000000..8539d0e11
--- /dev/null
+++ b/tests/test_hamiltonians_models.py
@@ -0,0 +1,61 @@
+"""Tests methods from `qibo/src/hamiltonians/models.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import hamiltonians, matrices
+
+models_config = [
+ ("TFIM", {"nqubits": 3, "h": 0.0}, "tfim_N3h0.0.out"),
+ ("TFIM", {"nqubits": 3, "h": 0.5}, "tfim_N3h0.5.out"),
+ ("TFIM", {"nqubits": 3, "h": 1.0}, "tfim_N3h1.0.out"),
+ ("XXZ", {"nqubits": 3, "delta": 0.0}, "heisenberg_N3delta0.0.out"),
+ ("XXZ", {"nqubits": 3, "delta": 0.5}, "heisenberg_N3delta0.5.out"),
+ ("XXZ", {"nqubits": 3, "delta": 1.0}, "heisenberg_N3delta1.0.out"),
+ ("X", {"nqubits": 3}, "x_N3.out"),
+ ("Y", {"nqubits": 4}, "y_N4.out"),
+ ("Z", {"nqubits": 5}, "z_N5.out"),
+ ("MaxCut", {"nqubits": 3}, "maxcut_N3.out"),
+ ("MaxCut", {"nqubits": 4}, "maxcut_N4.out"),
+ ("MaxCut", {"nqubits": 5}, "maxcut_N5.out"),
+]
+
+
+@pytest.mark.parametrize(("model", "kwargs", "filename"), models_config)
+def test_hamiltonian_models(backend, model, kwargs, filename):
+ """Test pre-coded Hamiltonian models generate the proper matrices."""
+ from .test_models_variational import assert_regression_fixture
+
+ H = getattr(hamiltonians, model)(**kwargs, backend=backend)
+ matrix = backend.to_numpy(H.matrix).flatten().real
+ assert_regression_fixture(backend, matrix, filename)
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize(
+ "dense,calcterms", [(True, False), (False, False), (False, True)]
+)
+def test_maxcut(backend, nqubits, dense, calcterms):
+ size = 2**nqubits
+ ham = np.zeros(shape=(size, size), dtype=np.complex128)
+ for i in range(nqubits):
+ for j in range(nqubits):
+ h = np.eye(1)
+ for k in range(nqubits):
+ if (k == i) ^ (k == j):
+ h = np.kron(h, matrices.Z)
+ else:
+ h = np.kron(h, matrices.I)
+ M = np.eye(2**nqubits) - h
+ ham += M
+ target_ham = backend.cast(-ham / 2)
+ final_ham = hamiltonians.MaxCut(nqubits, dense, backend=backend)
+ if (not dense) and calcterms:
+ _ = final_ham.terms
+ backend.assert_allclose(final_ham.matrix, target_ham)
+
+
+@pytest.mark.parametrize("model", ["XXZ", "TFIM"])
+def test_missing_neighbour_qubit(backend, model):
+ with pytest.raises(ValueError):
+ H = getattr(hamiltonians, model)(nqubits=1, backend=backend)
diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py
new file mode 100644
index 000000000..9ddc54531
--- /dev/null
+++ b/tests/test_hamiltonians_symbolic.py
@@ -0,0 +1,398 @@
+"""Test methods of :class:`qibo.core.hamiltonians.SymbolicHamiltonian`."""
+
+import numpy as np
+import pytest
+import sympy
+from pytest import approx
+
+from qibo import Circuit, gates, hamiltonians
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+from qibo.symbols import I, Y, Z
+
+
+def symbolic_tfim(nqubits, h=1.0):
+ """Constructs symbolic Hamiltonian for TFIM."""
+ from qibo.symbols import X, Z
+
+ sham = -sum(Z(i) * Z(i + 1) for i in range(nqubits - 1))
+ sham -= Z(0) * Z(nqubits - 1)
+ sham -= h * sum(X(i) for i in range(nqubits))
+ return sham
+
+
+def test_symbolic_hamiltonian_errors(backend):
+ # Wrong type of Symbol matrix
+ from qibo.symbols import Symbol
+
+ with pytest.raises(TypeError):
+ s = Symbol(0, "test")
+ # Wrong type of symbolic expression
+ with pytest.raises(TypeError):
+ ham = hamiltonians.SymbolicHamiltonian("test", backend=backend)
+ # Passing form with symbol that is not in ``symbol_map``
+ from qibo import matrices
+
+ Z, X = sympy.Symbol("Z"), sympy.Symbol("X")
+ symbol_map = {Z: (0, matrices.Z)}
+ with pytest.raises(ValueError):
+ ham = hamiltonians.SymbolicHamiltonian(
+ Z * X, symbol_map=symbol_map, backend=backend
+ )
+ # Invalid operation in Hamiltonian expresion
+ ham = hamiltonians.SymbolicHamiltonian(
+ sympy.cos(Z), symbol_map=symbol_map, backend=backend
+ )
+ with pytest.raises(TypeError):
+ dense = ham.dense
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_symbolictfim_hamiltonian_to_dense(backend, nqubits, calcterms):
+ final_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1), backend=backend
+ )
+ target_ham = hamiltonians.TFIM(nqubits, h=1, backend=backend)
+ if calcterms:
+ _ = final_ham.terms
+ backend.assert_allclose(final_ham.matrix, target_ham.matrix, atol=1e-15)
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_symbolicxxz_hamiltonian_to_dense(backend, nqubits, calcterms):
+ from qibo.symbols import X, Y, Z
+
+ sham = sum(X(i) * X(i + 1) for i in range(nqubits - 1))
+ sham += sum(Y(i) * Y(i + 1) for i in range(nqubits - 1))
+ sham += 0.5 * sum(Z(i) * Z(i + 1) for i in range(nqubits - 1))
+ sham += X(0) * X(nqubits - 1) + Y(0) * Y(nqubits - 1) + 0.5 * Z(0) * Z(nqubits - 1)
+ final_ham = hamiltonians.SymbolicHamiltonian(sham, backend=backend)
+ target_ham = hamiltonians.XXZ(nqubits, backend=backend)
+ if calcterms:
+ _ = final_ham.terms
+ backend.assert_allclose(final_ham.matrix, target_ham.matrix, atol=1e-15)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_scalar_mul(backend, nqubits, calcterms, calcdense):
+ """Test multiplication of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ target_ham = 2 * hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (2 * local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (local_ham * 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [4])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_scalar_add(backend, nqubits, calcterms, calcdense):
+ """Test addition of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ target_ham = 2 + hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (2 + local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (local_ham + 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_scalar_sub(backend, nqubits, calcterms, calcdense):
+ """Test subtraction of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ target_ham = 2 - hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (2 - local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) - 2
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ local_dense = (local_ham - 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_operator_add_and_sub(
+ backend, nqubits, calcterms, calcdense
+):
+ """Test addition and subtraction between Trotter Hamiltonians."""
+ local_ham1 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ local_ham2 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=0.5), backend=backend
+ )
+ if calcterms:
+ _ = local_ham1.terms
+ _ = local_ham2.terms
+ if calcdense:
+ _ = local_ham1.dense
+ _ = local_ham2.dense
+ local_ham = local_ham1 + local_ham2
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) + hamiltonians.TFIM(
+ nqubits, h=0.5, backend=backend
+ )
+ dense = local_ham.dense
+ backend.assert_allclose(dense.matrix, target_ham.matrix)
+
+ local_ham1 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ local_ham2 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=0.5), backend=backend
+ )
+ if calcterms:
+ _ = local_ham1.terms
+ _ = local_ham2.terms
+ if calcdense:
+ _ = local_ham1.dense
+ _ = local_ham2.dense
+ local_ham = local_ham1 - local_ham2
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) - hamiltonians.TFIM(
+ nqubits, h=0.5, backend=backend
+ )
+ dense = local_ham.dense
+ backend.assert_allclose(dense.matrix, target_ham.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [5])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits, calcterms, calcdense):
+ local_ham1 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ local_ham2 = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=0.5), backend=backend
+ )
+ dense_ham1 = hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ dense_ham2 = hamiltonians.TFIM(nqubits, h=0.5, backend=backend)
+ if calcterms:
+ _ = local_ham1.terms
+ _ = local_ham2.terms
+ if calcdense:
+ _ = local_ham1.dense
+ _ = local_ham2.dense
+ local_matmul = local_ham1 @ local_ham2
+ target_matmul = dense_ham1 @ dense_ham2
+ backend.assert_allclose(local_matmul.matrix, target_matmul.matrix)
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix, calcterms):
+ state = (
+ random_density_matrix(2**nqubits, backend=backend)
+ if density_matrix
+ else random_statevector(2**nqubits, backend=backend)
+ )
+ local_ham = hamiltonians.SymbolicHamiltonian(
+ symbolic_tfim(nqubits, h=1.0), backend=backend
+ )
+ dense_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+ local_matmul = local_ham @ state
+ target_matmul = dense_ham @ state
+ backend.assert_allclose(local_matmul, target_matmul)
+
+
+@pytest.mark.parametrize("nqubits,normalize", [(3, False), (4, False)])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_state_expectation(
+ backend, nqubits, normalize, calcterms, calcdense
+):
+ local_ham = (
+ hamiltonians.SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend)
+ + 2
+ )
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+ dense_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) + 2
+
+ state = random_statevector(2**nqubits, backend=backend)
+
+ local_ev = local_ham.expectation(state, normalize)
+ target_ev = dense_ham.expectation(state, normalize)
+ backend.assert_allclose(local_ev, target_ev)
+
+ local_ev = local_ham.expectation(state, normalize)
+ target_ev = dense_ham.expectation(state, normalize)
+ backend.assert_allclose(local_ev, target_ev)
+
+
+@pytest.mark.parametrize("give_nqubits", [False, True])
+@pytest.mark.parametrize("calcterms", [False, True])
+@pytest.mark.parametrize("calcdense", [False, True])
+def test_symbolic_hamiltonian_state_expectation_different_nqubits(
+ backend, give_nqubits, calcterms, calcdense
+):
+ expr = symbolic_tfim(3, h=1.0)
+ if give_nqubits:
+ local_ham = hamiltonians.SymbolicHamiltonian(expr, nqubits=5, backend=backend)
+ else:
+ local_ham = hamiltonians.SymbolicHamiltonian(expr, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+ if calcdense:
+ _ = local_ham.dense
+
+ dense_ham = hamiltonians.TFIM(3, h=1.0, backend=backend)
+ dense_matrix = np.kron(backend.to_numpy(dense_ham.matrix), np.eye(4))
+ dense_ham = hamiltonians.Hamiltonian(5, dense_matrix, backend=backend)
+
+ state = random_statevector(2**5, backend=backend)
+
+ if give_nqubits:
+ local_ev = local_ham.expectation(state)
+ target_ev = dense_ham.expectation(state)
+ backend.assert_allclose(local_ev, target_ev)
+
+ local_ev = local_ham.expectation(state)
+ target_ev = dense_ham.expectation(state)
+ backend.assert_allclose(local_ev, target_ev)
+ else:
+ with pytest.raises(ValueError):
+ local_ev = local_ham.expectation(state)
+ with pytest.raises(ValueError):
+ local_ev = local_ham.expectation(state)
+
+
+def test_hamiltonian_expectation_from_samples(backend):
+ """Test Hamiltonian expectation value calculation."""
+ backend.set_seed(0)
+ obs0 = 2 * Z(0) * Z(1) + Z(0) * Z(2)
+ obs1 = 2 * Z(0) * Z(1) + Z(0) * Z(2) * I(3)
+ h0 = hamiltonians.SymbolicHamiltonian(obs0, backend=backend)
+ h1 = hamiltonians.SymbolicHamiltonian(obs1, backend=backend)
+ c = Circuit(4)
+ c.add(gates.RX(0, np.random.rand()))
+ c.add(gates.RX(1, np.random.rand()))
+ c.add(gates.RX(2, np.random.rand()))
+ c.add(gates.RX(3, np.random.rand()))
+ c.add(gates.M(0, 1, 2, 3))
+ nshots = 10**5
+ result = backend.execute_circuit(c, nshots=nshots)
+ freq = result.frequencies(binary=True)
+ ev0 = h0.expectation_from_samples(freq, qubit_map=None)
+ ev1 = h1.expectation(result.state())
+ backend.assert_allclose(ev0, ev1, atol=20 / np.sqrt(nshots))
+
+
+def test_hamiltonian_expectation_from_samples_errors(backend):
+ obs = [Z(0) * Y(1), Z(0) * Z(1) ** 3]
+ h1 = hamiltonians.SymbolicHamiltonian(obs[0], backend=backend)
+ h2 = hamiltonians.SymbolicHamiltonian(obs[1], backend=backend)
+ with pytest.raises(NotImplementedError):
+ h1.expectation_from_samples(None, qubit_map=None)
+ with pytest.raises(NotImplementedError):
+ h2.expectation_from_samples(None, qubit_map=None)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("calcterms", [False, True])
+def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix, calcterms):
+ from qibo.symbols import Symbol, X
+
+ matrix = np.random.random((2, 2))
+ form = X(0) * Symbol(1, matrix) + Symbol(0, matrix) * X(1)
+ local_ham = hamiltonians.SymbolicHamiltonian(form, backend=backend)
+ if calcterms:
+ _ = local_ham.terms
+
+ state = (
+ random_density_matrix(4, backend=backend)
+ if density_matrix
+ else random_statevector(4, backend=backend)
+ )
+ local_ev = local_ham.expectation(state)
+ target_ev = local_ham.dense.expectation(state)
+ backend.assert_allclose(local_ev, target_ev)
+
+
+def test_trotter_hamiltonian_operation_errors(backend):
+ """Test errors in ``SymbolicHamiltonian`` addition and subtraction."""
+ h1 = hamiltonians.SymbolicHamiltonian(symbolic_tfim(3, h=1.0), backend=backend)
+ h2 = hamiltonians.SymbolicHamiltonian(symbolic_tfim(4, h=1.0), backend=backend)
+ with pytest.raises(RuntimeError):
+ h = h1 + h2
+ with pytest.raises(RuntimeError):
+ h = h1 - h2
+ with pytest.raises(NotImplementedError):
+ h = h1 + "test"
+ with pytest.raises(NotImplementedError):
+ h = "test" + h1
+ with pytest.raises(NotImplementedError):
+ h = h1 - "test"
+ with pytest.raises(NotImplementedError):
+ h = "test" - h1
+ with pytest.raises(NotImplementedError):
+ h = h1 * "test"
+ with pytest.raises(NotImplementedError):
+ h = h1 @ "test"
+ with pytest.raises(NotImplementedError):
+ h = h1 @ np.ones((2, 2, 2, 2))
+ h2 = hamiltonians.XXZ(3, dense=False, backend=backend)
+ with pytest.raises(NotImplementedError):
+ h = h1 @ h2
+
+
+def test_symbolic_hamiltonian_with_constant(backend):
+ c = Circuit(1)
+ c.add(gates.H(0))
+ c.add(gates.M(0))
+ h = hamiltonians.SymbolicHamiltonian(1e6 - Z(0), backend=backend)
+
+ result = c.execute(nshots=10000)
+ assert result.expectation_from_samples(h) == approx(1e6, rel=1e-5, abs=0.0)
diff --git a/tests/test_hamiltonians_terms.py b/tests/test_hamiltonians_terms.py
new file mode 100644
index 000000000..8cf6f1077
--- /dev/null
+++ b/tests/test_hamiltonians_terms.py
@@ -0,0 +1,239 @@
+"""Tests methods defined in `qibo/core/terms.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import gates, matrices, models
+from qibo.hamiltonians import terms
+from qibo.quantum_info import random_density_matrix, random_statevector
+
+
+def test_hamiltonian_term_initialization(backend):
+ """Test initialization and matrix assignment of ``HamiltonianTerm``."""
+ matrix = np.random.random((2, 2))
+ term = terms.HamiltonianTerm(matrix, 0)
+ assert term.target_qubits == (0,)
+ backend.assert_allclose(term.matrix, matrix)
+ matrix = np.random.random((4, 4))
+ term = terms.HamiltonianTerm(matrix, 2, 3)
+ assert term.target_qubits == (2, 3)
+ backend.assert_allclose(term.matrix, matrix)
+
+
+def test_hamiltonian_term_initialization_errors():
+ """Test initializing ``HamiltonianTerm`` with wrong parameters."""
+ # Wrong HamiltonianTerm matrix
+ with pytest.raises(TypeError):
+ t = terms.HamiltonianTerm("test", 0, 1)
+ # Passing negative target qubits in HamiltonianTerm
+ with pytest.raises(ValueError):
+ t = terms.HamiltonianTerm("test", 0, -1)
+ # Passing matrix shape incompatible with number of qubits
+ with pytest.raises(ValueError):
+ t = terms.HamiltonianTerm(np.random.random((4, 4)), 0, 1, 2)
+ # Merging terms with invalid qubits
+ t1 = terms.HamiltonianTerm(np.random.random((4, 4)), 0, 1)
+ t2 = terms.HamiltonianTerm(np.random.random((4, 4)), 1, 2)
+ with pytest.raises(ValueError):
+ t = t1.merge(t2)
+
+
+def test_hamiltonian_term_gates(backend):
+ """Test gate application of ``HamiltonianTerm``."""
+ nqubits = 4
+ matrix = np.random.random((nqubits, nqubits))
+ term = terms.HamiltonianTerm(matrix, 1, 2)
+ gate = term.gate
+ assert gate.target_qubits == (1, 2)
+ backend.assert_allclose(gate.matrix(backend), matrix)
+
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ final_state = term(backend, backend.np.copy(initial_state), nqubits)
+ c = models.Circuit(nqubits)
+ c.add(gates.Unitary(matrix, 1, 2))
+ target_state = backend.execute_circuit(c, backend.np.copy(initial_state)).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_hamiltonian_term_exponentiation(backend):
+ """Test exp gate application of ``HamiltonianTerm``."""
+ from scipy.linalg import expm
+
+ matrix = np.random.random((2, 2))
+ term = terms.HamiltonianTerm(matrix, 1)
+ exp_matrix = expm(-0.5j * matrix)
+ backend.assert_allclose(term.exp(0.5), exp_matrix)
+
+ initial_state = random_statevector(4, backend=backend)
+ final_state = term(backend, backend.np.copy(initial_state), 2, term.expgate(0.5))
+ exp_gate = gates.Unitary(exp_matrix, 1)
+ target_state = backend.apply_gate(exp_gate, backend.np.copy(initial_state), 2)
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_hamiltonian_term_mul(backend):
+ """Test scalar multiplication of ``HamiltonianTerm``."""
+ matrix = np.random.random((4, 4))
+ term = terms.HamiltonianTerm(matrix, 0, 2)
+ mterm = 2 * term
+ assert mterm.target_qubits == term.target_qubits
+ backend.assert_allclose(mterm.matrix, 2 * matrix)
+ mterm = term * 5
+ assert mterm.target_qubits == term.target_qubits
+ backend.assert_allclose(mterm.matrix, 5 * matrix)
+
+
+def test_hamiltonian_term_merge(backend):
+ """Test ``HamiltonianTerm.merge``."""
+ matrix1 = np.random.random((2, 2))
+ matrix2 = np.random.random((4, 4))
+ term1 = terms.HamiltonianTerm(matrix1, 1)
+ term2 = terms.HamiltonianTerm(matrix2, 0, 1)
+ mterm = term2.merge(term1)
+ target_matrix = np.kron(np.eye(2), matrix1) + matrix2
+ assert mterm.target_qubits == (0, 1)
+ backend.assert_allclose(mterm.matrix, target_matrix)
+ with pytest.raises(ValueError):
+ term1.merge(term2)
+
+
+@pytest.mark.parametrize("use_symbols", [True, False])
+def test_symbolic_term_creation(backend, use_symbols):
+ """Test creating ``SymbolicTerm`` from sympy expression."""
+ if use_symbols:
+ from qibo.symbols import X, Y
+
+ expression = X(0) * Y(1) * X(1)
+ symbol_map = {}
+ else:
+ import sympy
+
+ x0, x1, y1 = sympy.symbols("X0 X1 Y1", commutative=False)
+ expression = x0 * y1 * x1
+ symbol_map = {x0: (0, matrices.X), x1: (1, matrices.X), y1: (1, matrices.Y)}
+ term = terms.SymbolicTerm(2, expression, symbol_map)
+ assert term.target_qubits == (0, 1)
+ assert len(term.matrix_map) == 2
+ backend.assert_allclose(term.matrix_map.get(0)[0], matrices.X)
+ backend.assert_allclose(term.matrix_map.get(1)[0], matrices.Y)
+ backend.assert_allclose(term.matrix_map.get(1)[1], matrices.X)
+
+
+def test_symbolic_term_with_power_creation(backend):
+ """Test creating ``SymbolicTerm`` from sympy expression that contains powers."""
+ from qibo.symbols import X, Z
+
+ expression = X(0) ** 4 * Z(1) ** 2 * X(2)
+ term = terms.SymbolicTerm(2, expression)
+ assert term.target_qubits == (0, 1, 2)
+ assert len(term.matrix_map) == 3
+ assert term.coefficient == 2
+ backend.assert_allclose(term.matrix_map.get(0), 4 * [matrices.X])
+ backend.assert_allclose(term.matrix_map.get(1), 2 * [matrices.Z])
+ backend.assert_allclose(term.matrix_map.get(2), [matrices.X])
+
+
+def test_symbolic_term_with_imag_creation(backend):
+ """Test creating ``SymbolicTerm`` from sympy expression that contains imaginary coefficients."""
+ from qibo.symbols import Y
+
+ expression = 3j * Y(0)
+ term = terms.SymbolicTerm(2, expression)
+ assert term.target_qubits == (0,)
+ assert term.coefficient == 6j
+
+
+def test_symbolic_term_matrix(backend):
+ """Test matrix calculation of ``SymbolicTerm``."""
+ from qibo.symbols import X, Y, Z
+
+ expression = X(0) * Y(1) * Z(2) * X(1)
+ term = terms.SymbolicTerm(2, expression)
+ assert term.target_qubits == (0, 1, 2)
+ target_matrix = np.kron(matrices.X, matrices.Y @ matrices.X)
+ target_matrix = 2 * np.kron(target_matrix, matrices.Z)
+ backend.assert_allclose(term.matrix, target_matrix)
+
+
+def test_symbolic_term_mul(backend):
+ """Test multiplying scalar to ``SymbolicTerm``."""
+ from qibo.symbols import X, Y, Z
+
+ expression = Y(2) * Z(3) * X(2) * X(3)
+ term = terms.SymbolicTerm(1, expression)
+ assert term.target_qubits == (2, 3)
+ target_matrix = np.kron(matrices.Y @ matrices.X, matrices.Z @ matrices.X)
+ backend.assert_allclose(term.matrix, target_matrix)
+ backend.assert_allclose((2 * term).matrix, 2 * target_matrix)
+ backend.assert_allclose((term * 3).matrix, 3 * target_matrix)
+
+
+@pytest.mark.parametrize("density_matrix", [False])
+def test_symbolic_term_call(backend, density_matrix):
+ """Test applying ``SymbolicTerm`` to state."""
+ from qibo.symbols import X, Y, Z
+
+ expression = Z(0) * X(1) * Y(2)
+ term = terms.SymbolicTerm(2, expression)
+ matrixlist = [
+ np.kron(matrices.Z, np.eye(4)),
+ np.kron(np.kron(np.eye(2), matrices.X), np.eye(2)),
+ np.kron(np.eye(4), matrices.Y),
+ ]
+ matrixlist = backend.cast(matrixlist)
+ initial_state = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ final_state = term(
+ backend, backend.np.copy(initial_state), 3, density_matrix=density_matrix
+ )
+ target_state = 2 * backend.np.copy(initial_state)
+ for matrix in matrixlist:
+ target_state = matrix @ target_state
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_symbolic_term_merge(backend):
+ """Test merging ``SymbolicTerm`` to ``HamiltonianTerm``."""
+ from qibo.symbols import X, Z
+
+ matrix = np.random.random((4, 4))
+ term1 = terms.HamiltonianTerm(matrix, 0, 1)
+ term2 = terms.SymbolicTerm(1, X(0) * Z(1))
+ term = term1.merge(term2)
+ target_matrix = matrix + np.kron(matrices.X, matrices.Z)
+ backend.assert_allclose(term.matrix, target_matrix)
+
+
+def test_term_group_append():
+ """Test ``GroupTerm.can_append`` method."""
+ term1 = terms.HamiltonianTerm(np.random.random((8, 8)), 0, 1, 3)
+ term2 = terms.HamiltonianTerm(np.random.random((2, 2)), 0)
+ term3 = terms.HamiltonianTerm(np.random.random((2, 2)), 1)
+ term4 = terms.HamiltonianTerm(np.random.random((2, 2)), 2)
+ group = terms.TermGroup(term1)
+ assert group.can_append(term2)
+ assert group.can_append(term3)
+ assert not group.can_append(term4)
+ group.append(term2)
+ group.append(term3)
+ assert group.target_qubits == {0, 1, 3}
+
+
+def test_term_group_to_term(backend):
+ """Test ``GroupTerm.term`` property."""
+ from qibo.symbols import X, Z
+
+ matrix = np.random.random((8, 8))
+ term1 = terms.HamiltonianTerm(matrix, 0, 1, 3)
+ term2 = terms.SymbolicTerm(1, X(0) * Z(3))
+ term3 = terms.SymbolicTerm(2, X(1))
+ group = terms.TermGroup(term1)
+ group.append(term2)
+ group.append(term3)
+ matrix2 = np.kron(np.kron(matrices.X, np.eye(2)), matrices.Z)
+ matrix3 = np.kron(np.kron(np.eye(2), matrices.X), np.eye(2))
+ target_matrix = matrix + matrix2 + 2 * matrix3
+ backend.assert_allclose(group.term.matrix, target_matrix)
diff --git a/tests/test_hamiltonians_trotter.py b/tests/test_hamiltonians_trotter.py
new file mode 100644
index 000000000..0c2d2f175
--- /dev/null
+++ b/tests/test_hamiltonians_trotter.py
@@ -0,0 +1,163 @@
+"""Test Trotter Hamiltonian methods from `qibo/core/hamiltonians.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import hamiltonians, symbols
+from qibo.backends import NumpyBackend
+from qibo.quantum_info import random_hermitian, random_statevector
+
+
+@pytest.mark.parametrize("nqubits", [3, 4])
+@pytest.mark.parametrize("model", ["TFIM", "XXZ", "Y", "MaxCut"])
+def test_trotter_hamiltonian_to_dense(backend, nqubits, model):
+ """Test that Trotter Hamiltonian dense form agrees with normal Hamiltonian."""
+ local_ham = getattr(hamiltonians, model)(nqubits, dense=False, backend=backend)
+ target_ham = getattr(hamiltonians, model)(nqubits, backend=backend)
+ final_ham = local_ham.dense
+ backend.assert_allclose(final_ham.matrix, target_ham.matrix, atol=1e-15)
+
+
+def test_trotter_hamiltonian_scalar_mul(backend, nqubits=3):
+ """Test multiplication of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ target_ham = 2 * hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ local_dense = (2 * local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ local_dense = (local_ham * 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+def test_trotter_hamiltonian_scalar_add(backend, nqubits=4):
+ """Test addition of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ target_ham = 2 + hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ local_dense = (2 + local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ local_dense = (local_ham + 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+def test_trotter_hamiltonian_scalar_sub(backend, nqubits=3):
+ """Test subtraction of Trotter Hamiltonian with scalar."""
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ target_ham = 2 - hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+ local_dense = (2 - local_ham).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) - 2
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ local_dense = (local_ham - 2).dense
+ backend.assert_allclose(local_dense.matrix, target_ham.matrix)
+
+
+def test_trotter_hamiltonian_operator_add_and_sub(backend, nqubits=3):
+ """Test addition and subtraction between Trotter Hamiltonians."""
+ local_ham1 = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ local_ham2 = hamiltonians.TFIM(nqubits, h=0.5, dense=False, backend=backend)
+
+ local_ham = local_ham1 + local_ham2
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) + hamiltonians.TFIM(
+ nqubits, h=0.5, backend=backend
+ )
+ dense = local_ham.dense
+ backend.assert_allclose(dense.matrix, target_ham.matrix)
+
+ local_ham = local_ham1 - local_ham2
+ target_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend) - hamiltonians.TFIM(
+ nqubits, h=0.5, backend=backend
+ )
+ dense = local_ham.dense
+ backend.assert_allclose(dense.matrix, target_ham.matrix)
+
+
+@pytest.mark.parametrize("nqubits,normalize", [(3, False), (4, False)])
+def test_trotter_hamiltonian_matmul(backend, nqubits, normalize):
+ """Test Trotter Hamiltonian expectation value."""
+ state = random_statevector(2**nqubits, backend=backend)
+
+ local_ham = hamiltonians.TFIM(nqubits, h=1.0, dense=False, backend=backend)
+ dense_ham = hamiltonians.TFIM(nqubits, h=1.0, backend=backend)
+
+ trotter_ev = local_ham.expectation(state, normalize)
+ target_ev = dense_ham.expectation(state, normalize)
+ backend.assert_allclose(trotter_ev, target_ev)
+
+ trotter_ev = local_ham.expectation(state, normalize)
+ target_ev = dense_ham.expectation(state, normalize)
+ backend.assert_allclose(trotter_ev, target_ev)
+
+ trotter_matmul = local_ham @ state
+ target_matmul = dense_ham @ state
+ backend.assert_allclose(trotter_matmul, target_matmul)
+
+
+def test_trotter_hamiltonian_three_qubit_term(backend):
+ """Test creating ``TrotterHamiltonian`` with three qubit term."""
+ from scipy.linalg import expm
+
+ from qibo.hamiltonians.terms import HamiltonianTerm
+
+ numpy_backend = NumpyBackend()
+
+ m1 = random_hermitian(2**3, backend=numpy_backend)
+ m2 = random_hermitian(2**2, backend=numpy_backend)
+ m3 = random_hermitian(2**1, backend=numpy_backend)
+
+ terms = [
+ HamiltonianTerm(m1, 0, 1, 2),
+ HamiltonianTerm(m2, 2, 3),
+ HamiltonianTerm(m3, 1),
+ ]
+ m1 = backend.cast(m1, dtype=m1.dtype)
+ m2 = backend.cast(m2, dtype=m2.dtype)
+ m3 = backend.cast(m3, dtype=m3.dtype)
+
+ ham = hamiltonians.SymbolicHamiltonian(backend=backend)
+ ham.terms = terms
+
+ # Test that the `TrotterHamiltonian` dense matrix is correct
+ eye = np.eye(2, dtype=complex)
+ eye = backend.cast(eye, dtype=eye.dtype)
+ mm1 = backend.np.kron(m1, eye)
+ mm2 = backend.np.kron(backend.np.kron(eye, eye), m2)
+ mm3 = backend.np.kron(backend.np.kron(eye, m3), backend.np.kron(eye, eye))
+ target_ham = hamiltonians.Hamiltonian(4, mm1 + mm2 + mm3, backend=backend)
+ backend.assert_allclose(ham.matrix, target_ham.matrix)
+
+ dt = 1e-2
+ initial_state = random_statevector(2**4, backend=backend)
+ circuit = ham.circuit(dt=dt)
+ final_state = backend.execute_circuit(
+ circuit, backend.np.copy(initial_state)
+ ).state()
+ mm1 = backend.to_numpy(mm1)
+ mm2 = backend.to_numpy(mm2)
+ mm3 = backend.to_numpy(mm3)
+ u = [expm(-0.5j * dt * (mm1 + mm3)), expm(-0.5j * dt * mm2)]
+ u = backend.cast(u)
+ target_state = backend.np.matmul(u[1], backend.np.matmul(u[0], initial_state))
+ target_state = backend.np.matmul(u[0], backend.np.matmul(u[1], target_state))
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_symbolic_hamiltonian_circuit_different_dts(backend):
+ """Issue: https://github.com/qiboteam/qibo/issues/1357."""
+ ham = hamiltonians.SymbolicHamiltonian(symbols.Z(0))
+ a = ham.circuit(0.1)
+ b = ham.circuit(0.1)
+ matrix1 = ham.circuit(0.2).unitary(backend)
+ matrix2 = (a + b).unitary(backend)
+ backend.assert_allclose(matrix1, matrix2)
+
+
+def test_old_trotter_hamiltonian_errors():
+ """Check errors when creating the deprecated ``TrotterHamiltonian`` object."""
+ with pytest.raises(NotImplementedError):
+ h = hamiltonians.TrotterHamiltonian()
+ with pytest.raises(NotImplementedError):
+ h = hamiltonians.TrotterHamiltonian.from_symbolic(0, 1)
diff --git a/tests/test_measurements.py b/tests/test_measurements.py
new file mode 100644
index 000000000..050d2c197
--- /dev/null
+++ b/tests/test_measurements.py
@@ -0,0 +1,475 @@
+"""Test circuit result measurements and measurement gate and as part of circuit."""
+
+import pickle
+
+import numpy as np
+import pytest
+
+from qibo import gates, models
+
+
+def assert_result(
+ backend,
+ result,
+ decimal_samples=None,
+ binary_samples=None,
+ decimal_frequencies=None,
+ binary_frequencies=None,
+):
+ if decimal_frequencies is not None:
+ assert result.frequencies(False) == decimal_frequencies
+ if binary_frequencies is not None:
+ assert result.frequencies(True) == binary_frequencies
+ if decimal_samples is not None:
+ backend.assert_allclose(result.samples(False), decimal_samples)
+ if binary_samples is not None:
+ backend.assert_allclose(result.samples(True), binary_samples)
+
+
+def assert_dicts_equal(backend, d1, d2):
+ assert d1.keys() == d2.keys()
+ for k, v in d1.items():
+ if isinstance(v, dict):
+ assert v == d2[k]
+ else:
+ backend.assert_allclose(v, d2[k])
+
+
+def assert_register_result(
+ backend,
+ result,
+ decimal_samples=None,
+ binary_samples=None,
+ decimal_frequencies=None,
+ binary_frequencies=None,
+):
+ if decimal_samples:
+ register_result = result.samples(binary=False, registers=True)
+ assert_dicts_equal(backend, register_result, decimal_samples)
+ if binary_samples:
+ register_result = result.samples(binary=True, registers=True)
+ assert_dicts_equal(backend, register_result, binary_samples)
+ if decimal_frequencies:
+ register_result = result.frequencies(binary=False, registers=True)
+ assert_dicts_equal(backend, register_result, decimal_frequencies)
+ if binary_frequencies:
+ register_result = result.frequencies(binary=True, registers=True)
+ assert_dicts_equal(backend, register_result, binary_frequencies)
+
+
+@pytest.mark.parametrize("n", [0, 1])
+@pytest.mark.parametrize("nshots", [100, 1000000])
+def test_measurement_gate(backend, n, nshots):
+ c = models.Circuit(2)
+ if n:
+ c.add(gates.X(1))
+ c.add(gates.M(1))
+ result = backend.execute_circuit(c, nshots=nshots)
+ assert_result(
+ backend,
+ result,
+ n * np.ones(nshots),
+ n * np.ones((nshots, 1)),
+ {n: nshots},
+ {str(n): nshots},
+ )
+
+
+def test_multiple_qubit_measurement_gate(backend):
+ c = models.Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.M(0, 1))
+ result = backend.execute_circuit(c, nshots=100)
+ target_binary_samples = np.zeros((100, 2))
+ target_binary_samples[:, 0] = 1
+ assert_result(
+ backend,
+ result,
+ 2 * np.ones((100,)),
+ target_binary_samples,
+ {2: 100},
+ {"10": 100},
+ )
+
+
+def test_measurement_gate_errors(backend):
+ gate = gates.M(0)
+ # attempting to use `controlled_by`
+ with pytest.raises(NotImplementedError):
+ gate.controlled_by(1)
+ # attempting to construct unitary
+ with pytest.raises(NotImplementedError):
+ matrix = gate.matrix(backend)
+
+
+def test_measurement_circuit(backend, accelerators):
+ c = models.Circuit(4, accelerators)
+ c.add(gates.X(0))
+ c.add(gates.M(0))
+ result = backend.execute_circuit(c, nshots=100)
+ assert_result(
+ backend, result, np.ones((100,)), np.ones((100, 1)), {1: 100}, {"1": 100}
+ )
+
+
+@pytest.mark.parametrize("registers", [False, True])
+def test_measurement_qubit_order_simple(backend, registers):
+ c = models.Circuit(2)
+ c.add(gates.X(0))
+ if registers:
+ c.add(gates.M(1, 0))
+ else:
+ c.add(gates.M(1))
+ c.add(gates.M(0))
+ result = backend.execute_circuit(c, nshots=100)
+
+ target_binary_samples = np.zeros((100, 2))
+ target_binary_samples[:, 1] = 1
+ assert_result(
+ backend, result, np.ones(100), target_binary_samples, {1: 100}, {"01": 100}
+ )
+
+
+@pytest.mark.parametrize("nshots", [100, 1000000])
+def test_measurement_qubit_order(backend, accelerators, nshots):
+ c = models.Circuit(6, accelerators)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+ c.add(gates.M(1, 5, 2, 0))
+ result = backend.execute_circuit(c, nshots=nshots)
+
+ target_binary_samples = np.zeros((nshots, 4))
+ target_binary_samples[:, 0] = 1
+ target_binary_samples[:, 3] = 1
+ assert_result(
+ backend,
+ result,
+ 9 * np.ones(nshots),
+ target_binary_samples,
+ {9: nshots},
+ {"1001": nshots},
+ )
+
+
+def test_multiple_measurement_gates_circuit(backend):
+ c = models.Circuit(4)
+ c.add(gates.X(1))
+ c.add(gates.X(2))
+ c.add(gates.M(0, 1))
+ c.add(gates.M(2))
+ c.add(gates.X(3))
+ result = backend.execute_circuit(c, nshots=100)
+
+ target_binary_samples = np.ones((100, 3))
+ target_binary_samples[:, 0] = 0
+ assert_result(
+ backend, result, 3 * np.ones(100), target_binary_samples, {3: 100}, {"011": 100}
+ )
+
+
+def test_circuit_with_unmeasured_qubits(backend, accelerators):
+ c = models.Circuit(5, accelerators)
+ c.add(gates.X(4))
+ c.add(gates.X(2))
+ c.add(gates.M(0, 2))
+ c.add(gates.X(3))
+ c.add(gates.M(1, 4))
+ result = backend.execute_circuit(c, nshots=100)
+
+ target_binary_samples = np.zeros((100, 4))
+ target_binary_samples[:, 1] = 1
+ target_binary_samples[:, 3] = 1
+ assert_result(
+ backend,
+ result,
+ 5 * np.ones(100),
+ target_binary_samples,
+ {5: 100},
+ {"0101": 100},
+ )
+
+
+def test_circuit_addition_with_measurements(backend):
+ c = models.Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+
+ meas_c = models.Circuit(2)
+ c.add(gates.M(0, 1))
+
+ c += meas_c
+ result = backend.execute_circuit(c, nshots=100)
+
+ assert_result(
+ backend,
+ result,
+ 3 * np.ones(100),
+ np.ones((100, 2)),
+ {3: 100},
+ {"11": 100},
+ )
+
+
+def test_circuit_addition_with_measurements_in_both_circuits(backend, accelerators):
+ c1 = models.Circuit(4, accelerators)
+ c1.add(gates.X(0))
+ c1.add(gates.X(1))
+ c1.add(gates.M(1, register_name="a"))
+
+ c2 = models.Circuit(4, accelerators)
+ c2.add(gates.X(0))
+ c2.add(gates.M(0, register_name="b"))
+
+ c = c1 + c2
+ result = backend.execute_circuit(c, nshots=100)
+ assert_result(
+ backend,
+ result,
+ binary_frequencies={"10": 100},
+ )
+
+
+def test_circuit_copy_with_measurements(backend, accelerators):
+ c1 = models.Circuit(6, accelerators)
+ c1.add([gates.X(0), gates.X(1), gates.X(3)])
+ c1.add(gates.M(5, 1, 3, register_name="a"))
+ c1.add(gates.M(2, 0, register_name="b"))
+ c2 = c1.copy(deep=True)
+
+ r1 = backend.execute_circuit(c1, nshots=100)
+ r2 = backend.execute_circuit(c2, nshots=100)
+
+ backend.assert_allclose(r1.samples(), r2.samples())
+ rg1 = r1.frequencies(registers=True)
+ rg2 = r2.frequencies(registers=True)
+ assert rg1.keys() == rg2.keys()
+ for k in rg1.keys():
+ assert rg1[k] == rg2[k]
+
+
+def test_measurement_compiled_circuit(backend):
+ c = models.Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.M(0))
+ c.add(gates.M(1))
+ c.compile(backend)
+ result = c(nshots=100)
+ target_binary_samples = np.zeros((100, 2))
+ target_binary_samples[:, 0] = 1
+ assert_result(
+ backend,
+ result,
+ 2 * np.ones((100,)),
+ target_binary_samples,
+ {2: 100},
+ {"10": 100},
+ )
+
+ target_state = np.zeros_like(c.final_state.state())
+ target_state[2] = 1
+ backend.assert_allclose(c.final_state._state, target_state)
+
+
+def test_final_state(backend, accelerators):
+ """Check that final state is logged correctly when using measurements."""
+ c = models.Circuit(4, accelerators)
+ c.add(gates.X(1))
+ c.add(gates.X(2))
+ c.add(gates.M(0, 1))
+ c.add(gates.M(2))
+ c.add(gates.X(3))
+ result = backend.execute_circuit(c, nshots=100)
+ c = models.Circuit(4, accelerators)
+ c.add(gates.X(1))
+ c.add(gates.X(2))
+ c.add(gates.X(3))
+ target_state = backend.execute_circuit(c)
+ backend.assert_allclose(c.final_state, target_state)
+
+
+def test_measurement_gate_bitflip_errors():
+ gate = gates.M(0, 1, p0=2 * [0.1])
+ with pytest.raises(ValueError):
+ gate = gates.M(0, 1, p0=4 * [0.1])
+ with pytest.raises(KeyError):
+ gate = gates.M(0, 1, p0={0: 0.1, 2: 0.2})
+ with pytest.raises(TypeError):
+ gate = gates.M(0, 1, p0="test")
+
+
+def test_register_measurements(backend):
+ c = models.Circuit(3)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+ c.add(gates.M(0, 2))
+ c.add(gates.M(1))
+ result = backend.execute_circuit(c, nshots=100)
+
+ decimal_samples = {"register0": 2 * np.ones((100,)), "register1": np.ones((100,))}
+ binary_samples = {"register0": np.zeros((100, 2)), "register1": np.ones((100, 1))}
+ binary_samples["register0"][:, 0] = 1
+ decimal_frequencies = {"register0": {2: 100}, "register1": {1: 100}}
+ binary_frequencies = {"register0": {"10": 100}, "register1": {"1": 100}}
+ assert_register_result(
+ backend,
+ result,
+ decimal_samples,
+ binary_samples,
+ decimal_frequencies,
+ binary_frequencies,
+ )
+
+
+def test_measurement_qubit_order_multiple_registers(backend, accelerators):
+ c = models.Circuit(6, accelerators)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+ c.add(gates.X(3))
+ c.add(gates.M(5, 1, 3, register_name="a"))
+ c.add(gates.M(2, 0, register_name="b"))
+ result = backend.execute_circuit(c, nshots=100)
+
+ # Check full result
+ target_binary_samples = np.zeros((100, 5))
+ target_binary_samples[:, 1] = 1
+ target_binary_samples[:, 2] = 1
+ target_binary_samples[:, 4] = 1
+ assert_result(
+ backend,
+ result,
+ 13 * np.ones((100,)),
+ target_binary_samples,
+ {13: 100},
+ {"01101": 100},
+ )
+
+ decimal_samples = {"a": 3 * np.ones((100,)), "b": np.ones((100,))}
+ binary_samples = {"a": np.zeros((100, 3)), "b": np.zeros((100, 2))}
+ binary_samples["a"][:, 1] = 1
+ binary_samples["a"][:, 2] = 1
+ binary_samples["b"][:, 1] = 1
+ decimal_frequencies = {"a": {3: 100}, "b": {1: 100}}
+ binary_frequencies = {"a": {"011": 100}, "b": {"01": 100}}
+ assert_register_result(
+ backend,
+ result,
+ decimal_samples,
+ binary_samples,
+ decimal_frequencies,
+ binary_frequencies,
+ )
+
+
+def test_registers_in_circuit_with_unmeasured_qubits(backend, accelerators):
+ """Check that register measurements are unaffected by unmeasured qubits."""
+ c = models.Circuit(5, accelerators)
+ c.add(gates.X(1))
+ c.add(gates.X(2))
+ c.add(gates.M(0, 2, register_name="A"))
+ c.add(gates.X(3))
+ c.add(gates.M(1, 4, register_name="B"))
+ result = backend.execute_circuit(c, nshots=100)
+
+ target = {}
+ decimal_samples = {"A": np.ones((100,)), "B": 2 * np.ones((100,))}
+ binary_samples = {"A": np.zeros((100, 2)), "B": np.zeros((100, 2))}
+ binary_samples["A"][:, 1] = 1
+ binary_samples["B"][:, 0] = 1
+ decimal_frequencies = {"A": {1: 100}, "B": {2: 100}}
+ binary_frequencies = {"A": {"01": 100}, "B": {"10": 100}}
+ assert_register_result(
+ backend,
+ result,
+ decimal_samples,
+ binary_samples,
+ decimal_frequencies,
+ binary_frequencies,
+ )
+
+
+def test_measurement_density_matrix(backend):
+ c = models.Circuit(2, density_matrix=True)
+ c.add(gates.X(0))
+ c.add(gates.M(0, 1))
+ result = backend.execute_circuit(c, nshots=100)
+ target_binary_samples = np.zeros((100, 2))
+ target_binary_samples[:, 0] = 1
+ assert_result(
+ backend,
+ result,
+ decimal_samples=2 * np.ones((100,)),
+ binary_samples=target_binary_samples,
+ decimal_frequencies={2: 100},
+ binary_frequencies={"10": 100},
+ )
+
+
+def test_measurement_result_vs_circuit_result(backend, accelerators):
+ c = models.Circuit(6, accelerators)
+ c.add([gates.X(0), gates.X(1), gates.X(3)])
+ ma = c.add(gates.M(5, 1, 3, register_name="a"))
+ mb = c.add(gates.M(2, 0, register_name="b"))
+ result = backend.execute_circuit(c, nshots=100)
+
+ ma_freq = ma.frequencies()
+ mb_freq = mb.frequencies()
+ frequencies = result.frequencies(registers=True)
+ assert ma_freq == frequencies.get("a")
+ assert mb_freq == frequencies.get("b")
+
+
+@pytest.mark.parametrize("nqubits", [1, 4])
+@pytest.mark.parametrize("outcome", [0, 1])
+def test_measurement_basis(backend, nqubits, outcome):
+ c = models.Circuit(nqubits)
+ if outcome:
+ c.add(gates.X(q) for q in range(nqubits))
+ c.add(gates.H(q) for q in range(nqubits))
+ c.add(gates.M(*range(nqubits), basis=gates.X))
+ result = backend.execute_circuit(c, nshots=100)
+ assert result.frequencies() == {nqubits * str(outcome): 100}
+
+
+def test_measurement_basis_list(backend):
+ c = models.Circuit(4)
+ c.add(gates.H(0))
+ c.add(gates.X(2))
+ c.add(gates.H(2))
+ c.add(gates.X(3))
+ c.add(gates.M(0, 1, 2, 3, basis=[gates.X, gates.Z, gates.X, gates.Z]))
+ result = backend.execute_circuit(c, nshots=100)
+ assert result.frequencies() == {"0011": 100}
+ assert (
+ c.draw()
+ == """q0: ─H─H───M─
+q1: ───────M─
+q2: ─X─H─H─M─
+q3: ─X─────M─"""
+ )
+
+
+def test_measurement_basis_list_error(backend):
+ c = models.Circuit(4)
+ with pytest.raises(ValueError):
+ c.add(gates.M(0, 1, 2, 3, basis=[gates.X, gates.Z, gates.X]))
+
+
+def test_measurement_same_qubit_different_registers_error(backend):
+ c = models.Circuit(4)
+ c.add(gates.M(0, 1, 3, register_name="a"))
+ with pytest.raises(KeyError):
+ c.add(gates.M(1, 2, 3, register_name="a"))
+
+
+def test_measurementsymbol_pickling(backend):
+ from qibo.models import QFT
+
+ c = QFT(3)
+ c.add(gates.M(0, 2, basis=[gates.X, gates.Z]))
+ backend.execute_circuit(c).samples()
+ for symbol in c.measurements[0].result.symbols:
+ dumped_symbol = pickle.dumps(symbol)
+ new_symbol = pickle.loads(dumped_symbol)
+ assert symbol.index == new_symbol.index
+ assert symbol.name == new_symbol.name
+ backend.assert_allclose(symbol.result.samples(), new_symbol.result.samples())
diff --git a/tests/test_measurements_collapse.py b/tests/test_measurements_collapse.py
new file mode 100644
index 000000000..34cc48d9c
--- /dev/null
+++ b/tests/test_measurements_collapse.py
@@ -0,0 +1,262 @@
+"""Test :class:`qibo.gates.M` as standalone and as part of circuit."""
+
+import numpy as np
+import pytest
+
+from qibo import gates, models
+from qibo.quantum_info import random_density_matrix, random_statevector
+
+
+@pytest.mark.parametrize(
+ "nqubits,targets",
+ [(2, [1]), (3, [1]), (4, [1, 3]), (5, [0, 3, 4]), (6, [1, 3]), (4, [0, 2])],
+)
+def test_measurement_collapse(backend, nqubits, targets):
+ initial_state = random_statevector(2**nqubits, backend=backend)
+ c = models.Circuit(nqubits)
+ for q in np.random.randint(nqubits, size=np.random.randint(nqubits, size=1)):
+ c.add(gates.H(q))
+ r = c.add(gates.M(*targets, collapse=True))
+ c.add(gates.M(*targets))
+ outcome = backend.execute_circuit(
+ c, backend.cast(initial_state, copy=True), nshots=1
+ )
+ samples = r.samples()[0]
+ backend.assert_allclose(samples, outcome.samples()[0])
+
+
+@pytest.mark.parametrize(
+ "nqubits,targets", [(2, [1]), (3, [1]), (4, [1, 3]), (5, [0, 3, 4])]
+)
+def test_measurement_collapse_density_matrix(backend, nqubits, targets):
+ def assign_value(rho, index, value):
+ if backend.name == "tensorflow":
+ rho_numpy = rho.numpy()
+ rho_numpy[index] = value
+ return rho.__class__(rho_numpy, rho.device)
+
+ rho[index] = value
+ return rho
+
+ initial_rho = random_density_matrix(2**nqubits, backend=backend)
+ c = models.Circuit(nqubits, density_matrix=True)
+ r = c.add(gates.M(*targets, collapse=True))
+ final_rho = backend.execute_circuit(c, backend.np.copy(initial_rho), nshots=1)
+
+ samples = r.samples()[0]
+ target_rho = backend.np.reshape(initial_rho, 2 * nqubits * (2,))
+ for q, r in zip(targets, samples):
+ r = int(r)
+ slicer = 2 * nqubits * [slice(None)]
+ slicer[q], slicer[q + nqubits] = 1 - r, 1 - r
+ target_rho = assign_value(target_rho, tuple(slicer), 0)
+ slicer[q], slicer[q + nqubits] = r, 1 - r
+ target_rho = assign_value(target_rho, tuple(slicer), 0)
+ slicer[q], slicer[q + nqubits] = 1 - r, r
+ target_rho = assign_value(target_rho, tuple(slicer), 0)
+ target_rho = backend.np.reshape(target_rho, initial_rho.shape)
+ target_rho = target_rho / backend.np.trace(target_rho)
+ backend.assert_allclose(final_rho, target_rho)
+
+
+def test_measurement_collapse_bitflip_noise(backend):
+ c = models.Circuit(4)
+ with pytest.raises(NotImplementedError):
+ output = c.add(gates.M(0, 1, p0=0.2, collapse=True))
+
+
+@pytest.mark.parametrize("density_matrix", [True, False])
+@pytest.mark.parametrize("effect", [False, True])
+def test_measurement_result_parameters(backend, effect, density_matrix):
+ c = models.Circuit(4, density_matrix=density_matrix)
+ if effect:
+ c.add(gates.X(0))
+ r = c.add(gates.M(0, collapse=True))
+ c.add(gates.RX(1, theta=np.pi * r.symbols[0] / 4))
+ if not density_matrix:
+ c.add(gates.M(0))
+
+ target_c = models.Circuit(4, density_matrix=density_matrix)
+ if effect:
+ target_c.add(gates.X(0))
+ target_c.add(gates.RX(1, theta=np.pi / 4))
+ if not density_matrix:
+ target_c.add(gates.M(0))
+
+ final_state = backend.execute_circuit(c, nshots=1)
+ target_state = backend.execute_circuit(target_c)
+ if not density_matrix:
+ final_state = final_state.samples()[0]
+ target_state = target_state.samples()[0]
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_measurement_result_parameters_random(backend):
+ initial_state = random_density_matrix(2**4, backend=backend)
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ r = c.add(gates.M(1, collapse=True))
+ c.add(gates.RY(0, theta=np.pi * r.symbols[0] / 5))
+ c.add(gates.RX(2, theta=np.pi * r.symbols[0] / 4))
+ final_state = backend.execute_circuit(
+ c, initial_state=backend.np.copy(initial_state), nshots=1
+ )
+
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ m = c.add(gates.M(1, collapse=True))
+ target_state = backend.execute_circuit(
+ c, initial_state=backend.np.copy(initial_state), nshots=1
+ ).state()
+ if int(m.symbols[0].outcome()):
+ c = models.Circuit(4, density_matrix=True)
+ c.add(gates.RY(0, theta=np.pi / 5))
+ c.add(gates.RX(2, theta=np.pi / 4))
+ target_state = backend.execute_circuit(c, initial_state=target_state)
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.parametrize("use_loop", [True, False])
+def test_measurement_result_parameters_repeated_execution(backend, use_loop):
+ initial_state = random_density_matrix(2**4, backend=backend)
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ r = c.add(gates.M(1, collapse=True))
+ c.add(gates.RX(2, theta=np.pi * r.symbols[0] / 4))
+ if use_loop:
+ final_states = []
+ for _ in range(20):
+ final_state = backend.execute_circuit(
+ c, initial_state=backend.np.copy(initial_state), nshots=1
+ )
+ final_states.append(final_state.state())
+ final_states = backend.np.mean(backend.cast(final_states), 0)
+ else:
+ final_states = backend.execute_circuit(
+ c, initial_state=backend.np.copy(initial_state), nshots=20
+ ).state()
+
+ backend.set_seed(123)
+ target_states = []
+ for _ in range(20):
+ c = models.Circuit(4, density_matrix=True)
+ m = c.add(gates.M(1, collapse=True))
+ target_state = backend.execute_circuit(
+ c, backend.np.copy(initial_state), nshots=1
+ ).state()
+ if int(m.symbols[0].outcome()):
+ target_state = backend.apply_gate_density_matrix(
+ gates.RX(2, theta=np.pi / 4), target_state, 4
+ )
+ target_states.append(backend.to_numpy(target_state))
+
+ target_states = np.asarray(target_states).mean(0)
+ backend.assert_allclose(final_states, target_states)
+
+
+def test_measurement_result_parameters_repeated_execution_final_measurements(backend):
+ initial_state = random_density_matrix(2**4, backend=backend)
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ r = c.add(gates.M(1, collapse=True))
+ c.add(gates.RY(0, theta=np.pi * r.symbols[0] / 3))
+ c.add(gates.RY(2, theta=np.pi * r.symbols[0] / 4))
+ c.add(gates.M(0, 1, 2, 3))
+ result = backend.execute_circuit(
+ c, initial_state=backend.cast(initial_state, copy=True), nshots=30
+ )
+ final_samples = result.samples(binary=False)
+
+ backend.set_seed(123)
+ target_samples = []
+ for _ in range(30):
+ c = models.Circuit(4, density_matrix=True)
+ m = c.add(gates.M(1, collapse=True))
+ target_state = backend.execute_circuit(
+ c, backend.np.copy(initial_state), nshots=1
+ ).state()
+ c = models.Circuit(4, density_matrix=True)
+ if int(m.symbols[0].outcome()):
+ c.add(gates.RY(0, theta=np.pi / 3))
+ c.add(gates.RY(2, theta=np.pi / 4))
+ c.add(gates.M(0, 1, 2, 3))
+ result = backend.execute_circuit(c, target_state, nshots=1)
+ target_samples.append(result.samples(binary=False)[0])
+ backend.assert_allclose(final_samples, target_samples)
+
+
+def test_measurement_result_parameters_multiple_qubits(backend):
+ initial_state = random_density_matrix(2**4, backend=backend)
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ r = c.add(gates.M(0, 1, 2, collapse=True))
+ c.add(gates.RY(1, theta=np.pi * r.symbols[0] / 5))
+ c.add(gates.RX(3, theta=np.pi * r.symbols[2] / 3))
+ final_state = backend.execute_circuit(c, backend.np.copy(initial_state), nshots=1)
+
+ backend.set_seed(123)
+ c = models.Circuit(4, density_matrix=True)
+ m = c.add(gates.M(0, 1, 2, collapse=True))
+ target_state = backend.execute_circuit(
+ c, backend.np.copy(initial_state), nshots=1
+ ).state()
+ # not including in coverage because outcomes are probabilistic and may
+ # not occur for the CI run
+ if int(m.symbols[0].outcome()): # pragma: no cover
+ target_state = backend.apply_gate_density_matrix(
+ gates.RY(1, theta=np.pi / 5), target_state, 4
+ )
+ if int(m.symbols[2].outcome()): # pragma: no cover
+ target_state = backend.apply_gate_density_matrix(
+ gates.RX(3, theta=np.pi / 3), target_state, 4
+ )
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.skip(reason="this has to be updated for density matrices")
+@pytest.mark.parametrize("nqubits,targets", [(5, [2, 4]), (6, [3, 5])])
+def test_measurement_collapse_distributed(backend, accelerators, nqubits, targets):
+ initial_state = random_density_matrix(2**nqubits, backend=backend)
+ c = models.Circuit(nqubits, accelerators, density_matrix=True)
+ m = c.add(gates.M(*targets, collapse=True))
+ result = backend.execute_circuit(c, np.copy(initial_state), nshots=1).state()
+ slicer = 2 * nqubits * [slice(None)]
+ outcomes = [r.outcome() for r in m.symbols]
+ for t, r in zip(targets, outcomes):
+ slicer[t] = int(r)
+ slicer = tuple(slicer)
+ initial_state = initial_state.reshape(2 * nqubits * (2,))
+ target_state = np.zeros_like(initial_state)
+ target_state[slicer] = initial_state[slicer]
+ norm = (np.abs(target_state) ** 2).sum()
+ target_state = target_state.ravel() / np.sqrt(norm)
+ backend.assert_allclose(result, target_state.reshape(2**nqubits, 2**nqubits))
+
+
+def test_collapse_after_measurement(backend):
+ qubits = [0, 2, 3]
+ c = models.Circuit(5, density_matrix=True)
+ c.add(gates.H(i) for i in range(5))
+ m = c.add(gates.M(*qubits, collapse=True))
+ c.add(gates.H(i) for i in range(5))
+ final_state = backend.execute_circuit(c, nshots=1)
+
+ ct = models.Circuit(5, density_matrix=True)
+ bitstring = [r.outcome() for r in m.symbols]
+ for i, r in zip(qubits, bitstring):
+ if r:
+ ct.add(gates.X(i))
+ ct.add(gates.H(i) for i in qubits)
+ target_state = backend.execute_circuit(ct)
+ backend.assert_allclose(final_state, target_state, atol=1e-15)
+
+
+def test_collapse_error(backend):
+ c = models.Circuit(1)
+ m = c.add(gates.M(0, collapse=True))
+ with pytest.raises(Exception) as exc_info:
+ backend.execute_circuit(c)
+ assert (
+ str(exc_info.value)
+ == "The circuit contains only collapsing measurements (`collapse=True`) but `density_matrix=False`. Please set `density_matrix=True` to retrieve the final state after execution."
+ )
diff --git a/tests/test_measurements_probabilistic.py b/tests/test_measurements_probabilistic.py
new file mode 100644
index 000000000..742c4838b
--- /dev/null
+++ b/tests/test_measurements_probabilistic.py
@@ -0,0 +1,158 @@
+"""Test circuit measurements when outcome is probabilistic."""
+
+import numpy as np
+import pytest
+
+from qibo import gates, models
+
+from .test_measurements import assert_result
+
+
+@pytest.mark.parametrize("use_samples", [True, False])
+def test_probabilistic_measurement(backend, accelerators, use_samples):
+ # set single-thread to fix the random values generated from the frequency custom op
+ backend.set_threads(1)
+ c = models.Circuit(4, accelerators)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.M(0, 1))
+ result = backend.execute_circuit(c, nshots=1000)
+
+ backend.set_seed(1234)
+ if use_samples:
+ # calculates sample tensor directly using `tf.random.categorical`
+ # otherwise it uses the frequency-only calculation
+ _ = result.samples()
+
+ # update reference values based on backend and device
+ decimal_frequencies = backend._test_regressions("test_probabilistic_measurement")
+ assert sum(result.frequencies().values()) == 1000
+ assert_result(backend, result, decimal_frequencies=decimal_frequencies)
+
+
+def test_sample_frequency_agreement(backend):
+ # set single-thread to fix the random values generated from the frequency custom op
+ backend.set_threads(1)
+ c = models.Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.M(0, 1))
+ result = backend.execute_circuit(c, nshots=1000)
+
+ backend.set_seed(1234)
+ target_frequencies = result.frequencies(binary=False)
+ samples = result.samples(binary=False)
+ outcomes, counts = np.unique(samples, return_counts=True)
+ outcomes = backend.to_numpy(outcomes)
+ counts = backend.to_numpy(counts)
+ frequencies = dict(zip(outcomes, counts))
+ assert frequencies == target_frequencies
+
+
+@pytest.mark.parametrize("use_samples", [True, False])
+def test_unbalanced_probabilistic_measurement(backend, use_samples):
+ # set single-thread to fix the random values generated from the frequency custom op
+ backend.set_threads(1)
+ state = np.array([1, 1, 1, np.sqrt(3)]) / np.sqrt(6)
+ c = models.Circuit(2)
+ c.add(gates.M(0, 1))
+ result = backend.execute_circuit(c, initial_state=np.copy(state), nshots=1000)
+
+ backend.set_seed(1234)
+ if use_samples:
+ # calculates sample tensor directly using `tf.random.categorical`
+ # otherwise it uses the frequency-only calculation
+ _ = result.samples()
+ # update reference values based on backend and device
+ decimal_frequencies = backend._test_regressions(
+ "test_unbalanced_probabilistic_measurement"
+ )
+
+ assert sum(result.frequencies().values()) == 1000
+ assert_result(backend, result, decimal_frequencies=decimal_frequencies)
+
+
+def test_measurements_with_probabilistic_noise(backend):
+ """Check measurements when simulating noise with repeated execution."""
+ thetas = np.random.random(5)
+ c = models.Circuit(5)
+ c.add((gates.RX(i, t) for i, t in enumerate(thetas)))
+ c.add(
+ gates.PauliNoiseChannel(i, list(zip(["Y", "Z"], [0.2, 0.4]))) for i in range(5)
+ )
+ c.add(gates.M(*range(5)))
+ backend.set_seed(123)
+ result = backend.execute_circuit(c, nshots=20)
+ samples = result.samples()
+
+ backend.set_seed(123)
+ target_samples = []
+ channel_gates = [gates.Y, gates.Z]
+ probs = [0.2, 0.4, 0.4]
+ for _ in range(20):
+ noiseless_c = models.Circuit(5)
+ noiseless_c.add((gates.RX(i, t) for i, t in enumerate(thetas)))
+ for i in range(5):
+ index = backend.sample_shots(probs, 1)[0]
+ if index != len(channel_gates):
+ noiseless_c.add(channel_gates[index](i))
+ noiseless_c.add(gates.M(*range(5)))
+ result = backend.execute_circuit(noiseless_c, nshots=1)
+ target_samples.append(backend.to_numpy(result.samples()))
+ target_samples = np.concatenate(target_samples, axis=0)
+ backend.assert_allclose(samples, target_samples)
+
+
+@pytest.mark.parametrize(
+ "i,probs", [(0, [0.0, 0.0, 0.0]), (1, [0.1, 0.3, 0.2]), (2, [0.5, 0.5, 0.5])]
+)
+def test_post_measurement_bitflips_on_circuit(backend, accelerators, i, probs):
+ """Check bitflip errors on circuit measurements."""
+ backend.set_seed(123)
+ c = models.Circuit(5, accelerators=accelerators)
+ c.add([gates.X(0), gates.X(2), gates.X(3)])
+ c.add(gates.M(0, 1, p0={0: probs[0], 1: probs[1]}))
+ c.add(gates.M(3, p0=probs[2]))
+ result = backend.execute_circuit(c, nshots=30)
+ freqs = result.frequencies(binary=False)
+ targets = backend._test_regressions("test_post_measurement_bitflips_on_circuit")
+ assert freqs == targets[i]
+
+
+def test_post_measurement_bitflips_on_circuit_result(backend):
+ """Check bitflip errors on ``CircuitResult`` objects."""
+ thetas = np.random.random(4)
+ backend.set_seed(123)
+ c = models.Circuit(4)
+ c.add((gates.RX(i, theta=t) for i, t in enumerate(thetas)))
+ c.add(gates.M(0, 1, register_name="a", p0={0: 0.2, 1: 0.4}))
+ c.add(gates.M(3, register_name="b", p0=0.3))
+ result = backend.execute_circuit(c, nshots=30)
+ samples = result.samples(binary=True)
+ register_samples = result.samples(binary=True, registers=True)
+ backend.assert_allclose(register_samples["a"], samples[:, :2])
+ backend.assert_allclose(register_samples["b"], samples[:, 2:])
+
+
+@pytest.mark.parametrize(
+ "i,p0,p1",
+ [
+ (0, 0.2, None),
+ (1, 0.2, 0.1),
+ (2, (0.1, 0.0, 0.2), None),
+ (3, {0: 0.2, 1: 0.1, 2: 0.0}, None),
+ ],
+)
+def test_measurementresult_apply_bitflips(backend, i, p0, p1):
+ from qibo.result import CircuitResult
+
+ c = models.Circuit(3)
+ c.add(gates.M(*range(3)))
+ state = backend.zero_state(8)
+ result = CircuitResult(state, c.measurements, backend)
+ result._samples = backend.cast(np.zeros((10, 3)), dtype="int32")
+ backend.set_seed(123)
+ noisy_samples = result.apply_bitflips(p0, p1)
+ targets = backend._test_regressions("test_measurementresult_apply_bitflips")
+ noisy_samples = backend.samples_to_decimal(noisy_samples, 3)
+ backend.assert_allclose(noisy_samples, targets[i])
diff --git a/tests/test_models_circuit.py b/tests/test_models_circuit.py
new file mode 100644
index 000000000..33036dad2
--- /dev/null
+++ b/tests/test_models_circuit.py
@@ -0,0 +1,897 @@
+"""Test all methods defined in `qibo/models/circuit.py`."""
+
+from collections import Counter
+
+import pytest
+
+from qibo import Circuit, gates
+from qibo.models.utils import initialize
+
+
+def test_parametrizedgates_class():
+ from qibo.models.circuit import _ParametrizedGates
+
+ paramgates = _ParametrizedGates()
+ paramgates.append(gates.RX(0, 0.1234))
+ paramgates.append(gates.fSim(0, 1, 0.1234, 0.4321))
+ assert len(paramgates.set) == 2
+ assert paramgates.nparams == 3
+
+
+def test_queue_class():
+ from qibo.callbacks import EntanglementEntropy
+ from qibo.models.circuit import _Queue
+
+ entropy = EntanglementEntropy([0])
+ queue = _Queue(4)
+ gatelist = [
+ gates.H(0),
+ gates.H(1),
+ gates.X(0),
+ gates.H(2),
+ gates.CNOT(1, 2),
+ gates.Y(3),
+ gates.CallbackGate(entropy),
+ ]
+ for g in gatelist:
+ queue.append(g)
+ assert queue.moments == [
+ [gatelist[0], gatelist[1], gatelist[3], gatelist[5]],
+ [gatelist[2], gatelist[4], gatelist[4], None],
+ [gatelist[6] for _ in range(4)],
+ ]
+
+
+def test_circuit_init():
+ c = Circuit(2)
+ assert c.nqubits == 2
+
+
+def test_eigenstate(backend):
+ nqubits = 3
+ c = Circuit(nqubits)
+ c.add(gates.M(*list(range(nqubits))))
+ c2 = initialize(nqubits, eigenstate="-")
+ assert backend.execute_circuit(c, nshots=100, initial_state=c2).frequencies() == {
+ "111": 100
+ }
+ c2 = initialize(nqubits, eigenstate="+")
+ assert backend.execute_circuit(c, nshots=100, initial_state=c2).frequencies() == {
+ "000": 100
+ }
+
+ with pytest.raises(NotImplementedError):
+ c2 = initialize(nqubits, eigenstate="x")
+
+
+def test_initialize(backend):
+ nqubits = 3
+ for gate in [gates.X, gates.Y, gates.Z]:
+ c = Circuit(nqubits)
+ c.add(gates.M(*list(range(nqubits)), basis=gate))
+ c2 = initialize(nqubits, basis=gate)
+ assert backend.execute_circuit(
+ c, nshots=100, initial_state=c2
+ ).frequencies() == {"000": 100}
+
+
+@pytest.mark.parametrize("nqubits", [0, -10, 2.5])
+def test_circuit_init_errors(nqubits):
+ with pytest.raises((ValueError, TypeError)):
+ Circuit(nqubits)
+
+
+def test_circuit_constructor():
+ c = Circuit(5)
+ assert isinstance(c, Circuit)
+ assert not c.density_matrix
+ c = Circuit(5, density_matrix=True)
+ assert isinstance(c, Circuit)
+ assert c.density_matrix
+ c = Circuit(5, accelerators={"/GPU:0": 2})
+ with pytest.raises(NotImplementedError):
+ Circuit(5, accelerators={"/GPU:0": 2}, density_matrix=True)
+
+
+def test_circuit_add():
+ c = Circuit(2)
+ g1, g2, g3 = gates.H(0), gates.H(1), gates.CNOT(0, 1)
+ c.add(g1)
+ c.add(g2)
+ c.add(g3)
+ assert c.depth == 2
+ assert c.ngates == 3
+ assert list(c.queue) == [g1, g2, g3]
+
+
+def test_circuit_add_errors():
+ c = Circuit(2)
+ with pytest.raises(TypeError):
+ c.add(0)
+ with pytest.raises(ValueError):
+ c.add(gates.H(2))
+ c._final_state = 0
+ with pytest.raises(RuntimeError):
+ c.add(gates.H(1))
+
+
+def test_circuit_add_iterable():
+ c = Circuit(2)
+ # adding list
+ gatelist = [gates.H(0), gates.H(1), gates.CNOT(0, 1)]
+ c.add(gatelist)
+ assert c.depth == 2
+ assert c.ngates == 3
+ assert list(c.queue) == gatelist
+ # adding tuple
+ gatetuple = (gates.H(0), gates.H(1), gates.CNOT(0, 1))
+ c.add(gatetuple)
+ assert c.depth == 4
+ assert c.ngates == 6
+ assert isinstance(c.queue[-1], gates.CNOT)
+
+
+def test_circuit_add_generator():
+ """Check if `circuit.add` works with generators."""
+
+ def gen():
+ yield gates.H(0)
+ yield gates.H(1)
+ yield gates.CNOT(0, 1)
+
+ c = Circuit(2)
+ c.add(gen())
+ assert c.depth == 2
+ assert c.ngates == 3
+ assert isinstance(c.queue[-1], gates.CNOT)
+
+
+def test_circuit_add_nested_generator():
+ def gen():
+ yield gates.H(0)
+ yield gates.H(1)
+ yield gates.CNOT(0, 1)
+
+ c = Circuit(2)
+ c.add(gen() for _ in range(3))
+ assert c.depth == 6
+ assert c.ngates == 9
+ assert isinstance(c.queue[2], gates.CNOT)
+ assert isinstance(c.queue[5], gates.CNOT)
+ assert isinstance(c.queue[7], gates.H)
+
+
+def test_add_measurement():
+ c = Circuit(5)
+ g1 = gates.M(0, 2, register_name="a")
+ g2 = gates.M(3, register_name="b")
+ c.add([g1, g2])
+ assert len(c.queue) == 2
+ assert c.measurement_tuples == {"a": (0, 2), "b": (3,)}
+ assert g1.target_qubits == (0, 2)
+ assert g2.target_qubits == (3,)
+
+
+def test_add_measurement_collapse():
+ c = Circuit(3)
+ c.add(gates.X(0))
+ c.add(gates.M(0, 1))
+ c.add(gates.X(1))
+ c.add(gates.M(1))
+ c.add(gates.X(2))
+ c.add(gates.M(2))
+ assert len(c.queue) == 6
+ # assert that the first measurement was switched to collapse automatically
+ assert c.queue[1].collapse
+ assert len(c.measurements) == 2
+
+
+# :meth:`qibo.core.circuit.Circuit.fuse` is tested in `test_core_fusion.py`
+
+
+def test_gate_types():
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.X(2))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ target_counter = Counter({gates.H: 2, gates.X: 1, gates.CNOT: 2, gates.TOFFOLI: 1})
+ assert c.ngates == 6
+ assert c.gate_types == target_counter
+
+
+def test_gate_names():
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.X(2))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ target_counter = Counter({"h": 2, "x": 1, "cx": 2, "ccx": 1})
+ assert c.ngates == 6
+ assert c.gate_names == target_counter
+
+
+def test_gates_of_type():
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.X(1))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ c.add(gates.H(2))
+ h_gates = c.gates_of_type(gates.H)
+ cx_gates = c.gates_of_type("cx")
+ assert h_gates == [(0, c.queue[0]), (1, c.queue[1]), (6, c.queue[6])]
+ assert cx_gates == [(2, c.queue[2]), (4, c.queue[4])]
+ with pytest.raises(TypeError):
+ c.gates_of_type(5)
+
+
+def test_summary():
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 2))
+ c.add(gates.CNOT(1, 2))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ c.add(gates.H(2))
+ target_summary = "\n".join(
+ [
+ "Circuit depth = 5",
+ "Total number of gates = 6",
+ "Number of qubits = 3",
+ "Most common gates:",
+ "h: 3",
+ "cx: 2",
+ "ccx: 1",
+ ]
+ )
+ assert c.summary() == target_summary
+
+
+@pytest.mark.parametrize("measurements", [False, True])
+def test_circuit_addition(measurements):
+ c1 = Circuit(2)
+ g1, g2 = gates.H(0), gates.CNOT(0, 1)
+ c1.add(g1)
+ c1.add(g2)
+ if measurements:
+ c1.add(gates.M(0, register_name="a"))
+
+ c2 = Circuit(2)
+ g3 = gates.H(1)
+ c2.add(g3)
+ if measurements:
+ c2.add(gates.M(1, register_name="b"))
+
+ c3 = c1 + c2
+ assert c3.depth == 3 + int(measurements)
+ if measurements:
+ assert c3.measurement_tuples == {"a": (0,), "b": (1,)}
+
+
+def test_circuit_addition_errors():
+ c1 = Circuit(2)
+ c1.add(gates.H(0))
+ c1.add(gates.H(1))
+
+ c2 = Circuit(1)
+ c2.add(gates.X(0))
+
+ with pytest.raises(ValueError):
+ c1 + c2
+
+
+def test_circuit_on_qubits():
+ c = Circuit(3)
+ c.add([gates.H(0), gates.X(1), gates.Y(2)])
+ c.add([gates.CNOT(0, 1), gates.CZ(1, 2)])
+ c.add(gates.H(1).controlled_by(0, 2))
+ new_gates = list(c.on_qubits(2, 5, 4))
+ assert new_gates[0].target_qubits == (2,)
+ assert new_gates[1].target_qubits == (5,)
+ assert new_gates[2].target_qubits == (4,)
+ assert new_gates[3].target_qubits == (5,)
+ assert new_gates[3].control_qubits == (2,)
+ assert new_gates[4].target_qubits == (4,)
+ assert new_gates[4].control_qubits == (5,)
+ assert new_gates[5].target_qubits == (5,)
+ assert new_gates[5].control_qubits == (2, 4)
+
+
+def test_circuit_on_qubits_errors():
+ smallc = Circuit(2)
+ smallc.add(gates.H(i) for i in range(2))
+ with pytest.raises(ValueError):
+ next(smallc.on_qubits(0, 1, 2))
+
+ from qibo.callbacks import Callback
+
+ smallc = Circuit(4)
+ smallc.add(gates.CallbackGate(Callback()))
+ with pytest.raises(NotImplementedError):
+ next(smallc.on_qubits(0, 1, 2, 3))
+
+
+def test_circuit_serialization():
+ c = Circuit(4)
+ c.add(gates.RY(2, theta=0).controlled_by(0, 1))
+ c.add(gates.RX(3, theta=0))
+ c.add(gates.CNOT(1, 3))
+ c.add(gates.RZ(2, theta=0).controlled_by(0, 3))
+
+ raw = c.raw
+ assert isinstance(raw, dict)
+ assert Circuit.from_dict(raw).raw == raw
+
+ c.add(gates.M(0))
+ raw = c.raw
+ assert isinstance(raw, dict)
+ assert Circuit.from_dict(raw).raw == raw
+
+
+def test_circuit_light_cone():
+ from qibo import __version__
+
+ nqubits = 10
+ c = Circuit(nqubits)
+ c.add(gates.RY(i, theta=0) for i in range(nqubits))
+ c.add(gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))
+ c.add(gates.RY(i, theta=0) for i in range(nqubits))
+ c.add(gates.CZ(i, i + 1) for i in range(1, nqubits - 1, 2))
+ c.add(gates.CZ(0, nqubits - 1))
+ sc, qubit_map = c.light_cone(4, 5)
+ target_qasm = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[6];
+ry(0) q[0];
+ry(0) q[1];
+ry(0) q[2];
+ry(0) q[3];
+ry(0) q[4];
+ry(0) q[5];
+cz q[0],q[1];
+cz q[2],q[3];
+cz q[4],q[5];
+ry(0) q[1];
+ry(0) q[2];
+ry(0) q[3];
+ry(0) q[4];
+cz q[1],q[2];
+cz q[3],q[4];"""
+ assert qubit_map == {2: 0, 3: 1, 4: 2, 5: 3, 6: 4, 7: 5}
+ assert sc.to_qasm() == target_qasm
+
+
+def test_circuit_light_cone_controlled_by():
+ c = Circuit(4)
+ c.add(gates.RY(2, theta=0).controlled_by(0, 1))
+ c.add(gates.RX(3, theta=0))
+ sc, qubit_map = c.light_cone(3)
+ assert qubit_map == {3: 0}
+ assert sc.nqubits == 1
+ assert len(sc.queue) == 1
+ assert isinstance(sc.queue[0], gates.RX)
+ sc, qubit_map = c.light_cone(2)
+ assert qubit_map == {0: 0, 1: 1, 2: 2}
+ assert sc.nqubits == 3
+ assert len(sc.queue) == 1
+ assert isinstance(sc.queue[0], gates.RY)
+
+
+@pytest.mark.parametrize("deep", [False, True])
+def test_circuit_copy(deep):
+ c1 = Circuit(2)
+ c1.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+ c2 = c1.copy(deep)
+ assert c1.depth == 2
+ assert c2.depth == 2
+ assert c1.ngates == 3
+ assert c2.ngates == 3
+ assert c1.nqubits == 2
+ assert c2.nqubits == 2
+ for g1, g2 in zip(c1.queue, c2.queue):
+ if deep:
+ assert g1.__class__ == g2.__class__
+ assert g1.target_qubits == g2.target_qubits
+ assert g1.control_qubits == g2.control_qubits
+ else:
+ assert g1 is g2
+
+
+@pytest.mark.parametrize("deep", [False, True])
+def test_circuit_copy_with_measurements(deep):
+ c1 = Circuit(4)
+ c1.add([gates.H(0), gates.H(3), gates.CNOT(0, 2)])
+ c1.add(gates.M(0, 1, register_name="a"))
+ c1.add(gates.M(3, register_name="b"))
+ c2 = c1.copy(deep)
+ assert c1.nqubits == 4
+ assert c2.nqubits == 4
+ assert c1.depth == 3
+ assert c2.depth == 3
+ assert c1.ngates == 5
+ assert c2.ngates == 5
+ assert c2.measurement_tuples == {"a": (0, 1), "b": (3,)}
+
+
+@pytest.mark.parametrize("measurements", [False, True])
+def test_circuit_invert(measurements):
+ c = Circuit(3)
+ gatelist = [gates.H(0), gates.X(1), gates.Y(2), gates.CNOT(0, 1), gates.CZ(1, 2)]
+ c.add(gatelist)
+ if measurements:
+ c.add(gates.M(0, 2))
+ invc = c.invert()
+ for g1, g2 in zip(invc.queue, gatelist[::-1]):
+ g2 = g2.dagger()
+ assert isinstance(g1, g2.__class__)
+ assert g1.target_qubits == g2.target_qubits
+ assert g1.control_qubits == g2.control_qubits
+ if measurements:
+ assert invc.measurement_tuples == {"register0": (0, 2)}
+
+
+@pytest.mark.parametrize("measurements", [False, True])
+def test_circuit_decompose(measurements):
+ c = Circuit(4)
+ c.add([gates.H(0), gates.X(1), gates.Y(2)])
+ c.add([gates.CZ(0, 1), gates.CNOT(2, 3), gates.TOFFOLI(0, 1, 3)])
+ if measurements:
+ c.add(gates.M(0, 2))
+ decompc = c.decompose()
+
+ dgates = []
+ for gate in c.queue:
+ dgates.extend(gate.decompose())
+ for g1, g2 in zip(decompc.queue, dgates):
+ assert isinstance(g1, g2.__class__)
+ assert g1.target_qubits == g2.target_qubits
+ assert g1.control_qubits == g2.control_qubits
+ if measurements:
+ assert decompc.measurement_tuples == {"register0": (0, 2)}
+
+
+@pytest.mark.parametrize("measurements", [False, True])
+@pytest.mark.parametrize(
+ "noise_map",
+ [
+ list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])),
+ {0: list(zip(["X", "Z"], [0.1, 0.2])), 1: list(zip(["Y", "Z"], [0.2, 0.1]))},
+ ],
+)
+def test_circuit_with_pauli_noise(measurements, noise_map):
+ with pytest.raises(TypeError):
+ n_map = ["X", 0.2]
+ circuit = Circuit(1)
+ circuit.with_pauli_noise(n_map)
+
+ c = Circuit(2)
+ c.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+ if measurements:
+ c.add(gates.M(0, 1))
+ noisyc = c.with_pauli_noise(noise_map)
+
+ if not isinstance(noise_map, dict):
+ noise_map = {0: noise_map, 1: noise_map}
+ targetc = Circuit(2)
+ targetc.add(gates.H(0))
+ targetc.add(gates.PauliNoiseChannel(0, noise_map[0]))
+ targetc.add(gates.H(1))
+ targetc.add(gates.PauliNoiseChannel(1, noise_map[1]))
+ targetc.add(gates.CNOT(0, 1))
+ targetc.add(gates.PauliNoiseChannel(0, noise_map[0]))
+ targetc.add(gates.PauliNoiseChannel(1, noise_map[1]))
+ for g1, g2 in zip(noisyc.queue, targetc.queue):
+ assert isinstance(g1, g2.__class__)
+ assert g1.target_qubits == g2.target_qubits
+ assert g1.control_qubits == g2.control_qubits
+ if measurements:
+ assert noisyc.measurement_tuples == {"register0": (0, 1)}
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+@pytest.mark.parametrize("include_not_trainable", [True, False])
+@pytest.mark.parametrize("format", ["list", "dict", "flatlist"])
+def test_get_parameters(trainable, include_not_trainable, format):
+ import numpy as np
+
+ matrix = np.random.random((2, 2))
+ c = Circuit(3)
+ c.add(gates.RX(0, theta=0.123))
+ c.add(gates.RY(1, theta=0.456, trainable=trainable))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.Unitary(matrix, 2))
+ c.add(gates.fSim(0, 2, theta=0.789, phi=0.987, trainable=trainable))
+ c.add(gates.H(2))
+ params = c.get_parameters(format, include_not_trainable)
+ if trainable or include_not_trainable:
+ target_params = {
+ "list": [(0.123,), (0.456,), (0.789, 0.987)],
+ "dict": {
+ c.queue[0]: (0.123,),
+ c.queue[1]: (0.456,),
+ c.queue[4]: (0.789, 0.987),
+ },
+ "flatlist": [0.123, 0.456],
+ }
+ target_params["flatlist"].extend(list(matrix.ravel()))
+ target_params["flatlist"].extend([0.789, 0.987])
+ else:
+ target_params = {
+ "list": [(0.123,)],
+ "dict": {c.queue[0]: (0.123,)},
+ "flatlist": [0.123],
+ }
+ target_params["flatlist"].extend(list(matrix.ravel()))
+ if format == "list":
+ i = len(target_params["list"]) // 2 + 1
+ np.testing.assert_allclose(params.pop(i)[0], matrix)
+ elif format == "dict":
+ np.testing.assert_allclose(params.pop(c.queue[3])[0], matrix)
+ assert params == target_params[format]
+ with pytest.raises(ValueError):
+ c.get_parameters("test")
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_circuit_set_parameters_with_list(trainable):
+ """Check updating parameters of circuit with list."""
+ params = [0.123, 0.456, (0.789, 0.321)]
+
+ c = Circuit(3)
+ if trainable:
+ c.add(gates.RX(0, theta=0, trainable=trainable))
+ else:
+ c.add(gates.RX(0, theta=params[0], trainable=trainable))
+ c.add(gates.RY(1, theta=0))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.fSim(0, 2, theta=0, phi=0))
+ c.add(gates.H(2))
+ if trainable:
+ c.set_parameters(params)
+ assert c.queue[0].parameters == (params[0],)
+ else:
+ c.set_parameters(params[1:])
+ assert c.queue[1].parameters == (params[1],)
+ assert c.queue[3].parameters == params[2]
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_circuit_set_parameters_with_dictionary(trainable):
+ """Check updating parameters of circuit with list."""
+ params = [0.123, 0.456, 0.789]
+
+ c1 = Circuit(3)
+ c1.add(gates.X(0))
+ c1.add(gates.X(2))
+ if trainable:
+ c1.add(gates.U1(0, theta=0, trainable=trainable))
+ else:
+ c1.add(gates.U1(0, theta=params[0], trainable=trainable))
+ c2 = Circuit(3)
+ c2.add(gates.RZ(1, theta=0))
+ c2.add(gates.CZ(1, 2))
+ c2.add(gates.CU1(0, 2, theta=0))
+ c2.add(gates.H(2))
+ c = c1 + c2
+
+ if trainable:
+ params_dict = {c.queue[i]: p for i, p in zip([2, 3, 5], params)}
+ c.set_parameters(params_dict)
+ assert c.queue[2].parameters == (params[0],)
+ else:
+ params_dict = {c.queue[3]: params[1], c.queue[5]: params[2]}
+ c.set_parameters(params_dict)
+ assert c.queue[3].parameters == (params[1],)
+ assert c.queue[5].parameters == (params[2],)
+
+ # test not passing all parametrized gates
+ c.set_parameters({c.queue[5]: 0.7891})
+ if trainable:
+ assert c.queue[2].parameters == (params[0],)
+ assert c.queue[3].parameters == (params[1],)
+ assert c.queue[5].parameters == (0.7891,)
+
+
+def test_circuit_set_parameters_errors():
+ """Check updating parameters errors."""
+ c = Circuit(2)
+ c.add(gates.RX(0, theta=0.789))
+ c.add(gates.RX(1, theta=0.789))
+ c.add(gates.fSim(0, 1, theta=0.123, phi=0.456))
+
+ with pytest.raises(KeyError):
+ c.set_parameters({gates.RX(0, theta=1.0): 0.568})
+ with pytest.raises(ValueError):
+ c.set_parameters([0.12586])
+ with pytest.raises(TypeError):
+ c.set_parameters({0.3568})
+ with pytest.raises(ValueError):
+ c.queue[2].parameters = [0.1234, 0.4321, 0.156]
+
+
+def test_circuit_draw():
+ """Test circuit text draw."""
+ ref = (
+ "q0: ─H─U1─U1─U1─U1───────────────────────────x───\n"
+ "q1: ───o──|──|──|──H─U1─U1─U1────────────────|─x─\n"
+ "q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─\n"
+ "q3: ─────────o──|───────o──|────o──|──H─U1───|─x─\n"
+ "q4: ────────────o──────────o───────o────o──H─x───"
+ )
+ circuit = Circuit(5)
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ circuit.add(gates.CU1(i2, i1, theta=0))
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+
+ assert circuit.draw() == ref
+
+
+def test_circuit_wire_names_errors():
+ with pytest.raises(TypeError):
+ circuit = Circuit(5, wire_names=1)
+ with pytest.raises(ValueError):
+ circuit = Circuit(5, wire_names=["a", "b", "c"])
+ with pytest.raises(ValueError):
+ circuit = Circuit(2, wire_names={"q0": "1", "q1": "2", "q2": "3"})
+ with pytest.raises(ValueError):
+ circuit = Circuit(2, wire_names={"q0": "1", "q1": 2})
+ with pytest.raises(ValueError):
+ circuit = Circuit(2, wire_names=["1", 2])
+
+
+def test_circuit_draw_wire_names():
+ """Test circuit text draw."""
+ ref = (
+ "a : ─H─U1─U1─U1─U1───────────────────────────x───\n"
+ "b : ───o──|──|──|──H─U1─U1─U1────────────────|─x─\n"
+ "hello: ──────o──|──|────o──|──|──H─U1─U1────────|─|─\n"
+ "1 : ─────────o──|───────o──|────o──|──H─U1───|─x─\n"
+ "q4 : ────────────o──────────o───────o────o──H─x───"
+ )
+ circuit = Circuit(5, wire_names=["a", "b", "hello", "1", "q4"])
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ circuit.add(gates.CU1(i2, i1, theta=0))
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+
+ assert circuit.draw() == ref
+
+
+def test_circuit_draw_line_wrap():
+ """Test circuit text draw with line wrap."""
+ ref_line_wrap_50 = (
+ "q0: ─H─U1─U1─U1─U1───────────────────────────x───I───f ...\n"
+ "q1: ───o──|──|──|──H─U1─U1─U1────────────────|─x─I───| ...\n"
+ "q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─────| ...\n"
+ "q3: ─────────o──|───────o──|────o──|──H─U1───|─x───M─| ...\n"
+ "q4: ────────────o──────────o───────o────o──H─x───────f ...\n"
+ "\n"
+ "q0: ... ─o────gf───M─\n"
+ "q1: ... ─U3───|──o─M─\n"
+ "q2: ... ────X─gf─o─M─\n"
+ "q3: ... ────o────o───\n"
+ "q4: ... ────o────X───"
+ )
+
+ ref_line_wrap_30 = (
+ "q0: ─H─U1─U1─U1─U1──────────────── ...\n"
+ "q1: ───o──|──|──|──H─U1─U1─U1───── ...\n"
+ "q2: ──────o──|──|────o──|──|──H─U1 ...\n"
+ "q3: ─────────o──|───────o──|────o─ ...\n"
+ "q4: ────────────o──────────o────── ...\n"
+ "\n"
+ "q0: ... ───────────x───I───f─o────gf── ...\n"
+ "q1: ... ───────────|─x─I───|─U3───|──o ...\n"
+ "q2: ... ─U1────────|─|─────|────X─gf─o ...\n"
+ "q3: ... ─|──H─U1───|─x───M─|────o────o ...\n"
+ "q4: ... ─o────o──H─x───────f────o────X ...\n"
+ "\n"
+ "q0: ... ─M─\n"
+ "q1: ... ─M─\n"
+ "q2: ... ─M─\n"
+ "q3: ... ───\n"
+ "q4: ... ───"
+ )
+
+ import numpy as np
+
+ circuit = Circuit(5)
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ circuit.add(gates.CU1(i2, i1, theta=0))
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+ circuit.add(gates.I(*range(2)))
+ circuit.add(gates.M(3, collapse=True))
+ circuit.add(gates.fSim(0, 4, 0, 0))
+ circuit.add(gates.CU3(0, 1, 0, 0, 0))
+ circuit.add(gates.TOFFOLI(4, 3, 2))
+ circuit.add(gates.GeneralizedfSim(0, 2, np.eye(2), 0))
+ circuit.add(gates.X(4).controlled_by(1, 2, 3))
+ circuit.add(gates.M(*range(3)))
+ assert circuit.draw(line_wrap=50) == ref_line_wrap_50
+ assert circuit.draw(line_wrap=30) == ref_line_wrap_30
+
+
+def test_circuit_draw_line_wrap_names():
+ """Test circuit text draw with line wrap."""
+ ref_line_wrap_50 = (
+ "q0: ─H─U1─U1─U1─U1───────────────────────────x───I───f ...\n"
+ "a : ───o──|──|──|──H─U1─U1─U1────────────────|─x─I───| ...\n"
+ "q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─────| ...\n"
+ "q3: ─────────o──|───────o──|────o──|──H─U1───|─x───M─| ...\n"
+ "q4: ────────────o──────────o───────o────o──H─x───────f ...\n"
+ "\n"
+ "q0: ... ─o────gf───M─\n"
+ "a : ... ─U3───|──o─M─\n"
+ "q2: ... ────X─gf─o─M─\n"
+ "q3: ... ────o────o───\n"
+ "q4: ... ────o────X───"
+ )
+
+ ref_line_wrap_30 = (
+ "q0: ─H─U1─U1─U1─U1──────────────── ...\n"
+ "a : ───o──|──|──|──H─U1─U1─U1───── ...\n"
+ "q2: ──────o──|──|────o──|──|──H─U1 ...\n"
+ "q3: ─────────o──|───────o──|────o─ ...\n"
+ "q4: ────────────o──────────o────── ...\n"
+ "\n"
+ "q0: ... ───────────x───I───f─o────gf── ...\n"
+ "a : ... ───────────|─x─I───|─U3───|──o ...\n"
+ "q2: ... ─U1────────|─|─────|────X─gf─o ...\n"
+ "q3: ... ─|──H─U1───|─x───M─|────o────o ...\n"
+ "q4: ... ─o────o──H─x───────f────o────X ...\n"
+ "\n"
+ "q0: ... ─M─\n"
+ "a : ... ─M─\n"
+ "q2: ... ─M─\n"
+ "q3: ... ───\n"
+ "q4: ... ───"
+ )
+
+ import numpy as np
+
+ circuit = Circuit(5, wire_names={"q1": "a"})
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ circuit.add(gates.CU1(i2, i1, theta=0))
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+ circuit.add(gates.I(*range(2)))
+ circuit.add(gates.M(3, collapse=True))
+ circuit.add(gates.fSim(0, 4, 0, 0))
+ circuit.add(gates.CU3(0, 1, 0, 0, 0))
+ circuit.add(gates.TOFFOLI(4, 3, 2))
+ circuit.add(gates.GeneralizedfSim(0, 2, np.eye(2), 0))
+ circuit.add(gates.X(4).controlled_by(1, 2, 3))
+ circuit.add(gates.M(*range(3)))
+ assert circuit.draw(line_wrap=50) == ref_line_wrap_50
+ assert circuit.draw(line_wrap=30) == ref_line_wrap_30
+
+
+@pytest.mark.parametrize("legend", [True, False])
+def test_circuit_draw_channels(legend):
+ """Check that channels are drawn correctly."""
+
+ circuit = Circuit(2, density_matrix=True)
+ circuit.add(gates.H(0))
+ circuit.add(gates.PauliNoiseChannel(0, list(zip(["X", "Z"], [0.1, 0.2]))))
+ circuit.add(gates.H(1))
+ circuit.add(gates.PauliNoiseChannel(1, list(zip(["Y", "Z"], [0.2, 0.1]))))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.PauliNoiseChannel(0, list(zip(["X", "Z"], [0.1, 0.2]))))
+ circuit.add(gates.PauliNoiseChannel(1, list(zip(["Y", "Z"], [0.2, 0.1]))))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.DepolarizingChannel((0, 1), 0.1))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.DepolarizingChannel((0,), 0.1))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.DepolarizingChannel((1,), 0.1))
+
+ ref = "q0: ─H─PN─o─PN─o─D─o─D─o───\n" "q1: ─H─PN─X─PN─X─D─X───X─D─"
+
+ if legend:
+ ref += (
+ "\n\n Legend for callbacks and channels: \n"
+ "| Gate | Symbol |\n"
+ "|---------------------+----------|\n"
+ "| DepolarizingChannel | D |\n"
+ "| PauliNoiseChannel | PN |"
+ )
+
+ assert circuit.draw(legend=legend) == ref
+
+
+@pytest.mark.parametrize("legend", [True, False])
+def test_circuit_draw_callbacks(legend):
+ """Check that callbacks are drawn correcly."""
+ from qibo.callbacks import EntanglementEntropy
+
+ entropy = EntanglementEntropy([0])
+ c = Circuit(2)
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.H(0))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+
+ ref = "q0: ─EE─H─EE─o─EE─\n" "q1: ─EE───EE─X─EE─"
+
+ if legend:
+ ref += (
+ "\n\n Legend for callbacks and channels: \n"
+ "| Gate | Symbol |\n"
+ "|---------------------+----------|\n"
+ "| EntanglementEntropy | EE |"
+ )
+
+ assert c.draw(legend=legend) == ref
+
+
+def test_circuit_draw_labels():
+ """Test circuit text draw."""
+ ref = (
+ "q0: ─H─G1─G2─G3─G4───────────────────────────x───\n"
+ "q1: ───o──|──|──|──H─G2─G3─G4────────────────|─x─\n"
+ "q2: ──────o──|──|────o──|──|──H─G3─G4────────|─|─\n"
+ "q3: ─────────o──|───────o──|────o──|──H─G4───|─x─\n"
+ "q4: ────────────o──────────o───────o────o──H─x───"
+ )
+ circuit = Circuit(5)
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ gate = gates.CNOT(i2, i1)
+ gate.draw_label = f"G{i2}"
+ circuit.add(gate)
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+ assert circuit.draw() == ref
+
+
+def test_circuit_draw_names():
+ """Test circuit text draw."""
+ ref = (
+ "q0: ─H─cx─cx─cx─cx───────────────────────────x───\n"
+ "q1: ───o──|──|──|──H─cx─cx─cx────────────────|─x─\n"
+ "q2: ──────o──|──|────o──|──|──H─cx─cx────────|─|─\n"
+ "q3: ─────────o──|───────o──|────o──|──H─cx───|─x─\n"
+ "q4: ────────────o──────────o───────o────o──H─x───"
+ )
+ circuit = Circuit(5)
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ gate = gates.CNOT(i2, i1)
+ gate.draw_label = ""
+ circuit.add(gate)
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+ assert circuit.draw() == ref
+
+
+def test_circuit_draw_error():
+ """Test NotImplementedError in circuit draw."""
+ circuit = Circuit(1)
+ error_gate = gates.X(0)
+ error_gate.name = ""
+ error_gate.draw_label = ""
+ circuit.add(error_gate)
+
+ with pytest.raises(NotImplementedError):
+ circuit.draw()
diff --git a/tests/test_models_circuit_backpropagation.py b/tests/test_models_circuit_backpropagation.py
new file mode 100644
index 000000000..49ac6a77c
--- /dev/null
+++ b/tests/test_models_circuit_backpropagation.py
@@ -0,0 +1,66 @@
+"""Tests Tensorflow's backpropagation when using `tf.Variable` parameters."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+
+
+def construct_tensorflow_backend():
+ try:
+ from qibo.backends import construct_backend
+
+ backend = construct_backend("tensorflow")
+ except ModuleNotFoundError: # pragma: no cover
+ pytest.skip(
+ "Skipping backpropagation test because tensorflow is not available."
+ )
+ return backend
+
+
+def test_variable_backpropagation():
+ backend = construct_tensorflow_backend()
+ import tensorflow as tf
+
+ theta = tf.Variable(0.1234, dtype=tf.float64)
+ # TODO: Fix parametrized gates so that `Circuit` can be defined outside
+ # of the gradient tape
+ with tf.GradientTape() as tape:
+ c = Circuit(1)
+ c.add(gates.X(0))
+ c.add(gates.RZ(0, theta))
+ result = backend.execute_circuit(c)
+ loss = tf.math.real(result.state()[-1])
+ grad = tape.gradient(loss, theta)
+ grad = tf.math.real(grad)
+
+ target_loss = np.cos(theta / 2.0)
+ backend.assert_allclose(loss, target_loss)
+
+ target_grad = -np.sin(theta / 2.0) / 2.0
+ backend.assert_allclose(grad, target_grad)
+
+
+def test_two_variables_backpropagation():
+ backend = construct_tensorflow_backend()
+ import tensorflow as tf
+
+ theta = tf.Variable([0.1234, 0.4321], dtype=tf.float64)
+ # TODO: Fix parametrized gates so that `Circuit` can be defined outside
+ # of the gradient tape
+ with tf.GradientTape() as tape:
+ c = Circuit(2)
+ c.add(gates.RX(0, theta[0]))
+ c.add(gates.RY(1, theta[1]))
+ result = backend.execute_circuit(c)
+ loss = tf.math.real(result.state()[0])
+ grad = tape.gradient(loss, theta)
+
+ t = np.array([0.1234, 0.4321]) / 2.0
+ target_loss = np.cos(t[0]) * np.cos(t[1])
+ backend.assert_allclose(loss, target_loss)
+
+ target_grad1 = -np.sin(t[0]) * np.cos(t[1])
+ target_grad2 = -np.cos(t[0]) * np.sin(t[1])
+ target_grad = np.array([target_grad1, target_grad2]) / 2.0
+ backend.assert_allclose(grad, target_grad)
diff --git a/tests/test_models_circuit_execution.py b/tests/test_models_circuit_execution.py
new file mode 100644
index 000000000..297b715a1
--- /dev/null
+++ b/tests/test_models_circuit_execution.py
@@ -0,0 +1,156 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.quantum_info import random_density_matrix
+
+
+def test_eager_execute(backend, accelerators):
+ c = Circuit(4, accelerators)
+ c.add(gates.H(i) for i in range(4))
+ final_state = backend.execute_circuit(c)._state
+ target_state = np.ones(16) / 4.0
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_compiled_execute(backend):
+ def create_circuit(theta=0.1234):
+ c = Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.X(1))
+ c.add(gates.CU1(0, 1, theta))
+ return c
+
+ # Try to compile circuit without gates
+ empty_c = Circuit(2)
+ with pytest.raises(RuntimeError):
+ empty_c.compile()
+
+ # Run eager circuit
+ c1 = create_circuit()
+ r1 = backend.execute_circuit(c1).state()
+
+ # Run compiled circuit
+ c2 = create_circuit()
+ c2.compile(backend)
+ r2 = c2().state()
+ np.testing.assert_allclose(backend.to_numpy(r1), backend.to_numpy(r2))
+
+
+def test_compiling_twice_exception(backend):
+ """Check that compiling a circuit a second time raises error."""
+ c = Circuit(2)
+ c.add([gates.H(0), gates.H(1)])
+ c.compile()
+ with pytest.raises(RuntimeError):
+ c.compile()
+
+
+# TODO: Test circuit execution with measurements
+# TODO: Test compiled circuit execution with measurements
+
+
+@pytest.mark.linux
+def test_memory_error(backend, accelerators):
+ """Check that ``RuntimeError`` is raised if device runs out of memory."""
+ c = Circuit(40, accelerators)
+ c.add(gates.H(i) for i in range(0, 40, 5))
+ with pytest.raises(RuntimeError):
+ final_state = backend.execute_circuit(c)
+
+
+def test_repeated_execute(backend, accelerators):
+ if accelerators is not None:
+ with pytest.raises(NotImplementedError):
+ c = Circuit(4, accelerators, density_matrix=True)
+ else:
+ c = Circuit(4, accelerators, density_matrix=True)
+ thetas = np.random.random(4)
+ c.add((gates.RY(i, t) for i, t in enumerate(thetas)))
+ target_state = backend.execute_circuit(c).state()
+ target_state = target_state
+ c.has_collapse = True
+ if accelerators is not None:
+ with pytest.raises(NotImplementedError):
+ final_state = backend.execute_circuit(c, nshots=20)
+ else:
+ final_state = backend.execute_circuit(c, nshots=20).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_final_state_property(backend):
+ """Check accessing final state using the circuit's property."""
+ c = Circuit(2)
+ c.add([gates.H(0), gates.H(1)])
+
+ with pytest.raises(RuntimeError):
+ final_state = c.final_state
+
+ backend.execute_circuit(c)._state
+ target_state = np.ones(4) / 2
+ backend.assert_allclose(c.final_state, target_state)
+
+
+def test_density_matrix_circuit(backend):
+ initial_rho = random_density_matrix(2**3, backend=backend)
+
+ c = Circuit(3, density_matrix=True)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.H(2))
+ final_rho = backend.execute_circuit(c, backend.np.copy(initial_rho)).state()
+
+ h = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ m1 = np.kron(np.kron(h, h), np.eye(2))
+ m2 = np.kron(cnot, np.eye(2))
+ m3 = np.kron(np.eye(4), h)
+
+ target_rho = np.dot(
+ m1, np.dot(backend.to_numpy(initial_rho), np.transpose(np.conj(m1)))
+ )
+ target_rho = np.dot(m2, np.dot(target_rho, np.transpose(np.conj(m2))))
+ target_rho = np.dot(m3, np.dot(target_rho, np.transpose(np.conj(m3))))
+
+ backend.assert_allclose(final_rho, target_rho)
+
+
+@pytest.mark.parametrize("density_matrix", [True, False])
+def test_circuit_as_initial_state(backend, density_matrix):
+ nqubits = 10
+ c = Circuit(nqubits, density_matrix=density_matrix)
+ c.add(gates.X(i) for i in range(nqubits))
+
+ c1 = Circuit(nqubits, density_matrix=density_matrix)
+ c1.add(gates.H(i) for i in range(nqubits))
+
+ actual_circuit = c1 + c
+
+ output = backend.execute_circuit(c, c1).state()
+ target = backend.execute_circuit(actual_circuit).state()
+
+ backend.assert_allclose(target, output)
+
+
+def test_initial_state_error(backend):
+ nqubits = 10
+ c = Circuit(nqubits)
+ c.add(gates.X(i) for i in range(nqubits))
+
+ c1 = Circuit(nqubits, density_matrix=True)
+ c1.add(gates.H(i) for i in range(nqubits))
+
+ with pytest.raises(ValueError):
+ backend.execute_circuit(c, c1)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_initial_state_shape_error(backend, density_matrix):
+ nqubits = 2
+ c = Circuit(nqubits, density_matrix=density_matrix)
+ c.add(gates.X(i) for i in range(nqubits))
+
+ initial_state = random_density_matrix(2, backend=backend)
+ with pytest.raises(ValueError):
+ backend.execute_circuit(c, initial_state=initial_state)
diff --git a/tests/test_models_circuit_features.py b/tests/test_models_circuit_features.py
new file mode 100644
index 000000000..c0b08f8c5
--- /dev/null
+++ b/tests/test_models_circuit_features.py
@@ -0,0 +1,368 @@
+"""Test how features defined in :class:`qibo.models.circuit.Circuit` work during circuit execution."""
+
+import sys
+from collections import Counter
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.noise import NoiseModel, PauliError
+
+
+def test_circuit_unitary(backend):
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.X(0))
+ c.add(gates.Y(1))
+ final_matrix = c.unitary(backend)
+ h = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ target_matrix = np.kron(matrices.X, matrices.Y) @ cnot @ np.kron(h, h)
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("with_measurement", [False, True])
+def test_circuit_unitary_bigger(backend, with_measurement):
+ c = Circuit(4)
+ c.add(gates.H(i) for i in range(4))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.CNOT(0, 3))
+ if with_measurement:
+ c.add(gates.M(*range(4)))
+ final_matrix = c.unitary(backend)
+ h = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ h = np.kron(np.kron(h, h), np.kron(h, h))
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ cz = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]])
+ m1 = np.kron(cnot, np.eye(4))
+ m2 = np.kron(np.kron(np.eye(2), cz), np.eye(2))
+ m3 = np.kron(cnot, np.eye(4)).reshape(8 * (2,))
+ m3 = np.transpose(m3, [0, 2, 3, 1, 4, 6, 7, 5]).reshape((16, 16))
+ target_matrix = m3 @ m2 @ m1 @ h
+ backend.assert_allclose(final_matrix, target_matrix)
+
+
+def test_circuit_unitary_and_inverse_with_noise_channel(backend):
+ circuit = Circuit(2)
+ circuit.add(gates.H(0))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.DepolarizingChannel([0, 1], 0.2))
+ with pytest.raises(NotImplementedError):
+ circuit.unitary(backend)
+ with pytest.raises(NotImplementedError):
+ circuit.invert()
+
+
+def test_circuit_unitary_non_trivial(backend):
+ target = np.array(
+ [
+ [1, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 1],
+ ],
+ dtype=complex,
+ )
+ target = backend.cast(target, dtype=target.dtype)
+ nqubits = 3
+ circuit = Circuit(nqubits)
+ circuit.add(gates.SWAP(0, 2).controlled_by(1))
+ unitary = circuit.unitary(backend)
+ backend.assert_allclose(unitary, target)
+
+
+@pytest.mark.parametrize("compile", [False, True])
+def test_circuit_vs_gate_execution(backend, compile):
+ """Check consistency between executing circuit and stand alone gates."""
+ nqubits = 2
+ theta = 0.1234
+ target_c = Circuit(nqubits)
+ target_c.add(gates.X(0))
+ target_c.add(gates.X(1))
+ target_c.add(gates.CU1(0, 1, theta))
+ target_result = backend.execute_circuit(target_c)._state
+
+ # custom circuit
+ def custom_circuit(state, theta):
+ state = backend.apply_gate(gates.X(0), state, nqubits)
+ state = backend.apply_gate(gates.X(1), state, nqubits)
+ state = backend.apply_gate(gates.CU1(0, 1, theta), state, nqubits)
+ return state
+
+ initial_state = backend.zero_state(nqubits)
+ if compile:
+ c = backend.compile(custom_circuit)
+ else:
+ c = custom_circuit
+
+ result = c(initial_state, theta)
+ backend.assert_allclose(result, target_result)
+
+
+def test_circuit_addition_execution(backend, accelerators):
+ c1 = Circuit(4, accelerators)
+ c1.add(gates.H(0))
+ c1.add(gates.H(1))
+ c1.add(gates.H(2))
+ c2 = Circuit(4, accelerators)
+ c2.add(gates.CNOT(0, 1))
+ c2.add(gates.CZ(2, 3))
+ c3 = c1 + c2
+
+ c = Circuit(4, accelerators)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.H(2))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CZ(2, 3))
+ backend.assert_circuitclose(c3, c)
+
+
+@pytest.mark.parametrize("deep", [False, True])
+def test_copied_circuit_execution(backend, accelerators, deep):
+ """Check that circuit copy execution is equivalent to original circuit."""
+ theta = 0.1234
+ c1 = Circuit(4, accelerators)
+ c1.add([gates.X(0), gates.X(1), gates.CU1(0, 1, theta)])
+ c1.add([gates.H(2), gates.H(3), gates.CU1(2, 3, theta)])
+ if not deep and accelerators is not None: # pragma: no cover
+ with pytest.raises(ValueError):
+ c2 = c1.copy(deep)
+ else:
+ c2 = c1.copy(deep)
+ backend.assert_circuitclose(c2, c1)
+
+
+@pytest.mark.parametrize("fuse", [False, True])
+def test_inverse_circuit_execution(backend, accelerators, fuse):
+ c = Circuit(4, accelerators)
+ c.add(gates.RX(0, theta=0.1))
+ c.add(gates.U2(1, phi=0.2, lam=0.3))
+ c.add(gates.U3(2, theta=0.1, phi=0.3, lam=0.2))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.fSim(0, 2, theta=0.1, phi=0.3))
+ c.add(gates.CU2(0, 1, phi=0.1, lam=0.1))
+ if fuse:
+ if accelerators: # pragma: no cover
+ with pytest.raises(NotImplementedError):
+ c = c.fuse()
+ else:
+ c = c.fuse()
+ invc = c.invert()
+ target_state = np.ones(2**4) / 4.0
+ final_state = backend.execute_circuit(c, initial_state=np.copy(target_state))._state
+ final_state = backend.execute_circuit(invc, initial_state=final_state)._state
+ backend.assert_allclose(final_state, target_state, atol=1e-6)
+
+
+def test_circuit_invert_and_addition_execution(backend, accelerators):
+ subroutine = Circuit(6)
+ subroutine.add([gates.RX(i, theta=0.1) for i in range(5)])
+ subroutine.add([gates.CZ(i, i + 1) for i in range(0, 5, 2)])
+ middle = Circuit(6)
+ middle.add([gates.CU2(i, i + 1, phi=0.1, lam=0.2) for i in range(0, 5, 2)])
+ circuit = subroutine + middle + subroutine.invert()
+
+ c = Circuit(6)
+ c.add([gates.RX(i, theta=0.1) for i in range(5)])
+ c.add([gates.CZ(i, i + 1) for i in range(0, 5, 2)])
+ c.add([gates.CU2(i, i + 1, phi=0.1, lam=0.2) for i in range(0, 5, 2)])
+ c.add([gates.CZ(i, i + 1) for i in range(0, 5, 2)])
+ c.add([gates.RX(i, theta=-0.1) for i in range(5)])
+
+ assert c.depth == circuit.depth
+ backend.assert_circuitclose(circuit, c)
+
+
+@pytest.mark.parametrize("distribute_small", [False, True])
+def test_circuit_on_qubits_execution(backend, accelerators, distribute_small):
+ if distribute_small:
+ smallc = Circuit(3, accelerators=accelerators)
+ else:
+ smallc = Circuit(3)
+ smallc.add(gates.RX(i, theta=i + 0.1) for i in range(3))
+ smallc.add((gates.CNOT(0, 1), gates.CZ(1, 2)))
+
+ largec = Circuit(6, accelerators=accelerators)
+ largec.add(gates.RY(i, theta=i + 0.2) for i in range(0, 6, 2))
+ largec.add(smallc.on_qubits(1, 3, 5))
+
+ targetc = Circuit(6)
+ targetc.add(gates.RY(i, theta=i + 0.2) for i in range(0, 6, 2))
+ targetc.add(gates.RX(i, theta=i // 2 + 0.1) for i in range(1, 6, 2))
+ targetc.add((gates.CNOT(1, 3), gates.CZ(3, 5)))
+ assert largec.depth == targetc.depth
+ backend.assert_circuitclose(largec, targetc)
+
+
+@pytest.mark.parametrize("distribute_small", [False, True])
+def test_circuit_on_qubits_double_execution(backend, accelerators, distribute_small):
+ if distribute_small:
+ smallc = Circuit(3, accelerators=accelerators)
+ else:
+ smallc = Circuit(3)
+ smallc.add(gates.RX(i, theta=i + 0.1) for i in range(3))
+ smallc.add((gates.CNOT(0, 1), gates.CZ(1, 2)))
+ # execute the small circuit before adding it to the large one
+ _ = backend.execute_circuit(smallc)
+
+ largec = Circuit(6, accelerators=accelerators)
+ largec.add(gates.RY(i, theta=i + 0.2) for i in range(0, 6, 2))
+ if distribute_small and accelerators is not None: # pragma: no cover
+ with pytest.raises(RuntimeError):
+ largec.add(smallc.on_qubits(1, 3, 5))
+ else:
+ largec.add(smallc.on_qubits(1, 3, 5))
+ targetc = Circuit(6)
+ targetc.add(gates.RY(i, theta=i + 0.2) for i in range(0, 6, 2))
+ targetc.add(gates.RX(i, theta=i // 2 + 0.1) for i in range(1, 6, 2))
+ targetc.add((gates.CNOT(1, 3), gates.CZ(3, 5)))
+ assert largec.depth == targetc.depth
+ backend.assert_circuitclose(largec, targetc)
+
+
+def test_circuit_on_qubits_controlled_by_execution(backend, accelerators):
+ smallc = Circuit(3)
+ smallc.add(gates.RX(0, theta=0.1).controlled_by(1, 2))
+ smallc.add(gates.RY(1, theta=0.2).controlled_by(0))
+ smallc.add(gates.RX(2, theta=0.3).controlled_by(1, 0))
+ smallc.add(gates.RZ(1, theta=0.4).controlled_by(0, 2))
+
+ largec = Circuit(6, accelerators=accelerators)
+ largec.add(gates.H(i) for i in range(6))
+ largec.add(smallc.on_qubits(1, 4, 3))
+
+ targetc = Circuit(6)
+ targetc.add(gates.H(i) for i in range(6))
+ targetc.add(gates.RX(1, theta=0.1).controlled_by(3, 4))
+ targetc.add(gates.RY(4, theta=0.2).controlled_by(1))
+ targetc.add(gates.RX(3, theta=0.3).controlled_by(1, 4))
+ targetc.add(gates.RZ(4, theta=0.4).controlled_by(1, 3))
+
+ assert largec.depth == targetc.depth
+ backend.assert_circuitclose(largec, targetc)
+
+
+@pytest.mark.parametrize("controlled", [False, True])
+def test_circuit_on_qubits_with_unitary_execution(backend, accelerators, controlled):
+ unitaries = np.random.random((2, 2, 2))
+ smallc = Circuit(2)
+ if controlled:
+ smallc.add(gates.Unitary(unitaries[0], 0).controlled_by(1))
+ smallc.add(gates.Unitary(unitaries[1], 1).controlled_by(0))
+ else:
+ smallc.add(gates.Unitary(unitaries[0], 0))
+ smallc.add(gates.Unitary(unitaries[1], 1))
+ smallc.add(gates.CNOT(0, 1))
+
+ largec = Circuit(4, accelerators=accelerators)
+ largec.add(gates.RY(0, theta=0.1))
+ largec.add(gates.RY(1, theta=0.2))
+ largec.add(gates.RY(2, theta=0.3))
+ largec.add(gates.RY(3, theta=0.2))
+ largec.add(smallc.on_qubits(3, 0))
+
+ targetc = Circuit(4)
+ targetc.add(gates.RY(0, theta=0.1))
+ targetc.add(gates.RY(1, theta=0.2))
+ targetc.add(gates.RY(2, theta=0.3))
+ targetc.add(gates.RY(3, theta=0.2))
+ if controlled:
+ targetc.add(gates.Unitary(unitaries[0], 3).controlled_by(0))
+ targetc.add(gates.Unitary(unitaries[1], 0).controlled_by(3))
+ else:
+ targetc.add(gates.Unitary(unitaries[0], 3))
+ targetc.add(gates.Unitary(unitaries[1], 0))
+ targetc.add(gates.CNOT(3, 0))
+ assert largec.depth == targetc.depth
+ backend.assert_circuitclose(largec, targetc)
+
+
+def test_circuit_decompose_execution(backend):
+ c = Circuit(6)
+ c.add(gates.RX(0, 0.1234))
+ c.add(gates.RY(1, 0.4321))
+ c.add(gates.H(i) for i in range(2, 6))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.X(3).controlled_by(0, 1, 2, 4))
+ decomp_c = c.decompose(5)
+ backend.assert_circuitclose(c, decomp_c, atol=1e-6)
+
+
+def test_repeated_execute_pauli_noise_channel(backend):
+ thetas = np.random.random(4)
+ backend.set_seed(1234)
+ c = Circuit(4)
+ c.add((gates.RY(i, t) for i, t in enumerate(thetas)))
+ c.add(
+ gates.PauliNoiseChannel(i, list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ for i in range(4)
+ )
+ with pytest.raises(RuntimeError) as excinfo:
+ final_state = backend.execute_circuit(c, nshots=20)
+ assert (
+ str(excinfo.value)
+ == "Attempting to perform noisy simulation with `density_matrix=False` and no Measurement gate in the Circuit. If you wish to retrieve the statistics of the outcomes please include measurements in the circuit, otherwise set `density_matrix=True` to recover the final state."
+ )
+
+
+def test_repeated_execute_with_pauli_noise(backend):
+ thetas = np.random.random(4)
+ c = Circuit(4)
+ c.add((gates.RY(i, t) for i, t in enumerate(thetas)))
+ noisy_c = c.with_pauli_noise(list(zip(["X", "Z"], [0.2, 0.1])))
+ backend.set_seed(1234)
+ with pytest.raises(RuntimeError) as excinfo:
+ final_state = backend.execute_circuit(noisy_c, nshots=20)
+ assert (
+ str(excinfo.value)
+ == "Attempting to perform noisy simulation with `density_matrix=False` and no Measurement gate in the Circuit. If you wish to retrieve the statistics of the outcomes please include measurements in the circuit, otherwise set `density_matrix=True` to recover the final state."
+ )
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="Mac tests")
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_repeated_execute_probs_and_freqs(backend, nqubits):
+ circuit = Circuit(nqubits)
+ circuit.add(gates.X(q) for q in range(nqubits))
+ circuit.add(gates.M(q) for q in range(nqubits))
+
+ noise_map = list(zip(["X", "Y", "Z"], [0.1, 0.1, 0.1]))
+ noise_map = PauliError(noise_map)
+ noise = NoiseModel()
+ noise.add(noise_map, gates.X)
+ noisy_circuit = noise.apply(circuit)
+ backend.set_seed(1234)
+ result = backend.execute_circuit_repeated(noisy_circuit, nshots=1024)
+
+ # Tensorflow seems to yield different results with same seed
+ if backend.__class__.__name__ == "TensorflowBackend":
+ test_frequencies = (
+ Counter({"1": 844, "0": 180})
+ if nqubits == 1
+ else Counter({"11": 674, "10": 155, "01": 154, "00": 41})
+ )
+ elif backend.__class__.__name__ == "PyTorchBackend":
+ test_frequencies = (
+ Counter({"1": 817, "0": 207})
+ if nqubits == 1
+ else Counter({"11": 664, "01": 162, "10": 166, "00": 32})
+ )
+ else:
+ test_frequencies = (
+ Counter({"1": 790, "0": 234})
+ if nqubits == 1
+ else Counter({"11": 618, "10": 169, "01": 185, "00": 52})
+ )
+ for key in dict(test_frequencies).keys():
+ backend.assert_allclose(result.frequencies()[key], test_frequencies[key])
diff --git a/tests/test_models_circuit_fuse.py b/tests/test_models_circuit_fuse.py
new file mode 100644
index 000000000..6ba1ef729
--- /dev/null
+++ b/tests/test_models_circuit_fuse.py
@@ -0,0 +1,239 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+
+
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_fused_gate_construct_unitary(backend, nqubits):
+ gate = gates.FusedGate(0, 1)
+ gate.append(gates.H(0))
+ gate.append(gates.H(1))
+ gate.append(gates.CZ(0, 1))
+ hmatrix = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ czmatrix = np.diag([1, 1, 1, -1])
+ target_matrix = czmatrix @ np.kron(hmatrix, hmatrix)
+ if nqubits > 2:
+ gate.append(gates.TOFFOLI(0, 1, 2))
+ toffoli = np.eye(8)
+ toffoli[-2:, -2:] = np.array([[0, 1], [1, 0]])
+ target_matrix = toffoli @ np.kron(target_matrix, np.eye(2))
+ backend.assert_allclose(gate.matrix(backend), target_matrix)
+
+
+def test_single_fusion_gate():
+ """Check circuit fusion that creates a single ``FusedGate``."""
+ queue = [gates.H(0), gates.X(1), gates.CZ(0, 1)]
+ c = Circuit(2)
+ c.add(queue)
+ c = c.fuse()
+ assert len(c.queue) == 1
+ fgate = c.queue[0]
+ assert fgate.gates[0] == queue[1]
+ assert fgate.gates[1] == queue[0]
+ assert fgate.gates[2] == queue[2]
+
+
+def test_two_fusion_gate():
+ """Check fusion that creates two ``FusedGate``s."""
+ queue = [
+ gates.X(0),
+ gates.H(1),
+ gates.RX(2, theta=0.1234).controlled_by(1),
+ gates.H(2),
+ gates.Y(1),
+ gates.H(0),
+ ]
+ c = Circuit(3)
+ c.add(queue)
+ c = c.fuse()
+ assert len(c.queue) == 2
+ fgate1, fgate2 = c.queue
+ assert fgate2.gates[0] == queue[0]
+ assert fgate2.gates[1] == queue[-1]
+ assert fgate1.gates == [queue[1], queue[2], queue[4], queue[3]]
+
+
+def test_fusedgate_matrix_calculation(backend):
+ queue = [gates.H(0), gates.H(1), gates.CNOT(0, 1), gates.X(0), gates.X(1)]
+ circuit = Circuit(2)
+ circuit.add(queue)
+ circuit = circuit.fuse()
+ assert len(circuit.queue) == 1
+ fused_gate = circuit.queue[0]
+
+ x = np.array([[0, 1], [1, 0]])
+ h = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
+ cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ target_matrix = np.kron(x, x) @ cnot @ np.kron(h, h)
+ fused_matrix = fused_gate.matrix(backend)
+ backend.assert_allclose(fused_matrix, target_matrix)
+
+
+def test_fuse_circuit_two_qubit_gates(backend):
+ """Check circuit fusion in circuit with two-qubit gates only."""
+ c = Circuit(2)
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.RX(0, theta=0.1234).controlled_by(1))
+ c.add(gates.SWAP(0, 1))
+ c.add(gates.fSim(1, 0, theta=0.1234, phi=0.324))
+ c.add(gates.RY(1, theta=0.1234).controlled_by(0))
+ fused_c = c.fuse()
+ backend.assert_circuitclose(fused_c, c)
+
+
+@pytest.mark.parametrize("max_qubits", [2, 3, 4])
+def test_fuse_circuit_three_qubit_gate(backend, max_qubits):
+ """Check circuit fusion in circuit with three-qubit gate."""
+ c = Circuit(4)
+ c.add(gates.H(i) for i in range(4))
+ c.add(gates.CZ(0, 1))
+ c.add(gates.CZ(2, 3))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ c.add(gates.SWAP(1, 2))
+ c.add(gates.H(i) for i in range(4))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CZ(2, 3))
+ fused_c = c.fuse(max_qubits=max_qubits)
+ backend.assert_circuitclose(fused_c, c, atol=1e-12)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5, 10, 11])
+@pytest.mark.parametrize("nlayers", [1, 2])
+@pytest.mark.parametrize("max_qubits", [2, 3, 4])
+def test_variational_layer_fusion(backend, nqubits, nlayers, max_qubits):
+ """Check fused variational layer execution."""
+ theta = 2 * np.pi * np.random.random((2 * nlayers * nqubits,))
+ theta_iter = iter(theta)
+
+ c = Circuit(nqubits)
+ for _ in range(nlayers):
+ c.add(gates.RY(i, next(theta_iter)) for i in range(nqubits))
+ c.add(gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))
+ c.add(gates.RY(i, next(theta_iter)) for i in range(nqubits))
+ c.add(gates.CZ(i, i + 1) for i in range(1, nqubits - 1, 2))
+ c.add(gates.CZ(0, nqubits - 1))
+
+ fused_c = c.fuse(max_qubits=max_qubits)
+ backend.assert_circuitclose(fused_c, c)
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+@pytest.mark.parametrize("ngates", [10, 20])
+@pytest.mark.parametrize("max_qubits", [2, 3, 4])
+def test_random_circuit_fusion(backend, nqubits, ngates, max_qubits):
+ """Check gate fusion in randomly generated circuits."""
+ one_qubit_gates = [gates.RX, gates.RY, gates.RZ]
+ two_qubit_gates = [gates.CNOT, gates.CZ, gates.SWAP]
+ thetas = np.pi * np.random.random((ngates,))
+ c = Circuit(nqubits)
+ for i in range(ngates):
+ gate = one_qubit_gates[int(np.random.randint(0, 3))]
+ q0 = np.random.randint(0, nqubits)
+ c.add(gate(q0, thetas[i]))
+ gate = two_qubit_gates[int(np.random.randint(0, 3))]
+ q0, q1 = np.random.randint(0, nqubits, (2,))
+ while q0 == q1:
+ q0, q1 = np.random.randint(0, nqubits, (2,))
+ c.add(gate(q0, q1))
+ fused_c = c.fuse(max_qubits=max_qubits)
+ backend.assert_circuitclose(fused_c, c, atol=1e-7)
+
+
+def test_controlled_by_gates_fusion(backend):
+ """Check circuit fusion that contains ``controlled_by`` gates."""
+ c = Circuit(4)
+ c.add(gates.H(i) for i in range(4))
+ c.add(gates.RX(1, theta=0.1234).controlled_by(0))
+ c.add(gates.RX(3, theta=0.4321).controlled_by(2))
+ c.add(gates.RY(i, theta=0.5678) for i in range(4))
+ c.add(gates.RX(1, theta=0.1234).controlled_by(0))
+ c.add(gates.RX(3, theta=0.4321).controlled_by(2))
+ fused_c = c.fuse()
+ backend.assert_circuitclose(fused_c, c)
+
+
+def test_callbacks_fusion(backend):
+ """Check entropy calculation in fused circuit."""
+ from qibo import callbacks
+
+ entropy = callbacks.EntanglementEntropy([0])
+ c = Circuit(5)
+ c.add(gates.H(0))
+ c.add(gates.X(1))
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.CallbackGate(entropy))
+ fused_c = c.fuse()
+ backend.assert_circuitclose(fused_c, c)
+ target_entropy = [0.0, 1.0, 0.0, 1.0]
+ final_entropy = [backend.to_numpy(x) for x in entropy[:]]
+ backend.assert_allclose(final_entropy, target_entropy, atol=1e-7)
+
+
+def test_set_parameters_fusion(backend):
+ """Check gate fusion when ``circuit.set_parameters`` is used."""
+ c = Circuit(2)
+ c.add(gates.RX(0, theta=0.1234))
+ c.add(gates.RX(1, theta=0.1234))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.RY(0, theta=0.1234))
+ c.add(gates.RY(1, theta=0.1234))
+ fused_c = c.fuse()
+ backend.assert_circuitclose(fused_c, c)
+
+ c.set_parameters(4 * [0.4321])
+ fused_c.set_parameters(4 * [0.4321])
+ backend.assert_circuitclose(fused_c, c)
+
+
+@pytest.mark.parametrize("max_qubits", [1, 2, 3])
+def test_fusion_with_measurements(backend, max_qubits):
+ c = Circuit(3, density_matrix=True)
+ c.add(gates.X(i) for i in range(3))
+ c.add(gates.M(0))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.H(2))
+ c.add(gates.M(0, 1, 2))
+ fused_c = c.fuse(max_qubits=max_qubits)
+ assert fused_c.measurements == c.measurements
+ backend.assert_circuitclose(fused_c, c)
+
+
+def test_add_fused_gate(backend):
+ """Check adding fused gate to a circuit."""
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(0, 1))
+ fused_c = c.fuse()
+ fgate = fused_c.queue[0]
+ assert isinstance(fgate, gates.FusedGate)
+
+ new_c = Circuit(2)
+ new_c.add(fgate)
+ assert c.depth == 2
+ assert c.ngates == 3
+ assert new_c.depth == 1
+ assert new_c.ngates == 1
+ backend.assert_circuitclose(fused_c, c)
+ backend.assert_circuitclose(new_c, c)
+
+
+def test_fused_gate_draw():
+ ref = (
+ "q0: ─[─H─U1───]─U1─U1─U1─────────────────────────────────────x───\n"
+ "q1: ─[───o──H─]─|──|──|──[─U1───]─U1─U1──────────────────────|─x─\n"
+ "q2: ────────────o──|──|──[─o──H─]─|──|──[─U1───]─U1──────────|─|─\n"
+ "q3: ───────────────o──|───────────o──|──[─o──H─]─|──[─U1───]─|─x─\n"
+ "q4: ──────────────────o──────────────o───────────o──[─o──H─]─x───"
+ )
+ circuit = Circuit(5)
+ for i1 in range(5):
+ circuit.add(gates.H(i1))
+ for i2 in range(i1 + 1, 5):
+ circuit.add(gates.CU1(i2, i1, theta=0))
+ circuit.add(gates.SWAP(0, 4))
+ circuit.add(gates.SWAP(1, 3))
+ circuit = circuit.fuse()
+ assert circuit.draw() == ref
diff --git a/tests/test_models_circuit_noise.py b/tests/test_models_circuit_noise.py
new file mode 100644
index 000000000..74a14fb6b
--- /dev/null
+++ b/tests/test_models_circuit_noise.py
@@ -0,0 +1,250 @@
+"""Test :class:`qibo.models.circuit.Circuit` for density matrix and noise simulation."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info import (
+ random_clifford,
+ random_density_matrix,
+ random_statevector,
+ random_unitary,
+)
+
+
+def test_pauli_noise_channel(backend):
+ from qibo import matrices
+
+ c = Circuit(2, density_matrix=True)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.PauliNoiseChannel(0, list(zip(["X", "Z"], [0.5, 0.3]))))
+ c.add(gates.PauliNoiseChannel(1, list(zip(["Y", "Z"], [0.1, 0.3]))))
+ final_rho = backend.execute_circuit(c)._state
+
+ psi = np.ones(4) / 2
+ rho = np.outer(psi, psi.conj())
+ m1 = np.kron(matrices.X, matrices.I)
+ m2 = np.kron(matrices.Z, matrices.I)
+
+ rho = 0.2 * rho + 0.5 * m1.dot(rho.dot(m1)) + 0.3 * m2.dot(rho.dot(m2))
+ m1 = np.kron(matrices.I, matrices.Y)
+ m2 = np.kron(matrices.I, matrices.Z)
+ rho = 0.6 * rho + 0.1 * m1.dot(rho.dot(m1)) + 0.3 * m2.dot(rho.dot(m2))
+ backend.assert_allclose(final_rho, rho)
+
+
+def test_noisy_circuit_reexecution(backend):
+ c = Circuit(2, density_matrix=True)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.PauliNoiseChannel(0, [("X", 0.5)]))
+ c.add(gates.PauliNoiseChannel(1, [("Z", 0.3)]))
+ final_rho = backend.execute_circuit(c).state()
+ final_rho2 = backend.execute_circuit(c).state()
+ backend.assert_allclose(final_rho, final_rho2)
+
+
+def test_circuit_with_pauli_noise_gates():
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
+ noisy_c = c.with_pauli_noise(list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ assert noisy_c.depth == 4
+ assert noisy_c.ngates == 7
+ for i in [1, 3, 5, 6]:
+ assert noisy_c.queue[i].__class__.__name__ == "PauliNoiseChannel"
+
+
+def test_circuit_with_pauli_noise_execution(backend):
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1)])
+ noisy_c = c.with_pauli_noise(list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ final_state = backend.execute_circuit(noisy_c).state()
+
+ target_c = Circuit(2, density_matrix=True)
+ target_c.add(gates.H(0))
+ target_c.add(
+ gates.PauliNoiseChannel(0, list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ )
+ target_c.add(gates.H(1))
+ target_c.add(
+ gates.PauliNoiseChannel(1, list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.3])))
+ )
+ target_state = backend.execute_circuit(target_c).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_circuit_with_pauli_noise_measurements(backend):
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1)])
+ c.add(gates.M(0))
+ noisy_c = c.with_pauli_noise(list(zip(["X", "Y", "Z"], [0.1, 0.1, 0.1])))
+ final_state = backend.execute_circuit(noisy_c).state()
+
+ target_c = Circuit(2, density_matrix=True)
+ target_c.add(gates.H(0))
+ target_c.add(
+ gates.PauliNoiseChannel(0, list(zip(["X", "Y", "Z"], [0.1, 0.1, 0.1])))
+ )
+ target_c.add(gates.H(1))
+ target_c.add(
+ gates.PauliNoiseChannel(1, list(zip(["X", "Y", "Z"], [0.1, 0.1, 0.1])))
+ )
+ target_state = backend.execute_circuit(target_c).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_circuit_with_pauli_noise_noise_map(backend):
+ noise_map = {
+ 0: list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.1])),
+ 1: list(zip(["X", "Y", "Z"], [0.2, 0.3, 0.0])),
+ 2: list(zip(["X", "Y", "Z"], [0.0, 0.0, 0.0])),
+ }
+
+ c = Circuit(3, density_matrix=True)
+ c.add([gates.H(0), gates.H(1), gates.X(2)])
+ c.add(gates.M(2))
+ noisy_c = c.with_pauli_noise(noise_map)
+ final_state = backend.execute_circuit(noisy_c).state()
+
+ target_c = Circuit(3, density_matrix=True)
+ target_c.add(gates.H(0))
+ target_c.add(
+ gates.PauliNoiseChannel(0, list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.1])))
+ )
+ target_c.add(gates.H(1))
+ target_c.add(
+ gates.PauliNoiseChannel(1, list(zip(["X", "Y", "Z"], [0.2, 0.3, 0.0])))
+ )
+ target_c.add(gates.X(2))
+ target_state = backend.execute_circuit(target_c).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_circuit_with_pauli_noise_errors():
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1), gates.PauliNoiseChannel(0, [("X", 0.2)])])
+ with pytest.raises(ValueError):
+ noisy_c = c.with_pauli_noise(list(zip(["X", "Y"], [0.2, 0.3])))
+ c = Circuit(2, density_matrix=True)
+ c.add([gates.H(0), gates.H(1)])
+ with pytest.raises(ValueError):
+ noisy_c = c.with_pauli_noise({0: list(zip(["X", "Y", "Z"], [0.2, 0.3, 0.1]))})
+ with pytest.raises(TypeError):
+ noisy_c = c.with_pauli_noise({0, 1})
+
+
+def test_density_matrix_circuit_measurement(backend):
+ """Check measurement gate on density matrices using circuit."""
+ from .test_measurements import assert_register_result, assert_result
+
+ state = np.zeros(16)
+ state[0] = 1
+ init_rho = np.outer(state, state.conj())
+
+ c = Circuit(4, density_matrix=True)
+ c.add(gates.X(1))
+ c.add(gates.X(3))
+ c.add(gates.M(0, 1, register_name="A"))
+ c.add(gates.M(3, 2, register_name="B"))
+ result = backend.execute_circuit(c, init_rho, nshots=100)
+
+ target_binary_samples = np.zeros((100, 4))
+ target_binary_samples[:, 1] = 1
+ target_binary_samples[:, 2] = 1
+ assert_result(
+ backend,
+ result,
+ decimal_samples=6 * np.ones((100,)),
+ binary_samples=target_binary_samples,
+ decimal_frequencies={6: 100},
+ binary_frequencies={"0110": 100},
+ )
+
+ target = {}
+ target["decimal_samples"] = {"A": np.ones((100,)), "B": 2 * np.ones((100,))}
+ target["binary_samples"] = {"A": np.zeros((100, 2)), "B": np.zeros((100, 2))}
+ target["binary_samples"]["A"][:, 1] = 1
+ target["binary_samples"]["B"][:, 0] = 1
+ target["decimal_frequencies"] = {"A": {1: 100}, "B": {2: 100}}
+ target["binary_frequencies"] = {"A": {"01": 100}, "B": {"10": 100}}
+ assert_register_result(backend, result, **target)
+
+
+def test_circuit_add_sampling(backend):
+ """Check measurements when simulating added circuits with noise"""
+ # Create random noisy circuit and add noiseless inverted circuit
+ gates_set = [gates.X, gates.Y, gates.Z, gates.H, gates.S, gates.SDG, gates.I]
+ circ = Circuit(1)
+ circ_no_noise = Circuit(1)
+
+ for _ in range(10):
+ new_gate = np.random.choice(gates_set)(0)
+ circ.add(gates.PauliNoiseChannel(0, [("Z", 0.01)]))
+ circ.add(new_gate)
+ circ_no_noise.add(new_gate)
+
+ circ.add(gates.PauliNoiseChannel(0, [("Z", 0.01)]))
+ circ += circ_no_noise.invert()
+ measurement = circ.add(gates.M(0))
+
+ # Sampling using 10 shots
+ np.random.seed(123)
+ backend.set_seed(123)
+ samples = backend.execute_circuit(circ, nshots=10).samples()
+
+ # Sampling using 1 shot in for loop
+ target_samples = []
+ backend.set_seed(123)
+ np.random.seed(123)
+ for _ in range(10):
+ measurement.reset()
+ result = backend.execute_circuit(circ, nshots=1)
+ target_samples.append(result.samples())
+
+ target_samples = np.stack(target_samples)
+
+ backend.assert_allclose(samples, target_samples[:, 0])
+
+
+@pytest.mark.parametrize("nqubits", [2, 4, 6])
+def test_probabilities_repeated_execution(backend, nqubits):
+ probabilities = list(np.random.rand(nqubits + 1)) + [1.0]
+ probabilities /= np.sum(probabilities)
+
+ unitaries = [random_unitary(2**1, backend=backend) for _ in range(nqubits)]
+ unitaries += [random_unitary(2**nqubits, backend=backend)]
+
+ qubits_list = [(q,) for q in range(nqubits)]
+ qubits_list += [tuple(q for q in range(nqubits))]
+
+ circuit = random_clifford(nqubits, return_circuit=True, backend=backend)
+ circuit.add(gates.UnitaryChannel(qubits_list, list(zip(probabilities, unitaries))))
+ circuit.add(gates.M(*range(nqubits)))
+
+ circuit_density_matrix = circuit.copy(deep=True)
+ circuit_density_matrix.density_matrix = True
+
+ state = random_density_matrix(2**nqubits, backend=backend)
+
+ # set has_collapse=True just to trigger the repeated execution
+ # with density_matrix=True
+ circuit.has_collapse = True
+ # if we don't set density_matrix=True a MeasurementOutcomes object
+ # is returned, which doesn't have any probabilities() method.
+ circuit.density_matrix = True
+
+ result = backend.execute_circuit_repeated(
+ circuit, initial_state=state, nshots=int(1e2)
+ )
+ result = result.probabilities()
+
+ result_density_matrix = backend.execute_circuit(
+ circuit_density_matrix,
+ initial_state=state,
+ nshots=int(1e2),
+ )
+ result_density_matrix = result_density_matrix.probabilities()
+
+ backend.assert_allclose(result, result_density_matrix, rtol=2e-2, atol=5e-3)
diff --git a/tests/test_models_circuit_parametrized.py b/tests/test_models_circuit_parametrized.py
new file mode 100644
index 000000000..1a761f16e
--- /dev/null
+++ b/tests/test_models_circuit_parametrized.py
@@ -0,0 +1,240 @@
+"""Test :meth:`qibo.models.circuit.Circuit.get_parameters` and :meth:`qibo.models.circuit.Circuit.set_parameters`."""
+
+import sys
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+
+
+def test_rx_parameter_setter(backend):
+ """Check the parameter setter of RX gate."""
+
+ def exact_state(theta):
+ phase = np.exp(1j * theta / 2.0)
+ gate = np.array(
+ [[phase.real, -1j * phase.imag], [-1j * phase.imag, phase.real]]
+ )
+ return gate.dot(np.ones(2)) / np.sqrt(2)
+
+ theta = 0.1234
+ gate = gates.RX(0, theta=theta)
+ initial_state = backend.cast(np.ones(2) / np.sqrt(2))
+ final_state = backend.apply_gate(gate, initial_state, 1)
+ target_state = exact_state(theta)
+ backend.assert_allclose(final_state, target_state)
+
+ theta = 0.4321
+ gate.parameters = theta
+ initial_state = backend.cast(np.ones(2) / np.sqrt(2))
+ final_state = backend.apply_gate(gate, initial_state, 1)
+ target_state = exact_state(theta)
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_set_parameters_with_list(backend, trainable):
+ """Check updating parameters of circuit with list."""
+ params = [0.123, 0.456, (0.789, 0.321)]
+ c = Circuit(3)
+ if trainable:
+ c.add(gates.RX(0, theta=0, trainable=trainable))
+ else:
+ c.add(gates.RX(0, theta=params[0], trainable=trainable))
+ c.add(gates.RY(1, theta=0))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.fSim(0, 2, theta=0, phi=0))
+ c.add(gates.H(2))
+ # execute once
+ final_state = backend.execute_circuit(c)
+
+ target_c = Circuit(3)
+ target_c.add(gates.RX(0, theta=params[0]))
+ target_c.add(gates.RY(1, theta=params[1]))
+ target_c.add(gates.CZ(1, 2))
+ target_c.add(gates.fSim(0, 2, theta=params[2][0], phi=params[2][1]))
+ target_c.add(gates.H(2))
+
+ # Attempt using a flat np.ndarray/list
+ for new_params in (np.random.random(4), list(np.random.random(4))):
+ if trainable:
+ c.set_parameters(new_params)
+ else:
+ new_params[0] = params[0]
+ c.set_parameters(new_params[1:])
+ target_params = [new_params[0], new_params[1], (new_params[2], new_params[3])]
+ target_c.set_parameters(target_params)
+ backend.assert_circuitclose(c, target_c)
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_circuit_set_parameters_ungates(backend, trainable, accelerators):
+ """Check updating parameters of circuit with list."""
+ params = [0.1, 0.2, 0.3, (0.4, 0.5), (0.6, 0.7, 0.8)]
+ if trainable:
+ trainable_params = list(params)
+ else:
+ trainable_params = [0.1, 0.3, (0.4, 0.5)]
+
+ c = Circuit(3, accelerators)
+ c.add(gates.RX(0, theta=0))
+ if trainable:
+ c.add(gates.CRY(0, 1, theta=0, trainable=trainable))
+ else:
+ c.add(gates.CRY(0, 1, theta=params[1], trainable=trainable))
+ c.add(gates.CZ(1, 2))
+ c.add(gates.U1(2, theta=0))
+ c.add(gates.CU2(0, 2, phi=0, lam=0))
+ if trainable:
+ c.add(gates.U3(1, theta=0, phi=0, lam=0, trainable=trainable))
+ else:
+ c.add(gates.U3(1, *params[4], trainable=trainable))
+ # execute once
+ final_state = backend.execute_circuit(c)
+
+ target_c = Circuit(3)
+ target_c.add(gates.RX(0, theta=params[0]))
+ target_c.add(gates.CRY(0, 1, theta=params[1]))
+ target_c.add(gates.CZ(1, 2))
+ target_c.add(gates.U1(2, theta=params[2]))
+ target_c.add(gates.CU2(0, 2, *params[3]))
+ target_c.add(gates.U3(1, *params[4]))
+ c.set_parameters(trainable_params)
+ backend.assert_circuitclose(c, target_c)
+
+ # Attempt using a flat list
+ npparams = np.random.random(8)
+ if trainable:
+ trainable_params = np.copy(npparams)
+ else:
+ npparams[1] = params[1]
+ npparams[5:] = params[4]
+ trainable_params = np.delete(npparams, [1, 5, 6, 7])
+ target_c = Circuit(3)
+ target_c.add(gates.RX(0, theta=npparams[0]))
+ target_c.add(gates.CRY(0, 1, theta=npparams[1]))
+ target_c.add(gates.CZ(1, 2))
+ target_c.add(gates.U1(2, theta=npparams[2]))
+ target_c.add(gates.CU2(0, 2, *npparams[3:5]))
+ target_c.add(gates.U3(1, *npparams[5:]))
+ c.set_parameters(trainable_params)
+ backend.assert_circuitclose(c, target_c)
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_circuit_set_parameters_with_unitary(backend, trainable, accelerators):
+ """Check updating parameters of circuit that contains ``Unitary`` gate."""
+ params = [0.1234, np.random.random((4, 4))]
+ c = Circuit(4, accelerators)
+ c.add(gates.RX(0, theta=0))
+ if trainable:
+ c.add(gates.Unitary(np.zeros((4, 4)), 1, 2, trainable=trainable))
+ trainable_params = list(params)
+ else:
+ c.add(gates.Unitary(params[1], 1, 2, trainable=trainable))
+ trainable_params = [params[0]]
+ # execute once
+ final_state = backend.execute_circuit(c)
+
+ target_c = Circuit(4)
+ target_c.add(gates.RX(0, theta=params[0]))
+ target_c.add(gates.Unitary(params[1], 1, 2))
+ c.set_parameters(trainable_params)
+ backend.assert_circuitclose(c, target_c)
+
+ # Attempt using a flat list / np.ndarray
+ new_params = np.random.random(17)
+ if trainable:
+ c.set_parameters(new_params)
+ else:
+ c.set_parameters(new_params[:1])
+ new_params[1:] = params[1].ravel()
+ target_c = Circuit(4)
+ target_c.add(gates.RX(0, theta=new_params[0]))
+ target_c.add(gates.Unitary(new_params[1:].reshape((4, 4)), 1, 2))
+ backend.assert_circuitclose(c, target_c)
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_set_parameters_with_gate_fusion(backend, trainable):
+ """Check updating parameters of fused circuit."""
+ params = np.random.random(9)
+ c = Circuit(5)
+ c.add(gates.RX(0, theta=params[0], trainable=trainable))
+ c.add(gates.RY(1, theta=params[1]))
+ c.add(gates.CZ(0, 1))
+ c.add(gates.RX(2, theta=params[2]))
+ c.add(gates.RY(3, theta=params[3], trainable=trainable))
+ c.add(gates.fSim(2, 3, theta=params[4], phi=params[5]))
+ c.add(gates.RX(4, theta=params[6]))
+ c.add(gates.RZ(0, theta=params[7], trainable=trainable))
+ c.add(gates.RZ(1, theta=params[8]))
+ fused_c = c.fuse()
+ backend.assert_circuitclose(fused_c, c)
+
+ if trainable:
+ new_params = np.random.random(9)
+ new_params_list = list(new_params[:4])
+ new_params_list.append((new_params[4], new_params[5]))
+ new_params_list.extend(new_params[6:])
+ else:
+ new_params = np.random.random(9)
+ new_params_list = list(new_params[1:3])
+ new_params_list.append((new_params[4], new_params[5]))
+ new_params_list.append(new_params[6])
+ new_params_list.append(new_params[8])
+
+ c.set_parameters(new_params_list)
+ fused_c.set_parameters(new_params_list)
+ backend.assert_circuitclose(fused_c, c)
+
+
+@pytest.mark.parametrize("trainable", [True, False])
+def test_set_parameters_with_light_cone(backend, trainable):
+ """Check updating parameters of light cone circuit."""
+ params = np.random.random(4)
+ c = Circuit(4)
+ c.add(gates.RX(0, theta=params[0], trainable=trainable))
+ c.add(gates.RY(1, theta=params[1]))
+ c.add(gates.CZ(0, 1))
+ c.add(gates.RX(2, theta=params[2]))
+ c.add(gates.RY(3, theta=params[3], trainable=trainable))
+ c.add(gates.CZ(2, 3))
+ if trainable:
+ c.set_parameters(np.random.random(4))
+ else:
+ c.set_parameters(np.random.random(2))
+ target_state = backend.execute_circuit(c).state()
+ lc, _ = c.light_cone(1, 2)
+ final_state = backend.execute_circuit(lc).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.skipif(
+ sys.platform == "win32",
+ reason="no tensorflow-io-0.32.0's wheel available for Windows",
+)
+def test_variable_theta():
+ """Check that parametrized gates accept `tf.Variable` parameters."""
+ try:
+ from qibo.backends import construct_backend
+
+ backend = construct_backend("tensorflow")
+ except ModuleNotFoundError: # pragma: no cover
+ pytest.skip("Skipping variable test because tensorflow is not available.")
+
+ import tensorflow as tf # pylint: disable=import-error
+
+ theta1 = tf.Variable(0.1234, dtype=tf.float64)
+ theta2 = tf.Variable(0.4321, dtype=tf.float64)
+ cvar = Circuit(2)
+ cvar.add(gates.RX(0, theta1))
+ cvar.add(gates.RY(1, theta2))
+ final_state = backend.execute_circuit(cvar).state()
+
+ c = Circuit(2)
+ c.add(gates.RX(0, 0.1234))
+ c.add(gates.RY(1, 0.4321))
+ target_state = backend.execute_circuit(c).state()
+ backend.assert_allclose(final_state, target_state)
diff --git a/tests/test_models_circuit_qasm.py b/tests/test_models_circuit_qasm.py
new file mode 100644
index 000000000..43d15fd8c
--- /dev/null
+++ b/tests/test_models_circuit_qasm.py
@@ -0,0 +1,553 @@
+"""Tests creating abstract Qibo circuits from OpenQASM code."""
+
+import numpy as np
+import pytest
+from openqasm3 import parser
+
+import qibo
+from qibo import Circuit, __version__, gates
+
+
+def assert_strings_equal(a, b):
+ """Asserts that two strings are identical character by character."""
+ assert len(a) == len(b)
+ for x, y in zip(a, b):
+ assert x == y
+
+
+def test_empty():
+ c = Circuit(2)
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_simple():
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+h q[0];
+h q[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_singlequbit_gates():
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.X(1))
+ c.add(gates.Y(0))
+ c.add(gates.Z(1))
+ c.add(gates.S(0))
+ c.add(gates.SDG(1))
+ c.add(gates.T(0))
+ c.add(gates.TDG(1))
+ c.add(gates.I(0))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+h q[0];
+x q[1];
+y q[0];
+z q[1];
+s q[0];
+sdg q[1];
+t q[0];
+tdg q[1];
+id q[0];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_controlled_by_error():
+ """Check that using `to_qasm` with controlled by gates raises error."""
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.Y(1).controlled_by(0, 2))
+ with pytest.raises(ValueError):
+ c.to_qasm()
+
+
+def test_multiqubit_gates():
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.CNOT(0, 1))
+ c.add(gates.X(1))
+ c.add(gates.SWAP(0, 1))
+ c.add(gates.X(0).controlled_by(1))
+ # `controlled_by` here falls back to CNOT and should work
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+h q[0];
+cx q[0],q[1];
+x q[1];
+swap q[0],q[1];
+cx q[1],q[0];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_toffoli():
+ c = Circuit(3)
+ c.add(gates.Y(0))
+ c.add(gates.TOFFOLI(0, 1, 2))
+ c.add(gates.X(1))
+ c.add(gates.TOFFOLI(0, 2, 1))
+ c.add(gates.Z(2))
+ c.add(gates.TOFFOLI(1, 2, 0))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[3];
+y q[0];
+ccx q[0],q[1],q[2];
+x q[1];
+ccx q[0],q[2],q[1];
+z q[2];
+ccx q[1],q[2],q[0];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_parametrized_gate():
+ c = Circuit(2)
+ c.add(gates.Y(0))
+ c.add(gates.RY(1, 0.1234))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+y q[0];
+ry(0.1234) q[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_cu1():
+ c = Circuit(2)
+ c.add(gates.RX(0, 0.1234))
+ c.add(gates.RZ(1, 0.4321))
+ c.add(gates.CU1(0, 1, 0.567))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+rx(0.1234) q[0];
+rz(0.4321) q[1];
+cu1(0.567) q[0],q[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_ugates():
+ c = Circuit(3)
+ c.add(gates.RX(0, 0.1))
+ c.add(gates.RZ(1, 0.4))
+ c.add(gates.U2(2, 0.5, 0.6))
+ c.add(gates.CU1(0, 1, 0.7))
+ c.add(gates.CU3(2, 1, 0.2, 0.3, 0.4))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[3];
+rx(0.1) q[0];
+rz(0.4) q[1];
+u2(0.5, 0.6) q[2];
+cu1(0.7) q[0],q[1];
+cu3(0.2, 0.3, 0.4) q[2],q[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+ c = Circuit(2)
+ c.add(gates.CU2(0, 1, 0.1, 0.2))
+ with pytest.raises(NotImplementedError):
+ target = c.to_qasm()
+
+
+def test_crotations():
+ c = Circuit(3)
+ c.add(gates.RX(0, 0.1))
+ c.add(gates.RZ(1, 0.4))
+ c.add(gates.CRX(0, 2, 0.5))
+ c.add(gates.RY(1, 0.3).controlled_by(2))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[3];
+rx(0.1) q[0];
+rz(0.4) q[1];
+crx(0.5) q[0],q[2];
+cry(0.3) q[2],q[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+ c = Circuit(2)
+ c.add(gates.CU2(0, 1, 0.1, 0.2))
+ with pytest.raises(NotImplementedError):
+ target = c.to_qasm()
+
+
+@pytest.mark.parametrize(
+ "measurements",
+ [
+ [gates.M(0, 1)],
+ ],
+)
+def test_measurements(measurements):
+ c = Circuit(2)
+ c.add(gates.X(0))
+ c.add(gates.Y(1))
+ for m in measurements:
+ c.add(m)
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+creg register0[2];
+x q[0];
+y q[1];
+measure q[0] -> register0[0];
+measure q[1] -> register0[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_multiple_measurements():
+ c = Circuit(5)
+ c.add(gates.M(0, 2, 4, register_name="a"))
+ c.add(gates.M(1, 3, register_name="b"))
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[5];
+creg a[3];
+creg b[2];
+measure q[0] -> a[0];
+measure q[2] -> a[1];
+measure q[4] -> a[2];
+measure q[1] -> b[0];
+measure q[3] -> b[1];"""
+ assert_strings_equal(c.to_qasm(), target)
+
+
+def test_capital_in_register_name_error():
+ """Check that using capital letter in register name raises error."""
+ c = Circuit(2)
+ c.add(gates.M(0, 1, register_name="Abc"))
+ with pytest.raises(NameError):
+ c.to_qasm()
+
+
+def test_from_qasm_multiqubit_gates():
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[3];
+cx q[0],q[2];
+x q[1];
+swap q[0],q[1];
+cx q[1],q[0];
+ccx q[1],q[2],q[0];"""
+ c = Circuit.from_qasm(target)
+ assert c.nqubits == 3
+ assert c.depth == 4
+ assert c.ngates == 5
+ assert isinstance(c.queue[0], gates.CNOT)
+ assert c.queue[0].qubits == (0, 2)
+ assert isinstance(c.queue[1], gates.X)
+ assert c.queue[1].qubits == (1,)
+ assert isinstance(c.queue[2], gates.SWAP)
+ assert c.queue[2].qubits == (0, 1)
+ assert isinstance(c.queue[3], gates.CNOT)
+ assert c.queue[3].qubits == (1, 0)
+ assert isinstance(c.queue[4], gates.TOFFOLI)
+ assert c.queue[4].qubits == (1, 2, 0)
+
+
+def test_from_qasm_singlequbit_gates():
+ target = f"""// Generated by QIBO {__version__}
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+h q[0];
+x q[1];
+y q[0];
+z q[1];
+s q[0];
+sdg q[1];
+t q[0];
+tdg q[1];
+id q[0];"""
+ c = Circuit.from_qasm(target)
+ assert c.nqubits == 2
+ assert c.depth == 5
+ assert c.ngates == 9
+ assert isinstance(c.queue[0], gates.H)
+ assert c.queue[0].qubits == (0,)
+ assert isinstance(c.queue[1], gates.X)
+ assert c.queue[1].qubits == (1,)
+ assert isinstance(c.queue[2], gates.Y)
+ assert c.queue[2].qubits == (0,)
+ assert isinstance(c.queue[3], gates.Z)
+ assert c.queue[3].qubits == (1,)
+ assert isinstance(c.queue[4], gates.S)
+ assert c.queue[4].qubits == (0,)
+ assert isinstance(c.queue[5], gates.SDG)
+ assert c.queue[5].qubits == (1,)
+ assert isinstance(c.queue[6], gates.T)
+ assert c.queue[6].qubits == (0,)
+ assert isinstance(c.queue[7], gates.TDG)
+ assert c.queue[7].qubits == (1,)
+ assert isinstance(c.queue[8], gates.I)
+ assert c.queue[8].qubits == (0,)
+
+
+def test_from_qasm_ugates():
+ target = """OPENQASM 2.0;
+qreg q[2];
+u1(0.1) q[0];
+u2(0.2,0.6) q[1];
+cu3(0.3,0.4,0.5) q[0],q[1];"""
+ c = Circuit.from_qasm(target)
+ assert c.depth == 2
+ assert isinstance(c.queue[0], gates.U1)
+ assert isinstance(c.queue[1], gates.U2)
+ assert isinstance(c.queue[2], gates.CU3)
+ assert c.queue[0].parameters == (0.1,)
+ assert c.queue[1].parameters == (0.2, 0.6)
+ assert c.queue[2].parameters == (0.3, 0.4, 0.5)
+
+
+def test_from_qasm_crotations():
+ target = """OPENQASM 2.0;
+qreg q[2];
+crx(0.1) q[0],q[1];
+crz(0.3) q[1],q[0];
+cry(0.2) q[0],q[1];"""
+ c = Circuit.from_qasm(target)
+ assert c.depth == 3
+ assert isinstance(c.queue[0], gates.CRX)
+ assert isinstance(c.queue[1], gates.CRZ)
+ assert isinstance(c.queue[2], gates.CRY)
+ assert c.queue[0].parameters == (0.1,)
+ assert c.queue[1].parameters == (0.3,)
+ assert c.queue[2].parameters == (0.2,)
+
+
+def test_from_qasm_parametrized_gates():
+ target = """OPENQASM 2.0;
+qreg q[2];
+rx(0.1234) q[0];
+rz(0.4321) q[1];
+cu1(0.567) q[0],q[1];"""
+ c = Circuit.from_qasm(target)
+ assert c.depth == 2
+ assert isinstance(c.queue[0], gates.RX)
+ assert isinstance(c.queue[1], gates.RZ)
+ assert isinstance(c.queue[2], gates.CU1)
+ assert c.queue[0].parameters == (0.1234,)
+ assert c.queue[1].parameters == (0.4321,)
+ assert c.queue[2].parameters == (0.567,)
+
+
+def test_from_qasm_pi_half():
+ target = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[1];
+rx(pi/2) q[0];"""
+ c = Circuit.from_qasm(target)
+ assert c.depth == 1
+ assert c.queue[0].parameters == (np.pi / 2,)
+
+
+def test_from_qasm_invalid_script():
+
+ # Qubit index out of range
+ target = """OPENQASM 2.0;
+qreg q[2];
+x q[2];
+"""
+ with pytest.raises(IndexError):
+ c = Circuit.from_qasm(target)
+
+ # Invalid qubit index
+ target = """OPENQASM 2.0;
+qreg q[a];
+"""
+ with pytest.raises(AttributeError):
+ c = Circuit.from_qasm(target)
+
+ # Undefined qubit
+ target = """OPENQASM 2.0;
+qreg q[2];
+x a[0];
+"""
+ with pytest.raises(KeyError):
+ c = Circuit.from_qasm(target)
+ # Invalid command `test`
+ target = """OPENQASM 2.0;
+qreg q[2];
+test q[0];
+"""
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(target)
+
+
+def test_from_qasm_measurements():
+ target = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[5];
+creg a[3];
+creg b[2];
+measure q[0] -> a[0];
+x q[3];
+measure q[1] -> b[0];
+measure q[2] -> a[1];
+measure q[4] -> a[2];
+measure q[3] -> b[1];"""
+ c = Circuit.from_qasm(target)
+ assert c.depth == 2
+ assert isinstance(c.queue[1], gates.X)
+ assert c.measurement_tuples == {"a": (0, 2, 4), "b": (1, 3)}
+
+
+def test_from_qasm_measurements_order():
+ target = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[5];
+creg a[3];
+creg b[2];
+measure q[2] -> b[1];
+measure q[3] -> a[1];
+measure q[4] -> a[0];
+measure q[1] -> a[2];
+measure q[0] -> b[0];
+"""
+ c = Circuit.from_qasm(target)
+ assert c.measurement_tuples == {"a": (4, 3, 1), "b": (0, 2)}
+
+
+def test_from_qasm_invalid_measurements():
+ # Undefined qubit
+ target = """OPENQASM 2.0;
+qreg q[2];
+creg a[2];
+measure q[2] -> a[0];"""
+ with pytest.raises(IndexError):
+ c = Circuit.from_qasm(target)
+
+ # Undefined register
+ target = """OPENQASM 2.0;
+qreg q[2];
+creg a[2];
+measure q[0] -> b[0];"""
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(target)
+
+ # Register index out of range
+ target = """OPENQASM 2.0;
+qreg q[2];
+creg a[2];
+measure q[0] -> a[2];"""
+ with pytest.raises(IndexError):
+ c = Circuit.from_qasm(target)
+
+ # Invalid measurement command
+ target = """OPENQASM 2.0;
+qreg q[2];
+creg a[2];
+measure q[0] -> a[1] -> a[0];"""
+ with pytest.raises(parser.QASM3ParsingError):
+ c = Circuit.from_qasm(target)
+
+
+def test_from_qasm_invalid_parametrized_gates():
+ # Parametrize non-parametrized gate
+ target = """OPENQASM 2.0;
+qreg q[2];
+x(0.1234) q[0];
+"""
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(target)
+
+ # Failure to give theta value for parametrized gate
+ target = """OPENQASM 2.0;
+qreg q[2];
+rx q[0];
+"""
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(target)
+
+ # Invalid parameter value
+ target = """OPENQASM 2.0;
+qreg q[2];
+rx(0.123a) q[0];
+"""
+ with pytest.raises(parser.QASM3ParsingError):
+ c = Circuit.from_qasm(target)
+
+ # Invalid parenthesis use
+ target = """OPENQASM 2.0;
+qreg q[2];
+rx(0.123)(0.25)(0) q[0];
+"""
+ with pytest.raises(parser.QASM3ParsingError):
+ c = Circuit.from_qasm(target)
+
+ target = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[3];
+gate bob(theta,alpha) q0,q1 { h q1; cx q0,q1; rz(theta) q1; rx(alpha) q0; h q1; }
+"""
+
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(f"{target}bob(0.1, 0.2, 0.3) q[1],q[0];")
+
+ with pytest.raises(ValueError):
+ c = Circuit.from_qasm(f"{target}bob(0.1, 0.2) q[1],q[0],q[2];")
+
+
+def test_from_qasm_gate_command(backend):
+ target = """OPENQASM 2.0;
+include "qelib1.inc";
+gate bob(theta,alpha) q0,q1 { h q1; cx q0,q1; rz(theta) q1; rx(alpha) q0; h q1; }
+gate alice q0,q1 { bob(pi/4,pi) q0,q1; x q0; bob(-pi/4,pi/2) q0,q1; }
+qreg q[3];
+bob(-pi/2,pi) q[0],q[2];
+alice q[1],q[0];"""
+ c = Circuit.from_qasm(target)
+
+ def bob(theta, alpha, q0, q1):
+ gate = gates.FusedGate(q0, q1)
+ gate.append(gates.H(q1))
+ gate.append(gates.CNOT(q0, q1))
+ gate.append(gates.RZ(q1, theta=theta))
+ gate.append(gates.RX(q0, theta=alpha))
+ gate.append(gates.H(q1))
+ return gate
+
+ def alice(q0, q1):
+ gate = gates.FusedGate(q0, q1)
+ gate.append(bob(np.pi / 4, np.pi, q0, q1))
+ gate.append(gates.X(q0))
+ gate.append(bob(-np.pi / 4, np.pi / 2, q0, q1))
+ return gate
+
+ backend.assert_allclose(
+ c.queue[0].matrix(backend), bob(-np.pi / 2, np.pi, 0, 2).matrix(backend)
+ )
+ backend.assert_allclose(c.queue[1].matrix(backend), alice(1, 0).matrix(backend))
+
+
+def test_from_qasm_unsupported_statement():
+ target = """include "stdgates.inc";
+def logical_meas(qubit[3] d) -> bit {
+ bit[3] c;
+ bit r;
+ measure d -> c;
+ r = vote(c);
+ return r;
+}
+"""
+ with pytest.raises(RuntimeError):
+ c = Circuit.from_qasm(target)
diff --git a/tests/test_models_circuit_qasm_cirq.py b/tests/test_models_circuit_qasm_cirq.py
new file mode 100644
index 000000000..f0e683e88
--- /dev/null
+++ b/tests/test_models_circuit_qasm_cirq.py
@@ -0,0 +1,210 @@
+"""Tests executing Qibo circuits created from OpenQASM code."""
+
+import cirq
+import numpy as np
+import pytest
+from cirq.contrib.qasm_import import circuit_from_qasm, exception
+
+from qibo import Circuit, gates
+
+# Absolute testing tolerance for cirq-qibo comparison
+_atol = 1e-7
+
+
+def test_from_qasm_simple(backend, accelerators):
+ target = f"""OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[5];
+h q[0];
+h q[1];
+h q[2];
+h q[3];
+h q[4];"""
+ import qibo
+
+ c = Circuit.from_qasm(target, accelerators)
+ assert c.nqubits == 5
+ assert c.depth == 1
+ for i, gate in enumerate(c.queue):
+ assert gate.__class__.__name__ == "H"
+ assert gate.qubits == (i,)
+ target_state = np.ones(32) / np.sqrt(32)
+ final_state = backend.execute_circuit(c).state()
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_simple_cirq(backend):
+ c1 = Circuit(2)
+ c1.add(gates.H(0))
+ c1.add(gates.H(1))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ assert c3.depth == c2depth
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+
+def test_singlequbit_gates_cirq(backend):
+ c1 = Circuit(2)
+ c1.add(gates.H(0))
+ c1.add(gates.X(1))
+ c1.add(gates.Y(0))
+ c1.add(gates.Z(1))
+ c1.add(gates.S(0))
+ c1.add(gates.SDG(1))
+ c1.add(gates.T(0))
+ c1.add(gates.TDG(1))
+ c1.add(gates.I(0))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ assert c3.depth == c2depth
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+
+def test_multiqubit_gates_cirq(backend):
+ c1 = Circuit(2)
+ c1.add(gates.H(0))
+ c1.add(gates.CNOT(0, 1))
+ c1.add(gates.X(1))
+ c1.add(gates.SWAP(0, 1))
+ c1.add(gates.X(0).controlled_by(1))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ assert c3.depth == c2depth
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+
+def test_toffoli_cirq(backend):
+ c1 = Circuit(3)
+ c1.add(gates.Y(0))
+ c1.add(gates.TOFFOLI(0, 1, 2))
+ c1.add(gates.X(1))
+ c1.add(gates.TOFFOLI(0, 2, 1))
+ c1.add(gates.Z(2))
+ c1.add(gates.TOFFOLI(1, 2, 0))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ assert c3.depth == c2depth
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+
+def test_parametrized_gate_cirq(backend):
+ c1 = Circuit(2)
+ c1.add(gates.Y(0))
+ c1.add(gates.RY(1, 0.1234))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+
+def test_cu1_cirq():
+ c1 = Circuit(2)
+ c1.add(gates.RX(0, 0.1234))
+ c1.add(gates.RZ(1, 0.4321))
+ c1.add(gates.CU1(0, 1, 0.567))
+ # catches unknown gate "cu1"
+ with pytest.raises(exception.QasmException):
+ c2 = circuit_from_qasm(c1.to_qasm())
+
+
+def test_ugates_cirq(backend):
+ c1 = Circuit(3)
+ c1.add(gates.RX(0, 0.1))
+ c1.add(gates.RZ(1, 0.4))
+ c1.add(gates.U2(2, 0.5, 0.6))
+ final_state_c1 = backend.execute_circuit(c1).state()
+
+ c2 = circuit_from_qasm(c1.to_qasm())
+ c2depth = len(cirq.Circuit(c2.all_operations()))
+ assert c1.depth == c2depth
+ final_state_c2 = (
+ cirq.Simulator().simulate(c2).final_state_vector
+ ) # pylint: disable=no-member
+ backend.assert_allclose(final_state_c1, final_state_c2, atol=_atol)
+
+ c3 = Circuit.from_qasm(c2.to_qasm())
+ assert c3.depth == c2depth
+ final_state_c3 = backend.execute_circuit(c3).state()
+ backend.assert_allclose(final_state_c3, final_state_c2, atol=_atol)
+
+ c1 = Circuit(3)
+ c1.add(gates.RX(0, 0.1))
+ c1.add(gates.RZ(1, 0.4))
+ c1.add(gates.U2(2, 0.5, 0.6))
+ c1.add(gates.CU3(2, 1, 0.2, 0.3, 0.4))
+ # catches unknown gate "cu3"
+ with pytest.raises(exception.QasmException):
+ c2 = circuit_from_qasm(c1.to_qasm())
+
+
+def test_crotations_cirq():
+ c1 = Circuit(3)
+ c1.add(gates.RX(0, 0.1))
+ c1.add(gates.RZ(1, 0.4))
+ c1.add(gates.CRX(0, 2, 0.5))
+ c1.add(gates.RY(1, 0.3).controlled_by(2))
+ # catches unknown gate "crx"
+ with pytest.raises(exception.QasmException):
+ c2 = circuit_from_qasm(c1.to_qasm())
+
+
+def test_from_qasm_evaluation(backend):
+ import numpy as np
+
+ target = f"""OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+h q[0];
+h q[1];"""
+ c = Circuit.from_qasm(target)
+ target_state = np.ones(4) / 2.0
+ backend.assert_allclose(backend.execute_circuit(c).state(), target_state)
diff --git a/tests/test_models_dbi.py b/tests/test_models_dbi.py
new file mode 100644
index 000000000..d19168caa
--- /dev/null
+++ b/tests/test_models_dbi.py
@@ -0,0 +1,293 @@
+"""Testing DoubleBracketIteration model"""
+
+import numpy as np
+import pytest
+
+from qibo import hamiltonians
+from qibo.hamiltonians import Hamiltonian
+from qibo.models.dbi.double_bracket import (
+ DoubleBracketCostFunction,
+ DoubleBracketGeneratorType,
+ DoubleBracketIteration,
+ DoubleBracketScheduling,
+)
+from qibo.models.dbi.utils import *
+from qibo.models.dbi.utils_dbr_strategies import (
+ gradient_descent,
+ select_best_dbr_generator,
+)
+from qibo.models.dbi.utils_scheduling import polynomial_step
+from qibo.quantum_info import random_hermitian
+
+NSTEPS = 3
+seed = 10
+"""Number of steps for evolution."""
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_double_bracket_iteration_canonical(backend, nqubits):
+ """Check default (canonical) mode."""
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.canonical,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ for _ in range(NSTEPS):
+ dbi(step=np.sqrt(0.001))
+
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_double_bracket_iteration_group_commutator(backend, nqubits):
+ """Check group commutator mode."""
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ d = backend.cast(np.diag(np.diag(backend.to_numpy(h0))))
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.group_commutator,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+
+ # test first iteration with default d
+ dbi(mode=DoubleBracketGeneratorType.group_commutator, step=0.01)
+ for _ in range(NSTEPS):
+ dbi(step=0.01, d=d)
+
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_double_bracket_iteration_group_commutator_3rd_order(backend, nqubits):
+ """Check 3rd order group commutator mode."""
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ d = backend.cast(np.diag(np.diag(backend.to_numpy(h0))))
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.group_commutator_third_order,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+
+ # test first iteration with default d
+ dbi(mode=DoubleBracketGeneratorType.group_commutator_third_order, step=0.01)
+ for _ in range(NSTEPS):
+ dbi(step=0.01, d=d)
+
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_double_bracket_iteration_single_commutator(backend, nqubits):
+ """Check single commutator mode."""
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ d = backend.cast(np.diag(np.diag(backend.to_numpy(h0))))
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+
+ # test first iteration with default d
+ dbi(mode=DoubleBracketGeneratorType.single_commutator, step=0.01)
+
+ for _ in range(NSTEPS):
+ dbi(step=0.01, d=d)
+
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+@pytest.mark.parametrize("nqubits", [2, 3])
+@pytest.mark.parametrize(
+ "scheduling",
+ [
+ DoubleBracketScheduling.grid_search,
+ DoubleBracketScheduling.hyperopt,
+ DoubleBracketScheduling.simulated_annealing,
+ ],
+)
+def test_variational_scheduling(backend, nqubits, scheduling):
+ """Check schduling options."""
+ h = 2
+
+ # define the hamiltonian
+ h0 = hamiltonians.TFIM(nqubits=nqubits, h=h)
+ dbi = DoubleBracketIteration(h0, scheduling=scheduling)
+ # find initial best step with look_ahead = 1
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ for _ in range(NSTEPS):
+ step = dbi.choose_step()
+ dbi(step=step)
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+@pytest.mark.parametrize(
+ "cost",
+ [
+ DoubleBracketCostFunction.off_diagonal_norm,
+ DoubleBracketCostFunction.least_squares,
+ ],
+)
+def test_polynomial_cost_function(backend, cost):
+ nqubits = 2
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ cost=cost,
+ scheduling=DoubleBracketScheduling.polynomial_approximation,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ for i in range(NSTEPS):
+ s = dbi.choose_step(d=dbi.diagonal_h_matrix, n=5)
+ dbi(step=s, d=dbi.off_diag_h)
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+def test_polynomial_energy_fluctuation(backend):
+ nqubits = 4
+ h0 = random_hermitian(2**nqubits, seed=seed, backend=backend)
+ state = np.zeros(2**nqubits)
+ state[0] = 1
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ cost=DoubleBracketCostFunction.energy_fluctuation,
+ scheduling=DoubleBracketScheduling.polynomial_approximation,
+ ref_state=state,
+ )
+ for i in range(NSTEPS):
+ s = dbi.choose_step(d=dbi.diagonal_h_matrix, n=5)
+ dbi(step=s, d=dbi.diagonal_h_matrix)
+ assert dbi.energy_fluctuation(state=state) < dbi.h0.energy_fluctuation(state=state)
+
+
+@pytest.mark.parametrize("nqubits", [5, 6])
+def test_polynomial_fail_cases(backend, nqubits):
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ scheduling=DoubleBracketScheduling.polynomial_approximation,
+ )
+ with pytest.raises(ValueError):
+ polynomial_step(dbi, n=2, n_max=1)
+ assert polynomial_step(dbi, n=1) is None
+
+
+def test_least_squares(backend):
+ """Check least squares cost function."""
+ nqubits = 4
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ cost=DoubleBracketCostFunction.least_squares,
+ )
+ d = np.diag(np.linspace(1, 2**nqubits, 2**nqubits)) / 2**nqubits
+ initial_potential = dbi.least_squares(d=d)
+ step = dbi.choose_step(d=d)
+ dbi(d=d, step=step)
+ assert dbi.least_squares(d=d) < initial_potential
+
+
+@pytest.mark.parametrize("compare_canonical", [True, False])
+@pytest.mark.parametrize("step", [None, 1e-3])
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_select_best_dbr_generator(backend, nqubits, step, compare_canonical):
+ h0 = random_hermitian(2**nqubits, backend=backend, seed=seed)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ generate_local_Z = generate_Z_operators(nqubits, backend=backend)
+ Z_ops = list(generate_local_Z.values())
+ for _ in range(NSTEPS):
+ dbi, idx, step, flip_sign = select_best_dbr_generator(
+ dbi,
+ Z_ops,
+ compare_canonical=compare_canonical,
+ step=step,
+ )
+ assert dbi.off_diagonal_norm < initial_off_diagonal_norm
+
+
+@pytest.mark.parametrize("step", [None, 1e-3])
+def test_params_to_diagonal_operator(backend, step):
+ nqubits = 2
+ pauli_operator_dict = generate_pauli_operator_dict(
+ nqubits, parameterization_order=1, backend=backend
+ )
+ params = [1, 2, 3]
+ operator_pauli = sum(
+ [params[i] * list(pauli_operator_dict.values())[i] for i in range(nqubits)]
+ )
+ backend.assert_allclose(
+ operator_pauli,
+ params_to_diagonal_operator(
+ params,
+ nqubits=nqubits,
+ parameterization=ParameterizationTypes.pauli,
+ pauli_operator_dict=pauli_operator_dict,
+ backend=backend,
+ ),
+ )
+ operator_element = params_to_diagonal_operator(
+ params,
+ nqubits=nqubits,
+ parameterization=ParameterizationTypes.computational,
+ backend=backend,
+ )
+ for i in range(len(params)):
+ backend.assert_allclose(
+ backend.cast(backend.to_numpy(operator_element).diagonal())[i], params[i]
+ )
+
+
+@pytest.mark.parametrize("order", [1, 2])
+def test_gradient_descent(backend, order):
+ nqubits = 2
+ h0 = random_hermitian(2**nqubits, seed=seed, backend=backend)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ scheduling=DoubleBracketScheduling.hyperopt,
+ cost=DoubleBracketCostFunction.off_diagonal_norm,
+ )
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+ pauli_operator_dict = generate_pauli_operator_dict(
+ nqubits,
+ parameterization_order=order,
+ backend=backend,
+ )
+ pauli_operators = list(pauli_operator_dict.values())
+ # let initial d be approximation of $\Delta(H)
+ d_coef_pauli = decompose_into_pauli_basis(
+ dbi.diagonal_h_matrix, pauli_operators=pauli_operators, backend=backend
+ )
+ d_pauli = sum([d_coef_pauli[i] * pauli_operators[i] for i in range(nqubits)])
+ loss_hist_pauli, d_params_hist_pauli, s_hist_pauli = gradient_descent(
+ dbi,
+ NSTEPS,
+ d_coef_pauli,
+ ParameterizationTypes.pauli,
+ pauli_operator_dict=pauli_operator_dict,
+ pauli_parameterization_order=order,
+ backend=backend,
+ )
+ assert loss_hist_pauli[-1] < initial_off_diagonal_norm
+
+ # computational basis
+ d_coef_computational_partial = backend.cast(backend.to_numpy(d_pauli).diagonal())
+ (
+ loss_hist_computational_partial,
+ _,
+ _,
+ ) = gradient_descent(
+ dbi,
+ NSTEPS,
+ d_coef_computational_partial,
+ ParameterizationTypes.computational,
+ backend=backend,
+ )
+ assert loss_hist_computational_partial[-1] < initial_off_diagonal_norm
diff --git a/tests/test_models_dbi_utils.py b/tests/test_models_dbi_utils.py
new file mode 100644
index 000000000..29d1b1058
--- /dev/null
+++ b/tests/test_models_dbi_utils.py
@@ -0,0 +1,61 @@
+""""Testing utils for DoubleBracketIteration model"""
+
+import numpy as np
+import pytest
+
+from qibo.hamiltonians import Hamiltonian
+from qibo.models.dbi.double_bracket import (
+ DoubleBracketGeneratorType,
+ DoubleBracketIteration,
+)
+from qibo.models.dbi.utils import *
+from qibo.models.dbi.utils_dbr_strategies import select_best_dbr_generator
+from qibo.quantum_info import random_hermitian
+
+NSTEPS = 5
+"""Number of steps for evolution."""
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_generate_Z_operators(backend, nqubits):
+ h0 = random_hermitian(2**nqubits, backend=backend)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits=nqubits, matrix=h0, backend=backend)
+ )
+ generate_Z = generate_Z_operators(nqubits, backend=backend)
+ Z_ops = list(generate_Z.values())
+
+ delta_h0 = dbi.diagonal_h_matrix
+ dephasing_channel = (sum([Z_op @ h0 @ Z_op for Z_op in Z_ops]) + h0) / 2**nqubits
+ norm_diff = np.linalg.norm(backend.to_numpy(delta_h0 - dephasing_channel))
+
+ assert norm_diff < 1e-3
+
+
+@pytest.mark.parametrize("nqubits", [1, 2])
+@pytest.mark.parametrize("step", [0.1, 0.2])
+def test_select_best_dbr_generator(backend, nqubits, step):
+ h0 = random_hermitian(2**nqubits, seed=1, backend=backend)
+ dbi = DoubleBracketIteration(
+ Hamiltonian(nqubits, h0, backend=backend),
+ mode=DoubleBracketGeneratorType.single_commutator,
+ )
+ generate_Z = generate_Z_operators(nqubits, backend=backend)
+ Z_ops = list(generate_Z.values())
+ initial_off_diagonal_norm = dbi.off_diagonal_norm
+
+ for _ in range(NSTEPS):
+ dbi, idx, step_optimize, flip = select_best_dbr_generator(
+ dbi, Z_ops, step=step, compare_canonical=True, max_evals=5
+ )
+
+ assert initial_off_diagonal_norm > dbi.off_diagonal_norm
+
+
+def test_copy_dbi(backend):
+ h0 = random_hermitian(4, seed=1, backend=backend)
+ dbi = DoubleBracketIteration(Hamiltonian(2, h0, backend=backend))
+ dbi_copy = copy_dbi_object(dbi)
+
+ assert dbi is not dbi_copy
+ assert dbi.h.nqubits == dbi_copy.h.nqubits
diff --git a/tests/test_models_distcircuit.py b/tests/test_models_distcircuit.py
new file mode 100644
index 000000000..97d1e09c1
--- /dev/null
+++ b/tests/test_models_distcircuit.py
@@ -0,0 +1,191 @@
+"""Test functions defined in `qibo/models/distcircuit.py`."""
+
+import pytest
+
+from qibo import Circuit, gates
+from qibo.models.distcircuit import DistributedQubits
+
+
+def check_device_queues(queues):
+ """Asserts that global qubits do not collide with the gates to be applied."""
+ for gate_group in queues.queues:
+ for device_gates in gate_group:
+ target_qubits = set()
+ for gate in device_gates:
+ target_qubits |= set(gate.original_gate.target_qubits)
+ assert not queues.qubits.set & target_qubits
+
+
+def test_distributed_circuit_init():
+ """Check if error is raised if total devices is not a power of 2."""
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(5, devices)
+ assert c.ndevices == 4
+ assert c.nglobal == 2
+ devices = {"/GPU:0": 2, "/GPU:1": 1}
+ with pytest.raises(ValueError):
+ c = Circuit(4, devices)
+
+
+def test_distributed_circuit_add_gate():
+ # Attempt to add gate so that available global qubits are not enough
+ c = Circuit(2, {"/GPU:0": 2})
+ with pytest.raises(ValueError):
+ c.add(gates.SWAP(0, 1))
+ # Attempt adding noise channel
+ with pytest.raises(NotImplementedError):
+ c.add(gates.PauliNoiseChannel(0, [("X", 0.1), ("Z", 0.1)]))
+
+
+def test_distributed_circuit_various_errors():
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(5, devices)
+ # Attempt to use ``.with_pauli_noise``
+ with pytest.raises(NotImplementedError):
+ c.with_pauli_noise(list(zip(["X", "Y", "Z"], [0.1, 0.2, 0.1])))
+ # Attempt to compile
+ with pytest.raises(RuntimeError):
+ c.compile()
+ # Attempt to access state before being set
+ with pytest.raises(RuntimeError):
+ final_state = c.final_state
+
+
+def test_distributed_circuit_fusion(accelerators):
+ c = Circuit(4, accelerators)
+ c.add(gates.H(i) for i in range(4))
+ with pytest.raises(NotImplementedError):
+ c.fuse()
+
+
+def test_distributed_circuit_set_gates():
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(6, devices)
+ c.add(gates.H(i) for i in range(4))
+ c.queues.set(c.queue)
+
+ check_device_queues(c.queues)
+ assert len(c.queues.queues) == 1
+ assert len(c.queues.queues[0]) == 4
+ for queues in c.queues.queues[0]:
+ assert len(queues) == 4
+
+
+def test_distributed_circuit_set_gates_controlled():
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(6, devices)
+ c.add([gates.H(0), gates.H(2), gates.H(3)])
+ c.add(gates.CNOT(4, 5))
+ c.add(gates.Z(1).controlled_by(0))
+ c.add(gates.SWAP(2, 3))
+ c.add([gates.X(2), gates.X(3), gates.X(4)])
+ c.queues.set(c.queue)
+
+ check_device_queues(c.queues)
+ assert len(c.queues.queues) == 7
+ for i, queue in enumerate(c.queues.queues[:-2]):
+ assert len(queue) == 4 * (1 - i % 2)
+ for device_group in c.queues.queues[0]:
+ assert len(device_group) == 7
+ for device_group in c.queues.queues[2]:
+ assert len(device_group) == 1
+
+
+@pytest.mark.parametrize("nqubits", [28, 29, 30, 31, 32, 33, 34])
+@pytest.mark.parametrize("ndevices", [2, 4, 8, 16, 32, 64])
+def test_distributed_qft_global_qubits_validity(nqubits, ndevices):
+ """Check that no gates are applied to global qubits for practical QFT cases."""
+ from qibo.models import QFT
+
+ c = QFT(nqubits, accelerators={"/GPU:0": ndevices})
+ c.queues.set(c.queue) # pylint: disable=E1101
+ check_device_queues(c.queues) # pylint: disable=E1101
+
+
+def test_transform_queue_simple():
+ devices = {"/GPU:0": 1, "/GPU:1": 1}
+ c = Circuit(4, devices)
+ c.add(gates.H(i) for i in range(4))
+ c.queues.qubits = DistributedQubits([0], c.nqubits)
+ tqueue = c.queues.transform(c.queue)
+ assert len(tqueue) == 6
+ for i in range(3):
+ assert isinstance(tqueue[i], gates.H)
+ assert tqueue[i].target_qubits == (i + 1,)
+ assert isinstance(tqueue[3], gates.SWAP)
+ assert tqueue[3].target_qubits == (0, 1)
+ assert isinstance(tqueue[4], gates.H)
+ assert tqueue[4].target_qubits == (1,)
+ assert isinstance(tqueue[5], gates.SWAP)
+ assert tqueue[5].target_qubits == (0, 1)
+
+
+def test_transform_queue_more_gates():
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(4, devices)
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.add(gates.CNOT(2, 3))
+ c.add(gates.CZ(0, 1))
+ c.add(gates.CNOT(3, 0))
+ c.add(gates.CNOT(1, 2))
+ c.queues.qubits = DistributedQubits([2, 3], c.nqubits)
+ tqueue = c.queues.transform(c.queue)
+
+ assert len(tqueue) == 10
+ assert isinstance(tqueue[0], gates.H)
+ assert tqueue[0].target_qubits == (0,)
+ assert isinstance(tqueue[1], gates.H)
+ assert tqueue[1].target_qubits == (1,)
+ assert isinstance(tqueue[2], gates.CZ)
+ assert tqueue[2].target_qubits == (1,)
+ assert isinstance(tqueue[3], gates.SWAP)
+ assert set(tqueue[3].target_qubits) == {1, 3}
+ assert isinstance(tqueue[4], gates.CNOT)
+ assert tqueue[4].target_qubits == (1,)
+ assert isinstance(tqueue[5], gates.CNOT)
+ assert tqueue[5].target_qubits == (0,)
+ assert isinstance(tqueue[6], gates.SWAP)
+ assert set(tqueue[6].target_qubits) == {0, 2}
+ assert isinstance(tqueue[7], gates.CNOT)
+ assert tqueue[7].target_qubits == (0,)
+ assert isinstance(tqueue[8], gates.SWAP)
+ assert set(tqueue[8].target_qubits) == {0, 2}
+ assert isinstance(tqueue[9], gates.SWAP)
+ assert set(tqueue[9].target_qubits) == {1, 3}
+
+
+def test_create_queue_with_global_swap():
+ devices = {"/GPU:0": 2, "/GPU:1": 2}
+ c = Circuit(6, devices)
+ c.add([gates.H(0), gates.H(2), gates.H(3)])
+ c.add(gates.SWAP(3, 4))
+ c.add([gates.X(1), gates.X(2)])
+ c.queues.qubits = DistributedQubits([4, 5], c.nqubits)
+ c.queues.create(c.queues.transform(c.queue))
+
+ check_device_queues(c.queues)
+ assert len(c.queues.special_queue) == 1
+ assert len(c.queues.queues) == 3
+ assert len(c.queues.queues[0]) == 4
+ assert len(c.queues.queues[1]) == 0
+ assert len(c.queues.queues[2]) == 4
+ for device_group in c.queues.queues[0]:
+ assert len(device_group) == 3
+ for device_group in c.queues.queues[2]:
+ assert len(device_group) == 2
+
+
+def test_create_queue_errors(backend):
+ c = Circuit(4, {"/GPU:0": 2})
+ c.add(gates.H(0))
+ c.add(gates.H(1))
+ c.queues.qubits = DistributedQubits([0], c.nqubits)
+ with pytest.raises(ValueError):
+ c.queues.create(c.queue)
+
+ c = Circuit(4, {"/GPU:0": 4})
+ c.add(gates.SWAP(0, 1))
+ c.queues.qubits = DistributedQubits([0, 1], c.nqubits)
+ with pytest.raises(ValueError):
+ c.queues.create(c.queue)
diff --git a/tests/test_models_distcircuit_execution.py b/tests/test_models_distcircuit_execution.py
new file mode 100644
index 000000000..c726e9a17
--- /dev/null
+++ b/tests/test_models_distcircuit_execution.py
@@ -0,0 +1,171 @@
+"""Test :class:`qibo.models.distcircuit.DistributedCircuit` execution."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.quantum_info import random_statevector
+
+
+@pytest.mark.parametrize("use_global_qubits", [False, True])
+def test_distributed_circuit_execution(
+ backend, accelerators, use_global_qubits
+): # pragma: no cover
+ dist_c = Circuit(6, accelerators)
+ c = Circuit(6)
+ if use_global_qubits:
+ dist_c.add(gates.H(i) for i in range(dist_c.nqubits))
+ c.add(gates.H(i) for i in range(dist_c.nqubits))
+ else:
+ dist_c.add(gates.H(i) for i in range(dist_c.nlocal))
+ c.add(gates.H(i) for i in range(dist_c.nlocal))
+ dist_c.global_qubits = range(dist_c.nlocal, dist_c.nqubits)
+
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(dist_c, np.copy(initial_state))
+ target_state = backend.execute_circuit(c, np.copy(initial_state))
+ backend.assert_allclose(target_state, final_state)
+
+
+def test_distributed_circuit_execution_pretransformed(
+ backend, accelerators
+): # pragma: no cover
+ dist_c = Circuit(4, accelerators)
+ dist_c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+ dist_c.add(gates.SWAP(0, 2))
+ dist_c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+
+ c = Circuit(4)
+ c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+ c.add(gates.SWAP(0, 2))
+ c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(dist_c, np.copy(initial_state))
+ target_state = backend.execute_circuit(c, np.copy(initial_state))
+ backend.assert_allclose(target_state, final_state, atol=1e-7)
+
+
+def test_distributed_circuit_execution_with_swap(
+ backend, accelerators
+): # pragma: no cover
+ dist_c = Circuit(6, accelerators)
+ dist_c.add(gates.H(i) for i in range(6))
+ dist_c.add(gates.SWAP(i, i + 1) for i in range(5))
+ dist_c.global_qubits = [0, 1]
+
+ c = Circuit(6)
+ c.add(gates.H(i) for i in range(6))
+ c.add(gates.SWAP(i, i + 1) for i in range(5))
+
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(dist_c, np.copy(initial_state))
+ target_state = backend.execute_circuit(c, np.copy(initial_state))
+ backend.assert_allclose(target_state, final_state, atol=1e-7)
+
+
+def test_distributed_circuit_execution_special_gate(
+ backend, accelerators
+): # pragma: no cover
+ from qibo import callbacks
+
+ dist_c = Circuit(6, accelerators)
+ initial_state = random_statevector(2**dist_c.nqubits, backend=backend)
+ entropy = callbacks.EntanglementEntropy([0])
+ dist_c.add(gates.CallbackGate(entropy))
+ dist_c.add(gates.H(i) for i in range(dist_c.nlocal))
+ dist_c.add(gates.CallbackGate(entropy))
+ dist_c.global_qubits = range(dist_c.nlocal, dist_c.nqubits)
+ c = Circuit(6)
+ c.add(gates.CallbackGate(entropy))
+ c.add(gates.H(i) for i in range(dist_c.nlocal))
+ c.add(gates.CallbackGate(entropy))
+ final_state = backend.execute_circuit(dist_c, initial_state=np.copy(initial_state))
+ target_state = backend.execute_circuit(c, initial_state=np.copy(initial_state))
+ backend.assert_allclose(final_state, target_state, atol=1e-7)
+
+
+def test_distributed_circuit_execution_controlled_gate(
+ backend, accelerators
+): # pragma: no cover
+ dist_c = Circuit(4, accelerators)
+ dist_c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+ dist_c.add(gates.CNOT(0, 2))
+ c = Circuit(4)
+ c.add(gates.H(i) for i in range(dist_c.nglobal, 4))
+ c.add(gates.CNOT(0, 2))
+
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(dist_c, np.copy(initial_state))
+ target_state = backend.execute_circuit(c, np.copy(initial_state))
+ backend.assert_allclose(target_state, final_state)
+
+
+def test_distributed_circuit_execution_controlled_by_gates(
+ backend, accelerators
+): # pragma: no cover
+ dist_c = Circuit(6, accelerators)
+ dist_c.add([gates.H(0), gates.H(2), gates.H(3)])
+ dist_c.add(gates.CNOT(4, 5))
+ dist_c.add(gates.Z(1).controlled_by(0))
+ dist_c.add(gates.SWAP(2, 3))
+ dist_c.add([gates.X(2), gates.X(3), gates.X(4)])
+
+ c = Circuit(6)
+ c.add([gates.H(0), gates.H(2), gates.H(3)])
+ c.add(gates.CNOT(4, 5))
+ c.add(gates.Z(1).controlled_by(0))
+ c.add(gates.SWAP(2, 3))
+ c.add([gates.X(2), gates.X(3), gates.X(4)])
+
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(dist_c, np.copy(initial_state))
+ target_state = backend.execute_circuit(c, np.copy(initial_state))
+ backend.assert_allclose(target_state, final_state, atol=1e-7)
+
+
+def test_distributed_circuit_execution_addition(
+ backend, accelerators
+): # pragma: no cover
+ # Attempt to add circuits with different devices
+ c1 = Circuit(6, {"/GPU:0": 2, "/GPU:1": 2})
+ c2 = Circuit(6, {"/GPU:0": 2})
+ with pytest.raises(ValueError):
+ c = c1 + c2
+
+ c1 = Circuit(6, accelerators)
+ c2 = Circuit(6, accelerators)
+ c1.add([gates.H(i) for i in range(6)])
+ c2.add([gates.CNOT(i, i + 1) for i in range(5)])
+ c2.add([gates.Z(i) for i in range(6)])
+ dist_c = c1 + c2
+
+ c = Circuit(6)
+ c.add([gates.H(i) for i in range(6)])
+ c.add([gates.CNOT(i, i + 1) for i in range(5)])
+ c.add([gates.Z(i) for i in range(6)])
+ assert c.depth == dist_c.depth
+ final_state = backend.execute_circuit(dist_c)
+ target_state = backend.execute_circuit(c)
+ backend.assert_allclose(final_state, target_state, atol=1e-7)
+
+
+def test_distributed_circuit_empty_execution(backend, accelerators): # pragma: no cover
+ # test executing a circuit with the default initial state
+ c = Circuit(5, accelerators)
+ final_state = backend.execute_circuit(c).state()
+ target_state = np.zeros_like(final_state)
+ target_state[0] = 1
+ backend.assert_allclose(final_state, target_state)
+ # test re-executing the circuit with a given initial state
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(c, initial_state)
+ backend.assert_allclose(final_state, initial_state)
+ # test executing a new circuit with a given initial state
+ c = Circuit(5, accelerators)
+ initial_state = random_statevector(2**c.nqubits, backend=backend)
+ final_state = backend.execute_circuit(c, initial_state)
+ backend.assert_allclose(final_state, initial_state)
+ # test re-executing the circuit with the default initial state
+ final_state = backend.execute_circuit(c)
+ backend.assert_allclose(final_state, target_state)
diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py
new file mode 100644
index 000000000..1a935d75e
--- /dev/null
+++ b/tests/test_models_encodings.py
@@ -0,0 +1,277 @@
+"""Tests for qibo.models.encodings"""
+
+import math
+from itertools import product
+
+import numpy as np
+import pytest
+from scipy.optimize import curve_fit
+
+from qibo import Circuit, gates
+from qibo.models.encodings import (
+ comp_basis_encoder,
+ entangling_layer,
+ phase_encoder,
+ unary_encoder,
+ unary_encoder_random_gaussian,
+)
+
+
+def _gaussian(x, a, b, c):
+ """Gaussian used in the `unary_encoder_random_gaussian test"""
+ return np.exp(a * x**2 + b * x + c)
+
+
+@pytest.mark.parametrize(
+ "basis_element", [5, "101", ["1", "0", "1"], [1, 0, 1], ("1", "0", "1"), (1, 0, 1)]
+)
+def test_comp_basis_encoder(backend, basis_element):
+ with pytest.raises(TypeError):
+ circuit = comp_basis_encoder(2.3)
+ with pytest.raises(ValueError):
+ circuit = comp_basis_encoder("0b001")
+ with pytest.raises(ValueError):
+ circuit = comp_basis_encoder("001", nqubits=2)
+ with pytest.raises(TypeError):
+ circuit = comp_basis_encoder("001", nqubits=3.1)
+ with pytest.raises(ValueError):
+ circuit = comp_basis_encoder(3)
+
+ zero = np.array([1, 0], dtype=complex)
+ one = np.array([0, 1], dtype=complex)
+ target = np.kron(one, np.kron(zero, one))
+ target = backend.cast(target, dtype=target.dtype)
+
+ state = (
+ comp_basis_encoder(basis_element, nqubits=3)
+ if isinstance(basis_element, int)
+ else comp_basis_encoder(basis_element)
+ )
+
+ state = backend.execute_circuit(state).state()
+
+ backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("rotation", ["RX", "RY", "RZ"])
+def test_phase_encoder(backend, rotation, kind):
+ sampler = np.random.default_rng(1)
+
+ nqubits = 3
+ dims = 2**nqubits
+
+ with pytest.raises(TypeError):
+ data = sampler.random((nqubits, nqubits))
+ data = backend.cast(data, dtype=data.dtype)
+ phase_encoder(data, rotation=rotation)
+ with pytest.raises(TypeError):
+ data = sampler.random(nqubits)
+ data = backend.cast(data, dtype=data.dtype)
+ phase_encoder(data, rotation=True)
+ with pytest.raises(ValueError):
+ data = sampler.random(nqubits)
+ data = backend.cast(data, dtype=data.dtype)
+ phase_encoder(data, rotation="rzz")
+
+ phases = np.random.rand(nqubits)
+
+ if rotation in ["RX", "RY"]:
+ functions = list(product([np.cos, np.sin], repeat=nqubits))
+ target = []
+ for row in functions:
+ elem = 1.0
+ for phase, func in zip(phases, row):
+ elem *= func(phase / 2)
+ if rotation == "RX" and func.__name__ == "sin":
+ elem *= -1.0j
+ target.append(elem)
+ else:
+ target = [np.exp(-0.5j * sum(phases))] + [0.0] * (dims - 1)
+
+ target = np.array(target, dtype=complex)
+ target = backend.cast(target, dtype=target.dtype)
+
+ if kind is not None:
+ phases = kind(phases)
+
+ state = phase_encoder(phases, rotation=rotation)
+ state = backend.execute_circuit(state).state()
+
+ backend.assert_allclose(state, target)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("architecture", ["tree", "diagonal"])
+@pytest.mark.parametrize("nqubits", [8])
+def test_unary_encoder(backend, nqubits, architecture, kind):
+ sampler = np.random.default_rng(1)
+
+ with pytest.raises(TypeError):
+ data = sampler.random((nqubits, nqubits))
+ data = backend.cast(data, dtype=data.dtype)
+ unary_encoder(data, architecture=architecture)
+ with pytest.raises(TypeError):
+ data = sampler.random(nqubits)
+ data = backend.cast(data, dtype=data.dtype)
+ unary_encoder(data, architecture=True)
+ with pytest.raises(ValueError):
+ data = sampler.random(nqubits)
+ data = backend.cast(data, dtype=data.dtype)
+ unary_encoder(data, architecture="semi-diagonal")
+ if architecture == "tree":
+ with pytest.raises(ValueError):
+ data = sampler.random(nqubits + 1)
+ data = backend.cast(data, dtype=data.dtype)
+ unary_encoder(data, architecture=architecture)
+
+ # sampling random data in interval [-1, 1]
+ sampler = np.random.default_rng(1)
+ data = 2 * sampler.random(nqubits) - 1
+ data = kind(data) if kind is not None else backend.cast(data, dtype=data.dtype)
+
+ circuit = unary_encoder(data, architecture=architecture)
+ state = backend.execute_circuit(circuit).state()
+ indexes = np.flatnonzero(backend.to_numpy(state))
+ state = backend.np.real(state[indexes])
+
+ backend.assert_allclose(state, backend.cast(data) / backend.calculate_norm(data, 2))
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+@pytest.mark.parametrize("nqubits", [8])
+def test_unary_encoder_random_gaussian(backend, nqubits, seed):
+ """Tests if encoded vector are random variables sampled from
+ Gaussian distribution with 0.0 mean and variance close to the norm
+ of the random Gaussian vector that was encoded."""
+ with pytest.raises(TypeError):
+ unary_encoder_random_gaussian("1", seed=seed)
+ with pytest.raises(ValueError):
+ unary_encoder_random_gaussian(-1, seed=seed)
+ with pytest.raises(ValueError):
+ unary_encoder_random_gaussian(3, seed=seed)
+ with pytest.raises(TypeError):
+ unary_encoder_random_gaussian(nqubits, architecture=True, seed=seed)
+ with pytest.raises(NotImplementedError):
+ unary_encoder_random_gaussian(nqubits, architecture="diagonal", seed=seed)
+ with pytest.raises(TypeError):
+ unary_encoder_random_gaussian(nqubits, seed="seed")
+
+ samples = int(1e2)
+
+ local_state = np.random.default_rng(seed) if seed in [None, 10] else seed
+
+ amplitudes = []
+ for _ in range(samples):
+ circuit = unary_encoder_random_gaussian(nqubits, seed=local_state)
+ state = backend.execute_circuit(circuit).state()
+ indexes = np.flatnonzero(backend.to_numpy(state))
+ state = np.real(state[indexes])
+ amplitudes += [float(elem) for elem in list(state)]
+
+ y, x = np.histogram(amplitudes, bins=50, density=True)
+ x = (x[:-1] + x[1:]) / 2
+
+ params, _ = curve_fit(_gaussian, x, y)
+
+ stddev = np.sqrt(-1 / (2 * params[0]))
+ mean = stddev**2 * params[1]
+
+ theoretical_norm = (
+ math.sqrt(2) * math.gamma((nqubits + 1) / 2) / math.gamma(nqubits / 2)
+ )
+ theoretical_norm = 1.0 / theoretical_norm
+
+ backend.assert_allclose(0.0, mean, atol=1e-1)
+ backend.assert_allclose(stddev, theoretical_norm, atol=1e-1)
+
+
+def test_entangling_layer_errors():
+ with pytest.raises(TypeError):
+ entangling_layer(10.5)
+ with pytest.raises(ValueError):
+ entangling_layer(-4)
+ with pytest.raises(TypeError):
+ entangling_layer(10, architecture=True)
+ with pytest.raises(NotImplementedError):
+ entangling_layer(10, architecture="qibo")
+ with pytest.raises(TypeError):
+ entangling_layer(10, closed_boundary="True")
+ with pytest.raises(NotImplementedError):
+ entangling_layer(10, entangling_gate=gates.GeneralizedfSim)
+ with pytest.raises(NotImplementedError):
+ entangling_layer(10, entangling_gate=gates.TOFFOLI)
+
+
+@pytest.mark.parametrize("closed_boundary", [False, True])
+@pytest.mark.parametrize("entangling_gate", ["CNOT", gates.CZ, gates.RBS])
+@pytest.mark.parametrize(
+ "architecture", ["diagonal", "shifted", "even-layer", "odd-layer"]
+)
+@pytest.mark.parametrize("nqubits", [4, 9])
+def test_entangling_layer(nqubits, architecture, entangling_gate, closed_boundary):
+ target_circuit = Circuit(nqubits)
+ if architecture == "diagonal":
+ target_circuit.add(
+ _helper_entangling_test(entangling_gate, qubit)
+ for qubit in range(nqubits - 1)
+ )
+ elif architecture == "even-layer":
+ target_circuit.add(
+ _helper_entangling_test(entangling_gate, qubit)
+ for qubit in range(0, nqubits - 1, 2)
+ )
+ elif architecture == "odd-layer":
+ target_circuit.add(
+ _helper_entangling_test(entangling_gate, qubit)
+ for qubit in range(1, nqubits - 1, 2)
+ )
+ else:
+ target_circuit.add(
+ _helper_entangling_test(entangling_gate, qubit)
+ for qubit in range(0, nqubits - 1, 2)
+ )
+ target_circuit.add(
+ _helper_entangling_test(entangling_gate, qubit)
+ for qubit in range(1, nqubits - 1, 2)
+ )
+
+ if closed_boundary:
+ target_circuit.add(_helper_entangling_test(entangling_gate, nqubits - 1, 0))
+
+ circuit = entangling_layer(nqubits, architecture, entangling_gate, closed_boundary)
+ for gate, target in zip(circuit.queue, target_circuit.queue):
+ assert gate.__class__.__name__ == target.__class__.__name__
+
+
+def _helper_entangling_test(gate, qubit_0, qubit_1=None):
+ """Creates two-qubit gate with of without parameters."""
+ if qubit_1 is None:
+ qubit_1 = qubit_0 + 1
+
+ if callable(gate) and gate.__name__ == "RBS":
+ return gate(qubit_0, qubit_1, 0.0)
+
+ if gate == "CNOT":
+ gate = gates.CNOT
+
+ return gate(qubit_0, qubit_1)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_circuit_kwargs(density_matrix):
+ test = comp_basis_encoder(5, 7, density_matrix=density_matrix)
+ assert test.density_matrix is density_matrix
+
+ test = entangling_layer(5, density_matrix=density_matrix)
+ assert test.density_matrix is density_matrix
+
+ data = np.random.rand(5)
+ test = phase_encoder(data, density_matrix=density_matrix)
+ assert test.density_matrix is density_matrix
+
+ test = unary_encoder(data, "diagonal", density_matrix=density_matrix)
+ assert test.density_matrix is density_matrix
+
+ test = unary_encoder_random_gaussian(4, density_matrix=density_matrix)
+ assert test.density_matrix is density_matrix
diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py
new file mode 100644
index 000000000..4335f909f
--- /dev/null
+++ b/tests/test_models_error_mitigation.py
@@ -0,0 +1,375 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.backends import construct_backend
+from qibo.hamiltonians import SymbolicHamiltonian
+from qibo.models.error_mitigation import (
+ CDR,
+ ICS,
+ ZNE,
+ get_expectation_val_with_readout_mitigation,
+ get_response_matrix,
+ sample_clifford_training_circuit,
+ sample_training_circuit_cdr,
+ vnCDR,
+)
+from qibo.noise import DepolarizingError, NoiseModel, ReadoutError
+from qibo.quantum_info import random_stochastic_matrix
+from qibo.symbols import Z
+
+
+def get_noise_model(error, gate, resp_matrix=[False, None]):
+ noise = NoiseModel()
+ noise.add(error, gate)
+ if resp_matrix[0]:
+ noise.add(ReadoutError(probabilities=resp_matrix[1]), gate=gates.M)
+
+ return noise
+
+
+def get_circuit(nqubits, nmeas=None):
+ if nmeas is None:
+ nmeas = nqubits
+ # Define the circuit
+ hz = 0.5
+ hx = 0.5
+ dt = 0.25
+ c = Circuit(nqubits, density_matrix=True)
+ c.add(gates.RZ(q, theta=-2 * hz * dt - np.pi / 2) for q in range(nqubits))
+ c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ c.add(gates.RZ(q, theta=-2 * hx * dt + np.pi) for q in range(nqubits))
+ c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ c.add(gates.RZ(q, theta=-np.pi / 2) for q in range(nqubits))
+ c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(0, nqubits - 1, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ c.add(gates.M(*range(nmeas)))
+
+ return c
+
+
+backend = construct_backend("numpy")
+# # Generate random response matrices
+resp_matrix_1q = random_stochastic_matrix(
+ 2, diagonally_dominant=True, seed=2, backend=backend
+)
+resp_matrix_2q = random_stochastic_matrix(
+ 4, diagonally_dominant=True, seed=2, backend=backend
+)
+
+
+@pytest.mark.parametrize(
+ "nqubits,noise,insertion_gate,readout",
+ [
+ (3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", None),
+ (
+ 3,
+ get_noise_model(DepolarizingError(0.1), gates.CNOT),
+ "CNOT",
+ {"response_matrix": resp_matrix_2q, "ibu_iters": None},
+ ),
+ (
+ 3,
+ get_noise_model(DepolarizingError(0.1), gates.CNOT),
+ "CNOT",
+ {"ncircuits": 2},
+ ),
+ (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", None),
+ (
+ 1,
+ get_noise_model(DepolarizingError(0.3), gates.RX),
+ "RX",
+ {"response_matrix": resp_matrix_1q, "ibu_iters": None},
+ ),
+ (
+ 1,
+ get_noise_model(DepolarizingError(0.3), gates.RX),
+ "RX",
+ {"response_matrix": resp_matrix_1q, "ibu_iters": 10},
+ ),
+ (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", {"ncircuits": 2}),
+ ],
+)
+@pytest.mark.parametrize("solve", [False, True])
+def test_zne(backend, nqubits, noise, solve, insertion_gate, readout):
+ """Test that ZNE reduces the noise."""
+ if backend.name == "tensorflow":
+ import tensorflow as tf
+
+ tf.config.threading.set_inter_op_parallelism_threads = 1
+ tf.config.threading.set_intra_op_parallelism_threads = 1
+ else:
+ backend.set_threads(1)
+
+ if nqubits == 1:
+ nmeas = 1
+ else:
+ nmeas = nqubits - 1
+ # Define the circuit
+ c = get_circuit(nqubits, nmeas)
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nmeas)])
+ obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend)
+ obs = SymbolicHamiltonian(obs, backend=backend)
+ # Noise-free expected value
+ exact = obs_exact.expectation(backend.execute_circuit(c).state())
+ # Noisy expected value without mitigation
+ state = backend.execute_circuit(noise.apply(c), nshots=10000)
+ noisy = state.expectation_from_samples(obs)
+ # Mitigated expected value
+ estimate = ZNE(
+ circuit=c,
+ observable=obs,
+ noise_levels=np.array(range(4)),
+ noise_model=noise,
+ nshots=10000,
+ solve_for_gammas=solve,
+ insertion_gate=insertion_gate,
+ readout=readout,
+ backend=backend,
+ )
+
+ assert backend.np.abs(exact - estimate) <= backend.np.abs(exact - noisy)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+@pytest.mark.parametrize("full_output", [False, True])
+@pytest.mark.parametrize(
+ "noise,readout",
+ [
+ (get_noise_model(DepolarizingError(0.1), gates.CNOT), None),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"response_matrix": resp_matrix_2q, "ibu_iters": None},
+ ),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"response_matrix": resp_matrix_2q, "ibu_iters": 10},
+ ),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"ncircuits": 2},
+ ),
+ ],
+)
+def test_cdr(backend, nqubits, noise, full_output, readout):
+ """Test that CDR reduces the noise."""
+ if backend.name == "tensorflow":
+ import tensorflow as tf
+
+ tf.config.threading.set_inter_op_parallelism_threads = 1
+ tf.config.threading.set_intra_op_parallelism_threads = 1
+ else:
+ backend.set_threads(1)
+
+ nmeas = 1 if nqubits == 1 else nqubits - 1
+ # Define the circuit
+ c = get_circuit(nqubits, nmeas)
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nmeas)])
+ obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend)
+ obs = SymbolicHamiltonian(obs, backend=backend)
+ # Noise-free expected value
+ exact = obs_exact.expectation(backend.execute_circuit(c).state())
+ # Noisy expected value without mitigation
+ state = backend.execute_circuit(noise.apply(c), nshots=10000)
+ noisy = state.expectation_from_samples(obs)
+ # Mitigated expected value
+ estimate = CDR(
+ circuit=c,
+ observable=obs,
+ noise_model=noise,
+ nshots=10000,
+ n_training_samples=20,
+ full_output=full_output,
+ readout=readout,
+ backend=backend,
+ )
+ if full_output:
+ estimate = estimate[0]
+
+ assert backend.np.abs(exact - estimate) <= backend.np.abs(exact - noisy)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+def test_sample_training_circuit(nqubits):
+ # Define the circuit
+ hz = -2
+ hx = 1
+ dt = np.pi / 4
+ c = Circuit(nqubits, density_matrix=True)
+ c.add(gates.RZ(q, theta=-2 * hz * dt - np.pi / 2) for q in range(nqubits))
+ c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ c.add(gates.RZ(q, theta=-2 * hx * dt + np.pi) for q in range(nqubits))
+ c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits))
+ c.add(gates.RZ(q, theta=-np.pi / 2) for q in range(nqubits))
+ c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(0, nqubits - 1, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2))
+ c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2))
+ c.add(gates.M(q) for q in range(nqubits))
+
+ with pytest.raises(ValueError):
+ sample_training_circuit_cdr(c)
+ with pytest.raises(ValueError):
+ sample_clifford_training_circuit(c)
+
+
+@pytest.mark.parametrize(
+ "nqubits,noise,insertion_gate,readout",
+ [
+ (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", None),
+ (
+ 1,
+ get_noise_model(DepolarizingError(0.1), gates.RX, [True, resp_matrix_1q]),
+ "RX",
+ {"response_matrix": resp_matrix_1q, "ibu_iters": 10},
+ ),
+ (
+ 1,
+ get_noise_model(DepolarizingError(0.1), gates.RX, [True, resp_matrix_1q]),
+ "RX",
+ {"ncircuits": 2},
+ ),
+ ],
+)
+@pytest.mark.parametrize("full_output", [False, True])
+def test_vncdr(backend, nqubits, noise, full_output, insertion_gate, readout):
+ """Test that vnCDR reduces the noise."""
+ if backend.name == "tensorflow":
+ import tensorflow as tf
+
+ tf.config.threading.set_inter_op_parallelism_threads = 1
+ tf.config.threading.set_intra_op_parallelism_threads = 1
+ else:
+ backend.set_threads(1)
+ # Define the circuit
+ c = get_circuit(nqubits)
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nqubits)])
+ obs = SymbolicHamiltonian(obs, backend=backend)
+ # Noise-free expected value
+ exact = obs.expectation(backend.execute_circuit(c).state())
+ # Noisy expected value without mitigation
+ state = backend.execute_circuit(noise.apply(c), nshots=10000)
+ noisy = state.expectation_from_samples(obs)
+ # Mitigated expected value
+ estimate = vnCDR(
+ circuit=c,
+ observable=obs,
+ backend=backend,
+ noise_levels=range(3),
+ noise_model=noise,
+ nshots=10000,
+ n_training_samples=20,
+ insertion_gate=insertion_gate,
+ full_output=full_output,
+ readout=readout,
+ )
+ if full_output:
+ estimate = estimate[0]
+
+ assert backend.np.abs(exact - estimate) <= backend.np.abs(exact - noisy)
+
+
+@pytest.mark.parametrize("nqubits,nmeas", [(3, 2)])
+@pytest.mark.parametrize("method", ["response_matrix", "randomized"])
+@pytest.mark.parametrize("ibu_iters", [None, 10])
+def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters):
+ if backend.name == "tensorflow":
+ import tensorflow as tf
+
+ tf.config.threading.set_inter_op_parallelism_threads = 1
+ tf.config.threading.set_intra_op_parallelism_threads = 1
+ else:
+ backend.set_threads(1)
+ nshots = 10000
+ p = random_stochastic_matrix(2**nmeas, diagonally_dominant=True, seed=5)
+ noise = NoiseModel()
+ noise.add(ReadoutError(probabilities=p), gate=gates.M)
+ if method == "response_matrix":
+ response = get_response_matrix(
+ nmeas, None, noise, nshots=nshots, backend=backend
+ )
+ readout = {"response_matrix": response, "ibu_iters": ibu_iters}
+ elif method == "randomized":
+ readout = {"ncircuits": 10}
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nmeas)])
+ obs = SymbolicHamiltonian(obs, backend=backend)
+ # get noise free expected val
+ c = get_circuit(nqubits, nmeas)
+ true_state = backend.execute_circuit(c, nshots=nshots)
+ true_val = true_state.expectation_from_samples(obs)
+ # get noisy expected val
+ state = backend.execute_circuit(noise.apply(c), nshots=nshots)
+ noisy_val = state.expectation_from_samples(obs)
+
+ mit_val = get_expectation_val_with_readout_mitigation(
+ c, obs, noise, nshots, readout, backend=backend
+ )
+
+ assert np.abs(true_val - mit_val) <= np.abs(true_val - noisy_val)
+
+
+@pytest.mark.parametrize("nqubits", [3])
+@pytest.mark.parametrize("full_output", [False, True])
+@pytest.mark.parametrize(
+ "noise,readout",
+ [
+ (get_noise_model(DepolarizingError(0.1), gates.CNOT), None),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"response_matrix": resp_matrix_2q, "ibu_iters": None},
+ ),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"response_matrix": resp_matrix_2q, "ibu_iters": 10},
+ ),
+ (
+ get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]),
+ {"ncircuits": 2},
+ ),
+ ],
+)
+def test_ics(backend, nqubits, noise, full_output, readout):
+ if backend.name == "tensorflow":
+ import tensorflow as tf
+
+ tf.config.threading.set_inter_op_parallelism_threads = 1
+ tf.config.threading.set_intra_op_parallelism_threads = 1
+ else:
+ backend.set_threads(1)
+ """Test that ICS reduces the noise."""
+ # Define the circuit
+ c = get_circuit(nqubits, nqubits - 1)
+ # Define the observable
+ obs = np.prod([Z(i) for i in range(nqubits - 1)])
+ obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend)
+ obs = SymbolicHamiltonian(obs, backend=backend)
+ # Noise-free expected value
+ exact = obs_exact.expectation(backend.execute_circuit(c).state())
+ # Noisy expected value without mitigation
+ state = backend.execute_circuit(noise.apply(c), nshots=10000)
+ noisy = state.expectation_from_samples(obs)
+ # Mitigated expected value
+ estimate = ICS(
+ circuit=c,
+ observable=obs,
+ noise_model=noise,
+ nshots=10000,
+ n_training_samples=20,
+ full_output=full_output,
+ readout=readout,
+ backend=backend,
+ )
+ if full_output:
+ estimate = estimate[0]
+
+ assert backend.np.abs(exact - estimate) <= backend.np.abs(exact - noisy)
diff --git a/tests/test_models_evolution.py b/tests/test_models_evolution.py
new file mode 100644
index 000000000..9837ed7f3
--- /dev/null
+++ b/tests/test_models_evolution.py
@@ -0,0 +1,340 @@
+import numpy as np
+import pytest
+from scipy.linalg import expm
+
+from qibo import callbacks, hamiltonians, models
+from qibo.config import raise_error
+
+
+def assert_states_equal(backend, state, target_state, atol=0):
+ """Asserts that two state vectors are equal up to a phase."""
+ state = backend.to_numpy(state)
+ target_state = backend.to_numpy(target_state)
+ phase = state[0] / target_state[0]
+ backend.assert_allclose(state, phase * target_state, atol=atol)
+
+
+class TimeStepChecker(callbacks.Callback):
+ """Callback that checks each evolution time step."""
+
+ def __init__(self, target_states, atol=0):
+ super().__init__()
+ self.target_states = iter(target_states)
+ self.atol = atol
+
+ def apply(self, backend, state):
+ assert_states_equal(backend, state, next(self.target_states), atol=self.atol)
+
+ def apply_density_matrix(self, backend, state): # pragma: no cover
+ raise_error(NotImplementedError)
+
+
+def test_state_evolution_init(backend):
+ ham = hamiltonians.Z(2, backend=backend)
+ evolution = models.StateEvolution(ham, dt=1)
+ assert evolution.nqubits == 2
+ # time-dependent Hamiltonian bad type
+ with pytest.raises(TypeError):
+ evol = models.StateEvolution(lambda t: "abc", dt=1e-2)
+ # dt < 0
+ with pytest.raises(ValueError):
+ adev = models.StateEvolution(ham, dt=-1e-2)
+ # pass accelerators without trotter Hamiltonian
+ with pytest.raises(NotImplementedError):
+ adev = models.StateEvolution(ham, dt=1e-2, accelerators={"/GPU:0": 2})
+
+
+def test_state_evolution_get_initial_state(backend):
+ ham = hamiltonians.Z(2, backend=backend)
+ evolution = models.StateEvolution(ham, dt=1)
+ # execute without initial state
+ with pytest.raises(ValueError):
+ final_state = evolution(final_time=1)
+
+
+@pytest.mark.parametrize(
+ ("solver", "atol"), [("exp", 0), ("rk4", 1e-2), ("rk45", 1e-1)]
+)
+def test_state_evolution_constant_hamiltonian(backend, solver, atol):
+ nsteps = 200
+ t = np.linspace(0, 1, nsteps + 1)
+ phase = np.exp(2j * t)[:, np.newaxis]
+ ones = np.ones((nsteps + 1, 2))
+ target_psi = np.concatenate([phase, ones, phase.conj()], axis=1)
+
+ dt = t[1] - t[0]
+ checker = TimeStepChecker(target_psi, atol=atol)
+ ham = hamiltonians.Z(2, backend=backend)
+ evolution = models.StateEvolution(ham, dt=dt, solver=solver, callbacks=[checker])
+ final_psi = evolution(final_time=1, initial_state=target_psi[0])
+
+
+@pytest.mark.parametrize("nqubits,dt", [(2, 1e-2)])
+def test_state_evolution_time_dependent_hamiltonian(backend, nqubits, dt):
+ ham = lambda t: np.cos(t) * hamiltonians.Z(nqubits, backend=backend)
+ # Analytical solution
+ target_psi = [np.ones(2**nqubits) / np.sqrt(2**nqubits)]
+ for n in range(int(1 / dt)):
+ prop = expm(-1j * dt * backend.to_numpy(ham(n * dt).matrix))
+ target_psi.append(prop.dot(target_psi[-1]))
+
+ checker = TimeStepChecker(target_psi, atol=1e-8)
+ evolution = models.StateEvolution(ham, dt=dt, callbacks=[checker])
+ final_psi = evolution(final_time=1, initial_state=np.copy(target_psi[0]))
+
+
+@pytest.mark.parametrize("nqubits", [5])
+@pytest.mark.parametrize("solver,dt,atol", [("exp", 1e-1, 1e-2), ("rk45", 1e-2, 1e-1)])
+def test_state_evolution_trotter_hamiltonian(
+ backend, accelerators, nqubits, solver, dt, atol
+):
+ if accelerators is not None and solver != "exp": # pragma: no cover
+ pytest.skip("Distributed evolution is supported only with exp solver.")
+ h = 1.0
+
+ target_psi = [np.ones(2**nqubits) / np.sqrt(2**nqubits)]
+ ham_matrix = backend.to_numpy(
+ hamiltonians.TFIM(nqubits, h=h, backend=backend).matrix
+ )
+ prop = expm(-1j * dt * ham_matrix)
+ for n in range(int(1 / dt)):
+ target_psi.append(prop.dot(target_psi[-1]))
+
+ ham = hamiltonians.TFIM(nqubits, h=h, dense=False, backend=backend)
+ checker = TimeStepChecker(target_psi, atol=atol)
+ evolution = models.StateEvolution(
+ ham, dt, solver=solver, callbacks=[checker], accelerators=accelerators
+ )
+ final_psi = evolution(final_time=1, initial_state=np.copy(target_psi[0]))
+
+ # Change dt
+ if solver == "exp":
+ evolution = models.StateEvolution(ham, dt / 10, accelerators=accelerators)
+ final_psi = evolution(final_time=1, initial_state=np.copy(target_psi[0]))
+ assert_states_equal(backend, final_psi, target_psi[-1], atol=atol)
+
+
+def test_adiabatic_evolution_init(backend):
+ # Hamiltonians of bad type
+ h0 = hamiltonians.X(3, backend=backend)
+ s = lambda t: t
+ with pytest.raises(TypeError):
+ adev = models.AdiabaticEvolution(h0, lambda t: h0, s, dt=1e-2)
+ h1 = hamiltonians.TFIM(2, backend=backend)
+ with pytest.raises(TypeError):
+ adev = models.AdiabaticEvolution(lambda t: h1, h1, s, dt=1e-2)
+ # Hamiltonians with different number of qubits
+ with pytest.raises(ValueError):
+ adev = models.AdiabaticEvolution(h0, h1, s, dt=1e-2)
+ # Adiabatic Hamiltonian with bad hamiltonian types
+ from qibo.hamiltonians.adiabatic import AdiabaticHamiltonian
+
+ with pytest.raises(TypeError):
+ h = AdiabaticHamiltonian("a", "b") # pylint: disable=E0110
+ # s with three arguments
+ h0 = hamiltonians.X(2, backend=backend)
+ s = lambda t, a, b: t + a + b
+ with pytest.raises(ValueError):
+ adev = models.AdiabaticEvolution(h0, h1, s, dt=1e-2)
+
+
+def test_adiabatic_evolution_schedule(backend):
+ h0 = hamiltonians.X(3, backend=backend)
+ h1 = hamiltonians.TFIM(3, backend=backend)
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-2)
+ assert adev.schedule(0.2) == 0.2 # pylint: disable=E1102
+ assert adev.schedule(0.8) == 0.8 # pylint: disable=E1102
+ # s(0) != 0
+ with pytest.raises(ValueError):
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t + 1, dt=1e-2)
+ # s(T) != 0
+ with pytest.raises(ValueError):
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t / 2, dt=1e-2)
+
+
+def test_set_scheduling_parameters(backend):
+ """Test ``AdiabaticEvolution.set_parameters``."""
+ h0 = hamiltonians.X(3, backend=backend)
+ h1 = hamiltonians.TFIM(3, backend=backend)
+ sp = lambda t, p: (1 - p[0]) * np.sqrt(t) + p[0] * t
+ adevp = models.AdiabaticEvolution(h0, h1, sp, 1e-2)
+ # access parametrized scheduling before setting parameters
+ with pytest.raises(ValueError):
+ s = adevp.schedule
+
+ adevp.set_parameters([0.5, 1])
+
+ target_s = lambda t: 0.5 * np.sqrt(t) + 0.5 * t
+ for t in np.random.random(10):
+ assert adevp.schedule(t) == target_s(t) # pylint: disable=E1102
+
+
+@pytest.mark.parametrize("dense", [False, True])
+def test_adiabatic_evolution_hamiltonian(backend, dense):
+ """Test adiabatic evolution hamiltonian as a function of time."""
+ h0 = hamiltonians.X(2, dense=dense, backend=backend)
+ h1 = hamiltonians.TFIM(2, dense=dense, backend=backend)
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-2)
+ # try accessing hamiltonian before setting it
+ with pytest.raises(RuntimeError):
+ adev.hamiltonian(0.1)
+
+ m1 = np.array([[0, 1, 1, 0], [1, 0, 0, 1], [1, 0, 0, 1], [0, 1, 1, 0]])
+ m2 = np.diag([2, -2, -2, 2])
+ ham = lambda t, T: -(1 - t / T) * m1 - (t / T) * m2
+
+ adev.hamiltonian.total_time = 1
+ for t in [0, 0.3, 0.7, 1.0]:
+ if dense:
+ matrix = adev.hamiltonian(t).matrix
+ else:
+ matrix = adev.hamiltonian(t).dense.matrix
+ backend.assert_allclose(matrix, ham(t, 1))
+
+ # try using a different total time
+ adev.hamiltonian.total_time = 2
+ for t in [0, 0.3, 0.7, 1.0]:
+ if dense:
+ matrix = adev.hamiltonian(t).matrix
+ else:
+ matrix = adev.hamiltonian(t).dense.matrix
+ backend.assert_allclose(matrix, ham(t, 2))
+
+
+@pytest.mark.parametrize("dt", [1e-1])
+def test_adiabatic_evolution_execute_exp(backend, dt):
+ """Test adiabatic evolution with exponential solver."""
+ h0 = hamiltonians.X(2, backend=backend)
+ h1 = hamiltonians.TFIM(2, backend=backend)
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=dt)
+
+ m1 = np.array([[0, 1, 1, 0], [1, 0, 0, 1], [1, 0, 0, 1], [0, 1, 1, 0]])
+ m2 = np.diag([2, -2, -2, 2])
+ ham = lambda t: -(1 - t) * m1 - t * m2
+
+ target_psi = np.ones(4) / 2
+ nsteps = int(1 / dt)
+ for n in range(nsteps):
+ target_psi = expm(-1j * dt * ham(n * dt)).dot(target_psi)
+ final_psi = adev(final_time=1)
+ assert_states_equal(backend, final_psi, target_psi)
+
+
+@pytest.mark.parametrize("nqubits,dt", [(4, 1e-1)])
+def test_trotterized_adiabatic_evolution(backend, accelerators, nqubits, dt):
+ """Test adiabatic evolution using Trotterization."""
+ dense_h0 = hamiltonians.X(nqubits, backend=backend)
+ dense_h1 = hamiltonians.TFIM(nqubits, backend=backend)
+
+ target_psi = [np.ones(2**nqubits) / np.sqrt(2**nqubits)]
+ ham = lambda t: dense_h0 * (1 - t) + dense_h1 * t
+ for n in range(int(1 / dt)):
+ prop = backend.to_numpy(ham(n * dt).exp(dt))
+ target_psi.append(prop.dot(target_psi[-1]))
+
+ local_h0 = hamiltonians.X(nqubits, dense=False, backend=backend)
+ local_h1 = hamiltonians.TFIM(nqubits, dense=False, backend=backend)
+ checker = TimeStepChecker(target_psi, atol=dt)
+ adev = models.AdiabaticEvolution(
+ local_h0,
+ local_h1,
+ lambda t: t,
+ dt,
+ callbacks=[checker],
+ accelerators=accelerators,
+ )
+ final_psi = adev(final_time=1)
+
+
+@pytest.mark.parametrize("solver", ["rk4", "rk45"])
+@pytest.mark.parametrize("dense", [False, True])
+@pytest.mark.parametrize("dt", [0.1])
+def test_adiabatic_evolution_execute_rk(backend, solver, dense, dt):
+ """Test adiabatic evolution with Runge-Kutta solver."""
+ h0 = hamiltonians.X(3, dense=dense, backend=backend)
+ h1 = hamiltonians.TFIM(3, dense=dense, backend=backend)
+
+ target_psi = [np.ones(8) / np.sqrt(8)]
+ ham = lambda t: h0 * (1 - t) + h1 * t
+ for n in range(int(1 / dt)):
+ prop = backend.to_numpy(ham(n * dt).exp(dt))
+ target_psi.append(prop.dot(target_psi[-1]))
+
+ checker = TimeStepChecker(target_psi, atol=dt)
+ adev = models.AdiabaticEvolution(
+ h0, h1, lambda t: t, dt, solver="rk4", callbacks=[checker]
+ )
+ final_psi = adev(final_time=1, initial_state=np.copy(target_psi[0]))
+
+
+def test_adiabatic_evolution_execute_errors(backend):
+ h0 = hamiltonians.X(3, backend=backend)
+ h1 = hamiltonians.TFIM(3, backend=backend)
+ # Non-zero ``start_time``
+ adev = models.AdiabaticEvolution(h0, h1, lambda t: t, dt=1e-2)
+ with pytest.raises(NotImplementedError):
+ final_state = adev(final_time=2, start_time=1)
+ # execute without specifying variational parameters
+ sp = lambda t, p: (1 - p) * np.sqrt(t) + p * t
+ adevp = models.AdiabaticEvolution(h0, h1, sp, dt=1e-1)
+ with pytest.raises(RuntimeError):
+ final_state = adevp(final_time=1)
+
+
+@pytest.mark.parametrize("solver,dt,atol", [("exp", 1e-1, 1e-10), ("rk45", 1e-2, 1e-2)])
+def test_energy_callback(backend, solver, dt, atol):
+ """Test using energy callback in adiabatic evolution."""
+ h0 = hamiltonians.X(2, backend=backend)
+ h1 = hamiltonians.TFIM(2, backend=backend)
+ energy = callbacks.Energy(h1)
+ adev = models.AdiabaticEvolution(
+ h0, h1, lambda t: t, dt=dt, callbacks=[energy], solver=solver
+ )
+ final_psi = adev(final_time=1)
+
+ target_psi = np.ones(4) / 2
+ calc_energy = lambda psi: psi.conj().dot(backend.to_numpy(h1.matrix).dot(psi))
+ target_energies = [calc_energy(target_psi)]
+ ham = lambda t: h0 * (1 - t) + h1 * t
+ for n in range(int(1 / dt)):
+ prop = backend.to_numpy(ham(n * dt).exp(dt))
+ target_psi = prop.dot(target_psi)
+ target_energies.append(calc_energy(target_psi))
+
+ assert_states_equal(backend, final_psi, target_psi, atol=atol)
+ target_energies = backend.cast(target_energies)
+ final_energies = np.array([backend.to_numpy(x) for x in energy[:]])
+ backend.assert_allclose(final_energies, target_energies, atol=atol)
+
+
+test_names = "method,options,messages,dense,filename"
+test_values = [
+ ("BFGS", {"maxiter": 1}, True, True, "adiabatic_bfgs.out"),
+ ("BFGS", {"maxiter": 1}, True, False, "trotter_adiabatic_bfgs.out"),
+ ("sgd", {"nepochs": 5}, False, True, None),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_scheduling_optimization(backend, method, options, messages, dense, filename):
+ """Test optimization of s(t)."""
+ from .test_models_variational import assert_regression_fixture
+
+ h0 = hamiltonians.X(3, dense=dense, backend=backend)
+ h1 = hamiltonians.TFIM(3, dense=dense, backend=backend)
+ sp = lambda t, p: (1 - p) * np.sqrt(t) + p * t
+ adevp = models.AdiabaticEvolution(h0, h1, sp, dt=1e-1)
+
+ if method == "sgd":
+ if backend.name != "tensorflow":
+ with pytest.raises(RuntimeError):
+ best, params, _ = adevp.minimize(
+ [0.5, 1], method=method, options=options, messages=messages
+ )
+ else:
+ best, params, _ = adevp.minimize(
+ [0.5, 1], method=method, options=options, messages=messages
+ )
+
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename)
diff --git a/tests/test_models_grover.py b/tests/test_models_grover.py
new file mode 100644
index 000000000..b0725a80d
--- /dev/null
+++ b/tests/test_models_grover.py
@@ -0,0 +1,124 @@
+"""Test Grover model defined in `qibo/models/grover.py`."""
+
+import pytest
+
+from qibo import Circuit, gates
+from qibo.models import Grover
+
+
+def test_grover_init(backend):
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ superposition = Circuit(5)
+ superposition.add([gates.H(i) for i in range(5)])
+ grover = Grover(oracle, superposition_circuit=superposition)
+ assert grover.oracle == oracle
+ assert grover.superposition == superposition
+ assert grover.sup_qubits == 5
+ assert grover.sup_size == 32
+ assert not grover.iterative
+ grover = Grover(
+ oracle, superposition_circuit=superposition, superposition_size=int(2**5)
+ )
+ assert grover.oracle == oracle
+ assert grover.superposition == superposition
+ assert grover.sup_qubits == 5
+ assert grover.sup_size == 32
+ assert not grover.iterative
+
+
+def test_grover_init_default_superposition(backend):
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ # try to initialize without passing `superposition_qubits`
+ with pytest.raises(ValueError):
+ grover = Grover(oracle)
+
+ grover = Grover(oracle, superposition_qubits=4)
+ assert grover.oracle == oracle
+ assert grover.sup_qubits == 4
+ assert grover.sup_size == 16
+ assert grover.superposition.depth == 1
+ assert grover.superposition.ngates == 4
+
+
+def test_grover_initial_state(backend):
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ initial_state = Circuit(5)
+ initial_state.add(gates.X(4))
+ grover = Grover(
+ oracle,
+ superposition_qubits=5,
+ initial_state_circuit=initial_state,
+ number_solutions=1,
+ )
+ assert grover.initial_state_circuit == initial_state
+ solution, iterations = grover(logs=True, backend=backend)
+ assert solution == ["11111"]
+
+
+def test_grover_target_amplitude(backend):
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ grover = Grover(oracle, superposition_qubits=5, target_amplitude=1 / 2 ** (5 / 2))
+ solution, iterations = grover(logs=True, backend=backend)
+ assert len(solution) == 1
+ assert solution == ["11111"]
+
+
+def test_grover_wrong_solution(backend):
+ def check(result):
+ for i in result:
+ if int(i) != 1:
+ return False
+ return True
+
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ grover = Grover(oracle, superposition_qubits=5, check=check, number_solutions=2)
+ solution, iterations = grover(logs=True, backend=backend)
+ assert len(solution) == 2
+
+
+def test_grover_iterative(backend):
+ def check(result):
+ for i in result:
+ if int(i) != 1:
+ return False
+ return True
+
+ def check_false(result):
+ return False
+
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ grover = Grover(oracle, superposition_qubits=5, check=None, iterative=True)
+ with pytest.raises(ValueError):
+ solution, iterations = grover(backend=backend)
+ grover = Grover(oracle, superposition_qubits=5, check=check_false, iterative=True)
+ solution, iterations = grover(backend=backend)
+ grover = Grover(oracle, superposition_qubits=5, check=check, iterative=True)
+ solution, iterations = grover(logs=True, backend=backend)
+ assert solution == "11111"
+
+
+@pytest.mark.parametrize("num_sol", [None, 1])
+def test_grover_execute(backend, num_sol):
+ def check(result):
+ for i in result:
+ if int(i) != 1:
+ return False
+ return True
+
+ oracle = Circuit(5 + 1)
+ oracle.add(gates.X(5).controlled_by(*range(5)))
+ grover = Grover(
+ oracle, superposition_qubits=5, check=check, number_solutions=num_sol
+ )
+ solution, iterations = grover(freq=True, logs=True, backend=backend)
+ if num_sol:
+ assert solution == ["11111"]
+ assert iterations == 4
+ else:
+ assert solution == "11111"
diff --git a/tests/test_models_hep.py b/tests/test_models_hep.py
new file mode 100644
index 000000000..fdf0a4611
--- /dev/null
+++ b/tests/test_models_hep.py
@@ -0,0 +1,111 @@
+"""Testing HEP models."""
+
+import numpy as np
+import pytest
+
+from qibo.models.hep import qPDF
+
+test_names = "ansatz,layers,nqubits,multi_output,output"
+test_values = [
+ ("Fourier", 3, 1, True, np.array([[1.214777]])),
+ ("Weighted", 5, 1, True, np.array([[0.506931]])),
+ ("Fourier", 5, 1, True, np.array([[0.475777]])),
+ (
+ "Weighted",
+ 3,
+ 8,
+ True,
+ np.array(
+ [
+ [
+ 0.102331,
+ 0.196291,
+ 0.250836,
+ 0.246776,
+ 2.63353,
+ 0.658077,
+ 0.421151,
+ 0.025667,
+ ]
+ ]
+ ),
+ ),
+ (
+ "Fourier",
+ 3,
+ 8,
+ True,
+ np.array(
+ [
+ [
+ 1.441044,
+ 15.967666,
+ 4.176307,
+ 21.950216,
+ 4.55334,
+ 1.860604,
+ 33.80421,
+ 25.587542,
+ ]
+ ]
+ ),
+ ),
+ (
+ "Weighted",
+ 5,
+ 8,
+ True,
+ np.array(
+ [
+ [
+ 0.028491,
+ 0.239515,
+ 0.163759,
+ 0.090604,
+ 1.913209,
+ 0.36426,
+ 0.357044,
+ 0.028548,
+ ]
+ ]
+ ),
+ ),
+ (
+ "Fourier",
+ 5,
+ 8,
+ True,
+ np.array(
+ [
+ [
+ 2.473517,
+ 5.402246,
+ 1.073041,
+ 84.60309,
+ 2.271607,
+ 2.877924,
+ 27.200738,
+ 1.657042,
+ ]
+ ]
+ ),
+ ),
+ ("Weighted", 3, 1, False, np.array([[0.613767]])),
+ ("Fourier", 3, 1, False, np.array([[1.214777]])),
+ ("Weighted", 5, 1, False, np.array([[0.506931]])),
+ ("Fourier", 5, 1, False, np.array([[0.475777]])),
+ ("Weighted", 3, 8, False, np.array([[0.102331]])),
+ ("Fourier", 3, 8, False, np.array([[1.441044]])),
+ ("Weighted", 5, 8, False, np.array([[0.028491]])),
+ ("Fourier", 5, 8, False, np.array([[2.473517]])),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_qpdf(backend, ansatz, layers, nqubits, multi_output, output):
+ """Performs a qPDF circuit minimization test."""
+ model = qPDF(ansatz, layers, nqubits, multi_output, backend=backend)
+ np.random.seed(0)
+ params = np.random.rand(model.nparams)
+ result = model.predict(params, [0.1])
+ np.testing.assert_allclose(result, output, atol=1e-5)
diff --git a/tests/test_models_iqae.py b/tests/test_models_iqae.py
new file mode 100644
index 000000000..8f75880f1
--- /dev/null
+++ b/tests/test_models_iqae.py
@@ -0,0 +1,211 @@
+"""Test IQAE model defined in `qibo/models/iqae.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.models.iqae import IQAE
+
+
+def test_iqae_init(backend):
+ A = Circuit(3 + 1)
+ Q = Circuit(3 + 1)
+ alpha = 0.05
+ epsilon = 0.005
+ n_shots = 1024
+ method = "chernoff"
+ iqae = IQAE(A, Q, alpha, epsilon, n_shots, method)
+ assert iqae.circuit_a == A
+ assert iqae.circuit_q == Q
+ assert iqae.alpha == alpha
+ assert iqae.epsilon == epsilon
+ assert iqae.n_shots == n_shots
+ assert iqae.method == method
+
+
+def test_iqae_init_raising_errors(backend):
+ A = Circuit(3 + 1)
+ Q = Circuit(4 + 1)
+ # incorrect values of:
+ alpha = 2
+ epsilon = 2
+ method = "other"
+ # try to initialize passing a `circuit_A` with more qubits than `circuit_Q`
+ with pytest.raises(ValueError):
+ iqae = IQAE(circuit_a=Q, circuit_q=A)
+ # try to initialize with incorrect `alpha`
+ with pytest.raises(ValueError):
+ iqae = IQAE(A, Q, alpha=alpha)
+ # try to initialize with incorrect `epsilon`
+ with pytest.raises(ValueError):
+ iqae = IQAE(A, Q, epsilon=epsilon)
+ # try to initialize with incorrect `method`
+ with pytest.raises(ValueError):
+ iqae = IQAE(A, Q, method=method)
+ # try to initialize with incorrect `n_shots`
+ with pytest.raises(ValueError):
+ iqae = IQAE(A, Q, n_shots=0.5)
+ # testing the line of code when n_shots_i==0
+ iqae = IQAE(A, Q, method="beta", n_shots=10, alpha=0.05, epsilon=0.48)
+ results = iqae.execute(backend=backend)
+
+
+def test_iqae_execution(backend):
+ # Let's check if we get the correct result for the integral of Sin(x)^2 from 0 to 1
+ nbit = 3
+ A = A_circ(qx=list(range(nbit)), qx_measure=nbit, nbit=nbit, b_max=1, b_min=0)
+ Q = Q_circ(qx=list(range(nbit)), qx_measure=nbit, nbit=nbit, b_max=1, b_min=0)
+
+ iqae = IQAE(A, Q, method="chernoff")
+ results = iqae.execute(backend=backend)
+ # Check that we run in the lower half-plane
+ for i in range(10):
+ iqae = IQAE(A, Q, method="chernoff", alpha=0.05, epsilon=0.01, n_shots=100)
+ results = iqae.execute(backend=backend)
+ iqae = IQAE(A, Q, method="beta")
+ results = iqae.execute(backend=backend)
+ true_result = 1 / 4 * (2 - np.sin(2))
+ # Check that the result lies in the expected interval
+ assert results.estimation > true_result - 2 * results.epsilon_estimated
+ assert results.estimation < true_result + 2 * results.epsilon_estimated
+
+
+def reflect_qibo(qc, qx, qx_measure, qx_ancilla, nbit, b_max):
+ """
+ Computing reflection operator (I - 2|0><0|)
+ qc: quantum circuit
+ qx: quantum register
+ qx_measure: quantum register for measurement
+ qx_ancilla: temporal quantum register for decomposing multi controlled NOT gate
+ nbit: number of qubits to be used for defining the good and bad state
+ b_max: upper limit of integral
+ b_min: lower limit of integral
+ """
+ for i in range(nbit):
+ qc.add(gates.X(q=qx[i]))
+ qc.add(gates.X(q=qx_measure))
+ multi_control_NOT_qibo(qc, qx, qx_measure, qx_ancilla, nbit, b_max)
+ qc.add(gates.X(q=qx_measure))
+ for i in range(nbit):
+ qc.add(gates.X(q=qx[i]))
+
+
+def multi_control_NOT_qibo(qc, qx, qx_measure, qx_ancilla, nbit, b_max):
+ """
+ Computing multi controlled NOT gate
+ qc: quantum circuit
+ qx: quantum register
+ qx_measure: quantum register for measurement
+ qx_ancilla: temporal quantum register for decomposing multi controlled NOT gate
+ nbit: number of qubits to be used for defining the good and bad state
+ b_max: upper limit of integral
+ b_min: lower limit of integral
+ """
+ if nbit == 1:
+ qc.add(gates.CZ(qx[0], qx_measure))
+ elif nbit == 2:
+ qc.add(gates.H(qx_measure))
+ qc.add(gates.TOFFOLI(qx[0], qx[1], qx_measure))
+ qc.add(gates.H(qx_measure))
+ elif nbit > 2.0:
+ qc.add(gates.TOFFOLI(qx[0], qx[1], qx_ancilla[0]))
+ for i in range(nbit - 3):
+ qc.add(gates.TOFFOLI(qx[i + 2], qx_ancilla[i], qx_ancilla[i + 1]))
+ qc.add(gates.H(qx_measure))
+ qc.add(gates.TOFFOLI(qx[nbit - 1], qx_ancilla[nbit - 3], qx_measure))
+ qc.add(gates.H(qx_measure))
+ for i in range(nbit - 3)[::-1]:
+ qc.add(gates.TOFFOLI(qx[i + 2], qx_ancilla[i], qx_ancilla[i + 1]))
+ qc.add(gates.TOFFOLI(qx[0], qx[1], qx_ancilla[0]))
+
+
+def P_qibo(qc, qx):
+ """
+ Generating the uniform probability distribution p(x)=1/2^n
+ qc: quantum circuit
+ qx: quantum register
+
+ The inverse of P = P
+ """
+ for q in qx:
+ qc.add(gates.H(q=q))
+
+
+def R_qibo(qc, qx, qx_measure, nbit, b_max, b_min):
+ """
+ Encoding the function f(x)=sin(x)^2 to be integrated in the interval [b_min,b_max]
+ qc: quantum circuit
+ qx: quantum register
+ qx_measure: quantum register for measurement
+ nbit: number of qubits
+ b_max: upper limit of integral
+ b_min: lower limit of integral
+ """
+ qc.add(
+ gates.RY(q=qx_measure, theta=(b_max - b_min) / 2**nbit * 2 * 0.5 + 2 * b_min)
+ )
+ for i in range(nbit):
+ qc.add(gates.CU3(qx[i], qx_measure, 2**i * (b_max - b_min) / 2**nbit * 2, 0, 0))
+
+
+def Rinv_qibo(qc, qx, qx_measure, nbit, b_max, b_min):
+ """
+ The inverse of R
+ qc: quantum circuit
+ qx: quantum register
+ qx_measure : quantum register for measurement
+ nbit: number of qubits
+ b_max: upper limit of integral
+ b_min: lower limit of integral
+ """
+ for i in range(nbit)[::-1]:
+ qc.add(
+ gates.CU3(qx[i], qx_measure, -(2**i) * (b_max - b_min) / 2**nbit * 2, 0, 0)
+ )
+ qc.add(
+ gates.RY(q=qx_measure, theta=-(b_max - b_min) / 2**nbit * 2 * 0.5 - 2 * b_min)
+ )
+
+
+def A_circ(qx, qx_measure, nbit, b_max, b_min):
+ """
+ The initialization operator
+ A: quantum circuit
+ qx: quantum register
+ qx_measure: quantum register for measurement
+ nbit: number of qubits to be used for defining the good and bad state
+ b_max: upper limit of integral
+ b_max: lower limit of integral
+ """
+ circ = Circuit(nbit + 1)
+ # The operator P encodes the probability function p(x) into the state |0⟩_n
+ P_qibo(circ, qx)
+ # The operator R encodes the f (x) function into an ancillary qubit that is added to the circuit
+ R_qibo(circ, qx, qx_measure, nbit, b_max, b_min)
+ return circ
+
+
+def Q_circ(qx, qx_measure, nbit, b_max, b_min):
+ """
+ The Grover/Amplification operator: R P (I - 2|0><0|) P^+ R^+ U_psi_0
+ q: quantum circuit
+ qx: quantum register
+ qx_measure: quantum register for measurement
+ nbit: number of qubits to be used for defining the good and bad state
+ b_max: upper limit of integral
+ b_max: lower limit of integral
+ """
+ if nbit > 2:
+ circ = Circuit(2 * nbit - 1)
+ qx_ancilla = list(range(nbit + 1, nbit + 1 + nbit - 2))
+ else:
+ circ = Circuit(nbit + 1)
+ qx_ancilla = 0
+
+ circ.add(gates.Z(q=qx_measure))
+ Rinv_qibo(circ, qx, qx_measure, nbit, b_max, b_min)
+ P_qibo(circ, qx)
+ reflect_qibo(circ, qx, qx_measure, qx_ancilla, nbit, b_max)
+ P_qibo(circ, qx)
+ R_qibo(circ, qx, qx_measure, nbit, b_max, b_min)
+ return circ
diff --git a/tests/test_models_qcnn.py b/tests/test_models_qcnn.py
new file mode 100644
index 000000000..45dfe9292
--- /dev/null
+++ b/tests/test_models_qcnn.py
@@ -0,0 +1,354 @@
+import math
+
+import numpy as np
+
+import qibo
+from qibo import gates, set_backend
+from qibo.models import Circuit
+from qibo.models.qcnn import QuantumCNN
+
+num_angles = 21
+angles0 = [i * math.pi / num_angles for i in range(num_angles)]
+
+
+def test_classifier_circuit2():
+ """ """
+ set_backend("numpy")
+ nqubits = 2
+ nlayers = int(nqubits / 2)
+ init_state = np.ones(2**nqubits) / np.sqrt(2**nqubits) #
+
+ qcnn = QuantumCNN(nqubits, nlayers, nclasses=2) # , params=angles0)
+
+ angles = [0] + angles0
+
+ circuit = qcnn.Classifier_circuit(angles)
+ qcnn.set_circuit_params(
+ angles, has_bias=True
+ ) # only to test line 209-210 in qcnn.py
+
+ # circuit = qcnn._circuit
+ statevector = circuit(init_state).state()
+ real_vector = get_real_vector2()
+
+ # to compare statevector and real_vector
+ np.testing.assert_allclose(statevector.real, real_vector.real, atol=1e-5)
+ np.testing.assert_allclose(statevector.imag, real_vector.imag, atol=1e-5)
+
+
+def get_real_vector2():
+ nqubits = 2
+ bits = range(nqubits)
+ init_state = np.ones(2**nqubits) / np.sqrt(2**nqubits) #
+ angles = angles0
+
+ # convolution
+ k = 0
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[0], angles[k : k + 3]).unitary(), init_state
+ )
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[0], bits[1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[0], bits[1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[0], bits[1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ # pooling
+ ksink = k
+ a = np.dot(one_qubit_unitary(nqubits, bits[1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[0], angles[k : k + 3]).unitary(), a)
+ a = np.dot(CNOT_unitary(nqubits, bits[0], bits[1]).unitary(), a)
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[1], angles[ksink : ksink + 3])
+ .invert()
+ .unitary(),
+ a,
+ )
+
+ return a
+
+
+def test_classifier_circuit4():
+ """ """
+ set_backend("numpy")
+ nqubits = 4
+ nlayers = int(nqubits / 2)
+ init_state = np.ones(2**nqubits) / np.sqrt(2**nqubits) #
+
+ qcnn = QuantumCNN(nqubits, nlayers, nclasses=2)
+ angles = [0] + angles0 + angles0
+
+ circuit = qcnn.Classifier_circuit(angles)
+ statevector = circuit(init_state).state()
+ real_vector = get_real_vector4()
+
+ # to compare statevector and real_vector
+ np.testing.assert_allclose(statevector.real, real_vector.real, atol=1e-5)
+ np.testing.assert_allclose(statevector.imag, real_vector.imag, atol=1e-5)
+
+
+def get_real_vector4():
+ nqubits = 4
+ init_state = np.ones(2**nqubits) / np.sqrt(2**nqubits) #
+ angles = angles0
+ bits = range(nqubits)
+ # convolution - layer 1
+ # to declare matrix array a
+
+ b0 = 0
+ b1 = 1
+ k = 0
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), init_state
+ )
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+
+ b0 = 2
+ b1 = 3
+ k = 0
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+
+ b0 = 1
+ b1 = 2
+ k = 0
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+
+ b0 = 3
+ b1 = 0
+ k = 0
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[b0], bits[b1], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[b0], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[b1], angles[k : k + 3]).unitary(), a)
+
+ # pooling - layer 1
+ k = 15 # k+=3
+ ksink = k
+ a = np.dot(one_qubit_unitary(nqubits, bits[2], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[0], angles[k : k + 3]).unitary(), a)
+ a = np.dot(CNOT_unitary(nqubits, bits[0], bits[2]).unitary(), a)
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[2], angles[ksink : ksink + 3])
+ .invert()
+ .unitary(),
+ a,
+ )
+
+ k = 15 # k+=3
+ ksink = k
+ a = np.dot(one_qubit_unitary(nqubits, bits[3], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[1], angles[k : k + 3]).unitary(), a)
+ a = np.dot(CNOT_unitary(nqubits, bits[1], bits[3]).unitary(), a)
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[3], angles[ksink : ksink + 3])
+ .invert()
+ .unitary(),
+ a,
+ )
+
+ # convolution - layer 2
+ k = 0
+ a = np.dot(one_qubit_unitary(nqubits, bits[2], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[3], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(RZZ_unitary(nqubits, bits[2], bits[3], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RYY_unitary(nqubits, bits[2], bits[3], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(RXX_unitary(nqubits, bits[2], bits[3], angles[k]).unitary(), a)
+ k += 1
+ a = np.dot(one_qubit_unitary(nqubits, bits[2], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[3], angles[k : k + 3]).unitary(), a)
+ k += 3
+
+ # pooling - layer 2
+ ksink = k
+ a = np.dot(one_qubit_unitary(nqubits, bits[3], angles[k : k + 3]).unitary(), a)
+ k += 3
+ a = np.dot(one_qubit_unitary(nqubits, bits[2], angles[k : k + 3]).unitary(), a)
+ a = np.dot(CNOT_unitary(nqubits, bits[2], bits[3]).unitary(), a)
+ a = np.dot(
+ one_qubit_unitary(nqubits, bits[3], angles[ksink : ksink + 3])
+ .invert()
+ .unitary(),
+ a,
+ )
+
+ return a
+
+
+def one_qubit_unitary(nqubits, bit, symbols):
+ c = Circuit(nqubits)
+ c.add(gates.RX(bit, symbols[0]))
+ c.add(gates.RY(bit, symbols[1]))
+ c.add(gates.RZ(bit, symbols[2]))
+
+ return c
+
+
+def RXX_unitary(nqubits, bit0, bit1, angle):
+ c = Circuit(nqubits)
+ c.add(gates.RXX(bit0, bit1, angle))
+
+ return c
+
+
+def RYY_unitary(nqubits, bit0, bit1, angle):
+ c = Circuit(nqubits)
+ c.add(gates.RYY(bit0, bit1, angle))
+
+ return c
+
+
+def RZZ_unitary(nqubits, bit0, bit1, angle):
+ c = Circuit(nqubits)
+ c.add(gates.RZZ(bit0, bit1, angle))
+
+ return c
+
+
+def CNOT_unitary(nqubits, bit0, bit1):
+ c = Circuit(nqubits)
+ c.add(gates.CNOT(bit0, bit1))
+
+ return c
+
+
+def test_1_qubit_classifier_circuit_error():
+ try:
+ QuantumCNN(nqubits=1, nlayers=1, nclasses=2)
+ except:
+ pass
+
+
+def test_qcnn_training():
+ import random
+
+ set_backend("numpy")
+
+ # generate 2 random states and labels for pytest
+ data = np.zeros([2, 16])
+ for i in range(2):
+ data_i = np.random.rand(16)
+ data[i] = data_i / np.linalg.norm(data_i)
+ labels = [[1], [-1]]
+
+ # test qcnn training
+ testbias = np.zeros(1)
+ testangles = [random.uniform(0, 2 * np.pi) for i in range(21 * 2)]
+ init_theta = np.concatenate((testbias, testangles))
+ test_qcnn = QuantumCNN(nqubits=4, nlayers=1, nclasses=2, params=init_theta)
+ testcircuit = test_qcnn._circuit
+ result = test_qcnn.minimize(
+ init_theta, data=data, labels=labels, nshots=10000, method="Powell"
+ )
+
+ # test Predictions function
+ predictions = []
+ for n in range(len(data)):
+ predictions.append(test_qcnn.predict(data[n], nshots=10000)[0])
+
+ # test Accuracy function
+ predictions.append(1)
+ labels = np.array([[1], [-1], [1]])
+ test_qcnn.Accuracy(labels, predictions)
+
+
+def test_two_qubit_ansatz():
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.RX(0, 0))
+ c.add(gates.CNOT(1, 0))
+ test_qcnn = QuantumCNN(4, 2, 2, twoqubitansatz=c)
+
+
+def test_two_qubit_ansatz_training():
+ # test qibojit case (copy initial state as quick-fix for in-place update)
+ qibo.set_backend("qibojit")
+
+ c = Circuit(2)
+ c.add(gates.H(0))
+ c.add(gates.RX(0, 0))
+ c.add(gates.CNOT(1, 0))
+ test_qcnn = QuantumCNN(4, 2, 2, twoqubitansatz=c)
+
+ data = np.zeros([2, 16])
+ for i in range(2):
+ data_i = np.random.rand(16)
+ data[i] = data_i / np.linalg.norm(data_i)
+ labels = [[1], [-1]]
+
+ totalNParams = test_qcnn.nparams_layer * 2
+ init_theta = [
+ 0 for i in range(totalNParams + 1)
+ ] # totalNParams+1 to account for bias parameter.
+
+ result = test_qcnn.minimize(
+ init_theta, data=data, labels=labels, nshots=10000, method="Powell"
+ )
+
+ # test Predictions function
+ predictions = []
+ for n in range(len(data)):
+ predictions.append(test_qcnn.predict(data[n], nshots=10000)[0])
+
+ # test Accuracy function
+ predictions.append(1)
+ labels = np.array([[1], [-1], [1]])
+ test_qcnn.Accuracy(labels, predictions)
diff --git a/tests/test_models_qdp.py b/tests/test_models_qdp.py
new file mode 100644
index 000000000..c5f394586
--- /dev/null
+++ b/tests/test_models_qdp.py
@@ -0,0 +1,69 @@
+"""Test cases for DensityMatrixExponentiation model."""
+
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.models.circuit import Circuit
+from qibo.models.qdp.memory_usage_query import DensityMatrixExponentiation
+
+
+def test_increment_instruction_qubit():
+ """Test incrementing the current instruction register index."""
+ protocol = DensityMatrixExponentiation(
+ theta=np.pi,
+ N=1,
+ num_work_qubits=1,
+ num_instruction_qubits=2,
+ number_muq_per_call=1,
+ )
+ protocol.increment_current_instruction_register()
+ assert protocol.id_current_instruction_reg == 2
+
+
+def test_return_circuit():
+ """Test returning the circuit generated by DensityMatrixExponentiation."""
+ protocol = DensityMatrixExponentiation(
+ theta=np.pi,
+ N=2,
+ num_work_qubits=1,
+ num_instruction_qubits=2,
+ number_muq_per_call=1,
+ )
+ protocol.memory_call_circuit(num_instruction_qubits_per_query=2)
+ assert isinstance(protocol.return_circuit(), Circuit)
+
+
+def compute_swapped_probability(iteration, nshots=1000):
+ """Compute the probability of observing '1' after DensityMatrixExponentiation.
+
+ Args:
+ iteration (int): The number of iterations for the exponentiation.
+ nshots (int, optional): The number of shots for executing the circuit. Defaults to 1000.
+
+ Returns:
+ float: The probability of observing '1' after the exponentiation.
+ """
+ protocol = DensityMatrixExponentiation(
+ theta=np.pi,
+ N=iteration,
+ num_work_qubits=1,
+ num_instruction_qubits=iteration,
+ number_muq_per_call=1,
+ )
+ protocol.memory_call_circuit(num_instruction_qubits_per_query=iteration)
+ protocol.c.add(gates.M(0))
+ freq_dict = protocol.c.execute(nshots=nshots).frequencies(registers=True)
+ counter = freq_dict["register0"]
+ return counter["1"] / nshots
+
+
+def test_density_matrix_exponentiation_N_one():
+ """Test case for DensityMatrixExponentiation with N=1."""
+ assert compute_swapped_probability(1) == 1
+
+
+def test_density_matrix_exponentiation_N_20():
+ """Test case for DensityMatrixExponentiation with N=20."""
+ probability = compute_swapped_probability(20)
+ assert probability >= 0.7
diff --git a/tests/test_models_qft.py b/tests/test_models_qft.py
new file mode 100644
index 000000000..90daa4fe9
--- /dev/null
+++ b/tests/test_models_qft.py
@@ -0,0 +1,82 @@
+"""Test methods defined in `qibo/models/circuit.py`."""
+
+import numpy as np
+import pytest
+
+from qibo import models
+from qibo.quantum_info import random_statevector
+
+
+def qft_matrix(dimension: int, inverse: bool = False) -> np.ndarray:
+ """Creates exact QFT matrix.
+
+ Args:
+ dimension: Dimension d of the matrix. The matrix will be d x d.
+ inverse: Whether to construct matrix for the inverse QFT.
+
+ Return:
+ QFT transformation matrix as a numpy array with shape (d, d).
+ """
+ exponent = np.outer(np.arange(dimension), np.arange(dimension))
+ sign = 1 - 2 * int(inverse)
+ return np.exp(sign * 2 * np.pi * 1j * exponent / dimension) / np.sqrt(dimension)
+
+
+def exact_qft(
+ x: np.ndarray, density_matrix: bool = False, backend=None, inverse: bool = False
+) -> np.ndarray:
+ """Performs exact QFT to a given state vector."""
+ dim = len(x)
+ matrix = qft_matrix(dim, inverse)
+ if backend is not None:
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ if density_matrix:
+ return matrix @ x @ backend.np.conj(matrix).T
+ return matrix @ x
+
+
+@pytest.mark.parametrize("nqubits", [4, 10, 100])
+def test_qft_circuit_size(nqubits):
+ c = models.QFT(nqubits)
+ assert c.nqubits == nqubits
+ assert c.depth == 2 * nqubits
+ assert c.ngates == nqubits**2 // 2 + nqubits
+
+
+@pytest.mark.parametrize("nqubits", [4, 5])
+def test_qft_matrix(backend, nqubits):
+ c = models.QFT(nqubits)
+ dim = 2**nqubits
+ target_matrix = qft_matrix(dim)
+ backend.assert_allclose(c.unitary(backend), target_matrix)
+ c = c.invert()
+ target_matrix = qft_matrix(dim, inverse=True)
+ backend.assert_allclose(c.unitary(backend), target_matrix)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("nqubits", [5, 6])
+@pytest.mark.parametrize("random", [False, True])
+def test_qft_execution(backend, nqubits, random, density_matrix):
+ c = models.QFT(nqubits, density_matrix=density_matrix)
+ initial_state = (
+ random_statevector(2**nqubits, backend=backend)
+ if random
+ else backend.zero_state(nqubits)
+ )
+ if density_matrix:
+ initial_state = backend.np.outer(initial_state, backend.np.conj(initial_state))
+
+ final_state = backend.execute_circuit(c, backend.np.copy(initial_state))._state
+ target_state = exact_qft(initial_state, density_matrix, backend)
+ backend.assert_allclose(final_state, target_state)
+
+
+def test_qft_errors():
+ """Check that ``_DistributedQFT`` raises error if not sufficient qubits."""
+ from qibo.models.qft import _DistributedQFT
+
+ with pytest.raises(NotImplementedError):
+ c = models.QFT(10, with_swaps=False, accelerators={"/GPU:0": 2})
+ with pytest.raises(NotImplementedError):
+ c = _DistributedQFT(2, accelerators={"/GPU:0": 4})
diff --git a/tests/test_models_tsp.py b/tests/test_models_tsp.py
new file mode 100644
index 000000000..d0501844a
--- /dev/null
+++ b/tests/test_models_tsp.py
@@ -0,0 +1,45 @@
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.models import QAOA, Circuit
+from qibo.models.tsp import TSP
+
+from .test_models_variational import assert_regression_fixture
+
+
+def qaoa_function_of_layer(backend, layer):
+ """
+ This is a function to study the impact of the number of layers on QAOA, it takes
+ in the number of layers and compute the distance of the mode of the histogram obtained
+ from QAOA
+ """
+ num_cities = 3
+ distance_matrix = np.array([[0, 0.9, 0.8], [0.4, 0, 0.1], [0, 0.7, 0]])
+ # there are two possible cycles, one with distance 1, one with distance 1.9
+ distance_matrix = distance_matrix.round(1)
+
+ small_tsp = TSP(distance_matrix, backend=backend)
+ initial_state = small_tsp.prepare_initial_state([i for i in range(num_cities)])
+ obj_hamil, mixer = small_tsp.hamiltonians()
+ qaoa = QAOA(obj_hamil, mixer=mixer)
+ initial_state = backend.cast(initial_state, copy=True)
+ best_energy, final_parameters, extra = qaoa.minimize(
+ initial_p=[0.1 for i in range(layer)],
+ initial_state=initial_state,
+ method="BFGS",
+ options={"maxiter": 1},
+ )
+ qaoa.set_parameters(final_parameters)
+ return qaoa.execute(initial_state)
+
+
+@pytest.mark.parametrize("nlayers", [2, 4])
+def test_tsp(backend, nlayers):
+ final_state = backend.to_numpy(qaoa_function_of_layer(backend, nlayers))
+ assert_regression_fixture(
+ backend, final_state.real, f"tsp_layer{nlayers}_real.out", rtol=1e-3, atol=1e-5
+ )
+ assert_regression_fixture(
+ backend, final_state.imag, f"tsp_layer{nlayers}_imag.out", rtol=1e-3, atol=1e-5
+ )
diff --git a/tests/test_models_utils.py b/tests/test_models_utils.py
new file mode 100644
index 000000000..5b7ce88ee
--- /dev/null
+++ b/tests/test_models_utils.py
@@ -0,0 +1,36 @@
+"""Test `fourier_coefficients' in `qibo/models/utils.py`."""
+
+import numpy as np
+import pytest
+
+from qibo.models.utils import fourier_coefficients
+
+
+def test_fourier_coefficients_raising_errors():
+ result = fourier_coefficients(function, 1, 2, lowpass_filter=True)
+ with pytest.raises(ValueError):
+ # n_inputs!=len(degree)
+ result = fourier_coefficients(
+ function, n_inputs=1, degree=[2, 2], lowpass_filter=True
+ )
+ with pytest.raises(ValueError):
+ # n_inputs!=len(filter_threshold)
+ result = fourier_coefficients(
+ function, 1, 2, lowpass_filter=True, filter_threshold=[5, 6]
+ )
+ result = fourier_coefficients(
+ function, n_inputs=1, degree=2, lowpass_filter=True, filter_threshold=5
+ )
+ result = fourier_coefficients(function, n_inputs=1, degree=2, lowpass_filter=False)
+
+
+def test_fourier_coefficients_expected_result():
+ result = fourier_coefficients(function, 1, 2, lowpass_filter=True)
+ # The coefficients should have this form:
+ coeffs = np.array([1, 1 - 2j, 1.5 - 2.5j, 1.5 + 2.5j, 1 + 2j])
+ assert coeffs.all() == result.all()
+
+
+def function(x):
+ y = 1 + 2 * np.cos(x) + 3 * np.cos(2 * x) + 4 * np.sin(x) + 5 * np.sin(2 * x)
+ return y
diff --git a/tests/test_models_variational.py b/tests/test_models_variational.py
new file mode 100644
index 000000000..c59699048
--- /dev/null
+++ b/tests/test_models_variational.py
@@ -0,0 +1,377 @@
+"""
+Testing Variational Quantum Circuits.
+"""
+
+import pathlib
+
+import numpy as np
+import pytest
+from scipy.linalg import expm
+
+from qibo import gates, hamiltonians, models
+from qibo.models.utils import cvar, gibbs
+from qibo.quantum_info import random_statevector
+
+REGRESSION_FOLDER = pathlib.Path(__file__).with_name("regressions")
+
+
+def assert_regression_fixture(backend, array, filename, rtol=1e-5, atol=1e-12):
+ """Check array matches data inside filename.
+
+ Args:
+ array: numpy array/
+ filename: fixture filename
+
+ If filename does not exists, this function
+ creates the missing file otherwise it loads
+ from file and compare.
+ """
+
+ def load(filename):
+ return np.loadtxt(filename)
+
+ filename = REGRESSION_FOLDER / filename
+ try:
+ array_fixture = load(filename)
+ except: # pragma: no cover
+ # case not tested in GitHub workflows because files exist
+ np.savetxt(filename, array)
+ array_fixture = load(filename)
+ backend.assert_allclose(array, array_fixture, rtol=rtol, atol=atol)
+
+
+test_names = "method,options,compile,filename"
+test_values = [
+ ("Powell", {"maxiter": 1}, True, "vqc_powell.out"),
+ ("Powell", {"maxiter": 1}, False, "vqc_powell.out"),
+ ("BFGS", {"maxiter": 1}, True, "vqc_bfgs.out"),
+ ("BFGS", {"maxiter": 1}, False, "vqc_bfgs.out"),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_vqc(backend, method, options, compile, filename):
+ """Performs a variational circuit minimization test."""
+ from qibo.optimizers import optimize
+
+ def myloss(parameters, circuit, target):
+ circuit.set_parameters(parameters)
+ state = backend.to_numpy(backend.execute_circuit(circuit).state())
+ return 1 - np.abs(np.dot(np.conj(target), state))
+
+ nqubits = 3
+ nlayers = 4
+
+ # Create variational circuit
+ c = models.Circuit(nqubits)
+ for _ in range(nlayers):
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+ c.add(gates.CZ(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+ c.add(gates.CZ(q, q + 1) for q in range(1, nqubits - 2, 2))
+ c.add(gates.CZ(0, nqubits - 1))
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+
+ # Optimize starting from a random guess for the variational parameters
+ np.random.seed(0)
+ x0 = np.random.uniform(0, 2 * np.pi, 2 * nqubits * nlayers + nqubits)
+ data = np.random.normal(0, 1, size=2**nqubits)
+
+ # perform optimization
+ best, params, _ = optimize(
+ myloss, x0, args=(c, data), method=method, options=options, compile=compile
+ )
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename)
+
+
+test_names = "method,options,compile,filename"
+test_values = [
+ ("Powell", {"maxiter": 1}, True, "vqe_powell.out"),
+ ("BFGS", {"maxiter": 1}, True, "vqe_bfgs.out"),
+ ("BFGS", {"maxiter": 1}, False, "vqe_bfgs.out"),
+ ("parallel_L-BFGS-B", {"maxiter": 1}, True, None),
+ ("parallel_L-BFGS-B", {"maxiter": 1}, False, None),
+ ("cma", {"maxiter": 1}, False, None),
+ ("sgd", {"nepochs": 5}, False, None),
+ ("sgd", {"nepochs": 5}, True, None),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_vqe(backend, method, options, compile, filename):
+ """Performs a VQE circuit minimization test."""
+ if (method == "sgd" or compile) and (backend.name not in ("tensorflow", "pytorch")):
+ pytest.skip("Skipping SGD test for unsupported backend.")
+ if method != "sgd" and backend.name in ("tensorflow", "pytorch"):
+ pytest.skip("Skipping scipy optimizers for tensorflow and pytorch.")
+ n_threads = backend.nthreads
+ backend.set_threads(1)
+ nqubits = 3
+ layers = 4
+ circuit = models.Circuit(nqubits)
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+ for q in range(0, nqubits - 1, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+ for q in range(1, nqubits - 2, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ circuit.add(gates.CZ(0, nqubits - 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+ hamiltonian = hamiltonians.XXZ(nqubits=nqubits, backend=backend)
+ np.random.seed(0)
+ initial_parameters = np.random.uniform(0, 2 * np.pi, 2 * nqubits * layers + nqubits)
+ initial_parameters = backend.cast(
+ initial_parameters, dtype=initial_parameters.dtype
+ )
+ v = models.VQE(circuit, hamiltonian)
+
+ loss_values = []
+
+ def callback(parameters, loss_values=loss_values, vqe=v):
+ vqe.circuit.set_parameters(parameters)
+ loss_values.append(vqe.hamiltonian.expectation(vqe.circuit().state()))
+
+ best, params, _ = v.minimize(
+ initial_parameters,
+ method=method,
+ options=options,
+ compile=compile,
+ callback=callback,
+ )
+ if method == "cma":
+ # remove `outcmaes` folder
+ import shutil
+
+ shutil.rmtree("outcmaes")
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename)
+ assert best == min(loss_values)
+
+ # test energy fluctuation
+ state = backend.np.ones(2**nqubits) / np.sqrt(2**nqubits)
+ energy_fluctuation = v.energy_fluctuation(state)
+ assert energy_fluctuation >= 0
+ backend.set_threads(n_threads)
+
+
+@pytest.mark.parametrize(
+ "solver,dense",
+ [
+ ("exp", False),
+ ("exp", True),
+ ("rk4", False),
+ ("rk4", True),
+ ("rk45", False),
+ ("rk45", True),
+ ],
+)
+def test_qaoa_execution(backend, solver, dense, accel=None):
+ h = hamiltonians.TFIM(6, h=1.0, dense=dense, backend=backend)
+ m = hamiltonians.X(6, dense=dense, backend=backend)
+ # Trotter and RK require small p's!
+ params = 0.01 * (1 - 2 * np.random.random(4))
+ state = random_statevector(2**6, backend=backend)
+ # set absolute test tolerance according to solver
+ if "rk" in solver:
+ atol = 1e-2
+ elif not dense:
+ atol = 1e-5
+ else:
+ atol = 0
+
+ target_state = backend.cast(state, copy=True)
+ h_matrix = backend.to_numpy(h.matrix)
+ m_matrix = backend.to_numpy(m.matrix)
+ for i, p in enumerate(params):
+ if i % 2:
+ u = expm(-1j * p * m_matrix)
+ else:
+ u = expm(-1j * p * h_matrix)
+ target_state = backend.cast(u) @ target_state
+
+ qaoa = models.QAOA(h, mixer=m, solver=solver, accelerators=accel)
+ qaoa.set_parameters(params)
+ final_state = qaoa(backend.cast(state, copy=True))
+ backend.assert_allclose(final_state, target_state, atol=atol)
+
+
+def test_qaoa_distributed_execution(backend, accelerators):
+ test_qaoa_execution(backend, "exp", False, accelerators)
+
+
+def test_qaoa_callbacks(backend, accelerators):
+ from qibo import callbacks
+
+ # use ``Y`` Hamiltonian so that there are no errors
+ # in the Trotter decomposition
+ h = hamiltonians.Y(5, backend=backend)
+ energy = callbacks.Energy(h)
+ params = 0.1 * np.random.random(4)
+ state = random_statevector(2**5, backend=backend)
+
+ ham = hamiltonians.Y(5, dense=False, backend=backend)
+ qaoa = models.QAOA(ham, callbacks=[energy], accelerators=accelerators)
+ qaoa.set_parameters(params)
+ final_state = qaoa(backend.cast(state, copy=True))
+
+ h_matrix = backend.to_numpy(h.matrix)
+ m_matrix = backend.to_numpy(qaoa.mixer.matrix)
+ calc_energy = lambda s: (s.conj() * h_matrix.dot(s)).sum()
+ target_state = backend.to_numpy(state)
+ target_energy = [calc_energy(target_state)]
+ for i, p in enumerate(params):
+ if i % 2:
+ u = expm(-1j * p * m_matrix)
+ else:
+ u = expm(-1j * p * h_matrix)
+ target_state = u @ target_state
+ target_energy.append(calc_energy(target_state))
+ final_energies = np.array([backend.to_numpy(x) for x in energy])
+ backend.assert_allclose(final_energies, target_energy)
+
+
+def test_qaoa_errors(backend):
+ # Invalid Hamiltonian type
+ with pytest.raises(TypeError):
+ qaoa = models.QAOA("test")
+ # Hamiltonians of different type
+ h = hamiltonians.TFIM(4, h=1.0, dense=False, backend=backend)
+ m = hamiltonians.X(4, dense=True, backend=backend)
+ with pytest.raises(TypeError):
+ qaoa = models.QAOA(h, mixer=m)
+ # Hamiltonians acting on different qubit numbers
+ h = hamiltonians.TFIM(6, h=1.0, backend=backend)
+ m = hamiltonians.X(4, backend=backend)
+ with pytest.raises(ValueError):
+ qaoa = models.QAOA(h, mixer=m)
+ # distributed execution with RK solver
+ with pytest.raises(NotImplementedError):
+ qaoa = models.QAOA(h, solver="rk4", accelerators={"/GPU:0": 2})
+ # minimize with odd number of parameters
+ qaoa = models.QAOA(h)
+ with pytest.raises(ValueError):
+ qaoa.minimize(np.random.random(5))
+
+
+test_names = "method,options,dense,filename"
+test_values = [
+ ("BFGS", {"maxiter": 1}, True, "qaoa_bfgs.out"),
+ ("BFGS", {"maxiter": 1}, False, "trotter_qaoa_bfgs.out"),
+ ("Powell", {"maxiter": 1}, False, "trotter_qaoa_powell.out"),
+ ("sgd", {"nepochs": 5}, True, None),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_qaoa_optimization(backend, method, options, dense, filename):
+ if (method == "sgd") and (backend.name not in ["tensorflow", "pytorch"]):
+ pytest.skip("Skipping SGD test for unsupported backend.")
+ if method != "sgd" and backend.name in ("tensorflow", "pytorch"):
+ pytest.skip("Skipping scipy optimizers for tensorflow and pytorch.")
+ h = hamiltonians.XXZ(3, dense=dense, backend=backend)
+ qaoa = models.QAOA(h)
+ initial_p = [0.05, 0.06, 0.07, 0.08]
+ initial_p = backend.cast(initial_p, dtype=np.float64)
+ best, params, _ = qaoa.minimize(initial_p, method=method, options=options)
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename)
+
+
+test_names = "delta_t,max_layers,tolerance,filename"
+test_values = [
+ (0.1, 5, None, "falqon1.out"),
+ (0.01, 2, None, "falqon2.out"),
+ (0.01, 2, 1e-5, "falqon3.out"),
+ (0.01, 5, 1, "falqon4.out"),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_falqon_optimization(backend, delta_t, max_layers, tolerance, filename):
+ h = hamiltonians.XXZ(3, backend=backend)
+ falqon = models.FALQON(h)
+ best, params, extra = falqon.minimize(delta_t, max_layers, tol=tolerance)
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename)
+
+
+def test_falqon_optimization_callback(backend):
+ class TestCallback:
+ def __call__(self, x):
+ return np.sum(x)
+
+ callback = TestCallback()
+ h = hamiltonians.XXZ(3, backend=backend)
+ falqon = models.FALQON(h)
+ best, params, extra = falqon.minimize(0.1, 5, callback=callback)
+ assert len(extra["callbacks"]) == 5
+
+
+test_names = "method,options,compile,filename"
+test_values = [
+ ("BFGS", {"maxiter": 1}, False, "aavqe_bfgs.out"),
+ ("cma", {"maxiter": 1}, False, None),
+ ("parallel_L-BFGS-B", {"maxiter": 1}, False, None),
+]
+
+
+@pytest.mark.parametrize(test_names, test_values)
+def test_aavqe(backend, method, options, compile, filename):
+ """Performs a AAVQE circuit minimization test."""
+
+ nqubits = 4
+ layers = 1
+ circuit = models.Circuit(nqubits)
+
+ for l in range(layers):
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+ for q in range(0, nqubits - 1, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+ for q in range(1, nqubits - 2, 2):
+ circuit.add(gates.CZ(q, q + 1))
+ circuit.add(gates.CZ(0, nqubits - 1))
+ for q in range(nqubits):
+ circuit.add(gates.RY(q, theta=1.0))
+
+ easy_hamiltonian = hamiltonians.X(nqubits, backend=backend)
+ problem_hamiltonian = hamiltonians.XXZ(nqubits, backend=backend)
+ s = lambda t: t
+ aavqe = models.AAVQE(
+ circuit, easy_hamiltonian, problem_hamiltonian, s, nsteps=10, t_max=1
+ )
+ np.random.seed(0)
+ initial_parameters = np.random.uniform(0, 2 * np.pi, 2 * nqubits * layers + nqubits)
+ best, params = aavqe.minimize(
+ params=initial_parameters, method=method, options=options, compile=compile
+ )
+ if method == "cma":
+ # remove `outcmaes` folder
+ import shutil
+
+ shutil.rmtree("outcmaes")
+ if filename is not None:
+ assert_regression_fixture(backend, params, filename, rtol=1e-2)
+
+
+@pytest.mark.parametrize(
+ "test_input, test_param, expected",
+ [(cvar, {"alpha": 0.1}, -0.5), (gibbs, {"eta": 0.1}, -2.08)],
+)
+def test_custom_loss(test_input, test_param, expected):
+ from qibo import hamiltonians
+
+ h = hamiltonians.XXZ(3)
+ qaoa = models.QAOA(h)
+ initial_p = [0.314, 0.22, 0.05, 0.59]
+ best, params, _ = qaoa.minimize(
+ initial_p, loss_func=test_input, loss_func_param=test_param
+ )
+ assert abs(best - expected) <= 0.01
diff --git a/tests/test_noise.py b/tests/test_noise.py
new file mode 100644
index 000000000..325ec95e2
--- /dev/null
+++ b/tests/test_noise.py
@@ -0,0 +1,769 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.noise import (
+ AmplitudeDampingError,
+ CustomError,
+ DepolarizingError,
+ IBMQNoiseModel,
+ KrausError,
+ NoiseModel,
+ PauliError,
+ PhaseDampingError,
+ ReadoutError,
+ ResetError,
+ ThermalRelaxationError,
+ UnitaryError,
+)
+from qibo.quantum_info import (
+ random_clifford,
+ random_density_matrix,
+ random_statevector,
+ random_stochastic_matrix,
+)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+@pytest.mark.parametrize("nshots", [None, 10, 100])
+def test_kraus_error(backend, density_matrix, nshots):
+ k1 = np.sqrt(0.4) * np.array([[1, 0], [0, 1]])
+ k2 = np.sqrt(0.6) * np.array([[1, 0], [0, 1]])
+
+ with pytest.raises(ValueError):
+ KrausError([k1, np.array([1])])
+
+ kraus_error = KrausError([k1, k2])
+
+ noise = NoiseModel()
+ noise.add(kraus_error, gates.X, 1)
+ noise.add(kraus_error, gates.CNOT)
+ noise.add(kraus_error, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.KrausChannel(0, [k1, k2]))
+ target_circuit.add(gates.KrausChannel(1, [k1, k2]))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.KrausChannel(1, [k1, k2]))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.KrausChannel(1, [k1, k2]))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("nshots", [10, 100])
+def test_unitary_error(backend, density_matrix, nshots):
+ u1 = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ u2 = np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
+ qubits = (0, 1)
+ p1, p2 = (0.3, 0.7)
+
+ with pytest.raises(ValueError):
+ UnitaryError([p1, p2], [u1, np.array([[1, 0], [0, 1]])])
+
+ unitary_error = UnitaryError([p1, p2], [u1, u2])
+ unitary_error_1q = UnitaryError([0.1], [np.eye(2)])
+
+ noise = NoiseModel()
+ noise.add(unitary_error_1q, qubits=[0])
+ noise.add(unitary_error, gates.CNOT)
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.UnitaryChannel(qubits, [(p1, u1), (p2, u2)]))
+ target_circuit.add(gates.UnitaryChannel(0, [(0.1, np.eye(2))]))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("nshots", [10, 100])
+def test_pauli_error(backend, density_matrix, nshots):
+ list_paulis = ["X", "Y", "Z"]
+ probabilities = np.array([0, 0.2, 0.3])
+ zipped = list(zip(list_paulis, probabilities))
+
+ pauli = PauliError(zipped)
+ noise = NoiseModel()
+ noise.add(pauli, gates.X, 1)
+ noise.add(pauli, gates.CNOT)
+ noise.add(pauli, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.PauliNoiseChannel(0, zipped))
+ target_circuit.add(gates.PauliNoiseChannel(1, zipped))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.PauliNoiseChannel(1, zipped))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.PauliNoiseChannel(1, zipped))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("nshots", [10, 100])
+def test_depolarizing_error(backend, density_matrix, nshots):
+ depol = DepolarizingError(0.3)
+ noise = NoiseModel()
+ noise.add(depol, gates.X, 1)
+ noise.add(depol, gates.CNOT)
+ noise.add(depol, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.DepolarizingChannel((0, 1), 0.3))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.DepolarizingChannel((1,), 0.3))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.DepolarizingChannel((1,), 0.3))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit),
+ initial_state=backend.cast(initial_psi, copy=True, dtype=initial_psi.dtype),
+ nshots=nshots,
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_thermal_error(backend, density_matrix):
+ if not density_matrix:
+ pytest.skip("Thermal Relaxation error is not implemented for state vectors.")
+ thermal = ThermalRelaxationError(2, 1, 0.3)
+ noise = NoiseModel()
+ noise.add(thermal, gates.X, 1)
+ noise.add(thermal, gates.CNOT)
+ noise.add(thermal, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.ThermalRelaxationChannel(0, [2, 1, 0.3]))
+ target_circuit.add(gates.ThermalRelaxationChannel(1, [2, 1, 0.3]))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.ThermalRelaxationChannel(1, [2, 1, 0.3]))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.ThermalRelaxationChannel(1, [2, 1, 0.3]))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), backend.np.copy(initial_psi)
+ )._state
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, backend.np.copy(initial_psi)
+ )._state
+
+ backend.assert_allclose(final_state, target_final_state)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+@pytest.mark.parametrize("nshots", [None, 10, 100])
+def test_amplitude_damping_error(backend, density_matrix, nshots):
+ damping = AmplitudeDampingError(0.3)
+ noise = NoiseModel()
+ noise.add(damping, gates.X, 1)
+ noise.add(damping, gates.CNOT)
+ noise.add(damping, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.AmplitudeDampingChannel(0, 0.3))
+ target_circuit.add(gates.AmplitudeDampingChannel(1, 0.3))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.AmplitudeDampingChannel(1, 0.3))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.AmplitudeDampingChannel(1, 0.3))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit),
+ initial_state=backend.cast(initial_psi, copy=True, dtype=initial_psi.dtype),
+ nshots=nshots,
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+@pytest.mark.parametrize("nshots", [None, 10, 100])
+def test_phase_damping_error(backend, density_matrix, nshots):
+ damping = PhaseDampingError(0.3)
+ noise = NoiseModel()
+ noise.add(damping, gates.X, 1)
+ noise.add(damping, gates.CNOT)
+ noise.add(damping, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.PhaseDampingChannel(0, 0.3))
+ target_circuit.add(gates.PhaseDampingChannel(1, 0.3))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.PhaseDampingChannel(1, 0.3))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.PhaseDampingChannel(1, 0.3))
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit),
+ initial_state=backend.cast(initial_psi, copy=True, dtype=initial_psi.dtype),
+ nshots=nshots,
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_readout_error(backend, density_matrix):
+ if not density_matrix:
+ pytest.skip("Readout error is not implemented for state vectors.")
+
+ nqubits = 1
+ d = 2**nqubits
+
+ state = random_density_matrix(d, seed=1, backend=backend)
+ P = random_stochastic_matrix(d, seed=1, backend=backend)
+
+ readout = ReadoutError(P)
+ noise = NoiseModel()
+ noise.add(readout, gates.M, qubits=0)
+
+ circuit = Circuit(nqubits, density_matrix=density_matrix)
+ circuit.add(gates.M(0))
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(state)
+ )
+
+ target_state = gates.ReadoutErrorChannel(0, P).apply_density_matrix(
+ backend, backend.np.copy(state), nqubits
+ )
+
+ backend.assert_allclose(final_state, target_state)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_reset_error(backend, density_matrix):
+ if not density_matrix:
+ pytest.skip("Reset error is not implemented for state vectors.")
+ reset = ResetError(0.8, 0.2)
+ noise = NoiseModel()
+ noise.add(reset, gates.X, 1)
+ noise.add(reset, gates.CNOT)
+ noise.add(reset, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.ResetChannel(0, [0.8, 0.2]))
+ target_circuit.add(gates.ResetChannel(1, [0.8, 0.2]))
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(gates.ResetChannel(1, [0.8, 0.2]))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), backend.np.copy(initial_psi)
+ )._state
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, backend.np.copy(initial_psi)
+ )._state
+
+ backend.assert_allclose(final_state, target_final_state)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+@pytest.mark.parametrize("nshots", [None, 10, 100])
+def test_custom_error(backend, density_matrix, nshots):
+ a1 = np.sqrt(0.4) * np.array([[0, 1], [1, 0]])
+ a2 = np.sqrt(0.6) * np.array(
+ [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]
+ )
+ error_channel = gates.KrausChannel([(1,), (0, 2)], [a1, a2])
+ custom_error = CustomError(error_channel)
+
+ noise = NoiseModel()
+ noise.add(custom_error, gates.X, 1)
+ noise.add(custom_error, gates.CNOT)
+ noise.add(custom_error, gates.Z, (0, 1))
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.X(2))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(error_channel)
+ target_circuit.add(gates.Z(1))
+ target_circuit.add(error_channel)
+ target_circuit.add(gates.X(1))
+ target_circuit.add(error_channel)
+ target_circuit.add(gates.X(2))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ final_state_samples = final_state.samples() if nshots else None
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi), nshots=nshots
+ )
+ target_final_state_samples = target_final_state.samples() if nshots else None
+
+ if nshots is None:
+ backend.assert_allclose(final_state._state, target_final_state._state)
+ else:
+ backend.assert_allclose(final_state_samples, target_final_state_samples)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+def test_add_condition(backend, density_matrix):
+ def condition_pi_2(gate):
+ return np.pi / 2 in gate.parameters
+
+ def condition_3_pi_2(gate):
+ return 3 * np.pi / 2 in gate.parameters
+
+ reset = ResetError(0.8, 0.2)
+ thermal = ThermalRelaxationError(2, 1, 0.3)
+ noise = NoiseModel()
+ noise.add(reset, gates.RX, conditions=condition_pi_2)
+ noise.add(thermal, gates.RX, conditions=condition_3_pi_2)
+
+ with pytest.raises(TypeError):
+ noise.add(reset, gates.RX, conditions=2)
+ with pytest.raises(TypeError):
+ noise.add(reset, gates.RX, conditions=[condition_pi_2, 2])
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.RX(0, np.pi))
+ circuit.add(gates.RX(0, np.pi / 2))
+ circuit.add(gates.RX(0, np.pi / 3))
+ circuit.add(gates.RX(1, np.pi))
+ circuit.add(gates.RX(1, 3 * np.pi / 2))
+ circuit.add(gates.RX(1, 2 * np.pi / 3))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.RX(0, np.pi))
+ target_circuit.add(gates.RX(0, np.pi / 2))
+ target_circuit.add(gates.ResetChannel(0, [0.8, 0.2]))
+ target_circuit.add(gates.RX(0, np.pi / 3))
+ target_circuit.add(gates.RX(1, np.pi))
+ target_circuit.add(gates.RX(1, 3 * np.pi / 2))
+ target_circuit.add(gates.ThermalRelaxationChannel(1, [2, 1, 0.3]))
+ target_circuit.add(gates.RX(1, 2 * np.pi / 3))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), backend.np.copy(initial_psi)
+ )._state
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, backend.np.copy(initial_psi)
+ )._state
+
+ backend.assert_allclose(final_state, target_final_state)
+
+
+@pytest.mark.parametrize("density_matrix", [True])
+def test_gate_independent_noise(backend, density_matrix):
+ pauli = PauliError(list(zip(["Y", "Z"], [0.2, 0.3])))
+ depol = DepolarizingError(0.3)
+ noise = NoiseModel()
+ noise.add(pauli)
+ noise.add(depol, qubits=0)
+
+ circuit = Circuit(3, density_matrix=density_matrix)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.X(1))
+ circuit.add(gates.Z(2))
+ circuit.add(gates.M(0, 1, 2))
+
+ target_circuit = Circuit(3, density_matrix=density_matrix)
+ target_circuit.add(gates.CNOT(0, 1))
+ target_circuit.add(gates.PauliNoiseChannel(0, list(zip(["Y", "Z"], [0.2, 0.3]))))
+ target_circuit.add(gates.PauliNoiseChannel(1, list(zip(["Y", "Z"], [0.2, 0.3]))))
+ target_circuit.add(gates.DepolarizingChannel((0,), 0.3))
+ target_circuit.add(gates.X(1))
+ target_circuit.add(gates.PauliNoiseChannel(1, list(zip(["Y", "Z"], [0.2, 0.3]))))
+ target_circuit.add(gates.Z(2))
+ target_circuit.add(gates.PauliNoiseChannel(2, list(zip(["Y", "Z"], [0.2, 0.3]))))
+ target_circuit.add(gates.M(0, 1, 2))
+
+ initial_psi = (
+ random_density_matrix(2**3, backend=backend)
+ if density_matrix
+ else random_statevector(2**3, backend=backend)
+ )
+ backend.set_seed(123)
+ final_state = backend.execute_circuit(
+ noise.apply(circuit), initial_state=backend.np.copy(initial_psi)
+ )._state
+ backend.set_seed(123)
+ target_final_state = backend.execute_circuit(
+ target_circuit, initial_state=backend.np.copy(initial_psi)
+ )._state
+
+ backend.assert_allclose(final_state, target_final_state)
+
+
+class _Conditions:
+ def __init__(self, qubits=None):
+ self.qubits = qubits
+
+ def condition_single(self, gate):
+ return len(gate.qubits) == 1
+
+ def condition_two(self, gate):
+ return len(gate.qubits) == 2
+
+ def condition_qubits(self, gate):
+ return gate.qubits == self.qubits
+
+
+@pytest.mark.parametrize(
+ "readout_one_qubit", [0.01, {"0": 0.01, "1": [0.001], "4": (0.02, 0.03)}]
+)
+@pytest.mark.parametrize("excited_population", [0.0, 0.1])
+@pytest.mark.parametrize("gate_times", [(0.1, 0.2)])
+@pytest.mark.parametrize(
+ "t1, t2", [(0.1, 0.01), ({"1": 0.1, "2": 0.05}, {"1": 0.01, "2": 0.001})]
+)
+@pytest.mark.parametrize(
+ "depolarizing_two_qubit", [0.01, {"0-1": 0.01, "1-3": 0.02, "4-5": 0.05}]
+)
+@pytest.mark.parametrize(
+ "depolarizing_one_qubit", [0.01, {"0": 0.01, "1": 0.02, "4": 0.05}]
+)
+@pytest.mark.parametrize("nqubits", [5])
+def test_ibmq_noise(
+ backend,
+ nqubits,
+ depolarizing_one_qubit,
+ depolarizing_two_qubit,
+ t1,
+ t2,
+ gate_times,
+ excited_population,
+ readout_one_qubit,
+):
+ ## Since the IBMQNoiseModel inherits the NoiseModel class,
+ ## we will test only what is different
+
+ circuit = random_clifford(nqubits, density_matrix=True, backend=backend)
+ circuit.add(gates.M(qubit) for qubit in range(nqubits))
+
+ parameters = {
+ "t1": t1,
+ "t2": t2,
+ "depolarizing_one_qubit": depolarizing_one_qubit,
+ "depolarizing_two_qubit": depolarizing_two_qubit,
+ "excited_population": excited_population,
+ "readout_one_qubit": readout_one_qubit,
+ "gate_times": gate_times,
+ }
+
+ noise_model = IBMQNoiseModel()
+ noise_model.from_dict(parameters)
+ noisy_circuit = noise_model.apply(circuit)
+
+ noise_model_target = NoiseModel()
+ if isinstance(depolarizing_one_qubit, float):
+ noise_model_target.add(
+ DepolarizingError(depolarizing_one_qubit),
+ conditions=_Conditions().condition_single,
+ )
+
+ if isinstance(depolarizing_one_qubit, dict):
+ for qubit_key, lamb in depolarizing_one_qubit.items():
+ noise_model_target.add(
+ DepolarizingError(lamb),
+ qubits=int(qubit_key),
+ conditions=_Conditions().condition_single,
+ )
+
+ if isinstance(depolarizing_two_qubit, (float, int)):
+ noise_model_target.add(
+ DepolarizingError(depolarizing_two_qubit),
+ conditions=_Conditions().condition_two,
+ )
+
+ if isinstance(depolarizing_two_qubit, dict):
+ for key, lamb in depolarizing_two_qubit.items():
+ qubits = key.replace(" ", "").split("-")
+ qubits = tuple(map(int, qubits))
+ noise_model_target.add(
+ DepolarizingError(lamb),
+ qubits=qubits,
+ conditions=[
+ _Conditions().condition_two,
+ _Conditions(qubits).condition_qubits,
+ ],
+ )
+
+ if isinstance(t1, float):
+ noise_model_target.add(
+ ThermalRelaxationError(t1, t2, gate_times[0], excited_population),
+ conditions=_Conditions().condition_single,
+ )
+ noise_model_target.add(
+ ThermalRelaxationError(t1, t2, gate_times[1], excited_population),
+ conditions=_Conditions().condition_two,
+ )
+ else:
+ for qubit in t1.keys():
+ noise_model_target.add(
+ ThermalRelaxationError(
+ t1[qubit], t2[qubit], gate_times[0], excited_population
+ ),
+ qubits=int(qubit),
+ conditions=_Conditions().condition_single,
+ )
+ noise_model_target.add(
+ ThermalRelaxationError(
+ t1[qubit], t2[qubit], gate_times[1], excited_population
+ ),
+ qubits=int(qubit),
+ conditions=_Conditions().condition_two,
+ )
+
+ if isinstance(readout_one_qubit, float):
+ probabilities = [
+ [1 - readout_one_qubit, readout_one_qubit],
+ [readout_one_qubit, 1 - readout_one_qubit],
+ ]
+ noise_model_target.add(ReadoutError(probabilities), gate=gates.M)
+ else:
+ for qubit, probs in readout_one_qubit.items():
+ if isinstance(probs, (int, float)):
+ probs = (probs, probs)
+ elif isinstance(probs, (tuple, list)) and len(probs) == 1:
+ probs *= 2
+
+ probabilities = [[1 - probs[0], probs[0]], [probs[1], 1 - probs[1]]]
+ noise_model_target.add(
+ ReadoutError(probabilities),
+ gate=gates.M,
+ qubits=int(qubit),
+ )
+
+ noisy_circuit_target = noise_model_target.apply(circuit)
+
+ assert noisy_circuit.draw() == noisy_circuit_target.draw()
+
+ backend.set_seed(2024)
+ state = backend.execute_circuit(noisy_circuit, nshots=10)
+
+ backend.set_seed(2024)
+ state_target = backend.execute_circuit(noisy_circuit_target, nshots=10)
+
+ backend.assert_allclose(state, state_target)
diff --git a/tests/test_parallel.py b/tests/test_parallel.py
new file mode 100644
index 000000000..40f22c7cd
--- /dev/null
+++ b/tests/test_parallel.py
@@ -0,0 +1,115 @@
+"""
+Testing parallel evaluations.
+"""
+
+import sys
+
+import numpy as np
+import pytest
+
+import qibo
+from qibo import gates
+from qibo.models import QFT, Circuit
+from qibo.parallel import (
+ parallel_circuits_execution,
+ parallel_execution,
+ parallel_parametrized_execution,
+)
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="Mac tests")
+def test_parallel_states_evaluation(backend):
+ """Evaluate circuit for multiple input states."""
+ nqubits = 10
+ np.random.seed(0)
+ c = QFT(nqubits)
+
+ states = [np.random.random(2**nqubits) for i in range(5)]
+
+ r1 = []
+ for state in states:
+ r1.append(backend.execute_circuit(c, state))
+
+ r2 = parallel_execution(c, states=states, processes=2, backend=backend)
+ r1 = [x.state() for x in r1]
+ r2 = [x.state() for x in r2]
+ backend.assert_allclose(r1, r2)
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="Mac tests")
+@pytest.mark.parametrize("use_execute_circuits", [False, True])
+def test_parallel_circuit_evaluation(backend, use_execute_circuits):
+ """Evaluate multiple circuits in parallel."""
+ circuits = [QFT(n) for n in range(1, 11)]
+
+ r1 = []
+ for circuit in circuits:
+ r1.append(backend.execute_circuit(circuit))
+
+ if use_execute_circuits:
+ r2 = backend.execute_circuits(circuits, processes=2)
+ else:
+ r2 = parallel_circuits_execution(circuits, processes=2, backend=backend)
+
+ for x, y in zip(r1, r2):
+ target = x.state(numpy=True)
+ final = y.state(numpy=True)
+ backend.assert_allclose(final, target)
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="Mac tests")
+def test_parallel_circuit_states_evaluation(backend):
+ """Evaluate multiple circuits in parallel with different initial states."""
+ circuits = [QFT(n) for n in range(1, 11)]
+ states = [np.random.random(2**n) for n in range(1, 11)]
+
+ with pytest.raises(TypeError):
+ r2 = parallel_circuits_execution(
+ circuits, states=1, processes=2, backend=backend
+ )
+ with pytest.raises(ValueError):
+ r2 = parallel_circuits_execution(
+ circuits, states[:3], processes=2, backend=backend
+ )
+
+ r1 = []
+ for circuit, state in zip(circuits, states):
+ r1.append(backend.execute_circuit(circuit, state))
+
+ r2 = parallel_circuits_execution(circuits, states, processes=2, backend=backend)
+ for x, y in zip(r1, r2):
+ target = x.state(numpy=True)
+ final = y.state(numpy=True)
+ backend.assert_allclose(final, target)
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="Mac tests")
+def test_parallel_parametrized_circuit(backend):
+ """Evaluate circuit for multiple parameters."""
+ nqubits = 5
+ nlayers = 10
+ c = Circuit(nqubits)
+ for l in range(nlayers):
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+ c.add(gates.CZ(q, q + 1) for q in range(0, nqubits - 1, 2))
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+ c.add(gates.CZ(q, q + 1) for q in range(1, nqubits - 2, 2))
+ c.add(gates.CZ(0, nqubits - 1))
+ c.add(gates.RY(q, theta=0) for q in range(nqubits))
+
+ size = len(c.get_parameters())
+ np.random.seed(0)
+ parameters = [np.random.uniform(0, 2 * np.pi, size) for i in range(10)]
+ state = np.random.random(2**nqubits)
+
+ r1 = []
+ for params in parameters:
+ c.set_parameters(params)
+ r1.append(backend.execute_circuit(c, backend.cast(state)))
+
+ r2 = parallel_parametrized_execution(
+ c, parameters=parameters, initial_state=state, processes=2, backend=backend
+ )
+ r1 = [x.state() for x in r1]
+ r2 = [x.state() for x in r2]
+ backend.assert_allclose(r1, r2)
diff --git a/tests/test_parameter.py b/tests/test_parameter.py
new file mode 100644
index 000000000..4c934699c
--- /dev/null
+++ b/tests/test_parameter.py
@@ -0,0 +1,126 @@
+import numpy as np
+import pytest
+
+from qibo.parameter import Parameter
+
+
+def test_parameter():
+ # single feature
+ param = Parameter(
+ lambda x, th1, th2, th3: x**2 * th1 + th2 * th3**2,
+ features=[7.0],
+ trainable=[1.5, 2.0, 3.0],
+ )
+
+ indices = param.trainable_parameter_indices(10)
+ assert indices == [10, 11, 12]
+
+ fixed = param.unaffected_by(1)
+ assert fixed == 73.5
+
+ factor = param.partial_derivative(3)
+ assert factor == 12.0
+
+ param.trainable = [15.0, 10.0, 7.0]
+ param.features = [5.0]
+ gate_value = param()
+ assert gate_value == 865
+
+ # single feature, no list
+ param2 = Parameter(
+ lambda x, th1, th2, th3: x**2 * th1 + th2 * th3**2,
+ features=[7.0],
+ trainable=[1.5, 2.0, 3.0],
+ )
+
+ gate_value2 = param2()
+ assert gate_value2 == 91.5
+
+ # multiple features
+ param = Parameter(
+ lambda x1, x2, th1, th2, th3: x1**2 * th1 + x2 * th2 * th3,
+ features=[7.0, 4.0],
+ trainable=[1.5, 2.0, 3.0],
+ )
+
+ fixed = param.unaffected_by(1)
+ assert fixed == 73.5
+ assert param.nparams == 3
+ assert param.nfeat == 2
+
+ factor = param.partial_derivative(4)
+ assert factor == 8.0
+
+ param.trainable = np.array([15.0, 10.0, 7.0])
+ param.features = [5.0, 3.0]
+ gate_value = param()
+ assert gate_value == 585
+
+ # testing call with new values
+ executed = param(features=[0.5, 2.0], trainable=[2.0, 0.1, 4.0])
+ assert executed == 1.3
+
+ # injecting only trainable
+ param = Parameter(lambda x: x, trainable=[0.8])
+ nparams = param.nparams
+ nfeat = param.nfeat
+ ncomponents = param.ncomponents
+ assert nparams == 1
+ assert nfeat == 0
+ assert ncomponents == 1
+
+ # injecting only features
+ param = Parameter(lambda x: x, features=[0.8])
+ nparams = param.nparams
+ nfeat = param.nfeat
+ assert nparams == 0
+ assert nfeat == 1
+
+
+def test_parameter_errors():
+ param = Parameter(
+ lambda x, th1, th2, th3: x**2 * th1 + th2 * th3**2,
+ features=[7.0],
+ trainable=[1.5, 2.0, 3.0],
+ )
+
+ param.trainable = [1, 1, 1]
+ param.features = 1
+
+ try:
+ param()
+ assert False
+ except Exception as e:
+ assert True
+
+ param.trainable = [1, 1]
+ param.features = [1]
+ try:
+ param()
+ assert False
+ except Exception as e:
+ assert True
+
+ param.trainable = [1, 1, 1]
+ param.features = [1, 1]
+ try:
+ param()
+ assert False
+ except Exception as e:
+ assert True
+
+ # test type error due to wrong initialization
+ with pytest.raises(TypeError):
+ param = Parameter(func=lambda x, y: x + y**2)
+
+ # test call function with wrong features and trainable dimensionality
+ param = Parameter(
+ func=lambda x, th1, th2: th1 * x + th2, features=[1.2], trainable=[0.2, 9.1]
+ )
+
+ # wrong features length
+ with pytest.raises(TypeError):
+ param(features=[2.3, 9.2], trainable=[0.4, 9.3])
+ # wrong trainable length
+ with pytest.raises(TypeError):
+ param(features=[0.4], trainable=[3.4, 0.1, 5.6])
diff --git a/tests/test_prints.py b/tests/test_prints.py
new file mode 100644
index 000000000..fcf94ddf3
--- /dev/null
+++ b/tests/test_prints.py
@@ -0,0 +1,111 @@
+"""Test that the source contains no prints."""
+
+import os
+import pathlib
+
+import pytest
+
+import qibo
+from qibo.config import raise_error
+
+
+class CodeText:
+ """Helper class to iterate through code text skipping the docstrings."""
+
+ def __init__(self, code, filedir=None):
+ self.code = code
+ self.filedir = filedir
+ self.line_counter = 0
+ self.piece_counter = 0
+ self.pieces = None
+
+ @classmethod
+ def from_file(cls, filedir):
+ assert filedir[-3:] == ".py"
+ with open(filedir, encoding="utf-8") as file:
+ code = file.read()
+ return cls(code, filedir)
+
+ def __iter__(self):
+ self.line_counter = 0
+ self.piece_counter = -1
+ self.pieces = self.code.split('"""')
+ return self
+
+ def __next__(self):
+ """Returns pieces of code text skipping the docstrings."""
+ self.piece_counter += 1
+
+ if self.piece_counter > 0:
+ previous_piece = self.pieces[self.piece_counter - 1]
+ self.line_counter += len(previous_piece.split("\n")) - 1
+
+ if self.piece_counter < len(self.pieces):
+ # skip docstrings
+ if self.piece_counter % 2 == 0:
+ return self.pieces[self.piece_counter]
+ else:
+ return self.__next__()
+
+ raise StopIteration
+
+ def check_exists(self, target_word):
+ """Checks if a word exists in the code text.
+ Raises a ValueError if the word is found.
+ Ignores a specific occurence if `CodeText:skip` occurs on the same line.
+
+ Args:
+ target_word(str): word to be searched in the code text
+ """
+ for piece in self:
+ for offset, line in enumerate(piece.split("\n")):
+ if ("print" in line) and ("CodeText:skip" not in line):
+ line_num = self.line_counter + offset + 1
+ raise_error(
+ ValueError,
+ f"Found `{target_word}` in line {line_num} "
+ f"of {self.filedir}.",
+ )
+
+
+def python_files():
+ """Iterator that yields all python files (`.py`) in `/src/qibo/`."""
+ basedir = pathlib.Path(qibo.__file__).parent.absolute()
+ for subdir, _, files in os.walk(basedir):
+ for file in files:
+ pieces = file.split(".")
+ # skip non-`.py` files
+ # skip current file because it contains `print`
+ if len(pieces) == 2 and pieces[1] == "py" and pieces[0] != "test_prints":
+ yield os.path.join(subdir, file)
+
+
+# Make sure the CodeText class works as intended
+text_examples = [
+ ('print("Test")', True),
+ ('print("Test")\n""" docstring """', True),
+ ('""" docstring """\nprint("Test")\n""" docstring """', True),
+ ("pass", False),
+ ('pass\n""" docstring with print """', False),
+ ('""" docstring with print """\npass\n""" docstring with print """', False),
+ ("pass # CodeText:skip", False),
+ ('print("Test") # CodeText:skip', False),
+ ('print("Test")\nprint("Test) # CodeText:skip', True),
+ ('print("Test") # CodeText:skip\nprint("Test)', True),
+]
+
+
+@pytest.mark.parametrize(("text", "contains_print"), text_examples)
+def test_codetext_class(text, contains_print):
+ """Check if the CodeText class is working properly"""
+ if contains_print:
+ with pytest.raises(ValueError):
+ CodeText(text).check_exists("print")
+ else:
+ CodeText(text).check_exists("print")
+
+
+@pytest.mark.parametrize("filename", python_files())
+def test_qibo_code(filename):
+ text = CodeText.from_file(filename)
+ text.check_exists("print")
diff --git a/tests/test_quantum_info_basis.py b/tests/test_quantum_info_basis.py
new file mode 100644
index 000000000..6d3586350
--- /dev/null
+++ b/tests/test_quantum_info_basis.py
@@ -0,0 +1,102 @@
+"""Tests for quantum_info.basis module."""
+
+from functools import reduce
+from itertools import product
+
+import numpy as np
+import pytest
+
+from qibo import matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info import (
+ comp_basis_to_pauli,
+ pauli_basis,
+ pauli_to_comp_basis,
+ vectorization,
+)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column", "system"])
+@pytest.mark.parametrize("sparse", [False, True])
+@pytest.mark.parametrize("vectorize", [False, True])
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_pauli_basis(
+ backend, nqubits, normalize, vectorize, sparse, order, pauli_order
+):
+ with pytest.raises(ValueError):
+ pauli_basis(nqubits=-1, backend=backend)
+ with pytest.raises(TypeError):
+ pauli_basis(nqubits="1", backend=backend)
+ with pytest.raises(TypeError):
+ pauli_basis(nqubits=1, normalize="True", backend=backend)
+ with pytest.raises(TypeError):
+ pauli_basis(nqubits=1, normalize=False, vectorize="True", backend=backend)
+ with pytest.raises(TypeError):
+ pauli_basis(nqubits=1, normalize=False, sparse="True", backend=backend)
+ with pytest.raises(TypeError):
+ pauli_basis(nqubits=1, normalize=False, pauli_order=1, backend=backend)
+ with pytest.raises(ValueError):
+ pauli_basis(nqubits=1, normalize=False, pauli_order="IXY", backend=backend)
+ with pytest.raises(ValueError):
+ pauli_basis(
+ nqubits=1, normalize=False, vectorize=True, order=None, backend=backend
+ )
+ if pauli_order == "IXYZ":
+ basis_test = [matrices.I, matrices.X, matrices.Y, matrices.Z]
+ else:
+ basis_test = [matrices.I, matrices.Z, matrices.X, matrices.Y]
+ if nqubits >= 2:
+ basis_test = list(product(basis_test, repeat=nqubits))
+ basis_test = [reduce(np.kron, matrices) for matrices in basis_test]
+
+ if vectorize:
+ basis_test = [
+ vectorization(backend.cast(matrix), order=order, backend=backend)
+ for matrix in basis_test
+ ]
+
+ basis_test = backend.cast(basis_test)
+
+ if normalize:
+ basis_test = basis_test / np.sqrt(2**nqubits)
+
+ if vectorize and sparse:
+ elements, indexes = [], []
+ for row in basis_test:
+ row_indexes = list(np.flatnonzero(backend.to_numpy(row)))
+ indexes.append(row_indexes)
+ elements.append(row[row_indexes])
+ indexes = backend.cast(indexes)
+
+ if not vectorize and sparse:
+ with pytest.raises(NotImplementedError):
+ pauli_basis(nqubits=1, vectorize=False, sparse=True, order="row")
+ else:
+ basis = pauli_basis(
+ nqubits, normalize, vectorize, sparse, order, pauli_order, backend
+ )
+
+ if vectorize and sparse:
+ for elem_test, ind_test, elem, ind in zip(
+ elements, indexes, basis[0], basis[1]
+ ):
+ backend.assert_allclose(
+ np.linalg.norm(backend.to_numpy(elem_test - elem)) < PRECISION_TOL,
+ True,
+ )
+ backend.assert_allclose(
+ np.linalg.norm(backend.to_numpy(ind_test - ind)) < PRECISION_TOL,
+ True,
+ )
+ else:
+ for pauli, pauli_test in zip(basis, basis_test):
+ backend.assert_allclose(
+ np.linalg.norm(backend.to_numpy(pauli - pauli_test))
+ < PRECISION_TOL,
+ True,
+ )
+
+ comp_basis_to_pauli(nqubits, normalize, sparse, order, pauli_order, backend)
+ pauli_to_comp_basis(nqubits, normalize, sparse, order, pauli_order, backend)
diff --git a/tests/test_quantum_info_clifford.py b/tests/test_quantum_info_clifford.py
new file mode 100644
index 000000000..c9e8654df
--- /dev/null
+++ b/tests/test_quantum_info_clifford.py
@@ -0,0 +1,393 @@
+from collections import Counter
+from functools import reduce
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.backends import CliffordBackend, PyTorchBackend, TensorflowBackend
+from qibo.backends.clifford import _get_engine_name
+from qibo.quantum_info._clifford_utils import (
+ _cnot_cost,
+ _one_qubit_paulis_string_product,
+ _string_product,
+)
+from qibo.quantum_info.clifford import Clifford
+from qibo.quantum_info.random_ensembles import random_clifford
+
+
+def construct_clifford_backend(backend):
+ if (
+ isinstance(backend, (TensorflowBackend, PyTorchBackend))
+ or backend.__class__.__name__ == "CuQuantumBackend"
+ ):
+ with pytest.raises(NotImplementedError):
+ clifford_backend = CliffordBackend(backend.name)
+ pytest.skip("Clifford backend not defined for the this engine.")
+
+ return CliffordBackend(_get_engine_name(backend))
+
+
+@pytest.mark.parametrize("nqubits", [2, 10, 50, 100])
+def test_clifford_from_symplectic_matrix(backend, nqubits):
+ clifford_backend = construct_clifford_backend(backend)
+
+ symplectic_matrix = clifford_backend.zero_state(nqubits)
+ clifford_1 = Clifford(symplectic_matrix, engine=_get_engine_name(backend))
+ clifford_2 = Clifford(symplectic_matrix[:-1], engine=_get_engine_name(backend))
+
+ for clifford in [clifford_1, clifford_2]:
+ backend.assert_allclose(
+ clifford.symplectic_matrix.shape,
+ (2 * nqubits + 1, 2 * nqubits + 1),
+ )
+
+
+@pytest.mark.parametrize("measurement", [False, True])
+def test_clifford_from_circuit(backend, measurement):
+ clifford_backend = construct_clifford_backend(backend)
+ if not clifford_backend:
+ return
+
+ c = random_clifford(3, backend=backend)
+ if measurement:
+ c.add(gates.M(*np.random.choice(3, size=2, replace=False)))
+
+ result = clifford_backend.execute_circuit(c)
+ obj = Clifford.from_circuit(c, engine=_get_engine_name(backend))
+ backend.assert_allclose(obj.state(), result.state())
+ if measurement:
+ backend.assert_allclose(obj.probabilities(), result.probabilities())
+
+
+@pytest.mark.parametrize("seed", [1, 10])
+@pytest.mark.parametrize("algorithm", ["AG04", "BM20"])
+@pytest.mark.parametrize("nqubits", [1, 2, 3, 10, 50])
+def test_clifford_to_circuit(backend, nqubits, algorithm, seed):
+ clifford_backend = construct_clifford_backend(backend)
+
+ clifford = random_clifford(nqubits, seed=seed, backend=backend)
+
+ engine = _get_engine_name(backend)
+ symplectic_matrix_original = Clifford.from_circuit(
+ clifford, engine=engine
+ ).symplectic_matrix
+
+ symplectic_matrix_from_symplectic = Clifford(
+ symplectic_matrix_original, engine=engine
+ )
+
+ symplectic_matrix_compiled = Clifford.from_circuit(clifford, engine=engine)
+
+ if algorithm == "BM20" and nqubits > 3:
+ with pytest.raises(ValueError):
+ symplectic_matrix_compiled = symplectic_matrix_compiled.to_circuit(
+ algorithm=algorithm
+ )
+ with pytest.raises(ValueError):
+ _cnot_cost(symplectic_matrix_compiled)
+ else:
+ with pytest.raises(TypeError):
+ symplectic_matrix_compiled.to_circuit(algorithm=True)
+ with pytest.raises(ValueError):
+ symplectic_matrix_compiled.to_circuit(algorithm="BM21")
+
+ symplectic_matrix_from_symplectic = (
+ symplectic_matrix_from_symplectic.to_circuit(algorithm=algorithm)
+ )
+ symplectic_matrix_from_symplectic = Clifford.from_circuit(
+ symplectic_matrix_from_symplectic, engine=engine
+ ).symplectic_matrix
+
+ symplectic_matrix_compiled = symplectic_matrix_compiled.to_circuit(
+ algorithm=algorithm
+ )
+ symplectic_matrix_compiled = Clifford.from_circuit(
+ symplectic_matrix_compiled, engine=engine
+ ).symplectic_matrix
+
+ backend.assert_allclose(
+ symplectic_matrix_from_symplectic, symplectic_matrix_original
+ )
+ backend.assert_allclose(symplectic_matrix_compiled, symplectic_matrix_original)
+
+
+@pytest.mark.parametrize("nqubits", [1, 10, 50])
+def test_clifford_initialization(backend, nqubits):
+ if backend.__class__.__name__ == "TensorflowBackend":
+ pytest.skip("CliffordBackend not defined for Tensorflow engine.")
+ elif backend.__class__.__name__ == "PyTorchBackend":
+ pytest.skip("CliffordBackend not defined for PyTorch engine.")
+
+ clifford_backend = construct_clifford_backend(backend)
+
+ circuit = random_clifford(nqubits, backend=backend)
+ symplectic_matrix = clifford_backend.execute_circuit(circuit).symplectic_matrix
+
+ engine = _get_engine_name(backend)
+ clifford_from_symplectic = Clifford(symplectic_matrix, engine=engine)
+ clifford_from_circuit = Clifford.from_circuit(circuit, engine=engine)
+ clifford_from_initialization = Clifford(circuit, engine=engine)
+
+ backend.assert_allclose(
+ clifford_from_symplectic.symplectic_matrix, symplectic_matrix
+ )
+ backend.assert_allclose(clifford_from_circuit.symplectic_matrix, symplectic_matrix)
+ backend.assert_allclose(
+ clifford_from_initialization.symplectic_matrix, symplectic_matrix
+ )
+
+
+@pytest.mark.parametrize("return_array", [True, False])
+@pytest.mark.parametrize("symplectic", [True, False])
+def test_clifford_stabilizers(backend, symplectic, return_array):
+ clifford_backend = construct_clifford_backend(backend)
+ if not clifford_backend:
+ return
+
+ nqubits = 3
+ c = Circuit(nqubits)
+ c.add(gates.X(2))
+ c.add(gates.H(0))
+ obj = Clifford.from_circuit(c, engine=_get_engine_name(backend))
+ if return_array:
+ true_generators = [
+ reduce(np.kron, [getattr(matrices, gate) for gate in generator])
+ for generator in ["XII", "IZI", "IIZ"]
+ ]
+ true_generators = backend.cast(true_generators, dtype=true_generators[0].dtype)
+ else:
+ true_generators = ["XII", "IZI", "IIZ"]
+ true_phases = [1, 1, -1]
+ generators, phases = obj.generators(return_array=return_array)
+
+ if return_array:
+ backend.assert_allclose(generators[3:], true_generators)
+ backend.assert_allclose(phases.tolist()[3:], true_phases)
+ else:
+ assert generators[3:] == true_generators
+ assert phases.tolist()[3:] == true_phases
+
+ if symplectic:
+ true_stabilizers = obj.symplectic_matrix[nqubits:-1, :]
+ elif not symplectic and return_array:
+ true_stabilizers = []
+ for stab in [
+ "-XZZ",
+ "XZI",
+ "-XIZ",
+ "XII",
+ "-IZZ",
+ "IZI",
+ "-IIZ",
+ "III",
+ ]:
+ tmp = reduce(np.kron, [getattr(matrices, s) for s in stab.replace("-", "")])
+ if "-" in stab:
+ tmp *= -1
+ true_stabilizers.append(tmp)
+ true_stabilizers = backend.cast(
+ true_stabilizers, dtype=true_stabilizers[0].dtype
+ )
+ elif not symplectic and not return_array:
+ true_stabilizers = [
+ "-XZZ",
+ "XZI",
+ "-XIZ",
+ "XII",
+ "-IZZ",
+ "IZI",
+ "-IIZ",
+ "III",
+ ]
+
+ stabilizers = obj.stabilizers(symplectic, return_array)
+ if symplectic or (not symplectic and return_array):
+ backend.assert_allclose(stabilizers, true_stabilizers)
+ else:
+ assert stabilizers, true_stabilizers
+
+
+@pytest.mark.parametrize("return_array", [True, False])
+@pytest.mark.parametrize("symplectic", [True, False])
+def test_clifford_destabilizers(backend, symplectic, return_array):
+ clifford_backend = construct_clifford_backend(backend)
+ if not clifford_backend:
+ return
+
+ nqubits = 3
+ c = Circuit(nqubits)
+ c.add(gates.X(2))
+ c.add(gates.H(0))
+ obj = Clifford.from_circuit(c, engine=_get_engine_name(backend))
+ if return_array:
+ true_generators = [
+ reduce(np.kron, [getattr(matrices, gate) for gate in generator])
+ for generator in ["ZII", "IXI", "IIX"]
+ ]
+ true_generators = backend.cast(true_generators, dtype=true_generators[0].dtype)
+ else:
+ true_generators = ["ZII", "IXI", "IIX"]
+ true_phases = [1, 1, 1]
+ generators, phases = obj.generators(return_array=return_array)
+
+ if return_array:
+ backend.assert_allclose(generators[:3], true_generators)
+ backend.assert_allclose(phases.tolist()[:3], true_phases)
+ else:
+ assert generators[:3] == true_generators
+ assert phases.tolist()[:3] == true_phases
+
+ if symplectic:
+ true_destabilizers = obj.symplectic_matrix[:nqubits, :]
+ elif not symplectic and return_array:
+ true_destabilizers = []
+ for destab in [
+ "ZXX",
+ "ZXI",
+ "ZIX",
+ "ZII",
+ "IXX",
+ "IXI",
+ "IIX",
+ "III",
+ ]:
+ tmp = reduce(
+ np.kron,
+ [getattr(matrices, s) for s in destab.replace("-", "")],
+ )
+ if "-" in destab:
+ tmp *= -1
+ true_destabilizers.append(tmp)
+ true_destabilizers = backend.cast(
+ true_destabilizers, dtype=true_destabilizers[0].dtype
+ )
+ elif not symplectic and not return_array:
+ true_destabilizers = [
+ "ZXX",
+ "ZXI",
+ "ZIX",
+ "ZII",
+ "IXX",
+ "IXI",
+ "IIX",
+ "III",
+ ]
+ destabilizers = obj.destabilizers(symplectic, return_array)
+ if symplectic or (not symplectic and return_array):
+ backend.assert_allclose(destabilizers, true_destabilizers)
+ else:
+ assert destabilizers, true_destabilizers
+
+
+@pytest.mark.parametrize("binary", [True, False])
+def test_clifford_samples_frequencies(backend, binary):
+ clifford_backend = construct_clifford_backend(backend)
+ if not clifford_backend:
+ return
+ c = random_clifford(5)
+ c.add(gates.M(3, register_name="3"))
+ c.add(gates.M(0, 1, register_name="01"))
+ obj = Clifford.from_circuit(c, nshots=50, engine=_get_engine_name(backend))
+ samples_1 = obj.samples(binary=binary, registers=True)
+ samples_2 = obj.samples(binary=binary, registers=False)
+ if binary:
+ backend.assert_allclose(samples_2, np.hstack((samples_1["3"], samples_1["01"])))
+ else:
+ backend.assert_allclose(
+ samples_2, [s1 + 4 * s2 for s1, s2 in zip(samples_1["01"], samples_1["3"])]
+ )
+ freq_1 = obj.frequencies(binary=binary, registers=True)
+ freq_2 = obj.frequencies(binary=binary, registers=False)
+
+ if not binary:
+ freq_1 = {
+ reg: Counter({f"{int(k):0{len(reg)}b}": v for k, v in freq.items()})
+ for reg, freq in freq_1.items()
+ }
+ freq_2 = Counter({f"{int(k):03b}": v for k, v in freq_2.items()})
+
+ for register, counter in freq_1.items():
+ for bits_1, freq in counter.items():
+ tot = 0
+ for bits_2, counts in freq_2.items():
+ flag = bits_1 == bits_2[0] if register == "3" else bits_1 == bits_2[1:]
+ if flag:
+ tot += counts
+ backend.assert_allclose(freq, tot)
+
+
+def test_clifford_samples_error(backend):
+ clifford_backend = construct_clifford_backend(backend)
+
+ c = random_clifford(1, backend=backend)
+ obj = Clifford.from_circuit(c, engine=_get_engine_name(backend))
+ with pytest.raises(RuntimeError) as excinfo:
+ obj.samples()
+ assert str(excinfo.value) == "No measurement provided."
+
+
+@pytest.mark.parametrize("deep", [False, True])
+@pytest.mark.parametrize("nqubits", [1, 10, 100])
+def test_clifford_copy(backend, nqubits, deep):
+ clifford_backend = construct_clifford_backend(backend)
+
+ circuit = random_clifford(nqubits, backend=backend)
+ clifford = Clifford.from_circuit(circuit, engine=_get_engine_name(backend))
+
+ with pytest.raises(TypeError):
+ clifford.copy(deep="True")
+
+ copy = clifford.copy(deep=deep)
+
+ backend.assert_allclose(copy.symplectic_matrix, clifford.symplectic_matrix)
+ assert copy.nqubits == clifford.nqubits
+ assert copy.measurements == clifford.measurements
+ assert copy.nshots == clifford.nshots
+ assert copy.engine == clifford.engine
+
+
+@pytest.mark.parametrize("pauli_2", ["Z", "Y", "Y"])
+@pytest.mark.parametrize("pauli_1", ["X", "Y", "Z"])
+def test_one_qubit_paulis_string_product(pauli_1, pauli_2):
+ products = {
+ "XY": "iZ",
+ "YZ": "iX",
+ "ZX": "iY",
+ "YX": "-iZ",
+ "ZY": "-iX",
+ "XZ": "iY",
+ "XX": "I",
+ "ZZ": "I",
+ "YY": "I",
+ "XI": "X",
+ "IX": "X",
+ "YI": "Y",
+ "IY": "Y",
+ "ZI": "Z",
+ "IZ": "Z",
+ }
+
+ product = _one_qubit_paulis_string_product(pauli_1, pauli_2)
+ product_target = products[pauli_1 + pauli_2]
+
+ assert product == product_target
+
+
+@pytest.mark.parametrize(
+ ["operators", "target"],
+ [
+ [["X", "Y", "Z"], "iI"],
+ [["Z", "X", "Y", "X", "Z"], "-Y"],
+ [["Z", "I", "Z"], "I"],
+ [["Y", "X"], "-iZ"],
+ [["iY", "iX"], "iZ"],
+ ],
+)
+def test_string_product(operators, target):
+ product = _string_product(operators)
+ assert product == target
+
+
+def test_1q_paulis_string_product():
+ assert "-iZ" == _one_qubit_paulis_string_product("iX", "iY")
diff --git a/tests/test_quantum_info_entanglement.py b/tests/test_quantum_info_entanglement.py
new file mode 100644
index 000000000..a641d01c9
--- /dev/null
+++ b/tests/test_quantum_info_entanglement.py
@@ -0,0 +1,175 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.entanglement import (
+ concurrence,
+ entanglement_fidelity,
+ entanglement_of_formation,
+ entangling_capability,
+ meyer_wallach_entanglement,
+)
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+
+
+@pytest.mark.parametrize("check_purity", [True, False])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("bipartition", [[0], [1]])
+def test_concurrence_and_formation(backend, bipartition, base, check_purity):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = concurrence(
+ state, bipartition=bipartition, check_purity=check_purity, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = random_statevector(4, backend=backend)
+ test = concurrence(
+ state, bipartition=bipartition, check_purity="True", backend=backend
+ )
+
+ if check_purity is True:
+ with pytest.raises(NotImplementedError):
+ state = backend.identity_density_matrix(2, normalize=False)
+ test = concurrence(state, bipartition=bipartition, backend=backend)
+
+ nqubits = 2
+ dim = 2**nqubits
+ state = random_statevector(dim, backend=backend)
+ concur = concurrence(
+ state, bipartition=bipartition, check_purity=check_purity, backend=backend
+ )
+ ent_form = entanglement_of_formation(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_purity=check_purity,
+ backend=backend,
+ )
+ backend.assert_allclose(0.0 <= concur <= np.sqrt(2), True)
+ backend.assert_allclose(0.0 <= ent_form <= 1.0, True)
+
+ state = backend.np.kron(
+ random_density_matrix(2, pure=True, backend=backend),
+ random_density_matrix(2, pure=True, backend=backend),
+ )
+ concur = concurrence(state, bipartition, check_purity=check_purity, backend=backend)
+ ent_form = entanglement_of_formation(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_purity=check_purity,
+ backend=backend,
+ )
+ backend.assert_allclose(concur, 0.0, atol=10 * PRECISION_TOL)
+ backend.assert_allclose(ent_form, 0.0, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("check_hermitian", [False, True])
+@pytest.mark.parametrize("nqubits", [4, 6])
+@pytest.mark.parametrize("channel", [gates.DepolarizingChannel])
+def test_entanglement_fidelity(backend, channel, nqubits, check_hermitian):
+ with pytest.raises(TypeError):
+ test = entanglement_fidelity(
+ channel, nqubits=[0], check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(ValueError):
+ test = entanglement_fidelity(
+ channel, nqubits=0, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3, 2)
+ state = backend.cast(state, dtype=state.dtype)
+ test = entanglement_fidelity(
+ channel,
+ nqubits,
+ state=state,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ with pytest.raises(TypeError):
+ state = random_statevector(2, backend=backend)
+ test = entanglement_fidelity(
+ channel, nqubits, state=state, check_hermitian="False", backend=backend
+ )
+
+ channel = channel([0, 1], 0.5)
+
+ # test on maximally entangled state
+ ent_fid = entanglement_fidelity(
+ channel, nqubits=nqubits, check_hermitian=check_hermitian, backend=backend
+ )
+ backend.assert_allclose(ent_fid, 0.625, atol=PRECISION_TOL)
+
+ # test with a state vector
+ state = backend.plus_state(nqubits)
+ ent_fid = entanglement_fidelity(
+ channel,
+ nqubits=nqubits,
+ state=state,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ backend.assert_allclose(ent_fid, 0.625, atol=PRECISION_TOL)
+
+ # test on maximally mixed state
+ state = backend.identity_density_matrix(nqubits)
+ ent_fid = entanglement_fidelity(
+ channel,
+ nqubits=nqubits,
+ state=state,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ backend.assert_allclose(ent_fid, 1.0, atol=PRECISION_TOL)
+
+
+def test_meyer_wallach_entanglement(backend):
+ nqubits = 2
+
+ circuit1 = Circuit(nqubits)
+ circuit1.add([gates.RX(0, np.pi / 4)] for _ in range(nqubits))
+
+ circuit2 = Circuit(nqubits)
+ circuit2.add([gates.RX(0, np.pi / 4)] for _ in range(nqubits))
+ circuit2.add(gates.CNOT(0, 1))
+
+ backend.assert_allclose(
+ meyer_wallach_entanglement(circuit1, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+
+ backend.assert_allclose(
+ meyer_wallach_entanglement(circuit2, backend=backend), 0.5, atol=PRECISION_TOL
+ )
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_entangling_capability(backend, seed):
+ with pytest.raises(TypeError):
+ circuit = Circuit(1)
+ samples = 0.5
+ entangling_capability(circuit, samples, seed=seed, backend=backend)
+ with pytest.raises(TypeError):
+ circuit = Circuit(1)
+ entangling_capability(circuit, samples=10, seed="10", backend=backend)
+
+ nqubits = 2
+ samples = 100
+
+ c1 = Circuit(nqubits)
+ c1.add([gates.RX(q, 0, trainable=True) for q in range(nqubits)])
+ c1.add(gates.CNOT(0, 1))
+ c1.add([gates.RX(q, 0, trainable=True) for q in range(nqubits)])
+ ent_mw1 = entangling_capability(c1, samples, seed=seed, backend=backend)
+
+ c2 = Circuit(nqubits)
+ c2.add(gates.H(0))
+ c2.add(gates.CNOT(0, 1))
+ c2.add(gates.RX(0, 0, trainable=True))
+ ent_mw2 = entangling_capability(c2, samples, seed=seed, backend=backend)
+
+ c3 = Circuit(nqubits)
+ ent_mw3 = entangling_capability(c3, samples, seed=seed, backend=backend)
+
+ backend.assert_allclose(ent_mw3 < ent_mw1 < ent_mw2, True)
diff --git a/tests/test_quantum_info_entropies.py b/tests/test_quantum_info_entropies.py
new file mode 100644
index 000000000..84129e3c6
--- /dev/null
+++ b/tests/test_quantum_info_entropies.py
@@ -0,0 +1,757 @@
+import numpy as np
+import pytest
+from scipy.linalg import sqrtm
+
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.entropies import (
+ _matrix_power,
+ classical_relative_entropy,
+ classical_relative_renyi_entropy,
+ classical_renyi_entropy,
+ classical_tsallis_entropy,
+ entanglement_entropy,
+ relative_renyi_entropy,
+ relative_von_neumann_entropy,
+ renyi_entropy,
+ shannon_entropy,
+ tsallis_entropy,
+ von_neumann_entropy,
+)
+from qibo.quantum_info.random_ensembles import (
+ random_density_matrix,
+ random_statevector,
+ random_unitary,
+)
+
+
+def test_shannon_entropy_errors(backend):
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, -2, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([[1.0], [0.0]])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, -1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.1, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([0.5, 0.4999999])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = shannon_entropy(prob, backend=backend)
+
+
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+def test_shannon_entropy(backend, base):
+ prob_array = [1.0, 0.0]
+ prob_array = backend.cast(prob_array, dtype=np.float64)
+ result = shannon_entropy(prob_array, base, backend=backend)
+ backend.assert_allclose(result, 0.0)
+
+ if base == 2:
+ prob_array = np.array([0.5, 0.5])
+ prob_array = backend.cast(prob_array, dtype=prob_array.dtype)
+ result = shannon_entropy(prob_array, base, backend=backend)
+ backend.assert_allclose(result, 1.0)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+def test_classical_relative_entropy(backend, base, kind):
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)
+ prob_q = np.random.rand(1, 5)
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([-1, 2.0])
+ prob_q = np.random.rand(1, 5)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.random.rand(1, 2)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.array([0.0, 1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_entropy(prob, prob_q, base=-2, backend=backend)
+
+ prob_p = np.random.rand(10)
+ prob_q = np.random.rand(10)
+ prob_p /= np.sum(prob_p)
+ prob_q /= np.sum(prob_q)
+
+ target = np.sum(prob_p * np.log(prob_p) / np.log(base)) - np.sum(
+ prob_p * np.log(prob_q) / np.log(base)
+ )
+
+ if kind is not None:
+ prob_p, prob_q = kind(prob_p), kind(prob_q)
+ else:
+ prob_p = np.real(backend.cast(prob_p))
+ prob_q = np.real(backend.cast(prob_q))
+
+ divergence = classical_relative_entropy(prob_p, prob_q, base=base, backend=backend)
+
+ backend.assert_allclose(divergence, target, atol=1e-5)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1, 2, 3, np.inf])
+def test_classical_renyi_entropy(backend, alpha, base, kind):
+ with pytest.raises(TypeError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha="2", backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha=-2, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, base="2", backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, base=-2, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([[1.0], [0.0]])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, -1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.1, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([0.5, 0.4999999])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_renyi_entropy(prob, alpha, backend=backend)
+
+ prob_dist = np.random.rand(10)
+ prob_dist /= np.sum(prob_dist)
+
+ if alpha == 0.0:
+ target = np.log2(len(prob_dist)) / np.log2(base)
+ elif alpha == 1:
+ target = shannon_entropy(
+ backend.cast(prob_dist, dtype=np.float64), base=base, backend=backend
+ )
+ elif alpha == 2:
+ target = -1 * np.log2(np.sum(prob_dist**2)) / np.log2(base)
+ elif alpha == np.inf:
+ target = -1 * np.log2(max(prob_dist)) / np.log2(base)
+ else:
+ target = (1 / (1 - alpha)) * np.log2(np.sum(prob_dist**alpha)) / np.log2(base)
+
+ if kind is not None:
+ prob_dist = kind(prob_dist)
+ else:
+ prob_dist = np.real(backend.cast(prob_dist))
+
+ renyi_ent = classical_renyi_entropy(prob_dist, alpha, base=base, backend=backend)
+
+ backend.assert_allclose(renyi_ent, target, atol=1e-5)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1 / 2, 1, 2, 3, np.inf])
+def test_classical_relative_renyi_entropy(backend, alpha, base, kind):
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)
+ prob_q = np.random.rand(1, 5)
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base, backend=backend
+ )
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ prob = np.array([-1, 2.0])
+ prob_q = np.random.rand(1, 5)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.random.rand(1, 2)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.array([0.0, 1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha, base=-2, backend=backend
+ )
+ with pytest.raises(TypeError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.array([0.0, 1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha="1", base=base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.array([0.0, 1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = classical_relative_renyi_entropy(
+ prob, prob_q, alpha=-2, base=base, backend=backend
+ )
+
+ prob_p = np.random.rand(10)
+ prob_q = np.random.rand(10)
+ prob_p /= np.sum(prob_p)
+ prob_q /= np.sum(prob_q)
+
+ if alpha == 0.5:
+ target = -2 * np.log2(np.sum(np.sqrt(prob_p * prob_q))) / np.log2(base)
+ elif alpha == 1.0:
+ target = classical_relative_entropy(
+ np.real(backend.cast(prob_p)),
+ np.real(backend.cast(prob_q)),
+ base=base,
+ backend=backend,
+ )
+ elif alpha == np.inf:
+ target = np.log2(max(prob_p / prob_q)) / np.log2(base)
+ else:
+ target = (
+ (1 / (alpha - 1))
+ * np.log2(np.sum(prob_p**alpha * prob_q ** (1 - alpha)))
+ / np.log2(base)
+ )
+
+ if kind is not None:
+ prob_p, prob_q = kind(prob_p), kind(prob_q)
+ else:
+ prob_p = np.real(backend.cast(prob_p))
+ prob_q = np.real(backend.cast(prob_q))
+
+ divergence = classical_relative_renyi_entropy(
+ prob_p, prob_q, alpha=alpha, base=base, backend=backend
+ )
+
+ backend.assert_allclose(divergence, target, atol=1e-5)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1, 2, 3])
+def test_classical_tsallis_entropy(backend, alpha, base, kind):
+ with pytest.raises(TypeError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha="2", backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha=-2, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, base="2", backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, base=-2, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([[1.0], [0.0]])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, -1.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.1, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([0.5, 0.4999999])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ test = classical_tsallis_entropy(prob, alpha, backend=backend)
+
+ prob_dist = np.random.rand(10)
+ prob_dist /= np.sum(prob_dist)
+
+ if alpha == 1.0:
+ target = shannon_entropy(
+ np.real(backend.cast(prob_dist)), base=base, backend=backend
+ )
+ else:
+ target = (1 / (1 - alpha)) * (np.sum(prob_dist**alpha) - 1)
+
+ if kind is not None:
+ prob_dist = kind(prob_dist)
+ else:
+ prob_dist = np.real(backend.cast(prob_dist))
+
+ backend.assert_allclose(
+ classical_tsallis_entropy(prob_dist, alpha=alpha, base=base, backend=backend),
+ target,
+ atol=1e-5,
+ )
+
+
+@pytest.mark.parametrize("check_hermitian", [False, True])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+def test_von_neumann_entropy(backend, base, check_hermitian):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = von_neumann_entropy(
+ state, base=base, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(ValueError):
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ test = von_neumann_entropy(
+ state, base=0, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ test = von_neumann_entropy(
+ state, base=base, check_hermitian="False", backend=backend
+ )
+
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ state = random_unitary(4, backend=backend)
+ test = von_neumann_entropy(
+ state, base=base, check_hermitian=True, backend=backend
+ )
+ else:
+ state = random_unitary(4, backend=backend)
+ test = von_neumann_entropy(
+ state, base=base, check_hermitian=True, backend=backend
+ )
+
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ backend.assert_allclose(
+ von_neumann_entropy(state, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+
+ state = np.array([1.0, 0.0, 0.0, 0.0])
+ state = np.outer(state, state)
+ state = backend.cast(state, dtype=state.dtype)
+
+ nqubits = 2
+ state = backend.identity_density_matrix(nqubits)
+ if base == 2:
+ test = 2.0
+ elif base == 10:
+ test = 0.6020599913279624
+ elif base == np.e:
+ test = 1.3862943611198906
+ else:
+ test = 0.8613531161467861
+
+ backend.assert_allclose(
+ von_neumann_entropy(
+ state, base, check_hermitian=check_hermitian, backend=backend
+ ),
+ test,
+ )
+
+
+@pytest.mark.parametrize("check_hermitian", [False, True])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+def test_relative_entropy(backend, base, check_hermitian):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ target = random_density_matrix(2, pure=True, backend=backend)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ target = np.random.rand(2, 3)
+ target = backend.cast(target, dtype=target.dtype)
+ state = random_density_matrix(2, pure=True, backend=backend)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(ValueError):
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = np.array([0.0, 1.0])
+ target = backend.cast(target, dtype=target.dtype)
+ test = relative_von_neumann_entropy(
+ state, target, base=0, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = np.array([0.0, 1.0])
+ target = backend.cast(target, dtype=target.dtype)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian="False", backend=backend
+ )
+
+ nqubits = 2
+ dims = 2**nqubits
+
+ state = random_density_matrix(dims, backend=backend)
+ target = backend.identity_density_matrix(nqubits, normalize=True)
+
+ backend.assert_allclose(
+ relative_von_neumann_entropy(state, target, base, check_hermitian, backend),
+ np.log(dims) / np.log(base)
+ - von_neumann_entropy(
+ state, base=base, check_hermitian=check_hermitian, backend=backend
+ ),
+ atol=1e-5,
+ )
+
+ state = backend.cast([1.0, 0.0], dtype=np.float64)
+ target = backend.cast([0.0, 1.0], dtype=np.float64)
+
+ assert relative_von_neumann_entropy(state, target, backend=backend) == 0.0
+
+ # for coverage when GPUs are present
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ state = random_unitary(4, backend=backend)
+ target = random_density_matrix(4, backend=backend)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian=True, backend=backend
+ )
+ with pytest.raises(NotImplementedError):
+ target = random_unitary(4, backend=backend)
+ state = random_density_matrix(4, backend=backend)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian=True, backend=backend
+ )
+ else:
+ state = random_unitary(4, backend=backend)
+ target = random_unitary(4, backend=backend)
+ test = relative_von_neumann_entropy(
+ state, target, base=base, check_hermitian=True, backend=backend
+ )
+
+
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1, 2, 3, np.inf])
+def test_renyi_entropy(backend, alpha, base):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = renyi_entropy(state, alpha=alpha, base=base, backend=backend)
+ with pytest.raises(TypeError):
+ state = random_statevector(4, backend=backend)
+ test = renyi_entropy(state, alpha="2", base=base, backend=backend)
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ test = renyi_entropy(state, alpha=-1, base=base, backend=backend)
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ test = renyi_entropy(state, alpha=alpha, base=0, backend=backend)
+
+ state = random_density_matrix(4, backend=backend)
+
+ if alpha == 0.0:
+ target = np.log2(len(state)) / np.log2(base)
+ elif alpha == 1.0:
+ target = von_neumann_entropy(state, base=base, backend=backend)
+ elif alpha == np.inf:
+ target = backend.calculate_norm_density_matrix(state, order=2)
+ target = -1 * backend.np.log2(target) / np.log2(base)
+ else:
+ target = np.log2(
+ np.trace(np.linalg.matrix_power(backend.to_numpy(state), alpha))
+ )
+ target = (1 / (1 - alpha)) * target / np.log2(base)
+
+ backend.assert_allclose(
+ renyi_entropy(state, alpha=alpha, base=base, backend=backend), target, atol=1e-5
+ )
+
+ # test pure state
+ state = random_density_matrix(4, pure=True, backend=backend)
+ backend.assert_allclose(
+ renyi_entropy(state, alpha=alpha, base=base, backend=backend), 0.0, atol=1e-8
+ )
+
+
+@pytest.mark.parametrize(
+ ["state_flag", "target_flag"], [[True, True], [False, True], [True, False]]
+)
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1, 2, 3, 5.4, np.inf])
+def test_relative_renyi_entropy(backend, alpha, base, state_flag, target_flag):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ target = random_density_matrix(4, backend=backend)
+ test = relative_renyi_entropy(
+ state, target, alpha=alpha, base=base, backend=backend
+ )
+ with pytest.raises(TypeError):
+ target = np.random.rand(2, 3)
+ target = backend.cast(target, dtype=target.dtype)
+ state = random_density_matrix(4, backend=backend)
+ test = relative_renyi_entropy(
+ state, target, alpha=alpha, base=base, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = random_statevector(4, backend=backend)
+ target = random_statevector(4, backend=backend)
+ test = relative_renyi_entropy(
+ state, target, alpha="2", base=base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ target = random_statevector(4, backend=backend)
+ test = relative_renyi_entropy(
+ state, target, alpha=-1, base=base, backend=backend
+ )
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ target = random_statevector(4, backend=backend)
+ test = relative_renyi_entropy(
+ state, target, alpha=alpha, base=0, backend=backend
+ )
+
+ state = (
+ random_statevector(4, backend=backend)
+ if state_flag
+ else random_density_matrix(4, backend=backend)
+ )
+ target = (
+ random_statevector(4, backend=backend)
+ if target_flag
+ else random_density_matrix(4, backend=backend)
+ )
+
+ if state_flag and target_flag:
+ backend.assert_allclose(
+ relative_renyi_entropy(state, target, alpha, base, backend), 0.0, atol=1e-5
+ )
+ else:
+ if target_flag and alpha > 1:
+ with pytest.raises(NotImplementedError):
+ relative_renyi_entropy(state, target, alpha, base, backend)
+ else:
+ if alpha == 1.0:
+ log = relative_von_neumann_entropy(state, target, base, backend=backend)
+ elif alpha == np.inf:
+ new_state = _matrix_power(state, 0.5, backend)
+ new_target = _matrix_power(target, 0.5, backend)
+
+ log = backend.np.log2(
+ backend.calculate_norm_density_matrix(
+ new_state @ new_target, order=1
+ )
+ )
+
+ log = -2 * log / np.log2(base)
+ else:
+ if len(state.shape) == 1:
+ state = backend.np.outer(state, backend.np.conj(state))
+
+ if len(target.shape) == 1:
+ target = backend.np.outer(target, backend.np.conj(target))
+
+ log = _matrix_power(state, alpha, backend)
+ log = log @ _matrix_power(target, 1 - alpha, backend)
+ log = backend.np.log2(backend.np.trace(log))
+
+ log = (1 / (alpha - 1)) * log / np.log2(base)
+
+ backend.assert_allclose(
+ relative_renyi_entropy(
+ state, target, alpha=alpha, base=base, backend=backend
+ ),
+ log,
+ atol=1e-5,
+ )
+
+ # test pure states
+ state = random_density_matrix(4, pure=True, backend=backend)
+ target = random_density_matrix(4, pure=True, backend=backend)
+ backend.assert_allclose(
+ relative_renyi_entropy(state, target, alpha=alpha, base=base, backend=backend),
+ 0.0,
+ atol=1e-8,
+ )
+
+
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("alpha", [0, 1, 2, 3, 5.4])
+def test_tsallis_entropy(backend, alpha, base):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = tsallis_entropy(state, alpha=alpha, base=base, backend=backend)
+ with pytest.raises(TypeError):
+ state = random_statevector(4, backend=backend)
+ test = tsallis_entropy(state, alpha="2", base=base, backend=backend)
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ test = tsallis_entropy(state, alpha=-1, base=base, backend=backend)
+ with pytest.raises(ValueError):
+ state = random_statevector(4, backend=backend)
+ test = tsallis_entropy(state, alpha=alpha, base=0, backend=backend)
+
+ state = random_density_matrix(4, backend=backend)
+
+ if alpha == 1.0:
+ target = von_neumann_entropy(state, base=base, backend=backend)
+ else:
+ target = (1 / (1 - alpha)) * (
+ backend.np.trace(_matrix_power(state, alpha, backend)) - 1
+ )
+
+ backend.assert_allclose(
+ tsallis_entropy(state, alpha=alpha, base=base, backend=backend),
+ target,
+ atol=1e-5,
+ )
+
+ # test pure state
+ state = random_density_matrix(4, pure=True, backend=backend)
+ backend.assert_allclose(
+ tsallis_entropy(state, alpha=alpha, base=base, backend=backend), 0.0, atol=1e-5
+ )
+
+
+@pytest.mark.parametrize("check_hermitian", [False, True])
+@pytest.mark.parametrize("base", [2, 10, np.e, 5])
+@pytest.mark.parametrize("bipartition", [[0], [1]])
+def test_entanglement_entropy(backend, bipartition, base, check_hermitian):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = entanglement_entropy(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ with pytest.raises(ValueError):
+ state = np.array([1.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ test = entanglement_entropy(
+ state,
+ bipartition=bipartition,
+ base=0,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ if backend.__class__.__name__ == "CupyBackend":
+ with pytest.raises(NotImplementedError):
+ state = random_unitary(4, backend=backend)
+ test = entanglement_entropy(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_hermitian=True,
+ backend=backend,
+ )
+
+ # Bell state
+ state = np.array([1.0, 0.0, 0.0, 1.0]) / np.sqrt(2)
+ state = backend.cast(state, dtype=state.dtype)
+
+ entang_entrop = entanglement_entropy(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+
+ if base == 2:
+ test = 1.0
+ elif base == 10:
+ test = 0.30102999566398125
+ elif base == np.e:
+ test = 0.6931471805599454
+ else:
+ test = 0.4306765580733931
+
+ backend.assert_allclose(entang_entrop, test, atol=PRECISION_TOL)
+
+ # Product state
+ state = backend.np.kron(
+ random_statevector(2, backend=backend), random_statevector(2, backend=backend)
+ )
+
+ entang_entrop = entanglement_entropy(
+ state,
+ bipartition=bipartition,
+ base=base,
+ check_hermitian=check_hermitian,
+ backend=backend,
+ )
+ backend.assert_allclose(entang_entrop, 0.0, atol=PRECISION_TOL)
diff --git a/tests/test_quantum_info_metrics.py b/tests/test_quantum_info_metrics.py
new file mode 100644
index 000000000..1580cdfb7
--- /dev/null
+++ b/tests/test_quantum_info_metrics.py
@@ -0,0 +1,396 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.metrics import (
+ average_gate_fidelity,
+ bures_angle,
+ bures_distance,
+ diamond_norm,
+ expressibility,
+ fidelity,
+ frame_potential,
+ gate_error,
+ hilbert_schmidt_distance,
+ impurity,
+ infidelity,
+ process_fidelity,
+ process_infidelity,
+ purity,
+ trace_distance,
+)
+from qibo.quantum_info.random_ensembles import (
+ random_density_matrix,
+ random_hermitian,
+ random_unitary,
+)
+from qibo.quantum_info.superoperator_transformations import to_choi
+
+
+def test_purity_and_impurity(backend):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 3)
+ state = backend.cast(state, dtype=state.dtype)
+ test = purity(state, backend=backend)
+
+ state = np.array([1.0, 0.0, 0.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ backend.assert_allclose(purity(state, backend=backend), 1.0, atol=PRECISION_TOL)
+ backend.assert_allclose(impurity(state, backend=backend), 0.0, atol=PRECISION_TOL)
+
+ state = backend.np.outer(backend.np.conj(state), state)
+ state = backend.cast(state, dtype=state.dtype)
+ backend.assert_allclose(purity(state, backend=backend), 1.0, atol=PRECISION_TOL)
+ backend.assert_allclose(impurity(state, backend=backend), 0.0, atol=PRECISION_TOL)
+
+ dim = 4
+ state = backend.identity_density_matrix(2)
+ state = backend.cast(state, dtype=state.dtype)
+ backend.assert_allclose(
+ purity(state, backend=backend), 1.0 / dim, atol=PRECISION_TOL
+ )
+ backend.assert_allclose(
+ impurity(state, backend=backend), 1.0 - 1.0 / dim, atol=PRECISION_TOL
+ )
+
+
+@pytest.mark.parametrize("check_hermitian", [False, True])
+def test_trace_distance(backend, check_hermitian):
+ with pytest.raises(TypeError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ target = random_density_matrix(4, pure=True, backend=backend)
+ test = trace_distance(
+ state, target, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 2, 2)
+ target = np.random.rand(2, 2, 2)
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ test = trace_distance(
+ state, target, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = np.array([])
+ target = np.array([])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=state.dtype)
+ test = trace_distance(
+ state, target, check_hermitian=check_hermitian, backend=backend
+ )
+ with pytest.raises(TypeError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ target = random_density_matrix(2, pure=True, backend=backend)
+ test = trace_distance(state, target, check_hermitian="True", backend=backend)
+
+ state = np.array([1.0, 0.0, 0.0, 0.0])
+ target = np.array([1.0, 0.0, 0.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ trace_distance(state, target, check_hermitian=check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+
+ state = backend.np.outer(backend.np.conj(state), state)
+ target = backend.np.outer(backend.np.conj(target), target)
+ backend.assert_allclose(
+ trace_distance(state, target, check_hermitian=check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+
+ state = np.array([0.0, 1.0, 0.0, 0.0])
+ target = np.array([1.0, 0.0, 0.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ trace_distance(state, target, check_hermitian=check_hermitian, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+
+
+def test_hilbert_schmidt_distance(backend):
+ with pytest.raises(TypeError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ target = random_density_matrix(4, pure=True, backend=backend)
+ hilbert_schmidt_distance(
+ state,
+ target,
+ )
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 2, 2)
+ target = np.random.rand(2, 2, 2)
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ hilbert_schmidt_distance(state, target)
+ with pytest.raises(TypeError):
+ state = np.array([])
+ target = np.array([])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ hilbert_schmidt_distance(state, target)
+
+ state = np.array([1.0, 0.0, 0.0, 0.0])
+ target = np.array([1.0, 0.0, 0.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ hilbert_schmidt_distance(state, target, backend=backend), 0.0
+ )
+
+ state = backend.np.outer(backend.np.conj(state), state)
+ target = backend.np.outer(backend.np.conj(target), target)
+ backend.assert_allclose(
+ hilbert_schmidt_distance(state, target, backend=backend), 0.0
+ )
+
+ state = np.array([0.0, 1.0, 0.0, 0.0])
+ target = np.array([1.0, 0.0, 0.0, 0.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ hilbert_schmidt_distance(state, target, backend=backend), 2.0
+ )
+
+
+@pytest.mark.parametrize("check_hermitian", [True, False])
+def test_fidelity_and_infidelity_and_bures(backend, check_hermitian):
+ with pytest.raises(TypeError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ target = random_density_matrix(4, pure=True, backend=backend)
+ test = fidelity(state, target, check_hermitian=check_hermitian, backend=backend)
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 2, 2)
+ target = np.random.rand(2, 2, 2)
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ test = fidelity(state, target, check_hermitian, backend=backend)
+ with pytest.raises(TypeError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ target = random_density_matrix(2, pure=True, backend=backend)
+ test = fidelity(state, target, check_hermitian="True", backend=backend)
+
+ state = backend.identity_density_matrix(4)
+ target = backend.identity_density_matrix(4)
+ backend.assert_allclose(
+ fidelity(state, target, check_hermitian, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+
+ state = np.array([0.0, 0.0, 0.0, 1.0])
+ target = np.array([0.0, 0.0, 0.0, 1.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ fidelity(state, target, check_hermitian, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ infidelity(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_angle(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_distance(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+
+ state = backend.np.outer(backend.np.conj(state), state)
+ target = backend.np.outer(backend.np.conj(target), target)
+ backend.assert_allclose(
+ fidelity(state, target, check_hermitian, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ infidelity(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_angle(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_distance(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+
+ state = np.array([0.0, 1.0, 0.0, 0.0])
+ target = np.array([0.0, 0.0, 0.0, 1.0])
+ state = backend.cast(state, dtype=state.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ backend.assert_allclose(
+ fidelity(state, target, check_hermitian, backend=backend),
+ 0.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ infidelity(state, target, check_hermitian, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_angle(state, target, check_hermitian, backend=backend),
+ np.arccos(0.0),
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ bures_distance(state, target, check_hermitian, backend=backend),
+ np.sqrt(2),
+ atol=PRECISION_TOL,
+ )
+
+ state = random_unitary(4, backend=backend)
+ target = random_unitary(4, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ test = fidelity(state, target, check_hermitian=True, backend=backend)
+ else:
+ test = fidelity(state, target, check_hermitian=True, backend=backend)
+
+
+@pytest.mark.parametrize("seed", [10])
+def test_process_fidelity_and_infidelity(backend, seed):
+ d = 2
+ rng = np.random.default_rng(seed)
+ with pytest.raises(TypeError):
+ channel = rng.random(d**2, d**2)
+ target = rng.random(d**2, d**2, d**2)
+ channel = backend.cast(channel, dtype=channel.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ test = process_fidelity(channel, target, backend=backend)
+ with pytest.raises(TypeError):
+ channel = random_hermitian(d**2, seed=rng, backend=backend)
+ test = process_fidelity(channel, check_unitary=True, backend=backend)
+ with pytest.raises(TypeError):
+ channel = 10 * rng.random(d**2, d**2)
+ target = 10 * rng.random(d**2, d**2)
+ channel = backend.cast(channel, dtype=channel.dtype)
+ target = backend.cast(target, dtype=target.dtype)
+ test = process_fidelity(channel, target, check_unitary=True, backend=backend)
+
+ channel = np.eye(d**2)
+ channel = backend.cast(channel, dtype=channel.dtype)
+
+ backend.assert_allclose(
+ process_fidelity(channel, backend=backend), 1.0, atol=PRECISION_TOL
+ )
+ backend.assert_allclose(
+ process_infidelity(channel, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+
+ backend.assert_allclose(
+ process_fidelity(channel, channel, backend=backend), 1.0, atol=PRECISION_TOL
+ )
+ backend.assert_allclose(
+ process_infidelity(channel, channel, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+
+ backend.assert_allclose(
+ average_gate_fidelity(channel, backend=backend), 1.0, atol=PRECISION_TOL
+ )
+ backend.assert_allclose(
+ average_gate_fidelity(channel, channel, backend=backend),
+ 1.0,
+ atol=PRECISION_TOL,
+ )
+ backend.assert_allclose(
+ gate_error(channel, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+ backend.assert_allclose(
+ gate_error(channel, channel, backend=backend), 0.0, atol=PRECISION_TOL
+ )
+
+
+@pytest.mark.skip
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_diamond_norm(backend, nqubits):
+ with pytest.raises(TypeError):
+ test = random_unitary(2**nqubits, backend=backend)
+ test_2 = random_unitary(4**nqubits, backend=backend)
+ test = diamond_norm(test, test_2)
+
+ unitary = backend.identity_density_matrix(nqubits, normalize=False)
+ unitary = to_choi(unitary, order="row", backend=backend)
+
+ dnorm = diamond_norm(unitary, backend=backend)
+ backend.assert_allclose(dnorm, 1.0, atol=PRECISION_TOL)
+
+ dnorm = diamond_norm(unitary, unitary, backend=backend)
+ backend.assert_allclose(dnorm, 0.0, atol=PRECISION_TOL)
+
+
+def test_expressibility(backend):
+ with pytest.raises(TypeError):
+ circuit = Circuit(1)
+ t = 0.5
+ samples = 10
+ expressibility(circuit, t, samples, backend=backend)
+ with pytest.raises(TypeError):
+ circuit = Circuit(1)
+ t = 1
+ samples = 0.5
+ expressibility(circuit, t, samples, backend=backend)
+
+ nqubits = 2
+ samples = 100
+ t = 1
+
+ c1 = Circuit(nqubits)
+ c1.add([gates.RX(q, 0, trainable=True) for q in range(nqubits)])
+ c1.add(gates.CNOT(0, 1))
+ c1.add([gates.RX(q, 0, trainable=True) for q in range(nqubits)])
+ expr_1 = expressibility(c1, t, samples, backend=backend)
+
+ c2 = Circuit(nqubits)
+ c2.add(gates.H(0))
+ c2.add(gates.CNOT(0, 1))
+ c2.add(gates.RX(0, 0, trainable=True))
+ expr_2 = expressibility(c2, t, samples, backend=backend)
+
+ c3 = Circuit(nqubits)
+ expr_3 = expressibility(c3, t, samples, backend=backend)
+
+ backend.assert_allclose(expr_1 < expr_2 < expr_3, True)
+
+
+@pytest.mark.parametrize("samples", [int(1e1)])
+@pytest.mark.parametrize("power_t", [2])
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_frame_potential(backend, nqubits, power_t, samples):
+ depth = int(np.ceil(nqubits * power_t))
+
+ circuit = Circuit(nqubits)
+ circuit.add(gates.U3(q, 0.0, 0.0, 0.0) for q in range(nqubits))
+ for _ in range(depth):
+ circuit.add(gates.CNOT(q, q + 1) for q in range(nqubits - 1))
+ circuit.add(gates.U3(q, 0.0, 0.0, 0.0) for q in range(nqubits))
+
+ with pytest.raises(TypeError):
+ frame_potential(circuit, power_t="2", backend=backend)
+ with pytest.raises(TypeError):
+ frame_potential(circuit, 2, samples="1000", backend=backend)
+
+ dim = 2**nqubits
+ potential_haar = 2 / dim**4
+
+ potential = frame_potential(
+ circuit, power_t=power_t, samples=samples, backend=backend
+ )
+
+ backend.assert_allclose(potential, potential_haar, rtol=1e-2, atol=1e-2)
diff --git a/tests/test_quantum_info_operations.py b/tests/test_quantum_info_operations.py
new file mode 100644
index 000000000..057b69e6e
--- /dev/null
+++ b/tests/test_quantum_info_operations.py
@@ -0,0 +1,111 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.quantum_info.linalg_operations import (
+ anticommutator,
+ commutator,
+ partial_trace,
+)
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+
+
+def test_commutator(backend):
+ matrix_1 = np.random.rand(2, 2, 2)
+ matrix_1 = backend.cast(matrix_1, dtype=matrix_1.dtype)
+
+ matrix_2 = np.random.rand(2, 2)
+ matrix_2 = backend.cast(matrix_2, dtype=matrix_2.dtype)
+
+ matrix_3 = np.random.rand(3, 3)
+ matrix_3 = backend.cast(matrix_3, dtype=matrix_3.dtype)
+
+ with pytest.raises(TypeError):
+ test = commutator(matrix_1, matrix_2)
+ with pytest.raises(TypeError):
+ test = commutator(matrix_2, matrix_1)
+ with pytest.raises(TypeError):
+ test = commutator(matrix_2, matrix_3)
+
+ I, X, Y, Z = matrices.I, matrices.X, matrices.Y, matrices.Z
+ I = backend.cast(I, dtype=I.dtype)
+ X = backend.cast(X, dtype=X.dtype)
+ Y = backend.cast(Y, dtype=Y.dtype)
+ Z = backend.cast(Z, dtype=Z.dtype)
+
+ comm = commutator(X, I)
+ backend.assert_allclose(comm, 0.0)
+
+ comm = commutator(X, X)
+ backend.assert_allclose(comm, 0.0)
+
+ comm = commutator(X, Y)
+ backend.assert_allclose(comm, 2j * Z)
+
+ comm = commutator(X, Z)
+ backend.assert_allclose(comm, -2j * Y)
+
+
+def test_anticommutator(backend):
+ matrix_1 = np.random.rand(2, 2, 2)
+ matrix_1 = backend.cast(matrix_1, dtype=matrix_1.dtype)
+
+ matrix_2 = np.random.rand(2, 2)
+ matrix_2 = backend.cast(matrix_2, dtype=matrix_2.dtype)
+
+ matrix_3 = np.random.rand(3, 3)
+ matrix_3 = backend.cast(matrix_3, dtype=matrix_3.dtype)
+
+ with pytest.raises(TypeError):
+ test = anticommutator(matrix_1, matrix_2)
+ with pytest.raises(TypeError):
+ test = anticommutator(matrix_2, matrix_1)
+ with pytest.raises(TypeError):
+ test = anticommutator(matrix_2, matrix_3)
+
+ I, X, Y, Z = matrices.I, matrices.X, matrices.Y, matrices.Z
+ I = backend.cast(I, dtype=I.dtype)
+ X = backend.cast(X, dtype=X.dtype)
+ Y = backend.cast(Y, dtype=Y.dtype)
+ Z = backend.cast(Z, dtype=Z.dtype)
+
+ anticomm = anticommutator(X, I)
+ backend.assert_allclose(anticomm, 2 * X)
+
+ anticomm = anticommutator(X, X)
+ backend.assert_allclose(anticomm, 2 * I)
+
+ anticomm = anticommutator(X, Y)
+ backend.assert_allclose(anticomm, 0.0)
+
+ anticomm = anticommutator(X, Z)
+ backend.assert_allclose(anticomm, 0.0)
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_partial_trace(backend, density_matrix):
+ with pytest.raises(TypeError):
+ state = np.random.rand(2, 2, 2).astype(complex)
+ state += 1j * np.random.rand(2, 2, 2)
+ state = backend.cast(state, dtype=state.dtype)
+ test = partial_trace(state, 1, backend=backend)
+ with pytest.raises(ValueError):
+ state = (
+ random_density_matrix(5, backend=backend)
+ if density_matrix
+ else random_statevector(5, backend=backend)
+ )
+ test = partial_trace(state, 1, backend=backend)
+
+ nqubits = 4
+
+ circuit = Circuit(nqubits, density_matrix=density_matrix)
+ circuit.add(gates.H(0))
+ circuit.add(gates.CNOT(0, qubit + 1) for qubit in range(1, nqubits - 1))
+ state = backend.execute_circuit(circuit).state()
+
+ traced = partial_trace(state, (1, 2, 3), backend=backend)
+
+ Id = backend.identity_density_matrix(1, normalize=True)
+
+ backend.assert_allclose(traced, Id)
diff --git a/tests/test_quantum_info_quantum_networks.py b/tests/test_quantum_info_quantum_networks.py
new file mode 100644
index 000000000..daa8ab53f
--- /dev/null
+++ b/tests/test_quantum_info_quantum_networks.py
@@ -0,0 +1,520 @@
+"""Tests for quantum_info.quantum_networks submodule"""
+
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.quantum_info.quantum_networks import (
+ IdentityChannel,
+ QuantumChannel,
+ QuantumComb,
+ QuantumNetwork,
+ TraceOperation,
+ link_product,
+)
+from qibo.quantum_info.random_ensembles import (
+ random_density_matrix,
+ random_gaussian_matrix,
+ random_unitary,
+)
+
+
+def test_errors(backend):
+ lamb = float(np.random.rand())
+ channel = gates.DepolarizingChannel(0, lamb)
+ nqubits = len(channel.target_qubits)
+ dims = 2**nqubits
+ partition = (dims, dims)
+ network = QuantumNetwork.from_operator(
+ channel.to_choi(backend=backend), partition, backend=backend
+ )
+ quantum_comb = QuantumComb.from_operator(
+ channel.to_choi(backend=backend), partition, backend=backend
+ )
+ quantum_channel = QuantumChannel.from_operator(
+ channel.to_choi(backend=backend), partition, backend=backend
+ )
+
+ state = random_density_matrix(dims, backend=backend)
+ network_state = QuantumNetwork(state, (1, 2), backend=backend)
+
+ matrix = random_density_matrix(2**3, backend=backend)
+ net = QuantumNetwork(matrix, (2,) * 3, backend=backend)
+
+ comb_partition = (2,) * 4
+ comb_sys_out = (False, True) * 2
+ comb = random_density_matrix(2**4, backend=backend)
+ comb_choi = QuantumNetwork(
+ comb, comb_partition, system_input=comb_sys_out, backend=backend
+ )
+
+ with pytest.raises(TypeError):
+ QuantumNetwork(channel.to_choi(backend=backend), partition=True)
+
+ with pytest.raises(ValueError):
+ QuantumNetwork(channel.to_choi(backend=backend), partition=(1, "2"))
+
+ with pytest.raises(ValueError):
+ QuantumNetwork(channel.to_choi(backend=backend), partition=(-1, 2))
+
+ with pytest.raises(ValueError):
+ QuantumNetwork(
+ channel.to_choi(backend=backend), partition=(1, 2), system_input=(1, 2, 3)
+ )
+
+ with pytest.raises(TypeError):
+ QuantumNetwork(channel.to_choi(backend=backend), partition=(1, 2), pure="True")
+
+ with pytest.raises(TypeError):
+ QuantumNetwork(channel.to_choi(backend=backend), partition=1, pure=True)
+
+ with pytest.raises(ValueError):
+ network.is_hermitian(precision_tol=-1e-8)
+
+ with pytest.raises(ValueError):
+ network.is_positive_semidefinite(precision_tol=-1e-8)
+
+ with pytest.raises(ValueError):
+ quantum_comb.is_causal(precision_tol=-1e-8)
+
+ with pytest.raises(ValueError):
+ quantum_channel.is_unital(precision_tol=-1e-8)
+
+ with pytest.raises(TypeError):
+ network + 1
+
+ with pytest.raises(ValueError):
+ network + network_state
+
+ with pytest.raises(TypeError):
+ network * "1"
+
+ with pytest.raises(TypeError):
+ network / "1"
+
+ network_2 = network.copy()
+ with pytest.raises(ValueError):
+ network_2.system_input = (False,)
+ network += network_2
+
+ # Multiplying QuantumNetwork with non-QuantumNetwork
+ with pytest.raises(TypeError):
+ network @ network.operator(backend=backend)
+
+ # Linking QuantumNetwork with non-QuantumNetwork
+ with pytest.raises(TypeError):
+ network.link_product(network.operator(backend=backend))
+
+ with pytest.raises(TypeError):
+ network.link_product(network, subscripts=True)
+
+ with pytest.raises(NotImplementedError):
+ net @ net
+
+ with pytest.raises(NotImplementedError):
+ net @ network
+
+ with pytest.raises(ValueError):
+ network @ net
+
+ with pytest.raises(ValueError):
+ network @ QuantumNetwork(comb, (16, 16), pure=True, backend=backend)
+
+ with pytest.raises(ValueError):
+ comb_choi @ QuantumNetwork(comb, (16, 16), pure=True, backend=backend)
+
+ with pytest.raises(ValueError):
+ comb_choi @ net
+
+ with pytest.raises(ValueError):
+ QuantumNetwork(matrix, (1, 2), backend=backend)
+
+ with pytest.raises(ValueError):
+ QuantumNetwork(matrix, (1, 1), pure=True, backend=backend)
+
+ with pytest.raises(ValueError):
+ QuantumNetwork.from_operator(matrix, (1, 2), pure=True, backend=backend)
+
+ vec = np.random.rand(4)
+ vec = backend.cast(vec, dtype=vec.dtype)
+ vec = backend.cast(vec, dtype=vec.dtype)
+ with pytest.raises(ValueError):
+ QuantumNetwork.from_operator(vec, backend=backend)
+
+ with pytest.raises(ValueError):
+ QuantumComb.from_operator(vec, pure=True, backend=backend)
+
+ with pytest.raises(ValueError):
+ QuantumChannel(matrix, partition=(2, 2, 2), pure=True, backend=backend)
+
+ with pytest.raises(TypeError):
+ link_product(1, quantum_comb, backend=backend)
+
+ with pytest.raises(TypeError):
+ link_product("ij, i", quantum_comb, matrix, backend=backend)
+
+ # raise warning
+ link_product("ii", quantum_channel, backend=backend)
+ link_product("ij, kj", network_state, quantum_channel, backend=backend)
+ link_product("ij, jj", network_state, quantum_channel, backend=backend)
+ link_product(
+ "ij, jj, jj", network_state, quantum_channel, quantum_channel, backend=backend
+ )
+
+
+def test_class_methods(backend):
+ matrix = random_density_matrix(2**2, backend=backend)
+ with pytest.raises(ValueError):
+ QuantumNetwork._operator_to_tensor(matrix, (3,))
+
+
+def test_operational_logic(backend):
+ lamb = float(np.random.rand())
+ channel = gates.DepolarizingChannel(0, lamb)
+ nqubits = len(channel.target_qubits)
+ dims = 2**nqubits
+ partition = (dims, dims)
+ network = QuantumNetwork.from_operator(
+ channel.to_choi(backend=backend), partition, backend=backend
+ )
+
+ state = random_density_matrix(dims, backend=backend)
+ network_state_pure = QuantumNetwork(state, (2, 2), pure=True, backend=backend)
+
+ # Sum with itself has to match multiplying by int
+ backend.assert_allclose(
+ (network + network).operator(backend=backend),
+ (2 * network).operator(backend=backend),
+ )
+ backend.assert_allclose(
+ (network_state_pure + network_state_pure).operator(backend=backend),
+ (2 * network_state_pure).operator(full=True, backend=backend),
+ )
+
+ # Sum with itself has to match multiplying by float
+ backend.assert_allclose(
+ (network + network).operator(backend=backend),
+ (2.0 * network).operator(backend=backend),
+ )
+ backend.assert_allclose(
+ (network_state_pure + network_state_pure).operator(backend=backend),
+ (2.0 * network_state_pure).operator(full=True, backend=backend),
+ )
+
+ # Multiplying and dividing by same scalar has to bring back to original network
+ backend.assert_allclose(
+ ((2.0 * network) / 2).operator(backend=backend),
+ network.operator(backend=backend),
+ )
+
+ unitary = random_unitary(dims, backend=backend)
+ network_unitary = QuantumNetwork(unitary, (dims, dims), pure=True, backend=backend)
+ backend.assert_allclose(
+ (network_unitary / 2).operator(backend=backend), unitary / np.sqrt(2), atol=1e-5
+ )
+
+ # Complex conjugate of a network has to match the complex conjugate of the operator
+ backend.assert_allclose(
+ network.conj().operator(backend=backend),
+ backend.np.conj(network.operator(backend=backend)),
+ )
+
+
+def test_parameters(backend):
+ lamb = float(np.random.rand())
+ channel = gates.DepolarizingChannel(0, lamb)
+
+ nqubits = len(channel.target_qubits)
+ dims = 2**nqubits
+ partition = (dims, dims)
+
+ choi = channel.to_choi(backend=backend)
+
+ network = QuantumNetwork.from_operator(choi, partition, backend=backend)
+ quantum_comb = QuantumComb.from_operator(choi, partition, backend=backend)
+ quantum_channel = QuantumChannel.from_operator(
+ choi, partition, backend=backend, inverse=True
+ )
+
+ rand = random_density_matrix(dims**2, backend=backend)
+ non_channel = QuantumChannel.from_operator(
+ rand, partition, backend=backend, inverse=True
+ )
+
+ backend.assert_allclose(network.operator(backend=backend).shape, (2, 2, 2, 2))
+ backend.assert_allclose(network.dims, 4)
+ backend.assert_allclose(network.partition, partition)
+ backend.assert_allclose(network.system_input, (True, False))
+
+ assert network.is_hermitian()
+ assert network.is_positive_semidefinite()
+ assert quantum_comb.is_causal()
+ assert quantum_channel.is_unital()
+ assert quantum_channel.is_channel()
+
+ # Test non-unital and non_causal
+ assert not non_channel.is_causal()
+ assert not non_channel.is_unital()
+
+
+def test_with_states(backend):
+ nqubits = 1
+ dims = 2**nqubits
+
+ state = random_density_matrix(dims, backend=backend)
+ network_state = QuantumChannel.from_operator(state, backend=backend)
+
+ lamb = float(np.random.rand())
+ channel = gates.DepolarizingChannel(0, lamb)
+ network_channel = QuantumChannel.from_operator(
+ channel.to_choi(backend=backend), (dims, dims), backend=backend, inverse=True
+ )
+
+ state_output = channel.apply_density_matrix(backend, state, nqubits)
+ state_output_network = network_channel.apply(state)
+ state_output_link = network_state.link_product("ij,jk -> ik", network_channel)
+
+ backend.assert_allclose(state_output_network, state_output)
+ backend.assert_allclose(state_output_link.matrix(backend=backend), state_output)
+
+ assert network_state.is_hermitian()
+ assert network_state.is_positive_semidefinite()
+
+
+@pytest.mark.parametrize("subscript", ["jk,kl->jl", "jk,lj->lk"])
+def test_with_unitaries(backend, subscript):
+ nqubits = 2
+ dims = 2**nqubits
+
+ unitary_1 = random_unitary(dims, backend=backend)
+ unitary_2 = random_unitary(dims, backend=backend)
+
+ network_1 = QuantumComb.from_operator(
+ unitary_1, (dims, dims), pure=True, backend=backend, inverse=True
+ )
+ network_2 = QuantumComb.from_operator(
+ unitary_2, (dims, dims), pure=True, backend=backend, inverse=True
+ )
+ network_3 = QuantumComb.from_operator(
+ unitary_2 @ unitary_1, (dims, dims), pure=True, backend=backend, inverse=True
+ )
+ network_4 = QuantumComb.from_operator(
+ unitary_1 @ unitary_2, (dims, dims), pure=True, backend=backend, inverse=True
+ )
+
+ test = network_1.link_product(subscript, network_2).full(
+ backend=backend, update=True
+ )
+
+ if subscript[1] == subscript[3]:
+ backend.assert_allclose(
+ test, network_3.full(backend=backend, update=True), atol=1e-8
+ )
+
+ backend.assert_allclose(
+ test, (network_1 @ network_2).full(backend=backend, update=True), atol=1e-8
+ )
+
+ if subscript[0] == subscript[4]:
+ backend.assert_allclose(test, network_4.full(backend))
+
+ backend.assert_allclose(test, (network_2 @ network_1).full(backend=backend))
+
+ # Check properties for pure states
+ assert network_1.is_causal()
+ assert network_1.is_hermitian()
+ assert network_1.is_positive_semidefinite()
+
+
+def test_with_comb(backend):
+ subscript = "jklm,kl->jm"
+ comb_partition = (2,) * 4
+ channel_partition = (2,) * 2
+ comb_sys_in = (False, True) * 2
+ channel_sys_in = (False, True)
+
+ rand_choi = random_density_matrix(4**2, backend=backend)
+ unitary_1 = random_unitary(4, backend=backend)
+ unitary_2 = random_unitary(4, backend=backend)
+ non_channel = QuantumNetwork.from_operator(
+ rand_choi,
+ (2, 2, 2, 2),
+ system_input=(True, True, False, False),
+ backend=backend,
+ )
+ unitary_channel = QuantumNetwork.from_operator(
+ unitary_1,
+ (2, 2, 2, 2),
+ system_input=(True, True, False, False),
+ pure=True,
+ backend=backend,
+ )
+ unitary_channel2 = QuantumNetwork.from_operator(
+ unitary_2,
+ (2, 2, 2, 2),
+ system_input=(True, True, False, False),
+ pure=True,
+ backend=backend,
+ )
+
+ non_comb = link_product(
+ "ij kl, km on -> jl mn", non_channel, unitary_channel, backend=backend
+ )
+ non_comb = QuantumComb(
+ non_comb.full(backend=backend),
+ (2, 2, 2, 2),
+ system_input=(True, False, True, False),
+ backend=backend,
+ )
+ two_comb = link_product(
+ "ij kl, km on, i, o",
+ unitary_channel,
+ unitary_channel2,
+ TraceOperation(2, backend=backend),
+ TraceOperation(2, backend=backend),
+ backend=backend,
+ )
+ two_comb = QuantumComb(
+ two_comb.full(backend=backend),
+ (2, 2, 2, 2),
+ system_input=(True, False, True, False),
+ backend=backend,
+ )
+
+ comb = random_density_matrix(2**4, backend=backend)
+ channel = random_density_matrix(2**2, backend=backend)
+
+ comb_choi = QuantumNetwork.from_operator(
+ comb, comb_partition, system_input=comb_sys_in, backend=backend
+ )
+ channel_choi = QuantumNetwork.from_operator(
+ channel, channel_partition, system_input=channel_sys_in, backend=backend
+ )
+
+ test = comb_choi.link_product(subscript, channel_choi).full(
+ update=True, backend=backend
+ )
+ channel_choi2 = comb_choi @ channel_choi
+
+ backend.assert_allclose(test, channel_choi2.full(backend), atol=1e-5)
+
+ assert non_comb.is_hermitian()
+ assert not non_comb.is_causal()
+
+ assert two_comb.is_hermitian()
+ assert two_comb.is_causal()
+
+
+def test_apply(backend):
+ nqubits = 2
+ dims = 2**nqubits
+
+ state = random_density_matrix(dims, backend=backend)
+ unitary = random_unitary(dims, backend=backend)
+ network = QuantumChannel.from_operator(
+ unitary, (dims, dims), pure=True, backend=backend, inverse=True
+ )
+
+ applied = network.apply(state)
+ target = unitary @ state @ backend.np.conj(unitary).T
+
+ backend.assert_allclose(applied, target, atol=1e-8)
+
+
+def test_non_hermitian_and_prints(backend):
+ nqubits = 2
+ dims = 2**nqubits
+
+ matrix = random_gaussian_matrix(dims**2, backend=backend)
+ network = QuantumNetwork.from_operator(
+ matrix, (dims, dims), pure=False, backend=backend
+ )
+
+ assert not network.is_hermitian()
+ assert not network.is_positive_semidefinite()
+
+ assert network.__str__() == "J[┍4┑, ┕4┙]"
+
+
+def test_uility_function():
+ # _order_tensor2operator should convert
+ # (a0,a1,b0,b1,...) to (a0,b0,..., a1,b1,...)
+ old_shape = (0, 10, 1, 11, 2, 12, 3, 13)
+ test_ls = np.ones(old_shape)
+ n = len(test_ls.shape) // 2
+
+ order2op = QuantumNetwork._order_tensor_to_operator(n)
+ order2tensor = QuantumNetwork._order_operator_to_tensor(n)
+
+ new_shape = test_ls.transpose(order2op).shape
+ for i in range(n):
+ assert (new_shape[i] - new_shape[i + n]) == -10
+
+ assert tuple(test_ls.transpose(order2op).transpose(order2tensor).shape) == old_shape
+
+
+def test_predefined(backend):
+ tr_ch = TraceOperation(2, backend=backend)
+
+ id_ch = IdentityChannel(2, backend=backend)
+ id_mat = id_ch.matrix(backend=backend)
+
+ backend.assert_allclose(
+ id_mat,
+ backend.cast(
+ np.array([[1, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 1]]),
+ dtype=id_mat.dtype,
+ ),
+ atol=1e-8,
+ )
+
+ traced = link_product("ij,j", id_ch, tr_ch, backend=backend)
+
+ backend.assert_allclose(
+ tr_ch.matrix(backend=backend), traced.matrix(backend=backend), atol=1e-8
+ )
+
+
+def test_default_construction(backend):
+ vec = np.random.rand(4).reshape([4, 1])
+ mat = np.random.rand(16).reshape([2, 2, 2, 2])
+ tensor = np.random.rand(16).reshape([4, 4])
+ vec = backend.cast(vec, dtype=vec.dtype)
+ mat = backend.cast(mat, dtype=mat.dtype)
+ tensor = backend.cast(tensor, dtype=tensor.dtype)
+ vec = backend.cast(vec, dtype=vec.dtype)
+ mat = backend.cast(mat, dtype=mat.dtype)
+ tensor = backend.cast(tensor, dtype=tensor.dtype)
+ network = QuantumNetwork.from_operator(vec, pure=True, backend=backend)
+ assert network.partition == (4, 1)
+ assert network.system_input == (True, False)
+ comb1 = QuantumComb.from_operator(vec, (4, 1), pure=True, backend=backend)
+ assert comb1.system_input == (True, False)
+ comb2 = QuantumComb.from_operator(vec, pure=True, backend=backend)
+ assert comb2.partition == (4, 1)
+ assert comb2.system_input == (True, False)
+ comb3 = QuantumComb.from_operator(mat, pure=False, backend=backend)
+ assert comb3.partition == (2, 2)
+ assert comb3.system_input == (True, False)
+ comb3 = QuantumComb(vec, system_input=(True, True), pure=True, backend=backend)
+ assert comb3.partition == (4, 1)
+ assert comb3.system_input == (True, False)
+ channel1 = QuantumChannel.from_operator(vec, pure=True, backend=backend)
+ assert channel1.partition == (4, 1)
+ assert channel1.system_input == (True, False)
+ channel2 = QuantumChannel(
+ vec, partition=4, system_input=True, pure=True, backend=backend
+ )
+ assert channel2.partition == (4, 1)
+ assert channel2.system_input == (True, False)
+ channel3 = QuantumChannel(vec, partition=4, pure=True, backend=backend)
+ assert channel3.partition == (1, 4)
+ assert channel3.system_input == (True, False)
+ channel4 = QuantumChannel(
+ vec, partition=4, system_input=False, pure=True, backend=backend
+ )
+ assert channel4.partition == (1, 4)
+ assert channel4.system_input == (True, False)
+ channel5 = QuantumChannel(tensor, pure=False, backend=backend)
+ assert channel5.partition == (2, 2)
+ assert channel5.system_input == (True, False)
diff --git a/tests/test_quantum_info_random.py b/tests/test_quantum_info_random.py
new file mode 100644
index 000000000..d522f1458
--- /dev/null
+++ b/tests/test_quantum_info_random.py
@@ -0,0 +1,632 @@
+"""Tests for the quantum_info.random_ensembles module."""
+
+from functools import reduce
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.metrics import purity
+from qibo.quantum_info.random_ensembles import (
+ random_clifford,
+ random_density_matrix,
+ random_gaussian_matrix,
+ random_hermitian,
+ random_pauli,
+ random_pauli_hamiltonian,
+ random_quantum_channel,
+ random_statevector,
+ random_stochastic_matrix,
+ random_unitary,
+ uniform_sampling_U3,
+)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_uniform_sampling_U3(backend, seed):
+ with pytest.raises(TypeError):
+ uniform_sampling_U3("1", seed=seed, backend=backend)
+ with pytest.raises(ValueError):
+ uniform_sampling_U3(0, seed=seed, backend=backend)
+ with pytest.raises(TypeError):
+ uniform_sampling_U3(2, seed="1", backend=backend)
+
+ X = backend.cast(matrices.X, dtype=matrices.X.dtype)
+ Y = backend.cast(matrices.Y, dtype=matrices.Y.dtype)
+ Z = backend.cast(matrices.Z, dtype=matrices.Z.dtype)
+
+ ngates = int(1e3)
+ phases = uniform_sampling_U3(ngates, seed=seed, backend=backend)
+
+ # expectation values in the 3 directions should be the same
+ expectation_values = []
+ for row in phases:
+ row = [float(phase) for phase in row]
+ circuit = Circuit(1)
+ circuit.add(gates.U3(0, *row))
+ state = backend.execute_circuit(circuit).state()
+
+ expectation_values.append(
+ [
+ backend.np.conj(state) @ X @ state,
+ backend.np.conj(state) @ Y @ state,
+ backend.np.conj(state) @ Z @ state,
+ ]
+ )
+ expectation_values = backend.cast(expectation_values)
+
+ expectation_values = backend.np.mean(expectation_values, axis=0)
+
+ backend.assert_allclose(expectation_values[0], expectation_values[1], atol=1e-1)
+ backend.assert_allclose(expectation_values[0], expectation_values[2], atol=1e-1)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_random_gaussian_matrix(backend, seed):
+ with pytest.raises(TypeError):
+ dims = np.array([2])
+ random_gaussian_matrix(dims, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ rank = np.array([2])
+ random_gaussian_matrix(dims, rank, backend=backend)
+ with pytest.raises(ValueError):
+ dims = -1
+ random_gaussian_matrix(dims, backend=backend)
+ with pytest.raises(ValueError):
+ dims, rank = 2, 4
+ random_gaussian_matrix(dims, rank, backend=backend)
+ with pytest.raises(ValueError):
+ dims, rank = 2, -1
+ random_gaussian_matrix(dims, rank, backend=backend)
+ with pytest.raises(ValueError):
+ dims, stddev = 2, -1
+ random_gaussian_matrix(dims, stddev=stddev, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_gaussian_matrix(dims, seed=0.1, backend=backend)
+
+ # just runs the function with no tests
+ random_gaussian_matrix(4, seed=seed, backend=backend)
+
+
+def test_random_hermitian(backend):
+ with pytest.raises(TypeError):
+ dims = 2
+ random_hermitian(dims, semidefinite="True", backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_hermitian(dims, normalize="True", backend=backend)
+ with pytest.raises(TypeError):
+ dims = np.array([1])
+ random_hermitian(dims, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 0
+ random_hermitian(dims, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_hermitian(dims, seed=0.1, backend=backend)
+
+ # test if function returns Hermitian operator
+ dims = 4
+ matrix = random_hermitian(dims, backend=backend)
+ matrix_dagger = backend.np.conj(matrix).T
+ norm = float(backend.calculate_norm_density_matrix(matrix - matrix_dagger, order=2))
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+
+ # test if function returns semidefinite Hermitian operator
+ dims = 4
+ matrix = random_hermitian(dims, semidefinite=True, backend=backend)
+ matrix_dagger = backend.np.conj(matrix).T
+ norm = float(backend.calculate_norm_density_matrix(matrix - matrix_dagger, order=2))
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+
+ eigenvalues = np.linalg.eigvalsh(backend.to_numpy(matrix))
+ eigenvalues = np.real(eigenvalues)
+ backend.assert_allclose(all(eigenvalues >= 0), True)
+
+ # test if function returns normalized Hermitian operator
+ dims = 4
+ matrix = random_hermitian(dims, normalize=True, backend=backend)
+ matrix_dagger = backend.np.conj(matrix).T
+ norm = float(backend.calculate_norm_density_matrix(matrix - matrix_dagger, order=2))
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+
+ eigenvalues = np.linalg.eigvalsh(backend.to_numpy(matrix))
+ eigenvalues = np.real(eigenvalues)
+ backend.assert_allclose(all(eigenvalues <= 1), True)
+
+ # test if function returns normalized and semidefinite Hermitian operator
+ dims = 4
+ matrix = random_hermitian(dims, semidefinite=True, normalize=True, backend=backend)
+ matrix_dagger = backend.np.conj(matrix).T
+ norm = float(backend.calculate_norm(matrix - matrix_dagger, order=2))
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+
+ eigenvalues = np.linalg.eigvalsh(backend.to_numpy(matrix))
+ eigenvalues = np.real(eigenvalues)
+ backend.assert_allclose(all(eigenvalues >= 0), True)
+ backend.assert_allclose(all(eigenvalues <= 1), True)
+
+
+@pytest.mark.parametrize("measure", [None, "haar"])
+def test_random_unitary(backend, measure):
+ with pytest.raises(TypeError):
+ dims = np.array([1])
+ random_unitary(dims, measure=measure, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_unitary(dims, measure=1, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 0
+ random_unitary(dims, measure=measure, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 2
+ random_unitary(dims, measure="gaussian", backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_unitary(dims=2, measure=measure, seed=0.1, backend=backend)
+
+ # tests if operator is unitary (measure == "haar")
+ dims = 4
+ matrix = random_unitary(dims, measure=measure, backend=backend)
+ matrix_dagger = backend.np.conj(matrix).T
+ matrix_inv = (
+ backend.np.inverse(matrix)
+ if backend.name == "pytorch"
+ else np.linalg.inv(matrix)
+ )
+ norm = float(
+ backend.calculate_norm_density_matrix(matrix_inv - matrix_dagger, order=2)
+ )
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+
+
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("rank", [None, 4])
+@pytest.mark.parametrize("measure", [None, "haar", "bcsz"])
+@pytest.mark.parametrize(
+ "representation",
+ [
+ "chi",
+ "chi-IZXY",
+ "choi",
+ "kraus",
+ "liouville",
+ "pauli",
+ "pauli-IZXY",
+ "stinespring",
+ ],
+)
+def test_random_quantum_channel(backend, representation, measure, rank, order):
+ with pytest.raises(TypeError):
+ test = random_quantum_channel(4, representation=True, backend=backend)
+ with pytest.raises(ValueError):
+ test = random_quantum_channel(4, representation="Choi", backend=backend)
+ with pytest.raises(NotImplementedError):
+ test = random_quantum_channel(4, measure="bcsz", order="system")
+
+ # All subroutines are already tested elsewhere,
+ # so here we only execute them once for coverage
+ random_quantum_channel(
+ 4,
+ rank=rank,
+ representation=representation,
+ measure=measure,
+ order=order,
+ backend=backend,
+ )
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_random_statevector(backend, seed):
+ with pytest.raises(TypeError):
+ dims = "10"
+ random_statevector(dims, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 0
+ random_statevector(dims, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_statevector(dims, seed=0.1, backend=backend)
+
+ # tests if random statevector is a pure state
+ dims = 4
+ state = random_statevector(dims, seed=seed, backend=backend)
+ backend.assert_allclose(
+ abs(purity(state, backend=backend) - 1.0) < PRECISION_TOL, True
+ )
+
+
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("basis", [None, "pauli-IXYZ", "pauli-IZXY"])
+@pytest.mark.parametrize("metric", ["hilbert-schmidt", "ginibre", "bures"])
+@pytest.mark.parametrize("pure", [False, True])
+@pytest.mark.parametrize("dims", [2, 4])
+def test_random_density_matrix(backend, dims, pure, metric, basis, normalize):
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=np.array([1]), backend=backend)
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=0, backend=backend)
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=2, rank=np.array([1]), backend=backend)
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=2, rank=3, backend=backend)
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=2, rank=0, backend=backend)
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=2, pure="True", backend=backend)
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=2, metric=1, backend=backend)
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=2, metric="gaussian", backend=backend)
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=2, metric=metric, basis=True)
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=2, metric=metric, basis="Pauli")
+ with pytest.raises(TypeError):
+ test = random_density_matrix(dims=2, metric=metric, normalize="True")
+ with pytest.raises(TypeError):
+ random_density_matrix(dims=4, seed=0.1, backend=backend)
+
+ if basis is None and normalize is True:
+ with pytest.raises(ValueError):
+ test = random_density_matrix(dims=dims, normalize=True)
+ else:
+ norm_function = (
+ backend.calculate_norm_density_matrix
+ if basis is None
+ else backend.calculate_norm
+ )
+ state = random_density_matrix(
+ dims,
+ pure=pure,
+ metric=metric,
+ basis=basis,
+ normalize=normalize,
+ backend=backend,
+ )
+ if basis is None and normalize is False:
+ backend.assert_allclose(
+ np.real(np.trace(backend.to_numpy(state))) <= 1.0 + PRECISION_TOL, True
+ )
+ backend.assert_allclose(
+ np.real(np.trace(backend.to_numpy(state))) >= 1.0 - PRECISION_TOL, True
+ )
+ backend.assert_allclose(
+ purity(state, backend=backend) <= 1.0 + PRECISION_TOL, True
+ )
+ if pure is True:
+ backend.assert_allclose(
+ purity(state, backend=backend) >= 1.0 - PRECISION_TOL, True
+ )
+ norm = np.abs(
+ backend.to_numpy(
+ norm_function(state - backend.np.conj(state).T, order=2)
+ )
+ )
+ backend.assert_allclose(norm < PRECISION_TOL, True)
+ else:
+ normalization = 1.0 if normalize is False else 1.0 / np.sqrt(dims)
+ backend.assert_allclose(state[0], normalization)
+ assert all(
+ np.abs(backend.to_numpy(exp_value)) <= normalization
+ for exp_value in state[1:]
+ )
+
+
+@pytest.mark.parametrize("seed", [10])
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("return_circuit", [True, False])
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_random_clifford(backend, nqubits, return_circuit, density_matrix, seed):
+ with pytest.raises(TypeError):
+ test = random_clifford(
+ nqubits="1", return_circuit=return_circuit, backend=backend
+ )
+ with pytest.raises(ValueError):
+ test = random_clifford(
+ nqubits=-1, return_circuit=return_circuit, backend=backend
+ )
+ with pytest.raises(TypeError):
+ test = random_clifford(nqubits, return_circuit="True", backend=backend)
+ with pytest.raises(TypeError):
+ test = random_clifford(
+ nqubits, return_circuit=return_circuit, seed=0.1, backend=backend
+ )
+
+ result_single = matrices.Z @ matrices.H
+
+ result_two = np.kron(matrices.H, matrices.S) @ np.kron(matrices.S, matrices.Y)
+ result_two = np.kron(matrices.S @ matrices.X, matrices.I) @ result_two
+ result_two = matrices.CNOT @ matrices.CZ @ result_two
+
+ result = result_single if nqubits == 1 else result_two
+ result = backend.cast(result, dtype=result.dtype)
+
+ matrix = random_clifford(
+ nqubits,
+ return_circuit=return_circuit,
+ density_matrix=density_matrix,
+ seed=seed,
+ backend=backend,
+ )
+
+ if return_circuit:
+ matrix = matrix.unitary(backend)
+
+ backend.assert_allclose(matrix, result, atol=PRECISION_TOL)
+
+
+def test_random_pauli_errors(backend):
+ with pytest.raises(TypeError):
+ q, depth = "1", 1
+ random_pauli(q, depth, backend=backend)
+ with pytest.raises(ValueError):
+ q, depth = -1, 1
+ random_pauli(q, depth, backend=backend)
+ with pytest.raises(ValueError):
+ q = [0, 1, -3]
+ depth = 1
+ random_pauli(q, depth, backend=backend)
+ with pytest.raises(TypeError):
+ q, depth = 1, "1"
+ random_pauli(q, depth, backend=backend)
+ with pytest.raises(ValueError):
+ q, depth = 1, 0
+ random_pauli(q, depth, backend=backend)
+ with pytest.raises(TypeError):
+ q, depth, max_qubits = 1, 1, "1"
+ random_pauli(q, depth, max_qubits=max_qubits, backend=backend)
+ with pytest.raises(ValueError):
+ q, depth, max_qubits = 1, 1, 0
+ random_pauli(q, depth, max_qubits=max_qubits, backend=backend)
+ with pytest.raises(ValueError):
+ q, depth, max_qubits = 4, 1, 3
+ random_pauli(q, depth, max_qubits=max_qubits, backend=backend)
+ with pytest.raises(ValueError):
+ q = [0, 1, 3]
+ depth = 1
+ max_qubits = 2
+ random_pauli(q, depth, max_qubits=max_qubits, backend=backend)
+ with pytest.raises(TypeError):
+ q, depth = 1, 1
+ random_pauli(q, depth, return_circuit="True", backend=backend)
+ with pytest.raises(TypeError):
+ q, depth = 2, 1
+ subset = np.array([0, 1])
+ random_pauli(q, depth, subset=subset, backend=backend)
+ with pytest.raises(TypeError):
+ q, depth = 2, 1
+ subset = ["I", 0]
+ random_pauli(q, depth, subset=subset, backend=backend)
+ with pytest.raises(TypeError):
+ q, depth = 1, 1
+ random_pauli(q, depth, seed=0.1, backend=backend)
+
+
+def test_pauli_single(backend):
+ result = np.array([[1.0 + 0.0j, 0.0 + 0.0j], [0.0 + 0.0j, -1.0 + 0.0j]])
+ result = backend.cast(result, dtype=result.dtype)
+
+ matrix = random_pauli(0, 1, 1, seed=10, backend=backend).unitary(backend=backend)
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+
+ backend.assert_allclose(
+ np.abs(
+ backend.to_numpy(
+ backend.calculate_norm_density_matrix(matrix - result, order=2)
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+
+
+@pytest.mark.parametrize("qubits", [2, [0, 1], np.array([0, 1])])
+@pytest.mark.parametrize("depth", [2])
+@pytest.mark.parametrize("max_qubits", [None])
+@pytest.mark.parametrize("subset", [None, ["I", "X"]])
+@pytest.mark.parametrize("return_circuit", [True, False])
+@pytest.mark.parametrize("density_matrix", [False, True])
+@pytest.mark.parametrize("seed", [10])
+def test_random_pauli(
+ backend, qubits, depth, max_qubits, subset, return_circuit, density_matrix, seed
+):
+ result_complete_set = np.array(
+ [
+ [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j],
+ ]
+ )
+ result_complete_set = backend.cast(
+ result_complete_set, dtype=result_complete_set.dtype
+ )
+ result_subset = backend.identity_density_matrix(2, normalize=False)
+
+ matrix = random_pauli(
+ qubits, depth, max_qubits, subset, return_circuit, density_matrix, seed, backend
+ )
+
+ if return_circuit:
+ matrix = matrix.unitary(backend=backend)
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ if subset is None:
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ matrix - result_complete_set, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+ else:
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ matrix - result_subset, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+ else:
+ matrix = backend.np.transpose(matrix, (1, 0, 2, 3))
+ matrix = [reduce(backend.np.kron, row) for row in matrix]
+ matrix = reduce(backend.np.matmul, matrix)
+
+ if subset is None:
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ matrix - result_complete_set, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+ else:
+ backend.assert_allclose(
+ float(
+ backend.calculate_norm_density_matrix(
+ matrix - result_subset, order=2
+ )
+ )
+ < PRECISION_TOL,
+ True,
+ )
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("max_eigenvalue", [2, 3])
+@pytest.mark.parametrize("nqubits", [2, 3, 4])
+def test_random_pauli_hamiltonian(
+ backend, nqubits, max_eigenvalue, normalize, pauli_order
+):
+ with pytest.raises(TypeError):
+ random_pauli_hamiltonian(nqubits=[1], backend=backend)
+ with pytest.raises(ValueError):
+ random_pauli_hamiltonian(nqubits=0, backend=backend)
+ with pytest.raises(TypeError):
+ random_pauli_hamiltonian(nqubits=2, max_eigenvalue=[2], backend=backend)
+ with pytest.raises(TypeError):
+ random_pauli_hamiltonian(nqubits=2, normalize="True", backend=backend)
+ with pytest.raises(ValueError):
+ random_pauli_hamiltonian(
+ nqubits=2, normalize=True, max_eigenvalue=1, backend=backend
+ )
+ with pytest.raises(TypeError):
+ random_pauli_hamiltonian(
+ nqubits, max_eigenvalue=None, normalize=True, backend=backend
+ )
+
+ _, eigenvalues = random_pauli_hamiltonian(
+ nqubits, max_eigenvalue, normalize, pauli_order, backend=backend
+ )
+
+ if normalize is True:
+ backend.assert_allclose(
+ np.abs(backend.to_numpy(eigenvalues[0])) < PRECISION_TOL, True
+ )
+ backend.assert_allclose(
+ np.abs(backend.to_numpy(eigenvalues[1]) - 1) < PRECISION_TOL, True
+ )
+ backend.assert_allclose(
+ np.abs(backend.to_numpy(eigenvalues[-1]) - max_eigenvalue) < PRECISION_TOL,
+ True,
+ )
+
+
+def test_random_stochastic_matrix(backend):
+ with pytest.raises(TypeError):
+ dims = np.array([1])
+ random_stochastic_matrix(dims, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 0
+ random_stochastic_matrix(dims, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_stochastic_matrix(dims, bistochastic="True", backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_stochastic_matrix(dims, diagonally_dominant="True", backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ random_stochastic_matrix(dims, precision_tol=1, backend=backend)
+ with pytest.raises(ValueError):
+ dims, precision_tol = 2, -0.1
+ random_stochastic_matrix(dims, precision_tol=precision_tol, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 2
+ max_iterations = 1.1
+ random_stochastic_matrix(dims, max_iterations=max_iterations, backend=backend)
+ with pytest.raises(ValueError):
+ dims = 2
+ max_iterations = -1
+ random_stochastic_matrix(dims, max_iterations=max_iterations, backend=backend)
+ with pytest.raises(TypeError):
+ dims = 4
+ random_stochastic_matrix(dims, seed=0.1, backend=backend)
+
+ # tests if matrix is row-stochastic
+ dims = 4
+ matrix = random_stochastic_matrix(dims, backend=backend)
+ sum_rows = backend.np.sum(matrix, axis=1)
+
+ backend.assert_allclose(all(sum_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(sum_rows > 1 - PRECISION_TOL), True)
+
+ # tests if matrix is diagonally dominant
+ dims = 4
+ matrix = random_stochastic_matrix(
+ dims, diagonally_dominant=True, max_iterations=1000, backend=backend
+ )
+
+ sum_rows = backend.np.sum(matrix, axis=1)
+
+ backend.assert_allclose(all(sum_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(sum_rows > 1 - PRECISION_TOL), True)
+
+ backend.assert_allclose(all(2 * backend.np.diag(matrix) - sum_rows > 0), True)
+
+ # tests if matrix is bistochastic
+ dims = 4
+ matrix = random_stochastic_matrix(dims, bistochastic=True, backend=backend)
+ sum_rows = backend.np.sum(matrix, axis=1)
+ column_rows = backend.np.sum(matrix, axis=0)
+
+ backend.assert_allclose(all(sum_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(sum_rows > 1 - PRECISION_TOL), True)
+
+ backend.assert_allclose(all(column_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(column_rows > 1 - PRECISION_TOL), True)
+
+ # tests if matrix is bistochastic and diagonally dominant
+ dims = 4
+ matrix = random_stochastic_matrix(
+ dims,
+ bistochastic=True,
+ diagonally_dominant=True,
+ max_iterations=1000,
+ backend=backend,
+ )
+ sum_rows = backend.np.sum(matrix, axis=1)
+ column_rows = backend.np.sum(matrix, axis=0)
+
+ backend.assert_allclose(all(sum_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(sum_rows > 1 - PRECISION_TOL), True)
+
+ backend.assert_allclose(all(column_rows < 1 + PRECISION_TOL), True)
+ backend.assert_allclose(all(column_rows > 1 - PRECISION_TOL), True)
+
+ backend.assert_allclose(all(2 * backend.np.diag(matrix) - sum_rows > 0), True)
+ backend.assert_allclose(all(2 * backend.np.diag(matrix) - column_rows > 0), True)
+
+ # tests warning for max_iterations
+ dims = 4
+ random_stochastic_matrix(dims, bistochastic=True, max_iterations=1, backend=backend)
diff --git a/tests/test_quantum_info_superoperator_transformations.py b/tests/test_quantum_info_superoperator_transformations.py
new file mode 100644
index 000000000..d93db2e5e
--- /dev/null
+++ b/tests/test_quantum_info_superoperator_transformations.py
@@ -0,0 +1,1312 @@
+import numpy as np
+import pytest
+
+from qibo import matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.random_ensembles import random_density_matrix, random_statevector
+from qibo.quantum_info.superoperator_transformations import (
+ chi_to_choi,
+ chi_to_kraus,
+ chi_to_liouville,
+ chi_to_pauli,
+ chi_to_stinespring,
+ choi_to_chi,
+ choi_to_kraus,
+ choi_to_liouville,
+ choi_to_pauli,
+ choi_to_stinespring,
+ kraus_to_chi,
+ kraus_to_choi,
+ kraus_to_liouville,
+ kraus_to_pauli,
+ kraus_to_stinespring,
+ kraus_to_unitaries,
+ liouville_to_chi,
+ liouville_to_choi,
+ liouville_to_kraus,
+ liouville_to_pauli,
+ liouville_to_stinespring,
+ pauli_to_chi,
+ pauli_to_choi,
+ pauli_to_kraus,
+ pauli_to_liouville,
+ pauli_to_stinespring,
+ stinespring_to_chi,
+ stinespring_to_choi,
+ stinespring_to_kraus,
+ stinespring_to_liouville,
+ stinespring_to_pauli,
+ to_chi,
+ to_choi,
+ to_liouville,
+ to_pauli_liouville,
+ unvectorization,
+ vectorization,
+)
+
+
+@pytest.mark.parametrize("order", ["row", "column", "system"])
+@pytest.mark.parametrize("nqubits", [1, 2, 3])
+@pytest.mark.parametrize("statevector", [True, False])
+def test_vectorization(backend, nqubits, order, statevector):
+ with pytest.raises(TypeError):
+ x = np.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]])
+ x = backend.cast(x)
+ vectorization(x, backend=backend)
+ with pytest.raises(TypeError):
+ vectorization(np.array([]), backend=backend)
+ with pytest.raises(TypeError):
+ vectorization(random_statevector(4, backend=backend), order=1, backend=backend)
+ with pytest.raises(ValueError):
+ vectorization(
+ random_statevector(4, backend=backend), order="1", backend=backend
+ )
+
+ dim = 2**nqubits
+
+ if nqubits == 1:
+ if statevector:
+ if order == "system":
+ matrix_test = [0, 0, 0, 1, 0, 0, 2, 3, 0, 2, 0, 3, 4, 6, 6, 9]
+ else:
+ matrix_test = [0, 0, 0, 0, 0, 1, 2, 3, 0, 2, 4, 6, 0, 3, 6, 9]
+ else:
+ if order == "system" or order == "column":
+ matrix_test = [0, 2, 1, 3]
+ else:
+ matrix_test = [0, 1, 2, 3]
+ elif nqubits == 2:
+ if order == "row":
+ matrix_test = np.arange(dim**2)
+ elif order == "column":
+ matrix_test = np.arange(dim**2)
+ matrix_test = np.reshape(matrix_test, (dim, dim))
+ matrix_test = np.reshape(matrix_test, (1, -1), order="F")[0]
+ else:
+ matrix_test = [0, 4, 1, 5, 8, 12, 9, 13, 2, 6, 3, 7, 10, 14, 11, 15]
+ else:
+ if order == "row":
+ matrix_test = np.arange(dim**2)
+ elif order == "column":
+ matrix_test = np.arange(dim**2)
+ matrix_test = np.reshape(matrix_test, (dim, dim))
+ matrix_test = np.reshape(matrix_test, (1, -1), order="F")[0]
+ else:
+ matrix_test = [
+ 0,
+ 8,
+ 1,
+ 9,
+ 16,
+ 24,
+ 17,
+ 25,
+ 2,
+ 10,
+ 3,
+ 11,
+ 18,
+ 26,
+ 19,
+ 27,
+ 32,
+ 40,
+ 33,
+ 41,
+ 48,
+ 56,
+ 49,
+ 57,
+ 34,
+ 42,
+ 35,
+ 43,
+ 50,
+ 58,
+ 51,
+ 59,
+ 4,
+ 12,
+ 5,
+ 13,
+ 20,
+ 28,
+ 21,
+ 29,
+ 6,
+ 14,
+ 7,
+ 15,
+ 22,
+ 30,
+ 23,
+ 31,
+ 36,
+ 44,
+ 37,
+ 45,
+ 52,
+ 60,
+ 53,
+ 61,
+ 38,
+ 46,
+ 39,
+ 47,
+ 54,
+ 62,
+ 55,
+ 63,
+ ]
+ matrix_test = backend.cast(matrix_test)
+
+ dim = 2**nqubits
+ if statevector and nqubits == 1:
+ matrix = np.arange(dim**2)
+ else:
+ matrix = np.arange(dim**2).reshape((dim, dim))
+ matrix = vectorization(backend.cast(matrix), order, backend=backend)
+ backend.assert_allclose(matrix, matrix_test, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("order", ["row", "column", "system"])
+@pytest.mark.parametrize("nqubits", [2, 3, 4, 5])
+def test_unvectorization(backend, nqubits, order):
+ with pytest.raises(TypeError):
+ unvectorization(
+ random_density_matrix(2**nqubits, backend=backend), backend=backend
+ )
+ with pytest.raises(TypeError):
+ unvectorization(
+ random_statevector(4**nqubits, backend=backend), order=1, backend=backend
+ )
+ with pytest.raises(ValueError):
+ unvectorization(
+ random_statevector(4**2, backend=backend), order="1", backend=backend
+ )
+
+ dim = 2**nqubits
+ matrix_test = random_density_matrix(dim, backend=backend)
+
+ matrix = vectorization(matrix_test, order, backend)
+ matrix = unvectorization(matrix, order, backend)
+
+ backend.assert_allclose(matrix_test, matrix, atol=PRECISION_TOL)
+
+
+test_a0 = np.sqrt(0.4) * matrices.X
+test_a1 = np.sqrt(0.6) * matrices.Z
+test_kraus = [((0,), test_a0), ((0,), test_a1)]
+test_superop = np.array(
+ [
+ [0.6 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.4 + 0.0j],
+ [0.0 + 0.0j, -0.6 + 0.0j, 0.4 + 0.0j, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.4 + 0.0j, -0.6 + 0.0j, 0.0 + 0.0j],
+ [0.4 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.6 + 0.0j],
+ ]
+)
+test_non_CP = np.array(
+ [
+ [0.20031418, 0.37198771, 0.05642046, 0.37127765],
+ [0.15812476, 0.21466209, 0.41971479, 0.20749836],
+ [0.29408764, 0.01474688, 0.40320494, 0.28796054],
+ [0.17587069, 0.42052825, 0.16171658, 0.24188449],
+ ]
+)
+test_kraus_left = np.array(
+ [
+ [[-0.50326669, -0.50293148], [-0.49268667, -0.50104133]],
+ [[-0.54148474, 0.32931326], [0.64742923, -0.42329947]],
+ [[0.49035364, -0.62330138], [0.495577, -0.35419223]],
+ [[-0.46159531, -0.50010808], [0.30413594, 0.66657558]],
+ ]
+)
+test_kraus_right = np.array(
+ [
+ [[-0.41111026, -0.51035787], [-0.51635098, -0.55127567]],
+ [[0.13825514, -0.69451163], [0.69697805, -0.11296331]],
+ [[0.43827885, -0.49057101], [-0.48197173, 0.57875296]],
+ [[0.78726458, 0.12856331], [-0.12371948, -0.59023677]],
+ ]
+)
+test_coefficients = np.array([1.002719, 0.65635444, 0.43548, 0.21124177])
+
+test_stinespring = np.array(
+ [
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.63245553 + 0.0j, 0.0 + 0.0j],
+ [0.77459667 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [0.63245553 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, -0.77459667 + 0.0j, 0.0 + 0.0j],
+ ]
+)
+
+
+def pauli_superop(pauli_order):
+ elements = {"I": 2.0, "X": -0.4, "Y": -2.0, "Z": 0.4}
+ return np.diag([elements[p] for p in pauli_order])
+
+
+def chi_superop(pauli_order):
+ elements = {"I": 0, "X": 1.6, "Y": 0, "Z": 2.4}
+ return np.diag([elements[p] for p in pauli_order])
+
+
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_to_choi(backend, order):
+ test_a0_ = backend.cast(test_a0)
+ test_a1_ = backend.cast(test_a1)
+ choi_a0 = to_choi(test_a0_, order=order, backend=backend)
+ choi_a1 = to_choi(test_a1_, order=order, backend=backend)
+ choi = choi_a0 + choi_a1
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = np.reshape(test_superop, [2] * 4)
+ test_choi = np.swapaxes(test_choi, *axes)
+ test_choi = np.reshape(test_choi, (4, 4))
+
+ backend.assert_allclose(choi, test_choi, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_to_liouville(backend, order):
+ test_a0_ = backend.cast(test_a0)
+ test_a1_ = backend.cast(test_a1)
+ liouville_a0 = to_liouville(test_a0_, order=order, backend=backend)
+ liouville_a1 = to_liouville(test_a1_, order=order, backend=backend)
+ liouville = liouville_a0 + liouville_a1
+
+ backend.assert_allclose(liouville, test_superop, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_to_pauli_liouville(backend, normalize, order, pauli_order):
+ test_a0_ = backend.cast(test_a0)
+ test_a1_ = backend.cast(test_a1)
+ pauli_a0 = to_pauli_liouville(
+ test_a0_,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ pauli_a1 = to_pauli_liouville(
+ test_a1_,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ pauli = pauli_a0 + pauli_a1
+
+ test_pauli = pauli_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1.0 if not normalize else dim
+ test_pauli = backend.cast(test_pauli / aux, dtype=test_pauli.dtype)
+
+ backend.assert_allclose(pauli, test_pauli, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_to_chi(backend, normalize, order, pauli_order):
+ test_a0_ = backend.cast(test_a0)
+ test_a1_ = backend.cast(test_a1)
+ chi_a0 = to_chi(
+ test_a0_,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ chi_a1 = to_chi(
+ test_a1_,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+ chi = chi_a0 + chi_a1
+
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = 1.0 if not normalize else dim
+ test_chi = backend.cast(test_chi / aux, dtype=test_chi.dtype)
+
+ backend.assert_allclose(chi, test_chi, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_choi_to_liouville(backend, order, test_superop):
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.cast(
+ np.reshape(test_superop, [2] * 4).swapaxes(*axes).reshape([4, 4])
+ )
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ liouville = choi_to_liouville(test_choi, order=order, backend=backend)
+
+ backend.assert_allclose(liouville, test_superop, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_choi_to_pauli(backend, normalize, order, pauli_order, test_superop):
+ test_pauli = pauli_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1 if not normalize else dim
+
+ test_pauli = backend.cast(test_pauli, dtype=test_pauli.dtype)
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.swapaxes(
+ backend.np.reshape(test_superop * aux, [2] * 4), *axes
+ ).reshape([4, 4])
+
+ pauli_op = choi_to_pauli(
+ test_choi, normalize, order, pauli_order=pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_pauli, pauli_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_kraus_right", [test_kraus_right])
+@pytest.mark.parametrize("test_kraus_left", [test_kraus_left])
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("validate_cp", [False, True])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_choi_to_kraus(
+ backend, order, validate_cp, test_a0, test_a1, test_kraus_left, test_kraus_right
+):
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.cast(
+ np.reshape(test_superop, [2] * 4).swapaxes(*axes).reshape([4, 4])
+ )
+
+ with pytest.raises(TypeError):
+ choi_to_kraus(test_choi, str(PRECISION_TOL), backend=backend)
+ with pytest.raises(ValueError):
+ choi_to_kraus(test_choi, -1.0 * PRECISION_TOL, backend=backend)
+ with pytest.raises(TypeError):
+ choi_to_kraus(test_choi, validate_cp="True", backend=backend)
+
+ kraus_ops, _ = choi_to_kraus(
+ test_choi, order=order, validate_cp=validate_cp, backend=backend
+ )
+
+ a0 = kraus_ops[0]
+ a1 = kraus_ops[1]
+ a0 = backend.cast(a0, dtype=a0.dtype)
+ a1 = backend.cast(a1, dtype=a1.dtype)
+
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ test_kraus_left = backend.cast(test_kraus_left, dtype=backend.dtype)
+ test_kraus_right = backend.cast(test_kraus_right, dtype=backend.dtype)
+
+ state = random_density_matrix(2, backend=backend)
+
+ evolution_a0 = a0 @ state @ backend.np.conj(a0).T
+ evolution_a1 = a1 @ state @ backend.np.conj(a1).T
+
+ test_evolution_a0 = test_a0 @ state @ backend.np.conj(test_a0).T
+ test_evolution_a1 = test_a1 @ state @ backend.np.conj(test_a1).T
+
+ backend.assert_allclose(evolution_a0, test_evolution_a0, atol=2 * PRECISION_TOL)
+ backend.assert_allclose(evolution_a1, test_evolution_a1, atol=2 * PRECISION_TOL)
+
+ if validate_cp and order == "row":
+ (kraus_left, kraus_right), _ = choi_to_kraus(
+ test_non_CP, order=order, validate_cp=validate_cp, backend=backend
+ )
+
+ for test_left, left, test_right, right, test_coeff in zip(
+ test_kraus_left,
+ kraus_left,
+ test_kraus_right,
+ kraus_right,
+ test_coefficients,
+ ):
+ state = random_density_matrix(2, backend=backend)
+ evolution = left @ state @ backend.np.conj(right).T
+ test_evolution = (
+ test_coeff**2 * test_left @ state @ backend.np.conj(test_right).T
+ )
+
+ backend.assert_allclose(evolution, test_evolution, atol=2 * PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_choi_to_chi(backend, normalize, order, pauli_order, test_superop):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = 1 if not normalize else dim
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+ test_chi = backend.cast(test_chi, dtype=test_chi.dtype)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.swapaxes(
+ backend.np.reshape(test_superop * aux, [2] * 4), *axes
+ ).reshape([4, 4])
+
+ chi_matrix = choi_to_chi(
+ test_choi,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ backend.assert_allclose(test_chi, chi_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("test_stinespring", [test_stinespring])
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("validate_cp", [False, True])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_choi_to_stinespring(
+ backend, order, validate_cp, nqubits, test_stinespring, test_a0, test_a1
+):
+ if validate_cp is True:
+ with pytest.raises(NotImplementedError):
+ test = choi_to_stinespring(
+ test_non_CP,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ test_stinespring = backend.cast(test_stinespring, dtype=test_stinespring.dtype)
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = np.reshape(test_superop, [2] * 4).swapaxes(*axes).reshape([4, 4])
+ test_choi = backend.cast(test_choi, dtype=test_choi.dtype)
+ stinespring = choi_to_stinespring(
+ test_choi,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ backend=backend,
+ )
+ if nqubits is None:
+ nqubits = 1
+
+ v_0 = np.array([1, 0], dtype=complex)
+ v_0 = backend.cast(v_0, dtype=v_0.dtype)
+
+ state = random_density_matrix(2**nqubits, backend=backend)
+
+ # action of Kraus channel on state
+ state_final_test = test_a0 @ state @ test_a0 + test_a1 @ state @ test_a1
+
+ # action of stinespring channel on state + environment
+ stinespring = (
+ stinespring
+ @ backend.np.kron(state, backend.np.outer(v_0, v_0))
+ @ backend.np.conj(stinespring).T
+ )
+
+ # partial trace of the environment
+ stinespring = backend.np.reshape(stinespring, (2**nqubits, 2, 2**nqubits, 2))
+ stinespring = backend.np.swapaxes(stinespring, 1, 2)
+ state_final = np.zeros((2**nqubits, 2**nqubits), dtype=complex)
+ state_final = backend.cast(state_final, dtype=state_final.dtype)
+ for alpha in range(2):
+ vector_alpha = np.zeros(2, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ state_final = (
+ state_final + backend.np.conj(vector_alpha) @ stinespring @ vector_alpha
+ )
+
+ backend.assert_allclose(state_final, state_final_test, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_liouville_to_choi(backend, order, test_superop):
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ choi = liouville_to_choi(test_superop, order=order, backend=backend)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.reshape(test_superop, [2] * 4)
+ test_choi = backend.np.swapaxes(test_choi, *axes)
+ test_choi = backend.np.reshape(test_choi, (4, 4))
+
+ backend.assert_allclose(choi, test_choi, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column", "system"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_liouville_to_pauli(backend, normalize, order, pauli_order, test_superop):
+ test_pauli = pauli_superop(pauli_order)
+
+ with pytest.raises(ValueError):
+ liouville_to_pauli(test_superop[:-1, :], normalize, order, backend=backend)
+
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+ test_pauli = backend.cast(test_pauli)
+
+ pauli_op = liouville_to_pauli(
+ test_superop,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ backend.assert_allclose(test_pauli / aux, pauli_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_liouville_to_kraus(backend, order, test_a0, test_a1):
+ kraus_ops, _ = liouville_to_kraus(test_superop, order=order, backend=backend)
+
+ a0 = kraus_ops[0]
+ a1 = kraus_ops[1]
+ a0 = backend.cast(a0, dtype=a0.dtype)
+ a1 = backend.cast(a1, dtype=a1.dtype)
+
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ state = random_density_matrix(2, backend=backend)
+
+ evolution_a0 = a0 @ state @ backend.np.conj(a0).T
+ evolution_a1 = a1 @ state @ backend.np.conj(a1).T
+
+ test_evolution_a0 = test_a0 @ state @ backend.np.conj(test_a0).T
+ test_evolution_a1 = test_a1 @ state @ backend.np.conj(test_a1).T
+
+ backend.assert_allclose(evolution_a0, test_evolution_a0, atol=PRECISION_TOL)
+ backend.assert_allclose(evolution_a1, test_evolution_a1, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_liouville_to_chi(backend, normalize, order, pauli_order, test_superop):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=test_chi.dtype)
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ chi_matrix = liouville_to_chi(
+ test_superop, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_chi / aux, chi_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("test_stinespring", [test_stinespring])
+@pytest.mark.parametrize("nqubits", [1])
+@pytest.mark.parametrize("validate_cp", [False, True])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_liouville_to_stinespring(
+ backend,
+ order,
+ validate_cp,
+ nqubits,
+ test_stinespring,
+ test_superop,
+ test_a0,
+ test_a1,
+):
+ test_stinespring = backend.cast(test_stinespring, dtype=test_stinespring.dtype)
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = np.reshape(test_superop, [2] * 4).swapaxes(*axes).reshape([4, 4])
+ test_choi = backend.cast(test_choi, dtype=test_choi.dtype)
+ stinespring = liouville_to_stinespring(
+ test_superop,
+ order=order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ v_0 = np.array([1, 0], dtype=complex)
+ v_0 = backend.cast(v_0, dtype=v_0.dtype)
+
+ state = random_density_matrix(2**nqubits, backend=backend)
+
+ # action of Kraus channel on state
+ state_final_test = test_a0 @ state @ test_a0 + test_a1 @ state @ test_a1
+
+ # action of stinespring channel on state + environment
+ stinespring = (
+ stinespring
+ @ backend.np.kron(state, backend.np.outer(v_0, v_0))
+ @ backend.np.conj(stinespring).T
+ )
+
+ # partial trace of the environment
+ stinespring = backend.np.reshape(stinespring, (2**nqubits, 2, 2**nqubits, 2))
+ stinespring = backend.np.swapaxes(stinespring, 1, 2)
+ state_final = np.zeros((2**nqubits, 2**nqubits), dtype=complex)
+ state_final = backend.cast(state_final, dtype=state_final.dtype)
+ for alpha in range(2):
+ vector_alpha = np.zeros(2, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ state_final = (
+ state_final + backend.np.conj(vector_alpha) @ stinespring @ vector_alpha
+ )
+
+ backend.assert_allclose(state_final, state_final_test, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_kraus_to_choi(backend, order):
+ choi = kraus_to_choi(test_kraus, order=order, backend=backend)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = np.reshape(test_superop, [2] * 4).swapaxes(*axes).reshape([4, 4])
+ test_choi = backend.cast(test_choi, dtype=test_choi.dtype)
+
+ backend.assert_allclose(choi, test_choi, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_kraus_to_liouville(backend, order, test_superop):
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ liouville = kraus_to_liouville(test_kraus, order=order, backend=backend)
+
+ backend.assert_allclose(liouville, test_superop, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_kraus", [test_kraus])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_kraus_to_pauli(backend, normalize, order, pauli_order, test_kraus):
+ test_pauli = pauli_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ test_pauli = backend.cast(test_pauli, dtype=test_pauli.dtype)
+
+ pauli_op = kraus_to_pauli(
+ test_kraus, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_pauli / aux, pauli_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_kraus", [test_kraus])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_kraus_to_chi(backend, normalize, order, pauli_order, test_kraus):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=test_chi.dtype)
+
+ chi_matrix = kraus_to_chi(
+ test_kraus, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_chi / aux, chi_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("nqubits", [None, 1])
+def test_kraus_to_stinespring(backend, nqubits):
+ with pytest.raises(ValueError):
+ initial_state_env = random_statevector(4, backend=backend)
+ test = kraus_to_stinespring(
+ test_kraus,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+ with pytest.raises(ValueError):
+ initial_state_env = random_density_matrix(2, pure=True, backend=backend)
+ test = kraus_to_stinespring(
+ test_kraus,
+ nqubits=nqubits,
+ initial_state_env=initial_state_env,
+ backend=backend,
+ )
+
+ stinespring = kraus_to_stinespring(
+ test_kraus,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ backend.assert_allclose(stinespring, test_stinespring, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column", "system"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_pauli_to_liouville(backend, normalize, order, pauli_order, test_superop):
+ test_pauli = pauli_superop(pauli_order)
+
+ with pytest.raises(ValueError):
+ pauli_to_liouville(test_pauli[:-1, :], normalize, order, backend=backend)
+
+ dim = int(np.sqrt(test_superop.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+ test_pauli = backend.cast(test_pauli)
+
+ super_op = pauli_to_liouville(
+ test_pauli / aux, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_superop, super_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_pauli_to_choi(backend, normalize, order, pauli_order, test_superop):
+ test_pauli = pauli_superop(pauli_order)
+
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_pauli = backend.cast(test_pauli, dtype=backend.dtype)
+ test_superop = backend.cast(test_superop, dtype=backend.dtype)
+ choi_super_op = pauli_to_choi(
+ test_pauli / aux, normalize, order, pauli_order, backend=backend
+ )
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.swapaxes(backend.np.reshape(test_superop, [2] * 4), *axes)
+ test_choi = backend.np.reshape(test_choi, (4, 4))
+
+ backend.assert_allclose(test_choi, choi_super_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_pauli_to_kraus(backend, normalize, order, pauli_order, test_a0, test_a1):
+ test_pauli = pauli_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_pauli = backend.cast(test_pauli, dtype=backend.dtype)
+
+ kraus_ops, _ = pauli_to_kraus(
+ test_pauli / aux,
+ normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ a0 = kraus_ops[0]
+ a1 = kraus_ops[1]
+ a0 = backend.cast(a0, dtype=a0.dtype)
+ a1 = backend.cast(a1, dtype=a1.dtype)
+
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ state = random_density_matrix(2, backend=backend)
+
+ evolution_a0 = a0 @ state @ backend.np.conj(a0).T
+ evolution_a1 = a1 @ state @ backend.np.conj(a1).T
+
+ test_evolution_a0 = test_a0 @ state @ backend.np.conj(test_a0).T
+ test_evolution_a1 = test_a1 @ state @ backend.np.conj(test_a1).T
+
+ backend.assert_allclose(evolution_a0, test_evolution_a0, atol=2 * PRECISION_TOL)
+ backend.assert_allclose(evolution_a1, test_evolution_a1, atol=2 * PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_pauli_to_chi(backend, normalize, order, pauli_order):
+ test_pauli = pauli_superop(pauli_order)
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+ test_pauli = backend.cast(test_pauli / aux, dtype=backend.dtype)
+
+ chi_matrix = pauli_to_chi(
+ test_pauli, normalize, order, pauli_order, backend=backend
+ )
+
+ aux = 1.0 if not normalize else dim
+ backend.assert_allclose(test_chi / aux, chi_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("nqubits", [1])
+@pytest.mark.parametrize("validate_cp", [False, True])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_pauli_to_stinespring(
+ backend,
+ order,
+ validate_cp,
+ nqubits,
+ normalize,
+ pauli_order,
+ test_a0,
+ test_a1,
+):
+ test_pauli = pauli_superop(pauli_order)
+ test_pauli = backend.cast(test_pauli, dtype=backend.dtype)
+
+ dim = 2**nqubits
+ aux = dim**2 if normalize is False else dim
+ test_a0 = backend.cast(test_a0, dtype=backend.dtype)
+ test_a1 = backend.cast(test_a1, dtype=backend.dtype)
+
+ stinespring = pauli_to_stinespring(
+ test_pauli,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ v_0 = np.array([1, 0], dtype=complex)
+ v_0 = backend.cast(v_0, dtype=v_0.dtype)
+
+ state = random_density_matrix(2**nqubits, backend=backend)
+
+ # action of Kraus channel on state
+ state_final_test = test_a0 @ state @ test_a0 + test_a1 @ state @ test_a1
+
+ # action of stinespring channel on state + environment
+ stinespring = (
+ stinespring
+ @ backend.np.kron(state, backend.np.outer(v_0, v_0))
+ @ backend.np.conj(stinespring).T
+ )
+
+ # partial trace of the environment
+ stinespring = backend.np.reshape(stinespring, (2**nqubits, 2, 2**nqubits, 2))
+ stinespring = backend.np.swapaxes(stinespring, 1, 2)
+ state_final = np.zeros((2**nqubits, 2**nqubits), dtype=complex)
+ state_final = backend.cast(state_final, dtype=state_final.dtype)
+ for alpha in range(2):
+ vector_alpha = np.zeros(2, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ state_final = (
+ state_final + backend.np.conj(vector_alpha) @ stinespring @ vector_alpha
+ )
+
+ backend.assert_allclose(state_final, aux * state_final_test, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_chi_to_choi(backend, normalize, order, pauli_order, test_superop):
+ test_chi = chi_superop(pauli_order=pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+ test_superop = backend.cast(test_superop, dtype=backend.dtype)
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.swapaxes(
+ backend.np.reshape(test_superop, [2] * 4), *axes
+ ).reshape([4, 4])
+
+ choi_super_op = chi_to_choi(
+ test_chi / aux, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_choi, choi_super_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_chi_to_liouville(backend, normalize, order, pauli_order, test_superop):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+ test_superop = backend.cast(test_superop, dtype=backend.dtype)
+
+ super_op = chi_to_liouville(
+ test_chi / aux, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_superop, super_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_chi_to_pauli(backend, normalize, order, pauli_order):
+ test_pauli = pauli_superop(pauli_order)
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1.0 if normalize else dim**2
+
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+ test_pauli = backend.cast(test_pauli, dtype=backend.dtype)
+
+ pauli_op = chi_to_pauli(
+ test_chi / aux, normalize, order, pauli_order, backend=backend
+ )
+
+ backend.assert_allclose(test_pauli, pauli_op, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_chi_to_kraus(backend, normalize, order, pauli_order, test_a0, test_a1):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = dim**2 if not normalize else dim
+
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+
+ kraus_ops, _ = chi_to_kraus(
+ test_chi / aux, normalize, order=order, pauli_order=pauli_order, backend=backend
+ )
+
+ a0 = kraus_ops[0]
+ a1 = kraus_ops[1]
+ a0 = backend.cast(a0, dtype=a0.dtype)
+ a1 = backend.cast(a1, dtype=a1.dtype)
+
+ test_a0 = backend.cast(test_a0, dtype=test_a0.dtype)
+ test_a1 = backend.cast(test_a1, dtype=test_a1.dtype)
+
+ state = random_density_matrix(2, backend=backend)
+
+ evolution_a0 = a0 @ state @ backend.np.conj(a0).T
+ evolution_a1 = a1 @ state @ backend.np.conj(a1).T
+
+ test_evolution_a0 = test_a0 @ state @ backend.np.conj(test_a0).T
+ test_evolution_a1 = test_a1 @ state @ backend.np.conj(test_a1).T
+
+ backend.assert_allclose(evolution_a0, test_evolution_a0, atol=2 * PRECISION_TOL)
+ backend.assert_allclose(evolution_a1, test_evolution_a1, atol=2 * PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_a1", [test_a1])
+@pytest.mark.parametrize("test_a0", [test_a0])
+@pytest.mark.parametrize("nqubits", [1])
+@pytest.mark.parametrize("validate_cp", [False, True])
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+def test_chi_to_stinespring(
+ backend, normalize, order, pauli_order, validate_cp, nqubits, test_a0, test_a1
+):
+ test_chi = chi_superop(pauli_order)
+ test_chi = backend.cast(test_chi, dtype=backend.dtype)
+
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = dim**2 if not normalize else dim
+ test_a0 = backend.cast(test_a0, dtype=backend.dtype)
+ test_a1 = backend.cast(test_a1, dtype=backend.dtype)
+
+ stinespring = chi_to_stinespring(
+ test_chi,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ validate_cp=validate_cp,
+ nqubits=nqubits,
+ backend=backend,
+ )
+
+ v_0 = np.array([1, 0], dtype=complex)
+ v_0 = backend.cast(v_0, dtype=v_0.dtype)
+
+ state = random_density_matrix(2**nqubits, backend=backend)
+
+ # action of Kraus channel on state
+ state_final_test = test_a0 @ state @ test_a0 + test_a1 @ state @ test_a1
+
+ # action of stinespring channel on state + environment
+ stinespring = (
+ stinespring
+ @ backend.np.kron(state, backend.np.outer(v_0, v_0))
+ @ backend.np.conj(stinespring).T
+ )
+
+ # partial trace of the environment
+ stinespring = backend.np.reshape(stinespring, (2**nqubits, 2, 2**nqubits, 2))
+ stinespring = backend.np.swapaxes(stinespring, 1, 2)
+ state_final = np.zeros((2**nqubits, 2**nqubits), dtype=complex)
+ state_final = backend.cast(state_final, dtype=state_final.dtype)
+ for alpha in range(2):
+ vector_alpha = np.zeros(2, dtype=complex)
+ vector_alpha[alpha] = 1.0
+ vector_alpha = backend.cast(vector_alpha, dtype=vector_alpha.dtype)
+ state_final = (
+ state_final + backend.np.conj(vector_alpha) @ stinespring @ vector_alpha
+ )
+
+ backend.assert_allclose(state_final, aux * state_final_test, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("dim_env", [2])
+@pytest.mark.parametrize("stinespring", [test_stinespring])
+def test_stinespring_to_choi(
+ backend, stinespring, dim_env, nqubits, order, test_superop
+):
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+ choi_super_op = stinespring_to_choi(
+ stinespring, dim_env=dim_env, nqubits=nqubits, order=order, backend=backend
+ )
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = np.reshape(
+ np.swapaxes(np.reshape(test_superop, [2] * 4), *axes), [4, 4]
+ )
+ test_choi = backend.cast(test_choi, dtype=test_choi.dtype)
+
+ backend.assert_allclose(choi_super_op, test_choi, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("dim_env", [2])
+@pytest.mark.parametrize("stinespring", [test_stinespring])
+def test_stinespring_to_liouville(
+ backend, stinespring, dim_env, nqubits, order, test_superop
+):
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+
+ super_op = stinespring_to_liouville(
+ stinespring, dim_env, nqubits=nqubits, order=order, backend=backend
+ )
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ backend.assert_allclose(super_op, test_superop, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("dim_env", [2])
+@pytest.mark.parametrize("stinespring", [test_stinespring])
+def test_stinespring_to_pauli(
+ backend, stinespring, dim_env, nqubits, normalize, order, pauli_order
+):
+ test_pauli = pauli_superop(pauli_order)
+ dim = int(np.sqrt(test_pauli.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+ test_pauli = backend.cast(test_pauli / aux, dtype=test_pauli.dtype)
+
+ super_op_pauli = stinespring_to_pauli(
+ stinespring,
+ dim_env,
+ nqubits=nqubits,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ backend.assert_allclose(super_op_pauli, test_pauli, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("dim_env", [2])
+@pytest.mark.parametrize("stinespring", [test_stinespring])
+def test_stinespring_to_kraus(backend, stinespring, dim_env, nqubits):
+ with pytest.raises(TypeError):
+ test = stinespring_to_kraus(stinespring, dim_env=2.0, nqubits=nqubits)
+ with pytest.raises(ValueError):
+ test = stinespring_to_kraus(stinespring, dim_env=-1, nqubits=nqubits)
+ with pytest.raises(ValueError):
+ state = random_density_matrix(2, pure=True, backend=backend)
+ test = stinespring_to_kraus(
+ stinespring,
+ dim_env=dim_env,
+ initial_state_env=state,
+ nqubits=nqubits,
+ backend=backend,
+ )
+ with pytest.raises(TypeError):
+ test = stinespring_to_kraus(
+ stinespring,
+ dim_env=dim_env,
+ nqubits=1.0,
+ backend=backend,
+ )
+ with pytest.raises(ValueError):
+ test = stinespring_to_kraus(
+ stinespring,
+ dim_env=dim_env,
+ nqubits=-1,
+ backend=backend,
+ )
+ with pytest.raises(ValueError):
+ test = stinespring_to_kraus(stinespring, dim_env=3, nqubits=2, backend=backend)
+
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+ test = stinespring_to_kraus(stinespring, dim_env, nqubits=nqubits, backend=backend)
+
+ for kraus, test_kraus in zip(test, [test_a0, test_a1]):
+ backend.assert_allclose(kraus, test_kraus, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("pauli_order", ["IXYZ", "IZXY"])
+@pytest.mark.parametrize("order", ["row", "column"])
+@pytest.mark.parametrize("normalize", [False, True])
+@pytest.mark.parametrize("nqubits", [None, 1])
+@pytest.mark.parametrize("dim_env", [2])
+@pytest.mark.parametrize("stinespring", [test_stinespring])
+def test_stinespring_to_chi(
+ backend, stinespring, dim_env, nqubits, normalize, order, pauli_order
+):
+ test_chi = chi_superop(pauli_order)
+ dim = int(np.sqrt(test_chi.shape[0]))
+ aux = 1.0 if not normalize else dim
+
+ stinespring = backend.cast(stinespring, dtype=stinespring.dtype)
+ test_chi = backend.cast(test_chi, dtype=test_chi.dtype)
+
+ chi_matrix = stinespring_to_chi(
+ stinespring,
+ dim_env,
+ nqubits=nqubits,
+ normalize=normalize,
+ order=order,
+ pauli_order=pauli_order,
+ backend=backend,
+ )
+
+ backend.assert_allclose(test_chi / aux, chi_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_kraus_to_unitaries(backend, order):
+ test_a0 = np.sqrt(0.4) * matrices.X
+ test_a1 = np.sqrt(0.6) * matrices.Y
+ test_kraus = [((0,), test_a0), ((0,), test_a1)]
+
+ with pytest.raises(TypeError):
+ kraus_to_unitaries(test_kraus, order, str(PRECISION_TOL), backend=backend)
+ with pytest.raises(ValueError):
+ kraus_to_unitaries(test_kraus, order, -1.0 * PRECISION_TOL, backend=backend)
+
+ target = kraus_to_liouville(test_kraus, order=order, backend=backend)
+
+ unitaries, probabilities = kraus_to_unitaries(
+ test_kraus, order=order, backend=backend
+ )
+ unitaries = np.array(
+ [np.sqrt(prob) * unitary for prob, unitary in zip(probabilities, unitaries)]
+ )
+ unitaries = list(zip([(0,)] * len(unitaries), unitaries))
+
+ operator = kraus_to_liouville(unitaries, backend=backend)
+
+ backend.assert_allclose(target, operator, atol=2 * PRECISION_TOL)
+
+ # warning coverage
+ test_a0 = np.sqrt(0.4) * matrices.X
+ test_a1 = np.sqrt(0.6) * matrices.Z
+ test_kraus = [((0,), test_a0), ((0,), test_a1)]
+ kraus_to_unitaries(test_kraus, order=order, backend=backend)
+
+
+@pytest.mark.parametrize("test_superop", [test_superop])
+@pytest.mark.parametrize("order", ["row", "column"])
+def test_reshuffling(backend, order, test_superop):
+ from qibo.quantum_info.superoperator_transformations import _reshuffling
+
+ with pytest.raises(TypeError):
+ _reshuffling(test_superop, True, backend=backend)
+ with pytest.raises(ValueError):
+ _reshuffling(test_superop, "sustem", backend=backend)
+ with pytest.raises(NotImplementedError):
+ _reshuffling(test_superop, "system", backend=backend)
+ with pytest.raises(ValueError):
+ _reshuffling(test_superop[:-1, :-1], order, backend=backend)
+
+ reshuffled = _reshuffling(test_superop, order, backend=backend)
+ reshuffled = _reshuffling(reshuffled, order, backend=backend)
+
+ test_superop = backend.cast(test_superop, dtype=test_superop.dtype)
+
+ backend.assert_allclose(
+ np.linalg.norm(backend.to_numpy(reshuffled) - backend.to_numpy(test_superop))
+ < PRECISION_TOL,
+ True,
+ )
+
+ axes = [1, 2] if order == "row" else [0, 3]
+ test_choi = backend.np.reshape(
+ backend.np.swapaxes(backend.np.reshape(test_superop, [2] * 4), *axes), [4, 4]
+ )
+
+ reshuffled = _reshuffling(test_choi, order, backend=backend)
+ reshuffled = _reshuffling(reshuffled, order, backend=backend)
+
+ backend.assert_allclose(reshuffled, test_choi, atol=PRECISION_TOL)
diff --git a/tests/test_quantum_info_utils.py b/tests/test_quantum_info_utils.py
new file mode 100644
index 000000000..8a375e0f2
--- /dev/null
+++ b/tests/test_quantum_info_utils.py
@@ -0,0 +1,256 @@
+from functools import reduce
+from re import finditer
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.metrics import fidelity
+from qibo.quantum_info.random_ensembles import random_clifford
+from qibo.quantum_info.utils import (
+ haar_integral,
+ hadamard_transform,
+ hamming_distance,
+ hamming_weight,
+ hellinger_distance,
+ hellinger_fidelity,
+ hellinger_shot_error,
+ pqc_integral,
+)
+
+
+@pytest.mark.parametrize("bitstring", range(2**5))
+@pytest.mark.parametrize(
+ "kind",
+ [
+ str,
+ list,
+ tuple,
+ lambda b: np.array(list(b)),
+ lambda b: int(b, 2),
+ lambda b: list(map(int, b)),
+ ],
+)
+def test_hamming_weight(bitstring, kind):
+ with pytest.raises(TypeError):
+ test = hamming_weight("0101", return_indexes="True")
+ with pytest.raises(TypeError):
+ test = hamming_weight(2.3)
+
+ bitstring = f"{bitstring:b}"
+ weight_test = len(bitstring.replace("0", ""))
+ indexes_test = [item.start() for item in finditer("1", bitstring)]
+
+ weight = hamming_weight(kind(bitstring), False)
+ indexes = hamming_weight(kind(bitstring), True)
+
+ assert weight == weight_test
+ assert indexes == indexes_test
+
+
+bitstring_1, bitstring_2 = "11111", "10101"
+
+
+@pytest.mark.parametrize(
+ ["bitstring_1", "bitstring_2"],
+ [
+ [bitstring_1, bitstring_2],
+ [int(bitstring_1, 2), int(bitstring_2, 2)],
+ [list(bitstring_1), list(bitstring_2)],
+ [tuple(bitstring_1), tuple(bitstring_2)],
+ ],
+)
+def test_hamming_distance(bitstring_1, bitstring_2):
+ with pytest.raises(TypeError):
+ test = hamming_distance("0101", "1010", return_indexes="True")
+ with pytest.raises(TypeError):
+ test = hamming_distance(2.3, "1010")
+ with pytest.raises(TypeError):
+ test = hamming_distance("1010", 2.3)
+
+ if isinstance(bitstring_1, int):
+ bitstring_1, bitstring_2 = f"{bitstring_1:b}", f"{bitstring_2:b}"
+
+ distance = hamming_distance(bitstring_1, bitstring_2)
+ indexes = hamming_distance(bitstring_1, bitstring_2, return_indexes=True)
+
+ assert distance == 2
+ assert indexes == [1, 3]
+
+
+@pytest.mark.parametrize("is_matrix", [False, True])
+@pytest.mark.parametrize("implementation", ["fast", "regular"])
+@pytest.mark.parametrize("nqubits", [3, 4, 5])
+def test_hadamard_transform(backend, nqubits, implementation, is_matrix):
+ with pytest.raises(TypeError):
+ test = np.random.rand(2, 2, 2)
+ test = backend.cast(test, dtype=test.dtype)
+ test = hadamard_transform(test, implementation=implementation, backend=backend)
+ with pytest.raises(TypeError):
+ test = np.random.rand(2, 3)
+ test = backend.cast(test, dtype=test.dtype)
+ test = hadamard_transform(test, implementation=implementation, backend=backend)
+ with pytest.raises(TypeError):
+ test = np.random.rand(3, 3)
+ test = backend.cast(test, dtype=test.dtype)
+ test = hadamard_transform(test, implementation=implementation, backend=backend)
+ with pytest.raises(TypeError):
+ test = np.random.rand(2**nqubits)
+ test = backend.cast(test, dtype=test.dtype)
+ test = hadamard_transform(test, implementation=True, backend=backend)
+ with pytest.raises(ValueError):
+ test = np.random.rand(2**nqubits)
+ test = backend.cast(test, dtype=test.dtype)
+ test = hadamard_transform(test, implementation="fas", backend=backend)
+
+ dim = 2**nqubits
+
+ state = np.random.rand(dim, dim) if is_matrix else np.random.rand(dim)
+ state = backend.cast(state, dtype=state.dtype)
+
+ hadamards = np.real(reduce(np.kron, [matrices.H] * nqubits))
+ hadamards /= 2 ** (nqubits / 2)
+ hadamards = backend.cast(hadamards, dtype=hadamards.dtype)
+
+ test_transformed = hadamards @ state
+ if is_matrix:
+ test_transformed = test_transformed @ hadamards
+
+ transformed = hadamard_transform(
+ state, implementation=implementation, backend=backend
+ )
+
+ backend.assert_allclose(transformed, test_transformed, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("validate", [False, True])
+def test_hellinger(backend, validate, kind):
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)
+ prob_q = np.random.rand(1, 5)
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = hellinger_distance(prob, prob_q, backend=backend)
+ with pytest.raises(TypeError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = hellinger_distance(prob, prob_q, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([-1, 2.0])
+ prob_q = np.random.rand(1, 5)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = hellinger_distance(prob, prob_q, validate=True, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.random.rand(1, 2)[0]
+ prob_q = np.array([1.0, 0.0])
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = hellinger_distance(prob, prob_q, validate=True, backend=backend)
+ with pytest.raises(ValueError):
+ prob = np.array([1.0, 0.0])
+ prob_q = np.random.rand(1, 2)[0]
+ prob = backend.cast(prob, dtype=prob.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+ test = hellinger_distance(prob, prob_q, validate=True, backend=backend)
+
+ prob_p = np.random.rand(10)
+ prob_q = np.random.rand(10)
+ prob_p /= np.sum(prob_p)
+ prob_q /= np.sum(prob_q)
+ prob_p = backend.cast(prob_p, dtype=prob_p.dtype)
+ prob_q = backend.cast(prob_q, dtype=prob_q.dtype)
+
+ target = float(
+ backend.calculate_norm(backend.np.sqrt(prob_p) - backend.np.sqrt(prob_q))
+ / np.sqrt(2)
+ )
+
+ prob_p = (
+ kind(prob_p) if kind is not None else backend.cast(prob_p, dtype=prob_p.dtype)
+ )
+ prob_q = (
+ kind(prob_q) if kind is not None else backend.cast(prob_q, dtype=prob_q.dtype)
+ )
+
+ distance = hellinger_distance(prob_p, prob_q, validate=validate, backend=backend)
+ fidelity = hellinger_fidelity(prob_p, prob_q, validate=validate, backend=backend)
+
+ assert distance == target
+ assert fidelity == (1 - target**2) ** 2
+
+
+@pytest.mark.parametrize("kind", [None, list])
+@pytest.mark.parametrize("validate", [False, True])
+def test_hellinger_shot_error(backend, validate, kind):
+ nqubits, nshots = 5, 1000
+
+ circuit = random_clifford(nqubits, seed=1, backend=backend)
+ circuit.add(gates.M(qubit) for qubit in range(nqubits))
+
+ circuit_2 = random_clifford(nqubits, seed=2, backend=backend)
+ circuit_2.add(gates.M(qubit) for qubit in range(nqubits))
+
+ prob_dist_p = backend.execute_circuit(circuit, nshots=nshots).probabilities()
+ prob_dist_q = backend.execute_circuit(circuit_2, nshots=nshots).probabilities()
+
+ if kind is not None:
+ prob_dist_p = kind(prob_dist_p)
+ prob_dist_q = kind(prob_dist_q)
+
+ hellinger_error = hellinger_shot_error(
+ prob_dist_p, prob_dist_q, nshots, validate=validate, backend=backend
+ )
+ hellinger_fid = hellinger_fidelity(
+ prob_dist_p, prob_dist_q, validate=validate, backend=backend
+ )
+
+ assert 2 * hellinger_error < hellinger_fid
+
+
+def test_haar_integral_errors(backend):
+ with pytest.raises(TypeError):
+ nqubits, power_t, samples = 0.5, 2, 10
+ test = haar_integral(nqubits, power_t, samples, backend=backend)
+ with pytest.raises(TypeError):
+ nqubits, power_t, samples = 2, 0.5, 10
+ test = haar_integral(nqubits, power_t, samples, backend=backend)
+ with pytest.raises(TypeError):
+ nqubits, power_t, samples = 2, 1, 1.2
+ test = haar_integral(nqubits, power_t, samples=samples, backend=backend)
+
+
+@pytest.mark.parametrize("power_t", [1, 2])
+@pytest.mark.parametrize("nqubits", [2, 3])
+def test_haar_integral(backend, nqubits, power_t):
+ samples = int(1e3)
+
+ haar_int_exact = haar_integral(nqubits, power_t, samples=None, backend=backend)
+
+ haar_int_sampled = haar_integral(nqubits, power_t, samples=samples, backend=backend)
+
+ backend.assert_allclose(haar_int_sampled, haar_int_exact, atol=1e-1)
+
+
+def test_pqc_integral(backend):
+ with pytest.raises(TypeError):
+ power_t, samples = 0.5, 10
+ circuit = Circuit(2)
+ test = pqc_integral(circuit, power_t, samples, backend=backend)
+ with pytest.raises(TypeError):
+ power_t, samples = 2, 0.5
+ circuit = Circuit(2)
+ test = pqc_integral(circuit, power_t, samples, backend=backend)
+
+ circuit = Circuit(2)
+ power_t, samples = 1, 100
+
+ pqc_int = pqc_integral(circuit, power_t, samples, backend=backend)
+
+ fid = fidelity(pqc_int, pqc_int, backend=backend)
+
+ backend.assert_allclose(fid, 1.0, atol=PRECISION_TOL)
diff --git a/tests/test_result.py b/tests/test_result.py
new file mode 100644
index 000000000..7b37466a2
--- /dev/null
+++ b/tests/test_result.py
@@ -0,0 +1,111 @@
+from os import remove
+
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, models
+from qibo.config import raise_error
+from qibo.result import CircuitResult, MeasurementOutcomes, load_result
+
+
+@pytest.mark.parametrize("qubits", [None, [0], [1, 2]])
+def test_measurementoutcomes_probabilities(backend, qubits):
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.M(0, 2))
+ global_probs = backend.execute_circuit(c).probabilities(qubits=[0, 2])
+ probabilities = (
+ backend.execute_circuit(c).probabilities(qubits=qubits)
+ if qubits is not None
+ else backend.execute_circuit(c).probabilities(qubits=[0, 2])
+ )
+ c.has_collapse = True
+ if qubits is not None and 1 in qubits:
+ with pytest.raises(RuntimeError) as excinfo:
+ repeated_probabilities = backend.execute_circuit(c).probabilities(
+ qubits=qubits
+ )
+ assert (
+ str(excinfo.value)
+ == f"Asking probabilities for qubits {qubits}, but only qubits [0,2] were measured."
+ )
+ else:
+ repeated_probabilities = backend.execute_circuit(c, nshots=1000).probabilities(
+ qubits=qubits
+ )
+ result = MeasurementOutcomes(
+ c.measurements, backend=backend, probabilities=global_probs
+ )
+ backend.assert_allclose(probabilities, repeated_probabilities, atol=1e-1)
+ backend.assert_allclose(result.probabilities(qubits), probabilities, atol=1e-1)
+
+
+def test_measurementoutcomes_samples_from_measurements(backend):
+ c = Circuit(3)
+ c.add(gates.H(0))
+ c.add(gates.M(0, 2))
+ res = backend.execute_circuit(c)
+ samples = res.samples()
+ outcome = MeasurementOutcomes(c.measurements, backend=backend)
+ backend.assert_allclose(samples, outcome.samples())
+
+
+def test_circuit_result_error(backend):
+ c = models.Circuit(1)
+ state = np.array([1, 0])
+ with pytest.raises(Exception) as exc_info:
+ CircuitResult(state, c.measurements, backend)
+ assert (
+ str(exc_info.value)
+ == "Circuit does not contain measurements. Use a `QuantumState` instead."
+ )
+
+
+def test_measurement_gate_dump_load(backend):
+ c = models.Circuit(2)
+ c.add(gates.M(1, 0, basis=[gates.Z, gates.X]))
+ m = c.measurements
+ load = m[0].to_json()
+ new_m = gates.M.load(load)
+ assert new_m.to_json() == load
+
+
+@pytest.mark.parametrize("agnostic_load", [False, True])
+def test_measurementoutcomes_dump_load(backend, agnostic_load):
+ c = models.Circuit(2)
+ c.add(gates.M(1, 0, basis=[gates.Z, gates.X]))
+ # just to trigger repeated execution and test MeasurementOutcomes
+ c.has_collapse = True
+ measurement = backend.execute_circuit(c, nshots=100)
+ freq = measurement.frequencies()
+ measurement.dump("tmp.npy")
+ if agnostic_load:
+ loaded_meas = load_result("tmp.npy")
+ else:
+ loaded_meas = MeasurementOutcomes.load("tmp.npy")
+ loaded_freq = loaded_meas.frequencies()
+ for state, f in freq.items():
+ assert loaded_freq[state] == f
+ remove("tmp.npy")
+
+
+@pytest.mark.parametrize("agnostic_load", [False, True])
+def test_circuitresult_dump_load(backend, agnostic_load):
+ c = models.Circuit(2, density_matrix=True)
+ c.add(gates.M(1, 0, basis=[gates.Z, gates.X]))
+ # trigger repeated execution to build the CircuitResult object
+ # from samples and recover the same exact frequencies
+ c.has_collapse = True
+ result = backend.execute_circuit(c)
+ freq = result.frequencies()
+ # set probabilities to trigger the warning
+ result._probs = result.probabilities()
+ result.dump("tmp.npy")
+ loaded_res = (
+ load_result("tmp.npy") if agnostic_load else CircuitResult.load("tmp.npy")
+ )
+ loaded_freq = loaded_res.frequencies()
+ for state, f in freq.items():
+ assert loaded_freq[state] == f
+ assert backend.np.sum(result.state() - backend.cast(loaded_res.state())) == 0
+ remove("tmp.npy")
diff --git a/tests/test_states.py b/tests/test_states.py
new file mode 100644
index 000000000..fc0cd512d
--- /dev/null
+++ b/tests/test_states.py
@@ -0,0 +1,133 @@
+import numpy as np
+import pytest
+
+from qibo import Circuit, gates, hamiltonians
+from qibo.measurements import MeasurementResult
+from qibo.result import QuantumState, load_result
+from qibo.symbols import I, Z
+
+
+def test_measurement_result_repr():
+ result = MeasurementResult(gates.M(0), nshots=10)
+ assert str(result) == "MeasurementResult(qubits=(0,), nshots=10)"
+
+
+def test_measurement_result_error():
+ result = MeasurementResult(gates.M(0), nshots=10)
+ with pytest.raises(RuntimeError):
+ samples = result.samples()
+
+
+@pytest.mark.parametrize("target", range(5))
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_state_representation(backend, target, density_matrix):
+ c = Circuit(5, density_matrix=density_matrix)
+ c.add(gates.H(target))
+ result = backend.execute_circuit(c)
+ bstring = target * "0" + "1" + (4 - target) * "0"
+ if density_matrix:
+ target_str = 3 * [
+ f"(0.5+0j)|00000><00000| + (0.5+0j)|00000><{bstring}| + (0.5+0j)|{bstring}><00000|"
+ + f" + (0.5+0j)|{bstring}><{bstring}|"
+ ]
+ else:
+ target_str = [
+ f"(0.70711+0j)|00000> + (0.70711+0j)|{bstring}>",
+ f"(0.7+0j)|00000> + (0.7+0j)|{bstring}>",
+ f"(0.71+0j)|00000> + (0.71+0j)|{bstring}>",
+ ]
+ assert str(result) == target_str[0]
+ assert result.symbolic(decimals=5) == target_str[0]
+ assert result.symbolic(decimals=1) == target_str[1]
+ assert result.symbolic(decimals=2) == target_str[2]
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_state_representation_max_terms(backend, density_matrix):
+ c = Circuit(5, density_matrix=density_matrix)
+ c.add(gates.H(i) for i in range(5))
+ result = backend.execute_circuit(c)
+ if density_matrix:
+ assert (
+ result.symbolic(max_terms=3)
+ == "(0.03125+0j)|00000><00000| + (0.03125+0j)|00000><00001| + "
+ + "(0.03125+0j)|00000><00010| + ..."
+ )
+ assert (
+ result.symbolic(max_terms=5)
+ == "(0.03125+0j)|00000><00000| + (0.03125+0j)|00000><00001| + "
+ + "(0.03125+0j)|00000><00010| + (0.03125+0j)|00000><00011| + "
+ + "(0.03125+0j)|00000><00100| + ..."
+ )
+ else:
+ assert (
+ result.symbolic(max_terms=3)
+ == "(0.17678+0j)|00000> + (0.17678+0j)|00001> + (0.17678+0j)|00010> + ..."
+ )
+ assert (
+ result.symbolic(max_terms=5)
+ == "(0.17678+0j)|00000> + (0.17678+0j)|00001> + (0.17678+0j)|00010> + "
+ + "(0.17678+0j)|00011> + (0.17678+0j)|00100> + ..."
+ )
+
+
+@pytest.mark.parametrize("density_matrix", [False, True])
+def test_state_probabilities(backend, density_matrix):
+ c = Circuit(4, density_matrix=density_matrix)
+ c.add(gates.H(i) for i in range(4))
+ result = backend.execute_circuit(c)
+ # with pytest.raises(ValueError):
+ # final_probabilities = result.probabilities()
+
+ c = Circuit(4, density_matrix=density_matrix)
+ c.add(gates.H(i) for i in range(4))
+ c.add(gates.M(*range(4)))
+ result = backend.execute_circuit(c)
+ final_probabilities = result.probabilities()
+ target_probabilities = np.ones(16) / 16
+ backend.assert_allclose(final_probabilities, target_probabilities)
+
+
+def test_expectation_from_samples(backend):
+ # fix seed to use the same samples in every execution
+ np.random.seed(123)
+ obs0 = 2 * Z(0) * Z(1) + Z(0) * Z(2)
+ obs1 = 2 * Z(0) * Z(1) + Z(0) * Z(2) * I(3)
+ h_sym = hamiltonians.SymbolicHamiltonian(obs0, backend=backend)
+ h_dense = hamiltonians.Hamiltonian(3, h_sym.matrix, backend=backend)
+ h1 = hamiltonians.SymbolicHamiltonian(obs1, backend=backend)
+ c = Circuit(4)
+ c.add(gates.RX(0, np.random.rand()))
+ c.add(gates.RX(1, np.random.rand()))
+ c.add(gates.RX(2, np.random.rand()))
+ c.add(gates.RX(3, np.random.rand()))
+ c.add(gates.M(0, 1, 2))
+ nshots = 10**5
+ result = backend.execute_circuit(c, nshots=nshots)
+ expval_sym = result.expectation_from_samples(h_sym)
+ expval_dense = result.expectation_from_samples(h_dense)
+ expval = h1.expectation(result.state())
+ backend.assert_allclose(expval_sym, expval_dense)
+ backend.assert_allclose(expval_sym, expval, atol=10 / np.sqrt(nshots))
+
+
+def test_state_numpy(backend):
+ c = Circuit(1)
+ result = backend.execute_circuit(c)
+ assert isinstance(result.state(numpy=True), np.ndarray)
+
+
+@pytest.mark.parametrize("agnostic_load", [False, True])
+def test_state_dump_load(backend, agnostic_load):
+ from os import remove
+
+ c = Circuit(1)
+ c.add(gates.H(0))
+ state = backend.execute_circuit(c)
+ state.dump("tmp.npy")
+ if agnostic_load:
+ loaded_state = load_result("tmp.npy")
+ else:
+ loaded_state = QuantumState.load("tmp.npy")
+ assert str(state) == str(loaded_state)
+ remove("tmp.npy")
diff --git a/tests/test_tomography_gate_set_tomography.py b/tests/test_tomography_gate_set_tomography.py
new file mode 100644
index 000000000..bbed8cff0
--- /dev/null
+++ b/tests/test_tomography_gate_set_tomography.py
@@ -0,0 +1,305 @@
+from functools import reduce
+from itertools import repeat
+
+import numpy as np
+import pytest
+from sympy import S
+
+from qibo import gates, symbols
+from qibo.hamiltonians import SymbolicHamiltonian
+from qibo.noise import DepolarizingError, NoiseModel
+from qibo.quantum_info.superoperator_transformations import to_pauli_liouville
+from qibo.tomography.gate_set_tomography import (
+ GST,
+ _gate_tomography,
+ _get_observable,
+ _measurement_basis,
+ _prepare_state,
+)
+from qibo.transpiler.optimizer import Preprocessing
+from qibo.transpiler.pipeline import Passes
+from qibo.transpiler.placer import Random
+from qibo.transpiler.router import Sabre
+from qibo.transpiler.unroller import NativeGates, Unroller
+
+
+def _compare_gates(g1, g2):
+ assert g1.__class__.__name__ == g2.__class__.__name__
+ assert g1.qubits == g2.qubits
+
+
+INDEX_NQUBITS = (
+ list(zip(range(4), repeat(1, 4)))
+ + list(zip(range(16), repeat(2, 16)))
+ + [(0, 3), (17, 1)]
+)
+
+
+@pytest.mark.parametrize(
+ "k,nqubits",
+ INDEX_NQUBITS,
+)
+def test__prepare_state(k, nqubits):
+ correct_gates = {
+ 1: [[gates.I(0)], [gates.X(0)], [gates.H(0)], [gates.H(0), gates.S(0)]],
+ 2: [
+ [gates.I(0), gates.I(1)],
+ [gates.I(0), gates.X(1)],
+ [gates.I(0), gates.H(1)],
+ [gates.I(0), gates.H(1), gates.S(1)],
+ [gates.X(0), gates.I(1)],
+ [gates.X(0), gates.X(1)],
+ [gates.X(0), gates.H(1)],
+ [gates.X(0), gates.H(1), gates.S(1)],
+ [gates.H(0), gates.I(1)],
+ [gates.H(0), gates.X(1)],
+ [gates.H(0), gates.H(1)],
+ [gates.H(0), gates.H(1), gates.S(1)],
+ [gates.H(0), gates.S(0), gates.I(1)],
+ [gates.H(0), gates.S(0), gates.X(1)],
+ [gates.H(0), gates.S(0), gates.H(1)],
+ [gates.H(0), gates.S(0), gates.H(1), gates.S(1)],
+ ],
+ }
+ errors = {(0, 3): ValueError, (17, 1): IndexError}
+ if (k, nqubits) in [(0, 3), (17, 1)]:
+ with pytest.raises(errors[(k, nqubits)]):
+ prepared_states = _prepare_state(k, nqubits)
+ else:
+ prepared_states = _prepare_state(k, nqubits)
+ for groundtruth, gate in zip(correct_gates[nqubits][k], prepared_states):
+ _compare_gates(groundtruth, gate)
+
+
+@pytest.mark.parametrize(
+ "j,nqubits",
+ INDEX_NQUBITS,
+)
+def test__measurement_basis(j, nqubits):
+ correct_gates = {
+ 1: [
+ [gates.M(0)],
+ [gates.M(0, basis=gates.X)],
+ [gates.M(0, basis=gates.Y)],
+ [gates.M(0, basis=gates.Z)],
+ ],
+ 2: [
+ [gates.M(0), gates.M(1)],
+ [gates.M(0), gates.M(1, basis=gates.X)],
+ [gates.M(0), gates.M(1, basis=gates.Y)],
+ [gates.M(0), gates.M(1)],
+ [gates.M(0, basis=gates.X), gates.M(1)],
+ [gates.M(0, basis=gates.X), gates.M(1, basis=gates.X)],
+ [gates.M(0, basis=gates.X), gates.M(1, basis=gates.Y)],
+ [gates.M(0, basis=gates.X), gates.M(1)],
+ [gates.M(0, basis=gates.Y), gates.M(1)],
+ [gates.M(0, basis=gates.Y), gates.M(1, basis=gates.X)],
+ [gates.M(0, basis=gates.Y), gates.M(1, basis=gates.Y)],
+ [gates.M(0, basis=gates.Y), gates.M(1)],
+ [gates.M(0), gates.M(1)],
+ [gates.M(0), gates.M(1, basis=gates.X)],
+ [gates.M(0), gates.M(1, basis=gates.Y)],
+ [gates.M(0), gates.M(1)],
+ ],
+ }
+ errors = {(0, 3): ValueError, (17, 1): IndexError}
+ if (j, nqubits) in [(0, 3), (17, 1)]:
+ with pytest.raises(errors[(j, nqubits)]):
+ prepared_gates = _measurement_basis(j, nqubits)
+ else:
+ prepared_gates = _measurement_basis(j, nqubits)
+ for groundtruth, gate in zip(correct_gates[nqubits][j], prepared_gates):
+ _compare_gates(groundtruth, gate)
+ for g1, g2 in zip(groundtruth.basis, gate.basis):
+ _compare_gates(g1, g2)
+
+
+@pytest.mark.parametrize(
+ "j,nqubits",
+ INDEX_NQUBITS,
+)
+def test__get_observable(j, nqubits):
+ correct_observables = {
+ 1: [
+ (S(1),),
+ (symbols.Z(0),),
+ (symbols.Z(0),),
+ (symbols.Z(0),),
+ ],
+ 2: [
+ (S(1), S(1)),
+ (S(1), symbols.Z(1)),
+ (S(1), symbols.Z(1)),
+ (S(1), symbols.Z(1)),
+ (symbols.Z(0), S(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), S(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), S(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ (symbols.Z(0), symbols.Z(1)),
+ ],
+ }
+ correct_observables[1] = [
+ SymbolicHamiltonian(h[0]).form for h in correct_observables[1]
+ ]
+ correct_observables[2] = [
+ SymbolicHamiltonian(reduce(lambda x, y: x * y, h)).form
+ for h in correct_observables[2]
+ ]
+ errors = {(0, 3): ValueError, (17, 1): IndexError}
+ if (j, nqubits) in [(0, 3), (17, 1)]:
+ with pytest.raises(errors[(j, nqubits)]):
+ prepared_observable = _get_observable(j, nqubits)
+ else:
+ prepared_observable = _get_observable(j, nqubits).form
+ groundtruth = correct_observables[nqubits][j]
+ assert groundtruth == prepared_observable
+
+
+@pytest.mark.parametrize(
+ "nqubits, gate",
+ [
+ (1, gates.CNOT(0, 1)),
+ (3, gates.TOFFOLI(0, 1, 2)),
+ ],
+)
+def test_gate_tomography_value_error(backend, nqubits, gate):
+ with pytest.raises(ValueError):
+ matrix_jk = _gate_tomography(
+ nqubits=nqubits,
+ gate=gate,
+ nshots=int(1e4),
+ noise_model=None,
+ backend=backend,
+ )
+
+
+def test_gate_tomography_noise_model(backend):
+ nqubits = 1
+ gate = gates.H(0)
+ lam = 1.0
+ noise_model = NoiseModel()
+ noise_model.add(DepolarizingError(lam))
+ # return noise_model
+ target = _gate_tomography(
+ nqubits=nqubits,
+ gate=gate,
+ nshots=int(1e4),
+ noise_model=noise_model,
+ backend=backend,
+ )
+ exact_matrix = np.array([[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
+ backend.assert_allclose(
+ target,
+ exact_matrix,
+ atol=1e-1,
+ )
+
+
+@pytest.mark.parametrize(
+ "target_gates",
+ [[gates.SX(0), gates.Z(0), gates.CY(0, 1)], [gates.TOFFOLI(0, 1, 2)]],
+)
+@pytest.mark.parametrize("pauli_liouville", [False, True])
+def test_GST(backend, target_gates, pauli_liouville):
+ T = np.array([[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]])
+ T = backend.cast(T, dtype=T.dtype)
+ target_matrices = [g.matrix(backend=backend) for g in target_gates]
+ # superoperator representation of the target gates in the Pauli basis
+ target_matrices = [
+ to_pauli_liouville(m, normalize=True, backend=backend) for m in target_matrices
+ ]
+ gate_set = [g.__class__ for g in target_gates]
+
+ if len(target_gates) == 3:
+ empty_1q, empty_2q, *approx_gates = GST(
+ gate_set=gate_set,
+ nshots=int(1e4),
+ include_empty=True,
+ pauli_liouville=pauli_liouville,
+ backend=backend,
+ )
+ print(type(empty_1q), type(empty_2q))
+ T_2q = np.kron(T, T)
+ for target, estimate in zip(target_matrices, approx_gates):
+ if not pauli_liouville:
+ G = empty_1q if estimate.shape[0] == 4 else empty_2q
+ G_inv = np.linalg.inv(G)
+ T_matrix = T if estimate.shape[0] == 4 else T_2q
+ estimate = T_matrix @ G_inv @ estimate @ G_inv
+ backend.assert_allclose(
+ target,
+ estimate,
+ atol=1e-1,
+ )
+ else:
+ with pytest.raises(RuntimeError):
+ empty_1q, empty_2q, *approx_gates = GST(
+ gate_set=[g.__class__ for g in target_gates],
+ nshots=int(1e4),
+ include_empty=True,
+ pauli_liouville=pauli_liouville,
+ backend=backend,
+ )
+
+
+def test_GST_invertible_matrix():
+ T = np.array([[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 0, 1], [1, -1, 0, 0]])
+ matrices = GST(gate_set=[], pauli_liouville=True, gauge_matrix=T)
+ assert True
+
+
+def test_GST_non_invertible_matrix():
+ T = np.array([[1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [1, -1, 0, 0]])
+ with pytest.raises(ValueError):
+ matrices = GST(gate_set=[], pauli_liouville=True, gauge_matrix=T)
+
+
+def test_GST_with_transpiler(backend):
+ import networkx as nx
+
+ target_gates = [gates.SX(0), gates.Z(0), gates.CNOT(0, 1)]
+ gate_set = [g.__class__ for g in target_gates]
+ # standard not transpiled GST
+ empty_1q, empty_2q, *approx_gates = GST(
+ gate_set=gate_set,
+ nshots=int(1e4),
+ include_empty=True,
+ pauli_liouville=False,
+ backend=backend,
+ transpiler=None,
+ )
+ # define transpiler
+ connectivity = nx.Graph()
+ # star connectivity
+ connectivity.add_edges_from([(0, 2), (1, 2), (2, 3), (2, 4)])
+ transpiler = Passes(
+ connectivity=connectivity,
+ passes=[
+ Preprocessing(connectivity),
+ Random(connectivity),
+ Sabre(connectivity),
+ Unroller(NativeGates.default()),
+ ],
+ int_qubit_names=True,
+ )
+ # transpiled GST
+ T_empty_1q, T_empty_2q, *T_approx_gates = GST(
+ gate_set=gate_set,
+ nshots=int(1e4),
+ include_empty=True,
+ pauli_liouville=False,
+ backend=backend,
+ transpiler=transpiler,
+ )
+
+ backend.assert_allclose(empty_1q, T_empty_1q, atol=1e-1)
+ backend.assert_allclose(empty_2q, T_empty_2q, atol=1e-1)
+ for standard, transpiled in zip(approx_gates, T_approx_gates):
+ backend.assert_allclose(standard, transpiled, atol=1e-1)
diff --git a/tests/test_transpiler_blocks.py b/tests/test_transpiler_blocks.py
new file mode 100644
index 000000000..f875f394c
--- /dev/null
+++ b/tests/test_transpiler_blocks.py
@@ -0,0 +1,369 @@
+import pytest
+
+from qibo import Circuit, gates
+from qibo.transpiler._exceptions import BlockingError
+from qibo.transpiler.blocks import (
+ Block,
+ CircuitBlocks,
+ _check_multi_qubit_measurements,
+ _count_multi_qubit_gates,
+ _find_previous_gates,
+ _find_successive_gates,
+ _gates_on_qubit,
+ _initial_block_decomposition,
+ _remove_gates,
+ _split_multi_qubit_measurements,
+ block_decomposition,
+)
+
+
+def assert_gates_equality(gates_1: list, gates_2: list):
+ """Check that the gates are the same."""
+ for g_1, g_2 in zip(gates_1, gates_2):
+ assert g_1.qubits == g_2.qubits
+ assert g_1.__class__ == g_2.__class__
+
+
+def test_count_2q_gates():
+ block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1), gates.CZ(0, 1), gates.H(0)])
+ assert block._count_2q_gates() == 2
+
+
+def test_add_gate_and_entanglement():
+ block = Block(qubits=(0, 1), gates=[gates.H(0)])
+ assert not block.entangled
+ block.add_gate(gates.CZ(0, 1))
+ assert block.entangled
+ assert block._count_2q_gates() == 1
+
+
+def test_add_gate_error():
+ block = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
+ with pytest.raises(BlockingError):
+ block.add_gate(gates.CZ(0, 2))
+
+
+def test_fuse_blocks():
+ block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
+ block_2 = Block(qubits=(0, 1), gates=[gates.H(0)])
+ fused = block_1.fuse(block_2)
+ assert_gates_equality(fused.gates, block_1.gates + block_2.gates)
+
+
+def test_fuse_blocks_error():
+ block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
+ block_2 = Block(qubits=(1, 2), gates=[gates.CZ(1, 2)])
+ with pytest.raises(BlockingError):
+ fused = block_1.fuse(block_2)
+
+
+@pytest.mark.parametrize("qubits", [(0, 1), (2, 1)])
+def test_commute_false(qubits):
+ block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
+ block_2 = Block(qubits=qubits, gates=[gates.CZ(*qubits)])
+ assert not block_1.commute(block_2)
+
+
+def test_commute_true():
+ block_1 = Block(qubits=(0, 1), gates=[gates.CZ(0, 1)])
+ block_2 = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)])
+ assert block_1.commute(block_2)
+
+
+def test_count_multi_qubit_gates():
+ gatelist = [gates.CZ(0, 1), gates.H(0), gates.TOFFOLI(0, 1, 2)]
+ assert _count_multi_qubit_gates(gatelist) == 2
+
+
+def test_gates_on_qubit():
+ gatelist = [gates.H(0), gates.H(1), gates.H(2), gates.H(0)]
+ assert_gates_equality(_gates_on_qubit(gatelist, 0), [gatelist[0], gatelist[-1]])
+ assert_gates_equality(_gates_on_qubit(gatelist, 1), [gatelist[1]])
+ assert_gates_equality(_gates_on_qubit(gatelist, 2), [gatelist[2]])
+
+
+def test_remove_gates():
+ gatelist = [gates.H(0), gates.CZ(0, 1), gates.H(2), gates.CZ(0, 2)]
+ remaining = [gates.CZ(0, 1), gates.H(2)]
+ delete_list = [gatelist[0], gatelist[3]]
+ _remove_gates(gatelist, delete_list)
+ assert_gates_equality(gatelist, remaining)
+
+
+def test_find_previous_gates():
+ gatelist = [gates.H(0), gates.H(1), gates.H(2)]
+ previous_gates = _find_previous_gates(gatelist, (0, 1))
+ assert_gates_equality(previous_gates, gatelist[:2])
+
+
+def test_find_successive_gates():
+ gatelist = [gates.H(0), gates.CZ(2, 3), gates.H(1), gates.H(2), gates.CZ(2, 1)]
+ successive_gates = _find_successive_gates(gatelist, (0, 1))
+ assert_gates_equality(successive_gates, [gatelist[0], gatelist[2]])
+
+
+def test_initial_block_decomposition():
+ circ = Circuit(5)
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.H(3))
+ circ.add(gates.H(4))
+ blocks = _initial_block_decomposition(circ)
+ assert_gates_equality(blocks[0].gates, [gates.H(1), gates.H(0), gates.CZ(0, 1)])
+ assert len(blocks) == 4
+ assert len(blocks[0].gates) == 3
+ assert len(blocks[1].gates) == 1
+ assert blocks[2].entangled
+ assert not blocks[3].entangled
+ assert len(blocks[3].gates) == 2
+
+
+def test_check_measurements():
+ circ = Circuit(2)
+ circ.add(gates.H(1))
+ circ.add(gates.M(0, 1))
+ assert _check_multi_qubit_measurements(circ)
+ circ = Circuit(2)
+ circ.add(gates.H(1))
+ circ.add(gates.M(0))
+ circ.add(gates.M(1))
+ assert not _check_multi_qubit_measurements(circ)
+
+
+def test_split_measurements():
+ circ = Circuit(2)
+ circ.add(gates.H(1))
+ circ.add(gates.M(0, 1))
+ new_circ = _split_multi_qubit_measurements(circ)
+ assert_gates_equality(new_circ.queue, [gates.H(1), gates.M(0), gates.M(1)])
+
+
+def test_initial_block_decomposition_measurements():
+ circ = Circuit(5)
+ circ.add(gates.M(0))
+ circ.add(gates.M(1))
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.M(1))
+ circ.add(gates.M(3))
+ circ.add(gates.H(3))
+ circ.add(gates.H(4))
+ blocks = _initial_block_decomposition(circ)
+ assert_gates_equality(
+ blocks[0].gates,
+ [gates.M(0), gates.M(1), gates.H(1), gates.H(0), gates.CZ(0, 1)],
+ )
+ assert_gates_equality(blocks[1].gates, [gates.CZ(0, 1), gates.M(1)])
+ assert_gates_equality(blocks[2].gates, [gates.M(3), gates.H(3), gates.H(4)])
+
+
+def test_initial_block_decomposition_error():
+ circ = Circuit(3)
+ circ.add(gates.TOFFOLI(0, 1, 2))
+ with pytest.raises(BlockingError):
+ blocks = _initial_block_decomposition(circ)
+
+
+def test_block_decomposition_error():
+ circ = Circuit(1)
+ with pytest.raises(BlockingError):
+ block_decomposition(circ)
+
+
+def test_block_decomposition_no_fuse():
+ circ = Circuit(4)
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.H(1))
+ circ.add(gates.H(3))
+ blocks = block_decomposition(circ, fuse=False)
+ assert_gates_equality(
+ blocks[0].gates,
+ [gates.H(1), gates.H(0), gates.H(0), gates.CZ(0, 1), gates.H(0)],
+ )
+ assert len(blocks) == 4
+ assert len(blocks[0].gates) == 5
+ assert len(blocks[1].gates) == 1
+ assert blocks[2].entangled
+ assert not blocks[3].entangled
+
+
+def test_block_decomposition():
+ circ = Circuit(4)
+ circ.add(gates.H(1)) # first block
+ circ.add(gates.H(0)) # first block
+ circ.add(gates.CZ(0, 1)) # first block
+ circ.add(gates.H(0)) # first block
+ circ.add(gates.CZ(0, 1)) # first block
+ circ.add(gates.CZ(1, 2)) # second block
+ circ.add(gates.CZ(1, 2)) # second block
+ circ.add(gates.H(1)) # second block
+ circ.add(gates.H(3)) # 4 block
+ circ.add(gates.CZ(0, 1)) # 3 block
+ circ.add(gates.CZ(0, 1)) # 3 block
+ circ.add(gates.CZ(2, 3)) # 4 block
+ circ.add(gates.CZ(0, 1)) # 3 block
+ blocks = block_decomposition(circ)
+ assert_gates_equality(
+ blocks[0].gates,
+ [gates.H(1), gates.H(0), gates.CZ(0, 1), gates.H(0), gates.CZ(0, 1)],
+ )
+ assert len(blocks) == 4
+ assert blocks[0]._count_2q_gates() == 2
+ assert len(blocks[0].gates) == 5
+ assert blocks[0].qubits == (0, 1)
+ assert blocks[1]._count_2q_gates() == 2
+ assert len(blocks[1].gates) == 3
+ assert blocks[3]._count_2q_gates() == 1
+ assert len(blocks[3].gates) == 2
+ assert blocks[3].qubits == (2, 3)
+ assert blocks[2]._count_2q_gates() == 3
+ assert len(blocks[2].gates) == 3
+
+
+def test_block_decomposition_measurements():
+ circ = Circuit(4)
+ circ.add(gates.H(1)) # first block
+ circ.add(gates.H(0)) # first block
+ circ.add(gates.CZ(0, 1)) # first block
+ circ.add(gates.H(0)) # first block
+ circ.add(gates.M(0, 1)) # first block
+ circ.add(gates.CZ(1, 2)) # second block
+ circ.add(gates.CZ(1, 2)) # second block
+ circ.add(gates.H(1)) # second block
+ circ.add(gates.H(3)) # 4 block
+ circ.add(gates.CZ(0, 1)) # 3 block
+ circ.add(gates.CZ(0, 1)) # 3 block
+ circ.add(gates.CZ(2, 3)) # 4 block
+ circ.add(gates.M(0, 1)) # 3 block
+ blocks = block_decomposition(circ)
+ assert_gates_equality(
+ blocks[0].gates,
+ [gates.H(1), gates.H(0), gates.CZ(0, 1), gates.H(0), gates.M(0), gates.M(1)],
+ )
+ assert len(blocks) == 4
+ assert blocks[0]._count_2q_gates() == 1
+ assert len(blocks[0].gates) == 6
+ assert blocks[0].qubits == (0, 1)
+ assert blocks[1]._count_2q_gates() == 2
+ assert len(blocks[1].gates) == 3
+ assert blocks[3]._count_2q_gates() == 1
+ assert len(blocks[3].gates) == 2
+ assert blocks[3].qubits == (2, 3)
+ assert blocks[2]._count_2q_gates() == 2
+ assert len(blocks[2].gates) == 4
+
+
+def test_circuit_blocks(backend):
+ circ = Circuit(4)
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.H(1))
+ circ.add(gates.H(3))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(2, 3))
+ circ.add(gates.CZ(0, 1))
+ circuit_blocks = CircuitBlocks(circ, index_names=True)
+ for index, block in enumerate(circuit_blocks()):
+ assert block.name == index
+ reconstructed_circ = circuit_blocks.circuit()
+ backend.assert_allclose(
+ backend.execute_circuit(circ).state(),
+ backend.execute_circuit(reconstructed_circ).state(),
+ )
+ first_block = circuit_blocks.search_by_index(0)
+ assert_gates_equality(
+ first_block.gates,
+ [gates.H(1), gates.H(0), gates.CZ(0, 1), gates.H(0), gates.CZ(0, 1)],
+ )
+
+
+def test_add_block():
+ circ = Circuit(4)
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circuit_blocks = CircuitBlocks(circ)
+ new_block = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)])
+ circ.add(gates.CZ(2, 3))
+ circuit_blocks.add_block(new_block)
+ reconstructed_circ = circuit_blocks.circuit()
+ assert_gates_equality(reconstructed_circ.queue, circ.queue)
+
+
+def test_add_block_error():
+ circ = Circuit(2)
+ circ.add(gates.CZ(0, 1))
+ circuit_blocks = CircuitBlocks(circ)
+ new_block = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)])
+ with pytest.raises(BlockingError):
+ circuit_blocks.add_block(new_block)
+
+
+def test_remove_block():
+ circ = Circuit(3)
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circuit_blocks = CircuitBlocks(circ)
+ blocks = circuit_blocks()
+ circuit_blocks.remove_block(blocks[0])
+ remaining_block = circuit_blocks()
+ assert_gates_equality(remaining_block[0].gates, [gates.CZ(1, 2)])
+
+
+def test_remove_block_error():
+ circ = Circuit(3)
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circuit_blocks = CircuitBlocks(circ)
+ new_block = Block(qubits=(2, 3), gates=[gates.CZ(2, 3)])
+ with pytest.raises(BlockingError):
+ circuit_blocks.remove_block(new_block)
+
+
+def test_search_by_index_error_no_indexes():
+ circ = Circuit(2)
+ circ.add(gates.CZ(0, 1))
+ circuit_blocks = CircuitBlocks(circ)
+ with pytest.raises(BlockingError):
+ circuit_blocks.search_by_index(0)
+
+
+def test_search_by_index_error_no_index_found():
+ circ = Circuit(2)
+ circ.add(gates.CZ(0, 1))
+ circuit_blocks = CircuitBlocks(circ, index_names=True)
+ with pytest.raises(BlockingError):
+ circuit_blocks.search_by_index(1)
+
+
+def test_block_on_qubits():
+ block = Block(
+ qubits=(0, 1),
+ gates=[gates.H(0), gates.CZ(0, 1), gates.H(1), gates.CZ(1, 0), gates.M(1)],
+ )
+ new_block = block.on_qubits(new_qubits=(2, 3))
+ assert new_block.gates[0].qubits == (2,)
+ assert new_block.gates[1].qubits == (2, 3)
+ assert new_block.gates[2].qubits == (3,)
+ assert new_block.gates[3].qubits == (3, 2)
+ assert new_block.gates[4].qubits == (3,)
diff --git a/tests/test_transpiler_decompositions.py b/tests/test_transpiler_decompositions.py
new file mode 100644
index 000000000..663f73f5f
--- /dev/null
+++ b/tests/test_transpiler_decompositions.py
@@ -0,0 +1,243 @@
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.backends import NumpyBackend
+from qibo.models import Circuit
+from qibo.quantum_info.random_ensembles import random_unitary
+from qibo.transpiler.unroller import NativeGates, assert_decomposition, translate_gate
+
+default_natives = NativeGates.Z | NativeGates.RZ | NativeGates.M | NativeGates.I
+
+
+def assert_matrices_allclose(gate, natives, backend):
+ target_matrix = gate.matrix(backend)
+ # Remove global phase from target matrix
+ normalisation = np.power(
+ np.linalg.det(backend.to_numpy(target_matrix)),
+ 1 / float(target_matrix.shape[0]),
+ dtype=complex,
+ )
+ target_unitary = target_matrix / normalisation
+
+ circuit = Circuit(len(gate.qubits))
+ circuit.add(translate_gate(gate, natives))
+ native_matrix = circuit.unitary(backend)
+ # Remove global phase from native matrix
+ normalisation = np.power(
+ np.linalg.det(backend.to_numpy(native_matrix)),
+ 1 / float(native_matrix.shape[0]),
+ dtype=complex,
+ )
+ native_unitary = native_matrix / normalisation
+
+ # There can still be phase differences of -1, -1j, 1j
+ c = 0
+ for phase in [1, -1, 1j, -1j]:
+ if np.allclose(
+ backend.to_numpy(phase * native_unitary),
+ backend.to_numpy(target_unitary),
+ atol=1e-12,
+ ):
+ c = 1
+ backend.assert_allclose(c, 1)
+ assert_decomposition(circuit, natives)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+@pytest.mark.parametrize("gatename", ["H", "X", "Y", "I"])
+def test_pauli_to_native(backend, gatename, natives):
+ gate = getattr(gates, gatename)(0)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+@pytest.mark.parametrize("gatename", ["RX", "RY", "RZ"])
+def test_rotations_to_native(backend, gatename, natives):
+ gate = getattr(gates, gatename)(0, theta=0.1)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+@pytest.mark.parametrize("gatename", ["S", "SDG", "T", "TDG", "SX"])
+def test_special_single_qubit_to_native(backend, gatename, natives):
+ gate = getattr(gates, gatename)(0)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+def test_u1_to_native(backend, natives):
+ gate = gates.U1(0, theta=0.5)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+def test_u2_to_native(backend, natives):
+ gate = gates.U2(0, phi=0.1, lam=0.3)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+def test_u3_to_native(backend, natives):
+ gate = gates.U3(0, theta=0.2, phi=0.1, lam=0.3)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("natives", [NativeGates.U3, NativeGates.GPI2])
+def test_gpi2_to_native(backend, natives):
+ gate = gates.GPI2(0, phi=0.123)
+ assert_matrices_allclose(gate, natives=natives | default_natives, backend=backend)
+
+
+@pytest.mark.parametrize("gatename", ["CNOT", "CZ", "SWAP", "iSWAP", "FSWAP"])
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_two_qubit_to_native(backend, gatename, natives_1q, natives_2q):
+ gate = getattr(gates, gatename)(0, 1)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+@pytest.mark.parametrize("gatename", ["CRX", "CRY", "CRZ"])
+def test_controlled_rotations_to_native(backend, gatename, natives_1q, natives_2q):
+ gate = getattr(gates, gatename)(0, 1, 0.3)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_cu1_to_native(backend, natives_1q, natives_2q):
+ gate = gates.CU1(0, 1, theta=0.4)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_cu2_to_native(backend, natives_1q, natives_2q):
+ gate = gates.CU2(0, 1, phi=0.2, lam=0.3)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_cu3_to_native(backend, natives_1q, natives_2q):
+ gate = gates.CU3(0, 1, theta=0.2, phi=0.3, lam=0.4)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_fSim_to_native(backend, natives_1q, natives_2q):
+ gate = gates.fSim(0, 1, theta=0.3, phi=0.1)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_GeneralizedfSim_to_native(backend, natives_1q, natives_2q, seed):
+ unitary = random_unitary(2, seed=seed, backend=backend)
+ gate = gates.GeneralizedfSim(0, 1, unitary, phi=0.1)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+@pytest.mark.parametrize("gatename", ["RXX", "RZZ", "RYY"])
+def test_rnn_to_native(backend, gatename, natives_1q, natives_2q):
+ gate = getattr(gates, gatename)(0, 1, theta=0.1)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_TOFFOLI_to_native(backend, natives_1q, natives_2q):
+ gate = gates.TOFFOLI(0, 1, 2)
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+@pytest.mark.parametrize("nqubits", [1, 2])
+def test_unitary_to_native(backend, nqubits, natives_1q, natives_2q, seed):
+ u = random_unitary(2**nqubits, seed=seed, backend=NumpyBackend())
+ # transform to SU(2^nqubits) form
+ u = u / np.sqrt(np.linalg.det(u))
+ gate = gates.Unitary(u, *range(nqubits))
+ assert_matrices_allclose(gate, natives_1q | natives_2q | default_natives, backend)
+
+
+def test_count_1q():
+ from qibo.transpiler.unroller import cz_dec
+
+ np.testing.assert_allclose(cz_dec.count_1q(gates.CNOT(0, 1)), 2)
+ np.testing.assert_allclose(cz_dec.count_1q(gates.CRX(0, 1, 0.1)), 2)
+
+
+def test_count_2q():
+ from qibo.transpiler.unroller import cz_dec
+
+ np.testing.assert_allclose(cz_dec.count_2q(gates.CNOT(0, 1)), 1)
+ np.testing.assert_allclose(cz_dec.count_2q(gates.CRX(0, 1, 0.1)), 2)
diff --git a/tests/test_transpiler_optimizer.py b/tests/test_transpiler_optimizer.py
new file mode 100644
index 000000000..ad82e4991
--- /dev/null
+++ b/tests/test_transpiler_optimizer.py
@@ -0,0 +1,50 @@
+import networkx as nx
+import pytest
+
+from qibo import gates
+from qibo.models import Circuit
+from qibo.transpiler.optimizer import Preprocessing, Rearrange
+
+
+def star_connectivity():
+ Q = ["q" + str(i) for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2]
+ chip.add_edges_from(graph_list)
+ return chip
+
+
+def test_preprocessing_error():
+ circ = Circuit(7)
+ preprocesser = Preprocessing(connectivity=star_connectivity())
+ with pytest.raises(ValueError):
+ new_circuit = preprocesser(circuit=circ)
+
+
+def test_preprocessing_same():
+ circ = Circuit(5)
+ circ.add(gates.CNOT(0, 1))
+ preprocesser = Preprocessing(connectivity=star_connectivity())
+ new_circuit = preprocesser(circuit=circ)
+ assert new_circuit.ngates == 1
+
+
+def test_preprocessing_add():
+ circ = Circuit(3)
+ circ.add(gates.CNOT(0, 1))
+ preprocesser = Preprocessing(connectivity=star_connectivity())
+ new_circuit = preprocesser(circuit=circ)
+ assert new_circuit.ngates == 1
+ assert new_circuit.nqubits == 5
+
+
+def test_fusion():
+ circuit = Circuit(2)
+ circuit.add(gates.X(0))
+ circuit.add(gates.Z(0))
+ circuit.add(gates.Y(0))
+ circuit.add(gates.X(1))
+ fusion = Rearrange(max_qubits=1)
+ fused_circ = fusion(circuit)
+ assert isinstance(fused_circ.queue[0], gates.Unitary)
diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py
new file mode 100644
index 000000000..af69d4730
--- /dev/null
+++ b/tests/test_transpiler_pipeline.py
@@ -0,0 +1,313 @@
+import networkx as nx
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import ConnectivityError, TranspilerPipelineError
+from qibo.transpiler.optimizer import Preprocessing
+from qibo.transpiler.pipeline import (
+ Passes,
+ assert_circuit_equivalence,
+ assert_transpiling,
+ restrict_connectivity_qubits,
+)
+from qibo.transpiler.placer import Random, ReverseTraversal, Trivial
+from qibo.transpiler.router import Sabre, ShortestPaths
+from qibo.transpiler.unroller import NativeGates, Unroller
+
+
+def generate_random_circuit(nqubits, ngates, seed=None):
+ """Generate random circuits one-qubit rotations and CZ gates."""
+ if seed is not None: # pragma: no cover
+ np.random.seed(seed)
+
+ one_qubit_gates = [gates.RX, gates.RY, gates.RZ, gates.X, gates.Y, gates.Z, gates.H]
+ two_qubit_gates = [
+ gates.CNOT,
+ gates.CZ,
+ gates.SWAP,
+ gates.iSWAP,
+ gates.CRX,
+ gates.CRY,
+ gates.CRZ,
+ ]
+ n1, n2 = len(one_qubit_gates), len(two_qubit_gates)
+ n = n1 + n2 if nqubits > 1 else n1
+ circuit = Circuit(nqubits)
+ for _ in range(ngates):
+ igate = int(np.random.randint(0, n))
+ if igate >= n1:
+ q = tuple(np.random.randint(0, nqubits, 2))
+ while q[0] == q[1]:
+ q = tuple(np.random.randint(0, nqubits, 2))
+ gate = two_qubit_gates[igate - n1]
+ else:
+ q = (np.random.randint(0, nqubits),)
+ gate = one_qubit_gates[igate]
+ if issubclass(gate, gates.ParametrizedGate):
+ theta = 2 * np.pi * np.random.random()
+ circuit.add(gate(*q, theta=theta))
+ else:
+ circuit.add(gate(*q))
+ return circuit
+
+
+def star_connectivity():
+ Q = [i for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2]
+ chip.add_edges_from(graph_list)
+ return chip
+
+
+def test_restrict_qubits_error_no_subset():
+ with pytest.raises(ConnectivityError) as excinfo:
+ restrict_connectivity_qubits(star_connectivity(), [1, 2, 6])
+ assert "Some qubits are not in the original connectivity." in str(excinfo.value)
+
+
+def test_restrict_qubits_error_not_connected():
+ with pytest.raises(ConnectivityError) as excinfo:
+ restrict_connectivity_qubits(star_connectivity(), [1, 3])
+ assert "New connectivity graph is not connected." in str(excinfo.value)
+
+
+def test_restrict_qubits():
+ new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3])
+ assert list(new_connectivity.nodes) == [1, 2, 3]
+ assert list(new_connectivity.edges) == [(1, 2), (2, 3)]
+
+
+@pytest.mark.parametrize("ngates", [5, 10, 50])
+def test_pipeline_default(ngates):
+ circ = generate_random_circuit(nqubits=5, ngates=ngates)
+ default_transpiler = Passes(passes=None, connectivity=star_connectivity())
+ transpiled_circ, final_layout = default_transpiler(circ)
+ initial_layout = default_transpiler.get_initial_layout()
+ assert_transpiling(
+ original_circuit=circ,
+ transpiled_circuit=transpiled_circ,
+ connectivity=star_connectivity(),
+ initial_layout=initial_layout,
+ final_layout=final_layout,
+ native_gates=NativeGates.default(),
+ check_circuit_equivalence=False,
+ )
+
+
+def test_assert_circuit_equivalence_equal():
+ circ1 = Circuit(2)
+ circ2 = Circuit(2)
+ circ1.add(gates.X(0))
+ circ1.add(gates.CZ(0, 1))
+ circ2.add(gates.X(0))
+ circ2.add(gates.CZ(0, 1))
+ final_map = {"q0": 0, "q1": 1}
+ assert_circuit_equivalence(circ1, circ2, final_map=final_map)
+
+
+def test_assert_circuit_equivalence_swap():
+ circ1 = Circuit(2)
+ circ2 = Circuit(2)
+ circ1.add(gates.X(0))
+ circ2.add(gates.SWAP(0, 1))
+ circ2.add(gates.X(1))
+ final_map = {"q0": 1, "q1": 0}
+ assert_circuit_equivalence(circ1, circ2, final_map=final_map)
+
+
+def test_assert_circuit_equivalence_false():
+ circ1 = Circuit(2)
+ circ2 = Circuit(2)
+ circ1.add(gates.X(0))
+ circ2.add(gates.SWAP(0, 1))
+ circ2.add(gates.X(1))
+ final_map = {"q0": 0, "q1": 1}
+ with pytest.raises(TranspilerPipelineError):
+ assert_circuit_equivalence(circ1, circ2, final_map=final_map)
+
+
+def test_int_qubit_names():
+ circ = Circuit(2)
+ final_map = {i: i for i in range(5)}
+ default_transpiler = Passes(
+ passes=None, connectivity=star_connectivity(), int_qubit_names=True
+ )
+ _, final_layout = default_transpiler(circ)
+ assert final_map == final_layout
+
+
+def test_assert_circuit_equivalence_wrong_nqubits():
+ circ1 = Circuit(1)
+ circ2 = Circuit(2)
+ final_map = {"q0": 0, "q1": 1}
+ with pytest.raises(ValueError):
+ assert_circuit_equivalence(circ1, circ2, final_map=final_map)
+
+
+def test_error_connectivity():
+ with pytest.raises(TranspilerPipelineError):
+ default_transpiler = Passes(passes=None, connectivity=None)
+
+
+@pytest.mark.parametrize("qubits", [3, 5])
+def test_is_satisfied(qubits):
+ default_transpiler = Passes(passes=None, connectivity=star_connectivity())
+ circuit = Circuit(qubits)
+ circuit.add(gates.CZ(0, 2))
+ circuit.add(gates.Z(0))
+ assert default_transpiler.is_satisfied(circuit)
+
+
+def test_is_satisfied_false_decomposition():
+ default_transpiler = Passes(passes=None, connectivity=star_connectivity())
+ circuit = Circuit(5)
+ circuit.add(gates.CZ(0, 2))
+ circuit.add(gates.X(0))
+ assert not default_transpiler.is_satisfied(circuit)
+
+
+def test_is_satisfied_false_connectivity():
+ default_transpiler = Passes(passes=None, connectivity=star_connectivity())
+ circuit = Circuit(5)
+ circuit.add(gates.CZ(0, 1))
+ circuit.add(gates.Z(0))
+ assert not default_transpiler.is_satisfied(circuit)
+
+
+@pytest.mark.parametrize("qubits", [2, 5])
+@pytest.mark.parametrize("gates", [5, 20])
+@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal])
+@pytest.mark.parametrize("routing", [ShortestPaths, Sabre])
+def test_custom_passes(placer, routing, gates, qubits):
+ circ = generate_random_circuit(nqubits=qubits, ngates=gates)
+ custom_passes = []
+ custom_passes.append(Preprocessing(connectivity=star_connectivity()))
+ if placer == ReverseTraversal:
+ custom_passes.append(
+ placer(
+ connectivity=star_connectivity(),
+ routing_algorithm=routing(connectivity=star_connectivity()),
+ )
+ )
+ else:
+ custom_passes.append(placer(connectivity=star_connectivity()))
+ custom_passes.append(routing(connectivity=star_connectivity()))
+ custom_passes.append(Unroller(native_gates=NativeGates.default()))
+ custom_pipeline = Passes(
+ custom_passes,
+ connectivity=star_connectivity(),
+ native_gates=NativeGates.default(),
+ )
+ transpiled_circ, final_layout = custom_pipeline(circ)
+ initial_layout = custom_pipeline.get_initial_layout()
+ assert_transpiling(
+ original_circuit=circ,
+ transpiled_circuit=transpiled_circ,
+ connectivity=star_connectivity(),
+ initial_layout=initial_layout,
+ final_layout=final_layout,
+ native_gates=NativeGates.default(),
+ )
+
+
+@pytest.mark.parametrize("gates", [5, 20])
+@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal])
+@pytest.mark.parametrize("routing", [ShortestPaths, Sabre])
+def test_custom_passes_restict(gates, placer, routing):
+ circ = generate_random_circuit(nqubits=3, ngates=gates)
+ custom_passes = []
+ custom_passes.append(Preprocessing(connectivity=star_connectivity()))
+ if placer == ReverseTraversal:
+ custom_passes.append(
+ placer(
+ connectivity=star_connectivity(),
+ routing_algorithm=routing(connectivity=star_connectivity()),
+ )
+ )
+ else:
+ custom_passes.append(placer(connectivity=star_connectivity()))
+ custom_passes.append(routing(connectivity=star_connectivity()))
+ custom_passes.append(Unroller(native_gates=NativeGates.default()))
+ custom_pipeline = Passes(
+ custom_passes,
+ connectivity=star_connectivity(),
+ native_gates=NativeGates.default(),
+ on_qubits=[1, 2, 3],
+ )
+ transpiled_circ, final_layout = custom_pipeline(circ)
+ initial_layout = custom_pipeline.get_initial_layout()
+ assert_transpiling(
+ original_circuit=circ,
+ transpiled_circuit=transpiled_circ,
+ connectivity=restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]),
+ initial_layout=initial_layout,
+ final_layout=final_layout,
+ native_gates=NativeGates.default(),
+ )
+ assert transpiled_circ.wire_names == ["q1", "q2", "q3"]
+
+
+def test_custom_passes_multiple_placer():
+ custom_passes = []
+ custom_passes.append(Random(connectivity=star_connectivity()))
+ custom_passes.append(Trivial(connectivity=star_connectivity()))
+ custom_pipeline = Passes(
+ custom_passes,
+ connectivity=star_connectivity(),
+ native_gates=NativeGates.default(),
+ )
+ circ = generate_random_circuit(nqubits=5, ngates=20)
+ with pytest.raises(TranspilerPipelineError):
+ transpiled_circ, final_layout = custom_pipeline(circ)
+
+
+def test_custom_passes_no_placer():
+ custom_passes = []
+ custom_passes.append(ShortestPaths(connectivity=star_connectivity()))
+ custom_pipeline = Passes(
+ custom_passes,
+ connectivity=star_connectivity(),
+ native_gates=NativeGates.default(),
+ )
+ circ = generate_random_circuit(nqubits=5, ngates=20)
+ with pytest.raises(TranspilerPipelineError):
+ transpiled_circ, final_layout = custom_pipeline(circ)
+
+
+def test_custom_passes_wrong_pass():
+ custom_passes = [0]
+ custom_pipeline = Passes(passes=custom_passes, connectivity=None)
+ circ = generate_random_circuit(nqubits=5, ngates=5)
+ with pytest.raises(TranspilerPipelineError):
+ transpiled_circ, final_layout = custom_pipeline(circ)
+
+
+def test_int_qubit_names():
+ connectivity = star_connectivity()
+ transpiler = Passes(
+ connectivity=connectivity,
+ passes=[
+ Preprocessing(connectivity),
+ Random(connectivity, seed=0),
+ Sabre(connectivity),
+ Unroller(NativeGates.default()),
+ ],
+ int_qubit_names=True,
+ )
+ circuit = Circuit(1)
+ circuit.add(gates.I(0))
+ circuit.add(gates.H(0))
+ circuit.add(gates.M(0))
+ transpiled_circuit, final_map = transpiler(circuit)
+ initial_layout = transpiler.get_initial_layout()
+ assert_transpiling(
+ original_circuit=circuit,
+ transpiled_circuit=transpiled_circuit,
+ connectivity=connectivity,
+ initial_layout=initial_layout,
+ final_layout=final_map,
+ native_gates=NativeGates.default(),
+ )
diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py
new file mode 100644
index 000000000..3d19c03d1
--- /dev/null
+++ b/tests/test_transpiler_placer.py
@@ -0,0 +1,353 @@
+import networkx as nx
+import pytest
+
+from qibo import gates
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import PlacementError
+from qibo.transpiler.pipeline import restrict_connectivity_qubits
+from qibo.transpiler.placer import (
+ Custom,
+ Random,
+ ReverseTraversal,
+ StarConnectivityPlacer,
+ Subgraph,
+ Trivial,
+ _find_gates_qubits_pairs,
+ assert_mapping_consistency,
+ assert_placement,
+)
+from qibo.transpiler.router import ShortestPaths
+
+
+def star_connectivity():
+ Q = [i for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [
+ (Q[0], Q[2]),
+ (Q[1], Q[2]),
+ (Q[3], Q[2]),
+ (Q[4], Q[2]),
+ ]
+ chip.add_edges_from(graph_list)
+ return chip
+
+
+def star_circuit():
+ circuit = Circuit(5)
+ for i in range(1, 5):
+ circuit.add(gates.CNOT(i, 0))
+ return circuit
+
+
+@pytest.mark.parametrize("connectivity", [star_connectivity(), None])
+@pytest.mark.parametrize(
+ "layout",
+ [{"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4}, {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}],
+)
+def test_assert_placement_true(layout, connectivity):
+ circuit = Circuit(5)
+ assert_placement(circuit, layout, connectivity=connectivity)
+
+
+@pytest.mark.parametrize("qubits", [5, 3])
+@pytest.mark.parametrize(
+ "layout", [{"q0": 0, "q1": 1, "q2": 2, "q3": 3}, {"q0": 0, "q0": 1, "q2": 2}]
+)
+def test_assert_placement_false(qubits, layout):
+ circuit = Circuit(qubits)
+ with pytest.raises(PlacementError):
+ assert_placement(circuit, layout)
+
+
+@pytest.mark.parametrize(
+ "layout",
+ [{"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4}, {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}],
+)
+def test_mapping_consistency(layout):
+ assert_mapping_consistency(layout)
+
+
+@pytest.mark.parametrize(
+ "layout",
+ [
+ {"q0": 0, "q1": 0, "q2": 1, "q3": 4, "q4": 3},
+ {"q0": 0, "q1": 2, "q0": 1, "q3": 4, "q4": 3},
+ ],
+)
+def test_mapping_consistency_error(layout):
+ with pytest.raises(PlacementError):
+ assert_mapping_consistency(layout)
+
+
+def test_mapping_consistency_restricted():
+ layout = {"q0": 0, "q2": 1}
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2])
+ assert_mapping_consistency(layout, restricted_connectivity)
+
+
+@pytest.mark.parametrize(
+ "layout",
+ [
+ {"q0": 0, "q2": 2},
+ {"q0": 0, "q1": 1},
+ ],
+)
+def test_mapping_consistency_restricted_error(layout):
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2])
+ with pytest.raises(PlacementError):
+ assert_mapping_consistency(layout, restricted_connectivity)
+
+
+def test_gates_qubits_pairs():
+ circuit = Circuit(5)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.M(1, 2))
+ gates_qubits_pairs = _find_gates_qubits_pairs(circuit)
+ assert gates_qubits_pairs == [[0, 1], [1, 2]]
+
+
+def test_gates_qubits_pairs_error():
+ circuit = Circuit(5)
+ circuit.add(gates.TOFFOLI(0, 1, 2))
+ with pytest.raises(ValueError):
+ gates_qubits_pairs = _find_gates_qubits_pairs(circuit)
+
+
+def test_trivial():
+ circuit = Circuit(5)
+ connectivity = star_connectivity()
+ placer = Trivial(connectivity=connectivity)
+ layout = placer(circuit)
+ assert layout == {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4}
+ assert_placement(circuit, layout)
+
+
+def test_trivial_restricted():
+ circuit = Circuit(2)
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2])
+ placer = Trivial(connectivity=restricted_connectivity)
+ layout = placer(circuit)
+ assert layout == {"q0": 0, "q2": 1}
+ assert_placement(
+ circuit=circuit, layout=layout, connectivity=restricted_connectivity
+ )
+
+
+def test_trivial_error():
+ circuit = Circuit(4)
+ connectivity = star_connectivity()
+ placer = Trivial(connectivity=connectivity)
+ with pytest.raises(PlacementError):
+ layout = placer(circuit)
+
+
+@pytest.mark.parametrize(
+ "custom_layout", [[4, 3, 2, 1, 0], {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0}]
+)
+@pytest.mark.parametrize("give_circuit", [True, False])
+@pytest.mark.parametrize("give_connectivity", [True, False])
+def test_custom(custom_layout, give_circuit, give_connectivity):
+ if give_circuit:
+ circuit = Circuit(5)
+ else:
+ circuit = None
+ if give_connectivity:
+ connectivity = star_connectivity()
+ else:
+ connectivity = None
+ placer = Custom(connectivity=connectivity, initial_map=custom_layout)
+ layout = placer(circuit)
+ assert layout == {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0}
+
+
+@pytest.mark.parametrize("custom_layout", [[1, 0], {"q0": 1, "q2": 0}])
+def test_custom_restricted(custom_layout):
+ circuit = Circuit(2)
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2])
+ placer = Custom(connectivity=restricted_connectivity, initial_map=custom_layout)
+ layout = placer(circuit)
+ assert layout == {"q0": 1, "q2": 0}
+ assert_placement(
+ circuit=circuit, layout=layout, connectivity=restricted_connectivity
+ )
+
+
+def test_custom_error_circuit():
+ circuit = Circuit(3)
+ custom_layout = [4, 3, 2, 1, 0]
+ connectivity = star_connectivity()
+ placer = Custom(connectivity=connectivity, initial_map=custom_layout)
+ with pytest.raises(PlacementError):
+ layout = placer(circuit)
+
+
+def test_custom_error_no_circuit():
+ connectivity = star_connectivity()
+ custom_layout = {"q0": 4, "q1": 3, "q2": 2, "q3": 0, "q4": 0}
+ placer = Custom(connectivity=connectivity, initial_map=custom_layout)
+ with pytest.raises(PlacementError):
+ layout = placer()
+
+
+def test_custom_error_type():
+ circuit = Circuit(5)
+ connectivity = star_connectivity()
+ layout = 1
+ placer = Custom(connectivity=connectivity, initial_map=layout)
+ with pytest.raises(TypeError):
+ layout = placer(circuit)
+
+
+def test_subgraph_perfect():
+ connectivity = star_connectivity()
+ placer = Subgraph(connectivity=connectivity)
+ layout = placer(star_circuit())
+ assert layout["q2"] == 0
+ assert_placement(star_circuit(), layout)
+
+
+def imperfect_circuit():
+ circuit = Circuit(5)
+ circuit.add(gates.CNOT(1, 3))
+ circuit.add(gates.CNOT(2, 4))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(4, 3))
+ circuit.add(gates.CNOT(3, 2))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(4, 3))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(3, 1))
+ return circuit
+
+
+def test_subgraph_non_perfect():
+ connectivity = star_connectivity()
+ placer = Subgraph(connectivity=connectivity)
+ layout = placer(imperfect_circuit())
+ assert_placement(imperfect_circuit(), layout)
+
+
+def test_subgraph_error():
+ connectivity = star_connectivity()
+ placer = Subgraph(connectivity=connectivity)
+ circuit = Circuit(5)
+ with pytest.raises(ValueError):
+ layout = placer(circuit)
+
+
+def test_subgraph_restricted():
+ circuit = Circuit(4)
+ circuit.add(gates.CNOT(0, 3))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.CNOT(3, 2))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(3, 1))
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4])
+ placer = Subgraph(connectivity=restricted_connectivity)
+ layout = placer(circuit)
+ assert_placement(
+ circuit=circuit, layout=layout, connectivity=restricted_connectivity
+ )
+
+
+@pytest.mark.parametrize("reps", [1, 10, 100])
+def test_random(reps):
+ connectivity = star_connectivity()
+ placer = Random(connectivity=connectivity, samples=reps)
+ layout = placer(star_circuit())
+ assert_placement(star_circuit(), layout)
+
+
+def test_random_perfect():
+ circ = Circuit(5)
+ circ.add(gates.CZ(0, 1))
+ connectivity = star_connectivity()
+ placer = Random(connectivity=connectivity, samples=1000)
+ layout = placer(circ)
+ assert_placement(star_circuit(), layout)
+
+
+def test_random_restricted():
+ circuit = Circuit(4)
+ circuit.add(gates.CNOT(1, 3))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(3, 2))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(3, 1))
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4])
+ placer = Random(connectivity=restricted_connectivity, samples=100)
+ layout = placer(circuit)
+ assert_placement(
+ circuit=circuit, layout=layout, connectivity=restricted_connectivity
+ )
+
+
+@pytest.mark.parametrize("gates", [None, 5, 13])
+def test_reverse_traversal(gates):
+ circuit = star_circuit()
+ connectivity = star_connectivity()
+ routing = ShortestPaths(connectivity=connectivity)
+ placer = ReverseTraversal(connectivity, routing, depth=gates)
+ layout = placer(circuit)
+ assert_placement(circuit, layout)
+
+
+def test_reverse_traversal_no_gates():
+ connectivity = star_connectivity()
+ routing = ShortestPaths(connectivity=connectivity)
+ placer = ReverseTraversal(connectivity, routing, depth=10)
+ circuit = Circuit(5)
+ with pytest.raises(ValueError):
+ layout = placer(circuit)
+
+
+def test_reverse_traversal_restricted():
+ circuit = Circuit(4)
+ circuit.add(gates.CNOT(1, 3))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(3, 2))
+ circuit.add(gates.CNOT(2, 1))
+ circuit.add(gates.CNOT(1, 2))
+ circuit.add(gates.CNOT(3, 1))
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4])
+ routing = ShortestPaths(connectivity=restricted_connectivity)
+ placer = ReverseTraversal(
+ connectivity=restricted_connectivity, routing_algorithm=routing, depth=5
+ )
+ layout = placer(circuit)
+ assert_placement(
+ circuit=circuit, layout=layout, connectivity=restricted_connectivity
+ )
+
+
+def test_star_connectivity_placer():
+ circ = Circuit(3)
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.CZ(0, 2))
+ placer = StarConnectivityPlacer(middle_qubit=2)
+ layout = placer(circ)
+ assert_placement(circ, layout)
+ assert layout == {"q0": 0, "q1": 2, "q2": 1}
+
+
+@pytest.mark.parametrize("first", [True, False])
+def test_star_connectivity_placer_error(first):
+ circ = Circuit(3)
+ if first:
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.TOFFOLI(0, 1, 2))
+ placer = StarConnectivityPlacer(middle_qubit=2)
+ with pytest.raises(PlacementError):
+ layout = placer(circ)
diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py
new file mode 100644
index 000000000..c6a011fa4
--- /dev/null
+++ b/tests/test_transpiler_router.py
@@ -0,0 +1,474 @@
+import itertools
+
+import networkx as nx
+import numpy as np
+import pytest
+
+from qibo import gates
+from qibo.backends import NumpyBackend
+from qibo.models import Circuit
+from qibo.quantum_info.random_ensembles import random_unitary
+from qibo.transpiler._exceptions import ConnectivityError
+from qibo.transpiler.optimizer import Preprocessing
+from qibo.transpiler.pipeline import (
+ assert_circuit_equivalence,
+ restrict_connectivity_qubits,
+)
+from qibo.transpiler.placer import (
+ Custom,
+ Random,
+ StarConnectivityPlacer,
+ Subgraph,
+ Trivial,
+ assert_placement,
+)
+from qibo.transpiler.router import (
+ CircuitMap,
+ Sabre,
+ ShortestPaths,
+ StarConnectivityRouter,
+ assert_connectivity,
+)
+
+
+def star_connectivity(middle_qubit=2):
+ Q = [i for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [(Q[i], Q[middle_qubit]) for i in range(5) if i != middle_qubit]
+ chip.add_edges_from(graph_list)
+ return chip
+
+
+def grid_connectivity():
+ Q = [i for i in range(5)]
+ chip = nx.Graph()
+ chip.add_nodes_from(Q)
+ graph_list = [(Q[0], Q[1]), (Q[1], Q[2]), (Q[2], Q[3]), (Q[3], Q[0]), (Q[0], Q[4])]
+ chip.add_edges_from(graph_list)
+ return chip
+
+
+def generate_random_circuit(nqubits, ngates, seed=42):
+ """Generate a random circuit with RX and CZ gates."""
+ np.random.seed(seed)
+ one_qubit_gates = [gates.RX, gates.RY, gates.RZ]
+ two_qubit_gates = [gates.CZ, gates.CNOT, gates.SWAP]
+ n1, n2 = len(one_qubit_gates), len(two_qubit_gates)
+ n = n1 + n2 if nqubits > 1 else n1
+ circuit = Circuit(nqubits)
+ for _ in range(ngates):
+ igate = int(np.random.randint(0, n))
+ if igate >= n1:
+ q = tuple(np.random.randint(0, nqubits, 2))
+ while q[0] == q[1]:
+ q = tuple(np.random.randint(0, nqubits, 2))
+ gate = two_qubit_gates[igate - n1]
+ else:
+ q = (np.random.randint(0, nqubits),)
+ gate = one_qubit_gates[igate]
+ if issubclass(gate, gates.ParametrizedGate):
+ theta = 2 * np.pi * np.random.random()
+ circuit.add(gate(*q, theta=theta, trainable=False))
+ else:
+ circuit.add(gate(*q))
+ return circuit
+
+
+def star_circuit():
+ circuit = Circuit(5)
+ for i in range(1, 5):
+ circuit.add(gates.CNOT(i, 0))
+ return circuit
+
+
+def matched_circuit():
+ """Return a simple circuit that can be executed on star connectivity"""
+ circuit = Circuit(5)
+ circuit.add(gates.CZ(0, 2))
+ circuit.add(gates.CZ(1, 2))
+ circuit.add(gates.Z(1))
+ circuit.add(gates.CZ(2, 1))
+ circuit.add(gates.M(0))
+ return circuit
+
+
+def test_assert_connectivity():
+ assert_connectivity(star_connectivity(), matched_circuit())
+
+
+def test_assert_connectivity_false():
+ circuit = Circuit(5)
+ circuit.add(gates.CZ(0, 1))
+ with pytest.raises(ConnectivityError):
+ assert_connectivity(star_connectivity(), circuit)
+
+
+def test_assert_connectivity_3q():
+ circuit = Circuit(5)
+ circuit.add(gates.TOFFOLI(0, 1, 2))
+ with pytest.raises(ConnectivityError):
+ assert_connectivity(star_connectivity(), circuit)
+
+
+@pytest.mark.parametrize("gates", [5, 25])
+@pytest.mark.parametrize("placer", [Trivial, Random])
+@pytest.mark.parametrize("connectivity", [star_connectivity(), grid_connectivity()])
+def test_random_circuits_5q(gates, placer, connectivity):
+ placer = placer(connectivity=connectivity)
+ layout_circ = Circuit(5)
+ initial_layout = placer(layout_circ)
+ transpiler = ShortestPaths(connectivity=connectivity)
+ circuit = generate_random_circuit(nqubits=5, ngates=gates)
+ transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout)
+ assert transpiler.added_swaps >= 0
+ assert_connectivity(connectivity, transpiled_circuit)
+ assert_placement(transpiled_circuit, final_qubit_map)
+ assert gates + transpiler.added_swaps == transpiled_circuit.ngates
+ qubit_matcher = Preprocessing(connectivity=connectivity)
+ new_circuit = qubit_matcher(circuit=circuit)
+ assert_circuit_equivalence(
+ original_circuit=new_circuit,
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
+
+
+def test_star_circuit():
+ placer = Subgraph(star_connectivity())
+ initial_layout = placer(star_circuit())
+ transpiler = ShortestPaths(connectivity=star_connectivity())
+ transpiled_circuit, final_qubit_map = transpiler(star_circuit(), initial_layout)
+ assert transpiler.added_swaps == 0
+ assert_connectivity(star_connectivity(), transpiled_circuit)
+ assert_placement(transpiled_circuit, final_qubit_map)
+ assert_circuit_equivalence(
+ original_circuit=star_circuit(),
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
+
+
+def test_star_circuit_custom_map():
+ placer = Custom(initial_map=[1, 0, 2, 3, 4], connectivity=star_connectivity())
+ initial_layout = placer()
+ transpiler = ShortestPaths(connectivity=star_connectivity())
+ transpiled_circuit, final_qubit_map = transpiler(star_circuit(), initial_layout)
+ assert transpiler.added_swaps == 1
+ assert_connectivity(star_connectivity(), transpiled_circuit)
+ assert_placement(transpiled_circuit, final_qubit_map)
+ assert_circuit_equivalence(
+ original_circuit=star_circuit(),
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
+
+
+def test_routing_with_measurements():
+ placer = Trivial(connectivity=star_connectivity())
+ circuit = Circuit(5)
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.M(0, 2, 3))
+ initial_layout = placer(circuit=circuit)
+ transpiler = ShortestPaths(connectivity=star_connectivity())
+ transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout)
+ assert transpiled_circuit.ngates == 3
+ measured_qubits = transpiled_circuit.queue[2].qubits
+ assert measured_qubits == (0, 1, 3)
+ assert_circuit_equivalence(
+ original_circuit=circuit,
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
+
+
+def test_sabre_looping():
+ # Setup where the looping occurs
+ # Line connectivity, gates with gate_array, Trivial placer
+ gate_array = [(7, 2), (6, 0), (5, 6), (4, 8), (3, 5), (9, 1)]
+ loop_circ = Circuit(10)
+ for qubits in gate_array:
+ loop_circ.add(gates.CZ(*qubits))
+
+ chip = nx.Graph()
+ chip.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+ chip.add_edges_from(
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]
+ )
+
+ placer = Trivial(connectivity=chip)
+ initial_layout = placer(loop_circ)
+ router_no_threshold = Sabre(
+ connectivity=chip, swap_threshold=np.inf
+ ) # Without reset
+ router_threshold = Sabre(connectivity=chip) # With reset
+
+ routed_no_threshold, final_mapping_no_threshold = router_no_threshold(
+ loop_circ, initial_layout=initial_layout
+ )
+ routed_threshold, final_mapping_threshold = router_threshold(
+ loop_circ, initial_layout=initial_layout
+ )
+
+ count_no_threshold = router_no_threshold.added_swaps
+ count_threshold = router_threshold.added_swaps
+
+ assert count_no_threshold > count_threshold
+ assert_circuit_equivalence(
+ original_circuit=loop_circ,
+ transpiled_circuit=routed_no_threshold,
+ final_map=final_mapping_no_threshold,
+ initial_map=initial_layout,
+ )
+ assert_circuit_equivalence(
+ original_circuit=loop_circ,
+ transpiled_circuit=routed_threshold,
+ final_map=final_mapping_threshold,
+ initial_map=initial_layout,
+ )
+
+
+def test_sabre_shortest_path_routing():
+ gate_array = [(0, 9), (5, 9), (2, 8)] # The gate (2, 8) should be routed next
+
+ loop_circ = Circuit(10)
+ for qubits in gate_array:
+ loop_circ.add(gates.CZ(*qubits))
+
+ # line connectivity
+ chip = nx.Graph()
+ chip.add_nodes_from(range(10))
+ chip.add_edges_from(
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]
+ )
+
+ placer = Trivial(connectivity=chip)
+ initial_layout = placer(loop_circ)
+ router = Sabre(connectivity=chip)
+
+ router._preprocessing(circuit=loop_circ, initial_layout=initial_layout)
+ router._shortest_path_routing() # q2 should be moved adjacent to q8
+
+ gate_28 = router.circuit.circuit_blocks.block_list[2]
+ gate_28_qubits = router.circuit.get_physical_qubits(gate_28)
+
+ # Check if the physical qubits of the gate (2, 8) are adjacent
+ assert gate_28_qubits[1] in list(router.connectivity.neighbors(gate_28_qubits[0]))
+ assert gate_28_qubits[0] in list(router.connectivity.neighbors(gate_28_qubits[1]))
+
+
+def test_circuit_map():
+ circ = Circuit(4)
+ circ.add(gates.H(1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.H(0))
+ circ.add(gates.CZ(1, 2))
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(2, 3))
+ initial_layout = {"q0": 2, "q1": 0, "q2": 1, "q3": 3}
+ circuit_map = CircuitMap(initial_layout=initial_layout, circuit=circ)
+ block_list = circuit_map.circuit_blocks
+ # test blocks_qubits_pairs
+ assert circuit_map.blocks_qubits_pairs() == [(0, 1), (1, 2), (0, 1), (2, 3)]
+ # test execute_block and routed_circuit
+ circuit_map.execute_block(block_list.search_by_index(0))
+ routed_circuit = circuit_map.routed_circuit()
+ assert isinstance(routed_circuit.queue[0], gates.H)
+ assert len(routed_circuit.queue) == 4
+ assert routed_circuit.queue[2].qubits == (1, 2)
+ # test update
+ circuit_map.update((0, 2))
+ routed_circuit = circuit_map.routed_circuit()
+ assert isinstance(routed_circuit.queue[4], gates.SWAP)
+ assert routed_circuit.queue[4].qubits == (1, 0)
+ assert circuit_map._swaps == 1
+ assert circuit_map._circuit_logical == [2, 1, 0, 3]
+ circuit_map.update((1, 2))
+ routed_circuit = circuit_map.routed_circuit()
+ assert routed_circuit.queue[5].qubits == (2, 0)
+ assert circuit_map._circuit_logical == [1, 2, 0, 3]
+ # test execute_block after multiple swaps
+ circuit_map.execute_block(block_list.search_by_index(1))
+ circuit_map.execute_block(block_list.search_by_index(2))
+ circuit_map.execute_block(block_list.search_by_index(3))
+ routed_circuit = circuit_map.routed_circuit()
+ assert isinstance(routed_circuit.queue[6], gates.CZ)
+ # circuit to logical map: [1,2,0,3]. initial map: {"q0": 2, "q1": 0, "q2": 1, "q3": 3}.
+ assert routed_circuit.queue[6].qubits == (0, 1) # initial circuit qubits (1,2)
+ assert routed_circuit.queue[7].qubits == (2, 0) # (0,1)
+ assert routed_circuit.queue[8].qubits == (1, 3) # (2,3)
+ assert len(circuit_map.circuit_blocks()) == 0
+ # test final layout
+ assert circuit_map.final_layout() == {"q0": 1, "q1": 2, "q2": 0, "q3": 3}
+
+
+def test_sabre_matched():
+ placer = Trivial()
+ layout_circ = Circuit(5)
+ initial_layout = placer(layout_circ)
+ router = Sabre(connectivity=star_connectivity())
+ routed_circuit, final_map = router(
+ circuit=matched_circuit(), initial_layout=initial_layout
+ )
+ assert router.added_swaps == 0
+ assert final_map == {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4}
+ assert_connectivity(circuit=routed_circuit, connectivity=star_connectivity())
+ assert_circuit_equivalence(
+ original_circuit=matched_circuit(),
+ transpiled_circuit=routed_circuit,
+ final_map=final_map,
+ initial_map=initial_layout,
+ )
+
+
+@pytest.mark.parametrize("seed", [42])
+def test_sabre_simple(seed):
+ placer = Trivial()
+ circ = Circuit(5)
+ circ.add(gates.CZ(0, 1))
+ initial_layout = placer(circ)
+ router = Sabre(connectivity=star_connectivity(), seed=seed)
+ routed_circuit, final_map = router(circuit=circ, initial_layout=initial_layout)
+ assert router.added_swaps == 1
+ assert final_map == {"q0": 2, "q1": 1, "q2": 0, "q3": 3, "q4": 4}
+ assert routed_circuit.queue[0].qubits == (0, 2)
+ assert isinstance(routed_circuit.queue[0], gates.SWAP)
+ assert isinstance(routed_circuit.queue[1], gates.CZ)
+ assert_connectivity(circuit=routed_circuit, connectivity=star_connectivity())
+ assert_circuit_equivalence(
+ original_circuit=circ,
+ transpiled_circuit=routed_circuit,
+ final_map=final_map,
+ initial_map=initial_layout,
+ )
+
+
+@pytest.mark.parametrize("n_gates", [10, 40])
+@pytest.mark.parametrize("look", [0, 5])
+@pytest.mark.parametrize("decay", [0.5, 1.0])
+@pytest.mark.parametrize("placer", [Trivial, Random])
+@pytest.mark.parametrize("connectivity", [star_connectivity(), grid_connectivity()])
+def test_sabre_random_circuits(n_gates, look, decay, placer, connectivity):
+ placer = placer(connectivity=connectivity)
+ layout_circ = Circuit(5)
+ initial_layout = placer(layout_circ)
+ router = Sabre(connectivity=connectivity, lookahead=look, decay_lookahead=decay)
+ circuit = generate_random_circuit(nqubits=5, ngates=n_gates)
+ measurement = gates.M(*range(5))
+ circuit.add(measurement)
+ transpiled_circuit, final_qubit_map = router(circuit, initial_layout)
+ assert router.added_swaps >= 0
+ assert_connectivity(connectivity, transpiled_circuit)
+ assert_placement(transpiled_circuit, final_qubit_map)
+ assert n_gates + router.added_swaps + 1 == transpiled_circuit.ngates
+ assert_circuit_equivalence(
+ original_circuit=circuit,
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
+ assert transpiled_circuit.queue[-1].register_name == measurement.register_name
+
+
+def test_sabre_memory_map():
+ placer = Trivial()
+ layout_circ = Circuit(5)
+ initial_layout = placer(layout_circ)
+ router = Sabre(connectivity=star_connectivity())
+ router._preprocessing(circuit=star_circuit(), initial_layout=initial_layout)
+ router._memory_map = [[1, 0, 2, 3, 4]]
+ value = router._compute_cost((0, 1))
+ assert value == float("inf")
+
+
+def test_sabre_intermediate_measurements():
+ measurement = gates.M(1)
+ circ = Circuit(3, density_matrix=True)
+ circ.add(gates.H(0))
+ circ.add(measurement)
+ circ.add(gates.CNOT(0, 2))
+ connectivity = nx.Graph()
+ connectivity.add_nodes_from([0, 1, 2])
+ connectivity.add_edges_from([(0, 1), (1, 2)])
+ router = Sabre(connectivity=connectivity)
+ initial_layout = {"q0": 0, "q1": 1, "q2": 2}
+ routed_circ, _ = router(circuit=circ, initial_layout=initial_layout)
+ assert routed_circ.queue[3].register_name == measurement.register_name
+
+
+@pytest.mark.parametrize("router_algorithm", [Sabre, ShortestPaths])
+def test_restrict_qubits(router_algorithm):
+ circ = Circuit(3)
+ circ.add(gates.CZ(0, 1))
+ circ.add(gates.CZ(0, 2))
+ circ.add(gates.CZ(2, 1))
+ initial_layout = {"q0": 0, "q2": 2, "q3": 1}
+ connectivity = star_connectivity()
+ restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3])
+ router = router_algorithm(connectivity=restricted_connectivity)
+ routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout)
+ assert_circuit_equivalence(
+ original_circuit=circ,
+ transpiled_circuit=routed_circ,
+ final_map=final_layout,
+ initial_map=initial_layout,
+ )
+ assert_connectivity(restricted_connectivity, routed_circ)
+ assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity)
+ assert routed_circ.wire_names == ["q0", "q2", "q3"]
+
+
+def test_star_error_multi_qubit():
+ circuit = Circuit(3)
+ circuit.add(gates.TOFFOLI(0, 1, 2))
+ transpiler = StarConnectivityRouter(middle_qubit=2)
+ with pytest.raises(ConnectivityError):
+ transpiled, hardware_qubits = transpiler(
+ initial_layout={"q0": 0, "q1": 1, "q2": 2}, circuit=circuit
+ )
+
+
+@pytest.mark.parametrize("nqubits", [1, 3, 5])
+@pytest.mark.parametrize("middle_qubit", [0, 2, 4])
+@pytest.mark.parametrize("depth", [2, 10])
+@pytest.mark.parametrize("measurements", [True, False])
+@pytest.mark.parametrize("unitaries", [True, False])
+def test_star_router(nqubits, depth, middle_qubit, measurements, unitaries):
+ unitary_dim = min(2, nqubits)
+ connectivity = star_connectivity(middle_qubit)
+ if unitaries:
+ circuit = Circuit(nqubits)
+ pairs = list(itertools.combinations(range(nqubits), unitary_dim))
+ for _ in range(depth):
+ qubits = pairs[int(np.random.randint(len(pairs)))]
+ circuit.add(
+ gates.Unitary(
+ random_unitary(2**unitary_dim, backend=NumpyBackend()), *qubits
+ )
+ )
+ else:
+ circuit = generate_random_circuit(nqubits, depth)
+ if measurements:
+ circuit.add(gates.M(0))
+ transpiler = StarConnectivityRouter(middle_qubit=middle_qubit)
+ placer = StarConnectivityPlacer(middle_qubit=middle_qubit)
+ initial_layout = placer(circuit=circuit)
+ transpiled_circuit, final_qubit_map = transpiler(
+ circuit=circuit, initial_layout=initial_layout
+ )
+ assert_connectivity(connectivity, transpiled_circuit)
+ assert_placement(transpiled_circuit, final_qubit_map)
+ matched_original = Circuit(max(circuit.nqubits, middle_qubit + 1))
+ for gate in circuit.queue:
+ matched_original.add(gate)
+ assert_circuit_equivalence(
+ original_circuit=matched_original,
+ transpiled_circuit=transpiled_circuit,
+ final_map=final_qubit_map,
+ initial_map=initial_layout,
+ )
diff --git a/tests/test_transpiler_unitary_decompositions.py b/tests/test_transpiler_unitary_decompositions.py
new file mode 100644
index 000000000..df641208b
--- /dev/null
+++ b/tests/test_transpiler_unitary_decompositions.py
@@ -0,0 +1,227 @@
+import numpy as np
+import pytest
+from scipy.linalg import expm
+
+from qibo import Circuit, gates, matrices
+from qibo.config import PRECISION_TOL
+from qibo.quantum_info.linalg_operations import partial_trace
+from qibo.quantum_info.metrics import purity
+from qibo.quantum_info.random_ensembles import random_unitary
+from qibo.transpiler.unitary_decompositions import (
+ bell_basis,
+ calculate_h_vector,
+ calculate_psi,
+ calculate_single_qubit_unitaries,
+ cnot_decomposition,
+ cnot_decomposition_light,
+ magic_basis,
+ magic_decomposition,
+ to_bell_diagonal,
+ two_qubit_decomposition,
+)
+
+
+def bell_unitary(hx, hy, hz, backend):
+ ham = (
+ hx * backend.cast(np.kron(matrices.X, matrices.X))
+ + hy * backend.cast(np.kron(matrices.Y, matrices.Y))
+ + hz * backend.cast(np.kron(matrices.Z, matrices.Z))
+ )
+ return backend.cast(expm(-1j * backend.to_numpy(ham)))
+
+
+def assert_single_qubits(backend, psi, ua, ub):
+ """Assert UA, UB map the maximally entangled basis ``psi`` to the magic basis."""
+ uaub = backend.to_numpy(backend.np.kron(ua, ub))
+ psi = backend.to_numpy(psi)
+ for i, j in zip(range(4), [0, 1, 3, 2]):
+ final_state = np.matmul(uaub, psi[:, i])
+ target_state = magic_basis[:, j]
+ fidelity = np.abs(np.dot(np.conj(target_state), final_state))
+ backend.assert_allclose(fidelity, 1)
+
+
+def test_u3_decomposition(backend):
+ theta, phi, lam = 0.1, 0.2, 0.3
+ u3_matrix = gates.U3(0, theta, phi, lam).matrix(backend)
+
+ rz1 = gates.RZ(0, phi).matrix(backend)
+ rz2 = gates.RZ(0, theta).matrix(backend)
+ rz3 = gates.RZ(0, lam).matrix(backend)
+ rx1 = gates.RX(0, -np.pi / 2).matrix(backend)
+ rx2 = gates.RX(0, np.pi / 2).matrix(backend)
+
+ target_matrix = rz1 @ rx1 @ rz2 @ rx2 @ rz3
+
+ backend.assert_allclose(u3_matrix, target_matrix)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_eigenbasis_entanglement(backend, seed):
+ unitary = random_unitary(4, seed=seed, backend=backend)
+
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ calculate_psi(unitary, backend=backend)
+ else:
+ """Check that the eigenvectors of UT_U are maximally entangled."""
+ states, eigvals = calculate_psi(unitary, backend=backend)
+ eigvals = backend.cast(eigvals, dtype=eigvals.dtype)
+ backend.assert_allclose(backend.np.abs(eigvals), np.ones(4))
+ for state in states.T:
+ state = partial_trace(state, [1], backend=backend)
+ backend.assert_allclose(purity(state, backend=backend), 0.5)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_v_decomposition(backend, seed):
+ """Check that V_A V_B |psi_k> = |phi_k> according to Lemma 1."""
+ unitary = random_unitary(4, seed=seed, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ calculate_psi(unitary, backend=backend)
+ else:
+ psi, _ = calculate_psi(unitary, backend=backend)
+ va, vb = calculate_single_qubit_unitaries(psi, backend=backend)
+ assert_single_qubits(backend, psi, va, vb)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_u_decomposition(backend, seed):
+ r"""Check that U_A\dagger U_B\dagger |psi_k tilde> = |phi_k> according to Lemma 1."""
+ unitary = random_unitary(4, seed=seed, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ calculate_psi(unitary, backend=backend)
+ else:
+ psi, eigvals = calculate_psi(unitary, backend=backend)
+ psi_tilde = backend.np.conj(backend.np.sqrt(eigvals)) * backend.np.matmul(
+ unitary, psi
+ )
+ ua_dagger, ub_dagger = calculate_single_qubit_unitaries(
+ psi_tilde, backend=backend
+ )
+ assert_single_qubits(backend, psi_tilde, ua_dagger, ub_dagger)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_ud_eigenvalues(backend, seed):
+ """Check that U_d is diagonal in the Bell basis."""
+ unitary = random_unitary(4, seed=seed, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ magic_decomposition(unitary, backend=backend)
+ else:
+ ua, ub, ud, va, vb = magic_decomposition(unitary, backend=backend)
+ # Check kron
+ unitary_recon = backend.np.kron(ua, ub) @ ud @ backend.np.kron(va, vb)
+ backend.assert_allclose(unitary_recon, unitary)
+
+ ud_bell = (
+ backend.np.transpose(backend.np.conj(backend.cast(bell_basis)), (1, 0))
+ @ ud
+ @ backend.cast(bell_basis)
+ )
+ ud_diag = backend.np.diag(ud_bell)
+ backend.assert_allclose(backend.np.diag(ud_diag), ud_bell, atol=PRECISION_TOL)
+ backend.assert_allclose(backend.np.prod(ud_diag), 1)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_calculate_h_vector(backend, seed):
+ unitary = random_unitary(4, seed=seed, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ magic_decomposition(unitary, backend=backend)
+ else:
+ _, _, ud, _, _ = magic_decomposition(unitary, backend=backend)
+ ud_diag = to_bell_diagonal(ud, backend=backend)
+ assert ud_diag is not None
+ hx, hy, hz = calculate_h_vector(ud_diag, backend=backend)
+ target_matrix = bell_unitary(hx, hy, hz, backend)
+ backend.assert_allclose(ud, target_matrix, atol=PRECISION_TOL)
+
+
+def test_cnot_decomposition(backend):
+ hx, hy, hz = np.random.random(3)
+ target_matrix = bell_unitary(hx, hy, hz, backend)
+ c = Circuit(2)
+ c.add(cnot_decomposition(0, 1, hx, hy, hz, backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, target_matrix, atol=PRECISION_TOL)
+
+
+def test_cnot_decomposition_light(backend):
+ hx, hy = np.random.random(2)
+ target_matrix = bell_unitary(hx, hy, 0, backend)
+ c = Circuit(2)
+ c.add(cnot_decomposition_light(0, 1, hx, hy, backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, target_matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("seed", [None, 10, np.random.default_rng(10)])
+def test_two_qubit_decomposition(backend, seed):
+ unitary = random_unitary(4, seed=seed, backend=backend)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ two_qubit_decomposition(0, 1, unitary, backend=backend)
+ else:
+ c = Circuit(2)
+ c.add(two_qubit_decomposition(0, 1, unitary, backend=backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, unitary, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("gatename", ["CNOT", "CZ", "SWAP", "iSWAP", "fSim", "I"])
+def test_two_qubit_decomposition_common_gates(backend, gatename):
+ """Test general two-qubit decomposition on some common gates."""
+ if gatename == "fSim":
+ gate = gates.fSim(0, 1, theta=0.1, phi=0.2)
+ else:
+ gate = getattr(gates, gatename)(0, 1)
+ matrix = gate.matrix(backend)
+ if (
+ backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]
+ and gatename != "iSWAP"
+ ):
+ with pytest.raises(NotImplementedError):
+ two_qubit_decomposition(0, 1, matrix, backend=backend)
+ else:
+ c = Circuit(2)
+ c.add(two_qubit_decomposition(0, 1, matrix, backend=backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, matrix, atol=PRECISION_TOL)
+
+
+@pytest.mark.parametrize("hz_zero", [False, True])
+def test_two_qubit_decomposition_bell_unitary(backend, hz_zero):
+ hx, hy, hz = (2 * np.random.random(3) - 1) * np.pi
+ if hz_zero:
+ hz = 0
+ unitary = backend.cast(bell_unitary(hx, hy, hz, backend))
+ c = Circuit(2)
+ c.add(two_qubit_decomposition(0, 1, unitary, backend=backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, unitary, atol=PRECISION_TOL)
+
+
+def test_two_qubit_decomposition_no_entanglement(backend):
+ """Test two-qubit decomposition on unitary that creates no entanglement."""
+ matrix = np.array(
+ [
+ [-1.0, 0.0, 0.0, 0.0],
+ [0.0, -1.0, 0.0, 0.0],
+ [0.0, 0.0, 1.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0],
+ ]
+ )
+ matrix = backend.cast(matrix, dtype=matrix.dtype)
+ c = Circuit(2)
+ if backend.__class__.__name__ in ["CupyBackend", "CuQuantumBackend"]:
+ with pytest.raises(NotImplementedError):
+ two_qubit_decomposition(0, 1, matrix, backend=backend)
+ else:
+ c.add(two_qubit_decomposition(0, 1, matrix, backend=backend))
+ final_matrix = c.unitary(backend)
+ backend.assert_allclose(final_matrix, matrix, atol=PRECISION_TOL)
diff --git a/tests/test_transpiler_unroller.py b/tests/test_transpiler_unroller.py
new file mode 100644
index 000000000..61e2c11fc
--- /dev/null
+++ b/tests/test_transpiler_unroller.py
@@ -0,0 +1,123 @@
+import pytest
+
+from qibo import gates
+from qibo.models import Circuit
+from qibo.transpiler._exceptions import DecompositionError
+from qibo.transpiler.unroller import (
+ NativeGates,
+ Unroller,
+ assert_decomposition,
+ translate_gate,
+)
+
+
+def test_native_gates_from_gatelist():
+ natives = NativeGates.from_gatelist([gates.RZ, gates.CZ(0, 1)])
+ assert natives == NativeGates.RZ | NativeGates.CZ
+
+
+def test_native_gates_from_gatelist_fail():
+ with pytest.raises(ValueError):
+ NativeGates.from_gatelist([gates.RZ, gates.X(0)])
+
+
+def test_translate_gate_error_1q():
+ natives = NativeGates(0)
+ with pytest.raises(DecompositionError):
+ translate_gate(gates.X(0), natives)
+
+
+def test_translate_gate_error_2q():
+ natives = NativeGates(0)
+ with pytest.raises(DecompositionError):
+ translate_gate(gates.CZ(0, 1), natives)
+
+
+def test_assert_decomposition():
+ circuit = Circuit(2)
+ circuit.add(gates.CZ(0, 1))
+ circuit.add(gates.Z(0))
+ circuit.add(gates.M(1))
+ assert_decomposition(circuit, native_gates=NativeGates.default())
+
+
+def test_assert_decomposition_fail_1q():
+ circuit = Circuit(1)
+ circuit.add(gates.X(0))
+ with pytest.raises(DecompositionError):
+ assert_decomposition(circuit, native_gates=NativeGates.default())
+
+
+@pytest.mark.parametrize("gate", [gates.CNOT(0, 1), gates.iSWAP(0, 1)])
+def test_assert_decomposition_fail_2q(gate):
+ circuit = Circuit(2)
+ circuit.add(gate)
+ with pytest.raises(DecompositionError):
+ assert_decomposition(circuit, native_gates=NativeGates.default())
+
+
+def test_assert_decomposition_fail_3q():
+ circuit = Circuit(3)
+ circuit.add(gates.TOFFOLI(0, 1, 2))
+ with pytest.raises(DecompositionError):
+ assert_decomposition(circuit, native_gates=NativeGates.default())
+
+
+@pytest.mark.parametrize(
+ "natives_2q",
+ [NativeGates.CZ, NativeGates.iSWAP, NativeGates.CZ | NativeGates.iSWAP],
+)
+@pytest.mark.parametrize(
+ "natives_1q",
+ [NativeGates.U3, NativeGates.GPI2, NativeGates.U3 | NativeGates.GPI2],
+)
+def test_unroller(natives_1q, natives_2q):
+ circuit = Circuit(3)
+ circuit.add(gates.H(0))
+ circuit.add(gates.X(0))
+ circuit.add(gates.Y(0))
+ circuit.add(gates.Z(0))
+ circuit.add(gates.S(0))
+ circuit.add(gates.T(0))
+ circuit.add(gates.SDG(0))
+ circuit.add(gates.TDG(0))
+ circuit.add(gates.SX(0))
+ circuit.add(gates.RX(0, 0.1))
+ circuit.add(gates.RY(0, 0.2))
+ circuit.add(gates.RZ(0, 0.3))
+ circuit.add(gates.U1(0, 0.4))
+ circuit.add(gates.U2(0, 0.5, 0.6))
+ circuit.add(gates.U3(0, 0.7, 0.8, 0.9))
+ circuit.add(gates.GPI2(0, 0.123))
+ circuit.add(gates.CZ(0, 1))
+ circuit.add(gates.CNOT(0, 1))
+ circuit.add(gates.SWAP(0, 1))
+ circuit.add(gates.iSWAP(0, 1))
+ circuit.add(gates.FSWAP(0, 1))
+ circuit.add(gates.CRX(0, 1, 0.1))
+ circuit.add(gates.CRY(0, 1, 0.2))
+ circuit.add(gates.CRZ(0, 1, 0.3))
+ circuit.add(gates.CU1(0, 1, 0.4))
+ circuit.add(gates.CU2(0, 1, 0.5, 0.6))
+ circuit.add(gates.CU3(0, 1, 0.7, 0.8, 0.9))
+ circuit.add(gates.RXX(0, 1, 0.1))
+ circuit.add(gates.RYY(0, 1, 0.2))
+ circuit.add(gates.RZZ(0, 1, 0.3))
+ circuit.add(gates.fSim(0, 1, 0.4, 0.5))
+ circuit.add(gates.TOFFOLI(0, 1, 2))
+ unroller = Unroller(native_gates=natives_1q | natives_2q)
+ translated_circuit = unroller(circuit)
+ assert_decomposition(
+ translated_circuit,
+ native_gates=natives_1q | natives_2q | NativeGates.RZ | NativeGates.Z,
+ )
+
+
+def test_measurements_non_comp_basis():
+ unroller = Unroller(native_gates=NativeGates.default())
+ circuit = Circuit(1)
+ circuit.add(gates.M(0, basis=gates.X))
+ transpiled_circuit = unroller(circuit)
+ assert isinstance(transpiled_circuit.queue[2], gates.M)
+ # After transpiling the measurement gate should be in the computational basis
+ assert transpiled_circuit.queue[2].basis == []
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 000000000..5d77b812d
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,15 @@
+import numpy as np
+from scipy import sparse
+
+
+def random_sparse_matrix(backend, n, sparse_type=None):
+ if backend.name == "tensorflow":
+ nonzero = int(0.1 * n * n)
+ indices = np.random.randint(0, n, size=(nonzero, 2))
+ data = np.random.random(nonzero) + 1j * np.random.random(nonzero)
+ data = backend.cast(data)
+ return backend.tf.sparse.SparseTensor(indices, data, (n, n))
+ else:
+ re = sparse.rand(n, n, format=sparse_type)
+ im = sparse.rand(n, n, format=sparse_type)
+ return re + 1j * im