├── .github └── workflows │ ├── docs.yml │ ├── publish.yml │ └── python-package.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── index.html ├── qiskit_trebugger.html ├── qiskit_trebugger │ ├── debugger.html │ ├── debugger_error.html │ ├── model.html │ ├── model │ │ ├── circuit_comparator.html │ │ ├── circuit_stats.html │ │ ├── data_collector.html │ │ ├── log_entry.html │ │ ├── logging_handler.html │ │ ├── pass_type.html │ │ ├── property.html │ │ ├── transpilation_sequence.html │ │ └── transpilation_step.html │ ├── views.html │ └── views │ │ ├── cli.html │ │ ├── cli │ │ ├── cli_pass_pad.html │ │ ├── cli_utils.html │ │ ├── cli_view.html │ │ ├── config.html │ │ └── prototype.html │ │ ├── widget.html │ │ └── widget │ │ ├── button_with_value.html │ │ ├── timeline_utils.html │ │ └── timeline_view.html └── search.js ├── ecosystem.json ├── imgs ├── cli │ ├── full-view.png │ ├── global-transpiler-panel.png │ ├── indexed-1.png │ ├── indexed-2.png │ ├── overview.png │ ├── status-idx.png │ ├── status-input.png │ ├── status-main.png │ ├── title.png │ └── working.gif ├── jupyter │ ├── diff-1.png │ ├── diff-2.png │ ├── logs.png │ ├── stats.png │ └── working.gif └── logo.png ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── src └── qiskit_trebugger │ ├── __init__.py │ ├── debugger.py │ ├── debugger_error.py │ ├── model │ ├── __init__.py │ ├── circuit_comparator.py │ ├── circuit_stats.py │ ├── data_collector.py │ ├── log_entry.py │ ├── logging_handler.py │ ├── pass_type.py │ ├── property.py │ ├── transpilation_sequence.py │ └── transpilation_step.py │ └── views │ ├── __init__.py │ ├── cli │ ├── __init__.py │ ├── cli_pass_pad.py │ ├── cli_view.py │ └── config.py │ └── widget │ ├── __init__.py │ ├── button_with_value.py │ ├── resources │ └── qiskit-logo.png │ ├── timeline_utils.py │ └── timeline_view.py └── tests ├── __init__.py ├── ipynb ├── __init__.py └── manual_test.ipynb └── python ├── __init__.py └── test_debugger_mock.py /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | 3 | # build the documentation whenever there are new commits on main 4 | on: 5 | push: 6 | branches: 7 | - main 8 | # Alternative: only build for tags. 9 | # tags: 10 | # - '*' 11 | 12 | # security: restrict permissions for CI jobs. 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | # Build the documentation and upload the static HTML files as an artifact. 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.11' 25 | 26 | # install all dependencies (including pdoc) 27 | - run: pip install -e . 28 | - run: pip install pdoc 29 | - run: pdoc src/ -o docs/ 30 | 31 | - uses: actions/upload-pages-artifact@v2 32 | with: 33 | path: docs/ 34 | 35 | # Deploy the artifact to GitHub pages. 36 | # This is a separate job so that only actions/deploy-pages has the necessary permissions. 37 | deploy: 38 | needs: build 39 | runs-on: ubuntu-latest 40 | permissions: 41 | pages: write 42 | id-token: write 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | steps: 47 | - id: deployment 48 | uses: actions/deploy-pages@v2 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | # TODO: Update the event to trigger on a release 4 | 5 | # on: 6 | # release: 7 | # types: [published] 8 | # workflow_dispatch: 9 | 10 | # jobs: 11 | # pypi-publish: 12 | # name: Build dist & upload to PyPI 13 | # runs-on: ubuntu-latest 14 | # steps: 15 | # - uses: actions/checkout@v4 16 | # with: 17 | # fetch-depth: 1 18 | 19 | # - name: Set up Python 20 | # uses: actions/setup-python@v5 21 | # with: 22 | # python-version: '3.10' 23 | 24 | # - name: Build binary wheel + source tarball 25 | # run: | 26 | # python3 -m pip install --upgrade pip build 27 | # python3 -m build 28 | 29 | # - name: Publish package to PyPI 30 | # uses: pypa/gh-action-pypi-publish@release/v1 31 | # with: 32 | # user: __token__ 33 | # password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Install Package 33 | run: | 34 | python -m pip install .[test] 35 | - name: Lint with flake8 36 | run: | 37 | # stop the build if there are Python syntax errors or undefined names 38 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 39 | - name: Test with pytest 40 | run: | 41 | pytest 42 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude *.py[cod] 2 | 3 | # Include test files 4 | recursive-include tests *.py 5 | recursive-include imgs *.png 6 | recursive-include imgs *.gif 7 | include src/qiskit_trebugger/views/widget/resources/*.png 8 | include tests/ipython/*.ipynb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiskit Trebugger 2 | 3 | [![Unitary Fund](https://img.shields.io/badge/Supported%20By-UNITARY%20FUND-brightgreen.svg?style=for-the-badge)](http://unitary.fund) 4 | 5 | A new take on debuggers for quantum transpilers. 6 | This repository presents a debugger for the **qiskit transpiler** in the form of a lightweight jupyter widget. Built as a project for the Qiskit Advocate Mentorship Program, Fall 2021. 7 | 8 | 9 | ## Installation 10 | 1. To install the debugger using pip (a Python package manager), use - 11 | 12 | ```bash 13 | pip install qiskit-trebugger 14 | ``` 15 | PIP will handle the dependencies required for the package automatically and would install the latest version. 16 | 17 | 2. To directly install via github follow the steps below after using `git clone`: 18 | ```bash 19 | git clone https://github.com/TheGupta2012/qiskit-timeline-debugger.git 20 | ``` 21 | - Make sure `python3` and `pip` are installed in your system. It is recommended to use a Python virtual environment to install and develop the debugger 22 | - `cd` into the `qiskit-timeline-debugger` directory 23 | - Use `pip install -r requirements.txt` to install the project dependencies 24 | - Next, execute `pip install .` command to install the debugger 25 | 26 | ## Usage Instructions 27 | 28 | - After installing the package, import the `Debugger` instance from `qiskit_trebugger` package. 29 | - To run the debugger, simply replace the call to `transpile()` method of the qiskit module with `debug()` method of your debugger instance. 30 | - The debugger provides two types of views namely *jupyter* and *cli* 31 | - The **cli** view is the default view and recommender for users who want to use the debugger in a terminal environment 32 | - The **jupyter** view is recommended for usage in a jupyter notebook and provides a more interactive and detailed view of the transpilation process. 33 | - For example - 34 | 35 | ```python 36 | from qiskit_ibm_runtime.fake_provider import FakeCasablanca 37 | from qiskit.circuit.random import random_circuit 38 | from qiskit_trebugger import Debugger 39 | import warnings 40 | 41 | warnings.simplefilter('ignore') 42 | debugger = Debugger() 43 | backend = FakeCasablanca() 44 | circuit = random_circuit(num_qubits = 3, depth = 2 , seed = 42) 45 | # replace transpile call 46 | debugger.debug(circuit, view_type="jupyter", optimization_level = 2, backend = backend, initial_layout = list(range(4))) 47 | ``` 48 | - On calling the debug method, a new jupyter widget is displayed providing a complete summary and details of the transpilation process for circuits of < 2000 depth 49 | - With an easy-to-use and responsive interface, users can quickly see which transpiler passes ran when, how they changed the quantum circuit, and what exactly changed. 50 | 51 | 52 | ## Feature Highlights 53 | 54 | ### `jupyter` view 55 | 56 | 57 | 58 | #### 1. Circuit Evolution 59 | - See your circuit changing while going through the transpilation process for a target quantum processor. 60 | - A new custom feature enabling **visual diffs** for quantum circuits, allows you to see what exactly changed in your circuit using the matplotlib drawer of the qiskit module. 61 | 62 | > Example 63 | - Circuit 1 64 | 65 | 66 | - Circuit 2 67 | 68 | 69 | 70 | 71 | #### 2. Circuit statistics 72 | - Allows users to quickly scan through how the major properties of a circuit transform during each transpilation pass. 73 | - Helps to quickly isolate the passes which were responsible for the major changes in the resultant circuit. 74 | 75 | 76 | 77 | #### 3. Transpiler Logs and Property sets 78 | - Easily parse actions of the transpiler with logs emitted by each of its constituent passes and changes to the property set during transpilation 79 | - Every log record is color coded according to the level of severity i.e. `DEBUG`, `INFO`, `WARNING` and `CRITICAL`. 80 | 81 | 82 | 83 | 84 | 85 | ### `cli` view 86 | 87 | 88 | 89 | #### 1. Transpilation Summary and Statistics 90 | - A quick summary of the transpilation process for a given circuit. 91 | - Faster access to information in the CLI view. 92 | 93 | 94 | 95 | #### 2. Keyboard Shortcuts 96 | 97 | - The CLI view provides keyboard shortcuts for easy navigation and access to transpiler information. 98 | - An **interactive status bar** at the bottom of the screen provides information about the current state of the debugger. 99 | 100 | 101 | 102 | 103 | 104 | 105 | #### 3. Transpiler Logs and Property sets 106 | 107 | - Emits transpiler logs associated with each of the transpiler passes. 108 | - Highlights addition to the property set and its changes during the transpilation process. 109 | 110 | 111 | 112 | 113 | ## Docs 114 | We use the `pydoc` module to generate [project documentation](https://thegupta2012.github.io/qiskit-timeline-debugger/qiskit_trebugger.html) 115 | 116 | ## Demonstration and Blog 117 | - Here is a [demonstration of TreBugger](https://drive.google.com/file/d/1oRstcov-OQWDpsM7Q53x7BfgFC-edtkT/view?usp=sharing) as a part of the final showcase for the Qiskit Advocate Mentorship Program, Fall 2021. 118 | - You can also read some more details of our project in the [Qiskit medium blog](https://medium.com/qiskit/qiskit-trebugger-f7242066d368) 119 | 120 | ## Contributors 121 | - [Aboulkhair Foda](https://github.com/EgrettaThula) 122 | - [Harshit Gupta](https://github.com/TheGupta2012) 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/qiskit_trebugger/views.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | qiskit_trebugger.views API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 45 |
46 |
47 |

48 | qiskit_trebugger.views

49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 238 | -------------------------------------------------------------------------------- /docs/qiskit_trebugger/views/cli/cli_utils.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | qiskit_trebugger.views.cli.cli_utils API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 40 |
41 |
42 |

43 | qiskit_trebugger.views.cli.cli_utils

