├── .github └── workflows │ ├── coverage.yml │ ├── docs.yml │ ├── format.yml │ ├── lint.yml │ ├── mypy.yml │ ├── publish-to-pypi.yml │ └── test_latest_versions.yml ├── .gitignore ├── .pylintrc ├── .zenodo.json ├── CITATION.bib ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── check └── format-notebooks ├── docs ├── .nojekyll ├── apidocs │ ├── index.rst │ └── utils.rst ├── conf.py ├── getting_started.ipynb └── index.rst ├── mypy.ini ├── pyproject.toml ├── qiskit_research ├── __init__.py └── utils │ ├── __init__.py │ ├── backend.py │ ├── convenience.py │ ├── cost_funcs.py │ ├── dynamical_decoupling.py │ ├── gate_decompositions.py │ ├── gates.py │ ├── pauli_twirling.py │ ├── periodic_dynamical_decoupling.py │ └── pulse_scaling.py ├── test ├── __init__.py └── utils │ ├── __init__.py │ ├── test_cost_funcs.py │ ├── test_dynamical_decoupling.py │ ├── test_gate_decompositions.py │ ├── test_pauli_twirling.py │ ├── test_periodic_dynamical_decoupling.py │ └── test_pulse_scaling.py └── tox.ini /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'stable/**' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'stable/**' 12 | 13 | jobs: 14 | coverage: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' 22 | - name: Install tox 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox coveragepy-lcov 'coverage<7' 26 | - name: Run coverage 27 | run: tox -ecoverage 28 | - name: Convert to lcov 29 | run: coveragepy-lcov --output_file_path coveralls.info 30 | - name: Upload report to Coveralls 31 | uses: coverallsapp/github-action@master 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | path-to-lcov: coveralls.info 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy sphinx docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'stable/**' 8 | 9 | jobs: 10 | build_and_deploy_docs: 11 | permissions: 12 | contents: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.9' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install tox 23 | sudo apt-get update 24 | sudo apt-get install -y pandoc 25 | - name: Build docs 26 | run: | 27 | tox -edocs 28 | - name: Deploy docs 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./docs/_build/html/ 33 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'stable/**' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'stable/**' 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' 22 | - name: Install tox 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox 26 | - name: Run format check 27 | run: tox -eformat 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'stable/**' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'stable/**' 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' 22 | - name: Install tox 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox 26 | - name: Run lint check 27 | run: tox -elint 28 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | name: Type check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'stable/**' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'stable/**' 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' 22 | - name: Install tox 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox 26 | - name: Run type check 27 | run: tox -emypy 28 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.x" 16 | - name: Install pypa/build 17 | run: >- 18 | python3 -m 19 | pip install 20 | build 21 | --user 22 | - name: Build a binary wheel and a source tarball 23 | run: python3 -m build 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: python-package-distributions 28 | path: dist/ 29 | 30 | publish-to-pypi: 31 | name: >- 32 | Publish Python 🐍 distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/qiskit-research 40 | permissions: 41 | id-token: write # IMPORTANT: mandatory for trusted publishing 42 | 43 | steps: 44 | - name: Download all the dists 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: python-package-distributions 48 | path: dist/ 49 | - name: Publish distribution 📦 to PyPI 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | -------------------------------------------------------------------------------- /.github/workflows/test_latest_versions.yml: -------------------------------------------------------------------------------- 1 | name: Latest version tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "0 1 * * *" 12 | 13 | jobs: 14 | tests: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | max-parallel: 4 18 | matrix: 19 | os: [ubuntu-latest] 20 | python-version: [3.8, 3.9, "3.10", "3.11"] 21 | include: 22 | - os: macos-latest 23 | python-version: "3.11" 24 | - os: windows-latest 25 | python-version: "3.11" 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install tox 36 | - name: Modify tox.ini for more thorough check 37 | shell: bash 38 | run: | 39 | sed -i.bak -E '/#.*CI:[[:space:]]*skip-next-line/I{N;d;}' tox.ini 40 | cat tox.ini 41 | - name: Test using tox environment 42 | shell: bash 43 | run: | 44 | pver=${{ matrix.python-version }} 45 | tox -epy${pver/./} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # VS Code 163 | .vscode/ 164 | 165 | # data directories 166 | docs/mzm_generation/data/ 167 | 168 | # plot directories 169 | docs/mzm_generation/plots/ 170 | 171 | # Binary file from running notebook on MacOS 172 | .DS_Store -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=10.0 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins= 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # When enabled, pylint would attempt to guess common misconfiguration and emit 54 | # user-friendly hints instead of false-positive error messages. 55 | suggestion-mode=yes 56 | 57 | # Allow loading of arbitrary C extensions. Extensions are imported into the 58 | # active Python interpreter and may run arbitrary code. 59 | unsafe-load-any-extension=no 60 | 61 | 62 | [MESSAGES CONTROL] 63 | 64 | # Only show warnings with the listed confidence levels. Leave empty to show 65 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 66 | confidence= 67 | 68 | # Disable the message, report, category or checker with the given id(s). You 69 | # can either give multiple identifiers separated by comma (,) or put this 70 | # option multiple times (only on the command line, not in the configuration 71 | # file where it should appear only once). You can also use "--disable=all" to 72 | # disable everything first and then reenable specific checks. For example, if 73 | # you want to run only the similarities checker, you can use "--disable=all 74 | # --enable=similarities". If you want to run only the classes checker, but have 75 | # no Warning level messages displayed, use "--disable=all --enable=classes 76 | # --disable=W". 77 | disable=duplicate-code, 78 | fixme, 79 | invalid-name, 80 | too-many-arguments, 81 | too-many-branches, 82 | too-many-instance-attributes, 83 | too-many-locals, 84 | too-many-nested-blocks, 85 | too-many-statements, 86 | too-many-positional-arguments 87 | 88 | # Enable the message, report, category or checker with the given id(s). You can 89 | # either give multiple identifier separated by comma (,) or put this option 90 | # multiple time (only on the command line, not in the configuration file where 91 | # it should appear only once). See also the "--disable" option for examples. 92 | enable=c-extension-no-member 93 | 94 | 95 | [REPORTS] 96 | 97 | # Python expression which should return a score less than or equal to 10. You 98 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 99 | # which contain the number of messages in each category, as well as 'statement' 100 | # which is the total number of statements analyzed. This score is used by the 101 | # global evaluation report (RP0004). 102 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 103 | 104 | # Template used to display messages. This is a python new-style format string 105 | # used to format the message information. See doc for all details. 106 | #msg-template= 107 | 108 | # Set the output format. Available formats are text, parseable, colorized, json 109 | # and msvs (visual studio). You can also give a reporter class, e.g. 110 | # mypackage.mymodule.MyReporterClass. 111 | output-format=text 112 | 113 | # Tells whether to display a full report or only the messages. 114 | reports=no 115 | 116 | # Activate the evaluation score. 117 | score=yes 118 | 119 | 120 | [REFACTORING] 121 | 122 | # Maximum number of nested blocks for function / method body 123 | max-nested-blocks=5 124 | 125 | # Complete name of functions that never returns. When checking for 126 | # inconsistent-return-statements if a never returning function is called then 127 | # it will be considered as an explicit return statement and no message will be 128 | # printed. 129 | never-returning-functions=sys.exit,argparse.parse_error 130 | 131 | 132 | [LOGGING] 133 | 134 | # The type of string formatting that logging methods do. `old` means using % 135 | # formatting, `new` is for `{}` formatting. 136 | logging-format-style=old 137 | 138 | # Logging modules to check that the string format arguments are in logging 139 | # function parameter format. 140 | logging-modules=logging 141 | 142 | 143 | [SPELLING] 144 | 145 | # Limits count of emitted suggestions for spelling mistakes. 146 | max-spelling-suggestions=4 147 | 148 | # Spelling dictionary name. Available dictionaries: none. To make it work, 149 | # install the 'python-enchant' package. 150 | spelling-dict= 151 | 152 | # List of comma separated words that should be considered directives if they 153 | # appear and the beginning of a comment and should not be checked. 154 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 155 | 156 | # List of comma separated words that should not be checked. 157 | spelling-ignore-words= 158 | 159 | # A path to a file that contains the private dictionary; one word per line. 160 | spelling-private-dict-file= 161 | 162 | # Tells whether to store unknown words to the private dictionary (see the 163 | # --spelling-private-dict-file option) instead of raising a message. 164 | spelling-store-unknown-words=no 165 | 166 | 167 | [MISCELLANEOUS] 168 | 169 | # List of note tags to take in consideration, separated by a comma. 170 | notes=FIXME, 171 | XXX, 172 | TODO 173 | 174 | # Regular expression of note tags to take in consideration. 175 | #notes-rgx= 176 | 177 | 178 | [TYPECHECK] 179 | 180 | # List of decorators that produce context managers, such as 181 | # contextlib.contextmanager. Add to this list to register other decorators that 182 | # produce valid context managers. 183 | contextmanager-decorators=contextlib.contextmanager 184 | 185 | # List of members which are set dynamically and missed by pylint inference 186 | # system, and so shouldn't trigger E1101 when accessed. Python regular 187 | # expressions are accepted. 188 | generated-members= 189 | 190 | # Tells whether missing members accessed in mixin class should be ignored. A 191 | # mixin class is detected if its name ends with "mixin" (case insensitive). 192 | ignore-mixin-members=yes 193 | 194 | # Tells whether to warn about missing members when the owner of the attribute 195 | # is inferred to be None. 196 | ignore-none=yes 197 | 198 | # This flag controls whether pylint should warn about no-member and similar 199 | # checks whenever an opaque object is returned when inferring. The inference 200 | # can return multiple potential results while evaluating a Python object, but 201 | # some branches might not be evaluated, which results in partial inference. In 202 | # that case, it might be useful to still emit no-member and other checks for 203 | # the rest of the inferred objects. 204 | ignore-on-opaque-inference=yes 205 | 206 | # List of class names for which member attributes should not be checked (useful 207 | # for classes with dynamically set attributes). This supports the use of 208 | # qualified names. 209 | ignored-classes=optparse.Values,thread._local,_thread._local 210 | 211 | # List of module names for which member attributes should not be checked 212 | # (useful for modules/projects where namespaces are manipulated during runtime 213 | # and thus existing member attributes cannot be deduced by static analysis). It 214 | # supports qualified module names, as well as Unix pattern matching. 215 | ignored-modules= 216 | 217 | # Show a hint with possible names when a member name was not found. The aspect 218 | # of finding the hint is based on edit distance. 219 | missing-member-hint=yes 220 | 221 | # The minimum edit distance a name should have in order to be considered a 222 | # similar match for a missing member name. 223 | missing-member-hint-distance=1 224 | 225 | # The total number of similar names that should be taken in consideration when 226 | # showing a hint for a missing member. 227 | missing-member-max-choices=1 228 | 229 | # List of decorators that change the signature of a decorated function. 230 | signature-mutators= 231 | 232 | 233 | [VARIABLES] 234 | 235 | # List of additional names supposed to be defined in builtins. Remember that 236 | # you should avoid defining new builtins when possible. 237 | additional-builtins= 238 | 239 | # Tells whether unused global variables should be treated as a violation. 240 | allow-global-unused-variables=yes 241 | 242 | # List of names allowed to shadow builtins 243 | allowed-redefined-builtins= 244 | 245 | # List of strings which can identify a callback function by name. A callback 246 | # name must start or end with one of those strings. 247 | callbacks=cb_, 248 | _cb 249 | 250 | # A regular expression matching the name of dummy variables (i.e. expected to 251 | # not be used). 252 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 253 | 254 | # Argument names that match this expression will be ignored. Default to name 255 | # with leading underscore. 256 | ignored-argument-names=_.*|^ignored_|^unused_ 257 | 258 | # Tells whether we should check for unused import in __init__ files. 259 | init-import=no 260 | 261 | # List of qualified module names which can have objects that can redefine 262 | # builtins. 263 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 264 | 265 | 266 | [FORMAT] 267 | 268 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 269 | expected-line-ending-format= 270 | 271 | # Regexp for a line that is allowed to be longer than the limit. 272 | ignore-long-lines=^\s*(# )??$ 273 | 274 | # Number of spaces of indent required inside a hanging or continued line. 275 | indent-after-paren=4 276 | 277 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 278 | # tab). 279 | indent-string=' ' 280 | 281 | # Maximum number of characters on a single line. 282 | max-line-length=100 283 | 284 | # Maximum number of lines in a module. 285 | max-module-lines=1000 286 | 287 | # Allow the body of a class to be on the same line as the declaration if body 288 | # contains single statement. 289 | single-line-class-stmt=no 290 | 291 | # Allow the body of an if to be on the same line as the test if there is no 292 | # else. 293 | single-line-if-stmt=no 294 | 295 | 296 | [SIMILARITIES] 297 | 298 | # Ignore comments when computing similarities. 299 | ignore-comments=yes 300 | 301 | # Ignore docstrings when computing similarities. 302 | ignore-docstrings=yes 303 | 304 | # Ignore imports when computing similarities. 305 | ignore-imports=no 306 | 307 | # Ignore function signatures when computing similarities. 308 | ignore-signatures=no 309 | 310 | # Minimum lines number of a similarity. 311 | min-similarity-lines=4 312 | 313 | 314 | [BASIC] 315 | 316 | # Naming style matching correct argument names. 317 | argument-naming-style=snake_case 318 | 319 | # Regular expression matching correct argument names. Overrides argument- 320 | # naming-style. 321 | #argument-rgx= 322 | 323 | # Naming style matching correct attribute names. 324 | attr-naming-style=snake_case 325 | 326 | # Regular expression matching correct attribute names. Overrides attr-naming- 327 | # style. 328 | #attr-rgx= 329 | 330 | # Bad variable names which should always be refused, separated by a comma. 331 | bad-names=foo, 332 | bar, 333 | baz, 334 | toto, 335 | tutu, 336 | tata 337 | 338 | # Bad variable names regexes, separated by a comma. If names match any regex, 339 | # they will always be refused 340 | bad-names-rgxs= 341 | 342 | # Naming style matching correct class attribute names. 343 | class-attribute-naming-style=any 344 | 345 | # Regular expression matching correct class attribute names. Overrides class- 346 | # attribute-naming-style. 347 | #class-attribute-rgx= 348 | 349 | # Naming style matching correct class constant names. 350 | class-const-naming-style=UPPER_CASE 351 | 352 | # Regular expression matching correct class constant names. Overrides class- 353 | # const-naming-style. 354 | #class-const-rgx= 355 | 356 | # Naming style matching correct class names. 357 | class-naming-style=PascalCase 358 | 359 | # Regular expression matching correct class names. Overrides class-naming- 360 | # style. 361 | #class-rgx= 362 | 363 | # Naming style matching correct constant names. 364 | const-naming-style=UPPER_CASE 365 | 366 | # Regular expression matching correct constant names. Overrides const-naming- 367 | # style. 368 | #const-rgx= 369 | 370 | # Minimum line length for functions/classes that require docstrings, shorter 371 | # ones are exempt. 372 | docstring-min-length=-1 373 | 374 | # Naming style matching correct function names. 375 | function-naming-style=snake_case 376 | 377 | # Regular expression matching correct function names. Overrides function- 378 | # naming-style. 379 | #function-rgx= 380 | 381 | # Good variable names which should always be accepted, separated by a comma. 382 | good-names=i, 383 | j, 384 | k, 385 | ex, 386 | Run, 387 | _ 388 | 389 | # Good variable names regexes, separated by a comma. If names match any regex, 390 | # they will always be accepted 391 | good-names-rgxs= 392 | 393 | # Include a hint for the correct naming format with invalid-name. 394 | include-naming-hint=no 395 | 396 | # Naming style matching correct inline iteration names. 397 | inlinevar-naming-style=any 398 | 399 | # Regular expression matching correct inline iteration names. Overrides 400 | # inlinevar-naming-style. 401 | #inlinevar-rgx= 402 | 403 | # Naming style matching correct method names. 404 | method-naming-style=snake_case 405 | 406 | # Regular expression matching correct method names. Overrides method-naming- 407 | # style. 408 | #method-rgx= 409 | 410 | # Naming style matching correct module names. 411 | module-naming-style=snake_case 412 | 413 | # Regular expression matching correct module names. Overrides module-naming- 414 | # style. 415 | #module-rgx= 416 | 417 | # Colon-delimited sets of names that determine each other's naming style when 418 | # the name regexes allow several styles. 419 | name-group= 420 | 421 | # Regular expression which should only match function or class names that do 422 | # not require a docstring. 423 | no-docstring-rgx=^_ 424 | 425 | # List of decorators that produce properties, such as abc.abstractproperty. Add 426 | # to this list to register other decorators that produce valid properties. 427 | # These decorators are taken in consideration only for invalid-name. 428 | property-classes=abc.abstractproperty 429 | 430 | # Naming style matching correct variable names. 431 | variable-naming-style=snake_case 432 | 433 | # Regular expression matching correct variable names. Overrides variable- 434 | # naming-style. 435 | #variable-rgx= 436 | 437 | 438 | [STRING] 439 | 440 | # This flag controls whether inconsistent-quotes generates a warning when the 441 | # character used as a quote delimiter is used inconsistently within a module. 442 | check-quote-consistency=no 443 | 444 | # This flag controls whether the implicit-str-concat should generate a warning 445 | # on implicit string concatenation in sequences defined over several lines. 446 | check-str-concat-over-line-jumps=no 447 | 448 | 449 | [IMPORTS] 450 | 451 | # List of modules that can be imported at any level, not just the top level 452 | # one. 453 | allow-any-import-level= 454 | 455 | # Allow wildcard imports from modules that define __all__. 456 | allow-wildcard-with-all=no 457 | 458 | # Analyse import fallback blocks. This can be used to support both Python 2 and 459 | # 3 compatible code, which means that the block might have code that exists 460 | # only in one or another interpreter, leading to false positives when analysed. 461 | analyse-fallback-blocks=no 462 | 463 | # Deprecated modules which should not be used, separated by a comma. 464 | deprecated-modules= 465 | 466 | # Output a graph (.gv or any supported image format) of external dependencies 467 | # to the given file (report RP0402 must not be disabled). 468 | ext-import-graph= 469 | 470 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 471 | # external) dependencies to the given file (report RP0402 must not be 472 | # disabled). 473 | import-graph= 474 | 475 | # Output a graph (.gv or any supported image format) of internal dependencies 476 | # to the given file (report RP0402 must not be disabled). 477 | int-import-graph= 478 | 479 | # Force import order to recognize a module as part of the standard 480 | # compatibility libraries. 481 | known-standard-library= 482 | 483 | # Force import order to recognize a module as part of a third party library. 484 | known-third-party=enchant 485 | 486 | # Couples of modules and preferred modules, separated by a comma. 487 | preferred-modules= 488 | 489 | 490 | [CLASSES] 491 | 492 | # Warn about protected attribute access inside special methods 493 | check-protected-access-in-special-methods=no 494 | 495 | # List of method names used to declare (i.e. assign) instance attributes. 496 | defining-attr-methods=__init__, 497 | __new__, 498 | setUp, 499 | __post_init__ 500 | 501 | # List of member names, which should be excluded from the protected access 502 | # warning. 503 | exclude-protected=_asdict, 504 | _fields, 505 | _replace, 506 | _source, 507 | _make 508 | 509 | # List of valid names for the first argument in a class method. 510 | valid-classmethod-first-arg=cls 511 | 512 | # List of valid names for the first argument in a metaclass class method. 513 | valid-metaclass-classmethod-first-arg=cls 514 | 515 | 516 | [DESIGN] 517 | 518 | # Maximum number of arguments for function / method. 519 | max-args=10 520 | 521 | # Maximum number of attributes for a class (see R0902). 522 | max-attributes=7 523 | 524 | # Maximum number of boolean expressions in an if statement (see R0916). 525 | max-bool-expr=5 526 | 527 | # Maximum number of branch for function / method body. 528 | max-branches=12 529 | 530 | # Maximum number of locals for function / method body. 531 | max-locals=15 532 | 533 | # Maximum number of parents for a class (see R0901). 534 | max-parents=7 535 | 536 | # Maximum number of public methods for a class (see R0904). 537 | max-public-methods=20 538 | 539 | # Maximum number of return / yield for function / method body. 540 | max-returns=6 541 | 542 | # Maximum number of statements in function / method body. 543 | max-statements=50 544 | 545 | # Minimum number of public methods for a class (see R0903). 546 | min-public-methods=2 547 | -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Qiskit Research", 3 | "description": "Research using Qiskit. This repository contains modules for running quantum computing research experiments using Qiskit and the IBM Quantum Services, demonstrating by example best practices for running such experiments.", 4 | "upload_type": "software", 5 | "license": "Apache-2.0", 6 | "creators": [ 7 | { 8 | "name": "The Qiskit Research developers and contributors" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @software{the_qiskit_research_developers_and_contr_2023_7776174, 2 | author = {The Qiskit Research developers and contributors}, 3 | title = {Qiskit Research}, 4 | month = mar, 5 | year = 2023, 6 | publisher = {Zenodo}, 7 | version = {v0.0.2}, 8 | doi = {10.5281/zenodo.7776174}, 9 | url = {https://doi.org/10.5281/zenodo.7776174} 10 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | All members of this project agree to adhere to the Qiskit Code of Conduct listed at [https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md](https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your contributions. Please follow the guidelines below. 4 | 5 | ## Reporting bugs and requesting features 6 | 7 | Users are encouraged to use GitHub issues for reporting issues and requesting features. 8 | 9 | ## Code review 10 | 11 | All submissions require review. To make a submission, open a GitHub pull request. 12 | 13 | ## Running tests locally 14 | 15 | The tests can be run locally using [tox](https://tox.wiki/en/latest/). 16 | To run the full test suite, execute `tox -e ALL`. 17 | Individual checks can also be run separately. For example: 18 | 19 | Run unit tests for Python 3.9 20 | 21 | tox -epy39 22 | 23 | Run lint check 24 | 25 | tox -elint 26 | 27 | Run type check 28 | 29 | tox -emypy 30 | 31 | Run format check 32 | 33 | tox -eblack 34 | 35 | ## Building and checking the docs locally 36 | 37 | To build the docs locally, run 38 | 39 | tox -edocs 40 | 41 | This will generate the documentation files and place them in the directory 42 | `docs/_build/html`. You can then view the files in your web browser, for example, 43 | by navigating to `file:///QISKIT_RESEARCH_DIRECTORY/docs/_build/html/index.html`. 44 | Please take this step when submitting a pull request to ensure that the changes 45 | you make to the documentation look correct. 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 IBM and its contributors 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2017 IBM and its contributors. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiskit Research 2 | 3 | This repository contains useful software utilities for running quantum computing research experiments using Qiskit and the IBM Quantum Services. 4 | 5 | ## Installation 6 | 7 | pip install qiskit-research 8 | 9 | ## Documentation 10 | 11 | Documentation is located [here](https://qiskit-community.github.io/qiskit-research/). 12 | 13 | ## Contributing 14 | 15 | For information on how to contribute to this project, please take a look at our [contribution guidelines](CONTRIBUTING.md). 16 | 17 | ## Citing 18 | 19 | [![DOI](https://zenodo.org/badge/498756635.svg)](https://zenodo.org/badge/latestdoi/498756635) 20 | 21 | Qiskit Research is automatically uploaded to Zenodo with every release. Click the badge above to see all citation formats for all versions, or just use [this BibTeX file](CITATION.bib). 22 | 23 | ## Moved content 24 | 25 | Previously, this repository also contained the code for several research papers that used Qiskit and the IBM Quantum Services. The code for these papers has moved to the following locations: 26 | 27 | - Majorana zero mode generation: 28 | - Protein folding: 29 | - Quantum simulation tutorial: 30 | -------------------------------------------------------------------------------- /check/format-notebooks: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | import argparse 4 | import fnmatch 5 | import os 6 | 7 | import nbformat 8 | 9 | DOCS_DIR = "docs" 10 | EXCLUDE = [ 11 | "*/_build", 12 | "*/_build/*", 13 | "*/__pycache__", 14 | "*/__pycache__/*", 15 | "*/.ipynb_checkpoints", 16 | "*/.ipynb_checkpoints/*", 17 | ] 18 | 19 | parser = argparse.ArgumentParser(description="Format Jupyter notebooks") 20 | parser.add_argument("--apply", action="store_true", help="Whether to apply the changes") 21 | args = parser.parse_args() 22 | 23 | num_issues = 0 24 | num_changes = 0 25 | for dirpath, dirnames, filenames in os.walk(DOCS_DIR): 26 | if any(fnmatch.fnmatch(dirpath, pattern) for pattern in EXCLUDE): 27 | continue 28 | for filename in filenames: 29 | if not filename.endswith(".ipynb"): 30 | continue 31 | filepath = os.path.join(dirpath, filename) 32 | nb = nbformat.read(filepath, as_version=nbformat.NO_CONVERT) 33 | if nb["metadata"]["kernelspec"]["name"] != "python3": 34 | if args.apply: 35 | nb["metadata"]["kernelspec"]["name"] = "python3" 36 | nbformat.write(nb, filepath) 37 | print(f"{filepath} kernel name changed to 'python3'.") 38 | num_changes += 1 39 | else: 40 | print(f"{filepath} kernel name needs to be changed to 'python3'.") 41 | num_issues += 1 42 | 43 | if args.apply: 44 | print(f"Applied {num_changes} changes.") 45 | else: 46 | print(f"Found {num_issues} issues.") 47 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-research/e341400ad84675706915f3e28df94d14afff1be8/docs/.nojekyll -------------------------------------------------------------------------------- /docs/apidocs/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Qiskit Research API Reference 3 | ============================= 4 | 5 | .. module:: qiskit_research 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | utils -------------------------------------------------------------------------------- /docs/apidocs/utils.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: qiskit_research.utils 2 | :members: 3 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | # pylint: disable=invalid-name 12 | 13 | """ 14 | Sphinx documentation builder 15 | """ 16 | 17 | # General options: 18 | from pathlib import Path 19 | 20 | from importlib_metadata import version as metadata_version 21 | 22 | project = "Qiskit Research" 23 | copyright = "2022" # pylint: disable=redefined-builtin 24 | author = "" 25 | 26 | _rootdir = Path(__file__).parent.parent 27 | 28 | # The full version, including alpha/beta/rc tags 29 | release = metadata_version("qiskit_research") 30 | # The short X.Y version 31 | version = ".".join(release.split(".")[:2]) 32 | 33 | extensions = [ 34 | "sphinx.ext.napoleon", 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.autosummary", 37 | "sphinx.ext.mathjax", 38 | "sphinx.ext.viewcode", 39 | "sphinx.ext.extlinks", 40 | "jupyter_sphinx", 41 | "sphinx_autodoc_typehints", 42 | "nbsphinx", 43 | "qiskit_sphinx_theme", 44 | ] 45 | templates_path = ["_templates"] 46 | numfig = True 47 | numfig_format = {"table": "Table %s"} 48 | language = "en" 49 | pygments_style = "colorful" 50 | add_module_names = False 51 | modindex_common_prefix = ["qiskit_research."] 52 | 53 | # autodoc/autosummary options 54 | autosummary_generate = True 55 | autosummary_generate_overwrite = False 56 | autoclass_content = "both" 57 | 58 | # nbsphinx options (for tutorials) 59 | nbsphinx_timeout = 1000 60 | nbsphinx_execute = "always" 61 | nbsphinx_widgets_path = "" 62 | exclude_patterns = ["_build", "**.ipynb_checkpoints", "getting_started.ipynb"] 63 | 64 | html_theme = "qiskit-ecosystem" 65 | html_title = f"{project} {release}" 66 | -------------------------------------------------------------------------------- /docs/getting_started.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3f704ce0", 6 | "metadata": {}, 7 | "source": [ 8 | "# Getting Started\n", 9 | "\n", 10 | "This notebook outlines the best operating practices for mapping a given quantum circuit to real IBM backends. The tools used within build on Qiskit yet not all are contained within this repository. This repository also contains custom transpiler passes that users may construct their own `PassManager`s from. " 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "2d98425a", 17 | "metadata": { 18 | "ExecuteTime": { 19 | "end_time": "2022-07-04T15:29:26.272344Z", 20 | "start_time": "2022-07-04T15:29:23.767368Z" 21 | } 22 | }, 23 | "outputs": [], 24 | "source": [ 25 | "from qiskit import IBMQ, transpile\n", 26 | "from qiskit.circuit import Parameter, QuantumCircuit\n", 27 | "from qiskit.opflow import I, X, Z, PauliTrotterEvolution, Suzuki\n", 28 | "from qiskit_ibm_runtime.fake_provider import FakeMumbai\n", 29 | "\n", 30 | "import numpy as np\n", 31 | "import matplotlib.pyplot as plt\n", 32 | "\n", 33 | "plt.style.use(\"default\")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "dc93f698", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "IBMQ.load_account()\n", 44 | "provider = IBMQ.get_provider(hub=\"\", group=\"\", project=\"\")" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "id": "92c7af6d", 50 | "metadata": {}, 51 | "source": [ 52 | "## Problem: Quantum Simulation\n", 53 | "\n", 54 | "As an example, we'll use the problem statement from [this manuscript](https://arxiv.org/abs/2108.09197), the quantum simulation of an Ising model Hamiltonian:\n", 55 | "$$\n", 56 | "H = -J \\sum_{\\langle i,j \\rangle} Z_i Z_j + h \\sum_i X_i\n", 57 | "$$\n", 58 | "where $J$ is the exchange coupling between adjacent spins and $h$ is the transverse magnetic field. Here $X_i$ and $Z_j$ are the Pauli matrices acting on qubits $i$ and $j$, respectively. This model describes an interacting system of spins in a magnetic field, which is normally a nearest-neighbor interaction, in order to highlight the mapping of this problem to quantum hardware, we make it an all-to-all interaction.

\n", 59 | "\n", 60 | "We will use tools from `qiskit.opflow` to generate the circuits needed for the simulation. The Hamiltonian $H$ is formed by instantiating `Parameter`s and building the interactions from the Pauli matrices `I`, `X`, and `Z` by concatenating them in tensor products ($\\otimes$) represented in `opflow` by the caret symbol `^`. The formal solution to Schrödinger's equation is\n", 61 | "$$\n", 62 | "U = e^{-iHt},\n", 63 | "$$\n", 64 | "the *time-evolution unitary* corresponding to Hamiltonian evolution under $H$, and is found formally using the `.exp_i()` on an operator expression." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "856dff41", 71 | "metadata": { 72 | "ExecuteTime": { 73 | "end_time": "2022-07-04T15:29:27.189309Z", 74 | "start_time": "2022-07-04T15:29:26.279076Z" 75 | }, 76 | "scrolled": false 77 | }, 78 | "outputs": [], 79 | "source": [ 80 | "num_spins = 3\n", 81 | "\n", 82 | "JJ = Parameter(\"J\")\n", 83 | "hh = Parameter(\"h\")\n", 84 | "tt = Parameter(\"t\")\n", 85 | "\n", 86 | "ham = -JJ * sum(\n", 87 | " [\n", 88 | " sum(\n", 89 | " [\n", 90 | " (I ^ idx) ^ Z ^ (I ^ jdx) ^ Z ^ (I ^ (num_spins - idx - jdx - 2))\n", 91 | " for jdx in range(num_spins - idx - 1)\n", 92 | " ]\n", 93 | " )\n", 94 | " for idx in range(num_spins - 1)\n", 95 | " ]\n", 96 | ") + hh * sum([(I ^ idx) ^ X ^ (I ^ (num_spins - idx - 1)) for idx in range(num_spins)])\n", 97 | "U_ham = (ham * tt).exp_i()\n", 98 | "print(U_ham)" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "id": "9b713e5b", 104 | "metadata": {}, 105 | "source": [ 106 | "## Converting Operators to Circuits\n", 107 | "\n", 108 | "The `qiskit.opflow` module contains methods to convert the time-evolved operators (`EvolvedOp`s) to circuits. One common method is the Suzuki-Trotter decomposition, in which the total evolution time $t$ is broken into `num_steps` $N$. By choosing the second-order of the `PauliTrotterEvolution`, we create a circuit that acts like our unitary to second order $\\mathcal{O}((t/N)^2)$. \n", 109 | "\n", 110 | "Practically, since many of the following transpilation steps are computationally intensive, it may make sense to break up your circuit into smaller subcircuits, and then combine them together to get the final circuit. This is naturally acheived for Trotterized algorithms, since `reps` here just repeats the same circuit `num_steps` times, hence we just set `num_steps=1` and the resulting circuits can be combined into as many steps as desired later." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "76264beb", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "num_steps = 1\n", 121 | "trot_circ = (\n", 122 | " PauliTrotterEvolution(trotter_mode=Suzuki(order=2, reps=num_steps))\n", 123 | " .convert(U_ham)\n", 124 | " .to_circuit()\n", 125 | ")\n", 126 | "trot_circ.draw(\"mpl\")" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "d3d7f498", 132 | "metadata": {}, 133 | "source": [ 134 | "## Transpile for Good SWAP Mapping\n", 135 | "\n", 136 | "In general problems must respect the layout of the actual quantum hardware. Due to limited connectivity, this often entails doing SWAP operations to move quantum information around. SWAPs are costly in the sense they consist of three `CX`s. The Qiskit transpiler with `optimization_level=3` uses the [SABRE SWAP method](https://arxiv.org/abs/1809.02573), which is efficient, however stochastic, since the SWAP-mapping problem is NP-hard. Here we do it several time and take the solution with the lowest CNOT count. This operation serves only to minimize the number of SWAPs in the transpiled circuits, and is not aware of noise on the underlying qubits. That is considered in a following step." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "id": "bd67b483", 143 | "metadata": { 144 | "ExecuteTime": { 145 | "end_time": "2022-07-04T15:29:32.164235Z", 146 | "start_time": "2022-07-04T15:29:28.107249Z" 147 | }, 148 | "scrolled": false 149 | }, 150 | "outputs": [], 151 | "source": [ 152 | "# TODO - check issue with floats not being rounded off\n", 153 | "\n", 154 | "num_tries = 10\n", 155 | "\n", 156 | "# backend = FakeMumbai()\n", 157 | "# backend = provider.get_backend('ibmq_mumbai')\n", 158 | "backend = provider.get_backend(\"ibm_lagos\")\n", 159 | "trot_circ_ts = transpile(\n", 160 | " [trot_circ] * num_tries, backend, optimization_level=3, seed_transpiler=12345\n", 161 | ")\n", 162 | "cx_counts = [trot_circ_ts[idx].count_ops()[\"cx\"] for idx in range(num_tries)]\n", 163 | "print(cx_counts)" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "id": "446229d4", 170 | "metadata": { 171 | "ExecuteTime": { 172 | "end_time": "2022-07-04T15:29:32.173026Z", 173 | "start_time": "2022-07-04T15:29:32.167226Z" 174 | } 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "best_idx = np.argmin(cx_counts)\n", 179 | "trot_circ_t = trot_circ_ts[best_idx]" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "id": "781b20b2", 185 | "metadata": {}, 186 | "source": [ 187 | "## Noise-aware Layout\n", 188 | "\n", 189 | "Now that we have a SWAP-mapped optimal circuit, we consider the layouts on the actual quantum backend. These layouts are found by the VF2 subgraph isomorphism algorithm, which is very fast. Since the transpiler mapped the circuit to physical qubits, we must first \"deflate\" the circuits with `deflate_circuit` (which removes idle qubits), finds the layouts with `matching_layouts`, then scores those layouts due to error rates, which are calculated by a cost function that may be specified by the user, see [`mapomatic`](https://github.com/Qiskit-Partners/mapomatic) documentation for how. The default cost function includes errors determined for each qubit gates and measurements, although not decoherence/relaxation caused by idle time, producing an *infidelity score* where the lowest number is the preferred layout." 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "id": "f69e7d01", 196 | "metadata": { 197 | "ExecuteTime": { 198 | "end_time": "2022-07-04T15:29:32.216769Z", 199 | "start_time": "2022-07-04T15:29:32.177170Z" 200 | }, 201 | "scrolled": true 202 | }, 203 | "outputs": [], 204 | "source": [ 205 | "from mapomatic import deflate_circuit, evaluate_layouts, matching_layouts\n", 206 | "\n", 207 | "trot_circ_def = deflate_circuit(trot_circ_t)\n", 208 | "layouts = matching_layouts(trot_circ_def, backend)\n", 209 | "scored_layouts = evaluate_layouts(\n", 210 | " trot_circ_def, layouts, backend\n", 211 | ") # cost_function = cost_func\n", 212 | "print(scored_layouts)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "id": "abf874a1", 218 | "metadata": {}, 219 | "source": [ 220 | "## Pulse Scaling\n", 221 | "\n", 222 | "For certain problems, in particular Trotterized quantum simulation problems, or other algorithms that require small angles of rotation in the two-qubit Hilbert space, it is more efficient to implement operations in terms of pulses extracted from the CNOT gate. Basically, a CNOT gate is *locally-equivalent* to an $R_{ZX}(\\pi/2)$ rotation, meaning it is built from that and single-qubit rotations, as can be seen from the following code:" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "id": "d07c1600", 229 | "metadata": { 230 | "ExecuteTime": { 231 | "end_time": "2022-07-04T15:29:32.543299Z", 232 | "start_time": "2022-07-04T15:29:32.219802Z" 233 | } 234 | }, 235 | "outputs": [], 236 | "source": [ 237 | "# TODO: check out Weyl decomp stuff\n", 238 | "\n", 239 | "qc = QuantumCircuit(2)\n", 240 | "qc.cx(0, 1)\n", 241 | "qc_rzx = transpile(qc, basis_gates=[\"sx\", \"rz\", \"rzx\"])\n", 242 | "\n", 243 | "fig, (ax1, ax2) = plt.subplots(1, 2)\n", 244 | "qc.draw(\"mpl\", ax=ax1)\n", 245 | "qc_rzx.draw(\"mpl\", ax=ax2)" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "id": "d81f1805", 251 | "metadata": {}, 252 | "source": [ 253 | "The `RZXGate` is very similar to the native two-qubit interation called [echoed cross resonance](https://arxiv.org/abs/1603.04821) which is used to create entanglement on IBM backends. In particular, many two-qubit interactions for quantum simulation, such as the $ZZ$-interaction of our Ising Hamiltonian, can be more efficiently represented (in terms of error) by $R_{ZX}(\\theta)$ rotations, which are automatically broken into scaled echoed cross resonance `secr` gates unless `unroll_rzx_to_ecr` is set to `False`." 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": null, 259 | "id": "b6365ce4", 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "from qiskit_research.utils.convenience import scale_cr_pulses\n", 264 | "\n", 265 | "theta = Parameter(\"$\\\\theta$\")\n", 266 | "\n", 267 | "qc = QuantumCircuit(2)\n", 268 | "qc.rzz(theta, 0, 1)\n", 269 | "qc_cx = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", 270 | "qc_rzx = scale_cr_pulses(qc_cx, backend) # unroll_rzx_to_ecr = True, param_bind = {}\n", 271 | "\n", 272 | "fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 6))\n", 273 | "qc.draw(\"mpl\", ax=ax1)\n", 274 | "qc_cx.draw(\"mpl\", ax=ax2)\n", 275 | "qc_rzx.draw(\"mpl\", ax=ax3)\n", 276 | "ax2.set_title(\"These are all equivalent circuits\")" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "id": "9b78ad89", 282 | "metadata": { 283 | "ExecuteTime": { 284 | "end_time": "2022-06-23T12:54:42.579431Z", 285 | "start_time": "2022-06-23T12:54:42.268911Z" 286 | } 287 | }, 288 | "source": [ 289 | "When we are implementing two-qubit rotation angles $\\theta$ less than $\\pi/2$, we can more efficiently express these interaction in terms of $R_{ZX}(\\theta)$ rotations and directly build them from scaled echoed cross resonance (`secr`($\\theta$)) pulses obtained from the backend, as detailed in [this manuscript](http://arxiv.org/abs/2012.11660). This method first uses a greedy algorithm called [template opimization](http://arxiv.org/abs/1909.05270) to identify parts of the circuit that can be substituted by $R_{ZX}$ rotations. If parameters are passed to the method via `param_bind`, it will bind them to the circuit and attach the necessary pulse gates for implementing the $R_{ZX}$ rotations (otherwise they can be bound and scaled later with `attach_cr_pulses`). Below we will do them separately, because we will attach a series of `Parameter`s as a function of time, and it is more efficient to do the template optimization step once since it is greedy and attaching the pulse schedules in quick." 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": null, 295 | "id": "fb662713", 296 | "metadata": { 297 | "ExecuteTime": { 298 | "end_time": "2022-07-04T15:30:28.806455Z", 299 | "start_time": "2022-07-04T15:29:32.546008Z" 300 | } 301 | }, 302 | "outputs": [], 303 | "source": [ 304 | "my_layout = scored_layouts[0][0] # the layout with the lowest score (i.e., error)\n", 305 | "trot_circ_sca = scale_cr_pulses(\n", 306 | " transpile(trot_circ_def, initial_layout=my_layout), backend\n", 307 | ")\n", 308 | "trot_circ_sca.draw(\"mpl\", idle_wires=False)" 309 | ] 310 | }, 311 | { 312 | "cell_type": "code", 313 | "execution_count": null, 314 | "id": "3412d8ed", 315 | "metadata": { 316 | "ExecuteTime": { 317 | "end_time": "2022-07-04T15:30:30.184640Z", 318 | "start_time": "2022-07-04T15:30:28.814278Z" 319 | } 320 | }, 321 | "outputs": [], 322 | "source": [ 323 | "from qiskit_research.utils.convenience import attach_cr_pulses\n", 324 | "\n", 325 | "num_time_steps = 51\n", 326 | "t_range = np.linspace(0, 10, num_time_steps) # values from manuscript\n", 327 | "param_bind = {JJ: 0.5236, hh: 1} # values from manuscript\n", 328 | "\n", 329 | "circs = []\n", 330 | "for t_set in t_range:\n", 331 | " param_bind[tt] = t_set\n", 332 | " circs.append(attach_cr_pulses(trot_circ_sca, backend, param_bind))" 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "id": "5bbc0bc4", 338 | "metadata": {}, 339 | "source": [ 340 | "## Pauli Twirling\n", 341 | "\n", 342 | "Pauli twirling is a form of randomized compiling that inserts pairs of Pauli gates (`I`, `X`, `Y`, `Z`) before and after entangling gates such that the overall unitary is the same, but the way it is implemented is different. This has the effect of turning coherent errors into stochastic errors, which can then be elimated by sufficient averaging. This is done a number of times (`num_twirled_circuits`) for the benefit of averaging. **Note:** we are probably using an insufficient basis set to currently cancel all errors." 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "id": "a1a178b4", 349 | "metadata": { 350 | "ExecuteTime": { 351 | "end_time": "2022-07-04T15:30:33.928988Z", 352 | "start_time": "2022-07-04T15:30:30.194717Z" 353 | } 354 | }, 355 | "outputs": [], 356 | "source": [ 357 | "from qiskit_research.utils.convenience import add_pauli_twirls\n", 358 | "\n", 359 | "num_twirls = 5\n", 360 | "# this returns a circuit with shape len(circs) x num_twirled_circuits\n", 361 | "twirled_circs = add_pauli_twirls(\n", 362 | " circs, num_twirled_circuits=num_twirls, seed=12345\n", 363 | ") # transpile_added_paulis = False" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": null, 369 | "id": "e65db95c", 370 | "metadata": { 371 | "ExecuteTime": { 372 | "end_time": "2022-07-04T15:30:36.108324Z", 373 | "start_time": "2022-07-04T15:30:33.931670Z" 374 | } 375 | }, 376 | "outputs": [], 377 | "source": [ 378 | "twirled_circs[-1][-1].draw(\"mpl\", idle_wires=False)" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "id": "1cc0b6e1", 384 | "metadata": {}, 385 | "source": [ 386 | "Look good! Now before proceeding to dynamical decoupling, we must convert to the native basis gates of the backend so that we can retrieve gate timing information, which is necessary to add dynamical decoupling passes. (Unless you set the keyword argument `transpile_added_paulis=True` in the above). You will also need to run this before running on a backend." 387 | ] 388 | }, 389 | { 390 | "cell_type": "code", 391 | "execution_count": null, 392 | "id": "fe6d3bf9", 393 | "metadata": { 394 | "ExecuteTime": { 395 | "end_time": "2022-07-04T15:30:45.215839Z", 396 | "start_time": "2022-07-04T15:30:36.111161Z" 397 | } 398 | }, 399 | "outputs": [], 400 | "source": [ 401 | "from qiskit_research.utils.convenience import transpile_paulis\n", 402 | "\n", 403 | "twirled_circs_t = transpile_paulis(twirled_circs)" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "id": "77ea5e92", 409 | "metadata": {}, 410 | "source": [ 411 | "## Dynamical Decoupling\n", 412 | "\n", 413 | "Dynamical decoupling (DD) is a way of modifying the noise power spectrum $S(\\omega)$ observed by qubits (see [this recent review](https://arxiv.org/abs/2207.03670)), and is typically implemented by a sequence of gates scheduled during a given qubit idle time that compose to the identity with specific delay times to fill the idle time in a calculated manner. Considerations for which sequences to use may involve decoherent error due to idle time versus single-qubit gate errors and/or crosstalk during two-qubit gates. Because the addition of gates is not always in the set of `basis_gates` defined by the backend, `add_pulse_cals=True` uses [Pulse Gates](https://docs.quantum.ibm.com/build/pulse) to add the correct implementation to the circuit with added DD." 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": null, 419 | "id": "3f421d43", 420 | "metadata": { 421 | "ExecuteTime": { 422 | "end_time": "2022-07-04T15:30:51.938755Z", 423 | "start_time": "2022-07-04T15:30:45.221042Z" 424 | } 425 | }, 426 | "outputs": [], 427 | "source": [ 428 | "from qiskit_research.utils.convenience import add_dynamical_decoupling\n", 429 | "\n", 430 | "twirled_circs_with_dd = add_dynamical_decoupling(\n", 431 | " twirled_circs_t, backend, \"XY8\", add_pulse_cals=True\n", 432 | ")" 433 | ] 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": null, 438 | "id": "61ef6013", 439 | "metadata": { 440 | "ExecuteTime": { 441 | "end_time": "2022-07-04T15:30:52.840856Z", 442 | "start_time": "2022-07-04T15:30:51.940776Z" 443 | }, 444 | "scrolled": true 445 | }, 446 | "outputs": [], 447 | "source": [ 448 | "from qiskit.visualization import timeline_drawer\n", 449 | "\n", 450 | "# this just displays a small range\n", 451 | "timeline_drawer(twirled_circs_with_dd[-1][-1], time_range=[1, 12000], show_idle=False)" 452 | ] 453 | }, 454 | { 455 | "cell_type": "markdown", 456 | "id": "c372ad05", 457 | "metadata": {}, 458 | "source": [ 459 | "## Circuit Execution\n", 460 | "\n", 461 | "This runs the given circuits on the backend. This will be expanded to include different methods of running, i.e. Qiskit Runtime. " 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": null, 467 | "id": "85619f03", 468 | "metadata": { 469 | "ExecuteTime": { 470 | "start_time": "2022-07-04T15:29:23.806Z" 471 | } 472 | }, 473 | "outputs": [], 474 | "source": [ 475 | "# the backend only accepts a QuantumCircuit or List[QuantumCircuit]\n", 476 | "flattened_circs = [\n", 477 | " circ for circs in twirled_circs_with_dd for circ in circs\n", 478 | "] # first 5 circs are same Pauli twirled circuit at same time\n", 479 | "counts = backend.run(flattened_circs).result().get_counts()" 480 | ] 481 | }, 482 | { 483 | "cell_type": "markdown", 484 | "id": "a6a0e63b", 485 | "metadata": {}, 486 | "source": [ 487 | "## Measurement Error Mitigation\n", 488 | "\n", 489 | "This uses `mthree` (matrix-free measurement mitigation) to do $LU$-decomposition on a readout calibration routine to efficently correct for readout errors. Note that `cals_from_system` runs an experiment on your chosen `backend` and then applies it to your results to calculate quasi-probabilities, with the default `num_shots=8192`." 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": null, 495 | "id": "f39910db", 496 | "metadata": { 497 | "ExecuteTime": { 498 | "start_time": "2022-07-04T15:29:23.816Z" 499 | } 500 | }, 501 | "outputs": [], 502 | "source": [ 503 | "from mthree import M3Mitigation\n", 504 | "\n", 505 | "mit = M3Mitigation(backend)\n", 506 | "mit.cals_from_system(my_layout)" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": null, 512 | "id": "785fd00f", 513 | "metadata": { 514 | "ExecuteTime": { 515 | "start_time": "2022-07-04T15:29:23.818Z" 516 | } 517 | }, 518 | "outputs": [], 519 | "source": [ 520 | "# apply the correction\n", 521 | "quasi_probs = mit.apply_correction(counts, my_layout)" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": null, 527 | "id": "651984b6", 528 | "metadata": {}, 529 | "outputs": [], 530 | "source": [ 531 | "# collect quasi-probabilities from different Pauli twirls\n", 532 | "quasi_probs_twirled = []\n", 533 | "for time_idx in range(num_time_steps):\n", 534 | " quasi_prob_twirled = {}\n", 535 | " for twidx in range(num_twirls):\n", 536 | " for key in quasi_probs[time_idx * num_twirls + twidx].keys():\n", 537 | " try:\n", 538 | " quasi_prob_twirled[key] += (\n", 539 | " quasi_probs[time_idx * num_twirls + twidx][key] / num_twirls\n", 540 | " )\n", 541 | " except:\n", 542 | " quasi_prob_twirled[key] = (\n", 543 | " quasi_probs[time_idx * num_twirls + twidx][key] / num_twirls\n", 544 | " )\n", 545 | "\n", 546 | " quasi_probs_twirled.append(quasi_prob_twirled)" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "execution_count": null, 552 | "id": "84123be8", 553 | "metadata": {}, 554 | "outputs": [], 555 | "source": [ 556 | "# separate list of dicts into dict of lists\n", 557 | "quasi_probs_dict = {}\n", 558 | "for time_step in quasi_probs_twirled:\n", 559 | " for key in time_step.keys():\n", 560 | " try:\n", 561 | " quasi_probs_dict[key].append(time_step[key])\n", 562 | " except:\n", 563 | " quasi_probs_dict[key] = [time_step[key]]" 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": null, 569 | "id": "d4a97e11", 570 | "metadata": {}, 571 | "outputs": [], 572 | "source": [ 573 | "# plot results\n", 574 | "fig, ax = plt.subplots(figsize=(12, 4))\n", 575 | "for key in quasi_probs_dict.keys():\n", 576 | " ax.plot(t_range, quasi_probs_dict[key], lw=2, label=key)\n", 577 | "ax.set_xlabel(\"time step (arb)\")\n", 578 | "ax.set_ylabel(\"quasi-probability\")\n", 579 | "ax.legend(loc=4)\n", 580 | "plt.show()" 581 | ] 582 | } 583 | ], 584 | "metadata": { 585 | "kernelspec": { 586 | "display_name": "Python 3 (ipykernel)", 587 | "language": "python", 588 | "name": "python3" 589 | }, 590 | "language_info": { 591 | "codemirror_mode": { 592 | "name": "ipython", 593 | "version": 3 594 | }, 595 | "file_extension": ".py", 596 | "mimetype": "text/x-python", 597 | "name": "python", 598 | "nbconvert_exporter": "python", 599 | "pygments_lexer": "ipython3", 600 | "version": "3.9.12" 601 | }, 602 | "toc": { 603 | "base_numbering": 1, 604 | "nav_menu": {}, 605 | "number_sections": true, 606 | "sideBar": true, 607 | "skip_h1_title": false, 608 | "title_cell": "Table of Contents", 609 | "title_sidebar": "Contents", 610 | "toc_cell": false, 611 | "toc_position": {}, 612 | "toc_section_display": true, 613 | "toc_window_display": false 614 | } 615 | }, 616 | "nbformat": 4, 617 | "nbformat_minor": 5 618 | } 619 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Qiskit Research documentation 3 | ============================= 4 | 5 | .. toctree:: 6 | :hidden: 7 | 8 | Home 9 | 10 | Research using Qiskit, demonstrating best practices for running quantum computing experiments. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | apidocs/index 16 | 17 | .. Hiding - Indices and tables 18 | :ref:`genindex` 19 | :ref:`modindex` 20 | :ref:`search` 21 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_unused_configs = True 3 | ignore_missing_imports = True 4 | strict_optional = False 5 | no_implicit_optional = True 6 | warn_redundant_casts = True 7 | warn_unused_ignores = True 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "qiskit-research" 7 | version = "0.0.5.dev" 8 | description = "Research using Qiskit, demonstrating best practices for running quantum computing experiments." 9 | readme = "README.md" 10 | license = { file = "LICENSE.txt" } 11 | classifiers = [ 12 | "Intended Audience :: Developers", 13 | "Intended Audience :: Science/Research", 14 | "License :: OSI Approved :: Apache Software License", 15 | "Natural Language :: English", 16 | "Operating System :: MacOS", 17 | "Operating System :: POSIX :: Linux", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Topic :: Scientific/Engineering :: Physics", 22 | ] 23 | 24 | requires-python = ">=3.8" 25 | 26 | dependencies = [ 27 | "qiskit==1.1.1", 28 | "qiskit-aer", 29 | "qiskit-ibm-runtime", 30 | ] 31 | 32 | [project.optional-dependencies] 33 | dev = [ 34 | "black[jupyter]", 35 | "coverage", 36 | "ddt", 37 | "importlib_metadata", 38 | "mypy", 39 | "jupyter-sphinx", 40 | "nbmake", 41 | "nbsphinx", 42 | "pylint", 43 | "pytest", 44 | "qiskit[visualization]", 45 | "qiskit_sphinx_theme", 46 | "sphinx-autodoc-typehints", 47 | ] 48 | 49 | [tool.autoflake] 50 | remove-unused-variables = true 51 | imports = ["qiskit", "qiskit_aer", "qiskit_nature", "qiskit_ibm_runtime"] 52 | -------------------------------------------------------------------------------- /qiskit_research/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | """ 11 | =============== 12 | Qiskit Research 13 | =============== 14 | """ 15 | -------------------------------------------------------------------------------- /qiskit_research/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """ 12 | ====================================================== 13 | Utilities for running research experiments with Qiskit 14 | ====================================================== 15 | """ 16 | 17 | from qiskit_research.utils.backend import get_backend 18 | from qiskit_research.utils.dynamical_decoupling import ( 19 | add_pulse_calibrations, 20 | dynamical_decoupling_passes, 21 | ) 22 | from qiskit_research.utils.gate_decompositions import ( 23 | RZXtoEchoedCR, 24 | XXMinusYYtoRZX, 25 | XXPlusYYtoRZX, 26 | RZXWeylDecomposition, 27 | ) 28 | from qiskit_research.utils.pauli_twirling import ( 29 | PauliTwirl, 30 | pauli_transpilation_passes, 31 | ) 32 | from qiskit_research.utils.pulse_scaling import ( 33 | BindParameters, 34 | CombineRuns, 35 | SECRCalibrationBuilder, 36 | cr_scaling_passes, 37 | pulse_attaching_passes, 38 | ) 39 | from qiskit_research.utils.periodic_dynamical_decoupling import ( 40 | PeriodicDynamicalDecoupling, 41 | ) 42 | 43 | __all__ = [ 44 | "get_backend", 45 | "add_pulse_calibrations", 46 | "dynamical_decoupling_passes", 47 | "RZXtoEchoedCR", 48 | "XXMinusYYtoRZX", 49 | "XXPlusYYtoRZX", 50 | "RZXWeylDecomposition", 51 | "BindParameters", 52 | "CombineRuns", 53 | "SECRCalibrationBuilder", 54 | "cr_scaling_passes", 55 | "pulse_attaching_passes", 56 | "PauliTwirl", 57 | "pauli_transpilation_passes", 58 | "PeriodicDynamicalDecoupling", 59 | ] 60 | -------------------------------------------------------------------------------- /qiskit_research/utils/backend.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Utilities for dealing with backends.""" 12 | 13 | from typing import Optional 14 | 15 | from qiskit.providers import Backend, Provider 16 | from qiskit_aer import AerSimulator 17 | 18 | 19 | def get_backend( 20 | name: str, provider: Optional[Provider], seed_simulator: Optional[int] = None 21 | ) -> Backend: 22 | """Retrieve a backend.""" 23 | if provider is not None: 24 | return provider.get_backend(name) 25 | if name == "aer_simulator": 26 | return AerSimulator(seed_simulator=seed_simulator) 27 | raise ValueError("The given name does not match any supported backends.") 28 | -------------------------------------------------------------------------------- /qiskit_research/utils/convenience.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Convenience functions.""" 12 | 13 | from typing import Any, Iterable, List, Optional, overload, Union 14 | 15 | from qiskit.circuit import QuantumCircuit 16 | from qiskit.circuit import Gate 17 | from qiskit.circuit.library import XGate 18 | from qiskit.providers import Backend 19 | from qiskit.transpiler import PassManager, Target 20 | from qiskit.transpiler.passes.scheduling import ALAPScheduleAnalysis 21 | from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler 22 | 23 | from qiskit.transpiler.passes.calibration.rzx_templates import rzx_templates 24 | from qiskit_research.utils import ( 25 | PauliTwirl, 26 | dynamical_decoupling_passes, 27 | cr_scaling_passes, 28 | pauli_transpilation_passes, 29 | pulse_attaching_passes, 30 | add_pulse_calibrations, 31 | ) 32 | from qiskit_research.utils.dynamical_decoupling import ( 33 | periodic_dynamical_decoupling, 34 | PulseMethod, 35 | ) 36 | 37 | 38 | def add_dynamical_decoupling( 39 | circuits: Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]], 40 | target: Target, 41 | dd_str: str, 42 | scheduler: BaseScheduler = ALAPScheduleAnalysis, 43 | add_pulse_cals: bool = False, 44 | urdd_pulse_num: int = 4, 45 | pulse_method: PulseMethod = PulseMethod.PHASESHIFT, 46 | ) -> Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]]: 47 | """Add dynamical decoupling sequences and calibrations to circuits. 48 | 49 | Adds dynamical decoupling sequences and the calibrations necessary 50 | to run them on an IBM backend target. 51 | 52 | Args: 53 | circuits (Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]]): 54 | input QuantumCircuit or sequences thereof. 55 | target (Target): Backend to run on; gate timing is required for this method. 56 | dd_str (str): String describing DD sequence to use. 57 | scheduler (BaseScheduler, optional): Scheduler, defaults to ALAPScheduleAnalysis. 58 | add_pulse_cals (bool, optional): Add Pulse calibrations for non-basis 59 | gates? Defaults to False. 60 | urdd_pulse_num (int, optional): URDD pulse number must be even and at least 4. 61 | Defaults to 4. 62 | pulse_method (Optional[str], optional): Pulse calibration method for URDD 63 | sequences (if add_pulse cals). Defaults to "phase_shift". 64 | 65 | Returns: 66 | Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]]: Same 67 | single or sequence type of QuantumCircuit, shceduled with DD sequences 68 | inserted into idle times. 69 | """ 70 | pass_manager = PassManager( 71 | list( 72 | dynamical_decoupling_passes( 73 | target, dd_str, scheduler, urdd_pulse_num=urdd_pulse_num 74 | ) 75 | ) 76 | ) 77 | if isinstance(circuits, QuantumCircuit) or isinstance(circuits[0], QuantumCircuit): 78 | circuits_dd = pass_manager.run(circuits) 79 | if add_pulse_cals: 80 | add_pulse_calibrations(circuits_dd, target, pulse_method=pulse_method) 81 | else: 82 | circuits_dd = [pass_manager.run(circs) for circs in circuits] 83 | if add_pulse_cals: 84 | for circs_dd in circuits_dd: 85 | add_pulse_calibrations(circs_dd, target, pulse_method=pulse_method) 86 | 87 | return circuits_dd 88 | 89 | 90 | def add_periodic_dynamical_decoupling( 91 | circuits: Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]], 92 | backend: Backend, 93 | base_dd_sequence: List[Gate] = None, 94 | base_spacing: List[float] = None, 95 | avg_min_delay: int = None, 96 | max_repeats: int = 1, 97 | scheduler: BaseScheduler = ALAPScheduleAnalysis, 98 | add_pulse_cals: bool = False, 99 | ) -> Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]]: 100 | """Add periodic dynamical decoupling sequences and calibrations to circuits. 101 | 102 | Adds periodic dynamical decoupling sequences and the calibrations necessary 103 | to run them on an IBM backend. 104 | """ 105 | if base_dd_sequence is None: 106 | base_dd_sequence = [XGate(), XGate()] 107 | 108 | pass_manager = PassManager( 109 | list( 110 | periodic_dynamical_decoupling( 111 | backend, 112 | base_dd_sequence=base_dd_sequence, 113 | base_spacing=base_spacing, 114 | avg_min_delay=avg_min_delay, 115 | max_repeats=max_repeats, 116 | scheduler=scheduler, 117 | ) 118 | ) 119 | ) 120 | if isinstance(circuits, QuantumCircuit) or isinstance(circuits[0], QuantumCircuit): 121 | circuits_dd = pass_manager.run(circuits) 122 | if add_pulse_cals: 123 | add_pulse_calibrations(circuits_dd, backend) 124 | else: 125 | circuits_dd = [pass_manager.run(circs) for circs in circuits] 126 | if add_pulse_cals: 127 | for circs_dd in circuits_dd: 128 | add_pulse_calibrations(circs_dd, backend) 129 | 130 | return circuits_dd 131 | 132 | 133 | def add_pauli_twirls( 134 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 135 | num_twirled_circuits: int = 1, 136 | gates_to_twirl: Optional[Iterable[str]] = None, 137 | transpile_added_paulis: bool = False, 138 | seed: Any = None, 139 | ) -> Union[List[QuantumCircuit], List[List[QuantumCircuit]]]: 140 | """Add Pauli twirls to circuits. 141 | 142 | Args: 143 | circuits: Circuit or list of circuits to be twirled. 144 | num_twirled_circuits: Number of twirled circuits to return for each input circuit. 145 | gates_to_twirl: Names of gates to twirl. The default behavior is to twirl all 146 | supported gates. 147 | transpile_add_paulis: Transpile added Paulis to native basis gate set and combine 148 | single qubit gates and consecutive CXs. 149 | seed: Seed for the pseudorandom number generator. 150 | 151 | Returns: 152 | If the input is a single circuit, then a list of circuits is returned. 153 | If the input is a list of circuit, then a list of lists of circuits is returned. 154 | """ 155 | passes = [PauliTwirl(gates_to_twirl=gates_to_twirl, seed=seed)] 156 | if transpile_added_paulis: 157 | for pass_ in list(pauli_transpilation_passes()): 158 | passes.append(pass_) 159 | pass_manager = PassManager(passes) 160 | if isinstance(circuits, QuantumCircuit): 161 | return [pass_manager.run(circuits) for _ in range(num_twirled_circuits)] 162 | return [ 163 | [pass_manager.run(circuit) for _ in range(num_twirled_circuits)] 164 | for circuit in circuits 165 | ] 166 | 167 | 168 | @overload 169 | def scale_cr_pulses( 170 | circuits: List[QuantumCircuit], 171 | target: Target, 172 | unroll_rzx_to_ecr: Optional[bool] = True, 173 | force_zz_matches: Optional[bool] = True, 174 | param_bind: Optional[dict] = None, 175 | ) -> List[QuantumCircuit]: ... 176 | 177 | 178 | @overload 179 | def scale_cr_pulses( 180 | circuits: QuantumCircuit, 181 | target: Target, 182 | unroll_rzx_to_ecr: Optional[bool] = True, 183 | force_zz_matches: Optional[bool] = True, 184 | param_bind: Optional[dict] = None, 185 | ) -> QuantumCircuit: ... 186 | 187 | 188 | def scale_cr_pulses( 189 | circuits, 190 | target, 191 | unroll_rzx_to_ecr: Optional[bool] = True, 192 | force_zz_matches: Optional[bool] = True, 193 | param_bind: Optional[dict] = None, 194 | ): 195 | """ 196 | Scale circuits using Pulse scaling technique from 197 | http://arxiv.org/abs/2012.11660. If parameters are 198 | provided, they are also bound their corresponding 199 | pulse gates are attached. 200 | """ 201 | templates = rzx_templates() 202 | 203 | pass_manager = PassManager( 204 | list( 205 | cr_scaling_passes( 206 | target, 207 | templates, 208 | unroll_rzx_to_ecr=unroll_rzx_to_ecr, 209 | force_zz_matches=force_zz_matches, 210 | param_bind=param_bind, 211 | ) 212 | ) 213 | ) 214 | return pass_manager.run(circuits) 215 | 216 | 217 | def attach_cr_pulses( 218 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 219 | target: Target, 220 | param_bind: dict, 221 | ) -> Union[QuantumCircuit, List[QuantumCircuit]]: 222 | """ 223 | Scale circuits using Pulse scaling technique from 224 | http://arxiv.org/abs/2012.11660. Binds parameters 225 | in param_bind and attaches pulse gates. 226 | """ 227 | pass_manager = PassManager(list(pulse_attaching_passes(target, param_bind))) 228 | return pass_manager.run(circuits) 229 | 230 | 231 | def transpile_paulis( 232 | circuits: Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]], 233 | ) -> Union[QuantumCircuit, List[QuantumCircuit], List[List[QuantumCircuit]]]: 234 | """ 235 | Convert Pauli gates to native basis gates and do simple optimization. 236 | """ 237 | pass_manager = PassManager(list(pauli_transpilation_passes())) 238 | if isinstance(circuits, QuantumCircuit): 239 | return pass_manager.run(circuits) 240 | return [pass_manager.run(circs) for circs in circuits] 241 | -------------------------------------------------------------------------------- /qiskit_research/utils/cost_funcs.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Cost functions.""" 12 | 13 | from __future__ import annotations 14 | 15 | from qiskit.circuit import QuantumCircuit 16 | from qiskit.converters import circuit_to_dag 17 | from qiskit.transpiler import Target 18 | from qiskit.transpiler.passes.layout.vf2_utils import ( 19 | build_average_error_map, 20 | build_interaction_graph, 21 | score_layout, 22 | ) 23 | 24 | 25 | def avg_error_score(qc_isa: QuantumCircuit, target: Target) -> float: 26 | """Calculate average error score using vf2 utils 27 | 28 | Args: 29 | qc_isa (QuantumCircuit): transpiled circuit 30 | target (Target): backend target 31 | 32 | Returns: 33 | float: average error score determined by average error map 34 | """ 35 | init_layout = qc_isa.layout.final_index_layout() 36 | dag = circuit_to_dag(qc_isa) 37 | 38 | layout = {idx: init_layout[idx] for idx in range(len(init_layout))} 39 | avg_error_map = build_average_error_map(target, None, None) 40 | im_graph, im_graph_node_map, reverse_im_graph_node_map, _ = build_interaction_graph( 41 | dag, strict_direction=False 42 | ) 43 | return score_layout( 44 | avg_error_map, layout, im_graph_node_map, reverse_im_graph_node_map, im_graph 45 | ) 46 | -------------------------------------------------------------------------------- /qiskit_research/utils/dynamical_decoupling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Dynamical decoupling.""" 12 | 13 | from __future__ import annotations 14 | 15 | from enum import Enum 16 | from math import pi 17 | from typing import cast, Iterable, List, Optional, Sequence, Union 18 | 19 | import numpy as np 20 | from qiskit import QuantumCircuit, pulse 21 | from qiskit.circuit import Gate, Parameter, Qubit 22 | from qiskit.circuit.delay import Delay 23 | from qiskit.circuit.library import U3Gate, UGate, XGate, YGate 24 | from qiskit.circuit.reset import Reset 25 | from qiskit.converters import circuit_to_dag 26 | from qiskit.dagcircuit import DAGCircuit, DAGInNode, DAGNode, DAGOpNode 27 | from qiskit.providers.backend import Backend 28 | from qiskit.pulse import Drag, Waveform 29 | from qiskit.synthesis import OneQubitEulerDecomposer 30 | from qiskit.transpiler import InstructionDurations, Target 31 | from qiskit.transpiler.basepasses import BasePass 32 | from qiskit.transpiler.exceptions import TranspilerError 33 | from qiskit.transpiler.passes import Optimize1qGates, PadDynamicalDecoupling 34 | from qiskit.transpiler.passes.scheduling import ALAPScheduleAnalysis 35 | from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler 36 | 37 | from qiskit_research.utils.gates import PiPhiGate, XmGate, XpGate, YmGate, YpGate 38 | from qiskit_research.utils.periodic_dynamical_decoupling import ( 39 | PeriodicDynamicalDecoupling, 40 | ) 41 | 42 | X = XGate() 43 | Xp = XpGate() 44 | Xm = XmGate() 45 | Y = YGate() 46 | Yp = YpGate() 47 | Ym = YmGate() 48 | 49 | 50 | DD_SEQUENCE = { 51 | "X2": (X, X), 52 | "X2pm": (Xp, Xm), 53 | "XY4": (X, Y, X, Y), 54 | "XY4pm": (Xp, Yp, Xm, Ym), 55 | "XY8": (X, Y, X, Y, Y, X, Y, X), 56 | "XY8pm": (Xp, Yp, Xm, Ym, Ym, Xm, Yp, Xp), 57 | } 58 | 59 | 60 | class PulseMethod(Enum): 61 | """Class for enumerating way of implementing custom gates.""" 62 | 63 | GATEBASED = "decompose operation into standard gates" 64 | PHASESHIFT = "pulse implementation with phase-offset pulses" 65 | AMPLITUDEINVERT = "pulse implementation with positive/negative amplitudes" 66 | IQSUM = "complex sum of signals with no phase offset" 67 | 68 | 69 | def dynamical_decoupling_passes( 70 | target: Target, 71 | dd_str: str, 72 | scheduler: BaseScheduler = ALAPScheduleAnalysis, 73 | urdd_pulse_num: int = 4, 74 | ) -> Iterable[BasePass]: 75 | """ 76 | Yield transpilation passes for dynamical decoupling. 77 | 78 | Args: 79 | target (Target): Backend to run on; gate timing is required for this method. 80 | dd_str (str): String describing DD sequence to use. 81 | scheduler (BaseScheduler, optional): Scheduler, defaults to ALAPScheduleAnalysis. 82 | urdd_pulse_num (int, optional): URDD pulse number must be even and at least 4. 83 | Defaults to 4. 84 | 85 | Yields: 86 | Iterator[Iterable[BasePass]]: Transpiler passes used for adding DD sequences. 87 | """ 88 | for new_gate in [cast(Gate, gate) for gate in [Xp, Xm, Yp, Ym]]: 89 | if new_gate.name not in target: 90 | target.add_instruction(new_gate, target["x"]) 91 | 92 | if dd_str in DD_SEQUENCE: 93 | sequence = DD_SEQUENCE[dd_str] 94 | elif dd_str == "URDD": 95 | phis = get_urdd_angles(urdd_pulse_num) 96 | sequence = tuple(PiPhiGate(phi) for phi in phis) 97 | if "pi_phi" not in target: 98 | phi = Parameter("φ") 99 | target.add_instruction(PiPhiGate(phi), target["x"]) 100 | else: 101 | raise AttributeError("No DD sequence specified") 102 | 103 | yield scheduler(target.durations()) 104 | yield PadDynamicalDecoupling(target.durations(), list(sequence)) 105 | 106 | 107 | def periodic_dynamical_decoupling( 108 | backend: Backend, 109 | base_dd_sequence: Optional[List[Gate]] = None, 110 | base_spacing: Optional[List[float]] = None, 111 | avg_min_delay: int = None, 112 | max_repeats: int = 1, 113 | scheduler: BaseScheduler = ALAPScheduleAnalysis, 114 | ) -> Iterable[BasePass]: 115 | """Yields transpilation passes for periodic dynamical decoupling.""" 116 | target = backend.target 117 | durations = target.durations() 118 | pulse_alignment = backend.configuration().timing_constraints["pulse_alignment"] 119 | 120 | if base_dd_sequence is None: 121 | base_dd_sequence = [XGate(), XGate()] 122 | 123 | yield scheduler(durations) 124 | yield PeriodicDynamicalDecoupling( 125 | durations, 126 | base_dd_sequence, 127 | base_spacing=base_spacing, 128 | avg_min_delay=avg_min_delay, 129 | max_repeats=max_repeats, 130 | pulse_alignment=pulse_alignment, 131 | ) 132 | 133 | 134 | # TODO refactor this as a CalibrationBuilder transpilation pass 135 | def add_pulse_calibrations( 136 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 137 | target: Target, 138 | pulse_method: PulseMethod = PulseMethod.PHASESHIFT, 139 | ) -> None: 140 | """ 141 | Add pulse calibrations for custom gates to circuits in-place. 142 | 143 | Args: 144 | circuits (Union[QuantumCircuit, List[QuantumCircuit]]): Circuits which need pulse schedules 145 | attached to the non-basis gates. 146 | backend (Backend): Backend from which pulse information is obtained. 147 | pulse_method (PulseMethod, optional): Exact method of implemeting pulse schedules given by 148 | PulseMethod enumeration. These should all be equivalent but in practice they may differ. 149 | Defaults to PulseMethod.PHASESHIFT. 150 | 151 | Raises: 152 | ValueError: Not a defined method for implementing pulse schedules for URDD gates. 153 | """ 154 | inst_sched_map = target.instruction_schedule_map() 155 | num_qubits = target.num_qubits 156 | 157 | if isinstance(circuits, QuantumCircuit): 158 | circuits = [circuits] 159 | 160 | for qubit in range(num_qubits): 161 | # get XGate pulse to define the others 162 | x_sched = inst_sched_map.get("x", qubits=[qubit]) 163 | _, x_instruction = x_sched.instructions[0] 164 | 165 | # XpGate has the same pulse 166 | with pulse.build(f"xp gate for qubit {qubit}") as sched: 167 | pulse.play(x_instruction.pulse, x_instruction.channel) 168 | for circ in circuits: 169 | circ.add_calibration("xp", [qubit], sched) 170 | 171 | # XmGate has amplitude inverted 172 | with pulse.build(f"xm gate for qubit {qubit}") as sched: 173 | inverted_pulse = Drag( 174 | duration=x_instruction.pulse.duration, 175 | amp=-x_instruction.pulse.amp, 176 | sigma=x_instruction.pulse.sigma, 177 | beta=x_instruction.pulse.beta, 178 | ) 179 | pulse.play(inverted_pulse, x_instruction.channel) 180 | for circ in circuits: 181 | circ.add_calibration("xm", [qubit], sched) 182 | 183 | # YGate and YpGate have phase shifted 184 | with pulse.build(f"y gate for qubit {qubit}") as sched: 185 | with pulse.phase_offset(pi / 2, x_instruction.channel): 186 | pulse.play(x_instruction.pulse, x_instruction.channel) 187 | for circ in circuits: 188 | circ.add_calibration("y", [qubit], sched) 189 | circ.add_calibration("yp", [qubit], sched) 190 | 191 | # YmGate has phase shifted in opposite direction and amplitude inverted 192 | with pulse.build(f"ym gate for qubit {qubit}") as sched: 193 | with pulse.phase_offset(-pi / 2, x_instruction.channel): 194 | inverted_pulse = Drag( 195 | duration=x_instruction.pulse.duration, 196 | amp=-x_instruction.pulse.amp, 197 | sigma=x_instruction.pulse.sigma, 198 | beta=x_instruction.pulse.beta, 199 | ) 200 | pulse.play(inverted_pulse, x_instruction.channel) 201 | for circ in circuits: 202 | circ.add_calibration("ym", [qubit], sched) 203 | 204 | for circuit in circuits: 205 | dag = circuit_to_dag(circuit) 206 | for run in dag.collect_runs(["pi_phi"]): 207 | for node in run: 208 | qubit, _ = dag.find_bit(node.qargs[0]) 209 | phi = node.op.params[0] 210 | x_sched = inst_sched_map.get("x", qubits=[qubit]) 211 | _, x_instruction = x_sched.instructions[0] 212 | 213 | with pulse.build(f"PiPhi gate for qubit {qubit}") as sched: 214 | if pulse_method == PulseMethod.PHASESHIFT: 215 | with pulse.phase_offset(phi, x_instruction.channel): 216 | pulse.play(x_instruction.pulse, x_instruction.channel) 217 | elif pulse_method == PulseMethod.AMPLITUDEINVERT: 218 | amp_flip_pulse = Drag( 219 | duration=x_instruction.pulse.duration, 220 | amp=(-1) ** (phi // (pi / 2)) * x_instruction.pulse.amp, 221 | sigma=x_instruction.pulse.sigma, 222 | beta=(-1) ** (phi // (pi / 2)) * x_instruction.pulse.beta, 223 | ) 224 | phi %= pi / 2 225 | with pulse.phase_offset(phi, x_instruction.channel): 226 | pulse.play(amp_flip_pulse, x_instruction.channel) 227 | elif pulse_method == PulseMethod.IQSUM: 228 | wf_array = x_instruction.pulse.get_waveform().samples 229 | iq_waveform = Waveform( 230 | wf_array * (np.cos(phi) + 1j * np.sin(phi)) 231 | ) 232 | pulse.play(iq_waveform, x_instruction.channel) 233 | else: 234 | raise ValueError( 235 | f"{pulse_method} not a valid URDD pulse calibration type." 236 | ) 237 | 238 | circuit.add_calibration("pi_phi", [qubit], sched, params=[phi]) 239 | 240 | 241 | def get_urdd_angles(num_pulses: int = 4) -> Sequence[float]: 242 | """Gets \\phi_k values for n pulse UR sequence""" 243 | if num_pulses % 2 == 1: 244 | raise ValueError("num_pulses must be even") 245 | if num_pulses < 4: 246 | raise ValueError("num_pulses must be >= 4") 247 | 248 | # get capital Phi value 249 | if num_pulses % 4 == 0: 250 | m_divisor = int(num_pulses / 4) 251 | big_phi = np.pi / m_divisor 252 | else: 253 | m_divisor = int((num_pulses - 2) / 4) 254 | big_phi = (2 * m_divisor * np.pi) / (2 * m_divisor + 1) 255 | 256 | # keep track of unique phi added; we choose phi2 = big_phi by convention-- 257 | # only real requirement is (n * big_phi = 2pi * j for j int) 258 | unique_phi = [0, big_phi] 259 | # map each phi in [phis] to location (by index) of corresponding [unique_phi] 260 | phi_indices = [0, 1] 261 | # populate remaining phi values 262 | for kk in range(3, num_pulses + 1): 263 | phi_k = (kk * (kk - 1) * big_phi) / 2 264 | # values only matter modulo 2 pi 265 | phi_k = (phi_k) % (2 * np.pi) 266 | if np.isclose(phi_k, 0): 267 | phi_k = 0 268 | elif np.isclose(phi_k, 2 * np.pi): 269 | phi_k = 0 270 | 271 | added_new = False 272 | for idx, u_phi in enumerate(unique_phi): 273 | if np.isclose(u_phi, phi_k, atol=0.001): 274 | added_new = True 275 | phi_indices.append(idx) 276 | 277 | if added_new is False: 278 | unique_phi.append(phi_k) 279 | phi_indices.append(len(unique_phi) - 1) 280 | 281 | # construct phi list 282 | phis: list[float] = [] 283 | for idx in phi_indices: 284 | phis.append(unique_phi[idx]) 285 | 286 | return phis 287 | 288 | 289 | class URDDSequenceStrategy(PadDynamicalDecoupling): 290 | """URDD strategic timing dynamical decoupling insertion pass. 291 | 292 | This pass acts the same as PadDynamicalDecoupling, but only inserts 293 | a number of URDD pulses specified by a sequence of num_pulses only when there 294 | is a delay greater than a sequence of min_delay_time. Each delay will be 295 | considered and the large number of sequence will be given by that delay 296 | specificied max(min_delay_time) < delay. 297 | """ 298 | 299 | def __init__( 300 | self, 301 | durations: InstructionDurations = None, 302 | num_pulses: List[int] = None, 303 | min_delay_times: List[int] = None, 304 | qubits: Optional[List[int]] = None, 305 | spacing: Optional[List[float]] = None, 306 | skip_reset_qubits: bool = True, 307 | pulse_alignment: int = 1, 308 | extra_slack_distribution: str = "middle", 309 | ): 310 | """URDD strategic timing initializer. 311 | 312 | Args: 313 | durations: Durations of instructions to be used in scheduling. 314 | num_pulses Sequence[int]: Number of pulses to use corresponding 315 | to min_delay_time. Defaults to 4. 316 | min_delay_time Sequence[int]: Minimum delay for a given sequence length. In units of dt. 317 | qubits: Physical qubits on which to apply DD. 318 | If None, all qubits will undergo DD (when possible). 319 | spacing: A list of spacings between the DD gates. 320 | The available slack will be divided according to this. 321 | The list length must be one more than the length of dd_sequence, 322 | and the elements must sum to 1. If None, a balanced spacing 323 | will be used [d/2, d, d, ..., d, d, d/2]. 324 | skip_reset_qubits: If True, does not insert DD on idle periods that 325 | immediately follow initialized/reset qubits 326 | (as qubits in the ground state are less susceptile to decoherence). 327 | pulse_alignment: The hardware constraints for gate timing allocation. 328 | This is usually provided from ``backend.configuration().timing_constraints``. 329 | If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to 330 | satisfy this constraint. 331 | extra_slack_distribution: The option to control the behavior of DD sequence generation. 332 | The duration of the DD sequence should be identical to an idle time in the 333 | scheduled quantum circuit, however, the delay in between gates comprising the 334 | sequence should be integer number in units of dt, and it might be further 335 | truncated when ``pulse_alignment`` is specified. This sometimes results in 336 | the duration of the created sequence being shorter than the idle time 337 | that you want to fill with the sequence, i.e. `extra slack`. 338 | This option takes following values. 339 | 340 | - "middle": Put the extra slack to the interval at the middle of the sequence. 341 | - "edges": Divide the extra slack as evenly as possible into 342 | intervals at beginning and end of the sequence. 343 | 344 | Raises: 345 | TranspilerError: When invalid DD sequence is specified. 346 | TranspilerError: When pulse gate with the duration which is 347 | non-multiple of the alignment constraint value is found. 348 | """ 349 | if num_pulses is None: 350 | num_pulses = [4] 351 | if min_delay_times is None: 352 | min_delay_times = [0] 353 | 354 | phis = get_urdd_angles(min(num_pulses)) 355 | dd_sequence = tuple(PiPhiGate(phi) for phi in phis) 356 | 357 | super().__init__( 358 | durations=durations, 359 | dd_sequence=dd_sequence, 360 | qubits=qubits, 361 | spacing=spacing, 362 | skip_reset_qubits=skip_reset_qubits, 363 | pulse_alignment=pulse_alignment, 364 | extra_slack_distribution=extra_slack_distribution, 365 | ) 366 | 367 | self._num_pulses = num_pulses 368 | self._min_delay_times = np.array(min_delay_times) 369 | 370 | def __is_dd_qubit(self, qubit_index: int) -> bool: 371 | """DD can be inserted in the qubit or not.""" 372 | if self._qubits and qubit_index not in self._qubits: 373 | return False 374 | return True 375 | 376 | def _compute_spacing(self, num_pulses): 377 | mid = 1 / num_pulses 378 | end = mid / 2 379 | self._spacing = [end] + [mid] * (num_pulses - 1) + [end] 380 | 381 | def _compute_dd_sequence_lengths(self, dd_sequence, dag: DAGCircuit) -> dict: 382 | # Precompute qubit-wise DD sequence length for performance 383 | dd_sequence_lengths = {} 384 | for physical_index, qubit in enumerate(dag.qubits): 385 | if not self.__is_dd_qubit(physical_index): 386 | continue 387 | 388 | sequence_lengths = [] 389 | for gate in dd_sequence: 390 | try: 391 | # Check calibration. 392 | gate_length = dag.calibrations[gate.name][ 393 | (physical_index, gate.params) 394 | ] 395 | if gate_length % self._alignment != 0: 396 | # This is necessary to implement lightweight scheduling logic for this pass. 397 | # Usually the pulse alignment constraint and pulse data chunk size take 398 | # the same value, however, we can intentionally violate this pattern 399 | # at the gate level. For example, we can create a schedule consisting of 400 | # a pi-pulse of 32 dt followed by a post buffer, i.e. delay, of 4 dt 401 | # on the device with 16 dt constraint. Note that the pi-pulse length 402 | # is multiple of 16 dt but the gate length of 36 is not multiple of it. 403 | # Such pulse gate should be excluded. 404 | raise TranspilerError( 405 | f"Pulse gate {gate.name} with length non-multiple of {self._alignment} " 406 | f"is not acceptable in {self.__class__.__name__} pass." 407 | ) 408 | except KeyError: 409 | gate_length = self._durations.get(gate, physical_index) 410 | sequence_lengths.append(gate_length) 411 | # Update gate duration. This is necessary for current timeline drawer, 412 | # i.e. scheduled. 413 | gate.duration = gate_length 414 | dd_sequence_lengths[qubit] = sequence_lengths 415 | 416 | return dd_sequence_lengths 417 | 418 | def _pad( 419 | self, 420 | dag: DAGCircuit, 421 | qubit: Qubit, 422 | t_start: int, 423 | t_end: int, 424 | next_node: DAGNode, 425 | prev_node: DAGNode, 426 | ): 427 | # This routine takes care of the pulse alignment constraint for the URDD sequence. 428 | # The only difference is that it will only execute for time_intervals larger than 429 | # those specified by the internal property self._min_delay_time which is defined 430 | # at initialization. 431 | time_interval = t_end - t_start 432 | dd_indices = np.where(self._min_delay_times < time_interval)[0] 433 | if len(dd_indices) > 0: 434 | dd_idx = np.where( 435 | self._min_delay_times[dd_indices] 436 | == max(self._min_delay_times[dd_indices]) 437 | )[0][0] 438 | urdd_num = self._num_pulses[dd_idx] 439 | phis = get_urdd_angles(urdd_num) 440 | dd_sequence = tuple(PiPhiGate(phi) for phi in phis) 441 | dd_sequence_lengths = self._compute_dd_sequence_lengths(dd_sequence, dag) 442 | self._compute_spacing(urdd_num) 443 | 444 | if time_interval % self._alignment != 0: 445 | raise TranspilerError( 446 | f"Time interval {time_interval} is not divisible by alignment " 447 | f"{self._alignment} between DAGNode {prev_node.name} on qargs " 448 | f"{prev_node.qargs} and {next_node.name} on qargs {next_node.qargs}." 449 | ) 450 | 451 | if not self.__is_dd_qubit(dag.qubits.index(qubit)): 452 | # Target physical qubit is not the target of this DD sequence. 453 | self._apply_scheduled_op( 454 | dag, t_start, Delay(time_interval, dag.unit), qubit 455 | ) 456 | return 457 | 458 | if self._skip_reset_qubits and ( 459 | isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) 460 | ): 461 | # Previous node is the start edge or reset, i.e. qubit is ground state. 462 | self._apply_scheduled_op( 463 | dag, t_start, Delay(time_interval, dag.unit), qubit 464 | ) 465 | return 466 | 467 | slack = time_interval - np.sum(dd_sequence_lengths[qubit]) 468 | sequence_gphase = self._sequence_phase 469 | 470 | if slack <= 0: 471 | # Interval too short. 472 | self._apply_scheduled_op( 473 | dag, t_start, Delay(time_interval, dag.unit), qubit 474 | ) 475 | return 476 | 477 | if len(dd_sequence) == 1: 478 | # Special case of using a single gate for DD 479 | u_inv = dd_sequence[0].inverse().to_matrix() 480 | theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase( 481 | u_inv 482 | ) 483 | if isinstance(next_node, DAGOpNode) and isinstance( 484 | next_node.op, (UGate, U3Gate) 485 | ): 486 | # Absorb the inverse into the successor (from left in circuit) 487 | theta_r, phi_r, lam_r = next_node.op.params 488 | next_node.op.params = Optimize1qGates.compose_u3( 489 | theta_r, phi_r, lam_r, theta, phi, lam 490 | ) 491 | sequence_gphase += phase 492 | elif isinstance(prev_node, DAGOpNode) and isinstance( 493 | prev_node.op, (UGate, U3Gate) 494 | ): 495 | # Absorb the inverse into the predecessor (from right in circuit) 496 | theta_l, phi_l, lam_l = prev_node.op.params 497 | prev_node.op.params = Optimize1qGates.compose_u3( 498 | theta, phi, lam, theta_l, phi_l, lam_l 499 | ) 500 | sequence_gphase += phase 501 | else: 502 | # Don't do anything if there's no single-qubit gate to absorb the inverse 503 | self._apply_scheduled_op( 504 | dag, t_start, Delay(time_interval, dag.unit), qubit 505 | ) 506 | return 507 | 508 | def _constrained_length(values): 509 | return self._alignment * np.floor(values / self._alignment) 510 | 511 | # (1) Compute DD intervals satisfying the constraint 512 | taus = _constrained_length(slack * np.asarray(self._spacing)) 513 | extra_slack = slack - np.sum(taus) 514 | 515 | # (2) Distribute extra slack 516 | if self._extra_slack_distribution == "middle": 517 | mid_ind = int((len(taus) - 1) / 2) 518 | to_middle = _constrained_length(extra_slack) 519 | taus[mid_ind] += to_middle 520 | if extra_slack - to_middle: 521 | # If to_middle is not a multiple value of the pulse alignment, 522 | # it is truncated to the nearlest multiple value and 523 | # the rest of slack is added to the end. 524 | taus[-1] += extra_slack - to_middle 525 | elif self._extra_slack_distribution == "edges": 526 | to_begin_edge = _constrained_length(extra_slack / 2) 527 | taus[0] += to_begin_edge 528 | taus[-1] += extra_slack - to_begin_edge 529 | else: 530 | raise TranspilerError( 531 | f"Option extra_slack_distribution = {self._extra_slack_distribution} " 532 | f"is invalid." 533 | ) 534 | 535 | # (3) Construct DD sequence with delays 536 | num_elements = max(len(dd_sequence), len(taus)) 537 | idle_after = t_start 538 | for dd_ind in range(num_elements): 539 | if dd_ind < len(taus): 540 | tau = taus[dd_ind] 541 | if tau > 0: 542 | self._apply_scheduled_op( 543 | dag, idle_after, Delay(tau, dag.unit), qubit 544 | ) 545 | idle_after += tau 546 | if dd_ind < len(dd_sequence): 547 | gate = dd_sequence[dd_ind] 548 | gate_length = dd_sequence_lengths[qubit][dd_ind] 549 | self._apply_scheduled_op(dag, idle_after, gate, qubit) 550 | idle_after += gate_length 551 | 552 | dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase) 553 | 554 | @staticmethod 555 | def _mod_2pi(angle: float, atol: float = 0): 556 | """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" 557 | wrapped = (angle + np.pi) % (2 * np.pi) - np.pi 558 | if abs(wrapped - np.pi) < atol: 559 | wrapped = -np.pi 560 | return wrapped 561 | -------------------------------------------------------------------------------- /qiskit_research/utils/gate_decompositions.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Gate decompositions.""" 12 | 13 | from __future__ import annotations 14 | 15 | from collections.abc import Iterator 16 | from math import pi 17 | 18 | from qiskit import QuantumRegister 19 | from qiskit.circuit import ControlledGate, Gate, Qubit 20 | from qiskit.circuit.library import ( 21 | CXGate, 22 | HGate, 23 | RXGate, 24 | RZGate, 25 | RZXGate, 26 | SdgGate, 27 | SGate, 28 | XGate, 29 | XXMinusYYGate, 30 | XXPlusYYGate, 31 | ) 32 | from qiskit.dagcircuit import DAGCircuit 33 | from qiskit.transpiler import Target 34 | from qiskit.transpiler.basepasses import TransformationPass 35 | 36 | from .gates import SECRGate 37 | 38 | 39 | class RZXtoEchoedCR(TransformationPass): 40 | """ 41 | Class for the RZXGate to echoed cross resonance gate pass. The RZXGate 42 | is equivalent to the SECR gate plus a second XGate on the control qubit 43 | to return it to the initial state. 44 | 45 | See: https://arxiv.org/abs/1603.04821 46 | """ 47 | 48 | def __init__( 49 | self, 50 | target: Target, 51 | ): 52 | super().__init__() 53 | self._coupling_map = target.build_coupling_map() 54 | 55 | def run( 56 | self, 57 | dag: DAGCircuit, 58 | ) -> DAGCircuit: 59 | for rzx_run in dag.collect_runs(["rzx"]): 60 | control, _ = dag.find_bit(rzx_run[0].qargs[0]) 61 | target, _ = dag.find_bit(rzx_run[0].qargs[1]) 62 | # cr_forward_dir = cr_forward_direction( 63 | # control, target, self._inst_map, self._ctrl_chans 64 | # ) 65 | if (control, target) in self._coupling_map: 66 | cr_forward_dir = (control, target) 67 | else: 68 | cr_forward_dir = (target, control) 69 | 70 | for node in rzx_run: 71 | mini_dag = DAGCircuit() 72 | q0, q1 = QuantumRegister(2) 73 | mini_dag.add_qubits([q0, q1]) 74 | 75 | rzx_angle = node.op.params[0] 76 | 77 | if cr_forward_dir: 78 | mini_dag.apply_operation_back(SECRGate(rzx_angle), [q0, q1]) 79 | mini_dag.apply_operation_back(XGate(), [q0]) 80 | else: 81 | mini_dag.apply_operation_back(HGate(), [q0]) 82 | mini_dag.apply_operation_back(HGate(), [q1]) 83 | mini_dag.apply_operation_back(SECRGate(rzx_angle), [q1, q0]) 84 | mini_dag.apply_operation_back(XGate(), [q1]) 85 | mini_dag.apply_operation_back(HGate(), [q0]) 86 | mini_dag.apply_operation_back(HGate(), [q1]) 87 | 88 | dag.substitute_node_with_dag(node, mini_dag) 89 | 90 | return dag 91 | 92 | 93 | class ControlledRZZToCX(TransformationPass): 94 | """Transformation pass to decompose Controlled RZZGate to CXGate.""" 95 | 96 | def _decomposition( 97 | self, 98 | register: QuantumRegister, 99 | gate: ControlledGate, 100 | ) -> Iterator[tuple[Gate, tuple[Qubit, ...]]]: 101 | a, b, c = register 102 | (theta,) = gate.params 103 | 104 | yield CXGate(), (b, c) 105 | yield RZGate(theta).control(1), (a, c) 106 | yield CXGate(), (b, c) 107 | 108 | def run( 109 | self, 110 | dag: DAGCircuit, 111 | ) -> DAGCircuit: 112 | for run in dag.collect_runs(["crzz"]): 113 | for node in run: 114 | mini_dag = DAGCircuit() 115 | register = QuantumRegister(3) 116 | mini_dag.add_qreg(register) 117 | 118 | for instr, qargs in self._decomposition(register, node.op): 119 | mini_dag.apply_operation_back(instr, qargs) 120 | 121 | dag.substitute_node_with_dag(node, mini_dag) 122 | 123 | return dag 124 | 125 | 126 | class XXPlusYYtoRZX(TransformationPass): 127 | """Transformation pass to decompose XXPlusYYGate to RZXGate.""" 128 | 129 | def _decomposition( 130 | self, 131 | register: QuantumRegister, 132 | gate: XXPlusYYGate, 133 | ) -> Iterator[tuple[Gate, tuple[Qubit, ...]]]: 134 | a, b = register 135 | theta, beta = gate.params 136 | 137 | yield RZGate(-beta), (b,) 138 | 139 | yield HGate(), (a,) 140 | yield HGate(), (b,) 141 | 142 | yield RZGate(-0.5 * pi), (b,) 143 | yield RXGate(-0.5 * pi), (b,) 144 | yield RZGate(-0.5 * pi), (b,) 145 | yield RZXGate(0.5 * theta), (a, b) 146 | yield RXGate(0.5 * theta), (b,) 147 | yield RZGate(-0.5 * pi), (b,) 148 | yield RXGate(-0.5 * pi), (b,) 149 | yield RZGate(-0.5 * pi), (b,) 150 | yield RZGate(-0.5 * theta), (b,) 151 | 152 | yield RZGate(0.5 * pi), (a,) 153 | yield HGate(), (a,) 154 | yield RZGate(0.5 * pi), (b,) 155 | yield HGate(), (b,) 156 | 157 | yield RZGate(-0.5 * pi), (b,) 158 | yield RXGate(-0.5 * pi), (b,) 159 | yield RZGate(-0.5 * pi), (b,) 160 | yield RZXGate(0.5 * theta), (a, b) 161 | yield RXGate(0.5 * theta), (b,) 162 | yield RZGate(-0.5 * pi), (b,) 163 | yield RXGate(-0.5 * pi), (b,) 164 | yield RZGate(-0.5 * pi), (b,) 165 | yield RZGate(-0.5 * theta), (b,) 166 | 167 | yield HGate(), (a,) 168 | yield RZGate(-0.5 * pi), (a,) 169 | yield HGate(), (a,) 170 | yield HGate(), (b,) 171 | yield RZGate(-0.5 * pi), (b,) 172 | yield HGate(), (b,) 173 | 174 | yield RZGate(beta), (b,) 175 | 176 | def run( 177 | self, 178 | dag: DAGCircuit, 179 | ) -> DAGCircuit: 180 | for run in dag.collect_runs(["xx_plus_yy"]): 181 | for node in run: 182 | mini_dag = DAGCircuit() 183 | register = QuantumRegister(2) 184 | mini_dag.add_qreg(register) 185 | 186 | for instr, qargs in self._decomposition(register, node.op): 187 | mini_dag.apply_operation_back(instr, qargs) 188 | 189 | dag.substitute_node_with_dag(node, mini_dag) 190 | 191 | return dag 192 | 193 | 194 | class XXMinusYYtoRZX(TransformationPass): 195 | """Transformation pass to decompose XXMinusYYGate to RZXGate.""" 196 | 197 | def _decomposition( 198 | self, 199 | register: QuantumRegister, 200 | gate: XXMinusYYGate, 201 | ) -> Iterator[tuple[Gate, tuple[Qubit, ...]]]: 202 | a, b = register 203 | theta, beta = gate.params 204 | 205 | yield RZGate(-beta), (b,) 206 | 207 | yield HGate(), (a,) 208 | yield HGate(), (b,) 209 | 210 | yield RZGate(-0.5 * pi), (b,) 211 | yield RXGate(-0.5 * pi), (b,) 212 | yield RZGate(-0.5 * pi), (b,) 213 | yield RZXGate(0.5 * theta), (a, b) 214 | yield RXGate(-0.5 * theta), (b,) 215 | yield RZGate(-0.5 * pi), (b,) 216 | yield RXGate(-0.5 * pi), (b,) 217 | yield RZGate(-0.5 * pi), (b,) 218 | yield RZGate(0.5 * theta), (b,) 219 | 220 | yield RZGate(0.5 * pi), (a,) 221 | yield HGate(), (a,) 222 | yield RZGate(0.5 * pi), (b,) 223 | yield HGate(), (b,) 224 | 225 | yield RZGate(-0.5 * pi), (b,) 226 | yield RXGate(-0.5 * pi), (b,) 227 | yield RZGate(-0.5 * pi), (b,) 228 | yield RZXGate(-0.5 * theta), (a, b) 229 | yield RXGate(0.5 * theta), (b,) 230 | yield RZGate(-0.5 * pi), (b,) 231 | yield RXGate(-0.5 * pi), (b,) 232 | yield RZGate(-0.5 * pi), (b,) 233 | yield RZGate(-0.5 * theta), (b,) 234 | 235 | yield HGate(), (a,) 236 | yield RZGate(-0.5 * pi), (a,) 237 | yield HGate(), (a,) 238 | yield HGate(), (b,) 239 | yield RZGate(-0.5 * pi), (b,) 240 | yield HGate(), (b,) 241 | 242 | yield RZGate(beta), (b,) 243 | 244 | def run( 245 | self, 246 | dag: DAGCircuit, 247 | ) -> DAGCircuit: 248 | for run in dag.collect_runs(["xx_minus_yy"]): 249 | for node in run: 250 | mini_dag = DAGCircuit() 251 | register = QuantumRegister(2) 252 | mini_dag.add_qreg(register) 253 | 254 | for instr, qargs in self._decomposition(register, node.op): 255 | mini_dag.apply_operation_back(instr, qargs) 256 | 257 | dag.substitute_node_with_dag(node, mini_dag) 258 | 259 | return dag 260 | 261 | 262 | class RZXWeylDecomposition(TransformationPass): 263 | """ 264 | Decompose XX, YY, ZZ rotation gates using the Weyl Chamber 265 | decomposition, this version accepts Parameter values. 266 | 267 | See https://arxiv.org/abs/2105.01063 268 | """ 269 | 270 | def run(self, dag: DAGCircuit) -> DAGCircuit: 271 | for run in dag.collect_runs(["rxx", "ryy", "rzz"]): 272 | for node in run: 273 | mini_dag = DAGCircuit() 274 | register = QuantumRegister(2) 275 | mini_dag.add_qreg(register) 276 | 277 | angle = node.op.params[0] 278 | 279 | if node.op.name == "rxx": 280 | mini_dag.apply_operation_back(HGate(), [register[0]]) 281 | mini_dag.apply_operation_back( 282 | RZXGate(angle), [register[0], register[1]] 283 | ) 284 | mini_dag.apply_operation_back(HGate(), [register[0]]) 285 | elif node.op.name == "ryy": 286 | mini_dag.apply_operation_back(SdgGate(), [register[0]]) 287 | mini_dag.apply_operation_back(SdgGate(), [register[1]]) 288 | mini_dag.apply_operation_back(HGate(), [register[0]]) 289 | mini_dag.apply_operation_back( 290 | RZXGate(angle), [register[0], register[1]] 291 | ) 292 | mini_dag.apply_operation_back(HGate(), [register[0]]) 293 | mini_dag.apply_operation_back(SGate(), [register[0]]) 294 | mini_dag.apply_operation_back(SGate(), [register[1]]) 295 | elif node.op.name == "rzz": 296 | mini_dag.apply_operation_back(HGate(), [register[1]]) 297 | mini_dag.apply_operation_back( 298 | RZXGate(angle), [register[0], register[1]] 299 | ) 300 | mini_dag.apply_operation_back(HGate(), [register[1]]) 301 | 302 | dag.substitute_node_with_dag(node, mini_dag) 303 | 304 | return dag 305 | -------------------------------------------------------------------------------- /qiskit_research/utils/gates.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Gates.""" 12 | 13 | from __future__ import annotations 14 | 15 | from typing import Optional 16 | from math import pi 17 | 18 | import numpy as np 19 | from qiskit import QuantumCircuit, QuantumRegister 20 | from qiskit.circuit.gate import Gate 21 | from qiskit.circuit.library import RZXGate, RZGate, U3Gate, XGate 22 | from qiskit.circuit.parameterexpression import ParameterValueType 23 | 24 | 25 | class XpGate(Gate): 26 | r"""The single-qubit Pauli-X gate (:math:`\sigma_x`), implemented 27 | via RX(\pi). 28 | """ 29 | 30 | def __init__(self, label: Optional[str] = None): 31 | """Create new Xp gate.""" 32 | super().__init__("xp", 1, [], label=label) 33 | 34 | def _define(self): 35 | """ 36 | gate xp a { u3(pi,0,pi) a; } 37 | """ 38 | q = QuantumRegister(1, "q") 39 | qc = QuantumCircuit(q, name=self.name) 40 | rules = [(U3Gate(pi, 0, pi), [q[0]], [])] 41 | for instr, qargs, cargs in rules: 42 | qc.append(instr, qargs, cargs) 43 | 44 | self.definition = qc 45 | 46 | def __array__(self, dtype=None): 47 | """Gate matrix.""" 48 | return np.array([[0, 1], [1, 0]], dtype=dtype) 49 | 50 | 51 | class XmGate(Gate): 52 | r"""The single-qubit Pauli-X gate (:math:`\sigma_x`), implemented 53 | via RX(-\pi). 54 | """ 55 | 56 | def __init__(self, label: Optional[str] = None): 57 | """Create new Xm gate.""" 58 | super().__init__("xm", 1, [], label=label) 59 | 60 | def _define(self): 61 | """ 62 | gate xm a { u3(pi,0,pi) a; } 63 | """ 64 | q = QuantumRegister(1, "q") 65 | qc = QuantumCircuit(q, name=self.name) 66 | rules = [(U3Gate(pi, 0, pi), [q[0]], [])] 67 | for instr, qargs, cargs in rules: 68 | qc.append(instr, qargs, cargs) 69 | 70 | self.definition = qc 71 | 72 | def __array__(self, dtype=None): 73 | """Gate matrix""" 74 | return np.array([[0, 1], [1, 0]], dtype=dtype) 75 | 76 | 77 | class YpGate(Gate): 78 | r"""The single-qubit Pauli-Y gate (:math:`\sigma_y`), implemented 79 | via RY(\pi). 80 | """ 81 | 82 | def __init__(self, label: Optional[str] = None): 83 | """Create new Yp gate.""" 84 | super().__init__("yp", 1, [], label=label) 85 | 86 | def _define(self): 87 | q = QuantumRegister(1, "q") 88 | qc = QuantumCircuit(q, name=self.name) 89 | rules = [(U3Gate(pi, pi / 2, pi / 2), [q[0]], [])] 90 | for instr, qargs, cargs in rules: 91 | qc.append(instr, qargs, cargs) 92 | 93 | self.definition = qc 94 | 95 | def __array__(self, dtype=None): 96 | """Gate matrix.""" 97 | return np.array([[0, -1j], [1j, 0]], dtype=dtype) 98 | 99 | 100 | class YmGate(Gate): 101 | r"""The single-qubit Pauli-Y gate (:math:`\sigma_y`), implemented 102 | via RY(-\pi). 103 | """ 104 | 105 | def __init__(self, label: Optional[str] = None): 106 | """Create new Ym gate.""" 107 | super().__init__("ym", 1, [], label=label) 108 | 109 | def _define(self): 110 | q = QuantumRegister(1, "q") 111 | qc = QuantumCircuit(q, name=self.name) 112 | rules = [(U3Gate(pi, pi / 2, pi / 2), [q[0]], [])] 113 | for instr, qargs, cargs in rules: 114 | qc.append(instr, qargs, cargs) 115 | 116 | self.definition = qc 117 | 118 | def __array__(self, dtype=None): 119 | """Gate matrix.""" 120 | return np.array([[0, -1j], [1j, 0]], dtype=dtype) 121 | 122 | 123 | class PiPhiGate(Gate): 124 | r""" 125 | Rotated X gate. 126 | 127 | The 180-degree rotation about an axis offset by an angle :math:`\\phi` relative 128 | to the X-axis in the XY-plane of the Bloch sphere. 129 | """ 130 | 131 | def __init__( 132 | self, 133 | phi: ParameterValueType, 134 | label: Optional[str] = None, 135 | ): 136 | """Create new PiPhi gate.""" 137 | super().__init__("pi_phi", 1, [phi], label=label) 138 | self.phi = phi 139 | 140 | def _define(self): 141 | q = QuantumRegister(1, "q") 142 | qc = QuantumCircuit(q, name=self.name) 143 | rules = [ 144 | (RZGate(self.phi), [q[0]], []), 145 | (XGate(), [q[0]], []), 146 | (RZGate(-self.phi), [q[0]], []), 147 | ] 148 | for instr, qargs, cargs in rules: 149 | qc.append(instr, qargs, cargs) 150 | 151 | self.definition = qc 152 | 153 | def __array__(self, dtype=None): 154 | """Gate matrix.""" 155 | return np.cos(self.phi) * np.array([[0, 1], [1, 0]], dtype=dtype) + np.sin( 156 | self.phi 157 | ) * np.array([[0, -1j], [1j, 0]], dtype=dtype) 158 | 159 | 160 | class SECRGate(Gate): 161 | r"""The scaled echoed cross resonance gate, as detailed in 162 | https://arxiv.org/abs/1603.04821 and 163 | https://arxiv.org/abs/2202.12910. 164 | 165 | Definitions are derived by appending an XGate() to q0 166 | of an RZXGate. 167 | """ 168 | 169 | def __init__(self, theta: ParameterValueType, label: Optional[str] = None): 170 | """Create new SECR gate.""" 171 | super().__init__("secr", 2, [theta], label=label) 172 | 173 | def _define(self): 174 | """ 175 | gate secr(theta) a, b { h b; cx a, b; u1(theta) b; cx a, b; h b; x a} 176 | """ 177 | theta = self.params[0] 178 | q = QuantumRegister(2, "q") 179 | qc = QuantumCircuit(q, name=self.name) 180 | rules = [(RZXGate(theta), [q[0], q[1]], []), (XGate(), [q[0]], [])] 181 | for instr, qargs, cargs in rules: 182 | qc.append(instr, qargs, cargs) 183 | 184 | self.definition = qc 185 | 186 | def __array__(self, dtype=None): 187 | """Gate matrix.""" 188 | half_theta = float(self.params[0]) / 2 189 | cos = np.cos(half_theta) 190 | isin = 1j * np.sin(half_theta) 191 | return np.array( 192 | [ 193 | [0, cos, 0, isin], 194 | [cos, 0, -isin, 0], 195 | [0, isin, 0, cos], 196 | [-isin, 0, cos, 0], 197 | ], 198 | dtype=dtype, 199 | ) 200 | -------------------------------------------------------------------------------- /qiskit_research/utils/pauli_twirling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Pauli twirling.""" 12 | 13 | from typing import Any, Iterable, Optional 14 | import itertools 15 | import cmath 16 | 17 | import numpy as np 18 | from qiskit.circuit import QuantumRegister, QuantumCircuit 19 | from qiskit.circuit.library import ( 20 | IGate, 21 | XGate, 22 | YGate, 23 | ZGate, 24 | CXGate, 25 | CYGate, 26 | CZGate, 27 | CHGate, 28 | CSGate, 29 | DCXGate, 30 | CSXGate, 31 | CSdgGate, 32 | ECRGate, 33 | iSwapGate, 34 | SwapGate, 35 | ) 36 | from qiskit.dagcircuit import DAGCircuit 37 | from qiskit.transpiler.basepasses import BasePass, TransformationPass 38 | from qiskit.transpiler.passes import ( 39 | CXCancellation, 40 | Optimize1qGatesDecomposition, 41 | ) 42 | from qiskit.quantum_info import Pauli, Operator, pauli_basis 43 | from qiskit_research.utils.pulse_scaling import BASIS_GATES 44 | 45 | # Single qubit Pauli gates 46 | I = IGate() 47 | X = XGate() 48 | Y = YGate() 49 | Z = ZGate() 50 | 51 | # 2Q entangling gates 52 | CX = CXGate() # cnot; controlled-X 53 | CY = CYGate() # controlled-Y 54 | CZ = CZGate() # controlled-Z 55 | CH = CHGate() # controlled-Hadamard 56 | CS = CSGate() # controlled-S 57 | DCX = DCXGate() # double cnot 58 | CSX = CSXGate() # controlled sqrt X 59 | CSdg = CSdgGate() # controlled S^dagger 60 | ECR = ECRGate() # echoed cross-resonance 61 | Swap = SwapGate() # swap 62 | iSwap = iSwapGate() # imaginary swap 63 | 64 | # this list consists of the 2-qubit rotation gates 65 | TWO_QUBIT_PAULI_GENERATORS = { 66 | "rxx": Pauli("XX"), 67 | "ryy": Pauli("YY"), 68 | "rzx": Pauli("XZ"), 69 | "rzz": Pauli("ZZ"), 70 | "secr": Pauli("XZ"), 71 | } 72 | 73 | 74 | def match_global_phase(a, b): 75 | """Phase the given arrays so that their phases match at one entry. 76 | 77 | Args: 78 | a: A Numpy array. 79 | b: Another Numpy array. 80 | 81 | Returns: 82 | A pair of arrays (a', b') that are equal if and only if a == b * exp(i phi) 83 | for some real number phi. 84 | """ 85 | if a.shape != b.shape: 86 | return a, b 87 | # use the largest entry of one of the matrices to maximize precision 88 | index = max(np.ndindex(*a.shape), key=lambda i: abs(b[i])) 89 | phase_a = cmath.phase(a[index]) 90 | phase_b = cmath.phase(b[index]) 91 | return a * cmath.rect(1, -phase_a), b * cmath.rect(1, -phase_b) 92 | 93 | 94 | def allclose_up_to_global_phase(a, b, rtol=1e-05, atol=1e-08, equal_nan=False): 95 | """Check if two operators are close up to a global phase.""" 96 | # Phase both operators to match their phases 97 | phased_op1, phased_op2 = match_global_phase(a, b) 98 | return np.allclose(phased_op1, phased_op2, rtol, atol, equal_nan) 99 | 100 | 101 | def create_pauli_twirling_sets(two_qubit_gate): 102 | """Generate the Pauli twirling sets for a given 2Q gate. 103 | 104 | Sets are ordered such that gate[0] and gate[1] are pre-rotations 105 | applied to control and target, respectively. gate[2] and gate[3] 106 | are post-rotations for control and target, respectively. 107 | 108 | Parameters: 109 | two_qubit_gate (Gate): Input two-qubit gate 110 | 111 | Returns: 112 | tuple: Tuple of all twirling gate sets 113 | """ 114 | 115 | target_unitary = np.array(two_qubit_gate) 116 | twirling_sets = [] 117 | 118 | # Generate combinations of 4 gates from the operator list 119 | for gates in itertools.product(itertools.product([I, X, Y, Z], repeat=2), repeat=2): 120 | qc = _build_twirl_circuit(gates, two_qubit_gate) 121 | qc_array = Operator.from_circuit(qc).to_matrix() 122 | if allclose_up_to_global_phase(qc_array, target_unitary): 123 | twirling_sets.append(gates) 124 | 125 | return tuple(twirling_sets) 126 | 127 | 128 | def _build_twirl_circuit(gates, two_qubit_gate): 129 | """Build the twirled quantum circuit with specified gates.""" 130 | qc = QuantumCircuit(2) 131 | 132 | qc.append(gates[0][0], [0]) 133 | qc.append(gates[0][1], [1]) 134 | qc.append(two_qubit_gate, [0, 1]) 135 | qc.append(gates[1][0], [0]) 136 | qc.append(gates[1][1], [1]) 137 | 138 | return qc 139 | 140 | 141 | # this dictionary stores the twirl sets for each supported gate 142 | # each key is the name of a supported gate 143 | # each value is a tuple that represents the twirl set for the gate 144 | # the twirl set is a list of (before, after) pairs describing twirl gates 145 | # "before" and "after" are tuples of single-qubit gates to be applied 146 | # before and after the gate to be twirled 147 | TWIRL_GATES = { 148 | "cx": create_pauli_twirling_sets(CX), 149 | "cy": create_pauli_twirling_sets(CY), 150 | "cz": create_pauli_twirling_sets(CZ), 151 | "ch": create_pauli_twirling_sets(CH), 152 | "cs": create_pauli_twirling_sets(CS), 153 | "dcx": create_pauli_twirling_sets(DCX), 154 | "csx": create_pauli_twirling_sets(CSX), 155 | "csdg": create_pauli_twirling_sets(CSdg), 156 | "ecr": create_pauli_twirling_sets(ECR), 157 | "swap": create_pauli_twirling_sets(Swap), 158 | "iswap": create_pauli_twirling_sets(iSwap), 159 | } 160 | 161 | 162 | def parse_random_seed(seed: Any) -> np.random.Generator: 163 | """Parse a random number generator seed and return a Generator.""" 164 | if isinstance(seed, np.random.Generator): 165 | return seed 166 | return np.random.default_rng(seed) 167 | 168 | 169 | class PauliTwirl(TransformationPass): 170 | """Add Pauli twirls.""" 171 | 172 | def __init__( 173 | self, 174 | gates_to_twirl: Optional[Iterable[str]] = None, 175 | seed: Any = None, 176 | ): 177 | """ 178 | Args: 179 | gates_to_twirl: Names of gates to twirl. The default behavior is to twirl all 180 | supported gates. 181 | seed: Seed for the pseudorandom number generator. 182 | """ 183 | if gates_to_twirl is None: 184 | gates_to_twirl = TWIRL_GATES.keys() | TWO_QUBIT_PAULI_GENERATORS.keys() 185 | self.gates_to_twirl = gates_to_twirl 186 | self.rng = parse_random_seed(seed) 187 | super().__init__() 188 | 189 | def run( 190 | self, 191 | dag: DAGCircuit, 192 | ) -> DAGCircuit: 193 | for run in dag.collect_runs(list(self.gates_to_twirl)): 194 | for node in run: 195 | if node.op.name in TWO_QUBIT_PAULI_GENERATORS: 196 | mini_dag = DAGCircuit() 197 | q0, q1 = node.qargs 198 | mini_dag.add_qubits([q0, q1]) 199 | 200 | theta = node.op.params[0] 201 | this_pauli = Pauli( 202 | self.rng.choice(pauli_basis(2).to_labels()) 203 | ).to_instruction() 204 | if TWO_QUBIT_PAULI_GENERATORS[node.op.name].anticommutes( 205 | this_pauli 206 | ): 207 | theta *= -1 208 | 209 | new_op = node.op.copy() 210 | new_op.params[0] = theta 211 | 212 | mini_dag.apply_operation_back(this_pauli, [q0, q1]) 213 | mini_dag.apply_operation_back(new_op, [q0, q1]) 214 | if node.op.name == "secr": 215 | mini_dag.apply_operation_back(X, [q0]) 216 | mini_dag.apply_operation_back(this_pauli, [q0, q1]) 217 | if node.op.name == "secr": 218 | mini_dag.apply_operation_back(X, [q0]) 219 | 220 | dag.substitute_node_with_dag(node, mini_dag, wires=[q0, q1]) 221 | 222 | elif node.op.name in TWIRL_GATES: 223 | twirl_gates = TWIRL_GATES[node.op.name] 224 | (before0, before1), (after0, after1) = twirl_gates[ 225 | self.rng.integers(len(twirl_gates)) 226 | ] 227 | mini_dag = DAGCircuit() 228 | register = QuantumRegister(2) 229 | mini_dag.add_qreg(register) 230 | mini_dag.apply_operation_back(before0, [register[0]]) 231 | mini_dag.apply_operation_back(before1, [register[1]]) 232 | mini_dag.apply_operation_back(node.op, [register[0], register[1]]) 233 | mini_dag.apply_operation_back(after0, [register[0]]) 234 | mini_dag.apply_operation_back(after1, [register[1]]) 235 | dag.substitute_node_with_dag(node, mini_dag) 236 | else: 237 | raise TypeError(f"Unknown how to twirl Instruction {node.op}.") 238 | return dag 239 | 240 | 241 | def pauli_transpilation_passes() -> Iterable[BasePass]: 242 | "Yield simple transpilation steps after addition of Pauli gates." 243 | yield Optimize1qGatesDecomposition(BASIS_GATES) 244 | yield CXCancellation() 245 | -------------------------------------------------------------------------------- /qiskit_research/utils/periodic_dynamical_decoupling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2021. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | # 11 | # Modified from qiskit.transpiler.passes.scheduling.padding.dynamical_decoupling 12 | # https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py 13 | 14 | """Dynamical Decoupling insertion pass.""" 15 | 16 | from copy import copy 17 | from typing import List, Optional 18 | 19 | import numpy as np 20 | from qiskit.circuit import Qubit, Gate 21 | from qiskit.circuit.delay import Delay 22 | from qiskit.circuit.library.standard_gates import IGate 23 | from qiskit.circuit.reset import Reset 24 | from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode 25 | from qiskit.quantum_info.operators.predicates import matrix_equal 26 | from qiskit.transpiler.exceptions import TranspilerError 27 | from qiskit.transpiler.instruction_durations import InstructionDurations 28 | 29 | from qiskit.transpiler.passes.scheduling.padding.base_padding import BasePadding 30 | 31 | 32 | class PeriodicDynamicalDecoupling(BasePadding): 33 | """Dynamical decoupling insertion pass. 34 | 35 | This pass works on a scheduled, physical circuit. It scans the circuit for 36 | idle periods of time (i.e. those containing delay instructions) and inserts 37 | a DD sequence of gates in those spots. These gates amount to the identity, 38 | so do not alter the logical action of the circuit, but have the effect of 39 | mitigating decoherence in those idle periods. 40 | 41 | This pass will attempt to repeat the DD sequence as many times as possible 42 | up until ``max_repeats`` repetitions has been met, subject to the constraint 43 | that the average delay between each gate in a DD sequence is greater than 44 | ``avg_min_delay``. The average delay is calculated by dividing the delay 45 | time by the total number of gates in a sequence. 46 | 47 | As a special case, the pass allows a length-1 sequence (e.g. [XGate()]). 48 | In this case the DD insertion happens only when the gate inverse can be 49 | absorbed into a neighboring gate in the circuit (so we would still be 50 | replacing Delay with something that is equivalent to the identity). 51 | This can be used, for instance, as a Hahn echo. 52 | 53 | This pass ensures that the inserted sequence preserves the circuit exactly 54 | (including global phase). 55 | 56 | .. jupyter-execute:: 57 | 58 | import numpy as np 59 | from qiskit.circuit import QuantumCircuit 60 | from qiskit.circuit.library import XGate 61 | from qiskit.transpiler import PassManager, InstructionDurations 62 | from qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling 63 | from qiskit.visualization import timeline_drawer 64 | circ = QuantumCircuit(4) 65 | circ.h(0) 66 | circ.cx(0, 1) 67 | circ.cx(1, 2) 68 | circ.cx(2, 3) 69 | circ.measure_all() 70 | durations = InstructionDurations( 71 | [("h", 0, 50), ("cx", [0, 1], 700), ("reset", None, 10), 72 | ("cx", [1, 2], 200), ("cx", [2, 3], 300), 73 | ("x", None, 50), ("measure", None, 1000)] 74 | ) 75 | 76 | .. jupyter-execute:: 77 | 78 | # balanced X-X sequence on all qubits 79 | dd_sequence = [XGate(), XGate()] 80 | pm = PassManager([ALAPScheduleAnalysis(durations), 81 | PadDynamicalDecoupling(durations, dd_sequence)]) 82 | circ_dd = pm.run(circ) 83 | timeline_drawer(circ_dd) 84 | 85 | .. jupyter-execute:: 86 | 87 | # Uhrig sequence on qubit 0 88 | n = 8 89 | dd_sequence = [XGate()] * n 90 | def uhrig_pulse_location(k): 91 | return np.sin(np.pi * (k + 1) / (2 * n + 2)) ** 2 92 | spacing = [] 93 | for k in range(n): 94 | spacing.append(uhrig_pulse_location(k) - sum(spacing)) 95 | spacing.append(1 - sum(spacing)) 96 | pm = PassManager( 97 | [ 98 | ALAPScheduleAnalysis(durations), 99 | PadDynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), 100 | ] 101 | ) 102 | circ_dd = pm.run(circ) 103 | timeline_drawer(circ_dd) 104 | 105 | .. note:: 106 | 107 | You may need to call alignment pass before running dynamical decoupling to guarantee 108 | your circuit satisfies acquisition alignment constraints. 109 | """ 110 | 111 | def __init__( 112 | self, 113 | durations: InstructionDurations, 114 | base_dd_sequence: List[Gate], 115 | qubits: Optional[List[int]] = None, 116 | base_spacing: Optional[List[float]] = None, 117 | avg_min_delay: Optional[int] = None, 118 | max_repeats: int = 1, 119 | skip_reset_qubits: bool = True, 120 | pulse_alignment: int = 1, 121 | extra_slack_distribution: str = "middle", 122 | ): 123 | """Dynamical decoupling initializer. 124 | 125 | Args: 126 | durations: Durations of instructions to be used in scheduling. 127 | base_dd_sequence: Base sequence of gates to apply repeatedly in idle spots. 128 | qubits: Physical qubits on which to apply DD. 129 | If None, all qubits will undergo DD (when possible). 130 | base_spacing: A list of spacings between the DD gates. 131 | The available slack will be divided according to this. 132 | The list length must be one more than the length of base_dd_sequence, 133 | and the elements must sum to 1. If None, a balanced spacing 134 | will be used [d/2, d, d, ..., d, d, d/2]. 135 | avg_min_delay: A duration such that delay time between gates will not be lower than 136 | this. If None, then this is set equal to ``pulse_alignment`` 137 | max_repeats: Will attempt to repeat the DD sequence this number of times, provided that 138 | the ``avg_min_delay`` condition is met 139 | skip_reset_qubits: If True, does not insert DD on idle periods that 140 | immediately follow initialized/reset qubits 141 | (as qubits in the ground state are less susceptile to decoherence). 142 | pulse_alignment: The hardware constraints for gate timing allocation. 143 | This is usually provided from ``backend.configuration().timing_constraints``. 144 | If provided, the delay length, i.e. ``spacing``, is implicitly adjusted to 145 | satisfy this constraint. 146 | extra_slack_distribution: The option to control the behavior of DD sequence generation. 147 | The duration of the DD sequence should be identical to an idle time in the 148 | scheduled quantum circuit, however, the delay in between gates comprising the 149 | sequence should be integer number in units of dt, and it might be further truncated 150 | when ``pulse_alignment`` is specified. This sometimes results in the duration of 151 | the created sequence being shorter than the idle time 152 | that you want to fill with the sequence, i.e. `extra slack`. 153 | This option takes following values. 154 | 155 | - "middle": Put the extra slack to the interval at the middle of the sequence. 156 | - "edges": Divide the extra slack as evenly as possible into 157 | intervals at beginning and end of the sequence. 158 | 159 | Raises: 160 | TranspilerError: When invalid DD sequence is specified. 161 | TranspilerError: When pulse gate with the duration which is 162 | non-multiple of the alignment constraint value is found. 163 | """ 164 | super().__init__() 165 | self._durations = durations 166 | self._base_dd_sequence = base_dd_sequence 167 | self._qubits = qubits 168 | self._skip_reset_qubits = skip_reset_qubits 169 | self._alignment = pulse_alignment 170 | self._base_spacing = base_spacing 171 | self.avg_min_delay = avg_min_delay 172 | if avg_min_delay is None: 173 | self.avg_min_delay = pulse_alignment 174 | self.max_repeats = max_repeats 175 | self._extra_slack_distribution = extra_slack_distribution 176 | 177 | self._base_dd_sequence_lengths: dict[Qubit, list[float]] = {} 178 | self._sequence_phase = 0 179 | 180 | def _pre_runhook(self, dag: DAGCircuit): 181 | super()._pre_runhook(dag) 182 | 183 | num_pulses = len(self._base_dd_sequence) 184 | 185 | # Check if physical circuit is given 186 | if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: 187 | raise TranspilerError("DD runs on physical circuits only.") 188 | 189 | # Set default spacing otherwise validate user input 190 | if self._base_spacing is None: 191 | mid = 1 / num_pulses 192 | end = mid / 2 193 | self._base_spacing = [end] + [mid] * (num_pulses - 1) + [end] 194 | else: 195 | if sum(self._base_spacing) != 1 or any(a < 0 for a in self._base_spacing): 196 | raise TranspilerError( 197 | "The spacings must be given in terms of fractions " 198 | "of the slack period and sum to 1." 199 | ) 200 | if not len(self._base_spacing) == len(self._base_dd_sequence) + 1: 201 | raise TranspilerError( 202 | "The number of spacings must be 1 more than the " 203 | "number of gates in the sequence" 204 | ) 205 | 206 | # Check if DD sequence is identity 207 | if num_pulses % 2 != 0: 208 | raise TranspilerError( 209 | "DD sequence must contain an even number of gates (or 1)." 210 | ) 211 | noop = np.eye(2) 212 | for gate in self._base_dd_sequence: 213 | noop = noop.dot(gate.to_matrix()) 214 | if not matrix_equal(noop, IGate().to_matrix(), ignore_phase=True): 215 | raise TranspilerError( 216 | "The DD sequence does not make an identity operation." 217 | ) 218 | self._sequence_phase = np.angle(noop[0][0]) 219 | 220 | # Precompute qubit-wise DD sequence length for performance 221 | for qubit in dag.qubits: 222 | physical_index = dag.qubits.index(qubit) 223 | if self._qubits and physical_index not in self._qubits: 224 | continue 225 | 226 | sequence_lengths = [] 227 | for gate in self._base_dd_sequence: 228 | try: 229 | # Check calibration. 230 | gate_length = dag.calibrations[gate.name][ 231 | (physical_index, gate.params) 232 | ] 233 | if gate_length % self._alignment != 0: 234 | # This is necessary to implement lightweight scheduling logic for this pass. 235 | # Usually the pulse alignment constraint and pulse data chunk size take 236 | # the same value, however, we can intentionally violate this pattern 237 | # at the gate level. For example, we can create a schedule consisting of 238 | # a pi-pulse of 32 dt followed by a post buffer, i.e. delay, of 4 dt 239 | # on the device with 16 dt constraint. Note that the pi-pulse length 240 | # is multiple of 16 dt but the gate length of 36 is not multiple of it. 241 | # Such pulse gate should be excluded. 242 | raise TranspilerError( 243 | f"Pulse gate {gate.name} with length non-multiple of {self._alignment} " 244 | f"is not acceptable in {self.__class__.__name__} pass." 245 | ) 246 | except KeyError: 247 | gate_length = self._durations.get(gate, physical_index) 248 | sequence_lengths.append(gate_length) 249 | # Update gate duration. This is necessary for current timeline drawer, 250 | # i.e. scheduled. 251 | gate.duration = gate_length 252 | self._base_dd_sequence_lengths[qubit] = sequence_lengths 253 | 254 | def _pad( 255 | self, 256 | dag: DAGCircuit, 257 | qubit: Qubit, 258 | t_start: int, 259 | t_end: int, 260 | next_node: DAGNode, 261 | prev_node: DAGNode, 262 | ): 263 | # This routine takes care of the pulse alignment constraint for the DD sequence. 264 | # Note that the alignment constraint acts on the t0 of the DAGOpNode. 265 | # Now this constrained scheduling problem is simplified to the problem of 266 | # finding a delay amount which is a multiple of the constraint value by assuming 267 | # that the duration of every DAGOpNode is also a multiple of the constraint value. 268 | # 269 | # For example, given the constraint value of 16 and XY4 with 160 dt gates. 270 | # Here we assume current interval is 992 dt. 271 | # 272 | # relative spacing := [0.125, 0.25, 0.25, 0.25, 0.125] 273 | # slack = 992 dt - 4 x 160 dt = 352 dt 274 | # 275 | # unconstraind sequence: 44dt-X1-88dt-Y2-88dt-X3-88dt-Y4-44dt 276 | # constraind sequence : 32dt-X1-80dt-Y2-80dt-X3-80dt-Y4-32dt + extra slack 48 dt 277 | # 278 | # Now we evenly split extra slack into start and end of the sequence. 279 | # The distributed slack should be multiple of 16. 280 | # Start = +16, End += 32 281 | # 282 | # final sequence : 48dt-X1-80dt-Y2-80dt-X3-80dt-Y4-64dt / in total 992 dt 283 | # 284 | # Now we verify t0 of every node starts from multiple of 16 dt. 285 | # 286 | # X1: 48 dt (3 x 16 dt) 287 | # Y2: 48 dt + 160 dt + 80 dt = 288 dt (18 x 16 dt) 288 | # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) 289 | # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) 290 | # 291 | # As you can see, constraints on t0 are all satified without explicit scheduling. 292 | time_interval = t_end - t_start 293 | 294 | if self._qubits and dag.qubits.index(qubit) not in self._qubits: 295 | # Target physical qubit is not the target of this DD sequence. 296 | self._apply_scheduled_op( 297 | dag, t_start, Delay(time_interval, dag.unit), qubit 298 | ) 299 | return 300 | 301 | if self._skip_reset_qubits and ( 302 | isinstance(prev_node, DAGInNode) or isinstance(prev_node.op, Reset) 303 | ): 304 | # Previous node is the start edge or reset, i.e. qubit is ground state. 305 | self._apply_scheduled_op( 306 | dag, t_start, Delay(time_interval, dag.unit), qubit 307 | ) 308 | return 309 | 310 | slack = time_interval - np.sum(self._base_dd_sequence_lengths[qubit]) 311 | sequence_gphase = self._sequence_phase 312 | 313 | if slack <= 0: 314 | # Interval too short. 315 | self._apply_scheduled_op( 316 | dag, t_start, Delay(time_interval, dag.unit), qubit 317 | ) 318 | return 319 | 320 | def _constrained_length(values): 321 | return self._alignment * np.floor(values / self._alignment) 322 | 323 | # Calculates the number of repeats based on inequality: 324 | # avg_min_delay < (time_interval - repeats * _base_dd_sequence_lengths[qubit]) / 325 | # (repeats * len(_base_dd_sequence)) 326 | # The actual number of repeats is the smaller of this value and max_repeats 327 | actual_repeats = int( 328 | min( 329 | [ 330 | np.floor( 331 | time_interval 332 | / ( 333 | self.avg_min_delay * len(self._base_dd_sequence) 334 | + np.sum(self._base_dd_sequence_lengths[qubit]) 335 | ) 336 | ), 337 | self.max_repeats, 338 | ] 339 | ) 340 | ) 341 | if actual_repeats == 0: 342 | # Interval too short 343 | self._apply_scheduled_op( 344 | dag, t_start, Delay(time_interval, dag.unit), qubit 345 | ) 346 | return 347 | actual_dd_sequence_length = ( 348 | self._base_dd_sequence_lengths[qubit] * actual_repeats 349 | ) 350 | actual_slack = time_interval - np.sum(actual_dd_sequence_length) 351 | actual_sequence = self._base_dd_sequence * actual_repeats 352 | sequence_gphase *= actual_repeats 353 | 354 | # Calculate spacings after repeating actual_repeats times 355 | # For each repetition, the last spacing of the original sequence and the first 356 | # spacing of the the next sequence to be appended are added together. 357 | # Then each spacing is divided by the number of actual repeats to ensure 358 | # the sum of the fractions add to 1 359 | actual_spacing = copy(self._base_spacing) 360 | last_spacing = actual_spacing.pop() 361 | extending_spacing = copy(actual_spacing) 362 | extending_spacing[0] += last_spacing 363 | actual_spacing.extend(extending_spacing * (actual_repeats - 1)) 364 | actual_spacing.append(last_spacing) 365 | actual_spacing = [spacing / actual_repeats for spacing in actual_spacing] 366 | 367 | # (1) Compute DD intervals satisfying the constraint 368 | taus = _constrained_length(actual_slack * np.asarray(actual_spacing)) 369 | extra_slack = actual_slack - np.sum(taus) 370 | 371 | # (2) Distribute extra slack 372 | if self._extra_slack_distribution == "middle": 373 | mid_ind = int((len(taus) - 1) / 2) 374 | to_middle = _constrained_length(extra_slack) 375 | taus[mid_ind] += to_middle 376 | if extra_slack - to_middle: 377 | # If to_middle is not a multiple value of the pulse alignment, 378 | # it is truncated to the nearlest multiple value and 379 | # the rest of slack is added to the end. 380 | taus[-1] += extra_slack - to_middle 381 | elif self._extra_slack_distribution == "edges": 382 | to_begin_edge = _constrained_length(extra_slack / 2) 383 | taus[0] += to_begin_edge 384 | taus[-1] += extra_slack - to_begin_edge 385 | else: 386 | raise TranspilerError( 387 | f"Option extra_slack_distribution = {self._extra_slack_distribution} is invalid." 388 | ) 389 | 390 | # (3) Construct DD sequence with delays 391 | num_elements = max(len(actual_sequence), len(taus)) 392 | idle_after: float = t_start 393 | for dd_ind in range(num_elements): 394 | if dd_ind < len(taus): 395 | tau = taus[dd_ind] 396 | if tau > 0: 397 | # hallo george 398 | self._apply_scheduled_op( 399 | dag, idle_after, Delay(tau, dag.unit), qubit 400 | ) 401 | idle_after += tau 402 | if dd_ind < len(actual_sequence): 403 | gate = actual_sequence[dd_ind] 404 | gate_length = actual_dd_sequence_length[dd_ind] 405 | self._apply_scheduled_op(dag, idle_after, gate, qubit) 406 | idle_after += gate_length 407 | 408 | dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase) 409 | 410 | @staticmethod 411 | def _mod_2pi(angle: float, atol: float = 0): 412 | """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" 413 | wrapped = (angle + np.pi) % (2 * np.pi) - np.pi 414 | if abs(wrapped - np.pi) < atol: 415 | wrapped = -np.pi 416 | return wrapped 417 | -------------------------------------------------------------------------------- /qiskit_research/utils/pulse_scaling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Pulse scaling.""" 12 | 13 | from math import pi 14 | from typing import Iterable, List, Optional, Union 15 | 16 | from qiskit import pulse 17 | from qiskit.circuit import Instruction as CircuitInst 18 | from qiskit.circuit import QuantumCircuit, QuantumRegister 19 | from qiskit.circuit.library import CXGate, RZGate 20 | from qiskit.converters import circuit_to_dag, dag_to_circuit 21 | from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOpNode 22 | from qiskit.exceptions import QiskitError 23 | from qiskit.pulse import ( 24 | ControlChannel, 25 | Play, 26 | Schedule, 27 | ScheduleBlock, 28 | ) 29 | from qiskit.transpiler import Target 30 | from qiskit.transpiler.basepasses import BasePass, TransformationPass 31 | from qiskit.transpiler.passes import ( 32 | CommutativeCancellation, 33 | Optimize1qGatesDecomposition, 34 | RZXCalibrationBuilder, 35 | RZXCalibrationBuilderNoEcho, 36 | TemplateOptimization, 37 | ) 38 | from qiskit.transpiler.passes.calibration.rzx_templates import rzx_templates 39 | from qiskit_research.utils.gate_decompositions import ( 40 | RZXtoEchoedCR, 41 | ) 42 | from qiskit_research.utils.gates import SECRGate 43 | 44 | BASIS_GATES = ["sx", "rz", "rzx", "cx"] 45 | 46 | 47 | def cr_scaling_passes( 48 | target: Target, 49 | templates: List[QuantumCircuit], 50 | unroll_rzx_to_ecr: bool = True, 51 | force_zz_matches: Optional[bool] = True, 52 | param_bind: Optional[dict] = None, 53 | ) -> Iterable[BasePass]: 54 | """Yields transpilation passes for CR pulse scaling.""" 55 | 56 | yield TemplateOptimization(**templates) 57 | yield CombineRuns(["rzx"]) 58 | if force_zz_matches: 59 | yield ForceZZTemplateSubstitution() # workaround for Terra Issue 60 | if unroll_rzx_to_ecr: 61 | yield RZXtoEchoedCR(target) 62 | yield Optimize1qGatesDecomposition(BASIS_GATES) 63 | yield CommutativeCancellation() 64 | yield CombineRuns(["rz"]) 65 | if param_bind is not None: 66 | yield from pulse_attaching_passes(target, param_bind) 67 | 68 | 69 | def pulse_attaching_passes( 70 | target: Target, 71 | param_bind: dict, 72 | ) -> Iterable[BasePass]: 73 | """Yields transpilation passes for attaching pulse schedules.""" 74 | yield BindParameters(param_bind) 75 | yield Optimize1qGatesDecomposition(BASIS_GATES) 76 | yield CommutativeCancellation() 77 | yield SECRCalibrationBuilder(target=target) 78 | yield RZXCalibrationBuilder(target=target) 79 | 80 | 81 | class CombineRuns(TransformationPass): 82 | # TODO: Check to see if this can be fixed in Optimize1qGatesDecomposition 83 | """Combine consecutive gates of same type. 84 | 85 | This works with Parameters whereas other transpiling passes do not. 86 | """ 87 | 88 | def __init__(self, gate_names: List[str]): 89 | """ 90 | Args: 91 | 92 | gate_names: list of strings corresponding to the types 93 | of singe-parameter gates to combine. 94 | """ 95 | super().__init__() 96 | self._gate_names = gate_names 97 | 98 | def run(self, dag: DAGCircuit) -> DAGCircuit: 99 | for gate_name in self._gate_names: 100 | for run in dag.collect_runs([gate_name]): 101 | partition = [] 102 | chunk = [] 103 | for i in range(len(run) - 1): 104 | chunk.append(run[i]) 105 | 106 | qargs0 = run[i].qargs 107 | qargs1 = run[i + 1].qargs 108 | 109 | if qargs0 != qargs1: 110 | partition.append(chunk) 111 | chunk = [] 112 | 113 | chunk.append(run[-1]) 114 | partition.append(chunk) 115 | 116 | # simplify each chunk in the partition 117 | for chunk in partition: 118 | theta = 0 119 | for node in chunk: 120 | theta += node.op.params[0] 121 | 122 | # set the first chunk to sum of params 123 | chunk[0].op.params[0] = theta 124 | 125 | # remove remaining chunks if any 126 | if len(chunk) > 1: 127 | for node in chunk[1:]: 128 | dag.remove_op_node(node) 129 | return dag 130 | 131 | 132 | class ReduceAngles(TransformationPass): 133 | """Reduce angle of scaled pulses to between -pi and pi. 134 | 135 | This works only after Parameters are bound. Gate strings 136 | should only be single-parameter scaled pulses, i.e. 137 | 'rzx' and 'secr'. 138 | """ 139 | 140 | def __init__(self, gate_names: List[str]): 141 | """ 142 | Args: 143 | 144 | gate_names: list of strings corresponding to the types 145 | of singe-parameter gates to reduce. 146 | """ 147 | super().__init__() 148 | self._gate_names = gate_names 149 | 150 | def run(self, dag: DAGCircuit) -> DAGCircuit: 151 | for gate_name in self._gate_names: 152 | for run in dag.collect_runs([gate_name]): 153 | for node in run: 154 | theta = node.op.params[0] 155 | node.op.params[0] = (float(theta) + pi) % (2 * pi) - pi 156 | 157 | return dag 158 | 159 | 160 | class BindParameters(TransformationPass): 161 | """Bind Parameters to circuit.""" 162 | 163 | def __init__( 164 | self, 165 | param_bind: dict, 166 | ): 167 | super().__init__() 168 | self._param_bind = param_bind 169 | 170 | def run( 171 | self, 172 | dag: DAGCircuit, 173 | ) -> DAGCircuit: 174 | # TODO: Must this convert the DAG back to a QuantumCircuit? 175 | circuit = dag_to_circuit(dag) 176 | circuit.assign_parameters(self._param_bind, inplace=True) 177 | return circuit_to_dag(circuit) 178 | 179 | 180 | class ForceZZTemplateSubstitution(TransformationPass): 181 | """ 182 | Force sequences of the form CX-RZ(1)-CX to match to ZZ(theta) template. This 183 | is a workaround for known Qiskit Terra Issue TODO 184 | """ 185 | 186 | def __init__( 187 | self, 188 | template: Optional[QuantumCircuit] = None, 189 | ): 190 | super().__init__() 191 | if template is None: 192 | self._template = rzx_templates(["zz3"])["template_list"][0].copy() 193 | 194 | def get_zz_temp_sub(self) -> QuantumCircuit: 195 | """ 196 | Returns the inverse of the ZZ part of the template. 197 | """ 198 | rzx_dag = circuit_to_dag(self._template) 199 | temp_cx1_node = rzx_dag.front_layer()[0] 200 | for gp in rzx_dag.bfs_successors(temp_cx1_node): 201 | if gp[0] == temp_cx1_node: 202 | if isinstance(gp[1][0].op, CXGate) and isinstance(gp[1][1].op, RZGate): 203 | temp_rz_node = gp[1][1] 204 | temp_cx2_node = gp[1][0] 205 | 206 | rzx_dag.remove_op_node(temp_cx1_node) 207 | rzx_dag.remove_op_node(temp_rz_node) 208 | rzx_dag.remove_op_node(temp_cx2_node) 209 | 210 | return dag_to_circuit(rzx_dag).inverse() 211 | 212 | def sub_zz_in_dag( 213 | self, dag: DAGCircuit, cx1_node: DAGNode, rz_node: DAGNode, cx2_node: DAGNode 214 | ) -> DAGCircuit: 215 | """ 216 | Replaces ZZ part of the dag with it inverse from an rzx template. 217 | """ 218 | zz_temp_sub = self.get_zz_temp_sub().assign_parameters( 219 | {self.get_zz_temp_sub().parameters[0]: rz_node.op.params[0]} 220 | ) 221 | dag.remove_op_node(rz_node) 222 | dag.remove_op_node(cx2_node) 223 | 224 | qr = QuantumRegister(2, "q") 225 | mini_dag = DAGCircuit() 226 | mini_dag.add_qreg(qr) 227 | for _, (instr, qargs, _) in enumerate(zz_temp_sub.data): 228 | mini_dag.apply_operation_back(instr, qargs=qargs) 229 | 230 | dag.substitute_node_with_dag( 231 | node=cx1_node, input_dag=mini_dag, wires=[qr[0], qr[1]] 232 | ) 233 | return dag 234 | 235 | def run(self, dag: DAGCircuit) -> DAGCircuit: 236 | """ 237 | Finds patterns of CX-RZ(1)-CX and replaces them with inverse from template. 238 | """ 239 | cx_runs = dag.collect_runs("cx") 240 | for run in cx_runs: 241 | cx1_node = run[0] 242 | gp = next(dag.bfs_successors(cx1_node)) 243 | if isinstance(gp[0].op, CXGate): # dunno why this is needed 244 | if isinstance(gp[1][0], DAGOpNode) and isinstance(gp[1][1], DAGOpNode): 245 | if isinstance(gp[1][0].op, CXGate) and isinstance( 246 | gp[1][1].op, RZGate 247 | ): 248 | rz_node = gp[1][1] 249 | cx2_node = gp[1][0] 250 | gp1 = next(dag.bfs_successors(rz_node)) 251 | if cx2_node in gp1[1]: 252 | if ( 253 | dag.find_bit(cx1_node.qargs[0])[0] 254 | == dag.find_bit(cx2_node.qargs[0])[0] 255 | and dag.find_bit(cx1_node.qargs[1])[0] 256 | == dag.find_bit(cx2_node.qargs[1])[0] 257 | and dag.find_bit(cx2_node.qargs[1])[0] 258 | == dag.find_bit(rz_node.qargs[0])[0] 259 | ): 260 | dag = self.sub_zz_in_dag( 261 | dag, cx1_node, rz_node, cx2_node 262 | ) 263 | 264 | return dag 265 | 266 | 267 | # pylint: disable-next=too-many-ancestors 268 | class SECRCalibrationBuilder(RZXCalibrationBuilderNoEcho): 269 | """ 270 | Creates calibrations for SECRGate(theta) by stretching and compressing 271 | Gaussian square pulses in the CX gate. This is subclassed from RZXCalibrationBuilderNoEcho, 272 | and builds the schedule from the scaled single (non-echoed) CR pulses. 273 | """ 274 | 275 | def supported(self, node_op: CircuitInst, qubits: List) -> bool: 276 | """Determine if a given node supports the calibration. 277 | 278 | Args: 279 | node_op: Target instruction object. 280 | qubits: Integer qubit indices to check. 281 | 282 | Returns: 283 | Return ``True`` is calibration can be provided. 284 | """ 285 | return isinstance(node_op, SECRGate) and ( 286 | self._inst_map.has("cx", qubits) or self._inst_map.has("ecr", qubits) 287 | ) 288 | 289 | def get_calibration( 290 | self, node_op: CircuitInst, qubits: List 291 | ) -> Union[Schedule, ScheduleBlock]: 292 | """ 293 | Builds scaled echoed cross resonance (SECR) by doing echoing two single 294 | (unechoed) CR pulses of opposite amplitude. 295 | """ 296 | theta = node_op.params[0] 297 | try: 298 | theta = float(theta) 299 | except TypeError as ex: 300 | raise QiskitError("Target rotation angle is not assigned.") from ex 301 | 302 | op_plus = CircuitInst( 303 | name="rzx", num_qubits=2, num_clbits=0, params=[theta / 2.0] 304 | ) 305 | op_minus = CircuitInst( 306 | name="rzx", num_qubits=2, num_clbits=0, params=[-theta / 2.0] 307 | ) 308 | 309 | cr_plus = super().get_calibration(op_plus, qubits) 310 | echo_x_sched = self._inst_map.get("x", qubits=qubits[0]) 311 | cr_minus = super().get_calibration(op_minus, qubits) 312 | 313 | with pulse.build(name=f"secr{theta}") as secr_sched: 314 | with pulse.align_sequential(): 315 | pulse.call(cr_plus) 316 | pulse.call(echo_x_sched) 317 | pulse.call(cr_minus) 318 | 319 | return secr_sched 320 | 321 | 322 | def get_ecr_pairs_from_backend(target: Target) -> List[List[int]]: 323 | """ 324 | Retrieve the coupling map of only CX defined be echoed cross resonance. 325 | 326 | Args: 327 | target (Target): target one desires the ECR coupling map from 328 | 329 | Returns: 330 | List[List[int]]: coupling map consisting only of ECR pairs. 331 | """ 332 | coupling_map = target.build_coupling_map() 333 | inst_sched_map = target.instruction_schedule_map() 334 | 335 | new_coupling_map = [] 336 | for pair in coupling_map: 337 | cx = inst_sched_map.get("cx", qubits=pair) 338 | if ( 339 | len( 340 | cx.filter( 341 | channels=[ControlChannel(ii) for ii in range(len(coupling_map))], 342 | instruction_types=Play, 343 | ) 344 | ) 345 | == 2 346 | ): 347 | new_coupling_map.append(pair) 348 | 349 | return new_coupling_map 350 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | -------------------------------------------------------------------------------- /test/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | -------------------------------------------------------------------------------- /test/utils/test_cost_funcs.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test cost function.""" 12 | 13 | import unittest 14 | 15 | from qiskit.circuit import QuantumCircuit 16 | from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager 17 | from qiskit_ibm_runtime.fake_provider import FakeSherbrooke 18 | from qiskit_research.utils.cost_funcs import avg_error_score 19 | 20 | 21 | class TestScaledCostFuncs(unittest.TestCase): 22 | """Test cost function from vf2_utils""" 23 | 24 | def test_cost_func(self): 25 | """Test average error cost function""" 26 | backend = FakeSherbrooke() 27 | target = backend.target 28 | 29 | qc = QuantumCircuit(5) 30 | qc.h(0) 31 | qc.cx(0, 1) 32 | qc.cx(1, 2) 33 | 34 | qc2 = qc.copy() 35 | qc2.cx(2, 3) 36 | qc2.cx(3, 4) 37 | 38 | pm = generate_preset_pass_manager( 39 | target=target, optimization_level=2, seed_transpiler=12345 40 | ) 41 | qc_t = pm.run(qc) 42 | best_layout_score = avg_error_score(qc_t, target) 43 | 44 | self.assertGreater(best_layout_score, 0) 45 | self.assertLess(best_layout_score, 1) 46 | 47 | qc2_t = pm.run(qc2) 48 | best_layout_score2 = avg_error_score(qc2_t, target) 49 | 50 | self.assertLess(best_layout_score, best_layout_score2) 51 | -------------------------------------------------------------------------------- /test/utils/test_dynamical_decoupling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test dynamical decoupling.""" 12 | 13 | import unittest 14 | 15 | from qiskit.circuit import QuantumCircuit 16 | from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager 17 | from qiskit_ibm_runtime.fake_provider import FakeSherbrooke 18 | 19 | from qiskit_research.utils.convenience import ( 20 | add_dynamical_decoupling, 21 | add_pulse_calibrations, 22 | ) 23 | from qiskit_research.utils.dynamical_decoupling import PulseMethod 24 | 25 | 26 | class TestDynamicalDecoupling(unittest.TestCase): 27 | """Test PeriodicDynamicalDecoupling pass.""" 28 | 29 | def test_add_dynamical_decoupling(self): 30 | """Test adding dynamical decoupling.""" 31 | # TODO make this an actual test 32 | circuit = QuantumCircuit(3) 33 | circuit.h(1) 34 | circuit.cx(1, 2) 35 | circuit.cx(0, 1) 36 | circuit.rz(1.0, 1) 37 | circuit.cx(0, 1) 38 | circuit.rx(1.0, [0, 1, 2]) 39 | 40 | backend = FakeSherbrooke() 41 | target = backend.target 42 | pm = generate_preset_pass_manager(target=target, optimization_level=2) 43 | transpiled = pm.run(circuit) 44 | transpiled_dd = add_dynamical_decoupling( 45 | transpiled, target, "XY4pm", add_pulse_cals=True 46 | ) 47 | self.assertIsInstance(transpiled_dd, QuantumCircuit) 48 | self.assertIn("xp", transpiled_dd.count_ops()) 49 | self.assertIn("yp", transpiled_dd.count_ops()) 50 | self.assertIn("xm", transpiled_dd.count_ops()) 51 | self.assertIn("ym", transpiled_dd.count_ops()) 52 | 53 | def test_add_urdd_dynamical_decoupling(self): 54 | """Test adding dynamical decoupling.""" 55 | # TODO make this an actual test 56 | circuit = QuantumCircuit(3) 57 | circuit.h(1) 58 | circuit.cx(1, 2) 59 | circuit.cx(0, 1) 60 | circuit.rz(1.0, 1) 61 | circuit.cx(0, 1) 62 | circuit.rx(1.0, [0, 1, 2]) 63 | 64 | backend = FakeSherbrooke() 65 | target = backend.target 66 | pm = generate_preset_pass_manager(2, backend) 67 | transpiled = pm.run(circuit) 68 | transpiled_urdd4 = add_dynamical_decoupling( 69 | transpiled, 70 | target, 71 | "URDD", 72 | add_pulse_cals=True, 73 | urdd_pulse_num=4, 74 | pulse_method=PulseMethod.AMPLITUDEINVERT, 75 | ) 76 | self.assertIsInstance(transpiled_urdd4, QuantumCircuit) 77 | self.assertIn("pi_phi", transpiled_urdd4.count_ops()) 78 | self.assertEqual(transpiled_urdd4.count_ops()["pi_phi"] % 4, 0) 79 | 80 | transpiled_urdd8 = add_dynamical_decoupling( 81 | transpiled, 82 | target, 83 | "URDD", 84 | add_pulse_cals=True, 85 | urdd_pulse_num=8, 86 | pulse_method=PulseMethod.IQSUM, 87 | ) 88 | self.assertIsInstance(transpiled_urdd8, QuantumCircuit) 89 | self.assertIn("pi_phi", transpiled_urdd8.count_ops()) 90 | self.assertEqual(transpiled_urdd8.count_ops()["pi_phi"] % 8, 0) 91 | 92 | def test_add_pulse_calibrations(self): 93 | """Test adding dynamical decoupling.""" 94 | circuit = QuantumCircuit(2) 95 | backend = FakeSherbrooke() 96 | target = backend.target 97 | add_pulse_calibrations(circuit, target) 98 | for key in circuit.calibrations["xp"]: 99 | drag_xp = circuit.calibrations["xp"][key].instructions[0][1].operands[0] 100 | drag_xm = circuit.calibrations["xm"][key].instructions[0][1].operands[0] 101 | drag_yp = circuit.calibrations["yp"][key].instructions[1][1].operands[0] 102 | drag_ym = circuit.calibrations["ym"][key].instructions[1][1].operands[0] 103 | self.assertEqual(drag_xm.amp, -drag_xp.amp) 104 | self.assertEqual(drag_yp.amp, drag_xp.amp) 105 | self.assertEqual(drag_ym.amp, -drag_yp.amp) 106 | -------------------------------------------------------------------------------- /test/utils/test_gate_decompositions.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test Pauli twirling.""" 12 | 13 | import unittest 14 | 15 | import numpy as np 16 | from qiskit.circuit import QuantumCircuit, QuantumRegister 17 | from qiskit.circuit.library import RZZGate, XXMinusYYGate, XXPlusYYGate 18 | from qiskit.quantum_info import Operator 19 | from qiskit.transpiler import PassManager 20 | 21 | from qiskit_research.utils.gates import PiPhiGate 22 | from qiskit_research.utils.gate_decompositions import ( 23 | ControlledRZZToCX, 24 | RZXWeylDecomposition, 25 | XXMinusYYtoRZX, 26 | XXPlusYYtoRZX, 27 | ) 28 | 29 | 30 | class TestPasses(unittest.TestCase): 31 | """Test passes.""" 32 | 33 | def test_controlled_rzz_to_cx(self): 34 | """Test controlled RZZGate to CXGate decomposition.""" 35 | rng = np.random.default_rng() 36 | theta = rng.uniform(-10, 10) 37 | gate = RZZGate(theta).control(1) 38 | register = QuantumRegister(3) 39 | circuit = QuantumCircuit(register) 40 | circuit.append(gate, register) 41 | pass_ = ControlledRZZToCX() 42 | pass_manager = PassManager([pass_]) 43 | decomposed = pass_manager.run(circuit) 44 | self.assertTrue(Operator(circuit).equiv(Operator(decomposed))) 45 | 46 | def test_xxplusyy_to_rzx(self): 47 | """Test XXPlusYYGate to RZXGate decomposition.""" 48 | rng = np.random.default_rng() 49 | theta = rng.uniform(-10, 10) 50 | beta = rng.uniform(-10, 10) 51 | gate = XXPlusYYGate(theta, beta) 52 | register = QuantumRegister(2) 53 | circuit = QuantumCircuit(register) 54 | circuit.append(gate, register) 55 | pass_ = XXPlusYYtoRZX() 56 | pass_manager = PassManager([pass_]) 57 | decomposed = pass_manager.run(circuit) 58 | self.assertTrue(Operator(circuit).equiv(Operator(decomposed))) 59 | 60 | def test_xxminusyy_to_rzx(self): 61 | """Test XXMinusYYGate to RZXGate decomposition.""" 62 | rng = np.random.default_rng() 63 | theta = rng.uniform(-10, 10) 64 | beta = rng.uniform(-10, 10) 65 | gate = XXMinusYYGate(theta, beta) 66 | register = QuantumRegister(2) 67 | circuit = QuantumCircuit(register) 68 | circuit.append(gate, register) 69 | pass_ = XXMinusYYtoRZX() 70 | pass_manager = PassManager([pass_]) 71 | decomposed = pass_manager.run(circuit) 72 | self.assertTrue(Operator(circuit).equiv(Operator(decomposed))) 73 | 74 | def test_rzx_weyl_decomposition(self): 75 | """Test RZXWeylDecomposition.""" 76 | 77 | qc = QuantumCircuit(3) 78 | qc.rxx(np.pi / 3, 0, 1) 79 | qc.ryy(np.pi / 5, 1, 2) 80 | qc.rzz(np.pi / 7, 2, 0) 81 | pm = PassManager(RZXWeylDecomposition()) 82 | qc_w = pm.run(qc) 83 | 84 | self.assertNotIn("rxx", qc_w.count_ops()) 85 | self.assertNotIn("ryy", qc_w.count_ops()) 86 | self.assertNotIn("rzz", qc_w.count_ops()) 87 | self.assertIn("rzx", qc_w.count_ops()) 88 | self.assertTrue(np.allclose(Operator(qc), Operator(qc_w), atol=1e-8)) 89 | 90 | def test_piphi_definition(self): 91 | """Test PiPhi Definition and Decomposition""" 92 | 93 | rng = np.random.default_rng() 94 | phi = rng.uniform(-np.pi, np.pi) 95 | qc = QuantumCircuit(1) 96 | qc.append(PiPhiGate(phi), [0]) 97 | 98 | self.assertTrue(Operator(qc).equiv(PiPhiGate(phi).to_matrix())) 99 | -------------------------------------------------------------------------------- /test/utils/test_pauli_twirling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test Pauli twirling.""" 12 | 13 | import unittest 14 | 15 | import numpy as np 16 | from qiskit.circuit import Parameter, QuantumCircuit 17 | from qiskit.circuit.library import ( 18 | CXGate, 19 | CYGate, 20 | CZGate, 21 | CHGate, 22 | CSGate, 23 | DCXGate, 24 | CSXGate, 25 | CSdgGate, 26 | ECRGate, 27 | iSwapGate, 28 | SwapGate, 29 | ) 30 | from qiskit.quantum_info import Operator 31 | from qiskit_research.utils.convenience import add_pauli_twirls 32 | from qiskit_research.utils.gates import SECRGate 33 | from qiskit_research.utils.pauli_twirling import TWIRL_GATES 34 | 35 | 36 | class TestPauliTwirling(unittest.TestCase): 37 | """Test Pauli twirling.""" 38 | 39 | def test_twirl_gates_cnot(self): 40 | """Test twirling CNOT.""" 41 | twirl_gates = TWIRL_GATES["cx"] 42 | self.assertEqual(len(twirl_gates), 16) 43 | operator = Operator(CXGate()) 44 | for (a, b), (c, d) in twirl_gates: 45 | circuit = QuantumCircuit(2) 46 | circuit.append(a, [0]) 47 | circuit.append(b, [1]) 48 | circuit.append(CXGate(), [0, 1]) 49 | circuit.append(c, [0]) 50 | circuit.append(d, [1]) 51 | self.assertTrue(Operator(circuit).equiv(operator)) 52 | 53 | def test_twirl_gates_cy(self): 54 | """Test twirling CY""" 55 | twirl_gates = TWIRL_GATES["cy"] 56 | self.assertEqual(len(twirl_gates), 16) 57 | operator = Operator(CYGate()) 58 | for (a, b), (c, d) in twirl_gates: 59 | circuit = QuantumCircuit(2) 60 | circuit.append(a, [0]) 61 | circuit.append(b, [1]) 62 | circuit.append(CYGate(), [0, 1]) 63 | circuit.append(c, [0]) 64 | circuit.append(d, [1]) 65 | self.assertTrue(Operator(circuit).equiv(operator)) 66 | 67 | def test_twirl_gates_cz(self): 68 | """Test twirling CZ.""" 69 | twirl_gates = TWIRL_GATES["cz"] 70 | self.assertEqual(len(twirl_gates), 16) 71 | operator = Operator(CZGate()) 72 | for (a, b), (c, d) in twirl_gates: 73 | circuit = QuantumCircuit(2) 74 | circuit.append(a, [0]) 75 | circuit.append(b, [1]) 76 | circuit.append(CZGate(), [0, 1]) 77 | circuit.append(c, [0]) 78 | circuit.append(d, [1]) 79 | self.assertTrue(Operator(circuit).equiv(operator)) 80 | 81 | def test_twirl_gates_ch(self): 82 | """Test twirling CH.""" 83 | twirl_gates = TWIRL_GATES["ch"] 84 | self.assertEqual(len(twirl_gates), 4) 85 | operator = Operator(CHGate()) 86 | for (a, b), (c, d) in twirl_gates: 87 | circuit = QuantumCircuit(2) 88 | circuit.append(a, [0]) 89 | circuit.append(b, [1]) 90 | circuit.append(CHGate(), [0, 1]) 91 | circuit.append(c, [0]) 92 | circuit.append(d, [1]) 93 | self.assertTrue(Operator(circuit).equiv(operator)) 94 | 95 | def test_twirl_gates_cs(self): 96 | """Test twirling CS.""" 97 | twirl_gates = TWIRL_GATES["cs"] 98 | self.assertEqual(len(twirl_gates), 4) 99 | operator = Operator(CSGate()) 100 | for (a, b), (c, d) in twirl_gates: 101 | circuit = QuantumCircuit(2) 102 | circuit.append(a, [0]) 103 | circuit.append(b, [1]) 104 | circuit.append(CSGate(), [0, 1]) 105 | circuit.append(c, [0]) 106 | circuit.append(d, [1]) 107 | self.assertTrue(Operator(circuit).equiv(operator)) 108 | 109 | def test_twirl_gates_dcx(self): 110 | """Test twirling DCX.""" 111 | twirl_gates = TWIRL_GATES["dcx"] 112 | self.assertEqual(len(twirl_gates), 16) 113 | operator = Operator(DCXGate()) 114 | for (a, b), (c, d) in twirl_gates: 115 | circuit = QuantumCircuit(2) 116 | circuit.append(a, [0]) 117 | circuit.append(b, [1]) 118 | circuit.append(DCXGate(), [0, 1]) 119 | circuit.append(c, [0]) 120 | circuit.append(d, [1]) 121 | self.assertTrue(Operator(circuit).equiv(operator)) 122 | 123 | def test_twirl_gates_csx(self): 124 | """Test twirling CSX.""" 125 | twirl_gates = TWIRL_GATES["csx"] 126 | self.assertEqual(len(twirl_gates), 4) 127 | operator = Operator(CSXGate()) 128 | for (a, b), (c, d) in twirl_gates: 129 | circuit = QuantumCircuit(2) 130 | circuit.append(a, [0]) 131 | circuit.append(b, [1]) 132 | circuit.append(CSXGate(), [0, 1]) 133 | circuit.append(c, [0]) 134 | circuit.append(d, [1]) 135 | self.assertTrue(Operator(circuit).equiv(operator)) 136 | 137 | def test_twirl_gates_csdg(self): 138 | """Test twirling CSdg.""" 139 | twirl_gates = TWIRL_GATES["csdg"] 140 | self.assertEqual(len(twirl_gates), 4) 141 | operator = Operator(CSdgGate()) 142 | for (a, b), (c, d) in twirl_gates: 143 | circuit = QuantumCircuit(2) 144 | circuit.append(a, [0]) 145 | circuit.append(b, [1]) 146 | circuit.append(CSdgGate(), [0, 1]) 147 | circuit.append(c, [0]) 148 | circuit.append(d, [1]) 149 | self.assertTrue(Operator(circuit).equiv(operator)) 150 | 151 | def test_twirl_gates_ecr(self): 152 | """Test twirling ECR.""" 153 | twirl_gates = TWIRL_GATES["ecr"] 154 | self.assertEqual(len(twirl_gates), 16) 155 | operator = Operator(ECRGate()) 156 | for (a, b), (c, d) in twirl_gates: 157 | circuit = QuantumCircuit(2) 158 | circuit.append(a, [0]) 159 | circuit.append(b, [1]) 160 | circuit.append(ECRGate(), [0, 1]) 161 | circuit.append(c, [0]) 162 | circuit.append(d, [1]) 163 | self.assertTrue(Operator(circuit).equiv(operator)) 164 | 165 | def test_twirl_gates_swap(self): 166 | """Test twirling Swap.""" 167 | twirl_gates = TWIRL_GATES["swap"] 168 | self.assertEqual(len(twirl_gates), 16) 169 | operator = Operator(SwapGate()) 170 | for (a, b), (c, d) in twirl_gates: 171 | circuit = QuantumCircuit(2) 172 | circuit.append(a, [0]) 173 | circuit.append(b, [1]) 174 | circuit.append(SwapGate(), [0, 1]) 175 | circuit.append(c, [0]) 176 | circuit.append(d, [1]) 177 | self.assertTrue(Operator(circuit).equiv(operator)) 178 | 179 | def test_twirl_gates_iswap(self): 180 | """Test twirling iSwap.""" 181 | twirl_gates = TWIRL_GATES["iswap"] 182 | self.assertEqual(len(twirl_gates), 16) 183 | operator = Operator(iSwapGate()) 184 | for (a, b), (c, d) in twirl_gates: 185 | circuit = QuantumCircuit(2) 186 | circuit.append(a, [0]) 187 | circuit.append(b, [1]) 188 | circuit.append(iSwapGate(), [0, 1]) 189 | circuit.append(c, [0]) 190 | circuit.append(d, [1]) 191 | self.assertTrue(Operator(circuit).equiv(operator)) 192 | 193 | def test_add_pauli_twirls(self): 194 | """Test adding Pauli twirls.""" 195 | rng = np.random.default_rng() 196 | 197 | theta = Parameter("$\\theta$") 198 | phi = Parameter("$\\phi") 199 | 200 | circuit = QuantumCircuit(3) 201 | circuit.h([1, 2]) 202 | circuit.rzx(theta, 0, 1) 203 | circuit.h(1) 204 | circuit.rzx(phi, 1, 2) 205 | circuit.rzz(theta, 0, 1) 206 | circuit.h(1) 207 | circuit.rzz(phi, 1, 2) 208 | circuit.h(2) 209 | circuit.cx(0, 1) 210 | 211 | circuit.h(1) 212 | circuit.append(SECRGate(theta), [0, 1]) 213 | circuit.h(0) 214 | circuit.append(SECRGate(phi), [2, 0]) 215 | circuit.h(2) 216 | 217 | twirled_circs = add_pauli_twirls(circuit, num_twirled_circuits=5) 218 | more_twirled_circs = add_pauli_twirls( 219 | [circuit], num_twirled_circuits=5, seed=1234 220 | ) 221 | 222 | param_bind = {param: rng.uniform(-10, 10) for param in circuit.parameters} 223 | circuit.assign_parameters(param_bind, inplace=True) 224 | 225 | for t_circ in twirled_circs: 226 | t_circ.assign_parameters(param_bind, inplace=True) 227 | self.assertNotEqual(t_circ, circuit) 228 | self.assertTrue(Operator(t_circ).equiv(Operator(circuit))) 229 | 230 | for t_circ in more_twirled_circs[0]: 231 | t_circ.assign_parameters(param_bind, inplace=True) 232 | self.assertNotEqual(t_circ, circuit) 233 | self.assertTrue(Operator(t_circ).equiv(Operator(circuit))) 234 | -------------------------------------------------------------------------------- /test/utils/test_periodic_dynamical_decoupling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test dynamical decoupling.""" 12 | 13 | import unittest 14 | 15 | from qiskit.circuit import QuantumCircuit 16 | from qiskit.circuit.library import XGate 17 | 18 | from qiskit.transpiler.instruction_durations import InstructionDurations 19 | from qiskit.transpiler.passes.scheduling import ALAPScheduleAnalysis 20 | from qiskit.transpiler import PassManager 21 | 22 | from qiskit_research.utils.periodic_dynamical_decoupling import ( 23 | PeriodicDynamicalDecoupling, 24 | ) 25 | 26 | 27 | class TestPeriodicDynamicalDecoupling(unittest.TestCase): 28 | """Test PeriodicDynamicalDecoupling pass.""" 29 | 30 | @unittest.skip("string comparison is flaky for some reason") 31 | def test_add_periodic_dynamical_decoupling(self): 32 | """Test adding XX sequence with max 3 repeats and min_avg_delay""" 33 | circuit = QuantumCircuit(4) 34 | circuit.h(0) 35 | for i in range(3): 36 | circuit.cx(i, i + 1) 37 | circuit.measure_all() 38 | 39 | durations = InstructionDurations( 40 | [ 41 | ("h", 0, 50), 42 | ("cx", [0, 1], 700), 43 | ("cx", [1, 2], 200), 44 | ("cx", [2, 3], 300), 45 | ("x", None, 50), 46 | ("measure", None, 1000), 47 | ("reset", None, 1500), 48 | ] 49 | ) 50 | pulse_alignment = 25 51 | 52 | pm = PassManager( 53 | [ 54 | ALAPScheduleAnalysis(durations=durations), 55 | PeriodicDynamicalDecoupling( 56 | durations=durations, 57 | base_dd_sequence=[XGate(), XGate()], 58 | max_repeats=3, 59 | avg_min_delay=50, 60 | pulse_alignment=pulse_alignment, 61 | skip_reset_qubits=False, 62 | ), 63 | ] 64 | ) 65 | circ_dd = pm.run(circuit) 66 | 67 | # pylint: disable=trailing-whitespace 68 | self.assertEqual( 69 | str(circ_dd.draw()).strip(), 70 | """ 71 | ┌───┐ ┌───────────────┐ ┌───┐┌───────────────┐ ┌───┐» 72 | q_0: ──────┤ H ├────────■──┤ Delay(25[dt]) ├─┤ X ├┤ Delay(75[dt]) ├─┤ X ├» 73 | ┌─────┴───┴─────┐┌─┴─┐└───────────────┘ └───┘└───────────────┘ └───┘» 74 | q_1: ┤ Delay(50[dt]) ├┤ X ├──────────────────────────────────────────────» 75 | ├───────────────┤├───┤┌───────────────┐ ┌───┐┌───────────────┐ ┌───┐» 76 | q_2: ┤ Delay(25[dt]) ├┤ X ├┤ Delay(75[dt]) ├─┤ X ├┤ Delay(75[dt]) ├─┤ X ├» 77 | ├───────────────┤├───┤├───────────────┴┐├───┤├───────────────┴┐├───┤» 78 | q_3: ┤ Delay(50[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ X ├» 79 | └───────────────┘└───┘└────────────────┘└───┘└────────────────┘└───┘» 80 | meas: 4/════════════════════════════════════════════════════════════════════» 81 | » 82 | « ┌────────────────┐┌───┐┌───────────────┐ ┌───┐┌───────────────┐ » 83 | « q_0: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(75[dt]) ├─┤ X ├┤ Delay(25[dt]) ├──────» 84 | « └────────────────┘└───┘└───────────────┘ └───┘└───────────────┘ » 85 | « q_1: ─────────────────────────────────────────────────────────────────────» 86 | « ┌────────────────┐┌───┐┌───────────────┐ ┌───┐┌───────────────┐ ┌───┐» 87 | « q_2: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(75[dt]) ├─┤ X ├┤ Delay(75[dt]) ├─┤ X ├» 88 | « ├────────────────┤├───┤├───────────────┴┐├───┤├───────────────┴┐├───┤» 89 | « q_3: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ X ├» 90 | « └────────────────┘└───┘└────────────────┘└───┘└────────────────┘└───┘» 91 | «meas: 4/═════════════════════════════════════════════════════════════════════» 92 | « » 93 | « » 94 | « q_0: ───────────────────────────────────────────────────────────────────» 95 | « ┌───────────────┐┌───┐┌────────────────┐┌───┐» 96 | « q_1: ───────────────────■──┤ Delay(50[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ X ├» 97 | « ┌───────────────┐┌─┴─┐└───────────────┘└───┘└────────────────┘└───┘» 98 | « q_2: ┤ Delay(25[dt]) ├┤ X ├────────■────────────────────────────────────» 99 | « ├───────────────┤└───┘ ┌─┴─┐ » 100 | « q_3: ┤ Delay(50[dt]) ├───────────┤ X ├──────────────────────────────────» 101 | « └───────────────┘ └───┘ » 102 | «meas: 4/═══════════════════════════════════════════════════════════════════» 103 | « » 104 | « ░ ┌─┐ 105 | « q_0: ──────────────────░─┤M├───────── 106 | « ┌───────────────┐ ░ └╥┘┌─┐ 107 | « q_1: ┤ Delay(50[dt]) ├─░──╫─┤M├────── 108 | « └───────────────┘ ░ ║ └╥┘┌─┐ 109 | « q_2: ──────────────────░──╫──╫─┤M├─── 110 | « ░ ║ ║ └╥┘┌─┐ 111 | « q_3: ──────────────────░──╫──╫──╫─┤M├ 112 | « ░ ║ ║ ║ └╥┘ 113 | «meas: 4/═════════════════════╩══╩══╩══╩═ 114 | « 0 1 2 3 115 | """.strip(), 116 | ) 117 | -------------------------------------------------------------------------------- /test/utils/test_pulse_scaling.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright IBM 2022. 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | """Test pulse scaling.""" 12 | 13 | import unittest 14 | 15 | import numpy as np 16 | from ddt import data, ddt 17 | from qiskit import schedule 18 | from qiskit.circuit import Parameter, QuantumCircuit 19 | from qiskit.converters import circuit_to_dag 20 | from qiskit.pulse import ControlChannel, Play 21 | from qiskit.quantum_info import Operator 22 | from qiskit.transpiler import PassManager, Target 23 | from qiskit.transpiler.passes import Optimize1qGatesDecomposition 24 | from qiskit_ibm_runtime.fake_provider import FakeSherbrooke 25 | 26 | from qiskit_research.utils.convenience import scale_cr_pulses 27 | from qiskit_research.utils.gate_decompositions import RZXtoEchoedCR 28 | from qiskit_research.utils.pulse_scaling import ( 29 | BASIS_GATES, 30 | ReduceAngles, 31 | SECRCalibrationBuilder, 32 | ) 33 | 34 | 35 | @ddt 36 | class TestPulseScaling(unittest.TestCase): 37 | """Test pulse scaling.""" 38 | 39 | @data(FakeSherbrooke().target) 40 | def test_rzx_to_secr_forward(self, target: Target): 41 | """Test pulse scaling RZX with forward SECR.""" 42 | rng = np.random.default_rng() 43 | 44 | JJ = Parameter("$J$") 45 | hh = Parameter("$h$") 46 | dt = Parameter("$dt$") 47 | 48 | param_bind = { 49 | JJ: rng.uniform(0, 1), 50 | hh: rng.uniform(0, 2), 51 | dt: rng.uniform(0, 0.5), 52 | } 53 | 54 | qc = QuantumCircuit(3) 55 | qc.cx(0, 1) 56 | qc.rz(-2 * JJ * dt, 1) 57 | qc.cx(0, 1) 58 | qc.rx(2 * hh * dt, [0, 1, 2]) 59 | qc.cx(1, 2) 60 | qc.rz(-2 * JJ * dt, 2) 61 | qc.cx(1, 2) 62 | 63 | scaled_qc = scale_cr_pulses(qc, target, unroll_rzx_to_ecr=True) 64 | scaled_qc.assign_parameters(param_bind, inplace=True) 65 | qc.assign_parameters(param_bind, inplace=True) 66 | 67 | self.assertTrue(Operator(qc).equiv(Operator(scaled_qc))) 68 | 69 | @data(FakeSherbrooke().target) 70 | def test_rzx_to_secr_reverse(self, target: Target): 71 | """Test pulse scaling RZX with reverse SECR.""" 72 | rng = np.random.default_rng() 73 | 74 | JJ = Parameter("$J$") 75 | hh = Parameter("$h$") 76 | dt = Parameter("$dt$") 77 | 78 | param_bind = { 79 | JJ: rng.uniform(0, 1), 80 | hh: rng.uniform(0, 2), 81 | dt: rng.uniform(0, 0.5), 82 | } 83 | 84 | qc = QuantumCircuit(3) 85 | qc.cx(2, 1) 86 | qc.rz(-2 * JJ * dt, 1) 87 | qc.cx(2, 1) 88 | qc.rx(2 * hh * dt, [0, 1, 2]) 89 | qc.cx(1, 0) 90 | qc.rz(-2 * JJ * dt, 0) 91 | qc.cx(1, 0) 92 | 93 | scaled_qc = scale_cr_pulses( 94 | qc, target, unroll_rzx_to_ecr=True, param_bind=param_bind 95 | ) 96 | qc.assign_parameters(param_bind, inplace=True) 97 | 98 | self.assertTrue(Operator(qc).equiv(Operator(scaled_qc))) 99 | 100 | @data(FakeSherbrooke().target) 101 | def test_rzx_to_secr(self, target: Target): 102 | """Test pulse scaling with RZX gates.""" 103 | rng = np.random.default_rng() 104 | theta = rng.uniform(-np.pi, np.pi) 105 | 106 | qc = QuantumCircuit(2) 107 | qc.rzx(theta, 0, 1) 108 | 109 | scaled_qc = scale_cr_pulses(qc, target, unroll_rzx_to_ecr=True, param_bind={}) 110 | self.assertTrue(Operator(qc).equiv(Operator(scaled_qc))) 111 | 112 | qc = QuantumCircuit(2) 113 | qc.rzx(theta, 1, 0) 114 | 115 | scaled_qc = scale_cr_pulses(qc, target, unroll_rzx_to_ecr=True, param_bind={}) 116 | self.assertTrue(Operator(qc).equiv(Operator(scaled_qc))) 117 | 118 | @data(FakeSherbrooke().target) 119 | def test_forced_rzz_template_match(self, target: Target): 120 | """Test forced template optimization for CX-RZ(1)-CX matches""" 121 | theta = Parameter("$\\theta$") 122 | rng = np.random.default_rng(12345) 123 | 124 | qc = QuantumCircuit(2) 125 | qc.cx(0, 1) 126 | qc.rz(theta, 1) 127 | qc.cx(0, 1) 128 | qc.rz(np.pi / 4, 1) 129 | 130 | scale_qc_no_match = scale_cr_pulses( 131 | qc, 132 | target, 133 | unroll_rzx_to_ecr=False, 134 | force_zz_matches=False, 135 | param_bind=None, 136 | ) 137 | dag_no_match = circuit_to_dag(scale_qc_no_match) 138 | self.assertFalse(dag_no_match.collect_runs(["rzx"])) 139 | 140 | scale_qc_match = scale_cr_pulses( 141 | qc, target, unroll_rzx_to_ecr=False, force_zz_matches=True, param_bind=None 142 | ) 143 | dag_match = circuit_to_dag(scale_qc_match) 144 | self.assertTrue(dag_match.collect_runs(["rzx"])) 145 | 146 | theta_set = rng.uniform(-np.pi, np.pi) 147 | self.assertTrue( 148 | Operator(qc.assign_parameters({theta: theta_set})).equiv( 149 | Operator(scale_qc_match.assign_parameters({theta: theta_set})) 150 | ) 151 | ) 152 | 153 | def test_angle_reduction(self): 154 | """Test Angle Reduction""" 155 | pm = PassManager(ReduceAngles(["rzx"])) 156 | 157 | qc1 = QuantumCircuit(2) 158 | qc1.rzx(9 * np.pi / 2, 0, 1) 159 | qc1_s = pm.run(qc1) 160 | 161 | qc2 = QuantumCircuit(2) 162 | qc2.rzx(42, 0, 1) 163 | qc2_s = pm.run(qc2) 164 | 165 | qc3 = QuantumCircuit(2) 166 | qc3.rzx(-np.pi, 0, 1) 167 | qc3_s = pm.run(qc3) 168 | 169 | self.assertAlmostEqual(qc1_s.data[0].operation.params[0], np.pi / 2) 170 | self.assertAlmostEqual(qc2_s.data[0].operation.params[0], -1.9822971502571) 171 | self.assertAlmostEqual(qc3_s.data[0].operation.params[0], -np.pi) 172 | 173 | @data(FakeSherbrooke().target) 174 | def test_secr_calibration_builder(self, target: Target): 175 | """ 176 | Test SECR Calibration Builder 177 | 178 | Note the circuit must first pass through the RZXtoEchoedCR pass to correct 179 | for the direction of the native CR operation. 180 | """ 181 | inst_sched_map = target.instruction_schedule_map() 182 | coupling_map = target.build_coupling_map() 183 | ctrl_chans = [ControlChannel(idx) for idx in range(len(list(coupling_map)))] 184 | 185 | theta = -np.pi / 7 186 | qc = QuantumCircuit(2) 187 | qc.rzx(2 * theta, 1, 0) 188 | 189 | # Verify that there are no calibrations for this circuit yet. 190 | self.assertEqual(qc.calibrations, {}) 191 | 192 | pm = PassManager( 193 | [ 194 | RZXtoEchoedCR(target), 195 | SECRCalibrationBuilder(inst_sched_map), 196 | Optimize1qGatesDecomposition(BASIS_GATES), 197 | ] 198 | ) 199 | qc_cal = pm.run(qc) 200 | sched = schedule( 201 | qc_cal, inst_map=inst_sched_map, meas_map=[list(range(target.num_qubits))] 202 | ) 203 | 204 | crp_start_time = sched.filter( 205 | channels=ctrl_chans, instruction_types=[Play] 206 | ).instructions[0][0] 207 | crm_start_time = sched.filter( 208 | channels=ctrl_chans, instruction_types=[Play] 209 | ).instructions[1][0] 210 | 211 | crp_duration = ( 212 | sched.filter( 213 | channels=ctrl_chans, 214 | instruction_types=[Play], 215 | ) 216 | .instructions[0][1] 217 | .duration 218 | ) 219 | crm_duration = ( 220 | sched.filter( 221 | channels=ctrl_chans, 222 | instruction_types=[Play], 223 | ) 224 | .instructions[1][1] 225 | .duration 226 | ) 227 | 228 | # same duration for all 1Q native gates 229 | echo_duration = inst_sched_map.get("x", qubits=[0]).duration 230 | 231 | self.assertEqual(crp_start_time + crp_duration + echo_duration, crm_start_time) 232 | self.assertEqual(crp_duration, crm_duration) 233 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.25 3 | envlist = py{38,39,310,311}, lint, mypy, format, coverage, docs 4 | isolated_build = True 5 | 6 | [testenv] 7 | extras = 8 | dev 9 | commands = 10 | pytest test/ {posargs} 11 | 12 | [testenv:lint] 13 | base_python = 3.10 14 | extras = 15 | dev 16 | commands = 17 | pylint qiskit_research test 18 | 19 | [testenv:mypy] 20 | base_python = 3.10 21 | extras = 22 | dev 23 | commands = 24 | mypy --exclude=docs/_build . 25 | 26 | [testenv:format] 27 | base_python = 3.10 28 | extras = 29 | dev 30 | commands = 31 | black --check . 32 | 33 | [testenv:coverage] 34 | base_python = 3.10 35 | extras = 36 | dev 37 | commands = 38 | coverage3 run --source qiskit_research --parallel-mode -m pytest test/ {posargs} 39 | coverage3 combine 40 | coverage3 html 41 | coverage3 report --fail-under=50 42 | 43 | [testenv:docs] 44 | base_python = 3.10 45 | extras = 46 | dev 47 | commands = 48 | python -c 'import shutil, pathlib; shutil.rmtree(pathlib.Path("docs") / "stubs", ignore_errors=True)' 49 | python -c 'import shutil, pathlib; shutil.rmtree(pathlib.Path("docs") / "_build" / "html" / ".doctrees", ignore_errors=True)' 50 | sphinx-build -b html -W {posargs} docs/ docs/_build/html 51 | 52 | [pytest] 53 | addopts = --doctest-modules 54 | --------------------------------------------------------------------------------