diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 9eb2efa67..000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,270 +0,0 @@ -# Contributing - - -**We appreciate all kinds of help, so thank you!** - -## Contributor License Agreement - -We'd love to accept your code! Before we can, we have to get a few legal -requirements sorted out. By having you sign a Contributor License Agreement (CLA), we -ensure that the community is free to use your contributions. - -When you contribute to the Qiskit project with a new pull request, a bot will -evaluate whether you have signed the CLA. If required, the bot will comment on -the pull request, including a link to accept the agreement. The -[individual CLA](https://qiskit.org/license/qiskit-cla.pdf) document is -available for review as a PDF. - -If you work for a company that wants to allow you to contribute your work, -then you'll need to sign a [corporate CLA](https://qiskit.org/license/qiskit-corporate-cla.pdf) -and email it to us at qiskit@qiskit.org. - - -## Contributing to the Project - - -You can contribute in many ways to this project. - - -### Issue Reporting - -This is a good point to start, when you find a problem please add -it to the [issue tracker](https://github.com/Qiskit/qiskit-ignis/issues). -The ideal report should include the steps to reproduce it. - - -### Doubts Solving - -To help less advanced users is another wonderful way to start. You can -help us close some opened issues. A ticket of this kind should be -labeled as ``question``. - - -### Improvement Proposal - -If you have an idea for a new feature, please open a ticket labeled as -``enhancement``. If you could also add a piece of code with the idea -or a partial implementation, that would be awesome. - - -### Good First Contributions - -You are welcome to contribute wherever in the code you want to, of course, but -we recommend taking a look at the "Good First Contribution" label into the -issues and pick one. We would love to mentor you! - - -### Doc - -Review the parts of the documentation regarding the new changes and update it -if it's needed. - - -### Pull Requests - -We use [GitHub pull requests](https://help.github.com/articles/about-pull-requests) -to accept the contributions. - -A friendly reminder! We'd love to have a previous discussion about the best way to -implement the feature/bug you are contributing with. This is a good way to -improve code quality in our beloved Qiskit Ignis! So remember to file a new issue before -starting to code for a solution. - -After having discussed the best way to land your changes into the codebase, -you are ready to start coding. We have two options here: - -1. You think your implementation doesn't introduce a lot of code, right?. Ok, - no problem, you are all set to create the PR once you have finished coding. - We are waiting for it! -2. Your implementation does introduce many things in the codebase. That sounds - great! Thanks! In this case, you can start coding and create a PR with the - word: **[WIP]** as a prefix of the description. This means "Work In - Progress", and allows reviewers to make micro reviews from time to time - without waiting for the big and final solution. Otherwise, it would make - reviewing and coming changes pretty difficult to accomplish. The reviewer - will remove the **[WIP]** prefix from the description once the PR is ready - to merge. - - -#### Pull Request Checklist - - -When submitting a pull request and you feel it is ready for review, please -double check that: - -* The code follows the code style of the project. For convenience, you can - execute ``make style`` and ``make lint`` locally, which will print potential - style warnings and fixes. -* The documentation has been updated accordingly. In particular, if a function - or class has been modified during the PR, please update the docstring - accordingly. -* Your contribution passes the existing tests, and if developing a new feature, - that you have added new tests that cover those changes. -* You add a new line to the ``CHANGELOG.md`` file, in the ``UNRELEASED`` - section, with the title of your pull request and its identifier (for example, - "``Replace OldComponent with FluxCapacitor (#123)``". - - -#### Commit Messages - - -Please follow the next rules for any commit message: - -- It should include a reference to the issue ID in the first line of the commit, - **and** a brief description of the issue, so everybody knows what this ID - actually refers to without wasting to much time on following the link to the - issue. - -- It should provide enough information for a reviewer to understand the changes - and their relation to the rest of the code. - -A good example: - -``` -Issue #190: Short summary of the issue -* One of the important changes -* Another important change -``` - -## Code - -This section include some tips that will help you to push source code. - -We recommend using [Python virtual environments](https://docs.python.org/3/tutorial/venv.html) -to cleanly separate Qiskit from other applications and improve your experience. - - -## Setup with an Environment - -The simplest way to use environments is by using Anaconda - -``` -conda create -y -n QiskitDevenv python=3 -source activate QiskitDevenv -``` - -In order to execute the Ignis code, after cloning the Ignis GitHub repository on your machine, -you need to have some libraries, which can be installed in this way: - - -``` -cd qiskit-ignis -pip install -r requirements.txt -pip install -r requirements-dev.txt -``` - -To better contribute to Qiskit Ignis, we recommend that you clone the Qiskit Ignis repository -and then install Qiskit Ignis from source. This will give you the ability to inspect and extend -the latest version of the Ignis code more efficiently. The version of Qiskit Ignis in the repository's ``master`` -branch is typically ahead of the version in the Python Package Index (PyPI) repository, and -we strive to always keep Ignis in sync with the development versions of the Qiskit elements, -each available in the ``master`` branch of the corresponding repository. Therefore, -all the Qiskit elements and relevant components should be installed from source. This can be -correctly achieved by first uninstalling them from the Python environment in which you -have Qiskit (if they were previously installed), -using the ``pip uninstall`` command for each of them. Next, after cloning the -[Qiskit Terra](https://github.com/Qiskit/qiskit-terra), [Qiskit Aer](https://github.com/Qiskit/qiskit-aer), -[Qiskit IBMQ Provider](https://github.com/Qiskit/qiskit-ibmq-provider) and -[Qiskit Ignis](https://github.com/Qiskit/qiskit-ignis) repositories, you can install them -from source in the same Python environment by issuing the following command repeatedly, from each of the root -directories of those repository clones: - -``` -$ pip install -e . -``` - -exactly in the order specified above: Qiskit Terra, Qiskit Aer, Qiskit IBMQ Provider, and Qiskit Ignis. -All the other dependencies will be installed automatically. This process may have to be repeated often -as the ``master`` branch of Ignis is updated frequently. - -### Style guide - -Please submit clean code and please make effort to follow existing conventions -in order to keep it as readable as possible. We use the -[Pylint](https://www.pylint.org) and [PEP -8](https://www.python.org/dev/peps/pep-0008) style guide. To ensure -your changes respect the style guidelines, run the next commands (all platforms): - -``` -$> cd out -out$> make lint -out$> make style -``` - -## Documentation - - -The documentation source code for the project is located in the ``docs`` directory of the general -[Qiskit repository](https://github.com/Qiskit/qiskit) and automatically rendered on the -[Qiskit documentation Web site](https://qiskit.org/documentation/). The -documentation for the Python SDK is auto-generated from Python -docstrings using [Sphinx](http://www.sphinx-doc.org. Please follow [Google's Python Style -Guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) -for docstrings. A good example of the style can also be found with -[Sphinx's napolean converter -documentation](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). - -To generate the documentation, you need to invoke CMake first in order to generate -all specific files for our current platform. -See the [instructions](https://github.com/Qiskit/qiskit-terra/blob/master/.github/CONTRIBUTING.rst#dependencies) -in the Terra repository for details on how to install and run CMake. - -## Development Cycle - - -Our development cycle is straightforward. Use the **Projects** board in Github -for project management and use **Milestones** in the **Issues** board for releases. The features -that we want to include in these releases will be tagged and discussed -in the project boards. Whenever a new release is close to be launched, -we'll announce it and detail what has changed since the latest version in -our Release Notes and Changelog. The channels we'll use to announce new -releases are still being discussed, but for now, you can -[follow us](https://twitter.com/qiskit) on Twitter! - - -### Branch Model - - -There are two main branches in the repository: - -- ``master`` - - - This is the development branch. - - Next release is going to be developed here. For example, if the current - latest release version is r1.0.3, the master branch version will point to - r1.1.0 (or r2.0.0). - - You should expect this branch to be updated very frequently. - - Even though we are always doing our best to not push code that breaks - things, is more likely to eventually push code that breaks something... - we will fix it ASAP, promise :). - - This should not be considered as a stable branch to use in production - environments. - - The API of Qiskit could change without prior notice. - -- ``stable`` - - - This is our stable release branch. - - It's always synchronized with the latest distributed package: as for now, - the package you can download from pip. - - The code in this branch is well tested and should be free of errors - (unfortunately sometimes it's not). - - This is a stable branch (as the name suggest), meaning that you can expect - stable software ready for production environments. - - All the tags from the release versions are created from this branch. - -### Release Cycle - -From time to time, we will release brand new versions of Qiskit Terra. These -are well-tested versions of the software. - -When the time for a new release has come, we will: - -1. Merge the ``master`` branch with the ``stable`` branch. -2. Create a new tag with the version number in the ``stable`` branch. -3. Crate and distribute the pip package. -4. Change the ``master`` version to the next release version. -5. Announce the new version to the world! - -The ``stable`` branch should only receive changes in the form of bug fixes, so the -third version number (the maintenance number: [major].[minor].[maintenance]) -will increase on every new change. diff --git a/.gitignore b/.gitignore index d5a92d32a..15721499a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ test/python/*.prof # dotenv .env +# Jupyter notebooks checkpoints +qiskit/ignis/examples/.ipynb_checkpoints/ + # virtualenv .venv venv/ @@ -42,3 +45,4 @@ wheels/ *.egg-info/ .installed.cfg *.egg +*.DS_Store diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..01b8ca7b4 --- /dev/null +++ b/.mailmap @@ -0,0 +1,24 @@ +# Entries in this file are made for two reasons: +# 1) to merge multiple git commit authors that correspond to a single author +# 2) to change the canonical name and/or email address of an author. +# +# Format is: +# Canonical Name commit name +# \--------------+---------------/ \----------+-------------/ +# replace find +# See also: 'git shortlog --help' and 'git check-mailmap --help'. +# +# If you don't like the way your name is cited by qiskit, please feel free to +# open a pull request against this file to set your preferred naming. +# +# Note that each qiskit element uses its own mailmap so it may be necessary to +# propagate changes in other repos for consistency. +# +Ali Javadi-Abhari +Ali Javadi-Abhari +Christopher J. Wood +Gadi Aleksandrowicz +Gadi Aleksandrowicz +Jay M. Gambetta +Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> +Yael Ben-Haim diff --git a/.pylintrc b/.pylintrc index fdc780da2..02de5bb75 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,9 @@ [MASTER] -disable = invalid-name, import-error, too-many-statements, no-member, too-many-locals, too-many-branches, too-many-arguments, eval-used, too-many-nested-blocks, too-many-public-methods, redefined-builtin, wildcard-import, function-redefined, undefined-variable, unused-wildcard-import, duplicate-code, too-many-instance-attributes, too-few-public-methods, pointless-string-statement, no-name-in-module, no-self-use +disable = invalid-name, import-error, too-many-statements, too-many-locals, too-many-branches, too-many-arguments, eval-used, too-many-nested-blocks, too-many-public-methods, redefined-builtin, wildcard-import, function-redefined, unused-wildcard-import, duplicate-code, too-many-instance-attributes, too-few-public-methods, pointless-string-statement, no-name-in-module, no-self-use, arguments-differ, too-many-lines + +extension-pkg-whitelist=numpy + +generated-members = id, x, y, z, h, s, sdg, u1, u2, u3, cx, barrier, binary, measure, append notes = diff --git a/.stestr.conf b/.stestr.conf index 63b3c44c2..55302a160 100644 --- a/.stestr.conf +++ b/.stestr.conf @@ -1,3 +1,2 @@ [DEFAULT] -test_path=./tests -group_regex=([^\.]*\.)* +test_path=./test diff --git a/.travis.yml b/.travis.yml index ac899b5f5..e544c4dd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ -cache: pip +cache: + pip: true + directories: + - .stestr sudo: false addons: @@ -9,27 +12,98 @@ addons: - gcc-4.8 - g++-4.8 +install_osx: &stage_osx + os: osx + osx_image: xcode9.2 + language: generic + cache: + pip: true + directories: + - ~/python-interpreters/ + - .stestr + before_install: + # Travis does not provide support for Python 3 under osx - it needs to be + # installed manually. + | + if [ ${TRAVIS_OS_NAME} = "osx" ]; then + if [[ ! -d ~/python-interpreters/$PYTHON_VERSION ]]; then + git clone git://github.com/pyenv/pyenv.git + cd pyenv/plugins/python-build + ./install.sh + cd ../../.. + python-build $PYTHON_VERSION ~/python-interpreters/$PYTHON_VERSION + fi + virtualenv --python ~/python-interpreters/$PYTHON_VERSION/bin/python venv + source venv/bin/activate + fi + +before_script: + - | + if [ ! "$(ls -A .stestr)" ]; then + rm -r .stestr + fi + +stages: + - lint_and_python + - all_python + matrix: fast_finish: true include: - python: "3.5" env: TOXENV=py35 name: "Python 3.5 Tests" + stage: lint_and_python - python: "3.6" env: TOXENV=py36 name: "Python 3.6 Tests" + stage: all_python - os: linux dist: xenial python: "3.7" env: TOXENV=py37 sudo: true name: "Python 3.7 Tests" + stage: all_python - python: "3.5" env: TOXENV=lint name: "Linter Checks" + stage: lint_and_python + # OSX, Python 3.5.6 (via pyenv) + - stage: all_python + name: "Python 3.5 Tests on OSX" + <<: *stage_osx + env: + - MPLBACKEND=ps + - PYTHON_VERSION=3.5.6 + - TOXENV=py35 + # OSX, Python 3.6.5 (via pyenv) + - stage: all_python + name: "Python 3.6 Tests on OSX" + <<: *stage_osx + env: + - MPLBACKEND=ps + - PYTHON_VERSION=3.6.5 + - TOXENV=py36 + # OSX, Python 3.7.2 (via pyenv) + - stage: all_python + <<: *stage_osx + name: "Python 3.7 Tests on OSX" + env: + - MPLBACKEND=ps + - PYTHON_VERSION=3.7.2 + - TOXENV=py37 + - if: tag IS present + python: "3.6" + env: + - TWINE_USERNAME=qiskit + install: pip install -U twine + script: + - python3 setup.py sdist bdist_wheel + - twine upload dist/qiskit* language: python -install: pip install -U tox +install: pip install -U tox pip script: - tox diff --git a/CHANGELOG.md b/CHANGELOG.md index 14eab8bf5..f08e822c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,37 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added -- Align cliffs option to RB +- Pulse Discriminator (\#238) + +## [0.2.0](https://github.com/Qiskit/qiskit/compare/0.1.1...0.2.0)- 2019-08-22 + +### Added + +- Logging Module (\#153) +- Purity RB (\#218) +- Interleaved RB (\#174) +- Repetition Code for Verification (\#210) ### Changed +- Apply measurement mitigation in parallel when applied to multiple results (\#240) +- Add multiple results to measurement mitigation (\#240) +- Fixed bug in RB fit error +- Updates for Terra Qubit class (\#200) +- Added the ability to add arbitrary seeds to RB (not just in order) (\#208) +- Fix bug in the characterization fitter when selecting a qubit index to fit +- Improved guess values for RB fitters and enabled the user to input their own guess values + ### Removed +## [0.1.1] - 2019-05-02 + +### Added + +- Tensored Measurement Mitigation +- Align cliffs option to RB +- Quantum Volume +- Subset measurement mitigation ## [0.1.0] - 2019-03-04 diff --git a/.github/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md similarity index 100% rename from .github/CODE_OF_CONDUCT.md rename to CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ff0e6083b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,336 @@ +# Contributing + +## Contributing to Qiskit Ignis + +### Issue reporting + +When you encounter a problem please open an issue for it to +the [issue tracker](https://github.com/Qiskit/qiskit-ignis/issues). + +### Improvement proposal + +If you have an idea for a new feature please open an **Feature Requestt** issue +in the [issue tracker](https://github.com/Qiskit/qiskit-ignis/issues). Opening +an issue starts a discussion with the team about your idea, how it fits in with +the project, how it can be implemented, etc. + +### Code Review + +Code review is done in the open and open to anyone. While only maintainers have +access to merge commits, providing feedback on pull requests is very valuable +and helpful. It is also a good mechanism to learn about the code base. You can +view a list of all open pull requests here: +https://github.com/Qiskit/qiskit-ignis/pulls +to review any open pull requests and provide feedback on it. + +### Documentation + +If you make a change, make sure you update the associated +*docstrings* and parts of the +[documentation](https://github.com/Qiskit/qiskit/tree/master/docs/ignis) +that corresponds to it. You can also make a [documentation issue]( +https://github.com/Qiskit/qiskit/issues/new/choose) if you see doc bugs, have a +new feature that needs to be documented, or think that material could be added +to the existing docs. + +### Pull requests + +We use [GitHub pull requests]( +https://help.github.com/articles/about-pull-requests) to accept contributions. + +While not required, opening a new issue about the bug you're fixing or the +feature you're working on before you open a pull request is an important step +in starting a discussion with the community about your work. The issue gives us +a place to talk about the idea and how we can work together to implement it in +the code. It also lets the community know what you're working on and if you +need help, you can use the issue to go through it with other community and team +members. + +If you've written some code but need help finishing it, want to get initial +feedback on it prior to finishing it, or want to share it and discuss prior +to finishing the implementation you can open a *Work in Progress* pull request. +When you create the pull request prefix the title with the **\[WIP\]** tag (for +**W**ork **I**n **P**rogress). This will indicate to reviewers that the code in +the PR isn't in it's final state and will change. It also means that we will +not merge the commit until it is finished. You or a reviewer can remove the +[WIP] tag when the code is ready to be fully reviewed for merging. + +### Contributor License Agreement + +Before you can submit any code we need all contributors to sign a +contributor license agreement. By signing a contributor license +agreement (CLA) you're basically just attesting to the fact +that you are the author of the contribution and that you're freely +contributing it under the terms of the Apache-2.0 license. + +When you contribute to the Qiskit Terra project with a new pull request, +a bot will evaluate whether you have signed the CLA. If required, the +bot will comment on the pull request, including a link to accept the +agreement. The [individual CLA](https://qiskit.org/license/qiskit-cla.pdf) +document is available for review as a PDF. + +**Note**: +> If your contribution is part of your employment or your contribution +> is the property of your employer, then you will likely need to sign a +> [corporate CLA](https://qiskit.org/license/qiskit-corporate-cla.pdf) too and +> email it to us at . + + +### Pull request checklist + +When submitting a pull request and you feel it is ready for review, +please ensure that: + +1. The code follows the code style of the project and successfully + passes the tests. For convenience, you can execute `tox` locally, + which will run these checks and report any issues. +2. The documentation has been updated accordingly. In particular, if a + function or class has been modified during the PR, please update the + *docstring* accordingly. +3. If it makes sense for your change that you have added new tests that + cover the changes. +4. Ensure that if your change has an end user facing impact (new feature, + deprecation, removal etc) that you have updated the CHANGELOG.md file. + +### Commit messages + +As important as the content of the change, is the content of the commit message +describing it. The commit message provides the context for not only code review +but also the change history in the git log. Having a detailed commit message +will make it easier for your code to be reviewed and also provide context to the +change when it's being looked at years in the future. When writing a commit +message there are some important things to remember: + +* Do not assume the reviewer understands what the original problem was. + +When reading an issue, after a number of back & forth comments, it is often +clear what the root cause problem is. The commit message should have a clear +statement as to what the original problem is. The bug is merely interesting +historical background on *how* the problem was identified. It should be +possible to review a proposed patch for correctness from the commit message, + without needing to read the bug ticket. +bug ticket. + +* Do not assume the code is self-evident/self-documenting. + +What is self-evident to one person, might not be clear to another person. Always +document what the original problem was and how it is being fixed, for any change +except the most obvious typos, or whitespace only commits. + +* Describe why a change is being made. + +A common mistake is to just document how the code has been written, without +describing *why* the developer chose to do it that way. By all means describe +the overall code structure, particularly for large changes, but more importantly +describe the intent/motivation behind the changes. + +* Read the commit message to see if it hints at improved code structure. + +Often when describing a large commit message, it becomes obvious that a commit +should have in fact been split into 2 or more parts. Don't be afraid to go back +and rebase the change to split it up into separate pull requests. + +* Ensure sufficient information to decide whether to review. + +When Github sends out email alerts for new pull request submissions, there is +minimal information included, usually just the commit message and the list of +files changes. Because of the high volume of patches, commit message must +contain sufficient information for potential reviewers to find the patch that +they need to look at. + +* The first commit line is the most important. + +In Git commits, the first line of the commit message has special significance. +It is used as the default pull request title, email notification subject line, +git annotate messages, gitk viewer annotations, merge commit messages, and many +more places where space is at a premium. As well as summarizing the change +itself, it should take care to detail what part of the code is affected. + +* Describe any limitations of the current code. + +If the code being changed still has future scope for improvements, or any known +limitations, then mention these in the commit message. This demonstrates to the +reviewer that the broader picture has been considered and what tradeoffs have +been done in terms of short term goals vs. long term wishes. + +* Include references to issues + +If the commit fixes or is related to an issue make sure you annotate that in +the commit message. Using the syntax: + +Fixes #1234 + +if it fixes the issue (github will close the issue when the PR merges). + +The main rule to follow is: + +The commit message must contain all the information required to fully +understand & review the patch for correctness. Less is not more. + + +### Installing Qiskit Ignis from source +Please see the [Installing Qiskit Ignis from +Source](https://qiskit.org/documentation/contributing_to_qiskit.html#installing-ignis-from-source) +section of the Qiskit documentation. + + +### Test + +Once you've made a code change, it is important to verify that your change +does not break any existing tests and that any new tests that you've added +also run successfully. Before you open a new pull request for your change, +you'll want to run the test suite locally. + +The easiest way to run the test suite is to use +[**tox**](https://tox.readthedocs.io/en/latest/#). You can install tox +with pip: `pip install -U tox`. Tox provides several advantages, but the +biggest one is that it builds an isolated virtualenv for running tests. This +means it does not pollute your system python when running. Additionally, the +environment that tox sets up matches the CI environment more closely and it +runs the tests in parallel (resulting in much faster execution). To run tests +on all installed supported python versions and lint/style checks you can simply +run `tox`. Or if you just want to run the tests once run for a specific python +version: `tox -epy37` (or replace py37 with the python version you want to use, +py35 or py36). + +If you just want to run a subset of tests you can pass a selection regex to +the test runner. For example, if you want to run all tests that have "dag" in +the test id you can run: `tox -epy37 -- dag`. You can pass arguments directly to +the test runner after the bare `--`. To see all the options on test selection +you can refer to the stestr manual: +https://stestr.readthedocs.io/en/stable/MANUAL.html#test-selection + +If you want to run a single test module, test class, or individual test method +you can do this faster with the `-n`/`--no-discover` option. For example: + +to run a module: +``` +tox -epy37 -- -n test.test_examples +``` +or to run the same module by path: + +``` +tox -epy37 -- -n test/test_examples.py +``` +to run a class: + +``` +tox -epy37 -- -n test.test_examples.TestPythonExamples +``` +to run a method: +``` +tox -epy37 -- -n test.test_examples.TestPythonExamples.test_all_examples +``` + + +### Style guide + +To enforce a consistent code style in the project we use +[Pylint](https://www.pylint.org) and +[pycodesytle](https://pycodestyle.readthedocs.io/en/latest/) +to verify that code contributions conform respect the projects +style guide. To verify that your changes conform to the style +guide you can run: `tox -elint` + +## Documentation + + +The documentation source code for the project is located in the ``docs`` directory of the general +[Qiskit repository](https://github.com/Qiskit/qiskit) and automatically rendered on the +[Qiskit documentation Web site](https://qiskit.org/documentation/). The +documentation for the Python SDK is auto-generated from Python +docstrings using [Sphinx](http://www.sphinx-doc.org. Please follow [Google's Python Style +Guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) +for docstrings. A good example of the style can also be found with +[Sphinx's napolean converter +documentation](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). + +## Development Cycle + +The development cycle for qiskit-ignis is all handled in the open using +the project boards in Github for project management. We use milestones +in Github to track work for specific releases. The features or other changes +that we want to include in a release will be tagged and discussed in Github. +As we're preparing a new release we'll document what has changed since the +previous version in the release notes and Changelog. + +### Branches + +* `master`: + +The master branch is used for development of the next version of qiskit-ignis. +It will be updated frequently and should not be considered stable. The API +can and will change on master as we introduce and refine new features. + +* `stable`: +The stable branches is used to maintain the most recent released versions of +qiskit-ignis. It contains the version of the code corresponding to the latest +release for The API on these branches are stable and the only changes +merged to it are bugfixes. + + +### Release Cycle + +From time to time, we will release brand new versions of Qiskit Terra. These +are well-tested versions of the software. + +When the time for a new release has come, we will: + +1. Merge the `master` branch with the `stable` branch. +2. Create a new tag with the version number in the `stable` branch. +4. Change the `master` version to the next release version. + +The `stable` branch should only receive changes in the form of bug fixes. + +## Stable Branch Policy + +The stable branch is intended to be a safe source of fixes for high impact bugs +and security issues which have been fixed on master since a release. When +reviewing a stable branch PR we need to balance the risk of any given patch +with the value that it will provide to users of the stable branch. Only a +limited class of changes are appropriate for inclusion on the stable branch. A +large, risky patch for a major issue might make sense. As might a trivial fix +for a fairly obscure error handling case. A number of factors must be weighed +when considering a change: + +- The risk of regression: even the tiniest changes carry some risk of breaking + something and we really want to avoid regressions on the stable branch +- The user visible benefit: are we fixing something that users might actually + notice and, if so, how important is it? +- How self-contained the fix is: if it fixes a significant issue but also + refactors a lot of code, it's probably worth thinking about what a less + risky fix might look like +- Whether the fix is already on master: a change must be a backport of a change + already merged onto master, unless the change simply does not make sense on + master. + +### Backporting procedure: + +When backporting a patch from master to stable we want to keep a reference to +the change on master. When you create the branch for the stable PR you can use: + +`$ git cherry-pick -x $master_commit_id` + +However, this only works for small self contained patches from master. If you +need to backport a subset of a larger commit (from a squashed PR for +example) from master this just need be done manually. This should be handled +by adding:: + + Backported from: #master pr number + +in these cases, so we can track the source of the change subset even if a +strict cherry pick doesn't make sense. + +If the patch you're proposing will not cherry-pick cleanly, you can help by +resolving the conflicts yourself and proposing the resulting patch. Please keep +Conflicts lines in the commit message to help review of the stable patch. + +### Backport Tags + +Bugs or PRs tagged with `stable backport potential` are bugs which apply to the +stable release too and may be suitable for backporting once a fix lands in +master. Once the backport has been proposed, the tag should be removed. + +The PR against the stable branch should include `[stable]` in the title, as a +sign that setting the target branch as stable was not a mistake. Also, +reference to the PR number in master that you are porting. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..f4e110f32 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include qiskit/ignis/VERSION.txt diff --git a/README.md b/README.md index ba128cd54..9cf84c32f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License](https://img.shields.io/github/license/Qiskit/qiskit-ignis.svg?style=popout-square)](https://opensource.org/licenses/Apache-2.0)[![Build Status](https://img.shields.io/travis/com/Qiskit/qiskit-ignis/master.svg?style=popout-square)](https://travis-ci.com/Qiskit/qiskit-ignis)[![](https://img.shields.io/github/release/Qiskit/qiskit-ignis.svg?style=popout-square)](https://github.com/Qiskit/qiskit-ignis/releases)[![](https://img.shields.io/pypi/dm/qiskit-ignis.svg?style=popout-square)](https://pypi.org/project/qiskit-ignis/) -**Qiskit** is an open-source framework for working with noisy intermediate-scale quantum computers (NISQ) at the level of pulses, circuits, and algorithms. +**Qiskit** is an open-source framework for working with noisy quantum computers at the level of pulses, circuits, and algorithms. Qiskit is made up of elements that each work together to enable quantum computing. This element is **Ignis**, which provides tools for quantum hardware verification, noise characterization, and error correction. @@ -17,7 +17,7 @@ pip install qiskit PIP will handle all dependencies automatically for us and you will always install the latest (and well-tested) version. -To install from source, follow the instructions in the [contribution guidelines](.github/CONTRIBUTING.rst). +To install from source, follow the instructions in the [contribution guidelines](./CONTRIBUTING.md). ## Creating your first quantum experiment with Qiskit Ignis Now that you have Qiskit Ignis installed, you can start creating experiments, to reveal information about the device quality. Here is a basic example: @@ -79,13 +79,13 @@ meas_filter = meas_fitter.filter # Apply the filter to the raw counts to mitigate # the measurement errors mitigated_counts = meas_filter.apply(raw_counts) -print("Results with mitigation:", mitigated_counts) +print("Results with mitigation:", {l:int(mitigated_counts[l]) for l in mitigated_counts}) ``` ``` Results without mitigation: {'000': 181, '001': 83, '010': 59, '011': 65, '100': 101, '101': 48, '110': 72, '111': 391} -Results with mitigation: {'000': 420.866934, '001': 2.1002, '011': 1.30314, '100': 53.0165, '110': 13.1834, '111': 509.5296} +Results with mitigation: {'000': 421, '001': 2, '011': 1, '100': 53, '110': 13, '111': 510} ``` ## Contribution guidelines @@ -93,14 +93,14 @@ Results with mitigation: {'000': 420.866934, '001': 2.1002, '011': 1.30314, '100 ## Contribution Guidelines If you'd like to contribute to Qiskit Ignis, please take a look at our -[contribution guidelines](.github/CONTRIBUTING.md). This project adheres to Qiskit's [code of conduct](.github/CODE_OF_CONDUCT.md). By participating, you are expect to uphold to this code. +[contribution guidelines](./CONTRIBUTING.md). This project adheres to Qiskit's [code of conduct](./CODE_OF_CONDUCT.md). By participating, you are expect to uphold to this code. We use [GitHub issues](https://github.com/Qiskit/qiskit-ignis/issues) for tracking requests and bugs. Please use our [slack](https://qiskit.slack.com) for discussion and simple questions. To join our Slack community use the [link](https://join.slack.com/t/qiskit/shared_invite/enQtNDc2NjUzMjE4Mzc0LTMwZmE0YTM4ZThiNGJmODkzN2Y2NTNlMDIwYWNjYzA2ZmM1YTRlZGQ3OGM0NjcwMjZkZGE0MTA4MGQ1ZTVmYzk). For questions that are more suited for a forum we use the Qiskit tag in the [Stack Exchange](https://quantumcomputing.stackexchange.com/questions/tagged/qiskit). ## Next Steps Now you're set up and ready to check out some of the other examples from our -[Qiskit Tutorials](https://github.com/Qiskit/qiskit-tutorials/tree/master/qiskit/ignis) repository. +[Qiskit Tutorials](https://github.com/Qiskit/qiskit-iqx-tutorials/tree/master/qiskit/advanced/ignis) repository. ## Authors and Citation diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..b0ec432ce --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,38 @@ +# Python package +# Create and test a Python package on multiple Python versions. +# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/python + +trigger: +- master +- stable + +pool: + vmImage: 'vs2017-win2016' +strategy: + matrix: + Python35: + python.version: '3.5' + TOXENV: py35 + Python36: + python.version: '3.6' + TOXENV: py36 + Python37: + python.version: '3.7' + TOXENV: py37 + +steps: + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: Add conda to PATH + - script: conda create --yes --quiet --name qiskit-ignis + displayName: Create Anaconda environment + - script: | + call activate qiskit-ignis + conda install --yes --quiet --name qiskit-ignis python=%PYTHON_VERSION% mkl + displayName: Install Anaconda packages + - script: | + call activate qiskit-ignis + python -m pip install --upgrade pip + pip install tox + tox -e%TOXENV% + displayName: 'Install dependencies and run tests' diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..dce383c93 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,28 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +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) diff --git a/docs/apidocs/characterization/characterization.rst b/docs/apidocs/characterization/characterization.rst new file mode 100644 index 000000000..5e9cc5c46 --- /dev/null +++ b/docs/apidocs/characterization/characterization.rst @@ -0,0 +1,23 @@ +.. _qiskit-ignis-characterization: + +****************************** +qiskit.ignis.characterization +****************************** + +.. currentmodule:: qiskit.ignis.characterization + + +.. automodapi:: qiskit.ignis.characterization + :no-heading: + :no-inheritance-diagram: + :inherited-members: + +Submodules +========== + +.. toctree:: + :maxdepth: 1 + + coherence/coherence + gates/gates + hamiltonian/hamiltonian diff --git a/docs/apidocs/characterization/coherence/coherence.rst b/docs/apidocs/characterization/coherence/coherence.rst new file mode 100644 index 000000000..a0653fc67 --- /dev/null +++ b/docs/apidocs/characterization/coherence/coherence.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-characterization-coherence: + +**************************************** +qiskit.ignis.characterization.coherence +**************************************** + +.. currentmodule:: qiskit.ignis.characterization.coherence + + +.. automodapi:: qiskit.ignis.characterization.coherence + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/characterization/gates/gates.rst b/docs/apidocs/characterization/gates/gates.rst new file mode 100644 index 000000000..407c9343f --- /dev/null +++ b/docs/apidocs/characterization/gates/gates.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-characterization-gates: + +************************************ +qiskit.ignis.characterization.gates +************************************ + +.. currentmodule:: qiskit.ignis.characterization.gates + + +.. automodapi:: qiskit.ignis.characterization.gates + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/characterization/hamiltonian/hamiltonian b/docs/apidocs/characterization/hamiltonian/hamiltonian new file mode 100644 index 000000000..d55d33169 --- /dev/null +++ b/docs/apidocs/characterization/hamiltonian/hamiltonian @@ -0,0 +1,13 @@ +.. _qiskit-ignis-characterization-hamiltonian: + +****************************************** +qiskit.ignis.characterization.hamiltonian +****************************************** + +.. currentmodule:: qiskit.ignis.characterization.hamiltonian + + +.. automodapi:: qiskit.ignis.characterization.hamiltonian + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/ignis.rst b/docs/apidocs/ignis.rst new file mode 100644 index 000000000..2ce2287d2 --- /dev/null +++ b/docs/apidocs/ignis.rst @@ -0,0 +1,23 @@ +.. _qiskit-ignis: + +**************** +qiskit.ignis +**************** + +.. currentmodule:: qiskit.ignis + + +.. automodapi:: qiskit.ignis + :no-heading: + :no-inheritance-diagram: + :inherited-members: + +Submodules +========== + +.. toctree:: + :maxdepth: 1 + + characterization/characterization + mitigation/mitigation + verification/verification diff --git a/docs/apidocs/mitigation/measurement/measurement.rst b/docs/apidocs/mitigation/measurement/measurement.rst new file mode 100644 index 000000000..8fa585946 --- /dev/null +++ b/docs/apidocs/mitigation/measurement/measurement.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-mitigation-measurement: + +************************************ +qiskit.ignis.mitigation.measurement +************************************ + +.. currentmodule:: qiskit.ignis.mitigation.measurement + + +.. automodapi:: qiskit.ignis.mitigation.measurement + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/mitigation/mitigation.rst b/docs/apidocs/mitigation/mitigation.rst new file mode 100644 index 000000000..14a73d5d5 --- /dev/null +++ b/docs/apidocs/mitigation/mitigation.rst @@ -0,0 +1,21 @@ +.. _qiskit-ignis-mitigation: + +************************ +qiskit.ignis.mitigation +************************ + +.. currentmodule:: qiskit.ignis.mitigation + + +.. automodapi:: qiskit.ignis.mitigation + :no-heading: + :no-inheritance-diagram: + :inherited-members: + +Submodules +========== + +.. toctree:: + :maxdepth: 1 + + measurement/measurement diff --git a/docs/apidocs/verification/quantum_volume/quantum_volume.rst b/docs/apidocs/verification/quantum_volume/quantum_volume.rst new file mode 100644 index 000000000..fad62296f --- /dev/null +++ b/docs/apidocs/verification/quantum_volume/quantum_volume.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-verification-quantum_volume: + +***************************************** +qiskit.ignis.verification.quantum_volume +***************************************** + +.. currentmodule:: qiskit.ignis.verification.quantum_volume + + +.. automodapi:: qiskit.ignis.verification.quantum_volume + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/verification/randomized_benchmarking/randomized_benchmarking.rst b/docs/apidocs/verification/randomized_benchmarking/randomized_benchmarking.rst new file mode 100644 index 000000000..af5a1dd66 --- /dev/null +++ b/docs/apidocs/verification/randomized_benchmarking/randomized_benchmarking.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-verification-randomized_benchmarking: + +************************************************** +qiskit.ignis.verification.randomized_benchmarking +************************************************** + +.. currentmodule:: qiskit.ignis.verification.randomized_benchmarking + + +.. automodapi:: qiskit.ignis.verification.randomized_benchmarking + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/verification/tomography/tomography.rst b/docs/apidocs/verification/tomography/tomography.rst new file mode 100644 index 000000000..89fefb3b3 --- /dev/null +++ b/docs/apidocs/verification/tomography/tomography.rst @@ -0,0 +1,13 @@ +.. _qiskit-ignis-verification-tomography: + +************************************* +qiskit.ignis.verification.tomography +************************************* + +.. currentmodule:: qiskit.ignis.verification.tomography + + +.. automodapi:: qiskit.ignis.verification.tomography + :no-heading: + :no-inheritance-diagram: + :inherited-members: diff --git a/docs/apidocs/verification/verification.rst b/docs/apidocs/verification/verification.rst new file mode 100644 index 000000000..b8b953021 --- /dev/null +++ b/docs/apidocs/verification/verification.rst @@ -0,0 +1,23 @@ +.. _qiskit-ignis-verification: + +****************************** +qiskit.ignis.verification +****************************** + +.. currentmodule:: qiskit.ignis.verification + + +.. automodapi:: qiskit.ignis.verification + :no-heading: + :no-inheritance-diagram: + :inherited-members: + +Submodules +========== + +.. toctree:: + :maxdepth: 1 + + quantum_volume/quantum_volume + randomized_benchmarking/randomized_benchmarking + tomography/tomography diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..da803b1da --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=invalid-name +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- 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 +# sys.path.insert(0, os.path.abspath('.')) + +""" +Sphinx documentation builder +""" + +# -- Project information ----------------------------------------------------- +project = 'Qiskit' +copyright = '2019, Qiskit Development Team' # pylint: disable=redefined-builtin +author = 'Qiskit Development Team' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.12.0' + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# 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.mathjax', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', + 'sphinx.ext.extlinks', + 'sphinx_tabs.tabs', + 'sphinx_automodapi.automodapi', + 'IPython.sphinxext.ipython_console_highlighting', + 'IPython.sphinxext.ipython_directive' +] + + +# If true, figures, tables and code-blocks are automatically numbered if they +# have a caption. +numfig = True + +# A dictionary mapping 'figure', 'table', 'code-block' and 'section' to +# strings that are used for format of figure numbers. As a special character, +# %s will be replaced to figure number. +numfig_format = { + 'table': 'Table %s' +} +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# 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 = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +# A boolean that decides whether module names are prepended to all object names +# (for object types where a “module” of some kind is defined), e.g. for +# py:function directives. +add_module_names = False + +# A list of prefixes that are ignored for sorting the Python module index +# (e.g., if this is set to ['foo.'], then foo.bar is shown under B, not F). +# This can be handy if you document a project that consists of a single +# package. Works only for the HTML builder currently. +modindex_common_prefix = ['qiskit.'] + +# -- Configuration for extlinks extension ------------------------------------ +# Refer to https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html + + +# -- 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 = 'sphinx_rtd_theme' # use the theme in subdir 'theme' + +html_sidebars = {'**': ['globaltoc.html']} +html_last_updated_fmt = '%Y/%m/%d' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..99c038545 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,14 @@ +########################## +Qiskit Ignis documentation +########################## + +.. toctree:: + :maxdepth: 2 + :hidden: + + API References + +.. Hiding - Indices and tables + :ref:`genindex` + :ref:`modindex` + :ref:`search` diff --git a/examples/DiscriminatorTutorial.ipynb b/examples/DiscriminatorTutorial.ipynb new file mode 100644 index 000000000..cfc069f11 --- /dev/null +++ b/examples/DiscriminatorTutorial.ipynb @@ -0,0 +1,542 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Discriminating qubit states using Qiskit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this Jupyter notebook we show how to convert level 1 data, such as IQ data for superconducting qubits, to level 2 data, i.e. qubit states. We do this by training a discriminator using calibration circuits and create a filter, based on the fitted discriminator, which can be applied to subsequent measurement. This notebook does the following steps\n", + "\n", + "1) Creates pulse schedules to run on the IBM Q devices. The schedule list has calibration schedules and experiment schedules. \n", + "\n", + "2) Use the calibration schedules to train a discriminator.\n", + "\n", + "3) Create a filter from the discriminator to discriminate the qubit states for the experiment schedule." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt \n", + "import numpy as np\n", + "from copy import deepcopy\n", + "\n", + "%matplotlib inline\n", + "plt.rcParams['font.size'] = 16\n", + "\n", + "import qiskit\n", + "from qiskit.ignis.measurement.discriminator.iq_discriminators import \\\n", + " LinearIQDiscriminator, QuadraticIQDiscriminator\n", + "from qiskit.result.models import ExperimentResultData\n", + "from qiskit import IBMQ\n", + "\n", + "import qiskit.pulse as pulse\n", + "import qiskit.pulse.pulse_lib as pulse_lib\n", + "from qiskit.compiler import assemble" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1) Creating Pulse Schedules to run on IBM Q" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "account_provider = IBMQ.load_account()\n", + "hub = account_provider.credentials.hub\n", + "group = account_provider.credentials.group\n", + "project = account_provider.credentials.project" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "provider = IBMQ.get_provider(hub=hub, group=group, project=project)\n", + "backend = provider.get_backend('ibmq_20_tokyo')\n", + "back_config = backend.configuration().to_dict()\n", + "device = pulse.PulseChannelSpec.from_backend(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "defaults = backend.defaults()\n", + "\n", + "# command definition from defaults.\n", + "cmd_def = pulse.CmdDef.from_defaults(defaults.cmd_def, defaults.pulse_library)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We create schedules to measure two qubits. The calibration schedules `cal_00` and `cal_11` serve to calibrate the 0 and 1 sates of the qubits. The schedule `X90p` is our experiment where we apply a pi-half-pulse to both qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "qubits = [0, 1]\n", + "schedules = []\n", + "meas_buffer = 2\n", + "shots = 512\n", + "experiment_name = 'X90p'\n", + "\n", + "meas = cmd_def.get('measure', qubits=tuple(range(20)))\n", + "\n", + "# Create a calibration schedule for the ground state.\n", + "schedule_no_pi = pulse.Schedule(name='cal_00')\n", + "schedule_no_pi += meas\n", + "\n", + "# Create a calibration schedule for the excited state.\n", + "schedule_pi = pulse.Schedule(name='cal_11')\n", + "for q in qubits:\n", + " xgate = cmd_def.get('x', qubits=q)\n", + " schedule_pi += xgate\n", + "\n", + "schedule_pi += meas << (schedule_pi.duration + meas_buffer)\n", + "\n", + "# Measurement schedule. Do an X90p gate on both qubits.\n", + "schedule_x90p = pulse.Schedule(name=experiment_name)\n", + "for q in qubits:\n", + " x90p = cmd_def.get('u3', qubits=q, P0=np.pi/2., P1=0., P2=0.)\n", + " schedule_x90p += x90p\n", + " \n", + "schedule_x90p += meas << (schedule_x90p.duration + meas_buffer)\n", + " \n", + "schedules = [schedule_no_pi, schedule_pi, schedule_x90p]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlkAAAKrCAYAAADCophVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzde5RcZYHv/d+z69LV1beEhACJXERgNAwoSUhI5KZEEAk5oAs9AqMoknFmYPQdRkQ5OgsPM4iXQUXhiBxxeR1gDuICgaMDGIJKBMIBgiGAIQlJujt9r/tl7/28f1Snk3Sqk7491Z3u72ctVqerdu39VAc2395717ONtVYAAAAYX95EDwAAAGAqIrIAAAAcILIAAAAcILIAAAAcILIAAAAciE70AAZLJBJtxWLxsIkeR60lEomwUChMu+idru8btVdXVxcWi8Vp9+/adP1vbLq+b9ReIpFoz+fzh1d7zky2KRyMMXbz5s0TPYyaO+aYY5TL5SZ6GDWXTCan5ftG7SWTSbFvmT7Yt6BWksmkrLWm2nNUPgAAgANEFgAAgANEFgAAgANEFgAAgANEFgAAgAMHTWT19fXpJz/5ybis66677tLy5cv1/ve/X5deeqm2bds28NzNN9+sc889V+eee64efPDBcdmeC6tWrdIvf/nLEb3mpptu0re+9S1JUnd3t1asWKGTTjpJK1asUE9Pj4th1sTtt9+uU045RZ/4xCcmeig69NBDJ3oIB/TUU09p6dKlampq2u+/Q+edd57e+c53asmSJVqyZIl27twpSbruuusGHjv55JN1xBFH1GroTrBv2dtY9y3333+/Fi5cqIaGBj333HMuhlgz7FtGhn3Lvg6ayEqlUuO2I5w/f74efPBBPfroozr//PN18803S5Ief/xxvfzyy3r44Yf1wAMP6Ac/+IHS6fS4bHOy+eY3v6mzzz5bL730ks4++2x985vfnOghjdqdd96pBx98UHffffdej/u+P0EjmtyOPPJI3XnnnfrIRz5ywGV/+MMfau3atVq7dq3mzJkjSfra17428Njf/d3faeXKla6H7BT7lvE1f/58/eIXv9Dpp58+0UMZM/YtI8O+ZV8HTWTdcsst2rJli84//3z927/925jWtWzZMtXX10uSTjnlFLW1tUmSXnvtNS1evFjRaFTJZFJvf/vbtXr16jGPfax+9rOfafHixVqyZImuvPLKgcefeuopvec979H8+fOH/K3hlltu0cknn6xzzjlHr7322sDjDz30kC677DJJ0mWXXTapf7Pen2uuuUZvvPGGLr74Yt1222266aabdOWVV+q9732vrrzySm3ZskXLly/X0qVLtXTpUj399NOSpCeffFLnnnuuLrnkEs2fP19f+tKX9B//8R8644wzdOqpp2rTpk2SpI6ODn30ox/V6aefrtNPP11//OMfJUmZTEarVq3SqaeeqsWLF+uBBx4YGNO//Mu/aMmSJTrrrLPU3t4uSfr1r3+tM888U6eddpouuOCCgcdvuukm/e3f/q3OO+88zZ8/X7fffvvAem6++Wa9853v1DnnnKOPf/zjA0cKNm3apJUrV2rZsmVavny5Nm7cOKKf2dFHH62TTjpJnjf2//zvvfdeffjDHx7zeiYS+5bx3be8/e1v1wknnOB87K6xb2HfMh4m3YzvQ/n85z+vV199VY888kjV5y+55BJlMpl9Hr/hhhv2+xvVvffeq7PPPluS9I53vEPf/va3ddVVVymfz+uPf/yjjj/++HEZ/2j9+c9/1i233KLHH39cs2fPVnd398BzbW1teuyxx7Rx40Zdcskluvjii/d67bp16/Sf//mfevrpp+X7vpYtW6ZTTjlFkrRz586BQ7GHH374wOHag81tt92m3/72t3rkkUc0e/Zs3XTTTdqwYYMee+wx1dfXK5fL6aGHHlIikdDrr7+uj3/84/r9738vSXrppZe0bt06HXLIIZo/f76uuOIKrVmzRt/73vd0xx136Otf/7o+97nP6ZprrtGyZcv05ptvauXKlXr++ef11a9+VS0tLXrmmWckaeB0azab1eLFi3XjjTfqhhtu0N13363rr79ey5Yt0+rVq2WM0d13361bb71VX/3qVyVJr776qh599FGl02m9613v0lVXXaUXXnhBDzzwgNauXatyubzX393VV1+t73znOzruuOP0pz/9SZ/97Gf1yCOP6KGHHtK6dev05S9/edx+vp/+9KfleZ4uuugiXX/99TJm93x7W7du1ebNmwf++zlYsW8Z333LVMG+hX3LeDhoIutA7rvvvhG/5pe//KVefPFF3XPPPZKkM888Uy+++KI++MEPatasWVqwYMG4FPlYrF69WhdffLFmz54tSTrkkEMGnrvwwgvleZ7e8Y53VI2kP/zhD7rwwguVTCYlSRdccEHVbRhj9voX/GB3wQUXDBxNKJfL+qd/+ie9+OKL8jxPr7/++sByCxcuHAjNY489VsuXL5cknXjiiQNHGZ544glt2LBh4DWpVEqZTEaPP/64fvzjHw88PnPmTElSPB7XBz7wAUmVIxmPPfaYJGn79u362Mc+pra2NpVKJR199NEDr33/+9+vuro61dXV6dBDD1V7e7uefvpprVixQolEQolEYmCdmUxGTz/99MBRSEkqlUqSpBUrVmjFihXj8SOUVDmcP2/ePKXTaV166aX6+c9/vtd277vvPl188cWKRCLjts3JiH3L6PctUw37lvExnfYtUyayRvrb5lNPPaXvfve7uueee1RXVzfw+NVXX62rr75akvSP//iPOvbYY90Neoz2HPdIb480Z84ctba26ogjjlBra+tBcVHlcDU0NAz8+bbbbtOcOXO0du1ahWE4sMOSKjutXTzPG/je87yBay7CMNTq1auVSCSGte1YLDYQrJFIREEQSJKuvfZaXXPNNVqxYoWefPJJ/eu//uvAa/b8e9zzNdWEYaiWlhatXbt2WOORKqcYHn30UUka0evmzZsnSWpqatKHP/xhPfvss/vsCHedZpjK2LdMrluvTST2LXtj33JgB801WY2Njcpms0M+f9999+mRRx7Z559qO8H169fri1/8ou66666B3+IkKQiCgUOzGzZs0CuvvKIzzjhj/N/MCJx11ln65S9/qa6uLkna65D+gbz73e/WQw89pHw+r3Q6rYcffnjguQsuuEA/+9nPJFWuyxjP31Imk1QqpcMPP1ye5+nnP//5fncy1Zxzzjm64447Br5/4YUXBh7//ve/P/D4gT6d2dfXp7lz50qSfvrTnx5wu6eddpoefvhhFQoFZTKZgVNZzc3NOuaYY3T//fdLqvwP8MUXX9zvum688caBi0mHy/d9dXZ2Sqr8xv7II49o/vz5A89v3LhRvb29WrJkybDXOVmxbxnffct0wb6FfctwHDSRNXPmTC1cuFDnnnvumC9Ovfnmm5XL5fT3f//3Ov/88/WpT31KUuUv/JJLLtHy5cv1hS98Qbfeequi0Yk92Dd//nxdd911Ou+887RkyRJdf/31w37tKaecog996ENasmSJLrroIi1cuHDguWuvvVaPP/64TjrpJD3x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p3BHn7yZ9VyLBQAARiwyr1lBa0oJEx3ymqORHsnatOc31toeY4wkvVFl2R5JIz5MVOxK6eWvOz74VQ4VtKVkkjHtJ0ABAACGZNMFJUykZajnRxpZwQgfHxZjzCpJq3Z9v/Qrnx7L6gAAAGpihokPOTPpSCPLCWvtnZLulKQFCxbY1U+uOcArAAAAJl5zU2P7UM9xrgwAAMCBmh3JMsYcLelv+r89sf/rhcaYt/T/+SfW2i21Gg8AAIBLtTxd+FZJ/3PQYx/s/0eSnlJl2gcAAICD3rAiy1r7I0k/GuK5qnMgWGuvkHTFHt//ThLzJQAAgGmBa7IAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIAAAAcILIA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\n", + "text/plain": [ + "
" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plt_chs = []\n", + "for q in [0, 1]:\n", + " plt_chs.append(device.qubits[q].measure)\n", + " plt_chs.append(device.qubits[q].drive)\n", + "\n", + "schedules[2].draw(channels_to_plot=plt_chs, scaling=10.)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "qobj = assemble(schedules, backend, meas_level=1, meas_return='single', shots=shots)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "job = backend.run(qobj)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job.status()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "result = job.result(timeout=3600)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# Extract the cals from the result\n", + "def result_subset(result, names):\n", + "\n", + " new_result = deepcopy(result)\n", + " new_results = []\n", + " \n", + " for res in new_result.results:\n", + " if res.header.name in names:\n", + " new_results.append(res)\n", + "\n", + " new_result.results = new_results\n", + " \n", + " return new_result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2) Fitting and using a discriminator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use the calibration schedules `cal_00` and `cal_11` to fit two linear discriminant discriminators. One for each qubit. Doing so implies that the qubit-qubit correlations in the single shot data are neglected. The discrimination does not necessarily need to be done in this way. Indeed, a single discriminator could be used to account for qubit-qubit correlations. The decision boundary is however harder to illustrate since two qubits implies a 4D decision space." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "cal_names = ['cal_00', 'cal_11']\n", + "\n", + "# Results of the calibration circuits in the schedules\n", + "cal_results = result_subset(result, cal_names)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The routine below is the **core of the discriminant analysis**. A call to the constructor also fits the discriminator to the provided results. We fit one discriminator per qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "discriminators = {}\n", + "\n", + "for q in qubits:\n", + " discriminators[q] = LinearIQDiscriminator(cal_results, [q], ['0', '1'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can retrieve the state of an I/Q point by calling the discriminator's **discriminate(iq_data) method**. The code below illustrates this using two points in the IQ plane `(0,0)` and `(0, -2e11)`. Depending on the results, these points will correspond a $|0\\rangle$ or $|1\\rangle$ state of the `test_qubit`." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Example results for qubit 0:\n", + "IQ point (0, 0) corresponds to state 0\n", + "IQ point (0, -200000000000) corresponds to state 1\n" + ] + } + ], + "source": [ + "test_qubit = 0\n", + "test_iq_data = [[0.0, 0.0], [0.0, -2.0e11]]\n", + "test_states = discriminators[test_qubit].discriminate(test_iq_data)\n", + "\n", + "print('Example results for qubit %i:' % test_qubit)\n", + "for idx, iq_point in enumerate(test_iq_data):\n", + " print('IQ point ({:.0f}, {:.0f}) corresponds to state '.format(iq_point[0], iq_point[1]) + test_states[idx])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below illustrates the use of the discriminator through plots and shows the decision boundary for each qubit." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "def get_iq_grid(xdata, qubit):\n", + " \"\"\"Return a mesh grid used to plot the decision boundary.\"\"\"\n", + " max_i = np.max(xdata[:, qubit*2])\n", + " min_i = np.min(xdata[:, qubit*2])\n", + " max_q = np.max(xdata[:, qubit*2+1])\n", + " min_q = np.min(xdata[:, qubit*2+1])\n", + " \n", + " spacing = (max_i - min_i) / 100.0\n", + " xx, yy = np.meshgrid(np.arange(min_i-10*spacing, max_i+10*spacing, spacing), \n", + " np.arange(min_q-10*spacing, max_q+10*spacing, spacing))\n", + "\n", + " return xx, yy" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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6NK1K4s/+4KpU25j8qSbKdq0idZ5efTt248zz9qden3kUUfx1NPPcPuU2zj91DH06tWLaffcw6MPPxz1uA8dNJjXX3uVGY8/xq5du+jSpQtXXDmBayZe51/nvvsf5KoJV3DKSSdQXl7O9TfcyA033czTz8ziskv/yZWXX0Z2Tg4nnXQyd997DiedcHzQa9xy2xT+cfHFjD3zDEpLSznzrLHMeHIm++67L59+9jl3TJ3CVROuZMf27eS3aUNBQQHjLrgwatuVUg2XGFPn16OUUtC3n/nghZeT3QyllFKNQJt9+y8zxuyf7HaEEpEmgJM83l3GmN9Dtk0HKnE49ZeI9AF+As4wxrwY8txPwI/GmFMi7aNf/wLz4pv29wbMzo30TsBYZKVqasUv/0Nyw4vCKaVSyz692lpev7VnWymllFIRGWN2AT/X0cv9CpQD/QIXikg20BOYU0ftUEoppWpEqzgopZRSqt4wxlQA7wFjvL3iPicDWcDcpDRMKaWUipH2bCullFIq4URkf6A7u2/s9xUR36Dmed7eckRkJnCOMSbwO8lk4Etgtog84t3P3cArxphltd96pZRSquY02FZKKaVUbfgHcE7Az6d4HwA9gNXe/6d5H37GmOUiMhy4C3gH2A48C1xfi+1VSimlEkqDbaWUUkolnDHmXODceNczxnwKHJzgZimllFJ1RsdsK6WUUqrR0dlYVLLp36BSDZ8G20oppZRqXFzplJWVJrsVqpErLy9DXGnRV1RKpSwNtpVSSinVqJj0Jqxft54d27ZRVVWpPYyqThljKCsrZf269bjTc5PdHKVULdIx20oppZRqVFwZObhd6WzcvBXZVIQx1clukmpkxJWGOz0XV0ZOspuilKpFGmwrpZRSqtGRtAxIa+X5f5LbohonTS9VquHT97lSSimllFJKKZVgGmwrpZRSSimllFIJpsG2UkoppZRSSimVYBpsK6WUUkoppZRSCabBtlJKKaWUUkoplWAabCullFJKKaWUUgkmxphkt6FeE5FNwJpktyPB8oGiZDciCfS4G4/GeMzQOI+7oR1zN2NMm2Q3ItU10Gu3Ew3t/VCX9NzFR89bfPS8xa++njvL67cG242QiHxtjNk/2e2oa3rcjUdjPGZonMfdGI9ZKTv6foifnrv46HmLj563+KXaudM0cqWUUkoppZRSKsE02FZKKaWU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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.rcParams['font.size'] = 16\n", + "fig, ax = plt.subplots(1, 2, figsize=(14,6))\n", + "\n", + "expected_states = [0, 1]\n", + "XX, YY, ZZ = [], [], []\n", + "\n", + "for q in qubits:\n", + " xdata = np.array(discriminators[q].get_xdata(cal_results))\n", + " ydata = np.array(discriminators[q].get_ydata(cal_results))\n", + "\n", + " cal00 = xdata[ydata == '0']\n", + " cal11 = xdata[ydata == '1']\n", + "\n", + " q1_pi = xdata[ydata == '1']\n", + " q1_nopi = xdata[ydata == '0']\n", + " \n", + " xx, yy = get_iq_grid(xdata, 0)\n", + " \n", + " Z = discriminators[q].discriminate(np.c_[xx.ravel(), yy.ravel()])\n", + " \n", + " # Save the mesh grids for a later plot\n", + " XX.append(xx)\n", + " YY.append(yy)\n", + " ZZ.append(Z.astype(float).reshape(xx.shape))\n", + " \n", + " ax[q].contourf(XX[q], YY[q], ZZ[q], cmap=plt.cm.RdBu_r, alpha=.2)\n", + " ax[q].scatter(cal00[:, 0], cal00[:, 1], label='Ground state')\n", + " ax[q].scatter(cal11[:, 0], cal11[:, 1], label='Excited state')\n", + " ax[q].legend(frameon=True)\n", + " ax[q].set_xlabel('I (arb. units)')\n", + " ax[q].set_ylabel('Q (arb. units)')\n", + " ax[q].set_title('Discrimination of qubit {}'.format(q));\n", + " \n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3) Creating a filter associated to the discriminator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can create a filter based on the discriminator to convert level 1 data into level 2 data. This filter can then be used to discriminate subsequent data." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.ignis.measurement.discriminator.filters import DiscriminationFilter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Extract from the original result the experiment(s) that we want to discriminate." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "experiment_result = result_subset(result, [experiment_name])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the filters based on the discriminators." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "filters = {}\n", + "\n", + "for q in qubits:\n", + " filters[q] = DiscriminationFilter(discriminators[q])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the filters to create new results with the level 2 data." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "results_lvl2 = {}\n", + "\n", + "for q in qubits:\n", + " results_lvl2[q] = filters[q].apply(experiment_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Plot the result." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(2, 2, figsize=(14,10))\n", + "\n", + "for q in [0, 1]:\n", + " iq_data = discriminators[q].get_xdata(experiment_result)\n", + " iq_data = np.array(iq_data)\n", + " \n", + " ax[q, 0].contourf(XX[q], YY[q], ZZ[q], cmap=plt.cm.RdBu_r, alpha=.2)\n", + " ax[q, 0].scatter(iq_data[:, 0], iq_data[:, 1], label=experiment_name)\n", + " ax[q, 0].set_xlabel('I (arb. units)')\n", + " ax[q, 0].set_ylabel('Q (arb. units)')\n", + " ax[q, 0].set_title('Qubit %i' % q)\n", + " ax[q, 1].set_title('Qubit %i' % q)\n", + " ax[q, 0].legend(frameon=True)\n", + " \n", + " counts = results_lvl2[q].results[0].data.counts.to_dict()\n", + " \n", + " ax[q, 1].bar(counts.keys(), counts.values())\n", + " ax[q, 1].set_xlabel('Qubit states')\n", + " ax[q, 1].set_ylabel('Counts')\n", + " \n", + "fig.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/ignis_logging.ipynb b/examples/ignis_logging.ipynb deleted file mode 100644 index 6f00d41e7..000000000 --- a/examples/ignis_logging.ipynb +++ /dev/null @@ -1,123 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ignis Logging\n", - "\n", - "This tutorial shows how to use logging in Ignis. The purpose of Ignis logging is twofold:\n", - "1. Log run-time events to the console.\n", - "2. Log data of interest to a file. \n", - "\n", - "Ignis logging is based on [Python's logging package](https://docs.python.org/3/library/logging.html). There are 3 classes in the Ignis logging package: \n", - "\n", - "* **IgnisLogger** - Objects of this class are used for logging. The class is derived from the Logger class defined in the Python's logging package\n", - "* **IgnisLogging** - A singleton class responsible for configuring logging behavior in Ignis as well as for creating and getting IgnisLogger objects.\n", - "* **IgnisLogReader** - A class for reading file logs created by IgnisLogger objects. Support basic filtering capabilities. \n", - "\n", - "\n", - "## Using IgnisLogger\n", - "\n", - "In this section we will see how to log data to console and files using IgnisLogger objects\n", - "\n", - "### Creating a logger object\n", - "Console and file logging in Ignis is performed using an object of the class IgnisLogger. An object of the IgnisLogger class is essentialy a Logger object of the [Python's logging package](https://docs.python.org/3/library/logging.html), extended with a convenient file logging capability. \n", - "\n", - "Let's create such an object:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'qiskit'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mqiskit\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mignis\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlogging\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mlogger\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mIgnisLogging\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_logger\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m__name__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'qiskit'" - ] - } - ], - "source": [ - "import qiskit.ignis.logging\n", - "\n", - "logger = IgnisLogging().get_logger(__name__)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can see here the use of the IgnisLogging singleton class for getting an IgnisLogger object. The parameter for the _get_logger_ method give the logger a name. This name is used when messages are printed to the console, to identify the source file which the log was printed from. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Logging to console\n", - "\n", - "Logging to console using an _IgnisLogger_ object is identical to using [Python's logging package](https://docs.python.org/3/library/logging.html). For convenience, here are some examples:\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Logging to file\n", - "\n", - "Logging to file is carried out by using the _log_to_file_ method of the _IgnisLogger_ class. The method expect key-value pairs given as Python keyword parameters. Any number of key-value pairs can be given to the function. Each call to the _log_to_file_ method will result in a new line being stored to the log file. Each line contains a timestamp, an identifying and list of key-value pairs. Here are a few example:\n", - "\n", - "TODO:\n", - "\n", - "### Configuring the IgnisLogger\n", - "\n", - "Besides creating _IgnisLogger_ objects, the _IgnisLogger_ class is also used in order to configure the file logging aspects in Ignis. The main aspects controlled by the _IgnisLogger_ class are:\n", - "\n", - "* Enabling/disabling file logging\n", - "* Location of the log files\n", - "* Miscellenous log file controls. \n", - "\n", - "\n", - "\n", - "\n", - "## Reading logged data from file" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/qiskit/ignis/VERSION.txt b/qiskit/ignis/VERSION.txt new file mode 100644 index 000000000..0d91a54c7 --- /dev/null +++ b/qiskit/ignis/VERSION.txt @@ -0,0 +1 @@ +0.3.0 diff --git a/qiskit/ignis/__init__.py b/qiskit/ignis/__init__.py index e69de29bb..516039f80 100644 --- a/qiskit/ignis/__init__.py +++ b/qiskit/ignis/__init__.py @@ -0,0 +1,15 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Qiskit Ignis Root.""" + +from .version import __version__ diff --git a/qiskit/ignis/characterization/__init__.py b/qiskit/ignis/characterization/__init__.py index 347f84f52..37d3026a4 100644 --- a/qiskit/ignis/characterization/__init__.py +++ b/qiskit/ignis/characterization/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/characterization/characterization_utils.py b/qiskit/ignis/characterization/characterization_utils.py index e8c07931a..42ea0efe7 100644 --- a/qiskit/ignis/characterization/characterization_utils.py +++ b/qiskit/ignis/characterization/characterization_utils.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """Utilities for circuits generation.""" diff --git a/qiskit/ignis/characterization/coherence/__init__.py b/qiskit/ignis/characterization/coherence/__init__.py index c0df19117..0a3f8e719 100644 --- a/qiskit/ignis/characterization/coherence/__init__.py +++ b/qiskit/ignis/characterization/coherence/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/characterization/coherence/circuits.py b/qiskit/ignis/characterization/coherence/circuits.py index 4fbaf2a0c..6be2aa04d 100644 --- a/qiskit/ignis/characterization/coherence/circuits.py +++ b/qiskit/ignis/characterization/coherence/circuits.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Circuit generation for coherence experiments diff --git a/qiskit/ignis/characterization/coherence/fitters.py b/qiskit/ignis/characterization/coherence/fitters.py index f4337aaa5..a6294a634 100644 --- a/qiskit/ignis/characterization/coherence/fitters.py +++ b/qiskit/ignis/characterization/coherence/fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Fitters of characteristic times diff --git a/qiskit/ignis/characterization/fitters.py b/qiskit/ignis/characterization/fitters.py index 32f6bf166..29e27ee55 100644 --- a/qiskit/ignis/characterization/fitters.py +++ b/qiskit/ignis/characterization/fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Fitters of characteristic times @@ -102,11 +109,11 @@ def series(self): return self._series @property - def measured_qubit(self): + def measured_qubits(self): """ - Return the index of the qubit whose characteristic time is measured + Return the indices of the qubits whose characteristic time is measured """ - return self._qubit + return self._qubits @property def xdata(self): @@ -263,7 +270,7 @@ def fit_data(self, qid=-1, p0=None, bounds=None, series=None): series = [series] if qid == -1: - qfit = self._qubits.copy() + qfit = range(len(self._qubits)) else: qfit = [qid] @@ -274,7 +281,7 @@ def fit_data(self, qid=-1, p0=None, bounds=None, series=None): p0 = self._defaultp0 for _, serieslbl in enumerate(series): - for qind, _ in enumerate(qfit): + for qind in qfit: tmp_params, fcov = \ curve_fit(self._fit_fun, self._xdata, self._ydata[serieslbl][qind]['mean'], @@ -464,12 +471,10 @@ def plot(self, qind, series='0', ax=None, show_plot=True): def build_counts_dict_from_list(count_list): - """ Add dictionary counts together """ - if len(count_list) == 1: return count_list[0] diff --git a/qiskit/ignis/characterization/gates/__init__.py b/qiskit/ignis/characterization/gates/__init__.py index d4439e4a6..d71d24ea0 100644 --- a/qiskit/ignis/characterization/gates/__init__.py +++ b/qiskit/ignis/characterization/gates/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/characterization/gates/circuits.py b/qiskit/ignis/characterization/gates/circuits.py index 923741d21..bf8f2a3bd 100644 --- a/qiskit/ignis/characterization/gates/circuits.py +++ b/qiskit/ignis/characterization/gates/circuits.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Circuit generation for measuring gate errors @@ -46,6 +53,7 @@ def ampcal_1Q_circuits(max_reps, qubits): for qind, qubit in enumerate(qubits): circ.u2(0.0, 0.0, qr[qubit]) for _ in range(circ_length): + circ.barrier(qr[qubit]) circ.u2(0.0, 0.0, qr[qubit]) for qind, qubit in enumerate(qubits): @@ -88,10 +96,12 @@ def anglecal_1Q_circuits(max_reps, qubits, angleerr=0.0): if angleerr != 0: circ.u1(-2*angleerr, qr[qubit]) for _ in range(2): + circ.barrier(qr[qubit]) circ.u2(-np.pi/2, np.pi/2, qr[qubit]) # Xp if angleerr != 0: circ.u1(2*angleerr, qr[qubit]) for _ in range(2): + circ.barrier(qr[qubit]) circ.u2(0.0, 0.0, qr[qubit]) # Yp if angleerr != 0: @@ -143,6 +153,7 @@ def ampcal_cx_circuits(max_reps, qubits, control_qubits): circ.x(qr[control_qubits[qind]]) circ.u2(-np.pi/2, np.pi/2, qr[qubit]) # X90p for _ in range(circ_length): + circ.barrier([qr[control_qubits[qind]], qr[qubit]]) circ.cx(qr[control_qubits[qind]], qr[qubit]) for qind, qubit in enumerate(qubits): @@ -194,6 +205,7 @@ def anglecal_cx_circuits(max_reps, qubits, control_qubits, angleerr=0.0): for _ in range(circ_length): if angleerr != 0: circ.u1(-angleerr, qr[qubit]) + circ.barrier([qr[control_qubits[qind]], qr[qubit]]) circ.cx(qr[control_qubits[qind]], qr[qubit]) if angleerr != 0: circ.u1(angleerr, qr[qubit]) diff --git a/qiskit/ignis/characterization/gates/fitters.py b/qiskit/ignis/characterization/gates/fitters.py index 24ce1377f..e818e02c1 100644 --- a/qiskit/ignis/characterization/gates/fitters.py +++ b/qiskit/ignis/characterization/gates/fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Fitters for hamiltonian parameters diff --git a/qiskit/ignis/characterization/hamiltonian/__init__.py b/qiskit/ignis/characterization/hamiltonian/__init__.py index 4988214c7..860e6f78a 100644 --- a/qiskit/ignis/characterization/hamiltonian/__init__.py +++ b/qiskit/ignis/characterization/hamiltonian/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/characterization/hamiltonian/circuits.py b/qiskit/ignis/characterization/hamiltonian/circuits.py index e0787bb39..0e73e0203 100644 --- a/qiskit/ignis/characterization/hamiltonian/circuits.py +++ b/qiskit/ignis/characterization/hamiltonian/circuits.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Circuit generation for measuring hamiltonian parametes diff --git a/qiskit/ignis/characterization/hamiltonian/fitters.py b/qiskit/ignis/characterization/hamiltonian/fitters.py index ca230c5da..f5bb73fd6 100644 --- a/qiskit/ignis/characterization/hamiltonian/fitters.py +++ b/qiskit/ignis/characterization/hamiltonian/fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Fitters for hamiltonian parameters diff --git a/qiskit/ignis/logging/__init__.py b/qiskit/ignis/logging/__init__.py new file mode 100644 index 000000000..978efd82e --- /dev/null +++ b/qiskit/ignis/logging/__init__.py @@ -0,0 +1,5 @@ +""" +Ignis Logging package initialization +""" + +from .ignis_logging import IgnisLogger, IgnisLogging, IgnisLogReader diff --git a/qiskit/ignis/logging/ignis_logging.py b/qiskit/ignis/logging/ignis_logging.py new file mode 100644 index 000000000..71355455c --- /dev/null +++ b/qiskit/ignis/logging/ignis_logging.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" +Ignis Logging +""" + +import logging +import logging.handlers +import logging.config +from logging import Logger +import os +import glob +from datetime import datetime +import re + + +class IgnisLogger(logging.getLoggerClass()): + """ + A logger class for Ignis. IgnisLogger is a like any other logging.Logger + object except it has an additional method: log_to_file, used to log data + in the form of key:value pairs to a log file. Logging configuration is + performed via a configuration file and is handled by IgnisLogging. + + """ + def __init__(self, name, level=logging.NOTSET): + """ + :param name: name of the logger. Usually set to package name using + __name__ + :param level: Verbosity level (use logging package enums) + """ + Logger.__init__(self, name, level) + self._file_logging_enabled = False + self._file_handler = None + self._stream_handler = None + self._conf_file_exists = False + self._warning_omitted = False + + def configure(self, sh, conf_file_exists): + """ + Internal configuration method of IgnisLogger. Should only be called + by IgnisLogger + + :param sh: StreamHandler object + :param conf_file_exists: Whether or not a file config exists + """ + self._stream_handler = sh + self.addHandler(sh) + self._conf_file_exists = conf_file_exists + + def log_to_file(self, **kargs): + """ + This function logs key:value pairs to a log file. + Note: Logger name in the log file is fixed (ignis_logging) + + :param kargs: Keyword parameters to be logged (e.g t1=0.02, + qubits=[1,2,4]) + """ + if not self._file_logging_enabled: + if not self._warning_omitted: # Omitting this warning only once + msg = "File logging is disabled" + if not self._conf_file_exists: + msg += ": no config file" + logger = logging.getLogger(__name__) + logger.warning(msg) + self._warning_omitted = True + return + + # We defer setting up the file handler, since its __init__ method + # has the side effect of creating the file + if self._file_handler is None: + self._file_handler = IgnisLogging().get_file_handler() + + assert(self._file_handler is not None), "file_handler is not set" + + Logger.removeHandler(self, self._stream_handler) + Logger.addHandler(self, self._file_handler) + logstr = "" + for k, v in kargs.items(): + logstr += "'{}':'{}' ".format(k, v) + + Logger.log(self, 100, logstr) + + Logger.removeHandler(self, self._file_handler) + Logger.addHandler(self, self._stream_handler) + + def enable_file_logging(self): + """ + Enables file logging for this logger object (note there is a single + object for a given logger name + """ + self._file_logging_enabled = True + + def disable_file_logging(self): + """ + Enables file logging for this logger object (note there is a single + object for a given logger name + """ + self._file_logging_enabled = False + + +class IgnisLogging: + """ + Singleton class to configure file logging via IgnisLogger. Logging to file + is enabled only if there is a config file present. Otherwise IgnisLogger + will behave as a regular logger. + + Config file is assumed to be /.qiskit/logging.yaml + + Config file fields: + =================== + file_logging: {true/false} - Specifies whether file logging is enabled + log_file: - path to the log file. If not specified, + ignis.log will be used + max_size: <# bytes> - maximum size limit for a given log file. + If not specified file size is unlimited + max_rotations: - maximum number of log files to rotate + (oldest file is deleted in case count + is reached) + """ + + # TODO: Should we allow to override file settings programmatically ? + # (e.g. enable logging) + _instance = None + _file_logging_enabled = False + _log_file = None + _max_bytes = 0 + _max_rotations = 0 + _log_label = "ignis_logging" + _default_datefmt = '%Y/%m/%d %H:%M:%S' + _config_file_exists = False + + # Making the class a Singleton + def __new__(cls): + if IgnisLogging._instance is None: + IgnisLogging._instance = object.__new__(cls) + IgnisLogging._initialize() + + return IgnisLogging._instance + + @staticmethod + def _load_config_file(): + """ + Loads and parses the config file + :return: a dictionary containing the the settings + """ + config_file_path = os.path.join(os.path.expanduser('~'), + ".qiskit", "logging.yaml") + config = dict() + if os.path.exists(config_file_path): + with open(config_file_path, "r") as log_file: + for line in log_file: + # removing comments + line = line[:line.find('#') if "#" in line else None] + line = line.split(':') # Splitting to key value + if len(line) < 2: + continue + config[line[0].strip().lower()] = line[1].strip().lower() + IgnisLogging._config_file_exists = True + + return config + + @staticmethod + def _initialize(): + """ + Initializes the logging facility for Ignis. + """ + logging.setLoggerClass(IgnisLogger) + + log_config = IgnisLogging._load_config_file() + # Reading the config file content + IgnisLogging._file_logging_enabled = \ + log_config.get('file_logging') == "true" + IgnisLogging._log_file = log_config.get('log_file') if \ + log_config.get('log_file') is not None else "ignis.log" + max_size = log_config.get('max_size') + IgnisLogging._max_bytes = int(max_size) if \ + max_size is not None and max_size.isdigit() else 0 + max_rotations = log_config.get('max_rotations') + IgnisLogging._max_rotations = int(max_rotations) if \ + max_rotations is not None and max_rotations.isdigit() else 0 + + def get_logger(self, __name__): + """ + Return an IgnisLogger object + :param __name__: Name of the module being logged + :return: IgnisLogger + """ + logger = logging.getLogger(__name__) + assert(isinstance(logger, IgnisLogger)), \ + "IgnisLogger class was not registered" + self._configure_logger(logger) + + return logger + + def get_file_handler(self): + """ + Configures and retrieves the RotatingFileHandler object. Called on + demand the first time IgnisLoggers needs to write to a file + :return: + """ + # Configuring the file handling aspect + fh = logging.handlers.RotatingFileHandler( + IgnisLogging._log_file, maxBytes=IgnisLogging._max_bytes, + backupCount=IgnisLogging._max_rotations) + + # Formatting + formatter = logging.Formatter( + '%(asctime)s {} %(message)s'.format(IgnisLogging._log_label), + datefmt=IgnisLogging._default_datefmt) + fh.setFormatter(formatter) + + return fh + + def _configure_logger(self, logger): + # Configuring the stream handler + sh = logging.StreamHandler() + sh.setLevel(logging.NOTSET) + stream_fmt = logging.Formatter('%(levelname)s: %(name)s - %(message)s') + sh.setFormatter(stream_fmt) + # This will enable limiting file size and rotating once file size + # is exhausted + + logger.configure(sh, IgnisLogging._config_file_exists) + + if IgnisLogging._file_logging_enabled: + logger.enable_file_logging() + + def get_log_file(self): + """ + :return: name of the log file + """ + return IgnisLogging._log_file + + def default_datetime_fmt(self): + """ + :return: Default date time format used for writing log entries + """ + return IgnisLogging._default_datefmt + + +class IgnisLogReader: + """ + Class to read from Ignis log and construct tabular representation based on + date/time and key criteria + """ + + def get_log_files(self): + """ + :return: Names of all log files (several may be present due to logging + file rotation). File names are sorted by modification time. + """ + file_name = IgnisLogging().get_log_file() + search_path = os.path.abspath(file_name + "*") + files = sorted(glob.glob(search_path), key=os.path.getmtime) + + result = list() + m = re.compile( + os.path.abspath(file_name).replace('\\', r'\\') + r"$|" + + os.path.abspath(file_name).replace('\\', r'\\') + r".\d+$") + for f in files: + if m.match(f): + result.append(f) + + return result + + def read_values(self, log_files=None, keys=None, from_datetime=None, + from_datetime_format=None, to_datetime=None, + to_datetime_format=None): + """ + Retrieves log lines. + + :param log_files: List of log files to read from. + :param keys: Retrieve only key value pairs of corresponding to keys. + A row with no matching keys will not be + retrieved. If not specified, all keys are retrieved (optional) + :param from_datetime: Retrieve only rows newer than the given date and + time (optional) + :param from_datetime_format: datetime format string. If not specified + will assume "%Y/%m/%d %H:%M:%S" (optional) + :param to_datetime: Retrieve only rows older than the given date and + time (optional) + :param to_datetime_format: datetime format string. If not specified + will assume "%Y/%m/%d %H:%M:%S" (optional) + :return: A list containing the retrieved rows of key pair values + """ + + if log_files is not None: + files = [log_files] if isinstance(log_files, str) else log_files + else: + files = self.get_log_files() + retrieved_date = list() + + for file in files: + with open(file, "r") as f: + for line in f: + terms = line.split() + date_time = terms[0:2] + + dt_filterd = self._filter_by_datetime(date_time, + from_datetime, + from_datetime_format, + to_datetime, + to_datetime_format) + if dt_filterd: + continue + + key_values = terms[3:] + if keys is not None: + key_values = self._filter_keys(key_values, keys) + if not key_values: + continue + + retrieved_date.append(date_time + key_values) + + return retrieved_date + + def _filter_keys(self, key_values, keys): + """ + Retrieves key value pairs matching the given keys + + :param key_values: list of key value pairs + :param keys: list of keys to retrieve key value pair of + :return: list of key value pairs according to keys + """ + + result = list() + assert(isinstance(key_values, list)), "key_values is not a list" + + for kv in key_values: + if kv.split(":")[0].strip("'") in keys: + result.append(kv) + + return result + + def _filter_by_datetime(self, row_datetime, from_dt, + from_dt_fmt, to_dt, + to_dt_fmt): + + """ + :return: True if the row should be filtered out + """ + if from_dt is not None and not isinstance(from_dt, datetime): + try: + if from_dt_fmt is None: + from_dt_fmt = IgnisLogging().default_datetime_fmt() + from_dt = datetime.strptime(from_dt, from_dt_fmt) + except ValueError as ve: + raise ve + + if to_dt is not None and not isinstance(to_dt, datetime): + try: + if to_dt_fmt is None: + to_dt_fmt = IgnisLogging().default_datetime_fmt() + to_dt = datetime.strptime(to_dt, to_dt_fmt) + except ValueError as ve: + raise ve + + if from_dt is None and to_dt is None: + return False + + row_datetime = datetime.strptime("%s %s" % (row_datetime[0], + row_datetime[1]), + IgnisLogging().default_datetime_fmt()) + + if from_dt is not None: + if row_datetime < from_dt: + return True + + if to_dt is not None: + if row_datetime > to_dt: + return True + + return False diff --git a/qiskit/ignis/logging/logging.yaml b/qiskit/ignis/logging/logging.yaml new file mode 100644 index 000000000..ec8e66a66 --- /dev/null +++ b/qiskit/ignis/logging/logging.yaml @@ -0,0 +1,4 @@ +file_logging: true # Enables/disables file logging (true/false) +log_file: ignis_test_log.log # Name of the file log +max_size: 1000000 # Max file size (in bytes). +max_rotations: 5 # Max number of file rotations diff --git a/qiskit/ignis/measurement/discriminator/discriminators.py b/qiskit/ignis/measurement/discriminator/discriminators.py new file mode 100644 index 000000000..4474fe120 --- /dev/null +++ b/qiskit/ignis/measurement/discriminator/discriminators.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +from abc import ABC, abstractmethod +from typing import Union, List +from sklearn.preprocessing import StandardScaler + +from qiskit.exceptions import QiskitError +from qiskit.result import Result +from qiskit.pulse.schedule import Schedule + + +class BaseDiscriminationFitter(ABC): + """ + IQDiscriminatorFitter takes IQ level 1 data produced by calibration + measurements with a known expected state. It fits a discriminator + that can be used to produce level 2 data, i.e. counts of quantum states. + """ + + def __init__(self, cal_results: Union[Result, List[Result]], + qubit_mask: List[int], expected_states: List[str], + standardize: bool = False, + schedules: Union[List[str], List[Schedule]] = None): + """ + Args: + cal_results (Union[Result, List[Result]]): calibration results, + Result or list of Result used to fit the discriminator. + qubit_mask (List[int]): determines which qubit's level 1 data to + use in the discrimination process. + expected_states (List[str]): a list that should have the same + length as schedules. All results in cal_results are used if + schedules is None. expected_states must have the corresponding + length. + standardize (bool): if true the discriminator will standardize the + xdata using the internal method _scale_data. + schedules (Union[List[str], List[Schedule]]): The schedules or a + subset of schedules in cal_results used to train the + discriminator. The user may also pass the name of the schedules + instead of the schedules. If schedules is None, then all the + schedules in cal_results are used. + """ + + # Use all results in cal_results if schedules is None + if schedules is None: + schedules = self._get_schedules(cal_results) + + self._expected_state = {} + self._add_expected_states(expected_states, schedules) + + # Used to rescale the xdata qubit by qubit. + self._description = None + self._standardize = standardize + self._scaler = None + self._qubit_mask = qubit_mask + self._schedules = schedules + self._backend_result_list = [] + self._fitted = False + + if cal_results is not None: + if isinstance(cal_results, list): + for result in cal_results: + self._backend_result_list.append(result) + else: + self._backend_result_list.append(cal_results) + + self._xdata = self.get_xdata(self._backend_result_list, schedules) + self._ydata = self.get_ydata(self._backend_result_list, schedules) + + def _add_ydata(self, schedule: Union[Schedule, str]): + """ + Adds the expected state of schedule to self._ydata. + Args: + schedule (Union[Schedule, str]): schedule or schedule name. + Used to get the expected state. + """ + if isinstance(schedule, Schedule): + self._ydata.append(self._expected_state[schedule.name]) + else: + self._ydata.append(self._expected_state[schedule]) + + def add_data(self, result: Result, expected_states: List[str], + refit: bool = True, + schedules: Union[List[Schedule], List[str]] = None): + """ + Args: + result (Result): a Result containing new data to be used to + train the discriminator. + expected_states (List[str]): the expected states of the results in + result. + refit (bool): refit the discriminator if True. + schedules (Union[List[Schedule], List[str]]): + """ + if schedules is None: + schedules = self._get_schedules(result) + + self._backend_result_list.append(result) + self._add_expected_states(expected_states, schedules) + self._schedules.extend(schedules) + self._xdata = self.get_xdata(self._backend_result_list, schedules) + self._ydata = self.get_ydata(self._backend_result_list, schedules) + + if refit: + self.fit() + + def _add_expected_states(self, expected_states: List[str], + schedules: Union[List[Schedule], List[str]]): + """ + Adds the given expected states to self._expected_states. + Args: + expected_states (List[str]): list of expected states. Must have the + same length as the number of schedules. + schedules (Union[List[Schedule], List[str]]): schedules or their + names corresponding to the expected states. + """ + if len(expected_states) != len(schedules): + raise QiskitError('Number of input schedules and assigned ' + 'states must be equal.') + + for idx, schedule in enumerate(schedules): + if isinstance(schedule, Schedule): + name = schedule.name + else: + name = schedule + expected_state = expected_states[idx] + self._expected_state[name] = expected_state + + @staticmethod + def _get_schedules(results: Union[Result, List[Result]]) -> List[str]: + """ + Extracts the names of all Schedules in a Result or a list of Result. + Args: + results (Union[Result, List[Result]]): the results for which to + extract the names, + Returns (List[str]): + The name of the schedules in results. + """ + schedules = [] + if isinstance(results, Result): + for res in results.results: + schedules.append(res.header.name) + else: + for result in results: + schedules.extend([_.header.name for _ in result.results]) + + return schedules + + @property + def expected_states(self): + """Returns the expected states used to train the discriminator.""" + return self._expected_state + + @property + def schedules(self): + """Returns the schedules with which the discriminator was fitted.""" + return self._schedules + + @property + def fitted(self): + """True if the discriminator has been fitted to calibration data.""" + return self._fitted + + def _scale_data(self, xdata: List[List[float]], + refit: bool = False) -> List[List[float]]: + """ + Scales xdata, for instance, by transforming it to zero mean and unit + variance data. + Args: + xdata (List[List[float]]): data as a list of features. Each + feature is itself a list. + refit (bool): if true than self._scaler is refit using the given + xdata. + Returns (List[List[float]]): the scaled xdata as a list of features. + """ + if not self._standardize: + return xdata + + if not self._scaler or refit: + self._scaler = StandardScaler(with_std=True) + self._scaler.fit(xdata) + + return self._scaler.transform(xdata) + + @abstractmethod + def get_xdata(self, results: Union[Result, List[Result]], + schedules: Union[List[str], List[Schedule]] = None) \ + -> List[List[float]]: + """ + Retrieves feature data (xdata) for the discriminator. + Args: + results (Union[Result, List[Result]]): the get_memory() method is + used to retrieve the level 1 data. If result is a list of + Result then the first Result to return the data of schedule in + schedules is used. + schedules (Union[List[str], List[Schedule]]): Either the names of + the schedules or the schedules themselves. + Returns (List[List[float]]): data as a list of features. Each feature + is a list. + """ + pass + + @abstractmethod + def get_ydata(self, results: Union[Result, List[Result]], + schedules: Union[List[str], List[Schedule]] = None) \ + -> List[str]: + """ + Args: + results (Union[Result, List[Result]]): results for which to + retrieve the y data (i.e. expected states). + schedules (Union[List[str], List[Schedule]]): the schedules for + which to get the y data. + Returns (List[str]): the y data, i.e. expected states. get_ydata is + designed to produce y data with the same length as the x data. + """ + pass + + @abstractmethod + def fit(self): + """ Fits the discriminator using self._xdata and self._ydata. """ + pass + + @abstractmethod + def discriminate(self, x_data: List[List[float]]) -> List[str]: + """ + Applies the discriminator to x_data + Args: + x_data (List[List[float]]): list of features. Each feature is + itself a list. + Returns (List[str]): the discriminated x_data as a list of labels. + """ + pass diff --git a/qiskit/ignis/measurement/discriminator/filters.py b/qiskit/ignis/measurement/discriminator/filters.py new file mode 100644 index 000000000..d84531b7d --- /dev/null +++ b/qiskit/ignis/measurement/discriminator/filters.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=cell-var-from-loop + + +""" +Discrimination filters. + +""" +from copy import deepcopy +from typing import List + +from qiskit.exceptions import QiskitError +from qiskit.ignis.characterization.fitters import BaseFitter +from qiskit.ignis.measurement.discriminator.discriminators import \ + BaseDiscriminationFitter +from qiskit.result.result import Result +from qiskit.result.models import ExperimentResultData +from qiskit.validation.base import Obj + + +class DiscriminationFilter: + """ + Implements a filter based on a discriminator that takes level 1 data to + level 2 data. + + Usage: + my_filter = DiscriminationFilter(my_discriminator) + new_result = filter.apply(level_1_data) + """ + + def __init__(self, discriminator: BaseDiscriminationFitter, + base: int = None): + """ + Args: + discriminator (BaseFitter): a discriminator that maps level 1 data + to level 2 data. + - Level 1 data may correspond to, e. g., IQ data. + - Level 2 data is the state counts. + base: the base of the expected states. If it is not given the base + is inferred from the expected_state instance of discriminator. + """ + self.discriminator = discriminator + + if base: + self.base = base + else: + self.base = DiscriminationFilter.get_base( + discriminator.expected_states) + + def apply(self, raw_data: Result) -> Result: + """ + Create a new result from the raw_data by converting level 1 data to + level 2 data. + + Args: + raw_data: list of qiskit.Result or qiskit.Result. + Returns: + A list of qiskit.Result or qiskit.Result. + """ + new_results = deepcopy(raw_data) + + to_be_discriminated = [] + + # Extract all the meas level 1 data from the Result. + shots_per_experiment_result = [] + for result in new_results.results: + if result.meas_level == 1: + shots_per_experiment_result.append(result.shots) + to_be_discriminated.append(result) + + new_results.results = to_be_discriminated + + x_data = self.discriminator.get_xdata(new_results) + y_data = self.discriminator.discriminate(x_data) + + start = 0 + for idx, n_shots in enumerate(shots_per_experiment_result): + counts = Obj.from_dict(self.count(y_data[start:(start+n_shots)])) + new_results.results[idx].data = ExperimentResultData(counts=counts, + memory=y_data) + start += n_shots + + for result in new_results.results: + result.meas_level = 2 + + return new_results + + @staticmethod + def get_base(expected_states: dict): + """ + Returns the base inferred from expected_states. + + The intent is to allow users to discriminate states higher than 0/1. + + DiscriminationFilter infers the basis from the expected states to allow + users to discriminate states outside of the computational sub-space. + For example, if the discriminated states are 00, 01, 02, 10, 11, ..., + 22 the basis will be 3. + + With this implementation the basis can be at most 10. + :param expected_states: + :return: the base inferred from the expected states + """ + base = 0 + for key in expected_states: + for char in expected_states[key]: + try: + value = int(char) + except ValueError: + raise QiskitError('Cannot parse character in ' + + expected_states[key]) + + base = base if base > value else value + + return base+1 + + def count(self, y_data: List[str]) -> dict: + """ + Converts discriminated results into raw counts. + Args: + y_data: result of a discrimination. + Returns: + A dict of raw counts. + """ + raw_counts = {} + + for cnt in y_data: + cnt_hex = hex(int(cnt, self.base)) + if cnt_hex in raw_counts: + raw_counts[cnt_hex] += 1 + else: + raw_counts[cnt_hex] = 1 + + return raw_counts diff --git a/qiskit/ignis/measurement/discriminator/iq_discriminators.py b/qiskit/ignis/measurement/discriminator/iq_discriminators.py new file mode 100644 index 000000000..a295e304f --- /dev/null +++ b/qiskit/ignis/measurement/discriminator/iq_discriminators.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +from typing import Union, List + +import numpy as np +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis + +from qiskit.exceptions import QiskitError +from qiskit.ignis.measurement.discriminator.discriminators import \ + BaseDiscriminationFitter +from qiskit.pulse import PulseError +from qiskit.result import Result +from qiskit.pulse.schedule import Schedule + + +class IQDiscriminationFitter(BaseDiscriminationFitter): + """ + Abstract discriminator that implements the data formatting for IQ + level 1 data. + """ + + def __init__(self, cal_results: Union[Result, List[Result]], + qubit_mask: List[int], expected_states: List[str], + standardize: bool = False, + schedules: Union[List[str], List[Schedule]] = None): + """ + Args: + cal_results (Union[Result, List[Result]]): calibration results, + Result or list of Result used to fit the discriminator. + qubit_mask (List[int]): determines which qubit's level 1 data to + use in the discrimination process. + expected_states (List[str]): a list that should have the same + length as schedules. All results in cal_results are used if + schedules is None. expected_states must have the corresponding + length. + standardize (bool): if true the discriminator will standardize the + xdata using the internal method _scale_data. + schedules (Union[List[str], List[Schedule]]): The schedules or a + subset of schedules in cal_results used to train the + discriminator. The user may also pass the name of the schedules + instead of the schedules. If schedules is None, then all the + schedules in cal_results are used. + """ + + BaseDiscriminationFitter.__init__(self, cal_results, qubit_mask, + expected_states, standardize, + schedules) + + def get_xdata(self, results: Union[Result, List[Result]], + schedules: Union[List[str], List[Schedule]] = None) \ + -> List[List[float]]: + """ + Retrieves feature data (xdata) for the discriminator. + Args: + results (Union[Result, List[Result]]): the get_memory() method is + used to retrieve the level 1 data. If result is a list of + Result, then the first Result in the list that returns the data + of schedule (through get_memory(schedule)) is used. + schedules (Union[List[str], List[Schedule]]): Either the names of + the schedules or the schedules themselves. + Returns (List[List[float]]): data as a list of features. Each feature + is a list. + """ + xdata = [] + if schedules is None: + schedules = self._get_schedules(results) + + for schedule in schedules: + iq_data = None + if isinstance(results, list): + for result in results: + try: + iq_data = result.get_memory(schedule) + except QiskitError: + pass + else: + iq_data = results.get_memory(schedule) + + if iq_data is None: + raise PulseError('Could not find IQ data for %s' % schedule) + + xdata.extend(self.format_iq_data(iq_data)) + + return self._scale_data(xdata) + + def get_ydata(self, results: Union[Result, List[Result]], + schedules: Union[List[str], List[Schedule]] = None): + """ + Args: + results (Union[Result, List[Result]]): results for which to + retrieve the y data (i.e. expected states). + schedules (Union[List[str], List[Schedule]]): the schedules for + which to get the y data. + Returns (List[str]): the y data, i.e. expected states. get_ydata is + designed to produce y data with the same length as the x data. + """ + ydata = [] + + if schedules is None: + schedules = self._get_schedules(results) + + for schedule in schedules: + if isinstance(schedule, Schedule): + shed_name = schedule.name + else: + shed_name = schedule + + if isinstance(results, Result): + results = [results] + + for result in results: + try: + iq_data = result.get_memory(schedule) + n_shots = iq_data.shape[0] + ydata.extend([self._expected_state[shed_name]]*n_shots) + except QiskitError: + pass + + return ydata + + def format_iq_data(self, iq_data: np.ndarray) -> List[List[float]]: + """ + Takes IQ data obtained from get_memory(), applies the qubit mask + and formats the data as a list of lists. Each sub list is IQ data + where the first half of the list is the I data and the second half of + the list is the Q data. + + Args: + iq_data (np.ndarray): data obtained from get_memory(). + Returns (List[List[float]]): A list of shots where each entry is a list + of IQ points. + """ + xdata = [] + if len(iq_data.shape) == 2: # meas_return 'single' case + for shot in iq_data[:, self._qubit_mask]: + shot_i = list(np.real(shot)) + shot_q = list(np.imag(shot)) + xdata.append(shot_i + shot_q) + + elif len(iq_data.shape) == 1: # meas_return 'avg' case + avg_i = list(np.real(iq_data[self._qubit_mask])) + avg_q = list(np.imag(iq_data[self._qubit_mask])) + xdata.append(avg_i + avg_q) + + else: + raise PulseError('Unknown measurement return type.') + + return xdata + + +class LinearIQDiscriminator(IQDiscriminationFitter): + """Linear discriminant analysis discriminator for IQ data.""" + + def __init__(self, cal_results: Union[Result, List[Result]], + qubit_mask: List[int], expected_states: List[str], + standardize: bool = False, + schedules: Union[List[str], List[Schedule]] = None, + discriminator_parameters: dict = None): + """ + Args: + cal_results (Union[Result, List[Result]]): calibration results, + Result or list of Result used to fit the discriminator. + qubit_mask (List[int]): determines which qubit's level 1 data to + use in the discrimination process. + expected_states (List[str]): a list that should have the same + length as schedules. All results in cal_results are used if + schedules is None. expected_states must have the corresponding + length. + standardize (bool): if true the discriminator will standardize the + xdata using the internal method _scale_data. + schedules (Union[List[str], List[Schedule]]): The schedules or a + subset of schedules in cal_results used to train the + discriminator. The user may also pass the name of the schedules + instead of the schedules. If schedules is None, then all the + schedules in cal_results are used. + discriminator_parameters (dict): parameters for Sklearn's LDA. + """ + if not discriminator_parameters: + discriminator_parameters = {} + + solver = discriminator_parameters.get('solver', 'svd') + shrink = discriminator_parameters.get('shrinkage', None) + store_cov = discriminator_parameters.get('store_covariance', False) + tol = discriminator_parameters.get('tol', 1.0e-4) + + self._lda = LinearDiscriminantAnalysis(solver=solver, shrinkage=shrink, + store_covariance=store_cov, + tol=tol) + + # Also sets the x and y data. + IQDiscriminationFitter.__init__(self, cal_results, qubit_mask, + expected_states, standardize, + schedules) + + self._description = 'Linear IQ discriminator for measurement level 1.' + + self.fit() + + def fit(self): + """ Fits the discriminator using self._xdata and self._ydata. """ + if len(self._xdata) == 0: + return + + self._lda.fit(self._xdata, self._ydata) + self._fitted = True + + def discriminate(self, x_data: List[List[float]]) -> List[str]: + """ + Applies the discriminator to x_data + Args: + x_data (List[List[float]]): list of features. Each feature is + itself a list. + Returns (List[str]): the discriminated x_data as a list of labels. + """ + return self._lda.predict(x_data) + + +class QuadraticIQDiscriminator(IQDiscriminationFitter): + + def __init__(self, cal_results: Union[Result, List[Result]], + qubit_mask: List[int], expected_states: List[str], + standardize: bool = False, + schedules: Union[List[str], List[Schedule]] = None, + discriminator_parameters: dict = None): + """ + Args: + cal_results (Union[Result, List[Result]]): calibration results, + Result or list of Result used to fit the discriminator. + qubit_mask (List[int]): determines which qubit's level 1 data to + use in the discrimination process. + expected_states (List[str]): a list that should have the same + length as schedules. All results in cal_results are used if + schedules is None. expected_states must have the corresponding + length. + standardize (bool): if true the discriminator will standardize the + xdata using the internal method _scale_data. + schedules (Union[List[str], List[Schedule]]): The schedules or a + subset of schedules in cal_results used to train the + discriminator. The user may also pass the name of the schedules + instead of the schedules. If schedules is None, then all the + schedules in cal_results are used. + discriminator_parameters (dict): parameters for Sklearn's LDA. + """ + if not discriminator_parameters: + discriminator_parameters = {} + + store_cov = discriminator_parameters.get('store_covariance', False) + tol = discriminator_parameters.get('tol', 1.0e-4) + + self._qda = QuadraticDiscriminantAnalysis(store_covariance=store_cov, + tol=tol) + + # Also sets the x and y data. + IQDiscriminationFitter.__init__(self, cal_results, qubit_mask, + expected_states, standardize, + schedules) + + self._description = 'Quadratic IQ discriminator for measurement ' \ + 'level 1.' + + def fit(self): + """ Fits the discriminator using self._xdata and self._ydata. """ + if len(self._xdata) == 0: + return + + self._qda.fit(self._xdata, self._ydata) + self._fitted = True + + def discriminate(self, x_data: List[List[float]]) -> List[str]: + """ + Applies the discriminator to x_data + Args: + x_data (List[List[float]]): list of features. Each feature is + itself a list. + Returns (List[str]): the discriminated x_data as a list of labels. + """ + return self._qda.predict(x_data) diff --git a/qiskit/ignis/mitigation/__init__.py b/qiskit/ignis/mitigation/__init__.py index e69de29bb..428fe2e50 100644 --- a/qiskit/ignis/mitigation/__init__.py +++ b/qiskit/ignis/mitigation/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit/ignis/mitigation/measurement/__init__.py b/qiskit/ignis/mitigation/measurement/__init__.py index be08748e2..5df5ada73 100644 --- a/qiskit/ignis/mitigation/measurement/__init__.py +++ b/qiskit/ignis/mitigation/measurement/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ @@ -11,6 +18,6 @@ """ # Measurement correction functions -from .circuits import complete_meas_cal -from .filters import MeasurementFilter -from .fitters import CompleteMeasFitter +from .circuits import complete_meas_cal, tensored_meas_cal +from .filters import MeasurementFilter, TensoredFilter +from .fitters import CompleteMeasFitter, TensoredMeasFitter diff --git a/qiskit/ignis/mitigation/measurement/circuits.py b/qiskit/ignis/mitigation/measurement/circuits.py index 47854f078..c0850ffd4 100644 --- a/qiskit/ignis/mitigation/measurement/circuits.py +++ b/qiskit/ignis/mitigation/measurement/circuits.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. - +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Measurement calibration circuits. To apply the measurement mitigation @@ -21,9 +27,7 @@ def complete_meas_cal(qubit_list=None, qr=None, cr=None, circlabel=''): Return a list of measurement calibration circuits for the full Hilbert space. - Each circuits creates a basis state - - 2 ** n circuits + Each of the 2**n circuits creates a basis state Args: qubit_list: A list of qubits to perform the measurement correction on, @@ -35,10 +39,10 @@ def complete_meas_cal(qubit_list=None, qr=None, cr=None, circlabel=''): cr (ClassicalRegister): A classical register. If none one is created circlabel: A string to add to the front of circuit names for - unique identification. + unique identification Returns: - A list of QuantumCircuit objects containing the calibration circuits. + A list of QuantumCircuit objects containing the calibration circuits A list of calibration state labels @@ -47,7 +51,8 @@ def complete_meas_cal(qubit_list=None, qr=None, cr=None, circlabel=''): where XXX is the basis state, e.g., cal_1001 - Pass the results of these circuits to "MeasurementFitter" constructor. + Pass the results of these circuits to the CompleteMeasurementFitter + constructor """ if qubit_list is None and qr is None: @@ -60,28 +65,114 @@ def complete_meas_cal(qubit_list=None, qr=None, cr=None, circlabel=''): if qubit_list is None: qubit_list = range(len(qr)) - cal_circuits = [] nqubits = len(qubit_list) + # labels for 2**n qubit states + state_labels = count_keys(nqubits) + + cal_circuits, _ = tensored_meas_cal([qubit_list], + qr, cr, circlabel) + + return cal_circuits, state_labels + + +def tensored_meas_cal(mit_pattern=None, qr=None, cr=None, circlabel=''): + """ + Return a list of calibration circuits + + Args: + mit_pattern (list of lists of integers): Qubits to perform the + measurement correction on, divided to groups according to tensors. + if None and qr is given then assumed to be performed over the entire + qr as one group + + qr (QuantumRegister): A quantum register. If none one is created + + cr (ClassicalRegister): A classical register. If none one is created + + circlabel: A string to add to the front of circuit names for + unique identification + + Returns: + A list of two QuantumCircuit objects containing the calibration + circuits + mit_pattern + + Additional Information: + The returned circuits are named circlabel+cal_XXX + where XXX is the basis state, + e.g., cal_000 and cal_111 + + Pass the results of these circuits to the TensoredMeasurementFitter + constructor + """ + + if mit_pattern is None and qr is None: + raise QiskitError("Must give one of mit_pattern or qr") + + qubits_in_pattern = [] + if mit_pattern is not None: + for qubit_list in mit_pattern: + for qubit in qubit_list: + if qubit in qubits_in_pattern: + raise QiskitError("mit_pattern cannot contain \ + multiple instances of the same qubit") + qubits_in_pattern.append(qubit) + + # Create the registers if not already done + if qr is None: + qr = QuantumRegister(max(qubits_in_pattern)+1) + else: + qubits_in_pattern = range(len(qr)) + mit_pattern = [qubits_in_pattern] + + nqubits = len(qubits_in_pattern) + # create classical bit registers if cr is None: cr = ClassicalRegister(nqubits) - # labels for 2**n qubit states - state_labels = count_keys(nqubits) + qubits_list_sizes = [len(qubit_list) for qubit_list in mit_pattern] + nqubits = sum(qubits_list_sizes) + size_of_largest_group = max([list_size for list_size in qubits_list_sizes]) + largest_labels = count_keys(size_of_largest_group) + + state_labels = [] + for largest_state in largest_labels: + basis_state = '' + for list_size in qubits_list_sizes: + basis_state = largest_state[:list_size] + basis_state + state_labels.append(basis_state) + cal_circuits = [] for basis_state in state_labels: qc_circuit = QuantumCircuit(qr, cr, name='%scal_%s' % (circlabel, basis_state)) - for qind, _ in enumerate(basis_state): - if int(basis_state[nqubits-qind-1]): - # the index labeling of the label is backwards with - # the list - qc_circuit.x(qr[qubit_list[qind]]) - # add measurements - qc_circuit.measure(qr[qubit_list[qind]], cr[qind]) + end_index = nqubits + for qubit_list, list_size in zip(mit_pattern, qubits_list_sizes): + + start_index = end_index - list_size + substate = basis_state[start_index:end_index] + + for qind in range(list_size): + if substate[list_size-qind-1] == '1': + qc_circuit.x(qr[qubit_list[qind]]) + + end_index = start_index + + qc_circuit.barrier(qr) + + # add measurements + end_index = nqubits + for qubit_list, list_size in zip(mit_pattern, qubits_list_sizes): + + for qind in range(list_size): + qc_circuit.measure(qr[qubit_list[qind]], + cr[nqubits-(end_index-qind)]) + + end_index -= list_size cal_circuits.append(qc_circuit) - return cal_circuits, state_labels + return cal_circuits, mit_pattern diff --git a/qiskit/ignis/mitigation/measurement/filters.py b/qiskit/ignis/mitigation/measurement/filters.py index a66a5c4a1..b08382669 100644 --- a/qiskit/ignis/mitigation/measurement/filters.py +++ b/qiskit/ignis/mitigation/measurement/filters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=cell-var-from-loop @@ -17,7 +24,10 @@ import scipy.linalg as la import numpy as np import qiskit +from qiskit.validation.base import Obj from qiskit import QiskitError +from qiskit.tools import parallel_map +from ...verification.tomography import count_keys class MeasurementFilter(): @@ -52,6 +62,11 @@ def state_labels(self): """return the state label ordering of the cal matrix""" return self._state_labels + @state_labels.setter + def state_labels(self, new_state_labels): + """set the state label ordering of the cal matrix""" + self._state_labels = new_state_labels + @cal_matrix.setter def cal_matrix(self, new_cal_matrix): """Set cal_matrix.""" @@ -93,6 +108,7 @@ def apply(self, raw_data, method='least_squares'): result2.get_counts('circ1')) """ + # check forms of raw_data if isinstance(raw_data, dict): # counts dictionary @@ -127,24 +143,29 @@ def apply(self, raw_data, method='least_squares'): # counts and push back into the new result new_result = deepcopy(raw_data) - for resultidx, _ in enumerate(raw_data.results): - new_counts = self.apply( - raw_data.get_counts(resultidx), method=method) + new_counts_list = parallel_map( + self._apply_correction, + [resultidx for resultidx, _ in enumerate(raw_data.results)], + task_args=(raw_data, method)) + + for resultidx, new_counts in new_counts_list: new_result.results[resultidx].data.counts = \ - new_result.results[resultidx]. \ - data.counts.from_dict(new_counts) + Obj(**new_counts) return new_result else: raise QiskitError("Unrecognized type for raw_data.") + if method == 'pseudo_inverse': + pinv_cal_mat = la.pinv(self._cal_matrix) + # Apply the correction for data_idx, _ in enumerate(raw_data2): if method == 'pseudo_inverse': raw_data2[data_idx] = np.dot( - la.pinv(self._cal_matrix), raw_data2[data_idx]) + pinv_cal_mat, raw_data2[data_idx]) elif method == 'least_squares': nshots = sum(raw_data2[data_idx]) @@ -176,5 +197,239 @@ def fun(x): raw_data2 = new_count_dict else: + # TODO: should probably change to: + # raw_data2 = raw_data2[0].tolist() raw_data2 = raw_data2[0] return raw_data2 + + def _apply_correction(self, resultidx, raw_data, method): + """ + Wrapper to call apply with a counts dictionary + + """ + new_counts = self.apply( + raw_data.get_counts(resultidx), method=method) + return resultidx, new_counts + + +class TensoredFilter(): + """ + Tensored measurement error mitigation filter + + Produced from a tensored measurement calibration fitter and can be applied + to data + + """ + + def __init__(self, cal_matrices, substate_labels_list): + """ + Initialize a tensored measurement error mitigation filter using + the cal_matrices from a tensored measurement calibration fitter + + Args: + cal_matrices: the calibration matrices for applying the correction + qubit_list_sizes: the lengths of the lists in mit_pattern + (see tensored_meas_cal in circuits.py for mit_pattern) + substate_labels_list (list of lists): for each calibration matrix + a list of the states (as strings, states in the subspace) + """ + + self._cal_matrices = cal_matrices + self._qubit_list_sizes = [] + self._indices_list = [] + self._substate_labels_list = [] + self.substate_labels_list = substate_labels_list + + @property + def cal_matrices(self): + """Return cal_matrices.""" + return self._cal_matrices + + @cal_matrices.setter + def cal_matrices(self, new_cal_matrices): + """Set cal_matrices.""" + self._cal_matrices = deepcopy(new_cal_matrices) + + @property + def substate_labels_list(self): + """Return _substate_labels_list""" + return self._substate_labels_list + + @substate_labels_list.setter + def substate_labels_list(self, new_substate_labels_list): + """Return _substate_labels_list""" + self._substate_labels_list = new_substate_labels_list + + # get the number of qubits in each subspace + self._qubit_list_sizes = [] + for _, substate_label_list in enumerate(self._substate_labels_list): + self._qubit_list_sizes.append( + int(np.log2(len(substate_label_list)))) + + # get the indices in the calibration matrix + self._indices_list = [] + for _, sub_labels in enumerate(self._substate_labels_list): + + self._indices_list.append( + {lab: ind for ind, lab in enumerate(sub_labels)}) + + @property + def qubit_list_sizes(self): + """Return _qubit_list_sizes""" + return self._qubit_list_sizes + + @property + def nqubits(self): + """Return the number of qubits""" + return sum(self._qubit_list_sizes) + + def apply(self, raw_data, method='least_squares'): + """ + Apply the calibration matrices to results + + Args: + raw_data: The data to be corrected. Can be in a number of forms. + a counts dictionary from results.get_countsphy data); + or a qiskit Result + + method (str): fitting method. If None, then least_squares is used. + 'pseudo_inverse': direct inversion of the cal matrices + 'least_squares': constrained to have physical probabilities + + Returns: + The corrected data in the same form as raw_data + """ + + all_states = count_keys(self.nqubits) + num_of_states = 2**self.nqubits + + # check forms of raw_data + if isinstance(raw_data, dict): + # counts dictionary + # convert to list + raw_data2 = [np.zeros(num_of_states, dtype=float)] + for state, count in raw_data.items(): + stateidx = int(state, 2) + raw_data2[0][stateidx] = count + + elif isinstance(raw_data, qiskit.result.result.Result): + + # extract out all the counts, re-call the function with the + # counts and push back into the new result + new_result = deepcopy(raw_data) + + new_counts_list = parallel_map( + self._apply_correction, + [resultidx for resultidx, _ in enumerate(raw_data.results)], + task_args=(raw_data, method)) + + for resultidx, new_counts in new_counts_list: + new_result.results[resultidx].data.counts = \ + Obj(**new_counts) + + return new_result + + else: + raise QiskitError("Unrecognized type for raw_data.") + + if method == 'pseudo_inverse': + pinv_cal_matrices = [] + for cal_mat in self._cal_matrices: + pinv_cal_matrices.append(la.pinv(cal_mat)) + + # Apply the correction + for data_idx, _ in enumerate(raw_data2): + + if method == 'pseudo_inverse': + inv_mat_dot_raw = np.zeros([num_of_states], dtype=float) + for state1_idx, state1 in enumerate(all_states): + for state2_idx, state2 in enumerate(all_states): + if raw_data2[data_idx][state2_idx] == 0: + continue + + product = 1. + end_index = self.nqubits + for p_ind, pinv_mat in enumerate(pinv_cal_matrices): + + start_index = end_index - \ + self._qubit_list_sizes[p_ind] + + state1_as_int = \ + self._indices_list[p_ind][ + state1[start_index:end_index]] + + state2_as_int = \ + self._indices_list[p_ind][ + state2[start_index:end_index]] + + end_index = start_index + product *= \ + pinv_mat[state1_as_int][state2_as_int] + if product == 0: + break + inv_mat_dot_raw[state1_idx] += \ + (product * raw_data2[data_idx][state2_idx]) + raw_data2[data_idx] = inv_mat_dot_raw + + elif method == 'least_squares': + + def fun(x): + mat_dot_x = np.zeros([num_of_states], dtype=float) + for state1_idx, state1 in enumerate(all_states): + mat_dot_x[state1_idx] = 0. + for state2_idx, state2 in enumerate(all_states): + if x[state2_idx] != 0: + product = 1. + end_index = self.nqubits + for c_ind, cal_mat in \ + enumerate(self._cal_matrices): + + start_index = end_index - \ + self._qubit_list_sizes[c_ind] + + state1_as_int = \ + self._indices_list[c_ind][ + state1[start_index:end_index]] + + state2_as_int = \ + self._indices_list[c_ind][ + state2[start_index:end_index]] + + end_index = start_index + product *= \ + cal_mat[state1_as_int][state2_as_int] + if product == 0: + break + mat_dot_x[state1_idx] += \ + (product * x[state2_idx]) + return sum( + (raw_data2[data_idx] - mat_dot_x)**2) + + x0 = np.random.rand(num_of_states) + x0 = x0 / sum(x0) + nshots = sum(raw_data2[data_idx]) + cons = ({'type': 'eq', 'fun': lambda x: nshots - sum(x)}) + bnds = tuple((0, nshots) for x in x0) + res = minimize(fun, x0, method='SLSQP', + constraints=cons, bounds=bnds, tol=1e-6) + raw_data2[data_idx] = res.x + + else: + raise QiskitError("Unrecognized method.") + + # convert back into a counts dictionary + new_count_dict = {} + for state_idx, state in enumerate(all_states): + if raw_data2[0][state_idx] != 0: + new_count_dict[state] = raw_data2[0][state_idx] + + return new_count_dict + + def _apply_correction(self, resultidx, raw_data, method): + """ + Wrapper to call apply with a counts dictionary + + """ + new_counts = self.apply( + raw_data.get_counts(resultidx), method=method) + return resultidx, new_counts diff --git a/qiskit/ignis/mitigation/measurement/fitters.py b/qiskit/ignis/mitigation/measurement/fitters.py index 5f63eadf2..77fd638b4 100644 --- a/qiskit/ignis/mitigation/measurement/fitters.py +++ b/qiskit/ignis/mitigation/measurement/fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=cell-var-from-loop @@ -12,9 +19,12 @@ Measurement correction fitters. """ +import copy +import re import numpy as np from qiskit import QiskitError -from .filters import MeasurementFilter +from .filters import MeasurementFilter, TensoredFilter +from ...verification.tomography import count_keys try: from matplotlib import pyplot as plt @@ -28,47 +38,151 @@ class CompleteMeasFitter(): Measurement correction fitter for a full calibration """ - def __init__(self, results, state_labels, circlabel=''): + def __init__(self, results, state_labels, qubit_list=None, circlabel=''): """ Initialize a measurement calibration matrix from the results of running the circuits returned by `measurement_calibration_circuits` + A wrapper for the tensored fitter + Args: results: the results of running the measurement calibration - ciruits. If this is None the user will set a call matrix later + circuits. If this is None the user will set a calibrarion matrix + later state_labels: list of calibration state labels returned from `measurement_calibration_circuits`. The output matrix will obey this ordering. + + qubit_list: List of the qubits (for reference and if the + subset is needed) + + circlabel: if the qubits were labeled """ - self._results = results - self._state_labels = state_labels - self._cal_matrix = None - self._circlabel = circlabel + if qubit_list is None: + qubit_list = range(len(state_labels[0])) + self._qubit_list = qubit_list - if self._results is not None: - self._build_calibration_matrix() + self._tens_fitt = TensoredMeasFitter(results, + [qubit_list], + [state_labels], + circlabel) @property def cal_matrix(self): """Return cal_matrix.""" - return self._cal_matrix + return self._tens_fitt.cal_matrices[0] @cal_matrix.setter def cal_matrix(self, new_cal_matrix): """set cal_matrix.""" - self._cal_matrix = new_cal_matrix + self._tens_fitt.cal_matrices = [copy.deepcopy(new_cal_matrix)] + + @property + def state_labels(self): + """Return state_labels.""" + return self._tens_fitt.substate_labels_list[0] + + @property + def qubit_list(self): + """Return list of qubits.""" + return self._qubit_list + + @state_labels.setter + def state_labels(self, new_state_labels): + """set state label.""" + self._tens_fitt.substate_labels_list[0] = new_state_labels @property def filter(self): """return a measurement filter using the cal matrix""" - return MeasurementFilter(self._cal_matrix, self._state_labels) + return MeasurementFilter(self.cal_matrix, self.state_labels) + + def add_data(self, new_results, rebuild_cal_matrix=True): + """ + Add measurement calibration data + + Args: + new_results: a single result or list of results + rebuild_cal_matrix: rebuild the calibration matrix + """ + + self._tens_fitt.add_data(new_results, rebuild_cal_matrix) + + def subset_fitter(self, qubit_sublist=None): + """ + Return a fitter object that is a subset of the qubits in the original + list. + + Args: + qubit_sublist: must be a subset of qubit_list + + Returns: + A fitter than has the calibration for a subset of qubits + + """ + + if self._tens_fitt.cal_matrices is None: + raise QiskitError("Calibration matrix is not initialized") + + if qubit_sublist is None: + raise QiskitError("Qubit sublist must be specified") + + for qb in qubit_sublist: + if qb not in self._qubit_list: + raise QiskitError("Qubit not in the original set of qubits") + + # build state labels + new_state_labels = count_keys(len(qubit_sublist)) + + # mapping between indices in the state_labels and the qubits in + # the sublist + qubit_sublist_ind = [] + for sqb in qubit_sublist: + for qbind, qb in enumerate(self._qubit_list): + if qb == sqb: + qubit_sublist_ind.append(qbind) + + # states in the full calibration which correspond + # to the reduced labels + q_q_mapping = [] + state_labels_reduced = [] + for label in self.state_labels: + tmplabel = [label[l] for l in qubit_sublist_ind] + state_labels_reduced.append(''.join(tmplabel)) + + for sub_lab_ind, _ in enumerate(new_state_labels): + q_q_mapping.append([]) + for labelind, label in enumerate(state_labels_reduced): + if label == new_state_labels[sub_lab_ind]: + q_q_mapping[-1].append(labelind) + + new_fitter = CompleteMeasFitter(results=None, + state_labels=new_state_labels, + qubit_list=qubit_sublist) + + new_cal_matrix = np.zeros([len(new_state_labels), + len(new_state_labels)]) + + # do a partial trace + for i in range(len(new_state_labels)): + for j in range(len(new_state_labels)): + + for l in q_q_mapping[i]: + for k in q_q_mapping[j]: + new_cal_matrix[i, j] += self.cal_matrix[l, k] + + new_cal_matrix[i, j] /= len(q_q_mapping[i]) + + new_fitter.cal_matrix = new_cal_matrix + + return new_fitter def readout_fidelity(self, label_list=None): """ Based on the results output the readout fidelity which is the - trace of the calibration matrix + normalized trace of the calibration matrix Args: label_list: If none returns the average assignment fidelity @@ -82,20 +196,155 @@ def readout_fidelity(self, label_list=None): Additional Information: The on-diagonal elements of the calibration matrix are the probabilities of measuring state 'x' given preparation of state - 'x' and so the trace is the average assignment fidelity + 'x' and so the normalized trace is the average assignment fidelity """ + return self._tens_fitt.readout_fidelity(0, label_list) - if self._cal_matrix is None: + def plot_calibration(self, ax=None, show_plot=True): + """ + Plot the calibration matrix (2D color grid plot) + + Args: + show_plot (bool): call plt.show() + + """ + + self._tens_fitt.plot_calibration(0, ax, show_plot) + + +class TensoredMeasFitter(): + """ + Measurement correction fitter for a tensored calibration + """ + + def __init__(self, results, mit_pattern, + substate_labels_list=None, circlabel=''): + """ + Initialize a measurement calibration matrix from the results of running + the circuits returned by `measurement_calibration_circuits` + + Args: + results: the results of running the measurement calibration + circuits. If this is None the user will set calibration matrices + later + + mit_pattern (list of lists of integers): qubits to perform the + measurement correction on, divided to groups according to tensors + + substate_labels_list (list of lists of strings): for each + calibration matrix, the labels of its rows and columns. + If None then the labels are ordered lexicographically + """ + + self._result_list = [] + self._cal_matrices = None + self._circlabel = circlabel + + self._qubit_list_sizes = \ + [len(qubit_list) for qubit_list in mit_pattern] + + self._indices_list = [] + if substate_labels_list is None: + self._substate_labels_list = [] + for list_size in self._qubit_list_sizes: + self._substate_labels_list.append(count_keys(list_size)) + else: + self._substate_labels_list = substate_labels_list + if len(self._qubit_list_sizes) != len(substate_labels_list): + raise ValueError("mit_pattern does not match \ + substate_labels_list") + + self._indices_list = [] + for _, sub_labels in enumerate(self._substate_labels_list): + self._indices_list.append( + {lab: ind for ind, lab in enumerate(sub_labels)}) + + self.add_data(results) + + @property + def cal_matrices(self): + """Return cal_matrices.""" + return self._cal_matrices + + @cal_matrices.setter + def cal_matrices(self, new_cal_matrices): + """set cal_matrices.""" + self._cal_matrices = copy.deepcopy(new_cal_matrices) + + @property + def substate_labels_list(self): + """Return _substate_labels_list""" + return self._substate_labels_list + + @property + def filter(self): + """return a measurement filter using the cal matrices""" + return TensoredFilter(self._cal_matrices, self._substate_labels_list) + + @property + def nqubits(self): + """Return _qubit_list_sizes""" + return sum(self._qubit_list_sizes) + + def add_data(self, new_results, rebuild_cal_matrix=True): + """ + Add measurement calibration data + + Args: + new_results: a single result or list of results + rebuild_cal_matrix: rebuild the calibration matrix + """ + + if new_results is None: + return + + if not isinstance(new_results, list): + new_results = [new_results] + + for result in new_results: + self._result_list.append(result) + + if rebuild_cal_matrix: + self._build_calibration_matrices() + + def readout_fidelity(self, cal_index=0, label_list=None): + """ + Based on the results output the readout fidelity, which is the average + of the diagonal entries in the calibration matrices + + Args: + cal_index: readout fidelity of which sub cal? + label_list (list of lists on states): + Returns the average fidelity over of the groups of states. + If None then each state used in the construction of the + calibration matrices forms a group of size 1 + + Returns: + readout fidelity (assignment fidelity) + + + Additional Information: + The on-diagonal elements of the calibration matrices are the + probabilities of measuring state 'x' given preparation of state + 'x' + """ + + if self._cal_matrices is None: raise QiskitError("Cal matrix has not been set") + if label_list is None: + label_list = [[label] for label in + self._substate_labels_list[cal_index]] + + state_labels = self._substate_labels_list[cal_index] fidelity_label_list = [] if label_list is None: - fidelity_label_list = [[i] for i in range(len(self._cal_matrix))] + fidelity_label_list = [[label] for label in state_labels] else: for fid_sublist in label_list: fidelity_label_list.append([]) for fid_statelabl in fid_sublist: - for label_idx, label in enumerate(self._state_labels): + for label_idx, label in enumerate(state_labels): if fid_statelabl == label: fidelity_label_list[-1].append(label_idx) continue @@ -110,43 +359,76 @@ def readout_fidelity(self, label_list=None): for state_idx_i in fid_label_sublist: for state_idx_j in fid_label_sublist: assign_fid_list[-1] += \ - self._cal_matrix[state_idx_i][state_idx_j] + self._cal_matrices[cal_index][state_idx_i][state_idx_j] assign_fid_list[-1] /= len(fid_label_sublist) return np.mean(assign_fid_list) - def _build_calibration_matrix(self): + def _build_calibration_matrices(self): """ - Build the measurement calibration matrix from the results of running + Build the measurement calibration matrices from the results of running the circuits returned by `measurement_calibration` - - Creates a 2**n x 2**n matrix that can be used to correct measurement - errors """ - cal_matrix = np.zeros( - [len(self._state_labels), len(self._state_labels)], dtype=float) - - for stateidx, state in enumerate(self._state_labels): - state_cnts = self._results.get_counts('%scal_%s' % - (self._circlabel, state)) - shots = sum(state_cnts.values()) - for stateidx2, state2 in enumerate(self._state_labels): - cal_matrix[stateidx, stateidx2] = state_cnts.get( - state2, 0) / shots - - self._cal_matrix = cal_matrix.transpose() - - def plot_calibration(self, ax=None, show_plot=True): + # initialize the set of empty calibration matrices + self._cal_matrices = [] + for list_size in self._qubit_list_sizes: + self._cal_matrices.append(np.zeros([2**list_size, 2**list_size], + dtype=float)) + + # go through for each calibration experiment + for result in self._result_list: + for experiment in result.results: + circ_name = experiment.header.name + # extract the state from the circuit name + # this was the prepared state + circ_search = re.search('(?<=' + self._circlabel + 'cal_)\\w+', + circ_name) + + # this experiment is not one of the calcs so skip + if circ_search is None: + continue + + state = circ_search.group(0) + + # get the counts from the result + state_cnts = result.get_counts(circ_name) + for measured_state, counts in state_cnts.items(): + end_index = self.nqubits + for cal_ind, cal_mat in enumerate(self._cal_matrices): + + start_index = end_index - \ + self._qubit_list_sizes[cal_ind] + + substate_index = self._indices_list[cal_ind][ + state[start_index:end_index]] + measured_substate_index = \ + self._indices_list[cal_ind][ + measured_state[start_index:end_index]] + end_index = start_index + + cal_mat[measured_substate_index][substate_index] += \ + counts + + for mat_index, _ in enumerate(self._cal_matrices): + sums_of_columns = np.sum(self._cal_matrices[mat_index], axis=0) + # pylint: disable=assignment-from-no-return + self._cal_matrices[mat_index] = np.divide( + self._cal_matrices[mat_index], sums_of_columns, + out=np.zeros_like(self._cal_matrices[mat_index]), + where=sums_of_columns != 0) + + def plot_calibration(self, cal_index=0, ax=None, show_plot=True): """ - Plot the calibration matrix (2D color grid plot) + Plot one of the calibration matrices (2D color grid plot) Args: + cal_index: calibration matrix to plot show_plot (bool): call plt.show() """ - if self._cal_matrix is None: + if self._cal_matrices is None: raise QiskitError("Cal matrix has not been set") if not HAS_MATPLOTLIB: @@ -157,15 +439,17 @@ def plot_calibration(self, ax=None, show_plot=True): plt.figure() ax = plt.gca() - axim = ax.matshow(self._cal_matrix, cmap=plt.cm.binary, clim=[0, 1]) + axim = ax.matshow(self.cal_matrices[cal_index], + cmap=plt.cm.binary, + clim=[0, 1]) ax.figure.colorbar(axim) ax.set_xlabel('Prepared State') ax.xaxis.set_label_position('top') ax.set_ylabel('Measured State') - ax.set_xticks(np.arange(len(self._state_labels))) - ax.set_yticks(np.arange(len(self._state_labels))) - ax.set_xticklabels(self._state_labels) - ax.set_yticklabels(self._state_labels) + ax.set_xticks(np.arange(len(self._substate_labels_list[cal_index]))) + ax.set_yticks(np.arange(len(self._substate_labels_list[cal_index]))) + ax.set_xticklabels(self._substate_labels_list[cal_index]) + ax.set_yticklabels(self._substate_labels_list[cal_index]) if show_plot: plt.show() diff --git a/qiskit/ignis/verification/__init__.py b/qiskit/ignis/verification/__init__.py index e69de29bb..428fe2e50 100644 --- a/qiskit/ignis/verification/__init__.py +++ b/qiskit/ignis/verification/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit/ignis/verification/quantum_volume/__init__.py b/qiskit/ignis/verification/quantum_volume/__init__.py new file mode 100644 index 000000000..edd0e5735 --- /dev/null +++ b/qiskit/ignis/verification/quantum_volume/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +""" +Quantum volume module +""" + +# Quantum volume functions +from .circuits import qv_circuits +from .fitters import QVFitter diff --git a/qiskit/ignis/verification/quantum_volume/circuits.py b/qiskit/ignis/verification/quantum_volume/circuits.py new file mode 100644 index 000000000..ccbd60249 --- /dev/null +++ b/qiskit/ignis/verification/quantum_volume/circuits.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Generates quantum volume circuits +""" + +import numpy as np +import qiskit +from qiskit.quantum_info.random import random_unitary + + +def qv_circuits(qubit_lists=None, ntrials=1, + qr=None, cr=None): + """ + Return a list of square quantum volume circuits (depth=width) + + The qubit_lists is specified as a list of qubit lists. For each + set of qubits, circuits the depth as the number of qubits in the list + are generated + + Args: + qubit_lists: list of list of qubits to apply qv circuits to. Assume + the list is ordered in increasing number of qubits + ntrials: number of random iterations + qr: quantum register to act on (if None one is created) + cr: classical register to measure to (if None one is created) + + Returns: + qv_circs: list of lists of circuits for the qv sequences + (separate list for each trial) + qv_circs_nomeas: same as above with no measurements for the ideal + simulation + """ + + circuits = [[] for e in range(ntrials)] + circuits_nomeas = [[] for e in range(ntrials)] + + # get the largest qubit number out of all the lists (for setting the + # register) + + depth_list = [len(l) for l in qubit_lists] + + # go through for each trial + for trial in range(ntrials): + + # go through for each depth in the depth list + for depthidx, depth in enumerate(depth_list): + + n_q_max = np.max(qubit_lists[depthidx]) + + qr = qiskit.QuantumRegister(int(n_q_max+1), 'qr') + qr2 = qiskit.QuantumRegister(int(depth), 'qr') + cr = qiskit.ClassicalRegister(int(depth), 'cr') + + qc = qiskit.QuantumCircuit(qr, cr) + qc2 = qiskit.QuantumCircuit(qr2, cr) + + qc.name = 'qv_depth_%d_trial_%d' % (depth, trial) + qc2.name = qc.name + + # build the circuit + for _ in range(depth): + # Generate uniformly random permutation Pj of [0...n-1] + perm = np.random.permutation(depth) + # For each pair p in Pj, generate Haar random SU(4) + for k in range(int(np.floor(depth/2))): + U = random_unitary(4) + pair = int(perm[2*k]), int(perm[2*k+1]) + qc.append(U, [qr[qubit_lists[depthidx][pair[0]]], + qr[qubit_lists[depthidx][pair[1]]]]) + qc2.append(U, [qr2[pair[0]], + qr2[pair[1]]]) + + # append an id to all the qubits in the ideal circuits + # to prevent a truncation error in the statevector + # simulators + qc2.u1(0, qr2) + + circuits_nomeas[trial].append(qc2) + + # add measurement + for qind, qubit in enumerate(qubit_lists[depthidx]): + qc.measure(qr[qubit], cr[qind]) + + circuits[trial].append(qc) + + return circuits, circuits_nomeas diff --git a/qiskit/ignis/verification/quantum_volume/fitters.py b/qiskit/ignis/verification/quantum_volume/fitters.py new file mode 100644 index 000000000..e528bd540 --- /dev/null +++ b/qiskit/ignis/verification/quantum_volume/fitters.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Functions used for the analysis of quantum volume results. + +Based on Cross et al. "Validating quantum computers using +randomized model circuits", arXiv:1811.12926 +""" + +import math +import numpy as np +from qiskit import QiskitError +from ...characterization.fitters import build_counts_dict_from_list + +try: + from matplotlib import pyplot as plt + HAS_MATPLOTLIB = True +except ImportError: + HAS_MATPLOTLIB = False + + +class QVFitter: + """ + Class for fitters for quantum volume + """ + + def __init__(self, backend_result=None, statevector_result=None, + qubit_lists=None): + """ + Args: + backend_result: list of results (qiskit.Result). + statevector_result: the ideal statevectors of each circuit + qubit_lists: list of qubit lists (what was passed to the + circuit generation) + """ + + self._qubit_lists = qubit_lists + self._depths = [len(l) for l in qubit_lists] + self._ntrials = 0 + + self._result_list = [] + self._heavy_output_counts = {} + self._circ_shots = {} + self._heavy_output_prob_ideal = {} + self._ydata = [] + self._heavy_outputs = {} + self.add_statevectors(statevector_result) + self.add_data(backend_result) + + @property + def depths(self): + """Return depth list.""" + return self._depths + + @property + def qubit_lists(self): + """Return depth list.""" + return self._qubit_lists + + @property + def results(self): + """Return all the results.""" + return self._result_list + + @property + def heavy_outputs(self): + """Return the ideal heavy outputs dictionary.""" + return self._heavy_outputs + + @property + def heavy_output_counts(self): + """Return the number of heavy output counts as measured.""" + return self._heavy_output_counts + + @property + def heavy_output_prob_ideal(self): + """Return the heavy output probability ideally.""" + return self._heavy_output_prob_ideal + + @property + def ydata(self): + """Return the average and std of the output probability.""" + return self._ydata + + def add_statevectors(self, new_statevector_result): + """ + Add the ideal results and convert to the heavy outputs + Assume the result is from 'statevector_simulator' + + Args: + new_statevector_result: ideal results + """ + + if new_statevector_result is None: + return + + if not isinstance(new_statevector_result, list): + new_statevector_result = [new_statevector_result] + + for result in new_statevector_result: + for qvcirc in result.results: + + circname = qvcirc.header.name + + # get the depth/width from the circuit name + # qv_depth_%d_trial_%d + depth = int(circname.split('_')[2]) + + if circname in self._heavy_outputs: + raise QiskitError("Already added the ideal result " + "for circuit %s" % circname) + + # convert the result into probability dictionary + qstate = result.get_statevector(circname) + pvector = np.multiply(qstate, qstate.conjugate()) + format_spec = "{0:0%db}" % depth + pmap = {format_spec.format(b): + float(np.real(pvector[b])) + for b in range(2**depth)} + median_prob = self._median_probabilities([pmap]) + self._heavy_outputs[qvcirc.header.name] = \ + self._heavy_strings(pmap, median_prob[0]) + + # calculate the heavy output probability + self._heavy_output_prob_ideal[circname] = \ + self._subset_probability( + self._heavy_outputs[circname], + pmap) + + def add_data(self, new_backend_result, rerun_fit=True): + """ + Add a new result. Re calculate fit + + Args: + new_backend_result: list of qv results + rerun_fit: re caculate the means and fit the result + + Additional information: + Assumes that 'result' was executed is + the output of circuits generated by qv_circuits, + """ + + if new_backend_result is None: + return + + if not isinstance(new_backend_result, list): + new_backend_result = [new_backend_result] + + for result in new_backend_result: + self._result_list.append(result) + + # update the number of trials *if* new ones + # added. + for qvcirc in result.results: + ntrials_circ = int(qvcirc.header.name.split('_')[-1]) + if (ntrials_circ+1) > self._ntrials: + self._ntrials = ntrials_circ+1 + + if qvcirc.header.name not in self._heavy_output_prob_ideal: + raise QiskitError('Ideal distribution ' + 'must be loaded first') + + if rerun_fit: + self.calc_data() + self.calc_statistics() + + def calc_data(self): + """ + Make a count dictionary for each unique circuit from all the + results. + + Calculate the heavy output probability + + Additional information: + Assumes that 'result' was executed is + the output of circuits generated by qv_ciruits, + """ + + circ_counts = {} + for trialidx in range(self._ntrials): + for _, depth in enumerate(self._depths): + circ_name = 'qv_depth_%d_trial_%d' % (depth, trialidx) + + # get the counts form ALL executed circuits + count_list = [] + for result in self._result_list: + try: + count_list.append(result.get_counts(circ_name)) + except (QiskitError, KeyError): + pass + + circ_counts[circ_name] = \ + build_counts_dict_from_list(count_list) + + self._circ_shots[circ_name] = \ + sum(circ_counts[circ_name].values()) + + # calculate the heavy output probability + self._heavy_output_counts[circ_name] = \ + self._subset_probability( + self._heavy_outputs[circ_name], + circ_counts[circ_name]) + + def calc_statistics(self): + """ + Convert the heavy outputs in the different trials into mean and error + for plotting + + Here we assume the error is due to a binomial distribution + """ + + self._ydata = np.zeros([4, len(self._depths)], dtype=float) + + exp_vals = np.zeros(self._ntrials, dtype=float) + ideal_vals = np.zeros(self._ntrials, dtype=float) + + for depthidx, depth in enumerate(self._depths): + + exp_shots = 0 + + for trialidx in range(self._ntrials): + cname = 'qv_depth_%d_trial_%d' % (depth, trialidx) + exp_vals[trialidx] = self._heavy_output_counts[cname] + exp_shots += self._circ_shots[cname] + ideal_vals[trialidx] = self._heavy_output_prob_ideal[cname] + + self._ydata[0][depthidx] = np.sum(exp_vals)/np.sum(exp_shots) + self._ydata[1][depthidx] = (self._ydata[0][depthidx] * + (1.0-self._ydata[0][depthidx]) + / self._ntrials)**0.5 + self._ydata[2][depthidx] = np.mean(ideal_vals) + self._ydata[3][depthidx] = (self._ydata[2][depthidx] * + (1.0-self._ydata[2][depthidx]) + / self._ntrials)**0.5 + + def plot_qv_data(self, ax=None, show_plt=True): + """ + Plot the qv data as a function of depth + + Args: + ax (Axes or None): plot axis (if passed in). + add_label (bool): Add an EPC label + show_plt (bool): display the plot. + + Raises: + ImportError: If matplotlib is not installed. + """ + + if not HAS_MATPLOTLIB: + raise ImportError('The function plot_rb_data needs matplotlib. ' + 'Run "pip install matplotlib" before.') + + if ax is None: + plt.figure() + ax = plt.gca() + + xdata = range(len(self._depths)) + + # Plot the experimental data with error bars + ax.errorbar(xdata, self._ydata[0], + yerr=self._ydata[1], + color='r', linestyle=None, marker='o', markersize=5, + label='Exp') + + # Plot the ideal data with error bars + ax.errorbar(xdata, self._ydata[2], + yerr=self._ydata[3], + color='b', linestyle=None, marker='o', markersize=5, + label='Ideal') + + # Plot the threshold + ax.plot(xdata, + np.ones(len(xdata))*2.0/3.0, + color='black', linestyle='--', linewidth=2, label='Threshold') + ax.tick_params(labelsize=14) + + ax.set_xticks(xdata) + ax.set_xticklabels(self._qubit_lists, rotation=45) + + ax.set_xlabel('Qubit Subset', fontsize=16) + ax.set_ylabel('Heavy Probability', fontsize=16) + ax.grid(True) + + ax.legend() + + if show_plt: + plt.show() + + def qv_success(self): + """Return whether each depth was successful (>2/3 with confidence + greater than 97.5) and the confidence + + Returns: + List of lenth depth with eact element a 3 list with + - success True/False + - confidence + """ + + success_list = [] + + for depth_ind, _ in enumerate(self._depths): + success_list.append([False, 0.0]) + hmean = self._ydata[0][depth_ind] + if hmean > 2/3: + cfd = 0.5 * (1 + + math.erf((hmean - 2/3) + / (1e-10 + + self._ydata[1][depth_ind])/2**0.5)) + success_list[-1][1] = cfd + + if cfd > 0.975: + success_list[-1][0] = True + + return success_list + + def quantum_volume(self): + """Return the volume for each depth. + + Returns: + List of quantum volumes + """ + + qv_list = 2**np.array(self._depths) + + return qv_list + + def _heavy_strings(self, ideal_distribution, ideal_median): + """Return the set of heavy output strings. + + ideal_distribution = dict of ideal output distribution + where keys are bit strings (as strings) and values are + probabilities of observing those strings + ideal_mean = median probability across all outputs + + Return the set of heavy output strings, i.e. those strings + whose ideal probability of occurrence exceeds the median. + """ + return list(filter(lambda x: ideal_distribution[x] > ideal_median, + list(ideal_distribution.keys()))) + + def _median_probabilities(self, distributions): + """Return a list of median probabilities. + + distributions = list of dicts mapping binary strings + (as strings) to probabilities. + + Return a list of median probabilities. + """ + medians = [] + for dist in distributions: + values = np.array(list(dist.values())) + medians.append(float(np.real(np.median(values)))) + + return medians + + def _subset_probability(self, strings, distribution): + """Return the probability of a subset of outcomes. + + strings = list of bit strings (as strings) + distribution = dict where keys are bit strings (as strings) + and values are probabilities of observing those strings + + Return the probability of the subset of strings, i.e. the sum + of the probabilities of each string as given by the + distribution. + """ + return sum([distribution.get(value, 0) for value in strings]) diff --git a/qiskit/ignis/verification/randomized_benchmarking/Clifford.py b/qiskit/ignis/verification/randomized_benchmarking/Clifford.py index 29bc5a03d..c9ee7ac62 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/Clifford.py +++ b/qiskit/ignis/verification/randomized_benchmarking/Clifford.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # NOTE(mtreinish): Needed to avoid error on logical_xor where pylint thinks it # doesn't have a return. diff --git a/qiskit/ignis/verification/randomized_benchmarking/__init__.py b/qiskit/ignis/verification/randomized_benchmarking/__init__.py index a0181966a..4eeb80c12 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/__init__.py +++ b/qiskit/ignis/verification/randomized_benchmarking/__init__.py @@ -1,18 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ -Measurement correction module +Randomized Benchmarking module """ -# Measurement correction functions +# Randomized Benchmarking functions from .Clifford import Clifford -from . import clifford_utils +from .basic_utils import BasicUtils +from .clifford_utils import CliffordUtils from .circuits import randomized_benchmarking_seq -from .fitters import RBFitter +from .fitters import RBFitter, InterleavedRBFitter, PurityRBFitter from . import rb_utils diff --git a/qiskit/ignis/verification/randomized_benchmarking/basic_utils.py b/qiskit/ignis/verification/randomized_benchmarking/basic_utils.py new file mode 100644 index 000000000..26cedc351 --- /dev/null +++ b/qiskit/ignis/verification/randomized_benchmarking/basic_utils.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +A basic utils class for different groups for randomized benchmarking. +""" + +from abc import ABC, abstractmethod + + +class BasicUtils(ABC): + """ + Abstract base class (ABS) for utils for + various groups and gate sets for randomized benchmarking + """ + + @abstractmethod + def num_qubits(self): + """Return the number of qubits.""" + return + + @abstractmethod + def group_tables(self): + """Return the group tables.""" + return + + @abstractmethod + def elmnt(self): + """Return a group element.""" + return + + @abstractmethod + def elmnt_key(self): + """Return a key to a group element in the table.""" + return + + @abstractmethod + def gatelist(self): + """Return a list of gates corresponding to a group element.""" + return + + @abstractmethod + def load_tables(self): + """Load pickled group tables, + or generate them if they do not exist""" + return + + @abstractmethod + def compose_gates(self): + """Compose group elements.""" + return + + @abstractmethod + def find_inverse_gates(self): + """Compute the inverse group element.""" + return diff --git a/qiskit/ignis/verification/randomized_benchmarking/circuits.py b/qiskit/ignis/verification/randomized_benchmarking/circuits.py index 06f6d195e..9303df368 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/circuits.py +++ b/qiskit/ignis/verification/randomized_benchmarking/circuits.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # TODO(mtreinish): Remove these disables when implementation is finished # pylint: disable=unused-argument,unnecessary-pass @@ -13,27 +20,38 @@ """ import copy +import os +import sys import numpy as np import qiskit -from . import Clifford -from . import clifford_utils as clutils +from .Clifford import Clifford +from .clifford_utils import CliffordUtils as clutils +from ...logging import * + +logger = IgnisLogging().get_logger(__name__) -def handle_length_multiplier(length_multiplier, len_pattern): +def handle_length_multiplier(length_multiplier, len_pattern, + is_purity=False): """ Check validity of length_multiplier. In addition, transform it into a vector if it is a constant. + In case of purity rb the length multiplier should be None. Args: length_multiplier: length of the multiplier len_pattern: length of the RB pattern + is_purity: True only for purity rb (default is False) Returns: length_multiplier """ if hasattr(length_multiplier, "__len__"): + if is_purity: + raise ValueError( + "In case of Purity RB the length multiplier should be None") if len(length_multiplier) != len_pattern: raise ValueError( "Length mulitiplier must be the same length as the pattern") @@ -46,14 +64,20 @@ def handle_length_multiplier(length_multiplier, len_pattern): return length_multiplier -def check_pattern(pattern): +def check_pattern(pattern, is_purity=False): """ Verifies that the input pattern is valid i.e., that each qubit appears at most once + In case of purity rb, checkes that all + simultaneous sequences have the same dimension + (e.g. only 1-qubit squences, or only 2-qubits + sequences etc.) + Args: pattern: RB pattern n_qubits: number of qubits + is_purity: True only for purity rb (default is False) Raises: ValueError: if the pattern is not valid @@ -61,17 +85,28 @@ def check_pattern(pattern): Return: qlist: flat list of all the qubits in the pattern maxqubit: the maximum qubit number + maxdim: the maximal dimension (maximal number of qubits + in all sequences) """ pattern_flat = [] + pattern_dim = [] for pat in pattern: pattern_flat.extend(pat) + pattern_dim.append(len(pat)) _, uni_counts = np.unique(np.array(pattern_flat), return_counts=True) if (uni_counts > 1).any(): raise ValueError("Invalid pattern. Duplicate qubit index.") - return pattern_flat, np.max(pattern_flat) + dim_distinct = np.unique(pattern_dim) + if is_purity: + if len(dim_distinct) > 1: + raise ValueError("Invalid pattern for purity RB. \ + All simultaneous sequences should have the \ + same dimension.") + + return pattern_flat, np.max(pattern_flat).item(), np.max(pattern_dim) def calc_xdata(length_vector, length_multiplier): @@ -93,60 +128,24 @@ def calc_xdata(length_vector, length_multiplier): return np.array(xdata) -def load_tables(max_nrb=2): - """ - Returns the needed Clifford tables - - Args: - max_nrb: maximal number of qubits for the largest required table - - Returns: - A table of Clifford objects - """ - - clifford_tables = [[] for i in range(max_nrb)] - for rb_num in range(max_nrb): - # load the clifford tables, but only if we're using that particular rb - # number - if rb_num == 0: - # 1Q Cliffords, load table programmatically - clifford_tables[0] = clutils.clifford1_gates_table() - elif rb_num == 1: - # 2Q Cliffords - # Try to load the table in from file. If it doesn't exist then make - # the file - try: - clifford_tables[rb_num] = clutils.load_clifford_table( - picklefile='cliffords%d.pickle' % (rb_num + 1)) - except OSError: - # table doesn't exist, so save it - # this will save time next run - print('Making the n=%d Clifford Table' % (rb_num + 1)) - clutils.pickle_clifford_table( - picklefile='cliffords%d.pickle' % (rb_num + 1), - num_qubits=(rb_num+1)) - clifford_tables[rb_num] = clutils.load_clifford_table( - picklefile='cliffords%d.pickle' % (rb_num + 1)) - else: - raise ValueError("The number of qubits should be only 1 or 2") - - return clifford_tables - - def randomized_benchmarking_seq(nseeds=1, length_vector=None, rb_pattern=None, length_multiplier=1, seed_offset=0, - align_cliffs=False): + align_cliffs=False, + interleaved_gates=None, + is_purity=False, + group_gates=None, + rand_seed=None): """ Get a generic randomized benchmarking sequence Args: nseeds: number of seeds - length_vector: 'm' length vector of Clifford lengths. Must be in - ascending order. RB sequences of increasing length grow on top of the - previous sequences. + length_vector: 'm' length vector of sequence lengths. Must be in + ascending order. RB sequences of increasing length grow on top of + the previous sequences. rb_pattern: A list of the form [[i,j],[k],...] which will make - simultaneous RB sequences where + simultaneous RB sequences where Qi,Qj are a 2Q RB sequence and Qk is a 1Q sequence, etc. E.g. [[0,3],[2],[1]] would create RB sequences that are 2Q for Q0/Q3, 1Q for Q1+Q2 @@ -155,97 +154,252 @@ def randomized_benchmarking_seq(nseeds=1, length_vector=None, length_multiplier: if this is an array it scales each rb_sequence by the multiplier seed_offset: What to start the seeds at (e.g. if we - want to add more seeds later) + want to add more seeds later) align_cliffs: If true adds a barrier across all qubits in rb_pattern - after each set of cliffords (note: aligns after each increment - of cliffords including the length multiplier so if the multiplier - is [1,3] it will barrier after 1 clifford for the first pattern - and 3 for the second) + after each set of elements, not necessarily Cliffords + (note: aligns after each increment of elements including the + length multiplier so if the multiplier is [1,3] it will barrier + after 1 element for the first pattern and 3 for the second). + interleaved_gates: A list of gates of elements that + will be interleaved (for interleaved randomized benchmarking) + The length of the list would equal the length of the rb_pattern. + is_purity: True only for purity rb (default is False) + group_gates: On which group (or gate set) we perform RB + (default is the Clifford group) + rand_seed: random number generator seed, to be used when getting random + gates Returns: - rb_circs: list of lists of circuits for the rb sequences (separate list - for each seed) - xdata: the Clifford lengths (with multiplier if applicable) - rb_opts_dict: option dictionary back out with default options appended + circuits: list of lists of circuits for the rb sequences + (separate list for each seed) + xdata: the sequences lengths (with multiplier if applicable) + circuits_interleaved (only if interleaved_gates is not None): + list of lists of circuits for the interleaved rb sequences + (separate list for each seed) + circuits_purity (only if is_purity=True): + list of lists of lists of circuits for purity rb + (separate list for each seed and each of the 3^n circuits) + npurity (only if is_purity=True): + the number of purity rb circuits (per seed) + which equals to 3^n, where n is the dimension """ + # Set modules (default is Clifford) + if group_gates is None or 'Clifford' or 'clifford': + Gutils = clutils() + Ggroup = Clifford + else: + raise ValueError("Unknown group or set of gates.") + + rand_seed = int.from_bytes(os.urandom(4), byteorder=sys.byteorder) \ + if rand_seed is None else rand_seed + + logger.log_to_file(nseeds=nseeds, length_vector=length_vector, + rb_pattern=rb_pattern, + length_multiplier=length_multiplier, + seed_offset=seed_offset, align_cliffs=align_cliffs, + is_purity=is_purity, group_gates=group_gates, + rand_seed=rand_seed) + if rb_pattern is None: rb_pattern = [[0]] if length_vector is None: length_vector = [1, 10, 20] - qlist_flat, n_q_max = check_pattern(rb_pattern) + qlist_flat, n_q_max, max_dim = check_pattern(rb_pattern, is_purity) length_multiplier = handle_length_multiplier(length_multiplier, - len(rb_pattern)) + len(rb_pattern), + is_purity) + # number of purity rb circuits per seed + npurity = 3**max_dim xdata = calc_xdata(length_vector, length_multiplier) pattern_sizes = [len(pat) for pat in rb_pattern] - clifford_tables = load_tables(np.max(pattern_sizes)) + max_nrb = np.max(pattern_sizes) + + # load group tables + group_tables = [[] for _ in range(max_nrb)] + for rb_num in range(max_nrb): + group_tables[rb_num] = Gutils.load_tables(rb_num+1) + # initialization: rb sequences circuits = [[] for e in range(nseeds)] + # initialization: interleaved rb sequences + circuits_interleaved = [[] for e in range(nseeds)] + # initialization: purity rb sequences + circuits_purity = [[[] for d in range(npurity)] + for e in range(nseeds)] + # go through for each seed for seed in range(nseeds): qr = qiskit.QuantumRegister(n_q_max+1, 'qr') - cr = qiskit.ClassicalRegister(n_q_max+1, 'cr') + cr = qiskit.ClassicalRegister(len(qlist_flat), 'cr') general_circ = qiskit.QuantumCircuit(qr, cr) + interleaved_circ = qiskit.QuantumCircuit(qr, cr) - # make Clifford sequences for each of the separate sequences in + # make sequences for each of the separate sequences in # rb_pattern - Cliffs = [] - + Elmnts = [] for rb_q_num in pattern_sizes: - Cliffs.append(Clifford(rb_q_num)) + Elmnts.append(Ggroup(rb_q_num)) + # Sequences for interleaved rb sequences + Elmnts_interleaved = [] + for rb_q_num in pattern_sizes: + Elmnts_interleaved.append(Ggroup(rb_q_num)) - # go through and add Cliffords + # go through and add elements to RB sequences length_index = 0 - for cliff_index in range(length_vector[-1]): + for elmnts_index in range(length_vector[-1]): for (rb_pattern_index, rb_q_num) in enumerate(pattern_sizes): - for _ in range(length_multiplier[rb_pattern_index]): - new_cliff_gatelist = clutils.random_clifford_gates( - rb_q_num) - Cliffs[rb_pattern_index] = clutils.compose_gates( - Cliffs[rb_pattern_index], new_cliff_gatelist) + new_elmnt_gatelist = Gutils.random_gates( + rb_q_num, rand_seed) + Elmnts[rb_pattern_index] = Gutils.compose_gates( + Elmnts[rb_pattern_index], new_elmnt_gatelist) general_circ += replace_q_indices( - clutils.get_quantum_circuit(new_cliff_gatelist, - rb_q_num), + get_quantum_circuit(new_elmnt_gatelist, + rb_q_num), rb_pattern[rb_pattern_index], qr) + # add a barrier general_circ.barrier( *[qr[x] for x in rb_pattern[rb_pattern_index]]) + # interleaved rb sequences + if interleaved_gates is not None: + Elmnts_interleaved[rb_pattern_index] = \ + Gutils.compose_gates( + Elmnts_interleaved[rb_pattern_index], + new_elmnt_gatelist) + Elmnts_interleaved[rb_pattern_index] = \ + Gutils.compose_gates( + Elmnts_interleaved[rb_pattern_index], + interleaved_gates[rb_pattern_index]) + interleaved_circ += replace_q_indices( + get_quantum_circuit(new_elmnt_gatelist, + rb_q_num), + rb_pattern[rb_pattern_index], qr) + # add a barrier - interleaved rb + interleaved_circ.barrier( + *[qr[x] for x in rb_pattern[rb_pattern_index]]) + interleaved_circ += replace_q_indices( + get_quantum_circuit(interleaved_gates + [rb_pattern_index], + rb_q_num), + rb_pattern[rb_pattern_index], qr) + # add a barrier - interleaved rb + interleaved_circ.barrier( + *[qr[x] for x in rb_pattern[rb_pattern_index]]) + if align_cliffs: - # if align cliffords at a barrier across all patterns + # if align at a barrier across all patterns general_circ.barrier( *[qr[x] for x in qlist_flat]) + # align for interleaved rb + if interleaved_gates is not None: + interleaved_circ.barrier( + *[qr[x] for x in qlist_flat]) - # if the number of cliffords matches one of the sequence lengths + # if the number of elements matches one of the sequence lengths # then calculate the inverse and produce the circuit - if (cliff_index+1) == length_vector[length_index]: - + if (elmnts_index+1) == length_vector[length_index]: + # circ for rb: circ = qiskit.QuantumCircuit(qr, cr) circ += general_circ + # circ_interleaved for interleaved rb: + circ_interleaved = qiskit.QuantumCircuit(qr, cr) + circ_interleaved += interleaved_circ for (rb_pattern_index, rb_q_num) in enumerate(pattern_sizes): - inv_key = Cliffs[rb_pattern_index].index() - inv_circuit = clutils.find_inverse_clifford_gates( + inv_key = Gutils.find_key(Elmnts[rb_pattern_index]) + inv_circuit = Gutils.find_inverse_gates( rb_q_num, - clifford_tables[rb_q_num-1][inv_key]) + group_tables[rb_q_num-1][inv_key]) circ += replace_q_indices( - clutils.get_quantum_circuit(inv_circuit, rb_q_num), + get_quantum_circuit(inv_circuit, rb_q_num), rb_pattern[rb_pattern_index], qr) - - # add measurement - # q->c is 1 to 1 - circ.measure(qr, cr) + # calculate the inverse and produce the circuit + # for interleaved rb + if interleaved_gates is not None: + inv_key = Gutils.find_key(Elmnts_interleaved + [rb_pattern_index]) + inv_circuit = Gutils.find_inverse_gates( + rb_q_num, + group_tables[rb_q_num - 1][inv_key]) + circ_interleaved += replace_q_indices( + get_quantum_circuit(inv_circuit, rb_q_num), + rb_pattern[rb_pattern_index], qr) + + # Circuits for purity rb + if is_purity: + circ_purity = [[] for d in range(npurity)] + for d in range(npurity): + circ_purity[d] = qiskit.QuantumCircuit(qr, cr) + circ_purity[d] += circ + circ_purity[d].name = 'rb_purity_' + ind_d = d + purity_qubit_num = 0 + while True: + # Per each qubit: + # do nothing or rx(pi/2) or ry(pi/2) + purity_qubit_rot = np.mod(ind_d, 3) + ind_d = np.floor_divide(ind_d, 3) + if purity_qubit_rot == 0: # do nothing + circ_purity[d].name += 'Z' + if purity_qubit_rot == 1: # add rx(pi/2) + for pat in rb_pattern: + circ_purity[d].rx(np.pi / 2, + qr[pat[ + purity_qubit_num]]) + circ_purity[d].name += 'X' + if purity_qubit_rot == 2: # add ry(pi/2) + for pat in rb_pattern: + circ_purity[d].ry(np.pi / 2, + qr[pat[ + purity_qubit_num]]) + circ_purity[d].name += 'Y' + purity_qubit_num = purity_qubit_num + 1 + if ind_d == 0: + break + # padding the circuit name with Z's so that + # all circuits will have names of the same length + for _ in range(max_dim - purity_qubit_num): + circ_purity[d].name += 'Z' + # add measurement for purity rb + for qind, qb in enumerate(qlist_flat): + circ_purity[d].measure(qr[qb], cr[qind]) + circ_purity[d].name += '_length_%d_seed_%d' \ + % (length_index, + seed + seed_offset) + + # add measurement for standard rb + # qubits measure to the c registers as + # they appear in the pattern + for qind, qb in enumerate(qlist_flat): + circ.measure(qr[qb], cr[qind]) + # add measurement for interleaved rb + circ_interleaved.measure(qr[qb], cr[qind]) circ.name = 'rb_length_%d_seed_%d' % (length_index, seed + seed_offset) + circ_interleaved.name = 'rb_interleaved_length_%d_seed_%d' \ + % (length_index, seed + seed_offset) circuits[seed].append(circ) + circuits_interleaved[seed].append(circ_interleaved) + if is_purity: + for d in range(npurity): + circuits_purity[seed][d].append(circ_purity[d]) length_index += 1 + # output of interleaved rb + if interleaved_gates is not None: + return circuits, xdata, circuits_interleaved + # output of purity rb + if is_purity: + return circuits_purity, xdata, npurity + # output of standard (simultaneous) rb return circuits, xdata @@ -263,11 +417,43 @@ def replace_q_indices(circuit, q_nums, qr): """ new_circuit = qiskit.QuantumCircuit(qr) - for op in circuit.data: - original_qubits = op.qargs - new_op = copy.deepcopy(op) - new_op.qargs = [ - (qr, q_nums[x]) for x in [arg[1] for arg in original_qubits]] + for instr, qargs, cargs in circuit.data: + new_qargs = [ + qr[q_nums[x]] for x in [arg.index for arg in qargs]] + new_op = copy.deepcopy((instr, new_qargs, cargs)) new_circuit.data.append(new_op) return new_circuit + + +def get_quantum_circuit(gatelist, num_qubits): + """ + Returns the circuit in the form of a QuantumCircuit object. + + Args: + num_qubits: the number of qubits (dimension). + gatelist: a list of gates. + + Returns: + A QuantumCircuit object. + """ + qr = qiskit.QuantumRegister(num_qubits) + qc = qiskit.QuantumCircuit(qr) + + for op in gatelist: + split = op.split() + op_names = [split[0]] + + # temporary correcting the ops name since QuantumCircuit has no + # attributes 'v' or 'w' yet: + if op_names == ['v']: + op_names = ['sdg', 'h'] + elif op_names == ['w']: + op_names = ['h', 's'] + + qubits = [qr[int(x)] for x in split[1:]] + for sub_op in op_names: + operation = eval('qiskit.QuantumCircuit.' + sub_op) + operation(qc, *qubits) + + return qc diff --git a/qiskit/ignis/verification/randomized_benchmarking/clifford_utils.py b/qiskit/ignis/verification/randomized_benchmarking/clifford_utils.py index 332a5fa20..a234fa4e7 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/clifford_utils.py +++ b/qiskit/ignis/verification/randomized_benchmarking/clifford_utils.py @@ -1,18 +1,24 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Advanced Clifford operations needed for randomized benchmarking """ import numpy as np -import qiskit - -from . import Clifford +from .Clifford import Clifford +from .basic_utils import BasicUtils try: import cPickle as pickle @@ -20,374 +26,431 @@ import pickle -# ---------------------------------------------------------------------------------------- -# Functions that convert to/from a Clifford object -# ---------------------------------------------------------------------------------------- -def compose_gates(cliff, gatelist): - """ - Add gates to a Clifford object from a list of gates. - - Args: - cliff: A Clifford class object. - gatelist: a list of gates. - - Returns: - A Clifford class object. - """ - - for op in gatelist: - split = op.split() - q1 = int(split[1]) - if split[0] == 'v': - cliff.v(q1) - elif split[0] == 'w': - cliff.w(q1) - elif split[0] == 'x': - cliff.x(q1) - elif split[0] == 'y': - cliff.y(q1) - elif split[0] == 'z': - cliff.z(q1) - elif split[0] == 'cx': - cliff.cx(q1, int(split[2])) - elif split[0] == 'h': - cliff.h(q1) - elif split[0] == 's': - cliff.s(q1) - elif split[0] == 'sdg': - cliff.sdg(q1) - else: - raise ValueError("Unknown gate type: ", op) - return cliff - - -def clifford_from_gates(num_qubits, gatelist): - """ - Generates a Clifford object from a list of gates. - - Args: - num_qubits: the number of qubits for the Clifford. - gatelist: a list of gates. - - Returns: - A num-qubit Clifford class object. - """ - cliff = Clifford(num_qubits) - new_cliff = compose_gates(cliff, gatelist) - return new_cliff - - -# -------------------------------------------------------- -# Add gates to Cliffords -# -------------------------------------------------------- - -def pauli_gates(gatelist, q, pauli): - """adds a pauli gate on qubit q""" - if pauli == 2: - gatelist.append('x ' + str(q)) - elif pauli == 3: - gatelist.append('y ' + str(q)) - elif pauli == 1: - gatelist.append('z ' + str(q)) - - -def h_gates(gatelist, q, h): - """adds a hadamard gate or not on qubit q""" - if h == 1: - gatelist.append('h ' + str(q)) - - -def v_gates(gatelist, q, v): - """adds an axis-swap-gates on qubit q""" - # rotation is V=HSHS = [[0,1],[1,1]] tableau - # takes Z->X->Y->Z - # V is of order 3, and two V-gates is W-gate, so: W=VV and WV=I - if v == 1: - gatelist.append('v ' + str(q)) - elif v == 2: - gatelist.append('w ' + str(q)) - - -def cx_gates(gatelist, ctrl, tgt): - """adds a controlled=x gates""" - gatelist.append('cx ' + str(ctrl) + ' ' + str(tgt)) - - -# -------------------------------------------------------- -# Create a 1 or 2 Qubit Clifford based on a unique index -# -------------------------------------------------------- - -def clifford1_gates(idx: int): - """ - Make a single qubit Clifford gate. - - Args: - idx: the index (mod 24) of a single qubit Clifford. - - Returns: - A single qubit Clifford gate. - """ - - gatelist = [] - # Cannonical Ordering of Cliffords 0,...,23 - cannonicalorder = idx % 24 - pauli = np.mod(cannonicalorder, 4) - rotation = np.mod(cannonicalorder // 4, 3) - h_or_not = np.mod(cannonicalorder // 12, 2) - - h_gates(gatelist, 0, h_or_not) - - v_gates(gatelist, 0, rotation) # do the R-gates - - pauli_gates(gatelist, 0, pauli) - - return gatelist - - -def clifford2_gates(idx: int): - """ - Make a 2-qubit Clifford gate. - - Args: - idx: the index (mod 11520) of a two-qubit Clifford. - - Returns: - A 2-qubit Clifford gate. - """ - - gatelist = [] - cannon = idx % 11520 - - pauli = np.mod(cannon, 16) - symp = cannon // 16 - - if symp < 36: # 1-qubit Cliffords Class - r0 = np.mod(symp, 3) - r1 = np.mod(symp // 3, 3) - h0 = np.mod(symp // 9, 2) - h1 = np.mod(symp // 18, 2) - - h_gates(gatelist, 0, h0) - h_gates(gatelist, 1, h1) - v_gates(gatelist, 0, r0) - v_gates(gatelist, 1, r1) - - elif symp < 360: # CNOT-like Class - symp = symp - 36 - r0 = np.mod(symp, 3) - r1 = np.mod(symp // 3, 3) - r2 = np.mod(symp // 9, 3) - r3 = np.mod(symp // 27, 3) - h0 = np.mod(symp // 81, 2) - h1 = np.mod(symp // 162, 2) - - h_gates(gatelist, 0, h0) - h_gates(gatelist, 1, h1) - v_gates(gatelist, 0, r0) - v_gates(gatelist, 1, r1) - cx_gates(gatelist, 0, 1) - v_gates(gatelist, 0, r2) - v_gates(gatelist, 1, r3) - - elif symp < 684: # iSWAP-like Class - symp = symp - 360 - r0 = np.mod(symp, 3) - r1 = np.mod(symp // 3, 3) - r2 = np.mod(symp // 9, 3) - r3 = np.mod(symp // 27, 3) - h0 = np.mod(symp // 81, 2) - h1 = np.mod(symp // 162, 2) - - h_gates(gatelist, 0, h0) - h_gates(gatelist, 1, h1) - v_gates(gatelist, 0, r0) - v_gates(gatelist, 1, r1) - cx_gates(gatelist, 0, 1) - cx_gates(gatelist, 1, 0) - v_gates(gatelist, 0, r2) - v_gates(gatelist, 1, r3) - - else: # SWAP Class - symp = symp - 684 - r0 = np.mod(symp, 3) - r1 = np.mod(symp // 3, 3) - h0 = np.mod(symp // 9, 2) - h1 = np.mod(symp // 18, 2) - - h_gates(gatelist, 0, h0) - h_gates(gatelist, 1, h1) - - v_gates(gatelist, 0, r0) - v_gates(gatelist, 1, r1) - - cx_gates(gatelist, 0, 1) - cx_gates(gatelist, 1, 0) - cx_gates(gatelist, 0, 1) - - pauli_gates(gatelist, 0, np.mod(pauli, 4)) - pauli_gates(gatelist, 1, pauli // 4) - - return gatelist - - -# -------------------------------------------------------- -# Create a 1 or 2 Qubit Clifford tables -# -------------------------------------------------------- -def clifford2_gates_table(): - """ - Generate a table of all 2-qubit Clifford gates. - - Args: - None. - - Returns: - A table of all 2-qubit Clifford gates. - """ - cliffords2 = {} - for i in range(11520): - circ = clifford2_gates(i) - key = clifford_from_gates(2, circ).index() - cliffords2[key] = circ - return cliffords2 - - -def clifford1_gates_table(): - """ - Generate a table of all 1-qubit Clifford gates. - - Args: - None. - - Returns: - A table of all 1-qubit Clifford gates. - """ - cliffords1 = {} - for i in range(24): - circ = clifford1_gates(i) - key = clifford_from_gates(1, circ).index() - cliffords1[key] = circ - return cliffords1 - - -def pickle_clifford_table(picklefile='cliffords2.pickle', num_qubits=2): - """ - Create pickled versions of the 1 and 2 qubit Clifford tables. - - Args: - picklefile - pickle file name. - num_qubits - number of qubits. - - Returns: - A pickle file with the 1 and 2 qubit Clifford tables. - """ - cliffords = {} - if num_qubits == 1: - cliffords = clifford1_gates_table() - elif num_qubits == 2: - cliffords = clifford2_gates_table() - else: - raise ValueError( - "number of qubits bigger than is not supported for pickle") - - with open(picklefile, "wb") as pf: - pickle.dump(cliffords, pf) - - -def load_clifford_table(picklefile='cliffords2.pickle'): - """ - Load pickled files of the tables of 1 and 2 qubit Clifford tables. - - Args: - picklefile - pickle file name. - - Returns: - A table of 1 and 2 qubit Clifford gates. - """ - with open(picklefile, "rb") as pf: - return pickle.load(pf) - - -# -------------------------------------------------------- -# Main function that generates a random clifford gate -# -------------------------------------------------------- -def random_clifford_gates(num_qubits): +class CliffordUtils(BasicUtils): """ - Pick a random Clifford gate. - - Args: - num_qubits: dimension of the Clifford. - - Returns: - A 1 or 2 qubit Clifford gate. + Class for util functions for the Clifford group """ - if num_qubits == 1: - return clifford1_gates(np.random.randint(0, 24)) - if num_qubits == 2: - return clifford2_gates(np.random.randint(0, 11520)) - raise ValueError("The number of qubits should be only 1 or 2") - - -# -------------------------------------------------------- -# Main function that calculates an inverse of a clifford gate -# -------------------------------------------------------- -def find_inverse_clifford_gates(num_qubits, gatelist): - """ - Find the inverse of a Clifford gate. - - Args: - num_qubits: the dimension of the Clifford. - gatelist: a Clifford gate. - - Returns: - An inverse Clifford gate. - """ - - if num_qubits in (1, 2): - inv_gatelist = gatelist.copy() - inv_gatelist.reverse() - # replace v by w and w by v - for i, _ in enumerate(inv_gatelist): - split = inv_gatelist[i].split() + def __init__(self, num_qubits=2, group_tables=None, elmnt=None, + gatelist=None, elmnt_key=None): + """ + Args: + num_qubits: number of qubits. + group_table: table of Clifford objects. + elmnt: a group element. + elmnt_key: a unique index of a Clifford object. + gatelist: a list of gates corresponding to a + Cliffor object + """ + + self._num_qubits = num_qubits + self._group_tables = group_tables + self._elmnt = elmnt + self._elmnt_key = elmnt_key + self._gatelist = gatelist + + def num_qubits(self): + """Return the number of qubits.""" + return self._num_qubits + + def group_tables(self): + """Return the Clifford group tables.""" + return self._group_tables + + def elmnt(self): + """Return a Clifford object.""" + return self._elmnt + + def elmnt_key(self): + """Return a unique index of a Clifford object.""" + return self._elmnt_key + + def gatelist(self): + """Return a list of gates corresponding to + a Clifford object.""" + return self._gatelist + + # ---------------------------------------------------------------------------------------- + # Functions that convert to/from a Clifford object + # ---------------------------------------------------------------------------------------- + def compose_gates(self, cliff, gatelist): + """ + Add gates to a Clifford object from a list of gates. + + Args: + cliff: A Clifford class object. + gatelist: a list of gates. + + Returns: + A Clifford class object. + """ + + for op in gatelist: + split = op.split() + q1 = int(split[1]) if split[0] == 'v': - inv_gatelist[i] = 'w ' + split[1] + cliff.v(q1) elif split[0] == 'w': - inv_gatelist[i] = 'v ' + split[1] - return inv_gatelist - raise ValueError("The number of qubits should be only 1 or 2") - - -# -------------------------------------------------------- -# Returns the Clifford circuit in the form of a QuantumCircuit object -# -------------------------------------------------------- -def get_quantum_circuit(gatelist, num_qubits): - """ - Returns the Clifford circuit in the form of a QuantumCircuit object. - - Args: - num_qubits: the dimension of the Clifford. - gatelist: a Clifford gate. - - Returns: - A QuantumCircuit object. - """ - qr = qiskit.QuantumRegister(num_qubits) - qc = qiskit.QuantumCircuit(qr) - - for op in gatelist: - split = op.split() - op_names = [split[0]] - - # temporary correcting the ops name since QuantumCircuit has no - # attributes 'v' or 'w' yet: - if op_names == ['v']: - op_names = ['sdg', 'h'] - elif op_names == ['w']: - op_names = ['h', 's'] - - qubits = [qr[int(x)] for x in split[1:]] - for sub_op in op_names: - operation = eval('qiskit.QuantumCircuit.' + sub_op) - operation(qc, *qubits) - - return qc + cliff.w(q1) + elif split[0] == 'x': + cliff.x(q1) + elif split[0] == 'y': + cliff.y(q1) + elif split[0] == 'z': + cliff.z(q1) + elif split[0] == 'cx': + cliff.cx(q1, int(split[2])) + elif split[0] == 'h': + cliff.h(q1) + elif split[0] == 's': + cliff.s(q1) + elif split[0] == 'sdg': + cliff.sdg(q1) + else: + raise ValueError("Unknown gate type: ", op) + + self._elmnt = cliff + return cliff + + def clifford_from_gates(self, num_qubits, gatelist): + """ + Generates a Clifford object from a list of gates. + + Args: + num_qubits: the number of qubits for the Clifford. + gatelist: a list of gates. + + Returns: + A num-qubit Clifford class object. + """ + cliff = Clifford(num_qubits) + new_cliff = self.compose_gates(cliff, gatelist) + return new_cliff + + # -------------------------------------------------------- + # Add gates to Cliffords + # -------------------------------------------------------- + + def pauli_gates(self, gatelist, q, pauli): + """adds a pauli gate on qubit q""" + if pauli == 2: + gatelist.append('x ' + str(q)) + elif pauli == 3: + gatelist.append('y ' + str(q)) + elif pauli == 1: + gatelist.append('z ' + str(q)) + + def h_gates(self, gatelist, q, h): + """adds a hadamard gate or not on qubit q""" + if h == 1: + gatelist.append('h ' + str(q)) + + def v_gates(self, gatelist, q, v): + """adds an axis-swap-gates on qubit q""" + # rotation is V=HSHS = [[0,1],[1,1]] tableau + # takes Z->X->Y->Z + # V is of order 3, and two V-gates is W-gate, so: W=VV and WV=I + if v == 1: + gatelist.append('v ' + str(q)) + elif v == 2: + gatelist.append('w ' + str(q)) + + def cx_gates(self, gatelist, ctrl, tgt): + """adds a controlled=x gates""" + gatelist.append('cx ' + str(ctrl) + ' ' + str(tgt)) + + # -------------------------------------------------------- + # Create a 1 or 2 Qubit Clifford based on a unique index + # -------------------------------------------------------- + + def clifford1_gates(self, idx: int): + """ + Make a single qubit Clifford gate. + + Args: + idx: the index (mod 24) of a single qubit Clifford. + + Returns: + A single qubit Clifford gate. + """ + + gatelist = [] + # Cannonical Ordering of Cliffords 0,...,23 + cannonicalorder = idx % 24 + pauli = np.mod(cannonicalorder, 4) + rotation = np.mod(cannonicalorder // 4, 3) + h_or_not = np.mod(cannonicalorder // 12, 2) + + self.h_gates(gatelist, 0, h_or_not) + + self.v_gates(gatelist, 0, rotation) # do the R-gates + + self.pauli_gates(gatelist, 0, pauli) + + return gatelist + + def clifford2_gates(self, idx: int): + """ + Make a 2-qubit Clifford gate. + + Args: + idx: the index (mod 11520) of a two-qubit Clifford. + + Returns: + A 2-qubit Clifford gate. + """ + + gatelist = [] + cannon = idx % 11520 + + pauli = np.mod(cannon, 16) + symp = cannon // 16 + + if symp < 36: # 1-qubit Cliffords Class + r0 = np.mod(symp, 3) + r1 = np.mod(symp // 3, 3) + h0 = np.mod(symp // 9, 2) + h1 = np.mod(symp // 18, 2) + + self.h_gates(gatelist, 0, h0) + self.h_gates(gatelist, 1, h1) + self.v_gates(gatelist, 0, r0) + self.v_gates(gatelist, 1, r1) + + elif symp < 360: # CNOT-like Class + symp = symp - 36 + r0 = np.mod(symp, 3) + r1 = np.mod(symp // 3, 3) + r2 = np.mod(symp // 9, 3) + r3 = np.mod(symp // 27, 3) + h0 = np.mod(symp // 81, 2) + h1 = np.mod(symp // 162, 2) + + self.h_gates(gatelist, 0, h0) + self.h_gates(gatelist, 1, h1) + self.v_gates(gatelist, 0, r0) + self.v_gates(gatelist, 1, r1) + self.cx_gates(gatelist, 0, 1) + self.v_gates(gatelist, 0, r2) + self.v_gates(gatelist, 1, r3) + + elif symp < 684: # iSWAP-like Class + symp = symp - 360 + r0 = np.mod(symp, 3) + r1 = np.mod(symp // 3, 3) + r2 = np.mod(symp // 9, 3) + r3 = np.mod(symp // 27, 3) + h0 = np.mod(symp // 81, 2) + h1 = np.mod(symp // 162, 2) + + self.h_gates(gatelist, 0, h0) + self.h_gates(gatelist, 1, h1) + self.v_gates(gatelist, 0, r0) + self.v_gates(gatelist, 1, r1) + self.cx_gates(gatelist, 0, 1) + self.cx_gates(gatelist, 1, 0) + self.v_gates(gatelist, 0, r2) + self.v_gates(gatelist, 1, r3) + + else: # SWAP Class + symp = symp - 684 + r0 = np.mod(symp, 3) + r1 = np.mod(symp // 3, 3) + h0 = np.mod(symp // 9, 2) + h1 = np.mod(symp // 18, 2) + + self.h_gates(gatelist, 0, h0) + self.h_gates(gatelist, 1, h1) + + self.v_gates(gatelist, 0, r0) + self.v_gates(gatelist, 1, r1) + + self.cx_gates(gatelist, 0, 1) + self.cx_gates(gatelist, 1, 0) + self.cx_gates(gatelist, 0, 1) + + self.pauli_gates(gatelist, 0, np.mod(pauli, 4)) + self.pauli_gates(gatelist, 1, pauli // 4) + + return gatelist + + # -------------------------------------------------------- + # Create a 1 or 2 Qubit Clifford tables + # -------------------------------------------------------- + def clifford2_gates_table(self): + """ + Generate a table of all 2-qubit Clifford gates. + + Args: + None. + + Returns: + A table of all 2-qubit Clifford gates. + """ + cliffords2 = {} + for i in range(11520): + circ = self.clifford2_gates(i) + key = self.clifford_from_gates(2, circ).index() + cliffords2[key] = circ + return cliffords2 + + def clifford1_gates_table(self): + """ + Generate a table of all 1-qubit Clifford gates. + + Args: + None. + + Returns: + A table of all 1-qubit Clifford gates. + """ + cliffords1 = {} + for i in range(24): + circ = self.clifford1_gates(i) + key = self.clifford_from_gates(1, circ).index() + cliffords1[key] = circ + return cliffords1 + + def pickle_clifford_table(self, picklefile='cliffords2.pickle', + num_qubits=2): + """ + Create pickled versions of the 1 and 2 qubit Clifford tables. + + Args: + picklefile - pickle file name. + num_qubits - number of qubits. + + Returns: + A pickle file with the 1 and 2 qubit Clifford tables. + """ + cliffords = {} + if num_qubits == 1: + cliffords = self.clifford1_gates_table() + elif num_qubits == 2: + cliffords = self.clifford2_gates_table() + else: + raise ValueError( + "number of qubits bigger than is not supported for pickle") + + with open(picklefile, "wb") as pf: + pickle.dump(cliffords, pf) + + def load_clifford_table(self, picklefile='cliffords2.pickle'): + """ + Load pickled files of the tables of 1 and 2 qubit Clifford tables. + + Args: + picklefile - pickle file name. + + Returns: + A table of 1 and 2 qubit Clifford gates. + """ + with open(picklefile, "rb") as pf: + return pickle.load(pf) + + def load_tables(self, num_qubits): + """ + Returns the needed Clifford tables + + Args: + num_qubits: number of qubits for the required table + + Returns: + A table of Clifford objects + """ + + # load the clifford tables, but only if we're using that particular + # num_qubits + if num_qubits == 1: + # 1Q Cliffords, load table programmatically + clifford_tables = self.clifford1_gates_table() + + elif num_qubits == 2: + # 2Q Cliffords + # Try to load the table in from file. If it doesn't exist then make + # the file + try: + clifford_tables = self.load_clifford_table( + picklefile='cliffords%d.pickle' % num_qubits) + except OSError: + # table doesn't exist, so save it + # this will save time next run + print('Making the n=%d Clifford Table' % num_qubits) + self.pickle_clifford_table( + picklefile='cliffords%d.pickle' % num_qubits, + num_qubits=num_qubits) + clifford_tables = self.load_clifford_table( + picklefile='cliffords%d.pickle' % num_qubits) + else: + raise ValueError("The number of qubits should be only 1 or 2") + + return clifford_tables + + # -------------------------------------------------------- + # Main function that generates a random clifford gate + # -------------------------------------------------------- + def random_gates(self, num_qubits, rand_seed=None): + """ + Pick a random Clifford gate. + + Args: + num_qubits: dimension of the Clifford. + rand_seed: seed for the random number generator + + Returns: + A 1 or 2 qubit Clifford gate. + """ + if rand_seed is not None: + if not isinstance(rand_seed, int): + raise TypeError("Random seed number should be an integer") + np.random.seed(rand_seed) + + if num_qubits == 1: + cliff_gatelist = self.clifford1_gates(np.random.randint(0, 24)) + elif num_qubits == 2: + cliff_gatelist = self.clifford2_gates(np.random.randint(0, 11520)) + else: + raise ValueError("The number of qubits should be only 1 or 2") + + self._gatelist = cliff_gatelist + return cliff_gatelist + + # -------------------------------------------------------- + # Main function that calculates an inverse of a clifford gate + # -------------------------------------------------------- + def find_inverse_gates(self, num_qubits, gatelist): + """ + Find the inverse of a Clifford gate. + + Args: + num_qubits: the dimension of the Clifford. + gatelist: a Clifford gate. + + Returns: + An inverse Clifford gate. + """ + + if num_qubits in (1, 2): + inv_gatelist = gatelist.copy() + inv_gatelist.reverse() + # replace v by w and w by v + for i, _ in enumerate(inv_gatelist): + split = inv_gatelist[i].split() + if split[0] == 'v': + inv_gatelist[i] = 'w ' + split[1] + elif split[0] == 'w': + inv_gatelist[i] = 'v ' + split[1] + return inv_gatelist + raise ValueError("The number of qubits should be only 1 or 2") + + def find_key(self, cliff): + """ + Find the Clifford index. + + Args: + cliff: a Clifford object. + + Returns: + Clifford index (an integer). + """ + return cliff.index() diff --git a/qiskit/ignis/verification/randomized_benchmarking/fitters.py b/qiskit/ignis/verification/randomized_benchmarking/fitters.py index c3d5d5869..acf7971f7 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/fitters.py +++ b/qiskit/ignis/verification/randomized_benchmarking/fitters.py @@ -1,19 +1,31 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Functions used for the analysis of randomized benchmarking results. """ +from abc import ABC, abstractmethod from scipy.optimize import curve_fit import numpy as np from qiskit import QiskitError +from qiskit.quantum_info.analysis.average import average_data from ..tomography import marginal_counts from ...characterization.fitters import build_counts_dict_from_list +from ...logging.ignis_logging import * + +logger = IgnisLogging().get_logger(__name__) try: from matplotlib import pyplot as plt @@ -22,7 +34,99 @@ HAS_MATPLOTLIB = False -class RBFitter: +class RBFitterBase(ABC): + """ + Abstract base class (ABS) for fitters for randomized benchmarking + """ + + @abstractmethod + def raw_data(self): + """Return raw data.""" + return + + @abstractmethod + def cliff_lengths(self): + """Return clifford lengths.""" + return + + @abstractmethod + def ydata(self): + """Return ydata (means and std devs).""" + return + + @abstractmethod + def fit(self): + """Return fit.""" + return + + @abstractmethod + def rb_fit_fun(self): + """Return the function rb_fit_fun.""" + return + + @abstractmethod + def seeds(self): + """Return the number of loaded seeds.""" + return + + @abstractmethod + def results(self): + """Return all the results.""" + return + + @abstractmethod + def add_data(self): + """ + Add a new result. Re calculate the raw data, means and + fit. + """ + + return + + @abstractmethod + def calc_data(self): + """ + Retrieve probabilities of success from execution results. + """ + + return + + @abstractmethod + def calc_statistics(self): + """ + Extract averages and std dev from the raw data + """ + + return + + @abstractmethod + def fit_data_pattern(self): + """ + Fit the RB results of a particular pattern + to an exponential curve. + """ + + return + + @abstractmethod + def fit_data(self): + """ + Fit the RB results to an exponential curve. + """ + + return + + @abstractmethod + def plot_rb_data(self): + """ + Plot randomized benchmarking data of a single pattern. + + """ + + return + + +class RBFitter(RBFitterBase): """ Class for fitters for randomized benchmarking """ @@ -43,8 +147,9 @@ def __init__(self, backend_result, cliff_lengths, self._rb_pattern = rb_pattern self._raw_data = [] self._ydata = [] - self._fit = [] - self._nseeds = 0 + self._fit = [{} for e in rb_pattern] + self._nseeds = [] + self._circ_name_type = '' self._result_list = [] self.add_data(backend_result) @@ -54,21 +159,47 @@ def raw_data(self): """Return raw data.""" return self._raw_data + @raw_data.setter + def raw_data(self, raw_data): + if raw_data is None: + self._raw_data = [] + else: + self._raw_data = raw_data + @property def cliff_lengths(self): """Return clifford lengths.""" - return self.cliff_lengths + return self._cliff_lengths @property def ydata(self): """Return ydata (means and std devs).""" return self._ydata + @ydata.setter + def ydata(self, ydata): + if ydata is None: + self._ydata = [] + else: + self._ydata = ydata + @property def fit(self): """Return fit.""" return self._fit + @fit.setter + def fit(self, fit): + if fit is None: + self._fit = [] + else: + self._fit = fit + + @property + def rb_fit_fun(self): + """Return the function rb_fit_fun.""" + return self._rb_fit_fun + @property def seeds(self): """Return the number of loaded seeds.""" @@ -107,14 +238,8 @@ def add_data(self, new_backend_result, rerun_fit=True): # cliffords for rbcirc in result.results: nseeds_circ = int(rbcirc.header.name.split('_')[-1]) - if (nseeds_circ+1) > self._nseeds: - self._nseeds = nseeds_circ+1 - - for result in self._result_list: - if not len(result.results) == len(self._cliff_lengths[0]): - raise ValueError( - "The number of clifford lengths must match the number of " - "results") + if nseeds_circ not in self._nseeds: + self._nseeds.append(nseeds_circ) if rerun_fit: self.calc_data() @@ -140,11 +265,17 @@ def calc_data(self): the output of circuits generated by randomized_becnhmarking_seq, """ + # The type of the circuit name, e.g. rb or rb_interleaved + # as it appears in the result (before _length_%d_seed_%d) + self._circ_name_type = self._result_list[0].results[0]. \ + header.name.split("_length")[0] + circ_counts = {} circ_shots = {} - for seedidx in range(self._nseeds): + for seed in self._nseeds: for circ, _ in enumerate(self._cliff_lengths[0]): - circ_name = 'rb_length_%d_seed_%d' % (circ, seedidx) + circ_name = self._circ_name_type + '_length_%d_seed_%d' \ + % (circ, seed) count_list = [] for result in self._result_list: try: @@ -158,6 +289,7 @@ def calc_data(self): circ_shots[circ_name] = sum(circ_counts[circ_name].values()) self._raw_data = [] + startind = 0 for patt_ind in range(len(self._rb_pattern)): @@ -165,18 +297,21 @@ def calc_data(self): string_of_0s = string_of_0s.zfill(len(self._rb_pattern[patt_ind])) self._raw_data.append([]) + endind = startind+len(self._rb_pattern[patt_ind]) - for i in range(self._nseeds): + for seedidx, seed in enumerate(self._nseeds): self._raw_data[-1].append([]) for k, _ in enumerate(self._cliff_lengths[patt_ind]): - circ_name = 'rb_length_%d_seed_%d' % (k, i) + circ_name = self._circ_name_type + '_length_%d_seed_%d' \ + % (k, seed) counts_subspace = marginal_counts( circ_counts[circ_name], - self._rb_pattern[patt_ind]) - self._raw_data[-1][i].append( + np.arange(startind, endind)) + self._raw_data[-1][seedidx].append( counts_subspace.get(string_of_0s, 0) / circ_shots[circ_name]) + startind = endind def calc_statistics(self): """ @@ -206,11 +341,56 @@ def calc_statistics(self): else: self._ydata[-1]['std'] = np.std(self._raw_data[patt_ind], 0) + def fit_data_pattern(self, patt_ind, fit_guess): + """ + Fit the RB results of a particular pattern + to an exponential curve. + + Args: + patt_ind: index of the data to fit + fit_guess: guess values for the fit + + Puts the results into a list of fit dictionaries: + where each dictionary corresponds to a pattern and has fields: + 'params' - three parameters of rb_fit_fun. The middle one is the + exponent. + 'err' - the error limits of the parameters. + 'epc' - error per Clifford + """ + + lens = self._cliff_lengths[patt_ind] + qubits = self._rb_pattern[patt_ind] + + # if at least one of the std values is zero, then sigma is replaced + # by None + if not self._ydata[patt_ind]['std'] is None: + sigma = self._ydata[patt_ind]['std'].copy() + if len(sigma) - np.count_nonzero(sigma) > 0: + sigma = None + else: + sigma = None + params, pcov = curve_fit(self._rb_fit_fun, lens, + self._ydata[patt_ind]['mean'], + sigma=sigma, + p0=fit_guess, + bounds=([0, 0, 0], [1, 1, 1])) + alpha = params[1] # exponent + params_err = np.sqrt(np.diag(pcov)) + alpha_err = params_err[1] + + nrb = 2 ** len(qubits) + epc = (nrb-1)/nrb*(1-alpha) + epc_err = (nrb-1)/nrb*alpha_err/alpha + + self._fit[patt_ind] = {'params': params, 'params_err': params_err, + 'epc': epc, 'epc_err': epc_err} + def fit_data(self): """ Fit the RB results to an exponential curve. - Fit each of the patterns + Fit each of the patterns. Use the data to construct guess values + for the fits Puts the results into a list of fit dictionaries: where each dictionary corresponds to a pattern and has fields: @@ -220,32 +400,33 @@ def fit_data(self): 'epc' - error per Clifford """ - self._fit = [] - for patt_ind, (lens, qubits) in enumerate(zip(self._cliff_lengths, - self._rb_pattern)): - # if at least one of the std values is zero, then sigma is replaced - # by None - if not self._ydata[patt_ind]['std'] is None: - sigma = self._ydata[patt_ind]['std'].copy() - if len(sigma) - np.count_nonzero(sigma) > 0: - sigma = None - else: - sigma = None - params, pcov = curve_fit(self._rb_fit_fun, lens, - self._ydata[patt_ind]['mean'], - sigma=sigma, - p0=(1.0, 0.95, 0.0), - bounds=([-2, 0, -2], [2, 1, 2])) - alpha = params[1] # exponent - params_err = np.sqrt(np.diag(pcov)) - alpha_err = params_err[1] + for patt_ind, _ in enumerate(self._rb_pattern): - nrb = 2 ** len(qubits) - epc = (nrb-1)/nrb*(1-alpha) - epc_err = epc*alpha_err/alpha + qubits = self._rb_pattern[patt_ind] + + # Should decay to 1/2^n + fit_guess = [0.95, 0.99, 1/2**len(qubits)] + + # Use the first two points to guess the decay param + y0 = self._ydata[patt_ind]['mean'][0] + y1 = self._ydata[patt_ind]['mean'][1] + dcliff = (self._cliff_lengths[patt_ind][1] - + self._cliff_lengths[patt_ind][0]) + dy = ((y1 - fit_guess[2]) / + (y0 - fit_guess[2])) + alpha_guess = dy**(1/dcliff) + if alpha_guess < 1.0: + fit_guess[1] = alpha_guess + + if y0 > fit_guess[2]: + fit_guess[0] = ((y0 - fit_guess[2]) / + fit_guess[1]**self._cliff_lengths[patt_ind][0]) - self._fit.append({'params': params, 'params_err': params_err, - 'epc': epc, 'epc_err': epc_err}) + self.fit_data_pattern(patt_ind, tuple(fit_guess)) + + logger.log_to_file(rb=self.__class__.__name__, + qubits=self._rb_pattern[patt_ind], + **self.fit[patt_ind]) def plot_rb_data(self, pattern_index=0, ax=None, add_label=True, show_plt=True): @@ -255,7 +436,7 @@ def plot_rb_data(self, pattern_index=0, ax=None, Args: pattern_index: which RB pattern to plot ax (Axes or None): plot axis (if passed in). - add_label (bool): Add an EPC label + add_label (bool): Add an EPC label. show_plt (bool): display the plot. Raises: @@ -309,3 +490,666 @@ def plot_rb_data(self, pattern_index=0, ax=None, if show_plt: plt.show() + + +class InterleavedRBFitter(RBFitterBase): + """ + Class for fitters for interleaved RB + Derived from RBFitterBase class + + Contains two RBFitter objects + """ + + def __init__(self, original_result, interleaved_result, + cliff_lengths, rb_pattern=None): + """ + Args: + original_result: list of results of the + original RB sequence (qiskit.Result). + intelreaved_result: list of results of the + interleaved RB sequence (qiskit.Result). + cliff_lengths: the Clifford lengths, 2D list i x j where i is the + number of patterns, j is the number of cliffords lengths + rb_pattern: the pattern for the rb sequences. + """ + + self._cliff_lengths = cliff_lengths + self._rb_pattern = rb_pattern + self._fit_interleaved = [] + + self._rbfit_original = RBFitter( + original_result, cliff_lengths, rb_pattern) + self._rbfit_interleaved = RBFitter( + interleaved_result, cliff_lengths, rb_pattern) + + self.rbfit_std.add_data(original_result) + self.rbfit_int.add_data(interleaved_result) + + if not (original_result is None and interleaved_result is None): + self.fit_data() + + @property + def rbfit_std(self): + """Return the original RB fitter.""" + return self._rbfit_original + + @property + def rbfit_int(self): + """Return the interleaved RB fitter.""" + return self._rbfit_interleaved + + @property + def cliff_lengths(self): + """Return clifford lengths.""" + return self._cliff_lengths + + @property + def fit(self): + """Return fit as a 2 element list.""" + return [self.rbfit_std.fit, self.rbfit_int.fit] + + @property + def fit_int(self): + """Return interleaved fit parameters""" + return self._fit_interleaved + + @property + def rb_fit_fun(self): + """Return the function rb_fit_fun.""" + return self.rbfit_std.rb_fit_fun + + @property + def seeds(self): + """Return the number of loaded seeds as a + 2 element list.""" + return [self.rbfit_std.seeds, self.rbfit_int.seeds] + + @property + def results(self): + """Return all the results as a 2 element list.""" + return [self.rbfit_std.results, self.rbfit_int.results] + + @property + def ydata(self): + """Return ydata (means and std devs). + 2 element list [ydata_original, ydata_inteleaved]""" + return [self.rbfit_std.ydata, self.rbfit_int.ydata] + + @property + def raw_data(self): + """Return raw_data as 2 element list + [raw_original_data, raw_interleaved_data].""" + return [self.rbfit_std.raw_data, self.rbfit_int.raw_data] + + def add_data(self, new_original_result, + new_interleaved_result, rerun_fit=True): + """ + Add a new result. + + Args: + new_original_result: list of rb results + of the original circuits + new_interleaved_result: list of rb results + of the interleaved circuits + rerun_fit: re-caculate the means and fit the result + + Additional information: + Assumes that 'result' was executed is + the output of circuits generated by randomized_becnhmarking_seq + """ + self.rbfit_std.add_data(new_original_result, rerun_fit) + self.rbfit_int.add_data(new_interleaved_result, rerun_fit) + + if rerun_fit: + self.fit_data() + + def calc_data(self): + """ + Retrieve probabilities of success from execution results. Outputs + results into an internal variables: _raw_original_data and + _raw_interleaved_data + """ + self.rbfit_std.calc_data() + self.rbfit_int.calc_data() + + def calc_statistics(self): + """ + Extract averages and std dev. Output + [ydata_original, ydata_inteleaved] + """ + self.rbfit_std.calc_statistics() + self.rbfit_int.calc_statistics() + + def fit_data_pattern(self, patt_ind, fit_guess, fit_index=0): + """ + Fit the RB results of a particular pattern + to an exponential curve. + + Args: + patt_ind: index of the data to fit + fit_guess: guess values for the fit + fit_index: 0 fit the standard data, 1 fit the + interleaved data + + """ + + if fit_index == 0: + self.rbfit_std.fit_data_pattern(patt_ind, fit_guess) + else: + self.rbfit_int.fit_data_pattern(patt_ind, fit_guess) + + def fit_data(self): + """ + Fit the interleaved RB results + Fit each of the patterns + + According to the paper: "Efficient measurement of quantum gate + error by interleaved randomized benchmarking" (arXiv:1203.4550) + Equations (4) and (5) + + Puts the results into a list of fit dictionaries: + where each dictionary corresponds to a pattern and has fields: + 'epc_est' - the estimated error per the interleaved Clifford + 'epc_est_error' - the estimated error derived from the params_err + 'systematic_err' - systematic error bound of epc_est + 'systematic_err_L' = epc_est - systematic_err (left error bound) + 'systematic_err_R' = epc_est + systematic_err (right error bound) + """ + self.rbfit_std.fit_data() + self.rbfit_int.fit_data() + self._fit_interleaved = [] + + for patt_ind, (_, qubits) in enumerate(zip(self._cliff_lengths, + self._rb_pattern)): + # calculate nrb=d=2^n: + nrb = 2 ** len(qubits) + + # Calculate alpha (=p) and alpha_c (=p_c): + alpha = self.rbfit_std.fit[patt_ind]['params'][1] + alpha_c = self.rbfit_int.fit[patt_ind]['params'][1] + # Calculate their errors: + alpha_err = self.rbfit_std.fit[patt_ind]['params_err'][1] + alpha_c_err = self.rbfit_int.fit[patt_ind]['params_err'][1] + + # Calculate epc_est (=r_c^est) - Eq. (4): + epc_est = (nrb - 1) * (1 - alpha_c / alpha) / nrb + + # Calculate the systematic error bounds - Eq. (5): + systematic_err_1 = (nrb - 1) * (abs(alpha - alpha_c / alpha) + + (1 - alpha)) / nrb + systematic_err_2 = 2 * (nrb * nrb - 1) * (1 - alpha) / \ + (alpha * nrb * nrb) + 4 * (np.sqrt(1 - alpha)) * \ + (np.sqrt(nrb * nrb - 1)) / alpha + systematic_err = min(systematic_err_1, systematic_err_2) + systematic_err_L = epc_est - systematic_err + systematic_err_R = epc_est + systematic_err + + # Calculate epc_est_error + alpha_err_sq = (alpha_err / alpha) * (alpha_err / alpha) + alpha_c_err_sq = (alpha_c_err / alpha_c) * (alpha_c_err / alpha_c) + epc_est_err = ((nrb - 1) / nrb) * (alpha_c / alpha) \ + * (np.sqrt(alpha_err_sq + alpha_c_err_sq)) + + self._fit_interleaved.append({'alpha': alpha, + 'alpha_err': alpha_err, + 'alpha_c': alpha_c, + 'alpha_c_err': alpha_c_err, + 'epc_est': epc_est, + 'epc_est_err': epc_est_err, + 'systematic_err': + systematic_err, + 'systematic_err_L': + systematic_err_L, + 'systematic_err_R': + systematic_err_R}) + + logger.log_to_file(rb=self.__class__.__name__, + qubits=self._rb_pattern[patt_ind], + **self._fit_interleaved[patt_ind]) + + def plot_rb_data(self, pattern_index=0, ax=None, + add_label=True, show_plt=True): + """ + Plot interleaved randomized benchmarking data of a single pattern. + + Args: + pattern_index: which RB pattern to plot + ax (Axes or None): plot axis (if passed in). + add_label (bool): Add an EPC label + show_plt (bool): display the plot. + + Raises: + ImportError: If matplotlib is not installed. + """ + + if not HAS_MATPLOTLIB: + raise ImportError('The function plot_interleaved_rb_data \ + needs matplotlib. Run "pip install matplotlib" before.') + + if ax is None: + plt.figure() + ax = plt.gca() + + xdata = self._cliff_lengths[pattern_index] + + # Plot the original and interleaved result for each sequence + for one_seed_data in self.raw_data[0][pattern_index]: + ax.plot(xdata, one_seed_data, color='blue', linestyle='none', + marker='x') + for one_seed_data in self.raw_data[1][pattern_index]: + ax.plot(xdata, one_seed_data, color='red', linestyle='none', + marker='+') + + # Plot the fit + ax.plot(xdata, + self.rbfit_std.rb_fit_fun(xdata, + *self.fit[0] + [pattern_index]['params']), + color='blue', linestyle='-', linewidth=2, + label='Standard RB') + ax.tick_params(labelsize=14) + ax.plot(xdata, + self.rbfit_int.rb_fit_fun(xdata, + *self.fit[1] + [pattern_index]['params']), + color='red', linestyle='-', linewidth=2, + label='Interleaved RB') + ax.tick_params(labelsize=14) + + ax.set_xlabel('Clifford Length', fontsize=16) + ax.set_ylabel('Ground State Population', fontsize=16) + ax.grid(True) + ax.legend(loc='lower left') + + if add_label: + bbox_props = dict(boxstyle="round,pad=0.3", + fc="white", ec="black", lw=2) + + ax.text(0.6, 0.9, + "alpha: %.3f(%.1e) alpha_c: %.3e(%.1e) \n \ + EPC_est: %.3e(%.1e)" % + (self._fit_interleaved[pattern_index]['alpha'], + self._fit_interleaved[pattern_index]['alpha_err'], + self._fit_interleaved[pattern_index]['alpha_c'], + self._fit_interleaved[pattern_index]['alpha_c_err'], + self._fit_interleaved[pattern_index]['epc_est'], + self._fit_interleaved[pattern_index]['epc_est_err']), + ha="center", va="center", size=14, + bbox=bbox_props, transform=ax.transAxes) + + if show_plt: + plt.show() + + +class PurityRBFitter(RBFitterBase): + """ + Class for fitter for purity RB + Derived from RBFitterBase class + """ + + def __init__(self, purity_result, npurity, cliff_lengths, + rb_pattern=None): + """ + Args: + purity_result: list of results of the + 3^n purity RB sequences per seed (qiskit.Result). + npurity: equals 3^n (where n is the dimension) + cliff_lengths: the Clifford lengths, 2D list i x j where i is the + number of patterns, j is the number of cliffords lengths + rb_pattern: the pattern for the rb sequences. + """ + if rb_pattern is None: + rb_pattern = [[0]] + + self._cliff_lengths = cliff_lengths + self._rb_pattern = rb_pattern + self._npurity = npurity + self._nq = len(rb_pattern[0]) # all patterns have same length + + self._fit = [{} for e in rb_pattern] + self._circ_name_type = '' + + self._zdict_ops = [] + self.add_zdict_ops() + + # rb purity fitter + self._rbfit_purity = RBFitter(purity_result, cliff_lengths, + rb_pattern) + self.add_data(purity_result) + + @property + def rbfit_pur(self): + """Return the purity RB fitter.""" + return self._rbfit_purity + + @property + def raw_data(self): + """Return raw data.""" + return self.rbfit_pur.raw_data + + @property + def cliff_lengths(self): + """Return clifford lengths.""" + return self.cliff_lengths + + @property + def ydata(self): + """Return ydata (means and std devs).""" + return self.rbfit_pur.ydata + + @property + def fit(self): + """Return the purity fit parameters.""" + return self.rbfit_pur.fit + + @property + def rb_fit_fun(self): + """Return the function rb_fit_fun.""" + return self.rbfit_pur.rb_fit_fun + + @property + def seeds(self): + """Return the number of loaded seeds.""" + return self.rbfit_pur.seeds + + @property + def results(self): + """Return all the results.""" + return self.rbfit_pur.results + + @staticmethod + def _rb_pur_fit_fun(x, a, alpha, b): + """Function used to fit purity rb.""" + # pylint: disable=invalid-name + return a * alpha ** (2 * x) + b + + @staticmethod + def F234(n, a, b): + """Function than maps: + 2^n x 3^n --> 4^n + namely: + (a,b) --> c where + a in 2^n, b in 3^n, c in 4^n + + 0 <--> I + 1 <--> X + 2 <--> Y + 3 <--> Z + """ + LUT = [[0, 0, 0], [3, 1, 2]] + + # compute bits + aseq = [] + bseq = [] + + aa = a + bb = b + for i in range(n): + aseq.append(np.mod(aa, 2)) + bseq.append(np.mod(bb, 3)) + aa = np.floor_divide(aa, 2) + bb = np.floor_divide(bb, 3) + + c = 0 + for i in range(n): + c += (4 ** i) * LUT[aseq[i]][bseq[i]] + + return c + + def add_zdict_ops(self): + """Creating all Z-correlators + in order to compute the expectation values""" + statedict = {("{0:0%db}" % self._nq).format(i): 1 for i in + range(2 ** self._nq)} + + for i in range(2 ** self._nq): + self._zdict_ops.append(statedict.copy()) + for j in range(2 ** self._nq): + if bin(i & j).count('1') % 2 != 0: + self._zdict_ops[-1][("{0:0%db}" + % self._nq).format(j)] = -1 + + def add_data(self, new_purity_result, rerun_fit=True): + """ + Add a new result. + Args: + new_purity_result: list of rb results of the + purity rb circuits + rerun_fit: re-caculate the means and fit the result + Additional information: + Assumes that 'result' was executed is + the output of circuits generated by randomized_becnhmarking_seq + where is_purity = True. + """ + + if new_purity_result is None: + return + + self.rbfit_pur.add_data(new_purity_result, rerun_fit) + + if rerun_fit: + self.calc_data() + self.calc_statistics() + self.fit_data() + + def calc_data(self): + """ + Measure the purity calculation into an internal variable _raw_data + which is a 3-dimensional list, where item (i,j,k) is the purity + of the set of qubits in pattern "i" + for seed no. j and vector length self._cliff_lengths[i][k]. + Additional information: + Assumes that 'result' was executed is + the output of circuits generated by randomized_becnhmarking_seq, + """ + circ_counts = {} + circ_shots = {} + result_count = 0 + + # Calculating the result output + for _, seed in enumerate(self.rbfit_pur.seeds): + + for pur in range(self._npurity): + + self._circ_name_type = self.rbfit_pur.results[ + result_count].results[0].header.name.split("_length")[0] + result_count += 1 + + for circ, _ in enumerate(self._cliff_lengths[0]): + circ_name = self._circ_name_type + '_length_%d_seed_%d' \ + % (circ, seed) + count_list = [] + for result in self.rbfit_pur.results: + try: + count_list.append(result.get_counts(circ_name)) + except (QiskitError, KeyError): + pass + + circ_name = 'rb_purity_' + str(pur) + \ + '_length_%d_seed_%d' % (circ, seed) + + circ_counts[circ_name] = build_counts_dict_from_list( + count_list) + circ_shots[circ_name] = sum(circ_counts[circ_name]. + values()) + + # Calculating raw_data + self.rbfit_pur.raw_data = [] + startind = 0 + # for each pattern + for patt_ind, _ in enumerate(self._rb_pattern): + + endind = startind + len(self._rb_pattern[patt_ind]) + self.rbfit_pur.raw_data.append([]) + + # for each seed + for seedidx, seed in enumerate(self.rbfit_pur.seeds): + self.rbfit_pur.raw_data[-1].append([]) + + # for each length + for k, _ in enumerate(self._cliff_lengths[0]): + + # vector of the 4^n correlators and counts + corr_vec = [0] * (4 ** self._nq) + count_vec = [0] * (4 ** self._nq) + # corr_list = [[] for e in range(4 ** self._nq)] + + for pur in range(self._npurity): + + circ_name = 'rb_purity_' + str(pur) + \ + '_length_%d_seed_%d' % (k, seed) + + # marginal counts for the pattern + counts_subspace = marginal_counts( + circ_counts[circ_name], + np.arange(startind, endind)) + + # calculating the vector of 4^n correlators + for indcorr in range(2 ** self._nq): + zcorr = average_data(counts_subspace, + self._zdict_ops[indcorr]) + zind = self.F234(self._nq, indcorr, pur) + + corr_vec[zind] += zcorr + count_vec[zind] += 1 + + # calculating the purity + purity = 0 + for idx, _ in enumerate(corr_vec): + purity += (corr_vec[idx]/count_vec[idx]) ** 2 + purity = purity / (2 ** self._nq) + + self.rbfit_pur.raw_data[-1][seedidx].append(purity) + + startind = endind + + def calc_statistics(self): + """ + Extract averages and std dev from the raw data (self._raw_data). + Assumes that self._calc_data has been run. Output into internal + _ydata variable: + ydata is a list of dictionaries (length number of patterns). + Dictionary ydata[i]: + ydata[i]['mean'] is a numpy_array of length n; + entry j of this array contains the mean probability of + success over seeds, for vector length + self._cliff_lengths[i][j]. + And ydata[i]['std'] is a numpy_array of length n; + entry j of this array contains the std + of the probability of success over seeds, + for vector length self._cliff_lengths[i][j]. + """ + self.rbfit_pur.calc_statistics() + + def fit_data_pattern(self, patt_ind, fit_guess): + """ + Fit the RB results of a particular pattern + to an exponential curve. + Args: + patt_ind: index of the subsystem to fit + fit_guess: guess values for the fit + Puts the results into a list of fit dictionaries: + where each dictionary corresponds to a pattern and has fields: + 'params' - three parameters of rb_fit_fun. The middle one is the + exponent. + 'err' - the error limits of the parameters. + """ + self.rbfit_pur.fit_data_pattern(patt_ind, fit_guess) + + def fit_data(self): + """ + Fit the Purity RB results to an exponential curve. + Use the data to construct guess values + for the fits + Puts the results into a list of fit dictionaries: + where each dictionary corresponds to a pattern and has fields: + 'params' - three parameters of rb_fit_fun. The middle one is the + exponent. + 'err' - the error limits of the parameters. + 'epc' - Error per Clifford + 'pepc' - Purity Error per Clifford + """ + self.rbfit_pur.fit_data() + + for patt_ind, (_, _) in enumerate(zip(self._cliff_lengths, + self._rb_pattern)): + # Calculate alpha (=p): + # fitting the curve: A*p^(2m)+B + # where m is the Clifford length + alpha = self.rbfit_pur.fit[patt_ind]['params'][1] + alpha_pur = np.sqrt(alpha) + self.rbfit_pur.fit[patt_ind]['params'][1] = alpha_pur + + # calculate the error of alpha + alpha_err = self.rbfit_pur.fit[patt_ind]['params_err'][1] + alpha_pur_err = alpha_err / (2 * np.sqrt(alpha_pur)) + self.rbfit_pur.fit[patt_ind]['params_err'][1] = \ + alpha_pur_err + + # calcuate purity error per clifford (pepc) + nrb = 2 ** self._nq + pepc = (nrb-1)/nrb * (1-alpha_pur) + self.rbfit_pur.fit[patt_ind]['pepc'] = \ + pepc + + pepc_err = (nrb-1)/nrb * alpha_pur_err / alpha_pur + self.rbfit_pur.fit[patt_ind]['pepc_err'] = \ + pepc_err + + logger.log_to_file(rb=self.__class__.__name__, + qubits=self._rb_pattern[patt_ind], + **self.rbfit_pur.fit[patt_ind]) + + def plot_rb_data(self, pattern_index=0, ax=None, + add_label=True, show_plt=True): + """ + Plot purity rb data of a single pattern. + """ + fit_function = self._rb_pur_fit_fun + + if not HAS_MATPLOTLIB: + raise ImportError('The function plot_rb_data needs matplotlib. ' + 'Run "pip install matplotlib" before.') + + if ax is None: + plt.figure() + ax = plt.gca() + + xdata = self._cliff_lengths[pattern_index] + + # Plot the result for each sequence + for one_seed_data in self.rbfit_pur.raw_data[pattern_index]: + ax.plot(xdata, one_seed_data, color='gray', linestyle='none', + marker='x') + + # Plot the mean with error bars + ax.errorbar(xdata, self.rbfit_pur.ydata[pattern_index]['mean'], + yerr=self.rbfit_pur.ydata[pattern_index]['std'], + color='r', linestyle='--', linewidth=3) + + # Plot the fit + ax.plot(xdata, + fit_function(xdata, *self.rbfit_pur.fit[pattern_index][ + 'params']), + color='blue', linestyle='-', linewidth=2) + ax.tick_params(labelsize=14) + + ax.set_xlabel('Clifford Length', fontsize=16) + ax.set_ylabel('Trace of Rho Square', fontsize=16) + ax.grid(True) + + if add_label: + bbox_props = dict(boxstyle="round,pad=0.3", + fc="white", ec="black", lw=2) + + ax.text(0.6, 0.9, + "alpha: %.3f(%.1e) PEPC: %.3e(%.1e)" % + (self.rbfit_pur.fit[pattern_index]['params'][1], + self.rbfit_pur.fit[pattern_index]['params_err'][1], + self.rbfit_pur.fit[pattern_index]['pepc'], + self.rbfit_pur.fit[pattern_index]['pepc_err']), + ha="center", va="center", size=14, + bbox=bbox_props, transform=ax.transAxes) + + if show_plt: + plt.show() diff --git a/qiskit/ignis/verification/randomized_benchmarking/rb_utils.py b/qiskit/ignis/verification/randomized_benchmarking/rb_utils.py index 1b3830021..b27604b4e 100644 --- a/qiskit/ignis/verification/randomized_benchmarking/rb_utils.py +++ b/qiskit/ignis/verification/randomized_benchmarking/rb_utils.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # TODO(mtreinish): Remove these disables when implementation is finished # pylint: disable=unused-argument,unnecessary-pass diff --git a/qiskit/ignis/verification/tomography/__init__.py b/qiskit/ignis/verification/tomography/__init__.py index 099016958..5e46ebf38 100644 --- a/qiskit/ignis/verification/tomography/__init__.py +++ b/qiskit/ignis/verification/tomography/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/tomography/basis/__init__.py b/qiskit/ignis/verification/tomography/basis/__init__.py index a2f798e1c..8f03e37a3 100644 --- a/qiskit/ignis/verification/tomography/basis/__init__.py +++ b/qiskit/ignis/verification/tomography/basis/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Quantum tomography basis diff --git a/qiskit/ignis/verification/tomography/basis/circuits.py b/qiskit/ignis/verification/tomography/basis/circuits.py index fe50d2d34..d1eb1ba75 100644 --- a/qiskit/ignis/verification/tomography/basis/circuits.py +++ b/qiskit/ignis/verification/tomography/basis/circuits.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ @@ -207,8 +214,8 @@ def _tomography_circuits(circuit, measured_qubits, prepared_qubits=None, meas_circuit_fn(op, qubit, clbit) Args: op (str): the operator label - qubit (tuple(QuantumRegister, int)): measured qubit - clbit (tuple(ClassicalRegister, int)): measurement clbit + qubit (Qubit): measured qubit + clbit (Clbit): measurement clbit Returns: A QuantumCircuit object for the measurement. @@ -225,7 +232,7 @@ def _tomography_circuits(circuit, measured_qubits, prepared_qubits=None, prep_circuit_fn(op, qubit) Args: op (str): the operator label - qubit (tuple(QuantumRegister, int)): measured qubit + qubit (Qubit): measured qubit Returns: A QuantumCircuit object for the preparation gates. @@ -256,14 +263,14 @@ def _tomography_circuits(circuit, measured_qubits, prepared_qubits=None, raise QiskitError( "prepared_qubits and measured_qubits are different length.") num_qubits = len(meas_qubits) - meas_qubit_registers = set(q[0] for q in meas_qubits) + meas_qubit_registers = set(q.register for q in meas_qubits) # Check qubits being measured are defined in circuit for reg in meas_qubit_registers: if reg not in circuit.qregs: logger.warning('WARNING: circuit does not contain ' 'measured QuantumRegister: %s', reg.name) - prep_qubit_registers = set(q[0] for q in prep_qubits) + prep_qubit_registers = set(q.register for q in prep_qubits) # Check qubits being measured are defined in circuit for reg in prep_qubit_registers: if reg not in circuit.qregs: @@ -438,7 +445,7 @@ def _format_registers(*registers): for tuple_element in registers: if isinstance(tuple_element, QuantumRegister): for j in range(tuple_element.size): - qubits.append((tuple_element, j)) + qubits.append(tuple_element[j]) else: qubits.append(tuple_element) # Check registers are unique diff --git a/qiskit/ignis/verification/tomography/basis/paulibasis.py b/qiskit/ignis/verification/tomography/basis/paulibasis.py index 975716b80..598f29f60 100644 --- a/qiskit/ignis/verification/tomography/basis/paulibasis.py +++ b/qiskit/ignis/verification/tomography/basis/paulibasis.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Pauli tomography preparation and measurement basis @@ -34,7 +41,7 @@ def pauli_measurement_circuit(op, qubit, clbit): A QuantumCircuit object. """ - circ = QuantumCircuit(qubit[0], clbit[0]) + circ = QuantumCircuit(qubit.register, clbit.register) if op == 'X': circ.h(qubit) circ.measure(qubit, clbit) @@ -61,7 +68,7 @@ def pauli_preparation_circuit(op, qubit): A QuantumCircuit object. """ - circ = QuantumCircuit(qubit[0]) + circ = QuantumCircuit(qubit.register) if op == 'Xp': circ.h(qubit) if op == 'Xm': diff --git a/qiskit/ignis/verification/tomography/basis/sicbasis.py b/qiskit/ignis/verification/tomography/basis/sicbasis.py index f38ff281d..423bea725 100644 --- a/qiskit/ignis/verification/tomography/basis/sicbasis.py +++ b/qiskit/ignis/verification/tomography/basis/sicbasis.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Symmetric informationally complete (SIC)-POVM tomography preparation basis. @@ -34,7 +41,7 @@ def sicpovm_preparation_circuit(op, qubit): Returns: A QuantumCircuit object. """ - circ = QuantumCircuit(qubit[0]) + circ = QuantumCircuit(qubit.register) theta = -2 * np.arctan(np.sqrt(2)) if op == 'S1': circ.u3(theta, np.pi, 0.0, qubit) diff --git a/qiskit/ignis/verification/tomography/basis/tomographybasis.py b/qiskit/ignis/verification/tomography/basis/tomographybasis.py index 6bd1dd3de..e4c78efde 100644 --- a/qiskit/ignis/verification/tomography/basis/tomographybasis.py +++ b/qiskit/ignis/verification/tomography/basis/tomographybasis.py @@ -1,17 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ TomographyBasis class """ -from qiskit import QuantumRegister -from qiskit import ClassicalRegister from qiskit import QiskitError +from qiskit.circuit import Qubit, Clbit class TomographyBasis: @@ -78,12 +84,10 @@ def measurement_circuit(self, op, qubit, clbit): raise QiskitError( "{} is not a measurement basis".format(self._name)) - if not (isinstance(qubit, tuple) and isinstance(qubit[0], - QuantumRegister)): + if not isinstance(qubit, Qubit): raise QiskitError('Input must be a qubit in a QuantumRegister') - if not (isinstance(clbit, tuple) and isinstance(clbit[0], - ClassicalRegister)): + if not isinstance(clbit, Clbit): raise QiskitError('Input must be a bit in a ClassicalRegister') if op not in self._measurement_labels: @@ -101,8 +105,7 @@ def preparation_circuit(self, op, qubit): raise QiskitError("{} is not a preparation basis".format( self._name)) - if not (isinstance(qubit, tuple) and isinstance(qubit[0], - QuantumRegister)): + if not isinstance(qubit, Qubit): raise QiskitError('Input must be a qubit in a QuantumRegister') if op not in self._preparation_labels: diff --git a/qiskit/ignis/verification/tomography/data.py b/qiskit/ignis/verification/tomography/data.py index fdfea91d7..9b01c47c2 100644 --- a/qiskit/ignis/verification/tomography/data.py +++ b/qiskit/ignis/verification/tomography/data.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Quantum tomography data diff --git a/qiskit/ignis/verification/tomography/fitters/__init__.py b/qiskit/ignis/verification/tomography/fitters/__init__.py index 6990d6977..e48142935 100644 --- a/qiskit/ignis/verification/tomography/fitters/__init__.py +++ b/qiskit/ignis/verification/tomography/fitters/__init__.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2018, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/tomography/fitters/base_fitter.py b/qiskit/ignis/verification/tomography/fitters/base_fitter.py index 988c1d69f..696a4ceb8 100644 --- a/qiskit/ignis/verification/tomography/fitters/base_fitter.py +++ b/qiskit/ignis/verification/tomography/fitters/base_fitter.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/tomography/fitters/cvx_fit.py b/qiskit/ignis/verification/tomography/fitters/cvx_fit.py index fa6f46832..1ea9983e9 100644 --- a/qiskit/ignis/verification/tomography/fitters/cvx_fit.py +++ b/qiskit/ignis/verification/tomography/fitters/cvx_fit.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/tomography/fitters/lstsq_fit.py b/qiskit/ignis/verification/tomography/fitters/lstsq_fit.py index a16f6998f..59618da50 100644 --- a/qiskit/ignis/verification/tomography/fitters/lstsq_fit.py +++ b/qiskit/ignis/verification/tomography/fitters/lstsq_fit.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/tomography/fitters/process_fitter.py b/qiskit/ignis/verification/tomography/fitters/process_fitter.py index 7d0b42b94..3cc821fbf 100644 --- a/qiskit/ignis/verification/tomography/fitters/process_fitter.py +++ b/qiskit/ignis/verification/tomography/fitters/process_fitter.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ @@ -12,6 +19,7 @@ import numpy as np from qiskit import QiskitError +from qiskit.quantum_info.operators import Choi from .base_fitter import TomographyFitter from .cvx_fit import cvxpy, cvx_fit from .lstsq_fit import lstsq_fit @@ -22,7 +30,7 @@ class ProcessTomographyFitter(TomographyFitter): def fit(self, method='auto', standard_weights=True, beta=0.5, **kwargs): """ - Reconstruct a quantum state using CVXPY convex optimization. + Reconstruct a quantum channel using CVXPY convex optimization. Args: method (str): The fitter method 'auto', 'cvx' or 'lstsq'. @@ -35,11 +43,20 @@ def fit(self, method='auto', standard_weights=True, beta=0.5, **kwargs): **kwargs (optional): kwargs for fitter method. Returns: - The fitted matrix rho that minimizes - ||basis_matrix * vec(rho) - data||_2. + Choi: The fitted Choi-matrix J for the channel that maximizes + ||basis_matrix * vec(J) - data||_2. The Numpy matrix can be + obtained from `Choi.data`. Additional Information: + Choi matrix + ----------- + The Choi matrix object is a QuantumChannel representation which + may be converted to other representations using the classes + `SuperOp`, `Kraus`, `Stinespring`, `PTM`, `Chi` from the module + `qiskit.quantum_info.operators`. The raw matrix data for the + representation may be obtained by `channel.data`. + Fitter method ------------- The 'cvx' fitter method used CVXPY convex optimization package. @@ -127,9 +144,9 @@ def fit(self, method='auto', standard_weights=True, beta=0.5, **kwargs): else: method = 'cvx' if method == 'lstsq': - return lstsq_fit(data, basis_matrix, weights=weights, - trace=dim, **kwargs) + return Choi(lstsq_fit(data, basis_matrix, weights=weights, + trace=dim, **kwargs)) if method == 'cvx': - return cvx_fit(data, basis_matrix, weights=weights, trace=dim, - trace_preserving=True, **kwargs) + return Choi(cvx_fit(data, basis_matrix, weights=weights, trace=dim, + trace_preserving=True, **kwargs)) raise QiskitError('Unrecognised fit method {}'.format(method)) diff --git a/qiskit/ignis/verification/tomography/fitters/state_fitter.py b/qiskit/ignis/verification/tomography/fitters/state_fitter.py index 9f589d4d4..16a163144 100644 --- a/qiskit/ignis/verification/tomography/fitters/state_fitter.py +++ b/qiskit/ignis/verification/tomography/fitters/state_fitter.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ diff --git a/qiskit/ignis/verification/topological_codes/__init__.py b/qiskit/ignis/verification/topological_codes/__init__.py new file mode 100644 index 000000000..d13e28ac6 --- /dev/null +++ b/qiskit/ignis/verification/topological_codes/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +""" +Error correction benchmarking module +""" + +from .circuits import RepetitionCode +from .fitters import GraphDecoder +from .fitters import lookuptable_decoding +from .fitters import postselection_decoding diff --git a/qiskit/ignis/verification/topological_codes/circuits.py b/qiskit/ignis/verification/topological_codes/circuits.py new file mode 100755 index 000000000..7cdba6703 --- /dev/null +++ b/qiskit/ignis/verification/topological_codes/circuits.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +''' +Generates circuits for quantum error correction +''' + +from qiskit import QuantumRegister, ClassicalRegister +from qiskit import QuantumCircuit + + +class RepetitionCode(): + ''' + Implementation of a distance d repetition code, implemented over + T syndrome measurement rounds. + ''' + + def __init__(self, d, T=0): + ''' + Creates the circuits corresponding to a logical 0 and 1 encoded + using a repetition code. + + Args: + d: Number of code qubits (and hence repetitions) used. + T: Number of rounds of ancilla-assited syndrome measurement. + + + Additional information: + No measurements are added to the circuit if `T=0`. Otherwise + `T` rounds are added, followed by measurement of the code + qubits (corresponding to a logical measurement and final + syndrome measurement round). + ''' + + self.d = d + self.T = 0 + + self.code_qubit = QuantumRegister(d, 'code_qubit') + self.link_qubit = QuantumRegister((d - 1), 'link_qubit') + self.qubit_registers = {'code_qubit', 'link_qubit'} + + self.link_bits = [] + self.code_bit = ClassicalRegister(d, 'code_bit') + + self.circuit = {} + for log in ['0', '1']: + self.circuit[log] = QuantumCircuit( + self.link_qubit, self.code_qubit, name=log) + + self._preparation() + + for _ in range(T): + self.syndrome_measurement() + + if T != 0: + self.readout() + + def get_circuit_list(self): + ''' + Returns: + circuit_list: self.circuit as a list, with + circuit_list[0] = circuit['0'] + circuit_list[1] = circuit['1'] + ''' + circuit_list = [self.circuit[log] for log in ['0', '1']] + return circuit_list + + def x(self, logs=('0', '1')): + ''' + Applies a logical x to the circuits for the given logical values. + + Args: + logs: List or tuple of logical values expressed as strings. + ''' + for log in logs: + for j in range(self.d): + self.circuit[log].x(self.code_qubit[j]) + self.circuit[log].barrier() + + def _preparation(self): + ''' + Prepares logical bit states by applying an x to the circuit that will + encode a 1. + ''' + self.x(['1']) + + def syndrome_measurement(self): + ''' + Application of a syndrome measurement round. + ''' + + self.link_bits.append(ClassicalRegister( + (self.d - 1), 'round_' + str(self.T) + '_link_bit')) + + for log in ['0', '1']: + + self.circuit[log].add_register(self.link_bits[-1]) + + for j in range(self.d - 1): + self.circuit[log].cx(self.code_qubit[j], self.link_qubit[j]) + + for j in range(self.d - 1): + self.circuit[log].cx( + self.code_qubit[j + 1], self.link_qubit[j]) + + for j in range(self.d - 1): + self.circuit[log].measure( + self.link_qubit[j], self.link_bits[self.T][j]) + self.circuit[log].reset(self.link_qubit[j]) + + self.circuit[log].barrier() + + self.T += 1 + + def readout(self): + ''' + Readout of all code qubits, which corresponds to a logical measurement + as well as allowing for a measurement of the syndrome to be inferred. + ''' + for log in ['0', '1']: + self.circuit[log].add_register(self.code_bit) + self.circuit[log].measure(self.code_qubit, self.code_bit) + + def process_results(self, raw_results): + ''' + Args: + raw_results: A dictionary whose keys are logical values, and whose + values are standard counts dictionaries, (as obtained from the + `get_counts` method of a qiskit.Result object). + + Returns: + results: Dictionary with the same structure as the input, but with + the bit strings used as keys in the counts dictionaries converted + to the form required by the decoder. + + Additional information: + The circuits must be executed outside of this class, so that + their is full freedom to compile, choose a backend, use a + noise model, etc. The results from these executions should then + be used to create the input for this method. + ''' + results = {} + for log in raw_results: + results[log] = {} + for string in raw_results[log]: + + # logical readout taken from + measured_log = string[0] + ' ' + string[self.d - 1] + + # final syndrome deduced from final code qubit readout + full_syndrome = '' + for j in range(self.d - 1): + full_syndrome += '0' * (string[j] == string[j + 1]) \ + + '1' * (string[j] != string[j + 1]) + # results from all other syndrome measurements then added + full_syndrome = full_syndrome + string[self.d:] + + # changes between one syndrome and the next then calculated + syndrome_list = full_syndrome.split(' ') + syndrome_changes = '' + for t in range(self.T + 1): + for j in range(self.d - 1): + if t == 0: + change = (syndrome_list[-1][j] != '0') + else: + change = (syndrome_list[-t][j] + != syndrome_list[-t - 1][j]) + syndrome_changes += '0' * (not change) + '1' * change + syndrome_changes += ' ' + + # the space separated string of syndrome changes then gets a + # double space separated logical value on the end + new_string = measured_log + ' ' + syndrome_changes[:-1] + + results[log][new_string] = raw_results[log][string] + + return results diff --git a/qiskit/ignis/verification/topological_codes/fitters.py b/qiskit/ignis/verification/topological_codes/fitters.py new file mode 100755 index 000000000..888af5f1f --- /dev/null +++ b/qiskit/ignis/verification/topological_codes/fitters.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +''' +Decoders for quantum error correction codes, with a focus on those that can be +expressed as solving a graph-theoretic problem. +''' + +import copy +import warnings +import networkx as nx + +from qiskit import QuantumCircuit, Aer, execute + + +class GraphDecoder(): + ''' + Class to construct the graph corresponding to the possible syndromes + of a quantum error correction code, and then run suitable decoders. + ''' + + def __init__(self, code, S=None): + ''' + Args: + code: The QEC Code object for which this decoder will be used. + S: Graph describing connectivity between syndrome elements. Will + be generated automatically if not supplied. + Additional information: + The decoder for the supplied `code` is initialized by running + `_make_syndrome_graph()`. Since this process can take some + time, it is also possible to load in a premade `S`. However, + if this was created for a differently defined `code`,it won't work + properly. + ''' + + self.code = code + + if S: + self.S = S + else: + self.S = self._make_syndrome_graph() + + def _separate_string(self, string): + + separated_string = [] + for syndrome_type_string in string.split(' '): + separated_string.append(syndrome_type_string.split(' ')) + return separated_string + + def _make_syndrome_graph(self): + ''' + This method injects all possible Pauli errors into the circuit for + `code`. This is done by examining the qubits used in each gate of the + circuit for a stored logical 0. A graph is then created with a node + for each non-trivial syndrome element, and an edge between all such + elements that can be created by the same error. + ''' + + S = nx.Graph() + + qc = self.code.circuit['0'] + + blank_qc = QuantumCircuit() + for qreg in qc.qregs: + blank_qc.add_register(qreg) + for creg in qc.cregs: + blank_qc.add_register(creg) + + error_circuit = {} + circuit_name = {} + depth = len(qc) + for j in range(depth): + qubits = qc.data[j][1] + for qubit in qubits: + for error in ['x', 'y', 'z']: + temp_qc = copy.deepcopy(blank_qc) + temp_qc.name = str((j, qubit, error)) + temp_qc.data = qc.data[0:j] + eval('temp_qc.' + error + '(qubit)') + temp_qc.data += qc.data[j:depth + 1] + circuit_name[(j, qubit, error)] = temp_qc.name + error_circuit[temp_qc.name] = temp_qc + + job = execute(list(error_circuit.values()), + Aer.get_backend('qasm_simulator')) + + for j in range(depth): + qubits = qc.data[j][1] + for qubit in qubits: + for error in ['x', 'y', 'z']: + + raw_results = {} + raw_results['0'] = job.result().get_counts( + str((j, qubit, error))) + results = self.code.process_results(raw_results)['0'] + + for string in results: + separated_string = self._separate_string(string) + + nodes = [] + for syn_type, _ in enumerate(separated_string): + for syn_round in range( + len(separated_string[syn_type])): + elements = \ + separated_string[syn_type][syn_round] + for elem_num, element in enumerate(elements): + if element == '1': + nodes.append((syn_type, + syn_round, + elem_num)) + + for node in nodes: + S.add_node(node) + for source in nodes: + for target in nodes: + if source != target: + S.add_edge(source, target, distance=1) + + return S + + def make_error_graph(self, string, subgraphs=None): + ''' + Args: + string: A string describing the output from the code. + subgraphs: Used when multiple, semi-indepedent graphs need + need to created. + + Returns: + E: The subgraph(s) of S which corresponds to the non-trivial + syndrome elements in the given string. + ''' + + if subgraphs is None: + subgraphs = [] + for syndrome_type in string.split(' '): + subgraphs.append(['0']) + + set_subgraphs = [ + subgraph for subs4type in subgraphs for subgraph in subs4type] + + E = {subgraph: nx.Graph() for subgraph in set_subgraphs} + + separated_string = self._separate_string(string) + + for syndrome_type, _ in enumerate(separated_string): + for syndrome_round in range(len(separated_string[syndrome_type])): + elements = separated_string[syndrome_type][syndrome_round] + for elem_num, element in enumerate(elements): + if element == '1' or syndrome_type == 0: + for subgraph in subgraphs[syndrome_type]: + E[subgraph].add_node( + (syndrome_type, + syndrome_round, + elem_num)) + + # for each pair of nodes in error create an edge and weight with the + # distance + for subgraph in set_subgraphs: + for source in E[subgraph]: + for target in E[subgraph]: + if target != (source): + distance = int(nx.shortest_path_length( + self.S, source, target)) + E[subgraph].add_edge(source, target, weight=-distance) + + return E + + def matching(self, string): + ''' + Args: + string: A string describing the output from the code. + + Returns: + logical_string: A string with corrected logical values, + computed using minimum weight perfect matching. + + Additional information: + This function can be run directly, or used indirectly to + calculate a logical error probability with `get_logical_prob` + ''' + + # this matching algorithm is designed for a single graph + E = self.make_error_graph(string)['0'] + + # set up graph that is like E, but each syndrome node is connected to a + # separate copy of the nearest logical node + E_matching = nx.Graph() + syndrome_nodes = [] + logical_nodes = [] + logical_neighbours = [] + for node in E: + if node[0] == 0: + logical_nodes.append(node) + else: + syndrome_nodes.append(node) + for source in syndrome_nodes: + for target in syndrome_nodes: + if target != (source): + E_matching.add_edge( + source, target, weight=E[source][target]['weight']) + + potential_logical = {} + for target in logical_nodes: + potential_logical[target] = E[source][target]['weight'] + nearest_logical = max(potential_logical, key=potential_logical.get) + E_matching.add_edge( + source, + nearest_logical + source, + weight=potential_logical[nearest_logical]) + logical_neighbours.append(nearest_logical + source) + for source in logical_neighbours: + for target in logical_neighbours: + if target != (source): + E_matching.add_edge(source, target, weight=0) + + # do the matching on this + matches = nx.max_weight_matching(E_matching, maxcardinality=True) + + # use it to construct and return a correcetd logical string + logicals = self._separate_string(string)[0] + for (source, target) in matches: + if source[0] == 0 and target[0] != 0: + logicals[source[1]] = str((int(logicals[source[1]]) + 1) % 2) + if target[0] == 0 and source[0] != 0: + logicals[target[1]] = str((int(logicals[target[1]]) + 1) % 2) + + logical_string = '' + for logical in logicals: + logical_string += logical + ' ' + logical_string = logical_string[:-1] + + return logical_string + + def get_logical_prob(self, results, algorithm='matching'): + ''' + Args: + results: A results dictionary, as produced by the + `process_results` method of the code. + algorithm: Choice of which decoder to use. + + Returns: + logical_prob: Dictionary of logical error probabilities for + each of the encoded logical states whose results were given in + the input. + ''' + + logical_prob = {} + for log in results: + + shots = 0 + incorrect_shots = 0 + + corrected_results = {} + if algorithm == 'matching': + for string in results[log]: + corr_str = self.matching(string) + if corr_str in corrected_results: + corrected_results[corr_str] += results[log][string] + else: + corrected_results[corr_str] = results[log][string] + else: + warnings.warn( + "The requested algorithm " + + str(algorithm) + + " is not known.", + Warning) + + for string in corrected_results: + shots += corrected_results[string] + if string[0] != str(log): + incorrect_shots += corrected_results[string] + + logical_prob[log] = incorrect_shots / shots + + return logical_prob + + +def postselection_decoding(results): + ''' + Calculates the logical error probability using postselection decoding. + This postselects all results with trivial syndrome. + + Args: + results: A results dictionary, as produced by the + `process_results` method of a code. + + Returns: + logical_prob: Dictionary of logical error probabilities for + each of the encoded logical states whose results were given in + the input. + ''' + logical_prob = {} + postselected_results = {} + for log in results: + postselected_results[log] = {} + for string in results[log]: + + syndrome_list = string.split(' ') + syndrome_list.pop(0) + syndrome_string = ' '.join(syndrome_list) + + error_free = True + for char in syndrome_string: + error_free = error_free and (char in ['0', ' ']) + + if error_free: + postselected_results[log][string] = results[log][string] + + for log in results: + shots = 0 + incorrect_shots = 0 + for string in postselected_results[log]: + shots += postselected_results[log][string] + if string[0] != log: + incorrect_shots += postselected_results[log][string] + + logical_prob[log] = incorrect_shots / shots + + return logical_prob + + +def lookuptable_decoding(training_results, real_results): + ''' + Calculates the logical error probability using postselection decoding. + This postselects all results with trivial syndrome. + + Args: + training_results: A results dictionary, as produced by the + `process_results` method of a code. + real_results: A results dictionary, as produced by the + `process_results` method of a code. + + Returns: + logical_prob: Dictionary of logical error probabilities for + each of the encoded logical states whose results were given in + the input. + + + Additional information: + Given a two dictionaries of results, as produced by a code object, + thelogical error probability is calculated for lookup table + decoding. This is done using `training_results` as a guide to which + syndrome is most probable for each logical value, and the + probability is calculated for the results in `real_results`. + ''' + + logical_prob = {} + for log in real_results: + shots = 0 + incorrect_shots = 0 + for string in real_results[log]: + + p = {} + for testlog in ['0', '1']: + if string in training_results[testlog]: + p[testlog] = training_results[testlog][string] + else: + p[testlog] = 0 + + shots += real_results[log][string] + if p['1' * (log == '0') + '0' * (log == '1')] > p[log]: + incorrect_shots += real_results[log][string] + + logical_prob[log] = incorrect_shots / shots + + return logical_prob diff --git a/qiskit/ignis/version.py b/qiskit/ignis/version.py new file mode 100644 index 000000000..3696d2819 --- /dev/null +++ b/qiskit/ignis/version.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Functions for getting version information about ignis.""" + + +import os +import subprocess + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def _minimal_ext_cmd(cmd): + # construct minimal environment + env = {} + for k in ['SYSTEMROOT', 'PATH']: + v = os.environ.get(k) + if v is not None: + env[k] = v + # LANGUAGE is used on win32 + env['LANGUAGE'] = 'C' + env['LANG'] = 'C' + env['LC_ALL'] = 'C' + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env, + cwd=os.path.join(os.path.dirname(ROOT_DIR))) + out = proc.communicate()[0] + if proc.returncode > 0: + raise OSError + return out + + +def git_version(): + """Get the current git head sha1.""" + # Determine if we're at master + try: + out = _minimal_ext_cmd(['git', 'rev-parse', 'HEAD']) + git_revision = out.strip().decode('ascii') + except OSError: + git_revision = "Unknown" + + return git_revision + + +with open(os.path.join(ROOT_DIR, "VERSION.txt"), "r") as version_file: + VERSION = version_file.read().strip() + + +def get_version_info(): + """Get the full version string.""" + # Adding the git rev number needs to be done inside + # write_version_py(), otherwise the import of scipy.version messes + # up the build under Python 3. + full_version = VERSION + + if not os.path.exists(os.path.join(os.path.dirname( + os.path.dirname(ROOT_DIR)), '.git')): + return full_version + try: + release = _minimal_ext_cmd(['git', 'tag', '-l', '--points-at', 'HEAD']) + except Exception: # pylint: disable=broad-except + return full_version + if not release: + git_revision = git_version() + full_version += '.dev0+' + git_revision[:7] + + return full_version + + +__version__ = get_version_info() diff --git a/requirements-dev.txt b/requirements-dev.txt index 7db1f95dc..fc9e8ad7c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,10 @@ cvxpy>=1.0.15 cvxopt>=1.2.3 qiskit-aer>=0.1.1 +scikit-learn>=0.17 +Sphinx>=1.8.3 +sphinx-rtd-theme>=0.4.0 +sphinx-tabs>=1.1.11 +sphinx-automodapi +stestr>=2.5.0 +ddt>=1.2.0 diff --git a/setup.py b/setup.py index 84d64ef6b..9895fd599 100644 --- a/setup.py +++ b/setup.py @@ -1,33 +1,51 @@ # -*- coding: utf-8 -*- -# Copyright 2017, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. -from setuptools import setup, find_packages +import os +import inspect +import setuptools +import sys requirements = [ "numpy>=1.13", - "qiskit-terra>=0.7.0", + "qiskit-terra>=0.8.0", "scipy>=0.19,!=0.19.1", + "setuptools>=40.1.0", ] -def find_qiskit_ignis_packages(): - location = 'qiskit/ignis' - prefix = 'qiskit.ignis' - ignis_packages = find_packages(where=location, exclude=['test*']) - pkg_list = list( - map(lambda package_name: '{}.{}'.format(prefix, package_name), - ignis_packages) - ) - return pkg_list +if not hasattr(setuptools, + 'find_namespace_packages') or not inspect.ismethod( + setuptools.find_namespace_packages): + print("Your setuptools version:'{}' does not support PEP 420 " + "(find_namespace_packages). Upgrade it to version >='40.1.0' and " + "repeat install.".format(setuptools.__version__)) + sys.exit(1) + + +version_path = os.path.abspath( + os.path.join( + os.path.join( + os.path.join(os.path.dirname(__file__), 'qiskit'), 'ignis'), + 'VERSION.txt')) +with open(version_path, 'r') as fd: + version = fd.read().rstrip() -setup( +setuptools.setup( name="qiskit-ignis", - version="0.1.0", + version=version, description="Qiskit tools for quantum information science", url="https://github.com/Qiskit/qiskit-ignis", author="Qiskit Development Team", @@ -47,8 +65,9 @@ def find_qiskit_ignis_packages(): "Topic :: Scientific/Engineering", ], keywords="qiskit sdk quantum", - packages=find_qiskit_ignis_packages(), + packages=setuptools.find_namespace_packages(exclude=['test*']), install_requires=requirements, include_package_data=True, - python_requires=">=3.5" + python_requires=">=3.5", + zip_safe=False ) diff --git a/test/__init__.py b/test/__init__.py index 5fbc90bca..142aec81a 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,6 +1,13 @@ # -*- coding: utf-8 -*- -# Copyright 2017, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/characterization/__init__.py b/test/characterization/__init__.py index e69de29bb..428fe2e50 100644 --- a/test/characterization/__init__.py +++ b/test/characterization/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/characterization/test_characterization.py b/test/characterization/test_characterization.py index 6b03a23be..0e4e7a2b2 100644 --- a/test/characterization/test_characterization.py +++ b/test/characterization/test_characterization.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. - +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=no-name-in-module """ @@ -72,7 +78,8 @@ def test_t2star(self): backend_result = qiskit.execute( circs, backend, shots=shots, backend_options={'max_parallel_experiments': 0}, - noise_model=noise_model).result() + noise_model=noise_model, + optimization_level=0).result() initial_t2 = expected_t2 initial_a = 0.5 @@ -93,7 +100,8 @@ def test_t2star(self): circs_osc, backend, shots=shots, backend_options={'max_parallel_experiments': 0}, - noise_model=noise_model).result() + noise_model=noise_model, + optimization_level=0).result() initial_a = 0.5 initial_c = 0.5 @@ -141,7 +149,8 @@ def test_t1(self): circs, backend, shots=shots, backend_options={'max_parallel_experiments': 0}, - noise_model=noise_model).result() + noise_model=noise_model, + optimization_level=0).result() initial_t1 = expected_t1 initial_a = 1 @@ -184,7 +193,8 @@ def test_t2(self): circs, backend, shots=shots, backend_options={'max_parallel_experiments': 0}, - noise_model=noise_model).result() + noise_model=noise_model, + optimization_level=0).result() initial_t2 = expected_t2 initial_a = 1 @@ -230,7 +240,8 @@ def test_zz(self): # For demonstration purposes split the execution into two jobs backend_result = qiskit.execute(circs, backend, shots=shots, - noise_model=noise_model).result() + noise_model=noise_model, + optimization_level=0).result() initial_a = 0.5 initial_c = 0.5 @@ -297,14 +308,14 @@ def test_ampcal1Q(self): noise_model = NoiseModel() noise_model.add_all_qubit_quantum_error(error, 'u2') - initial_theta = 0.02 + initial_theta = 0.18 initial_c = 0.5 fit = AmpCalFitter(self.run_sim(noise_model), xdata, self._qubits, fit_p0=[initial_theta, initial_c], fit_bounds=([-np.pi, -1], [np.pi, 1])) - + print(fit.angle_err(0)) self.assertAlmostEqual(fit.angle_err(0), 0.1, 2) def test_anglecal1Q(self): @@ -316,7 +327,7 @@ def test_anglecal1Q(self): self._circs, xdata = anglecal_1Q_circuits(self._maxrep, self._qubits, angleerr=0.1) - initial_theta = 0.02 + initial_theta = 0.18 initial_c = 0.5 fit = AngleCalFitter(self.run_sim([]), xdata, self._qubits, @@ -347,7 +358,7 @@ def test_ampcalCX(self): noise_model = NoiseModel() noise_model.add_nonlocal_quantum_error(error, 'cx', [1, 0], [0, 1]) - initial_theta = 0.02 + initial_theta = 0.18 initial_c = 0.5 fit = AmpCalCXFitter(self.run_sim(noise_model), xdata, self._qubits, @@ -369,7 +380,7 @@ def test_anglecalCX(self): self._controls, angleerr=0.1) - initial_theta = 0.02 + initial_theta = 0.18 initial_c = 0.5 fit = AngleCalCXFitter(self.run_sim([]), xdata, self._qubits, diff --git a/test/characterization/test_characterization_fitters.py b/test/characterization/test_characterization_fitters.py index 47b38cbf1..37680800e 100644 --- a/test/characterization/test_characterization_fitters.py +++ b/test/characterization/test_characterization_fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=no-name-in-module diff --git a/test/logging/test_logging.py b/test/logging/test_logging.py new file mode 100644 index 000000000..73483b9d3 --- /dev/null +++ b/test/logging/test_logging.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +""" +Unit testing of the Ignis Logging facility. Covering the following specs: +1) Configuration aspects +====================== +1.1: No config file entails logging is disabled +1.2: A typo in one of the params is ignored +1.3: Supported params are affecting the logging +1.4: Programmatic settings of the logger overrides the config file + +2) File logging: +================ +2.1: Data is saved to the log +2.2: The message is in the right format +2.3: Data is accumulated across log_to_file calls + +3) Log reader: +============== +3.1: Values are read properly +3.2: Key filtering is working properly +3.3: Date filtering is working properly + + +""" +from qiskit.ignis.logging import * +import unittest +import os +import errno + + +class TestLoggingBase(unittest.TestCase): + + _qiskit_dir = "" + _default_log = "ignis.log" + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + self._qiskit_dir = os.path.join(os.path.expanduser('~'), ".qiskit") + + def setUp(self): + """ + Basic setup - making the .qiskit dir and preserving any existing files + :return: + """ + try: + os.makedirs(self._qiskit_dir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + # Protecting the original files, if exist + safe_rename_file(os.path.join(self._qiskit_dir, "logging.yaml"), + os.path.join(self._qiskit_dir, "logging.yaml.orig")) + # Assuming isnis.log for the default log file, as hard coded + safe_rename_file(self._default_log, self._default_log + ".orig") + + def tearDown(self): + """ + Remove auto-generated files, resurrecting original files, and + resetting the IgnisLogging singleton state + + :return: + """ + try: + os.remove("logging.yaml") + except OSError: + pass + + # Resurrecting the original files + safe_rename_file( + os.path.join(self._qiskit_dir, "logging.yaml.orig"), + os.path.join(self._qiskit_dir, "logging.yaml")) + + safe_rename_file(self._default_log + ".orig", self._default_log) + + # Resetting the following attributes, to make the singleton reset + IgnisLogging().get_logger(__name__).__init__(__name__) + IgnisLogging._instance = None + IgnisLogging._file_logging_enabled = False + IgnisLogging._log_file = None + IgnisLogging._config_file_exists = False + + +def safe_rename_file(src, dst): + try: + os.rename(src, dst) + except FileNotFoundError: + pass + except OSError: + raise + + +class TestLoggingConfiguration(TestLoggingBase): + def test_no_config_file(self): + logger = IgnisLogging().get_logger(__name__) + logger.log_to_file(test="test") + + self.assertFalse(os.path.exists(self._default_log)) + + def test_file_logging_typo(self): + """ + Only tests the main param: file_logging + :return: + """ + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging1: true") + + logger = IgnisLogging().get_logger(__name__) + logger.log_to_file(test="test") + + self.assertFalse(os.path.exists(self._default_log)) + + def test_file_true_typo(self): + """ + Only tests the main param: file_logging + :return: + """ + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging: tru") + + logger = IgnisLogging().get_logger(__name__) + logger.log_to_file(test="test") + + self.assertFalse(os.path.exists(self._default_log)) + + def test_log_file_path(self): + """ + test that a custom log file path is honored + :return: + """ + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging: true\n" + "log_file: test_log.log") + + logger = IgnisLogging().get_logger(__name__) + logger.log_to_file(test="test") + + self.assertTrue(os.path.exists("test_log.log")) + os.remove("test_log.log") + + def test_file_rotation(self): + """ + Test that the file rotation is working + :return: + """ + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging: true\n" + "max_size: 10\n" + "max_rotations: 3") + + logger = IgnisLogging().get_logger(__name__) + for i in range(100): + logger.log_to_file(test="test%d" % i) + + self.assertTrue(os.path.exists(self._default_log)) + self.assertTrue(os.path.exists(self._default_log + ".1")) + self.assertTrue(os.path.exists(self._default_log + ".2")) + self.assertTrue(os.path.exists(self._default_log + ".3")) + + list(map(os.remove, [self._default_log + ".1", + self._default_log + ".2", + self._default_log + ".3"])) + + def test_manual_enabling(self): + """ + Test that enabling the logging manually works + :return: + """ + logger = IgnisLogging().get_logger(__name__) + logger.enable_file_logging() + logger.log_to_file(test="test") + + self.assertTrue(os.path.exists(self._default_log)) + + def test_manual_disabling(self): + """ + Test that disabling the logging manually works + :return: + """ + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging: true\n") + + logger = IgnisLogging().get_logger(__name__) + logger.disable_file_logging() + logger.log_to_file(test="test") + + self.assertFalse(os.path.exists(self._default_log)) + + +class TestFileLogging(TestLoggingBase): + def test_save_line(self): + logger = IgnisLogging().get_logger(__name__) + + logger.enable_file_logging() + logger.log_to_file(test="test") + + self.assertTrue(os.path.exists(self._default_log)) + with open(self._default_log, 'r') as file: + self.assertIn("\'test\':\'test\'", file.read()) + + def test_format(self): + logger = IgnisLogging().get_logger(__name__) + + logger.enable_file_logging() + logger.log_to_file(test="test") + + with open(self._default_log, 'r') as file: + self.assertRegex( + file.read(), + r"\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} ignis_logging \S+") + + def test_multiple_lines(self): + logger = IgnisLogging().get_logger(__name__) + + logger.enable_file_logging() + logger.log_to_file(test="test1") + logger.log_to_file(test="test2") + + with open(self._default_log, 'r') as file: + lines = file.read().split('\n') + + self.assertGreaterEqual(len(lines), 2) + + +class TestLogReader(TestLoggingBase): + def setUp(self): + TestLoggingBase.setUp(self) + + def tearDown(self): + TestLoggingBase.tearDown(self) + + def test_read_multiple_files(self): + with open(os.path.join(self._qiskit_dir, "logging.yaml"), "w") as f: + f.write("file_logging: true\n" + "max_size: 40\n" + "max_rotations: 5") + + logger = IgnisLogging().get_logger(__name__) + for i in range(10): + logger.log_to_file(k="data%d" % i) + + reader = IgnisLogReader() + files = reader.get_log_files() + self.assertEqual(len(files), 6) + + for f in reader.get_log_files(): + os.remove(f) + + def test_filtering(self): + with open(self._default_log, 'w') as log: + log.write( + "2019/08/04 13:27:04 ignis_logging \'k1\':\'d1\'\n" + "2019/08/04 13:27:05 ignis_logging \'k1\':\'d2\'\n" + "2019/08/04 13:27:06 ignis_logging \'k1\':\'d3\'\n" + "2019/08/05 13:27:04 ignis_logging \'k2\':\'d4\'\n" + "2019/08/06 13:27:04 ignis_logging \'k2\':\'d5\'\n" + "2019/09/02 13:27:04 ignis_logging \'k3\':\'d6\'\n" + "2019/09/04 13:27:04 ignis_logging \'k4\':\'d7\'\n") + + reader = IgnisLogReader() + + self.assertEqual(len(reader.read_values(keys=["k1", "k2"])), 5) + self.assertEqual( + len(reader.read_values(from_datetime="2019/08/05 00:00:00")), 4) + self.assertEqual( + len(reader.read_values(to_datetime="2019/08/04 13:27:06")), 3) + + +if __name__ == '__main__': + unittest.main(warnings='ignore') diff --git a/test/measurement/__init__.py b/test/measurement/__init__.py new file mode 100644 index 000000000..142aec81a --- /dev/null +++ b/test/measurement/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/measurement/discriminator/__init__.py b/test/measurement/discriminator/__init__.py new file mode 100644 index 000000000..142aec81a --- /dev/null +++ b/test/measurement/discriminator/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/measurement/discriminator/test_filter.py b/test/measurement/discriminator/test_filter.py new file mode 100644 index 000000000..011553438 --- /dev/null +++ b/test/measurement/discriminator/test_filter.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Test discrimination filters. +""" + +import unittest +from operator import getitem + +import test.utils as utils +import qiskit +from qiskit import Aer +from qiskit.ignis.measurement.discriminator.filters import DiscriminationFilter +from qiskit.result.models import ExperimentResultData +from qiskit.ignis.measurement.discriminator.iq_discriminators import \ + LinearIQDiscriminator +from qiskit.ignis.mitigation.measurement import circuits + + +class TestDiscriminationFilter(unittest.TestCase): + """ + Test methods of discrimination filters. + """ + + def setUp(self) -> None: + """ + Initialize private variables. + """ + self.shots = 10 + self.qubits = [0, 1] + + def test_get_base(self): + """ + Test the get_base method to see if it can properly identify the number + of basis states per quantum element. E.g. second excited level of + transmon. + """ + expected_states = {'a': '02', 'b': '01', 'c': '00', 'd': '11'} + base = DiscriminationFilter.get_base(expected_states) + self.assertEqual(base, 3) + + def test_count(self): + """ + Test to see if the filter properly converts the result of + discriminator.discriminate to a dictionary of counts. + """ + fitter = LinearIQDiscriminator([], [], []) + d_filter = DiscriminationFilter(fitter, 2) + + raw_counts = d_filter.count(['01', '00', '01', '00', '00', '10']) + self.assertEqual(raw_counts['0x0'], 3) + self.assertEqual(raw_counts['0x1'], 2) + self.assertEqual(raw_counts['0x2'], 1) + self.assertRaises(KeyError, getitem, raw_counts, '0x3') + + d_filter = DiscriminationFilter(fitter, 3) + raw_counts = d_filter.count(['02', '02', '20', '21', '21', '02']) + self.assertEqual(raw_counts['0x2'], 3) + self.assertEqual(raw_counts['0x6'], 1) + self.assertEqual(raw_counts['0x7'], 2) + + def test_apply(self): + """ + Set-up a discriminator based on simulated data, train it and then + discriminate the calibration data. + """ + meas_cal, _ = circuits.tensored_meas_cal([[0], [1]]) + + backend = Aer.get_backend('qasm_simulator') + job = qiskit.execute(meas_cal, backend=backend, shots=self.shots, + meas_level=1) + + cal_results = job.result() + + i0, q0, i1, q1 = 0., -1., 0., 1. + ground = utils.create_shots(i0, q0, 0.1, 0.1, self.shots, self.qubits) + excited = utils.create_shots(i1, q1, 0.1, 0.1, self.shots, self.qubits) + + cal_results.results[0].meas_level = 1 + cal_results.results[1].meas_level = 1 + cal_results.results[0].data = ExperimentResultData(memory=ground) + cal_results.results[1].data = ExperimentResultData(memory=excited) + + discriminator = LinearIQDiscriminator(cal_results, + self.qubits, + ['00', '11']) + + d_filter = DiscriminationFilter(discriminator) + + self.assertEqual(cal_results.results[0].meas_level, 1) + new_results = d_filter.apply(cal_results) + + self.assertEqual(new_results.results[0].meas_level, 2) + + counts_00 = new_results.results[0].data.counts.to_dict()['0x0'] + counts_11 = new_results.results[1].data.counts.to_dict()['0x3'] + + self.assertEqual(counts_00, self.shots) + self.assertEqual(counts_11, self.shots) diff --git a/test/measurement/discriminator/test_iq_discriminator.py b/test/measurement/discriminator/test_iq_discriminator.py new file mode 100644 index 000000000..2c0883bdb --- /dev/null +++ b/test/measurement/discriminator/test_iq_discriminator.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +# pylint: disable=no-name-in-module + +""" +Test IQ discrimination fitters. +""" + +import unittest + +import test.utils as utils +import qiskit +from qiskit import Aer +from qiskit.ignis.measurement.discriminator.filters import DiscriminationFilter +from qiskit.ignis.measurement.discriminator.iq_discriminators import \ + LinearIQDiscriminator +from qiskit.ignis.mitigation.measurement import circuits +from qiskit.result.models import ExperimentResultData + + +class TestLinearIQDiscriminator(unittest.TestCase): + """ + Test methods of the IQ discriminators. + """ + + def setUp(self): + """ + Setup internal variables and a fake simulation. Aer is used to get the + structure of the qiskit.Result. The IQ data is generated using gaussian + random number generators. + """ + self.shots = 52 + self.qubits = [0, 1] + + meas_cal, _ = circuits.tensored_meas_cal([[0], [1]]) + + backend = Aer.get_backend('qasm_simulator') + job = qiskit.execute(meas_cal, backend=backend, shots=self.shots, + meas_level=1) + + self.cal_results = job.result() + + i0, q0, i1, q1 = 0., -1., 0., 1. + ground = utils.create_shots(i0, q0, 0.1, 0.1, self.shots, self.qubits) + excited = utils.create_shots(i1, q1, 0.1, 0.1, self.shots, self.qubits) + + self.cal_results.results[0].meas_level = 1 + self.cal_results.results[1].meas_level = 1 + self.cal_results.results[0].data = ExperimentResultData(memory=ground) + self.cal_results.results[1].data = ExperimentResultData(memory=excited) + + def test_get_xdata(self): + """ + Tests that the discriminator properly retrieves the x data from the + Qiskit result. + """ + discriminator = LinearIQDiscriminator(self.cal_results, + self.qubits, + ['00', '11']) + + xdata = discriminator.get_xdata(self.cal_results) + + self.assertEqual(len(xdata), self.shots*2) + self.assertEqual(len(xdata[0]), len(self.qubits) * 2) + + xdata = discriminator.get_xdata(self.cal_results, ['cal_00']) + + self.assertEqual(len(xdata), self.shots) + self.assertEqual(len(xdata[0]), 4) + + def test_get_ydata(self): + """ + Tests that the discriminator properly retrieves the y data from the + Qiskit calibration results. + """ + discriminator = LinearIQDiscriminator(self.cal_results, + self.qubits, + ['00', '11']) + + xdata = discriminator.get_xdata(self.cal_results) + ydata = discriminator.get_ydata(self.cal_results) + + self.assertEqual(len(xdata), len(ydata)) + + ydata = discriminator.get_ydata(self.cal_results, ['cal_00']) + + self.assertEqual(len(ydata), self.shots) + self.assertEqual(ydata[0], '00') + + def test_discrimination(self): + """ + Test that the discriminator can be trained on the simulated data and + that it can properly discriminate between ground and excited sates. + """ + i0, q0, i1, q1 = 0., -1., 0., 1. + discriminator = LinearIQDiscriminator(self.cal_results, + self.qubits, + ['00', '11']) + + excited_predicted = discriminator.discriminate([[i1, q1, i1, q1]]) + ground_predicted = discriminator.discriminate([[i0, q0, i0, q0]]) + + self.assertEqual(excited_predicted[0], '11') + self.assertEqual(ground_predicted[0], '00') + + def filter_and_discriminate(self): + """ + Test the process of discriminating and then applying the discriminator + using a filter. + """ + i0, q0, i1, q1 = 0., -1., 0., 1. + + discriminator = LinearIQDiscriminator(self.cal_results, + self.qubits, + ['00', '11']) + + iq_filter = DiscriminationFilter(discriminator) + + new_result = iq_filter.apply(self.cal_results) + + for nr in new_result.results: + self.assertEqual(nr.meas_level, 2) + + for state in new_result.results[0].data.counts.to_dict(): + self.assertEqual(state, '0x0') + + for state in new_result.results[1].data.counts.to_dict(): + self.assertEqual(state, '0x3') + + self.assertEqual(len(new_result.get_memory(0)), self.shots) + + self.qubits = [0] + + discriminator = LinearIQDiscriminator(self.cal_results, + self.qubits, + ['0', '1']) + + self.assertEqual(discriminator.discriminate([[i0, q0]])[0], '0') + self.assertEqual(discriminator.discriminate([[i1, q1]])[0], '1') diff --git a/test/measurement_calibration/__init__.py b/test/measurement_calibration/__init__.py index e69de29bb..428fe2e50 100644 --- a/test/measurement_calibration/__init__.py +++ b/test/measurement_calibration/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/measurement_calibration/test_meas.py b/test/measurement_calibration/test_meas.py index 40bc84fcd..f2ccdeac9 100644 --- a/test/measurement_calibration/test_meas.py +++ b/test/measurement_calibration/test_meas.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Test of measurement calibration: -1) Preparation of all 2 ** n basis states, generating the calibration circuits +1) Preparation of the basis states, generating the calibration circuits (without noise), computing the calibration matrices, and validating that they equal to the identity matrices @@ -15,9 +22,9 @@ the calibration output (without noise), and validating that it is equally distributed 3) Testing the the measurement calibration on a circuit -(without noise) verifying that it is close to the +(without noise), verifying that it is close to the expected (equally distributed) result -4) Testing MeasurementFitter on pre-generated data with noise +4) Testing the fitters on pre-generated data with noise """ import unittest @@ -27,9 +34,10 @@ import qiskit from qiskit import QuantumCircuit, ClassicalRegister, Aer from qiskit.ignis.mitigation.measurement \ - import (CompleteMeasFitter, - complete_meas_cal, + import (CompleteMeasFitter, TensoredMeasFitter, + complete_meas_cal, tensored_meas_cal, MeasurementFilter) +from qiskit.ignis.verification.tomography import count_keys class TestMeasCal(unittest.TestCase): @@ -51,7 +59,7 @@ def choose_calibration(nq, pattern_type): Args: nq: number of qubits - pattern_type: a pattern in range(1,2**nq) + pattern_type: a pattern in range(1, 2**nq) Returns: qubits: a list of qubits according to the given pattern @@ -108,8 +116,10 @@ def test_ideal_meas_cal(self): print("Testing %d qubit measurement calibration" % nq) for pattern_type in range(1, 2 ** nq): + # Generate the quantum register according to the pattern qubits, weight = self.choose_calibration(nq, pattern_type) + # Generate the calibration circuits meas_calibs, state_labels = \ complete_meas_cal(qubit_list=qubits, @@ -117,9 +127,8 @@ def test_ideal_meas_cal(self): # Perform an ideal execution on the generated circuits backend = Aer.get_backend('qasm_simulator') - qobj = qiskit.compile(meas_calibs, backend=backend, - shots=self.shots) - job = backend.run(qobj) + job = qiskit.execute(meas_calibs, backend=backend, + shots=self.shots) cal_results = job.result() # Make a calibration matrix @@ -142,7 +151,7 @@ def test_ideal_meas_cal(self): results_dict, results_list = \ self.generate_ideal_results(state_labels, weight) - # output the filter + # Output the filter meas_filter = meas_cal.filter # Apply the calibration matrix to results @@ -172,77 +181,76 @@ def test_meas_cal_on_circuit(self): """ print("Testing measurement calibration on a circuit") - # Choose all triples from 5 qubits - for q1 in range(5): - for q2 in range(q1+1, 5): - for q3 in range(q2+1, 5): - # Generate the quantum register according to the pattern - qr = qiskit.QuantumRegister(5) - # Generate the calibration circuits - meas_calibs, state_labels = \ - complete_meas_cal(qubit_list=[1, 2, 3], - qr=qr) - - # Run the calibration circuits - backend = Aer.get_backend('qasm_simulator') - qobj = qiskit.compile(meas_calibs, backend=backend, - shots=self.shots) - job = backend.run(qobj) - cal_results = job.result() - - # Make a calibration matrix - meas_cal = CompleteMeasFitter(cal_results, state_labels) - # Calculate the fidelity - fidelity = meas_cal.readout_fidelity() - - # Make a 3Q GHZ state - cr = ClassicalRegister(3) - ghz = QuantumCircuit(qr, cr) - ghz.h(qr[q1]) - ghz.cx(qr[q1], qr[q2]) - ghz.cx(qr[q2], qr[q3]) - ghz.measure(qr[q1], cr[0]) - ghz.measure(qr[q2], cr[1]) - ghz.measure(qr[q3], cr[2]) - - qobj = qiskit.compile([ghz], backend=backend, - shots=self.shots) - job = backend.run(qobj) - results = job.result() - - # Predicted equally distributed results - predicted_results = {'000': 0.5, - '111': 0.5} - - meas_filter = meas_cal.filter - - # Calculate the results after mitigation - output_results_pseudo_inverse = meas_filter.apply( - results, method='pseudo_inverse').get_counts(0) - output_results_least_square = meas_filter.apply( - results, method='least_squares').get_counts(0) - - # Compare with expected fidelity and expected results - self.assertAlmostEqual(fidelity, 1.0) - self.assertAlmostEqual( - output_results_pseudo_inverse['000']/self.shots, - predicted_results['000'], - places=1) - - self.assertAlmostEqual( - output_results_least_square['000']/self.shots, - predicted_results['000'], - places=1) - - self.assertAlmostEqual( - output_results_pseudo_inverse['111']/self.shots, - predicted_results['111'], - places=1) - - self.assertAlmostEqual( - output_results_least_square['111']/self.shots, - predicted_results['111'], - places=1) + # Choose 3 qubits + q1 = 1 + q2 = 2 + q3 = 3 + + # Generate the quantum register according to the pattern + qr = qiskit.QuantumRegister(5) + # Generate the calibration circuits + meas_calibs, state_labels = \ + complete_meas_cal(qubit_list=[1, 2, 3], + qr=qr) + + # Run the calibration circuits + backend = Aer.get_backend('qasm_simulator') + job = qiskit.execute(meas_calibs, backend=backend, + shots=self.shots) + cal_results = job.result() + + # Make a calibration matrix + meas_cal = CompleteMeasFitter(cal_results, state_labels) + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity() + + # Make a 3Q GHZ state + cr = ClassicalRegister(3) + ghz = QuantumCircuit(qr, cr) + ghz.h(qr[q1]) + ghz.cx(qr[q1], qr[q2]) + ghz.cx(qr[q2], qr[q3]) + ghz.measure(qr[q1], cr[0]) + ghz.measure(qr[q2], cr[1]) + ghz.measure(qr[q3], cr[2]) + + job = qiskit.execute([ghz], backend=backend, + shots=self.shots) + results = job.result() + + # Predicted equally distributed results + predicted_results = {'000': 0.5, + '111': 0.5} + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + results, method='pseudo_inverse').get_counts(0) + output_results_least_square = meas_filter.apply( + results, method='least_squares').get_counts(0) + + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, 1.0) + self.assertAlmostEqual( + output_results_pseudo_inverse['000']/self.shots, + predicted_results['000'], + places=1) + + self.assertAlmostEqual( + output_results_least_square['000']/self.shots, + predicted_results['000'], + places=1) + + self.assertAlmostEqual( + output_results_pseudo_inverse['111']/self.shots, + predicted_results['111'], + places=1) + + self.assertAlmostEqual( + output_results_least_square['111']/self.shots, + predicted_results['111'], + places=1) def test_meas_fitter_with_noise(self): """ @@ -298,6 +306,217 @@ def test_meas_fitter_with_noise(self): output_results_least_square['111'], tests[tst_index]['results_least_square']['111'], places=0) + def test_ideal_tensored_meas_cal(self): + """ + Test ideal execution, without noise + """ + + mit_pattern = [[1, 2], [3, 4, 5], [6]] + + # Generate the calibration circuits + meas_calibs, _ = tensored_meas_cal(mit_pattern=mit_pattern) + + # Perform an ideal execution on the generated circuits + backend = Aer.get_backend('qasm_simulator') + cal_results = qiskit.execute( + meas_calibs, backend=backend, shots=self.shots).result() + + # Make calibration matrices + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + + # Assert that the calibration matrices are equal to identity + cal_matrices = meas_cal.cal_matrices + self.assertEqual(len(mit_pattern), len(cal_matrices), + 'Wrong number of calibration matrices') + for qubit_list, cal_mat in zip(mit_pattern, cal_matrices): + IdentityMatrix = np.identity(2**len(qubit_list)) + self.assertListEqual(cal_mat.tolist(), + IdentityMatrix.tolist(), + 'Error: the calibration matrix is \ + not equal to identity') + + # Assert that the readout fidelity is equal to 1 + self.assertEqual(meas_cal.readout_fidelity(), 1.0, + 'Error: the average fidelity \ + is not equal to 1') + + # Generate ideal (equally distributed) results + results_dict, _ = \ + self.generate_ideal_results(count_keys(6), 6) + + # Output the filter + meas_filter = meas_cal.filter + + # Apply the calibration matrix to results + # in list and dict forms using different methods + results_dict_1 = meas_filter.apply(results_dict, + method='least_squares') + results_dict_0 = meas_filter.apply(results_dict, + method='pseudo_inverse') + + # Assert that the results are equally distributed + self.assertDictEqual(results_dict, results_dict_0) + round_results = {} + for key, val in results_dict_1.items(): + round_results[key] = np.round(val) + self.assertDictEqual(results_dict, round_results) + + def test_tensored_meas_cal_on_circuit(self): + """ + Test an execution on a circuit + """ + + mit_pattern = [[2], [4, 1]] + + qr = qiskit.QuantumRegister(5) + # Generate the calibration circuits + meas_calibs, _ = tensored_meas_cal(mit_pattern, qr=qr) + + # Run the calibration circuits + backend = Aer.get_backend('qasm_simulator') + cal_results = qiskit.execute(meas_calibs, backend=backend, + shots=self.shots).result() + + # Make a calibration matrix + meas_cal = TensoredMeasFitter(cal_results, + mit_pattern=mit_pattern) + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity(0)*meas_cal.readout_fidelity(1) + + # Make a 3Q GHZ state + cr = ClassicalRegister(3) + ghz = QuantumCircuit(qr, cr) + ghz.h(qr[2]) + ghz.cx(qr[2], qr[4]) + ghz.cx(qr[2], qr[1]) + ghz.measure(qr[2], cr[0]) + ghz.measure(qr[4], cr[1]) + ghz.measure(qr[1], cr[2]) + + results = qiskit.execute([ghz], backend=backend, + shots=self.shots).result() + + # Predicted equally distributed results + predicted_results = {'000': 0.5, + '111': 0.5} + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + results, method='pseudo_inverse').get_counts(0) + output_results_least_square = meas_filter.apply( + results, method='least_squares').get_counts(0) + + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, 1.0) + self.assertAlmostEqual( + output_results_pseudo_inverse['000']/self.shots, + predicted_results['000'], + places=1) + + self.assertAlmostEqual( + output_results_least_square['000']/self.shots, + predicted_results['000'], + places=1) + + self.assertAlmostEqual( + output_results_pseudo_inverse['111']/self.shots, + predicted_results['111'], + places=1) + + self.assertAlmostEqual( + output_results_least_square['111']/self.shots, + predicted_results['111'], + places=1) + + def test_tensored_meas_fitter_with_noise(self): + """ + Test the TensoredFitter with noise + """ + + # pre-generated results with noise + # load from pickled file + fo = open(os.path.join( + os.path.dirname(__file__), 'test_tensored_meas_results.pkl'), 'rb') + pickled_info = pickle.load(fo) + fo.close() + + meas_cal = TensoredMeasFitter( + pickled_info['cal_results'], + mit_pattern=pickled_info['mit_pattern']) + + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity(0)*meas_cal.readout_fidelity(1) + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, + pickled_info['fidelity'], + places=0) + + meas_filter = meas_cal.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + pickled_info['results'].get_counts(0), method='pseudo_inverse') + output_results_least_square = meas_filter.apply( + pickled_info['results'], method='least_squares') + + self.assertAlmostEqual( + output_results_pseudo_inverse['000'], + pickled_info['results_pseudo_inverse']['000'], places=0) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)['000'], + pickled_info['results_least_square']['000'], places=0) + + self.assertAlmostEqual( + output_results_pseudo_inverse['111'], + pickled_info['results_pseudo_inverse']['111'], places=0) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)['111'], + pickled_info['results_least_square']['111'], places=0) + + substates_list = [] + for qubit_list in pickled_info['mit_pattern']: + substates_list.append(count_keys(len(qubit_list))[::-1]) + + fitter_other_order = TensoredMeasFitter( + pickled_info['cal_results'], + substate_labels_list=substates_list, + mit_pattern=pickled_info['mit_pattern']) + + fidelity = fitter_other_order.readout_fidelity(0) * \ + meas_cal.readout_fidelity(1) + + self.assertAlmostEqual(fidelity, + pickled_info['fidelity'], + places=0) + + meas_filter = fitter_other_order.filter + + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + pickled_info['results'].get_counts(0), method='pseudo_inverse') + output_results_least_square = meas_filter.apply( + pickled_info['results'], method='least_squares') + + self.assertAlmostEqual( + output_results_pseudo_inverse['000'], + pickled_info['results_pseudo_inverse']['000'], places=0) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)['000'], + pickled_info['results_least_square']['000'], places=0) + + self.assertAlmostEqual( + output_results_pseudo_inverse['111'], + pickled_info['results_pseudo_inverse']['111'], places=0) + + self.assertAlmostEqual( + output_results_least_square.get_counts(0)['111'], + pickled_info['results_least_square']['111'], places=0) + if __name__ == '__main__': unittest.main() diff --git a/test/measurement_calibration/test_tensored_meas_results.pkl b/test/measurement_calibration/test_tensored_meas_results.pkl new file mode 100644 index 000000000..679872a98 Binary files /dev/null and b/test/measurement_calibration/test_tensored_meas_results.pkl differ diff --git a/test/quantum_volume/__init__.py b/test/quantum_volume/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/quantum_volume/qv_exp_results.pkl b/test/quantum_volume/qv_exp_results.pkl new file mode 100644 index 000000000..e2ec56fd8 Binary files /dev/null and b/test/quantum_volume/qv_exp_results.pkl differ diff --git a/test/quantum_volume/qv_ideal_results.pkl b/test/quantum_volume/qv_ideal_results.pkl new file mode 100644 index 000000000..e03e644c3 Binary files /dev/null and b/test/quantum_volume/qv_ideal_results.pkl differ diff --git a/test/quantum_volume/test_qv.py b/test/quantum_volume/test_qv.py new file mode 100644 index 000000000..4a2083f6e --- /dev/null +++ b/test/quantum_volume/test_qv.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=undefined-loop-variable + +""" +Run through Quantum volume +""" + +import unittest +import os +import pickle +import qiskit.ignis.verification.quantum_volume as qv + + +class TestQV(unittest.TestCase): + """ The test class """ + + def test_qv_circuits(self): + """ Test circuit generation """ + + # Qubit list + qubit_lists = [[0, 1, 2], [0, 1, 2, 4], [0, 1, 2, 4, 7]] + ntrials = 5 + + qv_circs, _ = qv.qv_circuits(qubit_lists, ntrials) + + self.assertEqual(len(qv_circs), ntrials, + "Error: Not enough trials") + + self.assertEqual(len(qv_circs[0]), len(qubit_lists), + "Error: Not enough circuits for the " + "number of specified qubit lists") + + def test_qv_fitter(self): + + """ Test the fitter with some pickled result data""" + + os.path.join(os.path.dirname(__file__), + 'test_fitter_results_2.pkl') + + f0 = open(os.path.join(os.path.dirname(__file__), + 'qv_ideal_results.pkl'), 'rb') + ideal_results = pickle.load(f0) + f0.close() + + f0 = open(os.path.join(os.path.dirname(__file__), + 'qv_exp_results.pkl'), 'rb') + exp_results = pickle.load(f0) + f0.close() + + qubit_lists = [[0, 1, 3], [0, 1, 3, 5], [0, 1, 3, 5, 7], + [0, 1, 3, 5, 7, 10]] + + qv_fitter = qv.QVFitter(qubit_lists=qubit_lists) + qv_fitter.add_statevectors(ideal_results) + qv_fitter.add_data(exp_results) + + qv_success_list = qv_fitter.qv_success() + self.assertFalse(qv_success_list[0][0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/rb/__init__.py b/test/rb/__init__.py index e69de29bb..428fe2e50 100644 --- a/test/rb/__init__.py +++ b/test/rb/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/rb/test_clifford.py b/test/rb/test_clifford.py index 17269b5c7..98b72f4dd 100644 --- a/test/rb/test_clifford.py +++ b/test/rb/test_clifford.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Test Clifford functions: @@ -15,15 +22,13 @@ """ import unittest -import filecmp import random import os -import tempfile import numpy as np # Import the clifford_utils functions from qiskit.ignis.verification.randomized_benchmarking \ - import clifford_utils as clutils + import CliffordUtils as clutils class TestClifford(unittest.TestCase): @@ -36,37 +41,33 @@ def setUp(self): """ self.number_of_tests = 20 # number of pseudo-random seeds self.max_nq = 2 # maximal number of qubits to check + self.clutils = clutils() def test_tables(self): """ test: generating the tables for 1 and 2 qubits """ - test_tables_fd, test_tables_file_path = tempfile.mkstemp() - self.addCleanup(os.remove, test_tables_file_path) - with os.fdopen(test_tables_fd, mode='w') as test_tables_file: - test_tables_file.write( - "test: generating the clifford group table for 1 qubit:\n") - clifford1 = clutils.clifford1_gates_table() - test_tables_file.write(str(len(clifford1))) - test_tables_file.write("\n") - test_tables_file.write(str(sorted(clifford1.values()))) - test_tables_file.write("\n") - test_tables_file.write( - "-------------------------------------------------------\n") + test_tables_content = [] + test_tables_content.append( + "test: generating the clifford group table for 1 qubit:\n") + clifford1 = self.clutils.clifford1_gates_table() + test_tables_content.append(str(len(clifford1)) + '\n') + test_tables_content.append(str(sorted(clifford1.values())) + '\n') + test_tables_content.append( + "-------------------------------------------------------\n") - test_tables_file.write( - "test: generating the clifford group table for 2 qubits:\n") - clifford2 = clutils.clifford2_gates_table() - test_tables_file.write(str(len(clifford2))) - test_tables_file.write("\n") - test_tables_file.write(str(sorted(clifford2.values()))) - test_tables_file.write("\n") + test_tables_content.append( + "test: generating the clifford group table for 2 qubits:\n") + clifford2 = self.clutils.clifford2_gates_table() + test_tables_content.append(str(len(clifford2)) + '\n') + test_tables_content.append(str(sorted(clifford2.values())) + '\n') expected_file_path = os.path.join( os.path.dirname(__file__), 'test_tables_expected.txt') - self.assertTrue( - filecmp.cmp(test_tables_file_path, expected_file_path), - "Error: tables on 1 and 2 qubits are not the same") + with open(expected_file_path, 'r') as fd: + expected_file_content = fd.readlines() + self.assertEqual(expected_file_content, test_tables_content, + "Error: tables on 1 and 2 qubits are not the same") def test_random_and_inverse(self): """ @@ -74,42 +75,39 @@ def test_random_and_inverse(self): and computing its inverse """ clifford_tables = [[]]*self.max_nq - clifford_tables[0] = clutils.clifford1_gates_table() - clifford_tables[1] = clutils.clifford2_gates_table() - test_random_fd, test_random_file_path = tempfile.mkstemp() - self.addCleanup(os.remove, test_random_file_path) - with os.fdopen(test_random_fd, mode='w') as test_random_file: - - # test: generating a pseudo-random Clifford using tables - - # 1&2 qubits and computing its inverse - for nq in range(1, 1+self.max_nq): - for i in range(0, self.number_of_tests): - my_seed = i - np.random.seed(my_seed) - random.seed(my_seed) - test_random_file.write( - "test: generating a pseudo-random clifford using the " - "tables - %d qubit - seed=%d:\n" % (nq, my_seed)) - cliff_nq = clutils.random_clifford_gates(nq) - test_random_file.write(str(cliff_nq)) - test_random_file.write("\n") - test_random_file.write( - "test: inverting a pseudo-random clifford using the " - "tables - %d qubit - seed=%d:\n" % (nq, my_seed)) - inv_cliff_nq = clutils.find_inverse_clifford_gates( - nq, cliff_nq) - test_random_file.write(str(inv_cliff_nq)) - test_random_file.write("\n") - test_random_file.write( - "-----------------------------------------------------" - "--\n") + clifford_tables[0] = self.clutils.clifford1_gates_table() + clifford_tables[1] = self.clutils.clifford2_gates_table() + test_random_file_content = [] + # test: generating a pseudo-random Clifford using tables - + # 1&2 qubits and computing its inverse + for nq in range(1, 1+self.max_nq): + for i in range(0, self.number_of_tests): + my_seed = i + np.random.seed(my_seed) + random.seed(my_seed) + test_random_file_content.append( + "test: generating a pseudo-random clifford using the " + "tables - %d qubit - seed=%d:\n" % (nq, my_seed)) + cliff_nq = self.clutils.random_gates(nq) + test_random_file_content.append(str(cliff_nq) + '\n') + test_random_file_content.append( + "test: inverting a pseudo-random clifford using the " + "tables - %d qubit - seed=%d:\n" % (nq, my_seed)) + inv_cliff_nq = self.clutils.find_inverse_gates( + nq, cliff_nq) + test_random_file_content.append(str(inv_cliff_nq) + '\n') + test_random_file_content.append( + "-----------------------------------------------------" + "--\n") expected_file_path = os.path.join( os.path.dirname(__file__), 'test_random_expected.txt') - self.assertTrue( - filecmp.cmp(test_random_file_path, expected_file_path), - "Error: random and/or inverse cliffords are not the same") + with open(expected_file_path, 'r') as fd: + expected_file_content = fd.readlines() + self.assertEqual(expected_file_content, test_random_file_content, + "Error: random and/or inverse cliffords are not " + "the same") if __name__ == '__main__': diff --git a/test/rb/test_fitter_coherent_purity_results.pkl b/test/rb/test_fitter_coherent_purity_results.pkl new file mode 100644 index 000000000..064eba91b Binary files /dev/null and b/test/rb/test_fitter_coherent_purity_results.pkl differ diff --git a/test/rb/test_fitter_interleaved_results.pkl b/test/rb/test_fitter_interleaved_results.pkl new file mode 100644 index 000000000..89793ee48 Binary files /dev/null and b/test/rb/test_fitter_interleaved_results.pkl differ diff --git a/test/rb/test_fitter_original_results.pkl b/test/rb/test_fitter_original_results.pkl new file mode 100644 index 000000000..5b4e72ade Binary files /dev/null and b/test/rb/test_fitter_original_results.pkl differ diff --git a/test/rb/test_fitter_purity_results.pkl b/test/rb/test_fitter_purity_results.pkl new file mode 100644 index 000000000..90490bcbc Binary files /dev/null and b/test/rb/test_fitter_purity_results.pkl differ diff --git a/test/rb/test_fitters.py b/test/rb/test_fitters.py index 369791979..0f0ac7525 100644 --- a/test/rb/test_fitters.py +++ b/test/rb/test_fitters.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. """ Test the fitters @@ -15,7 +22,8 @@ import numpy as np -from qiskit.ignis.verification.randomized_benchmarking import RBFitter +from qiskit.ignis.verification.randomized_benchmarking import \ + RBFitter, InterleavedRBFitter, PurityRBFitter class TestFitters(unittest.TestCase): @@ -68,13 +76,13 @@ def test_fitters(self): 'params_err': np.array([0.0065886, 0.00046714, 0.00556488]), 'epc': 0.014534104912075935, - 'epc_err': 6.923601336318206e-06}, + 'epc_err': 0.0003572769714798349}, {'params': np.array([0.49507094, 0.99354093, 0.50027262]), 'params_err': np.array([0.0146191, 0.0004157, 0.01487439]), 'epc': 0.0032295343343508587, - 'epc_err': 1.3512528169626024e-06}] + 'epc_err': 0.00020920242080699664}] }}, { 'rb_opts': { 'xdata': np.array([[1, 21, 41, 61, 81, 101, 121, 141, 161, @@ -100,7 +108,7 @@ def test_fitters(self): 'params_err': np.array([0.08843152, 0.00107311, 0.09074325]), 'epc': 0.0024089464034862673, - 'epc_err': 2.5975709525210163e-06}]}}] + 'epc_err': 0.0005391508310961153}]}}] for tst_index, tst in enumerate(tests): fo = open(tst['results_file'], 'rb') @@ -108,8 +116,13 @@ def test_fitters(self): fo.close() # RBFitter class - rb_fit = RBFitter(results_list, tst['rb_opts']['xdata'], + rb_fit = RBFitter(results_list[0], tst['rb_opts']['xdata'], tst['rb_opts']['rb_pattern']) + + # add the seeds in reverse order + for seedind in range(len(results_list)-1, 0, -1): + rb_fit.add_data([results_list[seedind]]) + ydata = rb_fit.ydata fit = rb_fit.fit @@ -146,6 +159,381 @@ def test_fitters(self): tst['expected']['fit'][i]['epc_err']), 'Incorrect EPC error in test no. ' + str(tst_index)) + def test_interleaved_fitters(self): + """ Test the interleaved fitters """ + + # Use pickled results files + + tests_interleaved = \ + [{ + 'rb_opts': { + 'xdata': np.array([[1, 11, 21, 31, 41, + 51, 61, 71, 81, 91], + [3, 33, 63, 93, 123, + 153, 183, 213, 243, 273]]), + 'rb_pattern': [[0, 2], [1]], + 'shots': 200}, + 'original_results_file': + os.path.join( + os.path.dirname(__file__), + 'test_fitter_original_results.pkl'), + 'interleaved_results_file': + os.path.join( + os.path.dirname(__file__), + 'test_fitter_interleaved_results.pkl'), + 'expected': { + 'original_ydata': + [{'mean': np.array([0.9775, 0.79, 0.66, + 0.5775, 0.5075, 0.4825, + 0.4075, 0.3825, + 0.3925, 0.325]), + 'std': np.array([0.0125, 0.02, 0.01, + 0.0125, 0.0025, + 0.0125, 0.0225, 0.0325, + 0.0425, 0.])}, + {'mean': np.array([0.985, 0.9425, 0.8875, + 0.8225, 0.775, 0.7875, + 0.7325, 0.705, + 0.69, 0.6175]), + 'std': np.array([0.005, 0.0125, 0.0025, + 0.0025, 0.015, 0.0125, + 0.0075, 0.01, + 0.02, 0.0375])}], + 'interleaved_ydata': + [{'mean': np.array([0.955, 0.7425, 0.635, + 0.4875, 0.44, 0.3625, + 0.3575, 0.2875, + 0.2975, 0.3075]), + 'std': np.array([0., 0.0025, 0.015, + 0.0075, 0.055, + 0.0075, 0.0075, 0.0025, + 0.0025, 0.0075])}, + {'mean': np.array([0.9775, 0.85, 0.77, + 0.7775, 0.6325, + 0.615, 0.64, 0.6125, + 0.535, 0.55]), + 'std': np.array([0.0075, 0.005, 0.01, + 0.0025, 0.0175, 0.005, + 0.01, 0.0075, + 0.01, 0.005])}], + 'joint_fit': [ + {'alpha': 0.9707393978697902, + 'alpha_err': 0.0028343593038762326, + 'alpha_c': 0.9661036105117012, + 'alpha_c_err': 0.003096602375173838, + 'epc_est': 0.003581641505636224, + 'epc_est_err': 0.0032362911276774308, + 'systematic_err': 0.04030926168967841, + 'systematic_err_L': -0.03672762018404219, + 'systematic_err_R': 0.043890903195314634}, + {'alpha': 0.9953124384370953, + 'alpha_err': 0.0014841466685991903, + 'alpha_c': 0.9955519189829325, + 'alpha_c_err': 0.002194868426034655, + 'epc_est': -0.00012030420629183247, + 'epc_est_err': 0.001331116936065506, + 'systematic_err': 0.004807865769196562, + 'systematic_err_L': -0.0049281699754883945, + 'systematic_err_R': 0.00468756156290473}] + }}] + + for tst_index, tst in enumerate(tests_interleaved): + fo = open(tst['original_results_file'], 'rb') + original_result_list = pickle.load(fo) + fo.close() + + fo = open(tst['interleaved_results_file'], 'rb') + interleaved_result_list = pickle.load(fo) + fo.close() + + # InterleavedRBFitter class + joint_rb_fit = InterleavedRBFitter( + original_result_list, interleaved_result_list, + tst['rb_opts']['xdata'], tst['rb_opts']['rb_pattern']) + + joint_fit = joint_rb_fit.fit_int + ydata_original = joint_rb_fit.ydata[0] + ydata_interleaved = joint_rb_fit.ydata[1] + + for i, _ in enumerate(ydata_original): + self.assertTrue(all(np.isclose(a, b) for a, b in + zip(ydata_original[i]['mean'], + tst['expected']['original_ydata'] + [i]['mean'])), + 'Incorrect mean in original data test no. ' + + str(tst_index)) + if tst['expected']['original_ydata'][i]['std'] is None: + self.assertIsNone( + ydata_original[i]['std'], + 'Incorrect std in original data test no. ' + + str(tst_index)) + else: + self.assertTrue( + all(np.isclose(a, b) for a, b in zip( + ydata_original[i]['std'], + tst['expected']['original_ydata'][i]['std'])), + 'Incorrect std in original data test no. ' + + str(tst_index)) + + for i, _ in enumerate(ydata_interleaved): + self.assertTrue(all(np.isclose(a, b) for a, b in + zip(ydata_interleaved[i]['mean'], + tst['expected']['interleaved_ydata'] + [i]['mean'])), + 'Incorrect mean in interleaved data test no. ' + + str(tst_index)) + if tst['expected']['interleaved_ydata'][i]['std'] is None: + self.assertIsNone( + ydata_interleaved[i]['std'], + 'Incorrect std in interleaved data test no. ' + + str(tst_index)) + else: + self.assertTrue( + all(np.isclose(a, b) for a, b in zip( + ydata_interleaved[i]['std'], + tst['expected']['interleaved_ydata'] + [i]['std'])), + 'Incorrect std in interleaved data test no. ' + + str(tst_index)) + + for i, _ in enumerate(joint_fit): + self.assertTrue( + np.isclose(joint_fit[i]['alpha'], + tst['expected']['joint_fit'] + [i]['alpha']), + 'Incorrect fit parameter alpha in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['alpha_err'], + tst['expected']['joint_fit'] + [i]['alpha_err']), + 'Incorrect fit parameter alpha_err in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['alpha_c'], + tst['expected']['joint_fit'] + [i]['alpha_c']), + 'Incorrect fit parameter alpha_c in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['alpha_c_err'], + tst['expected']['joint_fit'] + [i]['alpha_c_err']), + 'Incorrect fit parameter alpha_c_err in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['epc_est'], + tst['expected']['joint_fit'] + [i]['epc_est']), + 'Incorrect fit parameter epc_est in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['epc_est_err'], + tst['expected']['joint_fit'] + [i]['epc_est_err']), + 'Incorrect fit parameter epc_est_err in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['systematic_err'], + tst['expected']['joint_fit'] + [i]['systematic_err']), + 'Incorrect fit parameter systematic_err in test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['systematic_err_R'], + tst['expected']['joint_fit'] + [i]['systematic_err_R']), + 'Incorrect fit parameter systematic_err_R in ' + 'test no. ' + str(tst_index)) + self.assertTrue( + np.isclose(joint_fit[i]['systematic_err_L'], + tst['expected']['joint_fit'] + [i]['systematic_err_L']), + 'Incorrect fit parameter systematic_err_L ' + 'in test no. ' + str(tst_index)) + + def test_purity_fitters(self): + """ Test the purity fitters """ + + # Use pickled results files + + tests_purity = \ + [{ + 'npurity': 9, + 'rb_opts': { + 'xdata': np.array([[1, 21, 41, 61, 81, 101, 121, + 141, 161, 181], + [1, 21, 41, 61, 81, 101, 121, + 141, 161, 181]]), + 'rb_pattern': [[0, 1], [2, 3]], + 'shots': 200}, + 'results_file': os.path.join( + os.path.dirname(__file__), + 'test_fitter_purity_results.pkl'), + 'expected': { + 'ydata': + [{'mean': np.array([0.92534849, 0.51309098, + 0.3622178, 0.29969053, + 0.26635693, 0.25874519, + 0.25534863, 0.25298818, + 0.25352012, 0.2523394]), + 'std': np.array([0.01314403, 0.00393961, + 0.01189933, 0.00936296, + 0.00149143, 0.00248324, + 0.00162298, 0.00047547, + 0.00146307, 0.00104081])}, + {'mean': np.array([0.92369652, 0.52535891, + 0.36284821, 0.28978369, + 0.26764608, 0.26141492, + 0.25365907, 0.25399547, + 0.25308856, 0.25243922]), + 'std': np.array([0.01263948, 0.0139054, + 0.00774744, 0.00514974, + 0.00110454, 0.00185583, + 0.00103562, 0.00108479, + 0.00032715, 0.00067735])}], + 'fit': + [{'params': np.array([0.70657607, 0.97656138, + 0.25222978]), + 'params_err': np.array([0.00783723, 0.00028377, + 0.00026126]), + 'epc': 0.034745905818288264, + 'epc_err': 0.0004410703043924575, + 'pepc': 0.017578966279446773, + 'pepc_err': 0.00021793530767170674}, + {'params': np.array([0.70689622, 0.97678485, + 0.25258977]), + 'params_err': np.array([0.01243358, 0.00037419, + 0.00027745]), + 'epc': 0.03441852175571036, + 'epc_err': 0.0005814179624217788, + 'pepc': 0.017411364623216907, + 'pepc_err': 0.00028731473935779276}] + }}, + { + 'npurity': 9, + 'rb_opts': { + 'xdata': np.array([[1, 21, 41, 61, 81, 101, 121, + 141, 161, 181], + [1, 21, 41, 61, 81, 101, 121, + 141, 161, 181]]), + 'rb_pattern': [[0, 1], [2, 3]], + 'shots': 200}, + 'results_file': os.path.join( + os.path.dirname(__file__), + 'test_fitter_coherent_purity_results.pkl'), + 'expected': { + 'ydata': + [{'mean': np.array([1.03547598, 1.00945614, + 0.9874103, 0.99794296, + 0.98926947, 0.98898662, + 0.9908188, 1.04339706, + 1.02311855, 1.02636139]), + 'std': np.array([0.00349072, 0.05013115, + 0.01657108, 0.03048466, + 0.03496286, 0.02572242, + 0.03661921, 0.02406485, + 0.04192087, 0.05903551])}, + {'mean': np.array([1.04122543, 0.98568824, + 0.98702183, 1.00184751, + 1.02116973, 0.98867042, + 1.06620605, 1.11332653, + 1.04427034, 1.0687145]), + 'std': np.array([0.00519259, 0.02815319, + 0.06940576, 0.0232619, + 0.0442728, 0.05649533, + 0.05882039, 0.13732109, + 0.06189085, 0.0890274])}], + 'fit': + [{'params': np.array([0.04050766, 0.91275946, + 1.00172827]), + 'params_err': np.array([0.09520572, 1.04827404, + 0.00820391]), + 'epc': 0.12515262778294844, + 'epc_err': 1.8031488429069056, + 'pepc': 0.06543040590251992, + 'pepc_err': 0.8613501881980827}, + {'params': np.array([0.07347761, 0.68002963, + 1.00724559]), + 'params_err': np.array([1.20673822e+04, + 4.60490058e+04, + 1.15476367e-02]), + 'epc': 0.4031697796194298, + 'epc_err': 123174.20450564621, + 'pepc': 0.23997777961599784, + 'pepc_err': 50787.13189860349}] + }}] + + for tst_index, tst in enumerate(tests_purity[0:1]): + fo = open(tst['results_file'], 'rb') + purity_result_list = pickle.load(fo) + fo.close() + + # PurityRBFitter class + rbfit_purity = PurityRBFitter(purity_result_list, + tst['npurity'], + tst['rb_opts']['xdata'], + tst['rb_opts']['rb_pattern']) + + ydata = rbfit_purity.ydata + fit = rbfit_purity.fit + + for i, _ in enumerate(ydata): + self.assertTrue(all(np.isclose(a, b) for a, b in + zip(ydata[i]['mean'], + tst['expected']['ydata'][i]['mean'])), + 'Incorrect mean in purity data test no. ' + + str(tst_index)) + if tst['expected']['ydata'][i]['std'] is None: + self.assertIsNone( + ydata[i]['std'], + 'Incorrect std in purity data test no. ' + + str(tst_index)) + else: + self.assertTrue( + all(np.isclose(a, b) for a, b in zip( + ydata[i]['std'], + tst['expected']['ydata'][i]['std'])), + 'Incorrect std in purity data test no. ' + + str(tst_index)) + self.assertTrue( + all(np.isclose(a, b, atol=0.01) for a, b in zip( + fit[i]['params'], + tst['expected']['fit'][i]['params'])), + 'Incorrect fit parameters in purity data test no. ' + + str(tst_index) + ' ' + str(fit[i]['params']) + ' ' + + str(tst['expected']['fit'][i]['params'])) + self.assertTrue( + all(np.isclose(a, b, atol=0.01) for a, b in zip( + fit[i]['params_err'], + tst['expected']['fit'][i]['params_err'])), + 'Incorrect fit error in purity data test no. ' + + str(tst_index) + ' ' + str(fit[i]['params_err']) + + ' ' + str(tst['expected']['fit'][i]['params_err'])) + self.assertTrue(np.isclose(fit[i]['epc'], + tst['expected']['fit'][i]['epc'], + atol=0.01), + 'Incorrect EPC in purity data test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(fit[i]['epc_err'], + tst['expected']['fit'][i]['epc_err'], + atol=0.01), + 'Incorrect EPC error in purity data test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(fit[i]['pepc'], + tst['expected']['fit'][i]['pepc'], + atol=0.01), + 'Incorrect PEPC in purity data test no. ' + + str(tst_index)) + self.assertTrue( + np.isclose(fit[i]['pepc_err'], + tst['expected']['fit'][i]['pepc_err'], + atol=0.01), + 'Incorrect PEPC error in purity data test no. ' + + str(tst_index)) + if __name__ == '__main__': unittest.main() diff --git a/test/rb/test_rb.py b/test/rb/test_rb.py index ceff87ccd..05f8f23a9 100644 --- a/test/rb/test_rb.py +++ b/test/rb/test_rb.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=undefined-loop-variable @@ -12,12 +19,18 @@ and that it returns the identity """ -import unittest +import itertools import random +import unittest + +import numpy as np +from ddt import ddt, data, unpack + import qiskit import qiskit.ignis.verification.randomized_benchmarking as rb +@ddt class TestRB(unittest.TestCase): """ The test class """ @@ -31,8 +44,9 @@ def choose_pattern(pattern_type, nq): 1 - a list of lists of single qubits, for nq=5 it is [[1], [2], [3], [4], [5]] 2 - randomly choose a pattern which is a list of - two lists, for example for nq=5 it can be - [[4, 1, 2], [5, 3]] + two lists where the first one has 2 elements, + for example for nq=5 it can be + [[4, 1], [2, 5, 3]] :param nq: number of qubits :return: the pattern or None Returns None if the pattern type is not relevant to the @@ -42,23 +56,35 @@ def choose_pattern(pattern_type, nq): - for nq=2 this implies [[1], [2]], which is already tested when pattern_type = 1 + is_purity = True if the pattern fits for purity rb + (namely, all the patterns have the same dimension: + only 1-qubit, only 2-qubits etc.). ''' + is_purity = True if pattern_type == 0: res = [list(range(nq))] + if nq > 2: # since we only have 1-qubit and 2-qubit Cliffords + return None, None elif pattern_type == 1: if nq == 1: - return None + return None, None res = [[x] for x in range(nq)] else: if nq <= 2: - return None + return None, None shuffled_bits = list(range(nq)) random.shuffle(shuffled_bits) - split_loc = random.randint(1, nq-1) + # split_loc = random.randint(1, nq-1) + split_loc = 2 # deterministic test res = [shuffled_bits[:split_loc], shuffled_bits[split_loc:]] + # since we only have 1-qubit and 2-qubit Cliffords + if (split_loc > 2) | (nq-split_loc > 2): + return None, None + if 2*split_loc != nq: + is_purity = False - return res + return res, is_purity @staticmethod def choose_multiplier(mult_opt, len_pattern): @@ -76,7 +102,113 @@ def choose_multiplier(mult_opt, len_pattern): return res - def verify_circuit(self, circ, nq, rb_opts, vec_len, result, shots): + @staticmethod + def choose_interleaved_gates(rb_pattern): + ''' + :param rb_pattern: pattern for randomized benchmarking + :return: interleaved_gates: + A list of gates of Clifford elements that + will be interleaved (for interleaved randomized benchmarking) + The length of the list would equal the length of the rb_pattern. + ''' + pattern_sizes = [len(pat) for pat in rb_pattern] + interleaved_gates = [] + for (_, nq) in enumerate(pattern_sizes): + gatelist = [] + # The interleaved gates contain x gate on each qubit + # and cx gate on each pair of consecutive qubits + for qubit in range(nq): + gatelist.append('x ' + str(qubit)) + for qubit_i in range(nq): + for qubit_j in range(qubit_i+1, nq): + gatelist.append('cx ' + str(qubit_i) + ' ' + str(qubit_j)) + interleaved_gates.append(gatelist) + return interleaved_gates + + @staticmethod + def update_interleaved_gates(gatelist, pattern): + ''' + :param gatelist: list of Clifford gates + :param pattern: pattern of indexes (from rb_pattern) + :return: updated_gatelist: list of Clifford gates + after the following updates: + - change the indexes from [0,1,...] + according to the pattern + ''' + updated_gatelist = [] + for op in gatelist: + split = op.split() + op_names = split[0] + # updating the qubit indexes according to the pattern + # given in rb_pattern + op_qubits = [str(pattern[int(x)]) for x in split[1:]] + updated_gatelist += [op_names + ' ' + + (' '.join(op_qubits))] + return updated_gatelist + + @staticmethod + def update_purity_gates(npurity, purity_ind, rb_pattern): + ''' + :param npurity: equals to 3^n + :param purity_ind: purity index in [0,3^n-1] + :param rb_pattern: rb pattern + :return: name_type: type of name for rb_circuit + (e.g. XY, ZZ etc.) + :return: gate_list: list of purity gates + (e.g 'rx 0', 'ry 1' etc.) according to rb_pattern + ''' + name_type = '' + ind_d = purity_ind + purity_qubit_num = 0 + gate_list = [] + while True: + purity_qubit_rot = np.mod(ind_d, 3) + ind_d = np.floor_divide(ind_d, 3) + if purity_qubit_rot == 0: + name_type += 'Z' + if purity_qubit_rot == 1: + name_type += 'X' + for pat in rb_pattern: + gate_list.append('rx ' + str(pat[purity_qubit_num])) + if purity_qubit_rot == 2: + name_type += 'Y' + for pat in rb_pattern: + gate_list.append('ry ' + str(pat[purity_qubit_num])) + + purity_qubit_num = purity_qubit_num + 1 + if ind_d == 0: + break + # padding the circuit name with Z's so that + # all circuits will have names of the same length + for _ in range(int(np.log(npurity)/np.log(3)) - + purity_qubit_num): + name_type += 'Z' + + return name_type, gate_list + + @staticmethod + def ops_to_gates(ops, op_index, stop_gate='barrier'): + ''' + :param ops: of the form circ.data + :param op_index: int, the operation index + :param stop_gate: the gate to stop + (e.g. barrier or measure) + :return: gatelist: a list of gates + :return: op_index: int, updated index + ''' + gatelist = [] + while ops[op_index][0].name != stop_gate: + gate = ops[op_index][0].name + for x in ops[op_index][1]: + gate += ' ' + str(x.index) + gatelist.append(gate) + op_index += 1 + # increment because of the barrier gate + op_index += 1 + return gatelist, op_index + + def verify_circuit(self, circ, nq, rb_opts, vec_len, result, shots, + is_interleaved=False): ''' For a single sequence, verifies that it meets the requirements: - Executing it on the ground state ends up in the ground state @@ -92,6 +224,7 @@ def verify_circuit(self, circ, nq, rb_opts, vec_len, result, shots): :param result: the output of the simulator when executing all the sequences on the ground state :param shots: the number of shots in the simulator execution + :param is_interleaved: True if this is an interleaved circuit ''' if not hasattr(rb_opts['length_multiplier'], "__len__"): @@ -107,91 +240,320 @@ def verify_circuit(self, circ, nq, rb_opts, vec_len, result, shots): for pat_index in range(len(rb_opts['rb_pattern'])): # for each Clifford... for _ in range(rb_opts['length_multiplier'][pat_index]): - # for each basis gate... - while ops[op_index].name != 'barrier': - # Verify that the gate acts on the correct qubits - # This happens if the sequence is composed of the - # correct sub-sequences, as specified by vec_len and - # rb_opts - self.assertTrue( - all(x[1] in rb_opts['rb_pattern'][pat_index] - for x in ops[op_index].qargs), - "Error: operation acts on incorrect qubits") - op_index += 1 - # increment because of the barrier gate - op_index += 1 + # if it is an interleaved RB circuit, + # then it has twice as many Cliffords + for _ in range(is_interleaved+1): + # for each basis gate... + # in case of align_cliffs we may have extra barriers + # (after another barrier) + if ops[op_index][0].name != 'barrier': + while ops[op_index][0].name != 'barrier': + # Verify that the gate acts + # on the correct qubits. + # This happens if the sequence is composed + # of the correct sub-sequences, + # as specified by vec_len and rb_opts + self.assertTrue( + all(x.index in rb_opts['rb_pattern'][ + pat_index] + for x in ops[op_index][1]), + "Error: operation acts on \ + incorrect qubits") + op_index += 1 + # increment because of the barrier gate + op_index += 1 # check if the ground state returns self.assertEqual(result. get_counts(circ)['{0:b}'.format(0).zfill(nq)], shots, "Error: %d qubit RB does not return the \ ground state back to the ground state" % nq) - def test_rb(self): + def compare_interleaved_circuit(self, original_circ, interleaved_circ, + nq, rb_opts_interleaved, vec_len): + ''' + Verifies that interleaved RB circuits meet the requirements: + - The non-interleaved Clifford gates are the same as the + original Clifford gates. + - The interleaved Clifford gates are the same as the ones + given in: rb_opts_interleaved['interleaved_gates']. + :param original_circ: original rb circuits + :param interleaved_circ: interleaved rb circuits + :param nq: number of qubits + :param rb_opts_interleaved: the specification that + generated the set of sequences which includes circ + :param vec_len: the expected length vector of circ + (one of rb_opts['length_vector']) + ''' + + if not hasattr(rb_opts_interleaved['length_multiplier'], "__len__"): + rb_opts_interleaved['length_multiplier'] = [ + rb_opts_interleaved['length_multiplier'] for i in range( + len(rb_opts_interleaved['rb_pattern']))] + + original_ops = original_circ.data + interleaved_ops = interleaved_circ.data + + original_op_index = 0 + interleaved_op_index = 0 + # for each cycle (the sequence should consist of vec_len cycles) + for _ in range(vec_len): + # for each component of the pattern... + for pat_index in range(len(rb_opts_interleaved['rb_pattern'])): + # updating the gates in: + # rb_opts_interleaved['interleaved_gates'] + updated_gatelist = self.update_interleaved_gates( + rb_opts_interleaved['interleaved_gates'] + [pat_index], rb_opts_interleaved['rb_pattern'][pat_index]) + # for each Clifford... + for _ in range(rb_opts_interleaved['length_multiplier'] + [pat_index]): + # original RB sequence + original_gatelist, original_op_index = \ + self.ops_to_gates(original_ops, + original_op_index) + # interleaved RB sequence + compared_gatelist, interleaved_op_index = \ + self.ops_to_gates(interleaved_ops, + interleaved_op_index) + + # Clifford gates in the interleaved RB sequence + # should be equal to original gates + self.assertEqual(original_gatelist, compared_gatelist, + "Error: The gates in the %d qubit \ + interleaved RB are not the same as \ + in the original RB circuits" % nq) + # Clifford gates in the interleaved RB sequence + # should be equal to the given gates in + # rb_opts_interleaved['interleaved_gates'] + # (after updating them) + interleaved_gatelist, interleaved_op_index = \ + self.ops_to_gates(interleaved_ops, + interleaved_op_index) + + self.assertEqual(interleaved_gatelist, updated_gatelist, + "Error: The interleaved gates in the \ + %d qubit interleaved RB are not the same \ + as given in interleaved_gates input" % nq) + + def compare_purity_circuits(self, original_circ, purity_circ, nq, + purity_ind, npurity, rb_opts_purity, vec_len): + ''' + Verifies that purity RB circuits meet the requirements: + - The Clifford gates are the same as the original Clifford gates. + - The last gates are either Rx or Ry or nothing + (depend on d) + :param original_circ: original rb circuits + :param purity_circ: purity rb circuits + :param nq: number of qubits + :param purity_ind: purity index in [0,3^n-1] + :param npurity: equal to 3^n + :param rb_opts_purity: the specification that + generated the set of sequences which includes circ + :param vec_len: the expected length vector of circ + (one of rb_opts['length_vector']) + ''' + + original_ops = original_circ.data + purity_ops = purity_circ.data + op_index = 0 + pur_index = 0 + + # for each cycle (the sequence should consist of vec_len cycles) + for _ in range(vec_len): + # for each component of the pattern... + for pat_index in range(len(rb_opts_purity['rb_pattern'])): + # for each Clifford... + for _ in range(rb_opts_purity['length_multiplier'][pat_index]): + # original RB sequence + original_gatelist, op_index = \ + self.ops_to_gates(original_ops, op_index) + # purity RB sequence + purity_gatelist, pur_index = \ + self.ops_to_gates(purity_ops, pur_index) + # Clifford gates in the purity RB sequence + # should be equal to original gates + self.assertEqual(original_gatelist, purity_gatelist, + "Error: The purity gates in the \ + %d qubit purity RB are not the same \ + as in the original RB circuits" % nq) + + # The last gate in the purity RB sequence + # should be equal to the inverse clifford + # with either Rx or Ry or nothing (depend on d) + # original last gate + original_gatelist, op_index = \ + self.ops_to_gates(original_ops, op_index, 'measure') + _, purity_gates = self.update_purity_gates( + npurity, purity_ind, rb_opts_purity['rb_pattern']) + original_gatelist = original_gatelist + purity_gates + # purity last gate + purity_gatelist, pur_index = \ + self.ops_to_gates(purity_ops, pur_index, 'measure') + self.assertEqual(original_gatelist, purity_gatelist, + "Error: The last purity gates in the \ + %d qubit purity RB are wrong" % nq) + + @data(*itertools.product([1, 2, 3, 4], range(3), range(2))) + @unpack + def test_rb(self, nq, pattern_type, multiplier_type): """ Main function of the test """ # Load simulator backend = qiskit.Aer.get_backend('qasm_simulator') - # Test up to 2 qubits - nq_list = [1, 2] - - for nq in nq_list: - - print("Testing %d qubit RB" % nq) - - for pattern_type in range(2): - for multiplier_type in range(2): - # See documentation of choose_pattern for the meaning of - # the different pattern types - - rb_opts = {} - rb_opts['nseeds'] = 3 - rb_opts['length_vector'] = [1, 3, 4, 7] - rb_opts['rb_pattern'] = self.choose_pattern( - pattern_type, nq) - # if the pattern type is not relevant for nq - if rb_opts['rb_pattern'] is None: - continue - rb_opts['length_multiplier'] = self.choose_multiplier( - multiplier_type, len(rb_opts['rb_pattern'])) - - # Generate the sequences - try: - rb_circs, _ = rb.randomized_benchmarking_seq(**rb_opts) - except OSError: - skip_msg = ('Skipping tests for %s qubits because ' - 'tables are missing' % str(nq)) - print(skip_msg) - continue - - # Perform an ideal execution on the generated sequences - # basis_gates = ['u1','u2','u3','cx'] # use U, CX for now - # Shelly: changed format to fit qiskit current version - basis_gates = 'u1, u2, u3, cx' - shots = 100 - result = [] - for seed in range(rb_opts['nseeds']): - result.append( - qiskit.execute(rb_circs[seed], backend=backend, - basis_gates=basis_gates, - shots=shots).result()) - - # Verify the generated sequences - for seed in range(rb_opts['nseeds']): - length_vec = rb_opts['length_vector'] - for circ_index, vec_len in enumerate(length_vec): - - self.assertEqual( - rb_circs[seed][circ_index].name, - 'rb_length_%d_seed_%d' % ( - circ_index, seed), - 'Error: incorrect circuit name') - self.verify_circuit(rb_circs[seed][circ_index], - nq, rb_opts, - vec_len, result[seed], shots) - - self.assertEqual(circ_index, len(rb_circs), - "Error: additional circuits exist") + # See documentation of choose_pattern for the meaning of + # the different pattern types + # Choose options for standard (simultaneous) RB: + rb_opts = {} + rb_opts['nseeds'] = 3 + rb_opts['length_vector'] = [1, 3, 4, 7] + rb_opts['rb_pattern'], is_purity = \ + self.choose_pattern(pattern_type, nq) + # if the pattern type is not relevant for nq + if rb_opts['rb_pattern'] is None: + raise unittest.SkipTest('pattern type is not relevant for nq') + rb_opts_purity = rb_opts.copy() + rb_opts['length_multiplier'] = self.choose_multiplier( + multiplier_type, len(rb_opts['rb_pattern'])) + # Choose options for interleaved RB: + rb_opts_interleaved = rb_opts.copy() + rb_opts_interleaved['interleaved_gates'] = \ + self.choose_interleaved_gates(rb_opts['rb_pattern']) + # Choose options for purity rb + # no length_multiplier + rb_opts_purity['length_multiplier'] = 1 + rb_opts_purity['is_purity'] = is_purity + if multiplier_type > 0: + is_purity = False + # Adding seed_offset and align_cliffs + rb_opts['seed_offset'] = 10 + rb_opts['align_cliffs'] = True + + # Generate the sequences + try: + # Standard (simultaneous) RB sequences: + rb_circs, _ = rb.randomized_benchmarking_seq(**rb_opts) + # Interleaved RB sequences: + rb_original_circs, _, rb_interleaved_circs = \ + rb.randomized_benchmarking_seq( + **rb_opts_interleaved) + # Purity RB sequences: + if is_purity: + rb_purity_circs, _, npurity = \ + rb.randomized_benchmarking_seq( + **rb_opts_purity) + # verify: npurity = 3^n + self.assertEqual( + npurity, 3 ** len(rb_opts['rb_pattern'][0]), + 'Error: npurity does not equal to 3^n') + + except OSError: + skip_msg = ('Skipping tests for %s qubits because ' + 'tables are missing' % str(nq)) + raise unittest.SkipTest(skip_msg) + + # Perform an ideal execution on the generated sequences + basis_gates = ['u1', 'u2', 'u3', 'cx'] + shots = 100 + result = [] + result_original = [] + result_interleaved = [] + if is_purity: + result_purity = [[] for d in range(npurity)] + for seed in range(rb_opts['nseeds']): + result.append( + qiskit.execute(rb_circs[seed], backend=backend, + basis_gates=basis_gates, + shots=shots).result()) + result_original.append( + qiskit.execute(rb_original_circs[seed], + backend=backend, + basis_gates=basis_gates, + shots=shots).result()) + result_interleaved.append( + qiskit.execute(rb_interleaved_circs[seed], + backend=backend, + basis_gates=basis_gates, + shots=shots).result()) + if is_purity: + for d in range(npurity): + result_purity[d].append(qiskit.execute( + rb_purity_circs[seed][d], + backend=backend, + basis_gates=basis_gates, + shots=shots).result()) + + # Verify the generated sequences + for seed in range(rb_opts['nseeds']): + length_vec = rb_opts['length_vector'] + for circ_index, vec_len in enumerate(length_vec): + # Verify circuits names + self.assertEqual( + rb_circs[seed][circ_index].name, + 'rb_length_%d_seed_%d' % ( + circ_index, seed + + rb_opts['seed_offset']), + 'Error: incorrect circuit name') + self.assertEqual( + rb_original_circs[seed][circ_index].name, + 'rb_length_%d_seed_%d' % ( + circ_index, seed), + 'Error: incorrect circuit name') + self.assertEqual( + rb_interleaved_circs[seed][circ_index].name, + 'rb_interleaved_length_%d_seed_%d' % ( + circ_index, seed), + 'Error: incorrect interleaved circuit name') + if is_purity: + for d in range(npurity): + name_type, _ = self.update_purity_gates( + npurity, d, rb_opts_purity + ['rb_pattern']) + self.assertEqual( + rb_purity_circs[seed][d] + [circ_index].name, + 'rb_purity_%s_length_%d_seed_%d' % ( + name_type, circ_index, seed), + 'Error: incorrect purity circuit name') + + self.verify_circuit(rb_circs[seed][circ_index], + nq, rb_opts, + vec_len, result[seed], shots) + self.verify_circuit(rb_original_circs[seed] + [circ_index], + nq, rb_opts, + vec_len, + result_original[seed], shots) + self.verify_circuit(rb_interleaved_circs[seed] + [circ_index], + nq, rb_opts_interleaved, + vec_len, + result_interleaved[seed], + shots, + is_interleaved=True) + if is_purity: + self.verify_circuit(rb_purity_circs[seed][0] + [circ_index], + nq, rb_opts_purity, + vec_len, result_purity + [0][seed], shots) + # compare the purity RB circuits + # with the original circuit + for d in range(1, npurity): + self.compare_purity_circuits( + rb_purity_circs[seed][0][circ_index], + rb_purity_circs[seed][d][circ_index], + nq, d, npurity, rb_opts_purity, + vec_len) + # compare the interleaved RB circuits with + # the original RB circuits + self.compare_interleaved_circuit( + rb_original_circs[seed][circ_index], + rb_interleaved_circs[seed][circ_index], + nq, rb_opts_interleaved, vec_len) + + self.assertEqual(circ_index, len(rb_circs), + "Error: additional circuits exist") def test_rb_utils(self): diff --git a/test/tomography/__init__.py b/test/tomography/__init__.py index e69de29bb..428fe2e50 100644 --- a/test/tomography/__init__.py +++ b/test/tomography/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/tomography/test_pauli_basis.py b/test/tomography/test_pauli_basis.py index 5fafc448c..aaed7e2e4 100644 --- a/test/tomography/test_pauli_basis.py +++ b/test/tomography/test_pauli_basis.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- # -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=missing-docstring diff --git a/test/tomography/test_process_tomography.py b/test/tomography/test_process_tomography.py index 20b02cb0f..74fa8d2a1 100644 --- a/test/tomography/test_process_tomography.py +++ b/test/tomography/test_process_tomography.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- # -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=missing-docstring @@ -25,8 +32,8 @@ def run_circuit_and_tomography(circuit, qubits): job = qiskit.execute(qst, Aer.get_backend('qasm_simulator'), shots=5000) tomo_fit = tomo.ProcessTomographyFitter(job.result(), qst) - choi_cvx = tomo_fit.fit(method='cvx') - choi_mle = tomo_fit.fit(method='lstsq') + choi_cvx = tomo_fit.fit(method='cvx').data + choi_mle = tomo_fit.fit(method='lstsq').data return (choi_cvx, choi_mle, choi_ideal) diff --git a/test/tomography/test_state_tomography.py b/test/tomography/test_state_tomography.py index 6892183ce..3e798dfff 100644 --- a/test/tomography/test_state_tomography.py +++ b/test/tomography/test_state_tomography.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- # -# Copyright 2019, IBM. +# This code is part of Qiskit. # -# This source code is licensed under the Apache License, Version 2.0 found in -# the LICENSE.txt file in the root directory of this source tree. +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. # pylint: disable=missing-docstring @@ -90,6 +97,7 @@ def test_complex_1_qubit_circuit(self): def test_complex_3_qubit_circuit(self): def rand_angles(): + # pylint: disable=E1101 return tuple(2 * numpy.pi * numpy.random.random(3) - numpy.pi) q = QuantumRegister(3) diff --git a/test/topological_codes/__init__.py b/test/topological_codes/__init__.py new file mode 100644 index 000000000..428fe2e50 --- /dev/null +++ b/test/topological_codes/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/topological_codes/test_codes.py b/test/topological_codes/test_codes.py new file mode 100644 index 000000000..0f2a3244d --- /dev/null +++ b/test/topological_codes/test_codes.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +''' +Run codes and decoders +''' + +import unittest + +from qiskit.ignis.verification.topological_codes import RepetitionCode +from qiskit.ignis.verification.topological_codes import GraphDecoder +from qiskit.ignis.verification.topological_codes import lookuptable_decoding +from qiskit.ignis.verification.topological_codes import postselection_decoding + +from qiskit import execute, Aer +from qiskit.providers.aer.noise import NoiseModel +from qiskit.providers.aer.noise.errors import pauli_error, depolarizing_error + + +def get_syndrome(code, noise_model, shots=1014): + ''' + Runs a code to get required results. + ''' + circuits = [code.circuit[log] for log in ['0', '1']] + + job = execute( + circuits, + Aer.get_backend('qasm_simulator'), + noise_model=noise_model, + shots=shots) + raw_results = {} + for log in ['0', '1']: + raw_results[log] = job.result().get_counts(log) + + return code.process_results(raw_results) + + +def get_noise(p_meas, p_gate): + ''' + Define a noise model. + ''' + error_meas = pauli_error([('X', p_meas), ('I', 1 - p_meas)]) + error_gate1 = depolarizing_error(p_gate, 1) + error_gate2 = error_gate1.tensor(error_gate1) + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error( + error_meas, "measure") + noise_model.add_all_qubit_quantum_error( + error_gate1, ["u1", "u2", "u3"]) + noise_model.add_all_qubit_quantum_error( + error_gate2, ["cx"]) + + return noise_model + + +class TestCodes(unittest.TestCase): + """ The test class """ + + def test_rep(self): + """ + Repetition code test. + """ + matching_probs = {} + lookup_probs = {} + post_probs = {} + + max_dist = 5 + + noise_model = get_noise(0.02, 0.02) + + for d in range(3, max_dist + 1, 2): + + code = RepetitionCode(d, 2) + + results = get_syndrome(code, noise_model=noise_model, shots=8192) + + dec = GraphDecoder(code) + + logical_prob_match = dec.get_logical_prob( + results) + logical_prob_lookup = lookuptable_decoding( + results, results) + logical_prob_post = postselection_decoding( + results) + + for log in ['0', '1']: + matching_probs[(d, log)] = logical_prob_match[log] + lookup_probs[(d, log)] = logical_prob_lookup[log] + post_probs[(d, log)] = logical_prob_post[log] + + for d in range(3, max_dist-1, 2): + for log in ['0', '1']: + m_down = matching_probs[(d, log)] \ + > matching_probs[(d + 2, log)] + l_down = lookup_probs[(d, log)] \ + > lookup_probs[(d + 2, log)] + p_down = post_probs[(d, log)] \ + > post_probs[(d + 2, log)] + + m_error = "Error: Matching decoder does not improve "\ + + "logical error rate between repetition codes"\ + + " of distance " + str(d) + " and " + str(d + 2) + ".\n"\ + + "For d="+str(d)+": " + str(matching_probs[(d, log)])\ + + ".\n"\ + + "For d="+str(d+2)+": " + str(matching_probs[(d+2, log)])\ + + "." + l_error = "Error: Lookup decoder does not improve "\ + + "logical error rate between repetition codes"\ + + " of distance " + str(d) + " and " + str(d + 2) + ".\n"\ + + "For d="+str(d)+": " + str(lookup_probs[(d, log)])\ + + ".\n"\ + + "For d="+str(d+2)+": " + str(lookup_probs[(d+2, log)])\ + + "." + p_error = "Error: Postselection decoder does not improve "\ + + "logical error rate between repetition codes"\ + + " of distance " + str(d) + " and " + str(d + 2) + ".\n"\ + + "For d="+str(d)+": " + str(post_probs[(d, log)])\ + + ".\n"\ + + "For d="+str(d+2)+": " + str(post_probs[(d+2, log)])\ + + "." + + self.assertTrue( + m_down or matching_probs[(d, log)] == 0.0, m_error) + self.assertTrue( + l_down or lookup_probs[(d, log)] == 0.0, l_error) + self.assertTrue( + p_down or post_probs[(d, log)] == 0.0, p_error) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 000000000..f16111b17 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Functions of general purpose utility for Ignis. +""" +import random +from typing import List + + +def qubit_shot(i_mean, q_mean, i_std, q_std): + """Creates a random IQ point using a Gaussian distribution. + Args: + i_mean: mean of I distribution + q_mean: mean of Q distribution + i_std: standard deviation of I distribution + q_std: standard deviation of Q distribution + + Returns: + a list of length 2 with I and Q values. + """ + return [random.gauss(i_mean, i_std), random.gauss(q_mean, q_std)] + + +def create_shots(i_mean: float, q_mean: float, i_std: float, q_std: float, + shots: int, qubits: List[int]): + """Creates random IQ points for qubits using a Gaussian distribution. + Args: + i_mean: mean of I distribution + q_mean: mean of Q distribution + i_std: standard deviation of I distribution + q_std: standard deviation of Q distribution + shots: the number of single shots + qubits: a list of qubits. + + Returns: + a list containing lists representing the IQ data of the qubits. + """ + data = [] + for _ in range(shots): + shot = [] + for _ in qubits: + shot.append(qubit_shot(i_mean, q_mean, i_std, q_std)) + data.append(shot) + + return data diff --git a/tools/build_aer.py b/tools/build_aer.py new file mode 100755 index 000000000..466738d7e --- /dev/null +++ b/tools/build_aer.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 + +import os +import shutil +import subprocess +import sys +import tempfile + + + +if os.name == 'nt' or sys.platform == 'darwin': + subprocess.call(['pip', 'install', '-U', + 'git+https://github.com/Qiskit/qiskit-aer.git']) + +if sys.platform == 'linux' or sys.platform == 'linux2': + subprocess.call(['pip', 'install', '-U', + 'git+https://github.com/Qiskit/qiskit-aer.git', + '--install-option', '--', '--install-option', + '-DCMAKE_CXX_COMPILER=g++-7']) diff --git a/tox.ini b/tox.ini index 424ff1c23..c4fab1b36 100644 --- a/tox.ini +++ b/tox.ini @@ -11,14 +11,18 @@ setenv = LANGUAGE=en_US LC_ALL=en_US.utf-8 deps = numpy>=1.13 + Cython>=0.27.1 + setuptools>=40.1.0 commands = + pip install -U git+https://github.com/Qiskit/qiskit-terra.git pip install -U -r{toxinidir}/requirements-dev.txt - python -m unittest -v + stestr run {posargs} [testenv:lint] deps = pycodestyle pylint + setuptools>=40.1.0 commands = pycodestyle qiskit/ignis test/ pylint -rn --rcfile={toxinidir}/.pylintrc qiskit/ignis test/