44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 233 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies_files": [ 3 | "requirements.txt" 4 | ], 5 | "extra_dependencies": [ 6 | "pytest" 7 | ], 8 | "language": { 9 | "name": "python", 10 | "versions": ["3.9", "3.10", "3.11"] 11 | }, 12 | "tests_command": [ 13 | "pytest" 14 | ], 15 | "styles_check_command": [ 16 | "pylint -rn src tests" 17 | ], 18 | "coverages_check_command": [ 19 | "coverage3 run -m pytest", 20 | "coverage3 report --fail-under=80" 21 | ], 22 | "qiskit" : true 23 | } -------------------------------------------------------------------------------- /imgs/cli/full-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/full-view.png -------------------------------------------------------------------------------- /imgs/cli/global-transpiler-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/global-transpiler-panel.png -------------------------------------------------------------------------------- /imgs/cli/indexed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/indexed-1.png -------------------------------------------------------------------------------- /imgs/cli/indexed-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/indexed-2.png -------------------------------------------------------------------------------- /imgs/cli/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/overview.png -------------------------------------------------------------------------------- /imgs/cli/status-idx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/status-idx.png -------------------------------------------------------------------------------- /imgs/cli/status-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/status-input.png -------------------------------------------------------------------------------- /imgs/cli/status-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/status-main.png -------------------------------------------------------------------------------- /imgs/cli/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/title.png -------------------------------------------------------------------------------- /imgs/cli/working.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/cli/working.gif -------------------------------------------------------------------------------- /imgs/jupyter/diff-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/jupyter/diff-1.png -------------------------------------------------------------------------------- /imgs/jupyter/diff-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/jupyter/diff-2.png -------------------------------------------------------------------------------- /imgs/jupyter/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/jupyter/logs.png -------------------------------------------------------------------------------- /imgs/jupyter/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/jupyter/stats.png -------------------------------------------------------------------------------- /imgs/jupyter/working.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/jupyter/working.gif -------------------------------------------------------------------------------- /imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/imgs/logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42","wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipython>=8.12.0 2 | ipywidgets==8.0.5 3 | qiskit>=1.0 4 | qiskit-aer>=0.13.3 5 | tabulate==0.9.0 6 | matplotlib>=3.3 7 | pylatexenc>=1.4 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = qiskit_trebugger 3 | version = 1.1.2 4 | author = Egretta Thula, Harshit Gupta 5 | author_email = harshit.11235@gmail.com 6 | description = A timeline debugger for the qiskit transpiler 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/TheGupta2012/qiskit-timeline-debugger 10 | 11 | classifiers = 12 | Programming Language :: Python :: 3 13 | License :: OSI Approved :: Apache Software License 14 | Operating System :: OS Independent 15 | 16 | [options] 17 | package_dir = 18 | = src 19 | packages = find: 20 | python_requires = >=3.9 21 | install_requires = 22 | ipython>=8.12.0 23 | ipywidgets==8.0.5 24 | qiskit>=1.0 25 | qiskit-aer>=0.13.3 26 | tabulate==0.9.0 27 | matplotlib>=3.3 28 | pylatexenc>=1.4 29 | 30 | [options.extras_require] 31 | test = qiskit_ibm_runtime>=0.20 32 | ipykernel>=6.29.5 33 | 34 | [options.package_data] # add the logo file to the package 35 | * = views/widget/resources/*.png 36 | 37 | [options.packages.find] 38 | where = src -------------------------------------------------------------------------------- /src/qiskit_trebugger/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qiskit Trebugger 3 | ========= 4 | 5 | Qiskit Trebugger is a debugger for Qiskit Quantum Circuits. 6 | """ 7 | 8 | from .debugger import Debugger 9 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/debugger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements the main Debugger class. 3 | Raises: 4 | DebuggerError: if multiple quantum circuits supplied for debugging 5 | """ 6 | 7 | import curses 8 | import logging 9 | import warnings 10 | from typing import Optional, Union 11 | 12 | from IPython.display import display 13 | from qiskit import QuantumCircuit, __version__, transpile 14 | from qiskit.providers.backend import Backend, BackendV1, BackendV2 15 | from qiskit.transpiler.basepasses import AnalysisPass, TransformationPass 16 | from qiskit_aer import Aer 17 | 18 | from qiskit_trebugger.model import ( 19 | TranspilationSequence, 20 | TranspilerDataCollector, 21 | TranspilerLoggingHandler, 22 | ) 23 | from qiskit_trebugger.views.cli.cli_view import CLIView 24 | from qiskit_trebugger.views.widget.timeline_view import TimelineView 25 | 26 | from .debugger_error import DebuggerError 27 | 28 | 29 | class Debugger: 30 | """Main debugger class for thr qiskit timeline debugger. 31 | 32 | Raises: 33 | DebuggerError: if multiple quantum circuits are supplied 34 | """ 35 | 36 | @classmethod 37 | def debug( 38 | cls, 39 | circuit: QuantumCircuit, 40 | backend: Optional[Union[Backend, BackendV1, BackendV2]] = None, 41 | optimization_level: Optional[int] = 0, 42 | view_type: Optional[str] = "cli", 43 | draw_coupling_map: Optional[bool] = False, 44 | show: Optional[bool] = True, 45 | **kwargs, 46 | ): 47 | """Calls the transpile method of qiskit with the given parameters 48 | and populates the view of the widget with circuit diagram and 49 | statistics. 50 | 51 | Args: 52 | circuit (QuantumCircuit): quantum circuit to debug 53 | backend (Optional[Union[Backend, BackendV1, BackendV2]], optional): 54 | Quantum Backend for execution. Defaults to None. 55 | optimization_level (Optional[int], optional): 56 | Optimization level of transpiler. Defaults to 0. 57 | 58 | Raises: 59 | DebuggerError: if multiple quantum circuits are supplied 60 | """ 61 | if view_type not in ["cli", "jupyter"]: 62 | raise DebuggerError("Invalid view type supplied!") 63 | 64 | if not isinstance(circuit, QuantumCircuit): 65 | raise DebuggerError("Debugger currently supports single QuantumCircuit only!") 66 | if backend is None: 67 | backend = Aer.get_backend("qasm_simulator") 68 | 69 | if view_type == "cli": 70 | if not cls._is_regular_interpreter(): 71 | raise DebuggerError("Can not invoke CLI view in IPython or Juptyer Environment!") 72 | cls.view = CLIView() 73 | else: 74 | cls.view = TimelineView() 75 | 76 | def on_step_callback(step): 77 | cls.view.add_step(step) 78 | 79 | # Prepare the model: 80 | transpilation_sequence = TranspilationSequence(on_step_callback) 81 | 82 | if isinstance(backend, BackendV2): 83 | backend_name = backend.name 84 | else: 85 | backend_name = backend.name() 86 | 87 | warnings.simplefilter("ignore") 88 | 89 | transpilation_sequence.general_info = { 90 | "backend": backend_name, 91 | "optimization_level": optimization_level, 92 | "qiskit version": __version__, 93 | } 94 | 95 | transpilation_sequence.original_circuit = circuit 96 | 97 | Debugger.register_logging_handler(transpilation_sequence) 98 | transpiler_callback = Debugger._get_data_collector(transpilation_sequence) 99 | 100 | # Pass the model to the view: 101 | cls.view.transpilation_sequence = transpilation_sequence 102 | 103 | if view_type == "jupyter": 104 | cls.view.update_params(**kwargs) 105 | if show: 106 | display(cls.view) 107 | 108 | final_circ = transpile( 109 | circuit, 110 | backend, 111 | optimization_level=optimization_level, 112 | callback=transpiler_callback, 113 | **kwargs, 114 | ) 115 | 116 | if view_type == "jupyter": 117 | cls.view.update_summary() 118 | cls.view.update_routing(final_circ, backend, draw_coupling_map) 119 | cls.view.update_timeline(final_circ, kwargs.get("scheduling_method", None)) 120 | cls.view.add_class("done") 121 | elif view_type == "cli": 122 | curses.wrapper(cls.view.display) 123 | 124 | @classmethod 125 | def register_logging_handler(cls, transpilation_sequence): 126 | """Registers logging handlers of different transpiler passes. 127 | 128 | Args: 129 | transpilation_sequence (TranspilationSequence): 130 | data structure to store the transpiler 131 | passes as a sequence of transpilation 132 | steps 133 | """ 134 | 135 | # TODO: Do not depend on loggerDict 136 | all_loggers = logging.Logger.manager.loggerDict 137 | passes_loggers = { 138 | key: value 139 | for (key, value) in all_loggers.items() 140 | if key.startswith("qiskit.transpiler.passes.") 141 | } 142 | 143 | loggers_map = {} 144 | for _pass in AnalysisPass.__subclasses__(): 145 | if _pass.__module__ in passes_loggers.keys(): 146 | loggers_map[_pass.__module__] = _pass.__name__ 147 | 148 | for _pass in TransformationPass.__subclasses__(): 149 | if _pass.__module__ in passes_loggers.keys(): 150 | loggers_map[_pass.__module__] = _pass.__name__ 151 | 152 | handler = TranspilerLoggingHandler( 153 | transpilation_sequence=transpilation_sequence, loggers_map=loggers_map 154 | ) 155 | logger = logging.getLogger("qiskit.transpiler.passes") 156 | logger.setLevel(logging.DEBUG) 157 | logger.addHandler(handler) 158 | 159 | @classmethod 160 | def _get_data_collector(cls, transpilation_sequence): 161 | """Returns the data collector callback function for transpiler. 162 | 163 | Args: 164 | transpilation_sequence (list): List of transpilation steps 165 | 166 | Returns: 167 | function: Callback function for transpiler 168 | """ 169 | return TranspilerDataCollector(transpilation_sequence).transpiler_callback 170 | 171 | @classmethod 172 | def _is_regular_interpreter(cls): 173 | """Checks if the interpreter is regular python interpreter or IPython 174 | 175 | Returns: 176 | bool: True if regular python interpreter, False otherwise 177 | """ 178 | try: 179 | # The function get_ipython() is available on the global 180 | # namespace by default when IPython is started. 181 | _ = get_ipython().__class__.__name__ 182 | 183 | # if this works, I am not in regular python 184 | # interpreter 185 | return False 186 | except NameError: 187 | return True 188 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/debugger_error.py: -------------------------------------------------------------------------------- 1 | """Module for debugger errors 2 | """ 3 | 4 | 5 | class DebuggerError(Exception): 6 | """Base class for errors raised by the debugger.""" 7 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/__init__.py: -------------------------------------------------------------------------------- 1 | """Sub-package to implement the model of the transpiler debugger 2 | """ 3 | 4 | from .circuit_comparator import CircuitComparator 5 | from .circuit_stats import CircuitStats 6 | from .data_collector import TranspilerDataCollector 7 | from .log_entry import LogEntry 8 | from .logging_handler import TranspilerLoggingHandler 9 | from .pass_type import PassType 10 | from .property import Property 11 | from .transpilation_sequence import TranspilationSequence 12 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/circuit_comparator.py: -------------------------------------------------------------------------------- 1 | """Implements the circuit diff functionality for quantum circuits. 2 | """ 3 | 4 | from numpy import uint16, zeros 5 | from qiskit.converters import circuit_to_dag, dag_to_circuit 6 | 7 | # make the global DP array 8 | LCS_DP = zeros((2000, 2000), dtype=uint16) 9 | 10 | 11 | class CircuitComparator: 12 | """Compares two quantum circuits and generates the 13 | circuit level diffs using longest common subsequences. 14 | """ 15 | 16 | @staticmethod 17 | def get_moments(dag): 18 | """Returns the layers of the dag circuit as list 19 | 20 | Args: 21 | dag (DAGCircuit): DAGCircuit representing a quantum circuit 22 | 23 | Returns: 24 | List: list of depth-1 circuits as graphs 25 | """ 26 | moments = [l["graph"] for l in list(dag.layers())] 27 | return moments 28 | 29 | @staticmethod 30 | def make_lcs(moments1, moments2): 31 | """Populates the LCS table by comparing the 32 | first circuit's layers with the second 33 | 34 | Args: 35 | moments1 (List): list of depth-1 layers 36 | moments2 (List): list of depth-1 layers 37 | """ 38 | 39 | # clear for the base cases of dp 40 | for i in range(2000): 41 | LCS_DP[i][0], LCS_DP[0][i] = 0, 0 42 | 43 | size_1, size_2 = len(moments1), len(moments2) 44 | 45 | for i in range(1, size_1 + 1): 46 | for j in range(1, size_2 + 1): 47 | # if the layers are isomorphic then okay 48 | if moments1[i - 1] == moments2[j - 1]: 49 | LCS_DP[i][j] = 1 + LCS_DP[i - 1][j - 1] 50 | else: 51 | LCS_DP[i][j] = max(LCS_DP[i - 1][j], LCS_DP[i][j - 1]) 52 | 53 | @staticmethod 54 | def compare(prev_circ, curr_circ): 55 | """Compares two circuits and returns the circuit diff as a 56 | quantum circuit with changed colors of diff 57 | 58 | Args: 59 | prev_circ (QuantumCircuit): first circuit 60 | curr_circ (QuantumCircuit): second circuit 61 | 62 | Returns: 63 | QuantumCircuit: the quantum circuit representing the 64 | circuit diff 65 | """ 66 | if prev_circ is None: 67 | return (False, curr_circ) 68 | 69 | # update by reference as there is no qasm now 70 | prev_dag = circuit_to_dag(prev_circ.copy()) 71 | curr_dag = circuit_to_dag(curr_circ.copy()) 72 | 73 | moments1 = CircuitComparator.get_moments(prev_dag) 74 | moments2 = CircuitComparator.get_moments(curr_dag) 75 | 76 | CircuitComparator.make_lcs(moments1, moments2) 77 | 78 | (size_1, size_2) = (len(moments1), len(moments2)) 79 | 80 | id_set = set() 81 | i = size_1 82 | j = size_2 83 | 84 | while i > 0 and j > 0: 85 | if moments1[i - 1] == moments2[j - 1]: 86 | # just want diff for second one 87 | id_set.add(j - 1) 88 | i -= 1 89 | j -= 1 90 | 91 | else: 92 | if LCS_DP[i - 1][j] > LCS_DP[i][j - 1]: 93 | # means the graph came from the 94 | # first circuit , go up 95 | i -= 1 96 | else: 97 | # if equal or small, go left 98 | j -= 1 99 | 100 | # if the whole circuit has not changed 101 | fully_changed = len(id_set) == 0 102 | 103 | if not fully_changed: 104 | for id2, layer in enumerate(list(curr_dag.layers())): 105 | if id2 not in id_set: 106 | # this is not an LCS node -> highlight it 107 | for node in layer["graph"].front_layer(): 108 | node.name = node.name + " " 109 | 110 | return (fully_changed, dag_to_circuit(curr_dag)) 111 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/circuit_stats.py: -------------------------------------------------------------------------------- 1 | """Module to implement a circuit stats class.""" 2 | 3 | 4 | class CircuitStats: 5 | """Class to capture different circuit statistics for a quantum circuit.""" 6 | 7 | def __init__(self) -> None: 8 | self.width = None 9 | self.size = None 10 | self.depth = None 11 | self.ops_1q = None 12 | self.ops_2q = None 13 | self.ops_3q = None 14 | 15 | def __eq__(self, other): 16 | return ( 17 | self.width == other.width 18 | and self.size == other.size 19 | and self.depth == other.depth 20 | and self.ops_1q == other.ops_1q 21 | and self.ops_2q == other.ops_2q 22 | and self.ops_3q == other.ops_3q 23 | ) 24 | 25 | def __repr__(self) -> str: 26 | return f"""CircuitStats(width={self.width}, 27 | size={self.size}, depth={self.depth}, 28 | 1q-ops={self.ops_1q}, 29 | 2q-ops={self.ops_2q}, 30 | 3+q-ops={self.ops_3q})""" 31 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/data_collector.py: -------------------------------------------------------------------------------- 1 | """Implements a data collector for the qiskit transpiler with a custom callback 2 | function. 3 | """ 4 | 5 | from copy import deepcopy 6 | 7 | from .pass_type import PassType 8 | from .property import Property 9 | from .transpilation_step import TranspilationStep 10 | 11 | 12 | class TranspilerDataCollector: 13 | """Class to implement the data collector and the custom 14 | callback function to collect data from the qiskit 15 | transpiler 16 | """ 17 | 18 | def __init__(self, transpilation_sequence) -> None: 19 | self.transpilation_sequence = transpilation_sequence 20 | self._properties = {} 21 | 22 | def callback(**kwargs): 23 | pass_ = kwargs["pass_"] 24 | 25 | pass_type = PassType.ANALYSIS if pass_.is_analysis_pass else PassType.TRANSFORMATION 26 | 27 | transpilation_step = TranspilationStep(pass_.name(), pass_type) 28 | transpilation_step.docs = pass_.__doc__ 29 | transpilation_step.run_method_docs = getattr(pass_, "run").__doc__ 30 | 31 | transpilation_step.duration = round(1000 * kwargs["time"], 2) 32 | 33 | # Properties 34 | property_set = kwargs["property_set"] 35 | _added_props = [] 36 | _updated_props = [] 37 | for key in property_set: 38 | value = property_set[key] 39 | if key not in self._properties.keys(): 40 | _added_props.append(key) 41 | elif (self._properties[key] is None) and (value is not None): 42 | _updated_props.append(key) 43 | elif hasattr(value, "__len__") and (len(value) != len(self._properties[key])): 44 | _updated_props.append(key) 45 | 46 | if len(_added_props) > 0 or len(_updated_props) > 0: 47 | for property_name in property_set: 48 | self._properties[property_name] = property_set[property_name] 49 | 50 | property_state = "" 51 | if property_name in _added_props: 52 | property_state = "new" 53 | elif property_name in _updated_props: 54 | property_state = "updated" 55 | 56 | transpilation_step.property_set[property_name] = Property( 57 | property_name, 58 | type(property_set[property_name]), 59 | property_set[property_name], 60 | property_state, 61 | ) 62 | 63 | dag = deepcopy(kwargs["dag"]) 64 | 65 | # circuit stats: 66 | if pass_.is_analysis_pass and len(self.transpilation_sequence.steps) > 0: 67 | transpilation_step.circuit_stats = self.transpilation_sequence.steps[ 68 | -1 69 | ].circuit_stats 70 | else: 71 | transpilation_step.circuit_stats.width = dag.width() 72 | transpilation_step.circuit_stats.size = dag.size() 73 | transpilation_step.circuit_stats.depth = dag.depth() 74 | 75 | circ_ops = {1: 0, 2: 0, 3: 0} 76 | 77 | for node in dag.op_nodes(include_directives=False): 78 | operands_count = len(node.qargs) 79 | if operands_count < 4: 80 | circ_ops[operands_count] += 1 81 | 82 | transpilation_step.circuit_stats.ops_1q = circ_ops[1] 83 | transpilation_step.circuit_stats.ops_2q = circ_ops[2] 84 | transpilation_step.circuit_stats.ops_3q = circ_ops[3] 85 | 86 | # Store `dag` to use it for circuit plot generation: 87 | if ( 88 | transpilation_step.pass_type == PassType.TRANSFORMATION 89 | and transpilation_step.circuit_stats.depth <= 300 90 | ): 91 | transpilation_step.dag = dag 92 | 93 | self.transpilation_sequence.add_step(transpilation_step) 94 | 95 | self._transpiler_callback = callback 96 | 97 | @property 98 | def transpiler_callback(self): 99 | """Custom callback for the transpiler 100 | 101 | Returns: 102 | function object: function handle for the callback 103 | """ 104 | return self._transpiler_callback 105 | 106 | def show_properties(self): 107 | """Displays transpilation sequence properties""" 108 | print("Properties of transpilation sequence : ", self._properties) 109 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/log_entry.py: -------------------------------------------------------------------------------- 1 | """Defines a log entry for the debugger""" 2 | 3 | from time import time 4 | 5 | 6 | class LogEntry: 7 | """Class for a log entry of the debugger""" 8 | 9 | def __init__(self, levelname, msg, args): 10 | """Defines a log entry with the level of log, 11 | log message and the arguments 12 | 13 | Args: 14 | levelname (str): Level of severity for the log 15 | msg (str): log message 16 | args (list): list of arguments for the log 17 | """ 18 | self.levelname = levelname 19 | self.msg = msg 20 | self.args = args 21 | self.time = time() 22 | 23 | def __repr__(self) -> str: 24 | return f"[{self.levelname}] {self.msg}" 25 | 26 | def get_args(self): 27 | """Get the arguments of log entry 28 | 29 | Returns: 30 | list: argument list 31 | """ 32 | return self.args 33 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/logging_handler.py: -------------------------------------------------------------------------------- 1 | """Implements a custom logging handler for the debugger 2 | """ 3 | 4 | import logging 5 | 6 | from .log_entry import LogEntry 7 | 8 | 9 | class TranspilerLoggingHandler(logging.Handler): 10 | """Implements a custom logging handler for the 11 | debugger 12 | """ 13 | 14 | def __init__(self, *args, **kwargs): 15 | """Define a logging handler with custom attributes""" 16 | self.loggers_map = kwargs["loggers_map"] 17 | kwargs.pop("loggers_map", None) 18 | 19 | self.transpilation_sequence = kwargs["transpilation_sequence"] 20 | kwargs.pop("transpilation_sequence", None) 21 | 22 | super().__init__(*args, **kwargs) 23 | 24 | def emit(self, record): 25 | log_entry = LogEntry(record.levelname, record.msg, record.args) 26 | self.transpilation_sequence.add_log_entry(self.loggers_map[record.name], log_entry) 27 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/pass_type.py: -------------------------------------------------------------------------------- 1 | """Implements enum for the transpiler passes. 2 | """ 3 | 4 | from enum import Enum 5 | 6 | 7 | class PassType(Enum): 8 | """Defines the analysis and transformation passes 9 | as enums. 10 | """ 11 | 12 | ANALYSIS = "Analysis" 13 | TRANSFORMATION = "Transformation" 14 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/property.py: -------------------------------------------------------------------------------- 1 | """Module to implement a property of a circuit. 2 | """ 3 | 4 | from collections import defaultdict 5 | 6 | 7 | class Property: 8 | """Implements the property of a transpiler pass as a data structure.""" 9 | 10 | LARGE_VALUE_THRESHOLD = 2000 11 | 12 | def __init__(self, name, prop_type, value, state) -> None: 13 | self.name = name 14 | self.prop_type = prop_type 15 | self.state = state 16 | 17 | if prop_type in (list, defaultdict) and (len(value) > self.LARGE_VALUE_THRESHOLD): 18 | print(len(value)) 19 | self.value = "LARGE_VALUE" 20 | else: 21 | self.value = value 22 | 23 | def __repr__(self) -> str: 24 | return f"{self.name} ({self.prop_type.__name__}) : {self.value}" 25 | 26 | def __eq__(self, other): 27 | if self.name != other.name: 28 | return False 29 | if self.prop_type != other.prop_type: 30 | return False 31 | if self.state != other.state: 32 | return False 33 | if self.value != other.value: 34 | return False 35 | return True 36 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/transpilation_sequence.py: -------------------------------------------------------------------------------- 1 | """Implements the module for the transpilation sequence of a quantum circuit. 2 | """ 3 | 4 | 5 | class TranspilationSequence: 6 | """Class to implement the transpilation sequence.""" 7 | 8 | def __init__(self, on_step_callback) -> None: 9 | self._original_circuit = None 10 | self._general_info = {} 11 | self.on_step_callback = on_step_callback 12 | self.steps = [] 13 | self.total_runtime = 0 14 | self._collected_logs = {} 15 | 16 | @property 17 | def original_circuit(self): 18 | """Returns the original circuit object""" 19 | return self._original_circuit 20 | 21 | @original_circuit.setter 22 | def original_circuit(self, circuit): 23 | self._original_circuit = circuit 24 | 25 | @property 26 | def general_info(self): 27 | """Returns the general_info dictionary""" 28 | return self._general_info 29 | 30 | @general_info.setter 31 | def general_info(self, info): 32 | self._general_info = info 33 | 34 | def add_step(self, step): 35 | """Adds a transpilation step to the sequence 36 | 37 | Args: 38 | step (TranspilationStep): a transpilation step 39 | """ 40 | if step.name in self._collected_logs: 41 | step.logs = self._collected_logs[step.name] 42 | self._collected_logs.pop(step.name, None) 43 | 44 | step.index = len(self.steps) 45 | self.steps.append(step) 46 | self.total_runtime += round(step.duration, 2) 47 | 48 | # property set index: 49 | idx = step.index 50 | while len(self.steps[idx].property_set) == 0: 51 | idx = idx - 1 52 | if idx < 0: 53 | idx = 0 54 | break 55 | self.steps[-1].property_set_index = idx 56 | 57 | # Notify: 58 | self.on_step_callback(self.steps[-1]) 59 | 60 | def add_log_entry(self, pass_name, log_entry): 61 | """Adds log entry to the transpilation pass 62 | 63 | Args: 64 | pass_name (str): name of the pass 65 | log_entry (LogEntry): Log entry to be appended 66 | """ 67 | if not pass_name in self._collected_logs: 68 | self._collected_logs[pass_name] = [] 69 | 70 | self._collected_logs[pass_name].append(log_entry) 71 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/model/transpilation_step.py: -------------------------------------------------------------------------------- 1 | """Implements a transpiler pass as a common data structure called TranspilationStep. 2 | """ 3 | 4 | from .circuit_stats import CircuitStats 5 | 6 | 7 | class TranspilationStep: 8 | """Models a transpilation pass as a step 9 | with different types of properties and 10 | statistics 11 | """ 12 | 13 | def __init__(self, name, pass_type) -> None: 14 | self.index = None 15 | self.name = name 16 | self.pass_type = pass_type 17 | self.docs = "" 18 | self.run_method_docs = "" 19 | self.duration = 0 20 | self.circuit_stats = CircuitStats() 21 | self.property_set = {} 22 | self.property_set_index = None 23 | self.logs = [] 24 | self.dag = None 25 | 26 | def __repr__(self) -> str: 27 | return f"(name={self.name}, pass_type={self.pass_type})" 28 | 29 | def get_docs(self): 30 | """Return doc string of the pass 31 | 32 | Returns: 33 | str: docstring of the step 34 | """ 35 | return self.docs 36 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/src/qiskit_trebugger/views/__init__.py -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/src/qiskit_trebugger/views/cli/__init__.py -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/cli/cli_pass_pad.py: -------------------------------------------------------------------------------- 1 | """The Transpiler Pass Pad for the CLI Debugger 2 | """ 3 | 4 | import curses 5 | from collections import defaultdict 6 | from datetime import datetime 7 | 8 | import tabulate 9 | 10 | 11 | class TranspilerPassPad: 12 | """The Transpiler Pass Pad""" 13 | 14 | def __init__(self, step, circuit, property_set, height, width, pad_obj): 15 | """Pass Pad for the CLI Debugger 16 | 17 | Args: 18 | step (TranspilationStep): The transpilation step to be displayed 19 | circuit (): Text circuit diagram 20 | property_set (default dict): The property set to be displayed 21 | height (int)): The height of the pad 22 | width (int): The width of the pad 23 | pad_obj (curses.Window): The curses pad object 24 | """ 25 | self.transpiler_pass = step 26 | self.circuit = circuit 27 | self.property_set = property_set 28 | self.log_data = [] 29 | self.height = height 30 | self.width = width 31 | self.pad = pad_obj 32 | self._start_row = 0 33 | 34 | def _get_center(self, width, string_len, divisor=2): 35 | """Get the center of the pad 36 | 37 | Args: 38 | width (int): The width of the pad 39 | string_len (int): The length of the string to be centered 40 | divisor (int, optional): The divisor to be used. Defaults to 2. 41 | 42 | """ 43 | return max(0, int(width // divisor - string_len // 2 - string_len % 2)) 44 | 45 | def _display_header(self, string): 46 | """Display a header in the pad 47 | 48 | Args: 49 | string (str): The string to be displayed 50 | """ 51 | offset = self._get_center(self.width, len(string)) 52 | self.pad.addstr(self._start_row, offset, string, curses.A_BOLD) 53 | 54 | def _add_title(self): 55 | """Add the title of the pass to the pad 56 | 57 | Args: 58 | None 59 | """ 60 | pass_name = f"{self.transpiler_pass.index}. {self.transpiler_pass.name}"[: self.width - 1] 61 | title_offset = self._get_center(self.width - 4, len(pass_name)) 62 | self.pad.addstr( 63 | self._start_row, 64 | title_offset, 65 | pass_name, 66 | curses.A_BOLD, 67 | ) 68 | self._start_row += 1 69 | self.pad.hline(self._start_row, 0, "_", self.width - 4) 70 | 71 | def _add_information(self): 72 | """Add the information of the pass to the pad 73 | 74 | Args: 75 | None 76 | """ 77 | self._start_row += 2 78 | pass_type = self.transpiler_pass.pass_type.value 79 | pass_runtime = self.transpiler_pass.duration 80 | info_string = f"Type : {pass_type} | Runtime (ms) : {pass_runtime}"[: self.width - 1] 81 | 82 | self._display_header(info_string) 83 | 84 | def _add_statistics(self): 85 | """Add the statistics of the pass to the pad 86 | 87 | Args: 88 | None 89 | """ 90 | 91 | self._start_row += 2 92 | stats = self.transpiler_pass.circuit_stats 93 | props_string = ( 94 | f"Depth : {stats.depth} | Width : {stats.width} | " 95 | f"Size : {stats.size} | 1Q Ops : {stats.ops_1q} | " 96 | f"2Q Ops : {stats.ops_2q}"[: self.width - 1] 97 | ) 98 | 99 | props_string = props_string[: self.width - 1] 100 | props_offset = self._get_center(self.width, len(props_string)) 101 | self.pad.addstr(self._start_row, props_offset, props_string) 102 | 103 | def _get_property_data(self): 104 | """Get the property set data as a list of lists 105 | 106 | Args: 107 | None 108 | """ 109 | 110 | prop_data = [] 111 | vf2_properties = { 112 | "VF2Layout_stop_reason", 113 | "VF2PostLayout_stop_reason", 114 | } 115 | 116 | for name, property_ in self.property_set.items(): 117 | changed_prop = True 118 | if property_.prop_type not in (int, float, bool, str): 119 | if name in vf2_properties: 120 | txt = property_.value.name 121 | elif name == "optimization_loop_minimum_point_state": 122 | txt = f"""score : {property_.value.score}, since : {property_.value.since}""" 123 | elif name == "commutation_set": 124 | txt = "(dict)" 125 | else: 126 | txt = ( 127 | "(dict)" 128 | if isinstance(property_.value, defaultdict) 129 | else "(" + property_.prop_type.__name__ + ")" 130 | ) 131 | 132 | else: 133 | txt = str(property_.value) 134 | 135 | if not property_.state or len(property_.state) == 0: 136 | changed_prop = False 137 | property_.state = "---" 138 | 139 | data_item = [name, txt, property_.state] 140 | if changed_prop: 141 | prop_data.insert(0, data_item) 142 | else: 143 | prop_data.append(data_item) 144 | 145 | return prop_data 146 | 147 | def _add_property_set(self): 148 | """Add the property set to the pad 149 | 150 | Args: 151 | None 152 | """ 153 | 154 | self._start_row += 2 155 | self._display_header("Property Set"[: self.width - 1]) 156 | self._start_row += 1 157 | 158 | headers = ["Property", "Value", "State"] 159 | 160 | prop_data = self._get_property_data() 161 | 162 | prop_set_table = tabulate.tabulate( 163 | tabular_data=prop_data, 164 | headers=headers, 165 | tablefmt="simple_grid", 166 | stralign="center", 167 | numalign="center", 168 | showindex=True, 169 | ).splitlines() 170 | 171 | props_offset = self._get_center(self.width, len(prop_set_table[0])) 172 | for index, row in enumerate(prop_set_table): 173 | # 0 is default 174 | highlight = 0 if index > 2 else curses.A_BOLD 175 | self.pad.addstr( 176 | index + self._start_row, 177 | props_offset, 178 | row[: self.width - 1], 179 | highlight, 180 | ) 181 | self._start_row += len(prop_set_table) 182 | 183 | def _add_original_qubits(self): 184 | """Add information about original qubit indices to the pad.""" 185 | if "original_qubit_indices" not in self.property_set: 186 | return 187 | 188 | self._start_row += 2 189 | self._display_header("Original Qubit Indices"[: self.width - 1]) 190 | self._start_row += 1 191 | 192 | original_indices = self.property_set["original_qubit_indices"].value.items() 193 | 194 | index_data = [] 195 | for qubit, index in original_indices: 196 | index_data.append([qubit, index]) 197 | 198 | headers = ["Qubit", "Index"] 199 | 200 | indices_table = tabulate.tabulate( 201 | tabular_data=index_data, 202 | headers=headers, 203 | tablefmt="simple_grid", 204 | stralign="center", 205 | numalign="center", 206 | showindex=False, 207 | ).splitlines() 208 | 209 | indices_offset = self._get_center(self.width, len(indices_table[0])) 210 | for index, row in enumerate(indices_table): 211 | # 0 is default 212 | highlight = 0 if index > 2 else curses.A_BOLD 213 | self.pad.addstr( 214 | index + self._start_row, 215 | indices_offset, 216 | row[: self.width - 1], 217 | highlight, 218 | ) 219 | self._start_row += len(indices_table) 220 | 221 | def _add_layout(self, layout_type): 222 | """Add layout information to the pad. 223 | 224 | Args: 225 | layout_type (str): The type of layout to be added. 226 | """ 227 | if ( 228 | "original_qubit_indices" not in self.property_set 229 | or layout_type not in self.property_set 230 | ): 231 | return 232 | 233 | # total num of physical qubits 234 | physical_qubits = len(self.property_set["original_qubit_indices"].value) 235 | curr_layout = self.property_set[layout_type].value.get_physical_bits() 236 | 237 | # original map of qubits to indices 238 | original_indices = self.property_set["original_qubit_indices"].value 239 | 240 | # add the layout to the pad 241 | self._start_row += 2 242 | self._display_header(f"{layout_type}"[: self.width - 1]) 243 | self._start_row += 1 244 | 245 | elements_per_table = 15 246 | # multiple tables required 247 | num_tables = physical_qubits // elements_per_table 248 | num_tables += 1 if physical_qubits % elements_per_table != 0 else 0 249 | 250 | for i in range(num_tables): 251 | start = i * elements_per_table 252 | end = start + elements_per_table - 1 253 | 254 | if start >= end: 255 | break 256 | 257 | data = [f"Physical Qubits({start}-{min(physical_qubits-1,end)})"] 258 | 259 | for qubit in range(start, end + 1): 260 | if qubit not in curr_layout: 261 | data.append("--") 262 | continue 263 | virtual_qubit = curr_layout[qubit] 264 | data.append(original_indices[virtual_qubit]) 265 | 266 | # draw this single row table now 267 | data_table = tabulate.tabulate( 268 | tabular_data=[data], 269 | tablefmt="simple_grid", 270 | stralign="center", 271 | numalign="center", 272 | showindex=False, 273 | ).splitlines() 274 | 275 | table_offset = self._get_center(self.width, len(data_table[0])) 276 | for row, _ in enumerate(data_table): 277 | self.pad.addstr( 278 | row + self._start_row, 279 | table_offset, 280 | data_table[row][: self.width - 1], 281 | curses.A_BOLD, 282 | ) 283 | self._start_row += len(data_table) + 1 284 | 285 | self._start_row += 1 286 | 287 | def _add_commutation_set(self): 288 | """Add commutation set information to the pad.""" 289 | if "commutation_set" not in self.property_set: 290 | return 291 | 292 | # add the layout to the pad 293 | self._start_row += 2 294 | self._display_header("Commutation Set"[: self.width - 1]) 295 | self._start_row += 1 296 | 297 | comm_set = self.property_set["commutation_set"].value 298 | comm_data_1, comm_data_2 = [], [] 299 | for key, value in comm_set.items(): 300 | if not isinstance(key, tuple): 301 | comm_data_1.append([key, value]) 302 | else: 303 | comm_data_2.append([key, value]) 304 | 305 | def _display_comm_table(data, header): 306 | if len(data) == 0: 307 | data = [[]] 308 | comm_table = tabulate.tabulate( 309 | tabular_data=data, 310 | headers=header, 311 | tablefmt="simple_grid", 312 | stralign="center", 313 | numalign="center", 314 | showindex=False, 315 | maxcolwidths=50, 316 | ).splitlines() 317 | 318 | table_offset = self._get_center(self.width, len(comm_table[0])) 319 | for row in range(len(comm_table)): 320 | self.pad.addstr( 321 | row + self._start_row, 322 | table_offset, 323 | comm_table[row][: self.width - 1], 324 | ) 325 | self._start_row += len(comm_table) + 2 326 | 327 | header_1 = ["Bit", "Node List"] 328 | header_2 = ["Node Tuple", "Set Index"] 329 | _display_comm_table(comm_data_1, header_1) 330 | _display_comm_table(comm_data_2, header_2) 331 | 332 | def _add_documentation(self): 333 | """Add the documentation to the pad 334 | 335 | Args: 336 | None 337 | """ 338 | 339 | self._start_row += 2 340 | self._display_header("Documentation"[: self.width - 1]) 341 | self._start_row += 1 342 | pass_docs = self.transpiler_pass.get_docs() 343 | 344 | pass_docs = " " + pass_docs if pass_docs and pass_docs.count("\n") > 0 else "" 345 | pass_docs = [[pass_docs], [self.transpiler_pass.run_method_docs]] 346 | 347 | docs_table = tabulate.tabulate( 348 | tabular_data=pass_docs, 349 | tablefmt="simple_grid", 350 | stralign="left", 351 | ).splitlines() 352 | 353 | docs_offset = self._get_center(self.width, len(docs_table[0])) 354 | 355 | for idx, row in enumerate(docs_table): 356 | self.pad.addstr( 357 | idx + self._start_row, 358 | docs_offset, 359 | row[: self.width - 1], 360 | ) 361 | self._start_row += len(docs_table) 362 | 363 | def _add_circuit(self): 364 | """Add the circuit diagram to the pad 365 | 366 | Args: 367 | None 368 | """ 369 | self._start_row += 2 370 | self._display_header("Circuit Diagram"[: self.width - 1]) 371 | self._start_row += 1 372 | if self.transpiler_pass.circuit_stats.depth < 300: 373 | # only if <300 depth, we will get a circuit to draw 374 | circ_string = [[self.circuit.draw(output="text", fold=100)]] 375 | else: 376 | circ_string = [ 377 | [f"Circuit depth {self.transpiler_pass.circuit_stats.depth} too large to display"] 378 | ] 379 | circ_table = tabulate.tabulate( 380 | tabular_data=circ_string, 381 | tablefmt="simple_grid", 382 | stralign="center", 383 | numalign="center", 384 | ).splitlines() 385 | 386 | circ_offset = self._get_center(self.width, len(circ_table[0])) 387 | for index, row in enumerate(circ_table): 388 | self.pad.addstr(index + self._start_row, circ_offset, row) 389 | 390 | self._start_row += len(circ_table) 391 | 392 | def _add_logs(self): 393 | """Add the logs to the pad 394 | 395 | Args: 396 | None 397 | """ 398 | self._start_row += 2 399 | self._display_header("Logs"[: self.width - 1]) 400 | self._start_row += 1 401 | 402 | if not self.log_data: 403 | self.log_data = [] 404 | for entry in self.transpiler_pass.logs: 405 | log_string = f"{datetime.fromtimestamp(entry.time).strftime('%H:%M:%S.%f')[:-3]} | " 406 | 407 | log_string += f"{entry.levelname} \n {entry.msg}" % entry.args 408 | 409 | self.log_data.append([log_string]) 410 | if len(self.log_data) > 100: 411 | self.log_data.append(["..."]) 412 | break 413 | 414 | if not self.log_data: 415 | self.log_data = [["This pass does not display any Logs."]] 416 | 417 | log_table = tabulate.tabulate( 418 | tabular_data=self.log_data, 419 | tablefmt="simple_grid", 420 | stralign="left", 421 | numalign="center", 422 | ).splitlines() 423 | 424 | logs_offset = self._get_center(self.width, len(log_table[0])) 425 | for index, row in enumerate(log_table): 426 | self.pad.addstr(index + self._start_row, logs_offset, row[: self.width - 1]) 427 | self._start_row += len(log_table) 428 | 429 | def build_pad(self): 430 | """Build the pad view""" 431 | 432 | self._add_title() 433 | self._add_information() 434 | self._add_statistics() 435 | self._add_property_set() 436 | self._add_original_qubits() 437 | self._add_layout("layout") 438 | self._add_commutation_set() 439 | self._add_circuit() 440 | self._add_documentation() 441 | self._add_logs() 442 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/cli/cli_view.py: -------------------------------------------------------------------------------- 1 | """A module for the CLI view for the Qiskit Transpiler Debugger. 2 | """ 3 | 4 | import curses 5 | from curses.textpad import Textbox 6 | 7 | import tabulate 8 | from qiskit.converters import dag_to_circuit 9 | from qiskit.dagcircuit import DAGCircuit 10 | 11 | from ...model.pass_type import PassType 12 | from .cli_pass_pad import TranspilerPassPad 13 | from .config import COLORS 14 | 15 | 16 | class CLIView: 17 | """A class representing the CLI view for the Qiskit Transpiler Debugger.""" 18 | 19 | def __init__(self): 20 | """Initialize the CLIView object.""" 21 | self._title = None 22 | self._overview = None 23 | 24 | self._all_passes_data = [] 25 | self._all_passes_table = None 26 | self._pass_table_headers = [ 27 | "Pass Name", 28 | "Pass Type", 29 | "Runtime (ms)", 30 | "Depth", 31 | "Size", 32 | "1q Gates", 33 | "2q Gates", 34 | "Width", 35 | ] 36 | # add the whitespace option 37 | tabulate.PRESERVE_WHITESPACE = True 38 | 39 | self._all_passes_pad = None 40 | self._pass_pad_list = None 41 | self._status_bar = None 42 | self._title_string = "Qiskit Transpiler Debugger" 43 | 44 | self._status_strings = { 45 | "normal": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down |" 46 | " 'I': Index into a pass | 'H': Toggle overview | 'Q': Exit", 47 | "index": " STATUS BAR | Enter the index of the pass you want to view : ", 48 | "invalid": " STATUS BAR | Invalid input entered. Press Enter to continue.", 49 | "out_of_bounds": " STATUS BAR | Number entered is out of bounds." 50 | " Press Enter to continue.", 51 | "pass": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down |" 52 | " 'N/P': Move to next/previous | 'I': Index into a pass |" 53 | " 'B': Back to home | 'Q': Exit", 54 | } 55 | 56 | self._colors = { 57 | "title": None, 58 | "status": None, 59 | "base_pass_title": None, 60 | "changing_pass": None, 61 | } 62 | 63 | # define status object 64 | self._reset_view_params() 65 | 66 | # add the transpilation sequence 67 | self.transpilation_sequence = None 68 | 69 | def _reset_view_params(self): 70 | """Reset the view parameters to their default values.""" 71 | 72 | self._view_params = { 73 | "curr_row": 0, 74 | "curr_col": 0, 75 | "last_width": 0, 76 | "last_height": 0, 77 | "pass_id": -1, 78 | "transpiler_pad_width": 800, 79 | "transpiler_pad_height": 5000, 80 | "transpiler_start_row": 6, 81 | "transpiler_start_col": None, 82 | "status_type": "normal", 83 | "overview_visible": True, 84 | "overview_change": False, 85 | } 86 | 87 | def _init_color(self): 88 | """Initialize colors for the CLI interface.""" 89 | # Start colors in curses 90 | curses.start_color() 91 | 92 | curses.init_pair(1, COLORS.TITLE["front"], COLORS.TITLE["back"]) 93 | curses.init_pair(2, COLORS.STATUS["front"], COLORS.STATUS["back"]) 94 | curses.init_pair(3, COLORS.BASE_PASSES_TITLE["front"], COLORS.BASE_PASSES_TITLE["back"]) 95 | curses.init_pair(4, COLORS.CHANGING_PASS["front"], COLORS.CHANGING_PASS["back"]) 96 | 97 | self._colors["title"] = curses.color_pair(1) 98 | self._colors["status"] = curses.color_pair(2) 99 | self._colors["base_pass_title"] = curses.color_pair(3) 100 | self._colors["changing_pass"] = curses.color_pair(4) 101 | 102 | def _get_center(self, width, string_len, divisor=2): 103 | """Calculate the starting position for centering a string. 104 | 105 | Args: 106 | width (int): Total width of the container. 107 | string_len (int): Length of the string to be centered. 108 | divisor (int, optional): Divisor for the centering calculation. Defaults to 2. 109 | 110 | Returns: 111 | int: Starting position for centering the string. 112 | """ 113 | return max(0, int(width // divisor - string_len // 2 - string_len % 2)) 114 | 115 | def _handle_keystroke(self, key): 116 | """Handle the keystrokes for navigation within the CLI interface. 117 | 118 | Args: 119 | key (int): The key pressed by the user. 120 | 121 | Returns: 122 | None 123 | """ 124 | if key == curses.KEY_UP: 125 | self._view_params["curr_row"] -= 1 126 | self._view_params["curr_row"] = max(self._view_params["curr_row"], 0) 127 | elif key == curses.KEY_LEFT: 128 | self._view_params["curr_col"] -= 1 129 | elif key == curses.KEY_DOWN: 130 | self._view_params["curr_row"] += 1 131 | if self._view_params["status_type"] == "normal": 132 | self._view_params["curr_row"] = min( 133 | self._view_params["curr_row"], len(self._all_passes_table) - 1 134 | ) 135 | elif self._view_params["status_type"] in ["index", "pass"]: 136 | self._view_params["curr_row"] = min( 137 | self._view_params["curr_row"], 138 | 1999, 139 | ) 140 | 141 | elif key == curses.KEY_RIGHT: 142 | self._view_params["curr_col"] += 1 143 | 144 | if self._view_params["status_type"] == "normal": 145 | self._view_params["curr_col"] = min( 146 | self._view_params["curr_col"], len(self._all_passes_table[1]) - 1 147 | ) 148 | 149 | elif self._view_params["status_type"] in ["index", "pass"]: 150 | self._view_params["curr_col"] = min( 151 | self._view_params["curr_col"], 152 | curses.COLS - self._view_params["transpiler_start_col"] - 1, 153 | ) 154 | elif key in [ord("u"), ord("U")]: 155 | self._view_params["curr_row"] = max(self._view_params["curr_row"] - 10, 0) 156 | 157 | elif key in [ord("d"), ord("D")]: 158 | self._view_params["curr_row"] += 10 159 | if self._view_params["status_type"] == "normal": 160 | self._view_params["curr_row"] = min( 161 | self._view_params["curr_row"], len(self._all_passes_table) - 1 162 | ) 163 | elif self._view_params["status_type"] in ["index", "pass"]: 164 | self._view_params["curr_row"] = min( 165 | self._view_params["curr_row"], 166 | 1999, 167 | ) 168 | 169 | elif key in [ord("i"), ord("I")]: 170 | # user wants to index into the pass 171 | self._view_params["status_type"] = "index" 172 | 173 | elif key in [ord("n"), ord("N")]: 174 | if self._view_params["status_type"] in ["index", "pass"]: 175 | self._view_params["pass_id"] = min( 176 | self._view_params["pass_id"] + 1, 177 | len(self.transpilation_sequence.steps) - 1, 178 | ) 179 | self._view_params["status_type"] = "pass" 180 | 181 | elif key in [ord("p"), ord("P")]: 182 | if self._view_params["status_type"] in ["index", "pass"]: 183 | self._view_params["pass_id"] = max(0, self._view_params["pass_id"] - 1) 184 | self._view_params["status_type"] = "pass" 185 | 186 | elif key in [ord("b"), ord("B")]: 187 | # reset the required state variables 188 | self._view_params["status_type"] = "normal" 189 | self._view_params["pass_id"] = -1 190 | self._view_params["curr_col"] = 0 191 | self._view_params["curr_row"] = 0 192 | 193 | elif key in [ord("h"), ord("H")]: 194 | self._view_params["overview_visible"] = not self._view_params["overview_visible"] 195 | self._view_params["overview_change"] = True 196 | self._view_params["curr_col"] = 0 197 | self._view_params["curr_row"] = 0 198 | if not self._view_params["overview_visible"]: 199 | self._view_params["transpiler_start_col"] = 0 200 | 201 | def _build_title_win(self, cols): 202 | """Builds the title window for the debugger 203 | 204 | Args: 205 | cols (int): width of the window 206 | 207 | Returns: 208 | title_window (curses.window): title window object 209 | """ 210 | title_rows = 4 211 | title_cols = cols 212 | begin_row = 1 213 | title_window = curses.newwin(title_rows, title_cols, begin_row, 0) 214 | 215 | title_str = self._title_string[: title_cols - 1] 216 | 217 | # Add title string to the title window 218 | start_x_title = self._get_center(title_cols, len(title_str)) 219 | title_window.bkgd(self._colors["title"]) 220 | title_window.hline(0, 0, "-", title_cols) 221 | title_window.addstr(1, start_x_title, title_str, curses.A_BOLD) 222 | title_window.hline(2, 0, "-", title_cols) 223 | 224 | # add Subtitle 225 | subtitle = "| " 226 | for key, value in self.transpilation_sequence.general_info.items(): 227 | subtitle += f"{key}: {value} | " 228 | 229 | subtitle = subtitle[: title_cols - 1] 230 | start_x_subtitle = self._get_center(title_cols, len(subtitle)) 231 | title_window.addstr(3, start_x_subtitle, subtitle) 232 | 233 | return title_window 234 | 235 | def _get_overview_stats(self): 236 | """Get the overview statistics for the transpilation sequence. 237 | 238 | Returns: 239 | dict: A dictionary containing overview statistics for the transpilation sequence. 240 | """ 241 | init_step = self.transpilation_sequence.steps[0] 242 | final_step = self.transpilation_sequence.steps[-1] 243 | 244 | # build overview 245 | overview_stats = { 246 | "depth": {"init": 0, "final": 0}, 247 | "size": {"init": 0, "final": 0}, 248 | "width": {"init": 0, "final": 0}, 249 | } 250 | 251 | # get the depths, size and width 252 | init_step_dict = init_step.circuit_stats.__dict__ 253 | final_step_dict = final_step.circuit_stats.__dict__ 254 | 255 | for ( 256 | prop, 257 | value, 258 | ) in overview_stats.items(): # prop should have same name as in CircuitStats 259 | value["init"] = init_step_dict[prop] 260 | value["final"] = final_step_dict[prop] 261 | 262 | # get the op counts 263 | overview_stats["ops"] = {"init": 0, "final": 0} 264 | overview_stats["ops"]["init"] = ( 265 | init_step.circuit_stats.ops_1q 266 | + init_step.circuit_stats.ops_2q 267 | + init_step.circuit_stats.ops_3q 268 | ) 269 | 270 | overview_stats["ops"]["final"] = ( 271 | final_step.circuit_stats.ops_1q 272 | + final_step.circuit_stats.ops_2q 273 | + final_step.circuit_stats.ops_3q 274 | ) 275 | 276 | return overview_stats 277 | 278 | def _build_overview_win(self, rows, cols): 279 | """Build and return the overview window for the debugger. 280 | 281 | Args: 282 | rows (int): Height of the window. 283 | cols (int): Width of the window. 284 | 285 | Returns: 286 | curses.window: The overview window object. 287 | """ 288 | begin_row = 6 289 | overview_win = curses.newwin(rows, cols, begin_row, 0) 290 | 291 | total_passes = {"T": 0, "A": 0} 292 | for step in self.transpilation_sequence.steps: 293 | if step.pass_type == PassType.TRANSFORMATION: 294 | total_passes["T"] += 1 295 | else: 296 | total_passes["A"] += 1 297 | 298 | total_pass_str = f"Total Passes : {total_passes['A'] + total_passes['T']}"[: cols - 1] 299 | pass_categories_str = ( 300 | f"Transformation : {total_passes['T']} | Analysis : {total_passes['A']}"[: cols - 1] 301 | ) 302 | 303 | start_x = 5 304 | overview_win.addstr(5, start_x, "Pass Overview"[: cols - 1], curses.A_BOLD) 305 | overview_win.addstr(6, start_x, total_pass_str) 306 | overview_win.addstr(7, start_x, pass_categories_str) 307 | 308 | # runtime 309 | runtime_str = f"Runtime : {round(self.transpilation_sequence.total_runtime,2)} ms"[ 310 | : cols - 1 311 | ] 312 | overview_win.addstr(9, start_x, runtime_str, curses.A_BOLD) 313 | 314 | # circuit stats 315 | headers = ["Property", "Initial", "Final"] 316 | 317 | overview_stats = self._get_overview_stats() 318 | rows = [] 319 | for prop, value in overview_stats.items(): 320 | rows.append([prop.capitalize(), value["init"], value["final"]]) 321 | stats_table = tabulate.tabulate( 322 | rows, 323 | headers=headers, 324 | tablefmt="simple_grid", 325 | stralign=("center"), 326 | numalign="center", 327 | ).splitlines() 328 | 329 | for row in range(12, 12 + len(stats_table)): 330 | overview_win.addstr(row, start_x, stats_table[row - 12][: cols - 1]) 331 | 332 | # for correct formatting of title 333 | max_line_length = len(stats_table[0]) 334 | 335 | # add titles 336 | 337 | # stats header 338 | stats_str = "Circuit Statistics"[: cols - 1] 339 | stats_head_offset = self._get_center(max_line_length, len(stats_str)) 340 | overview_win.addstr(11, start_x + stats_head_offset, stats_str, curses.A_BOLD) 341 | 342 | # overview header 343 | overview_str = "TRANSPILATION OVERVIEW"[: cols - 1] 344 | start_x_overview = start_x + self._get_center(max_line_length, len(overview_str)) 345 | overview_win.hline(0, start_x, "_", min(cols, max_line_length)) 346 | overview_win.addstr(2, start_x_overview, overview_str, curses.A_BOLD) 347 | overview_win.hline(3, start_x, "_", min(cols, max_line_length)) 348 | 349 | # update the dimensions 350 | self._view_params["transpiler_start_col"] = start_x + max_line_length + 5 351 | return overview_win 352 | 353 | def _get_pass_title(self, cols): 354 | """Get the window object for the title of the pass table. 355 | 356 | Args: 357 | cols (int): Width of the window. 358 | 359 | Returns: 360 | curses.window: The window object for the pass title. 361 | """ 362 | height = 4 363 | 364 | width = max(5, cols - self._view_params["transpiler_start_col"] - 1) 365 | pass_title = curses.newwin( 366 | height, 367 | width, 368 | self._view_params["transpiler_start_row"], 369 | self._view_params["transpiler_start_col"], 370 | ) 371 | # add the title of the table 372 | transpiler_passes = "Transpiler Passes"[: cols - 1] 373 | start_header = self._get_center(width, len(transpiler_passes)) 374 | try: 375 | pass_title.hline(0, 0, "_", width - 4) 376 | pass_title.addstr(2, start_header, "Transpiler Passes", curses.A_BOLD) 377 | pass_title.hline(3, 0, "_", width - 4) 378 | except Exception as _: 379 | pass_title = None 380 | 381 | return pass_title 382 | 383 | def _get_statusbar_win(self, rows, cols, status_type="normal"): 384 | """Returns the status bar window object 385 | 386 | Args: 387 | rows (int): Current height of the terminal 388 | cols (nt): Current width of the terminal 389 | status_type (str, optional): Type of status of the debugger. Corresponds to 390 | different view states of the debugger. 391 | Defaults to "normal". 392 | 393 | STATUS STATES 394 | -normal : normal status bar 395 | -index : index status bar - user is entering the numbers 396 | (requires input to be shown to user) 397 | -invalid : error status bar - user has entered an invalid character 398 | -out_of_bounds : out of bounds status bar - user has entered a number out of bounds 399 | -pass : pass status bar - user has entered a valid number and is now 400 | viewing the pass details 401 | 402 | NOTE : processing is done after the user presses enter. 403 | This will only return a status bar window, TEXT processing is done within 404 | this function ONLY 405 | Returns: 406 | curses.window : Statusbar window object 407 | """ 408 | 409 | status_str = self._status_strings[status_type][: cols - 1] 410 | 411 | statusbar_window = curses.newwin(1, cols, rows - 1, 0) 412 | statusbar_window.bkgd(" ", self._colors["status"]) 413 | 414 | offset = 0 415 | statusbar_window.addstr(0, offset, status_str) 416 | offset += len(status_str) 417 | 418 | # now if index, enter a text box 419 | if status_type == "index": 420 | textbox = Textbox(statusbar_window) 421 | textbox.edit() 422 | str_value = textbox.gather().split(":")[1].strip() # get the value of the entered text 423 | 424 | try: 425 | num = int(str_value) 426 | total_passes = len(self.transpilation_sequence.steps) 427 | if num >= total_passes or num < 0: 428 | status_str = self._status_strings["out_of_bounds"] 429 | else: 430 | status_str = self._status_strings["pass"] 431 | self._view_params["pass_id"] = num 432 | except ValueError as _: 433 | # Invalid number entered 434 | status_str = self._status_strings["invalid"] 435 | status_str = status_str[: cols - 1] 436 | 437 | # display the new string 438 | statusbar_window.clear() 439 | offset = 0 440 | statusbar_window.addstr(0, 0, status_str) 441 | offset += len(status_str) 442 | 443 | statusbar_window.addstr(0, offset, " " * (cols - offset - 1)) 444 | 445 | return statusbar_window 446 | 447 | def _refresh_base_windows(self, resized, height, width): 448 | """Refreshes the base windows of the debugger 449 | 450 | Args: 451 | width (int): Current width of the terminal 452 | 453 | Returns: 454 | None 455 | """ 456 | if resized: 457 | self._title = self._build_title_win(width) 458 | self._title.noutrefresh() 459 | 460 | overview_toggle = ( 461 | self._view_params["overview_visible"] and self._view_params["overview_change"] 462 | ) 463 | if resized or overview_toggle: 464 | try: 465 | self._overview = self._build_overview_win(height, width) 466 | self._overview.noutrefresh() 467 | except Exception as _: 468 | # change the view param for overview 469 | self._view_params["transpiler_start_col"] = 0 470 | 471 | pass_title_window = self._get_pass_title(width) 472 | if pass_title_window: 473 | pass_title_window.noutrefresh() 474 | 475 | def _get_pass_circuit(self, step): 476 | if step.pass_type == PassType.TRANSFORMATION: 477 | if step.circuit_stats.depth > 300: 478 | # means it had depth > 300, so we can't show it 479 | return None 480 | return dag_to_circuit(step.dag) 481 | idx = step.index 482 | # Due to a bug in DAGCircuit.__eq__, we can not use ``step.dag != None`` 483 | 484 | found_transform = False 485 | while not isinstance(self.transpilation_sequence.steps[idx].dag, DAGCircuit) and idx > 0: 486 | idx = idx - 1 487 | if idx >= 0: 488 | found_transform = ( 489 | self.transpilation_sequence.steps[idx].pass_type == PassType.TRANSFORMATION 490 | ) 491 | 492 | if not found_transform: 493 | return self.transpilation_sequence.original_circuit 494 | 495 | return dag_to_circuit(self.transpilation_sequence.steps[idx].dag) 496 | 497 | def _get_pass_property_set(self, step): 498 | if step.property_set_index is not None: 499 | return self.transpilation_sequence.steps[step.property_set_index].property_set 500 | 501 | return {} 502 | 503 | def _build_pass_pad(self, index): 504 | step = self.transpilation_sequence.steps[index] 505 | pad = curses.newpad( 506 | self._view_params["transpiler_pad_height"], 507 | self._view_params["transpiler_pad_width"], 508 | ) 509 | pass_pad = TranspilerPassPad( 510 | step, 511 | self._get_pass_circuit(step), 512 | self._get_pass_property_set(step), 513 | self._view_params["transpiler_pad_height"], 514 | self._view_params["transpiler_pad_width"], 515 | pad, 516 | ) 517 | pass_pad.build_pad() 518 | self._pass_pad_list[index] = pass_pad.pad 519 | 520 | def add_step(self, step): 521 | """Adds a step to the transpilation sequence. 522 | 523 | Args: 524 | step (TranspilationStep): `TranspilationStep` object to be added to the 525 | transpilation sequence. 526 | """ 527 | self._all_passes_data.append( 528 | [ 529 | step.name, 530 | step.pass_type.value, 531 | step.duration, 532 | step.circuit_stats.depth, 533 | step.circuit_stats.size, 534 | step.circuit_stats.ops_1q, 535 | step.circuit_stats.ops_2q, 536 | step.circuit_stats.width, 537 | ] 538 | ) 539 | 540 | def _get_all_passes_table(self): 541 | """Generate and return the table containing all the transpiler passes. 542 | 543 | Returns: 544 | list: The list representing the table of all transpiler passes. 545 | """ 546 | # build from the transpilation sequence 547 | # make table 548 | pass_table = tabulate.tabulate( 549 | headers=self._pass_table_headers, 550 | tabular_data=self._all_passes_data, 551 | tablefmt="simple_grid", 552 | stralign="center", 553 | numalign="center", 554 | showindex="always", 555 | ).splitlines() 556 | 557 | return pass_table 558 | 559 | def _get_changing_pass_list(self): 560 | """Get the list of indices of passes that caused a change in the circuit. 561 | 562 | Returns: 563 | list: A list containing the indices of changing passes. 564 | """ 565 | pass_id_list = [] 566 | for i in range(1, len(self.transpilation_sequence.steps)): 567 | prev_step = self.transpilation_sequence.steps[i - 1] 568 | curr_step = self.transpilation_sequence.steps[i] 569 | if prev_step.circuit_stats != curr_step.circuit_stats: 570 | pass_id_list.append(i) 571 | return pass_id_list 572 | 573 | def _get_all_passes_pad(self): 574 | """Generate and return the pad containing all the transpiler passes. 575 | 576 | Returns: 577 | curses.pad: The pad containing all the transpiler passes. 578 | """ 579 | start_x = 4 580 | table_width = 500 # for now 581 | table_height = len(self._all_passes_table) + 1 582 | pass_pad = curses.newpad(table_height, table_width) 583 | 584 | header_height = 3 585 | 586 | # centering is required for each row 587 | for row in range(header_height): 588 | offset = self._get_center( 589 | table_width, len(self._all_passes_table[row][: table_width - 1]) 590 | ) 591 | pass_pad.addstr( 592 | row, 593 | start_x + offset, 594 | self._all_passes_table[row][: table_width - 1], 595 | curses.A_BOLD | self._colors["base_pass_title"], 596 | ) 597 | 598 | # generate a changing pass set to see which pass 599 | # changed the circuit and which didn't 600 | changing_pass_list = set(self._get_changing_pass_list()) 601 | 602 | def _is_changing_pass_row(row): 603 | # dashes only at even rows 604 | if row % 2 == 0: 605 | return False 606 | index = (row - header_height) // 2 607 | if index in changing_pass_list: 608 | return True 609 | return False 610 | 611 | # now start adding the passes 612 | for row in range(header_height, len(self._all_passes_table)): 613 | offset = self._get_center( 614 | table_width, len(self._all_passes_table[row][: table_width - 1]) 615 | ) 616 | highlight = 0 617 | 618 | if _is_changing_pass_row(row): 619 | highlight = curses.A_BOLD | self._colors["changing_pass"] 620 | 621 | pass_pad.addstr( 622 | row, 623 | start_x + offset, 624 | self._all_passes_table[row][: table_width - 1], 625 | highlight, 626 | ) 627 | 628 | # populated pad with passes 629 | return pass_pad 630 | 631 | def _render_transpilation_pad(self, pass_pad, curr_row, curr_col, rows, cols): 632 | """Function to render the pass pad. 633 | 634 | NOTE : this is agnostic of whether we are passing the base pad 635 | or the individual transpiler pass pad. Why? 636 | Because we are not shifting the pad, we are just refreshing it. 637 | 638 | Args: 639 | pass_pad (curses.pad): The pad containing the individual pass details. 640 | curr_row (int): Current row position. 641 | curr_col (int): Current column position. 642 | rows (int): Total number of rows in the terminal. 643 | cols (int): Total number of columns in the terminal. 644 | 645 | Returns: 646 | None 647 | """ 648 | if not pass_pad: 649 | return 650 | 651 | # 4 rows for the title + curr_row (curr_row is the row of the pass) 652 | title_height = 5 653 | start_row = self._view_params["transpiler_start_row"] + title_height 654 | 655 | # if we don't have enough rows 656 | if start_row >= rows - 2: 657 | return 658 | 659 | # if we don't have enough columns 660 | if self._view_params["transpiler_start_col"] >= cols - 6: 661 | return 662 | 663 | actual_width = pass_pad.getmaxyx()[1] 664 | window_width = cols - self._view_params["transpiler_start_col"] 665 | col_offset = (actual_width - window_width) // 2 666 | 667 | pass_pad.noutrefresh( 668 | curr_row, 669 | col_offset + curr_col, 670 | start_row, 671 | self._view_params["transpiler_start_col"], 672 | rows - 2, 673 | cols - 6, 674 | ) 675 | 676 | def _pre_input(self, height, width): 677 | """Function to render the pad before any input is entered 678 | by the user 679 | 680 | Args: 681 | height (int): Number of rows 682 | width (int): Number of cols 683 | """ 684 | pad_to_render = None 685 | 686 | if self._view_params["status_type"] == "index": 687 | pass_id = self._view_params["pass_id"] 688 | if pass_id == -1: 689 | pad_to_render = self._all_passes_pad 690 | else: 691 | if self._pass_pad_list[pass_id] is None: 692 | self._build_pass_pad(pass_id) 693 | pad_to_render = self._pass_pad_list[pass_id] 694 | 695 | self._render_transpilation_pad( 696 | pad_to_render, 697 | self._view_params["curr_row"], 698 | self._view_params["curr_col"], 699 | height, 700 | width, 701 | ) 702 | 703 | def _post_input(self, height, width): 704 | """Render the pad after user input is entered. 705 | 706 | Args: 707 | height (int): Number of rows in the terminal. 708 | width (int): Number of columns in the terminal. 709 | 710 | Returns: 711 | None 712 | """ 713 | pad_to_render = None 714 | if self._view_params["status_type"] == "normal": 715 | pad_to_render = self._all_passes_pad 716 | elif self._view_params["status_type"] in ["index", "pass"]: 717 | # using zero based indexing 718 | pass_id = self._view_params["pass_id"] 719 | if pass_id >= 0: 720 | self._view_params["status_type"] = "pass" 721 | if self._pass_pad_list[pass_id] is None: 722 | self._build_pass_pad(pass_id) 723 | pad_to_render = self._pass_pad_list[pass_id] 724 | 725 | self._render_transpilation_pad( 726 | pad_to_render, 727 | self._view_params["curr_row"], 728 | self._view_params["curr_col"], 729 | height, 730 | width, 731 | ) 732 | 733 | def display(self, stdscr): 734 | """Display the Qiskit Transpiler Debugger on the terminal. 735 | 736 | Args: 737 | stdscr (curses.window): The main window object provided by the curses library. 738 | 739 | Returns: 740 | None 741 | """ 742 | key = 0 743 | 744 | # Clear and refresh the screen for a blank canvas 745 | stdscr.clear() 746 | stdscr.refresh() 747 | 748 | # initiate color 749 | self._init_color() 750 | 751 | # hide the cursor 752 | curses.curs_set(0) 753 | 754 | # reset view params 755 | self._reset_view_params() 756 | 757 | height, width = stdscr.getmaxyx() 758 | self._refresh_base_windows(True, height, width) 759 | 760 | # build the base transpiler pad using the transpilation sequence 761 | self._all_passes_table = self._get_all_passes_table() 762 | self._all_passes_pad = self._get_all_passes_pad() 763 | self._pass_pad_list = [None] * len(self.transpilation_sequence.steps) 764 | 765 | # build the individual pass pad list 766 | # done, via add_step 767 | assert len(self._pass_pad_list) > 0 768 | 769 | while key not in [ord("q"), ord("Q")]: 770 | height, width = stdscr.getmaxyx() 771 | 772 | # Check for clearing 773 | panel_initiated = self._view_params["last_height"] + self._view_params["last_width"] > 0 774 | panel_resized = ( 775 | self._view_params["last_width"] != width 776 | or self._view_params["last_height"] != height 777 | ) 778 | 779 | if panel_initiated and panel_resized: 780 | stdscr.clear() 781 | 782 | self._view_params["overview_change"] = False 783 | self._handle_keystroke(key) 784 | 785 | whstr = f"Width: {width}, Height: {height}" 786 | stdscr.addstr(0, 0, whstr) 787 | 788 | # refresh the screen and then the windows 789 | stdscr.noutrefresh() 790 | self._refresh_base_windows(panel_resized, height, width) 791 | 792 | # pre input rendering 793 | self._pre_input(height, width) 794 | 795 | # render the status bar , irrespective of width / height 796 | # and get the input (if any) 797 | self._status_bar = self._get_statusbar_win( 798 | height, width, self._view_params["status_type"] 799 | ) 800 | self._status_bar.noutrefresh() 801 | 802 | # post input rendering 803 | self._post_input(height, width) 804 | 805 | self._view_params["last_width"] = width 806 | self._view_params["last_height"] = height 807 | 808 | curses.doupdate() 809 | 810 | # wait for the next input 811 | key = stdscr.getch() 812 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/cli/config.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | 4 | class COLORS: 5 | TITLE = {"back": curses.COLOR_WHITE, "front": curses.COLOR_MAGENTA} 6 | 7 | STATUS = {"back": curses.COLOR_CYAN, "front": curses.COLOR_BLACK} 8 | 9 | BASE_PASSES_TITLE = {"back": curses.COLOR_BLACK, "front": curses.COLOR_CYAN} 10 | 11 | CHANGING_PASS = {"back": curses.COLOR_BLACK, "front": curses.COLOR_CYAN} 12 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/widget/__init__.py: -------------------------------------------------------------------------------- 1 | """Implements the timeline view of the debugger 2 | """ 3 | 4 | from .button_with_value import ButtonWithValue 5 | from .timeline_utils import * 6 | from .timeline_view import TimelineView 7 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/widget/button_with_value.py: -------------------------------------------------------------------------------- 1 | """Implement a button with value 2 | """ 3 | 4 | import ipywidgets as widgets 5 | 6 | 7 | class ButtonWithValue(widgets.Button): 8 | """Implements a button with value by inheriting from 9 | Button class. 10 | """ 11 | 12 | def __init__(self, *args, **kwargs): 13 | self.value = kwargs["value"] 14 | kwargs.pop("value", None) 15 | super().__init__(*args, **kwargs) 16 | -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/widget/resources/qiskit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/src/qiskit_trebugger/views/widget/resources/qiskit-logo.png -------------------------------------------------------------------------------- /src/qiskit_trebugger/views/widget/timeline_utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions to support the timeline view 2 | """ 3 | 4 | import base64 5 | import os 6 | from binascii import b2a_base64 7 | from io import BytesIO 8 | 9 | import ipywidgets as widgets 10 | from qiskit import qpy as qpy_serialization 11 | from qiskit.qasm3 import dumps as qasm3_dumps 12 | from qiskit.visualization import plot_circuit_layout, timeline_drawer 13 | 14 | 15 | def get_args_panel(**kwargs): 16 | """Returns kwarg panel for the debugger 17 | 18 | Returns: 19 | widgets.HBox: Horizontal Box containing the 20 | arg panel 21 | """ 22 | # make two boxes for each key and values 23 | key_box = {} 24 | val_box = {} 25 | 26 | box_kwargs = { 27 | "width": "50%", 28 | "display": "flex", 29 | "align_items": "stretch", 30 | "flex_flow": "column", 31 | } 32 | for i in range(2): 33 | key_box[i] = widgets.VBox(layout=box_kwargs) 34 | val_box[i] = widgets.VBox(layout=box_kwargs) 35 | 36 | # make children dicts 37 | key_children = {0: [], 1: []} 38 | value_children = {0: [], 1: []} 39 | 40 | # counter 41 | index = 0 42 | 43 | for i, (key, val) in enumerate(kwargs.items()): 44 | if val is None: 45 | continue 46 | 47 | # make key and value labels 48 | key_label = widgets.HTML(r"

