├── .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 | [](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 |
17 |
18 |
19 |
44 |
45 |
46 |
47 |
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 |
17 |
18 |
19 |
39 |
40 |
41 |
42 |
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 |
186 | """
187 | return img_html
188 |
189 |
190 | def get_spinner_html():
191 | """Return the spinner html string"""
192 | return ''
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 |
--------------------------------------------------------------------------------