" + key + "

") 49 | 50 | value = val 51 | value_label = widgets.HTML(r"

" + str(value) + "

") 52 | 53 | # add to the list 54 | key_children[index].append(key_label) 55 | value_children[index].append(value_label) 56 | 57 | # flip box id 58 | index = 0 if i < len(kwargs.items()) // 2 else 1 59 | 60 | # construct the inner vertical boxes 61 | for i in range(2): 62 | key_box[i].children = key_children[i] 63 | val_box[i].children = value_children[i] 64 | 65 | # construct HBoxes 66 | args_boxes = [ 67 | widgets.HBox([key_box[0], val_box[0]], layout={"width": "50%"}), 68 | widgets.HBox([key_box[1], val_box[1]], layout={"width": "50%"}), 69 | ] 70 | 71 | # construct final HBox 72 | return widgets.HBox(args_boxes, layout={"margin": "10px 0 0 15px"}) 73 | 74 | 75 | def _get_img_html(fig): 76 | """Returns the html string for the image 77 | 78 | Args: 79 | fig (matplotlib.figure.Figure): The figure to convert to html 80 | 81 | Returns: 82 | str: HTML string with img data to be 83 | rendered into the debugger 84 | """ 85 | img_bio = BytesIO() 86 | fig.savefig(img_bio, format="png", bbox_inches="tight") 87 | fig.clf() 88 | img_data = b2a_base64(img_bio.getvalue()).decode() 89 | img_html = f""" 90 |
91 | 92 |
93 | """ 94 | return img_html 95 | 96 | 97 | def view_routing(circuit, backend, route_type): 98 | """Displays the routing of the circuit 99 | 100 | Args: 101 | circuit (QuantumCircuit): The circuit to route 102 | backend (IBMQBackend): The backend to route on 103 | route_type (str): The routing type to use 104 | 105 | Returns: 106 | str: HTML string with img data to be 107 | rendered into the debugger 108 | """ 109 | fig = plot_circuit_layout(circuit, backend, route_type) 110 | return _get_img_html(fig) 111 | 112 | 113 | def view_timeline(circuit): 114 | """Displays the timeline of the circuit 115 | 116 | Args: 117 | circuit (QuantumCircuit): The circuit for timeline view 118 | 119 | Returns: 120 | str: HTML string with img data to be 121 | rendered into the debugger 122 | """ 123 | fig = timeline_drawer(circuit) 124 | return _get_img_html(fig) 125 | 126 | 127 | def view_circuit(disp_circuit, suffix): 128 | """Displays the circuit with diff for the debuuger 129 | 130 | Args: 131 | disp_circuit : The circuit to display 132 | suffix (str) : The name to be added to pass 133 | 134 | Returns: 135 | str : HTML string with img data to be 136 | rendered into the debugger 137 | """ 138 | if "diff" in suffix: 139 | # means checkbox has been chosen for diff 140 | img_style = {"gatefacecolor": "orange", "gatetextcolor": "black"} 141 | else: 142 | img_style = None 143 | 144 | fig = disp_circuit.draw( 145 | "mpl", 146 | idle_wires=False, 147 | with_layout=False, 148 | scale=0.9, 149 | fold=20, 150 | style=img_style, 151 | ) 152 | 153 | img_bio = BytesIO() 154 | fig.savefig(img_bio, format="png", bbox_inches="tight") 155 | fig.clf() 156 | img_data = b2a_base64(img_bio.getvalue()).decode() 157 | 158 | qpy_bio = BytesIO() 159 | qpy_serialization.dump(disp_circuit, qpy_bio) 160 | qpy_data = b2a_base64(qpy_bio.getvalue()).decode() 161 | 162 | # qasm couldn't handle the circuit changed names 163 | # for instr in disp_circuit.data: 164 | # instr[0].name = instr[0].name.strip() 165 | 166 | qasm_str = qasm3_dumps(disp_circuit) 167 | qasm_bio = BytesIO(bytes(qasm_str, "ascii")) 168 | qasm_data = b2a_base64(qasm_bio.getvalue()).decode() 169 | 170 | img_html = f""" 171 |
172 | 173 |
174 |
175 | Save: 176 | 177 | PNG 178 | 179 | 180 | QPY 181 | 182 | 183 | QASM 184 | 185 |
186 | """ 187 | return img_html 188 | 189 | 190 | def get_spinner_html(): 191 | """Return the spinner html string""" 192 | return '
\ 193 |
\ 194 |
' 195 | 196 | 197 | def get_styles(): 198 | """Return the style for the debugger""" 199 | # Construct the path to the image file 200 | script_dir = os.path.dirname(__file__) 201 | image_path = os.path.join(script_dir, "resources", "qiskit-logo.png") 202 | with open(image_path, "rb") as image_file: 203 | encoded_image = base64.b64encode(image_file.read()).decode("utf-8") 204 | final_style = ( 205 | """ 206 | 556 | """ 557 | ) 558 | return final_style 559 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/tests/__init__.py -------------------------------------------------------------------------------- /tests/ipynb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/tests/ipynb/__init__.py -------------------------------------------------------------------------------- /tests/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGupta2012/qiskit-timeline-debugger/4177b8f7201641393ee8ca4d64e6ec71896bed37/tests/python/__init__.py -------------------------------------------------------------------------------- /tests/python/test_debugger_mock.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from qiskit.circuit.random import random_circuit 4 | from qiskit_ibm_runtime.fake_provider import ( 5 | FakeAthensV2, 6 | FakeBelemV2, 7 | FakeSherbrooke, 8 | FakeBrisbane, 9 | FakeKyiv, 10 | ) 11 | 12 | from qiskit_trebugger import Debugger 13 | import pytest 14 | 15 | MAX_DEPTH = 5 16 | 17 | 18 | class TestDebuggerMock: 19 | """Unit tests for different IBMQ fake backends v2""" 20 | 21 | all_backends = [FakeAthensV2(), FakeBelemV2(), FakeSherbrooke(), FakeBrisbane(), FakeKyiv()] 22 | 23 | def _internal_tester(self, view, backend, num_qubits): 24 | for qubits in [1, num_qubits // 2, num_qubits]: 25 | circ = random_circuit(qubits, MAX_DEPTH, measure=True) 26 | debugger = Debugger() 27 | debugger.debug( 28 | circ, 29 | backend, 30 | view_type=view, 31 | show=False, 32 | ) 33 | 34 | @pytest.mark.parametrize("backend", all_backends) 35 | def test_backend_v2(self, backend): 36 | """Backend V2 tests""" 37 | for view in ["jupyter"]: 38 | print(f"Testing with {backend.name}...") 39 | self._internal_tester(view, backend, backend.num_qubits) 40 | --------------------------------------------------------------------------------