├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── codecov.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── cov.yml │ ├── doc.yml │ ├── release.yml │ ├── ruff.yml │ └── typecheck.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── benchmarks ├── readme.rst └── statevec.py ├── docs ├── Makefile ├── imgs │ ├── circuit.png │ ├── classical.png │ ├── fam_logo.png │ ├── graph.png │ ├── graph2.png │ ├── graphH.png │ ├── graph_cnot.png │ ├── graph_rot.png │ ├── graph_space1.png │ ├── graph_space2.png │ ├── graphsim.png │ ├── graphsim2.png │ ├── mbqc.png │ ├── noisy_mqbc.png │ ├── pattern_visualization_1.png │ ├── pattern_visualization_2.png │ ├── pattern_visualization_3.png │ ├── pauli.png │ ├── qaoa.png │ └── transpile.png ├── logo │ ├── black.png │ ├── black_with_name.png │ ├── black_with_text.png │ ├── graphix-logo.pptx │ ├── white.png │ └── white_with_text.png ├── make.bat └── source │ ├── _static │ ├── black_with_name.png │ ├── css │ │ ├── custom.css │ │ └── my_theme.css │ └── white_with_text.png │ ├── channels.rst │ ├── clifford.rst │ ├── conf.py │ ├── contributing.rst │ ├── data.rst │ ├── device_interface.rst │ ├── extraction.rst │ ├── flow.rst │ ├── generator.rst │ ├── graphsim.rst │ ├── index.rst │ ├── intro.rst │ ├── lc-mbqc.rst │ ├── modifier.rst │ ├── open_graph.rst │ ├── random_objects.rst │ ├── references.rst │ ├── simulator.rst │ ├── tutorial.rst │ └── visualization.rst ├── examples ├── deutsch_jozsa.py ├── fusion_extraction.py ├── ghz_with_tn.py ├── mbqc_vqe.py ├── qaoa.py ├── qft_with_tn.py ├── readme.rst ├── rotation.py ├── tn_simulation.py └── visualization.py ├── graphix ├── __init__.py ├── _db.py ├── channels.py ├── clifford.py ├── command.py ├── device_interface.py ├── extraction.py ├── fundamentals.py ├── generator.py ├── gflow.py ├── graphsim.py ├── instruction.py ├── linalg.py ├── linalg_validations.py ├── measurements.py ├── noise_models │ ├── __init__.py │ ├── noise_model.py │ └── noiseless_noise_model.py ├── opengraph.py ├── ops.py ├── parameter.py ├── pattern.py ├── pauli.py ├── pretty_print.py ├── pyzx.py ├── random_objects.py ├── rng.py ├── sim │ ├── __init__.py │ ├── base_backend.py │ ├── density_matrix.py │ ├── statevec.py │ └── tensornet.py ├── simulator.py ├── states.py ├── transpiler.py ├── utils.py └── visualization.py ├── noxfile.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements-doc.txt ├── requirements-extra.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_clifford.py ├── test_command.py ├── test_db.py ├── test_density_matrix.py ├── test_extraction.py ├── test_fundamentals.py ├── test_generator.py ├── test_gflow.py ├── test_graphsim.py ├── test_kraus.py ├── test_linalg.py ├── test_noisy_density_matrix.py ├── test_opengraph.py ├── test_parameter.py ├── test_pattern.py ├── test_pauli.py ├── test_pretty_print.py ├── test_pyzx.py ├── test_random_utilities.py ├── test_rng.py ├── test_runner.py ├── test_statevec.py ├── test_statevec_backend.py ├── test_tnsim.py ├── test_transpiler.py └── test_visualization.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug 4 | title: "[Bug]: " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Environment (please complete the following information):** 19 | 20 | - OS: [e.g. Windows10, macOS Monteley] 21 | - Python version: [e.g. 3.8.1] 22 | - Related module versions if applicable: [e.g. numpy=1.23.5] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: new feature 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. 11 | 12 | **Describe the feature you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | groups: 8 | python-packages: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Before submitting, please check the following: 2 | 3 | - Make sure you have tests for the new code and that test passes (run `nox`) 4 | - If applicable, add a line to the [unreleased] part of CHANGELOG.md, following [keep-a-changelog](https://keepachangelog.com/en/1.0.0/). 5 | - Format added code by `ruff` 6 | - See `CONTRIBUTING.md` for more details 7 | - Make sure the checks (github actions) pass. 8 | 9 | Then, please fill in below: 10 | 11 | **Context (if applicable):** 12 | 13 | **Description of the change:** 14 | 15 | **Related issue:** 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 20 | python: ["3.9", "3.10", "3.11", "3.12", "3.13"] 21 | 22 | name: "Python ${{ matrix.python }} / ${{ matrix.os }}" 23 | runs-on: ${{ matrix.os }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python }} 31 | 32 | - run: python -m pip install --upgrade pip 33 | 34 | - name: Setup nox 35 | run: pip install -c requirements-dev.txt nox 36 | 37 | - run: nox --python ${{ matrix.python }} 38 | -------------------------------------------------------------------------------- /.github/workflows/cov.yml: -------------------------------------------------------------------------------- 1 | name: pytest-cov 2 | 3 | # Need to include "push" 4 | on: [push, pull_request] 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | python-version: "3.13" 11 | 12 | jobs: 13 | cov: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ env.python-version }} 23 | 24 | - name: Upgrade pip 25 | run: python -m pip install --upgrade pip 26 | 27 | - name: Install graphix with dev deps. 28 | run: pip install .[dev] 29 | 30 | - name: Run pytest 31 | run: pytest --cov=./graphix --cov-report=xml --cov-report=term 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: doc 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | python-version: "3.13" 17 | 18 | jobs: 19 | check-doc: 20 | name: "Check documentation" 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ env.python-version }} 28 | 29 | - name: Upgrade pip 30 | run: python -m pip install --upgrade pip 31 | 32 | - name: Install graphix 33 | run: pip install -e ".[dev,doc]" 34 | 35 | - name: Make docs 36 | run: sphinx-build -W docs/source docs/build -j auto 37 | 38 | lint-text: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ env.python-version }} 46 | 47 | - name: Run language-agnostic linters 48 | run: | 49 | pip install pre-commit 50 | 51 | # Non-fixable 52 | pre-commit run -a check-case-conflict 53 | pre-commit run -a check-yaml 54 | 55 | # Fixable 56 | pre-commit run -a end-of-file-fixer || true 57 | pre-commit run -a fix-byte-order-marker || true 58 | pre-commit run -a mixed-line-ending || true 59 | pre-commit run -a trailing-whitespace || true 60 | 61 | - uses: actions/setup-node@v4 62 | 63 | - run: | 64 | corepack enable pnpm 65 | pnpm dlx prettier --write . 66 | 67 | - uses: reviewdog/action-suggester@v1 68 | with: 69 | tool_name: ":art: text-formatter" 70 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | python-version: "3.13" 9 | 10 | jobs: 11 | publish: 12 | name: Build and publish Python distributions to PyPI 13 | runs-on: ubuntu-latest 14 | environment: release 15 | permissions: 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ env.python-version }} 22 | - name: Install pypa/build 23 | run: | 24 | python -m pip install -U pip 25 | pip install build 26 | - name: Build a binary wheel and a source tarball 27 | run: python -m build --sdist --wheel --outdir dist/ . 28 | - name: Publish distribution 📦 to PyPI 29 | uses: pypa/gh-action-pypi-publish@release/v1 30 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: ruff 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Cache pip dependencies 21 | uses: actions/cache@v4 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} 25 | restore-keys: | 26 | ${{ runner.os }}-pip- 27 | 28 | - name: Install dependencies 29 | run: python -m pip install --upgrade pip && python -m pip install ruff -c requirements-dev.txt 30 | 31 | - name: Run Ruff Linter 32 | run: ruff check --output-format=github 33 | 34 | format: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Cache pip dependencies 40 | uses: actions/cache@v4 41 | with: 42 | path: ~/.cache/pip 43 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pip- 46 | 47 | - name: Install dependencies 48 | run: python -m pip install --upgrade pip && python -m pip install ruff -c requirements-dev.txt 49 | 50 | - name: Run Ruff Formatter Check 51 | run: ruff format --check 52 | -------------------------------------------------------------------------------- /.github/workflows/typecheck.yml: -------------------------------------------------------------------------------- 1 | name: typecheck 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | python-version: "3.13" 16 | 17 | jobs: 18 | mypy-pyright: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ env.python-version }} 27 | 28 | - run: | 29 | python -m pip install --upgrade pip 30 | pip install -e .[dev,extra] 31 | 32 | - run: mypy 33 | 34 | - run: pyright 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # Custom stuff 165 | .DS_Store 166 | docs/source/benchmarks 167 | docs/source/gallery 168 | graphix/_version.py 169 | docs/source/sg_execution_times.rst 170 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | # Non-fixable 6 | - id: check-case-conflict 7 | - id: check-yaml 8 | # Fixable 9 | - id: end-of-file-fixer 10 | - id: fix-byte-order-marker 11 | - id: mixed-line-ending 12 | - id: trailing-whitespace 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-20.04 10 | tools: 11 | python: "3.10" 12 | 13 | sphinx: 14 | configuration: docs/source/conf.py 15 | 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | - method: pip 20 | path: . 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: Sunami 5 | given-names: Shinichi 6 | orcid: https://orcid.org/0000-0002-0969-9909 7 | - family-names: Fukushima 8 | given-names: Masato 9 | title: Graphix 10 | version: v0.2.1 11 | doi: 10.5281/zenodo.7861382 12 | date-released: 2023-04-25 13 | url: https://github.com/TeamGraphix/graphix 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | thank you for your interest in `Graphix`! 4 | 5 | ## Motivation 6 | 7 | The aim of `graphix` is to make the measurement-based quantum computing (MBQC) accessible by creating a one-stop environment to study and research MBQC. 8 | 9 | ## Getting started working on your contribution for `graphix` 10 | 11 | We recommend to [fork the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) before working on new features, whether that being an existing issue or your own idea. 12 | Once created, you'll need to clone the repository, and you can follow below to set up the environment. You may want to set up virtual environment, such as `conda env` or `pipenv` before setting up the environment. 13 | 14 | ```bash 15 | git clone git@github.com:/graphix.git 16 | cd graphix 17 | pip install -e .[dev] 18 | ``` 19 | 20 | You may want to install additional packages. 21 | Specifically, `matplotlib` is necessary to run codes in the `example` folder. 22 | 23 | ```bash 24 | pip install matplotlib 25 | ``` 26 | 27 | For other depencencies for the docs build, see `docs/requirements.txt`. 28 | 29 | Before comitting the code, make sure to format with `ruff`. 30 | To format a python file, just run in the top level of the repository: 31 | 32 | ```bash 33 | # Be sure to use the latest version of ruff 34 | pip install -U ruff 35 | # Sort imports and format 36 | ruff check --select I --fix . 37 | ruff format . 38 | ``` 39 | 40 | ### Local checks 41 | 42 | To replicate the CI pipeline locally, install `nox` and run the tests: 43 | 44 | ```bash 45 | pip install -c requirements-dev.txt nox 46 | nox 47 | ``` 48 | 49 | With the development dependencies installed, run the test suite explicitly: 50 | 51 | ```bash 52 | pip install .[dev] 53 | pytest --cov=./graphix --cov-report=xml --cov-report=term 54 | ``` 55 | 56 | ### VS Code configuration 57 | 58 | If you use VS Code for development, add a ``.vscode/settings.json`` file to 59 | enable the linter and basic type checking on save: 60 | 61 | ```json 62 | { 63 | "python.formatting.provider": "ruff", 64 | "editor.codeActionsOnSave": { 65 | "source.organizeImports": true, 66 | "source.fixAll": true 67 | }, 68 | "python.analysis.typeCheckingMode": "basic" 69 | } 70 | ``` 71 | 72 | and you are ready to commit the changes. 73 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude .* 2 | exclude noxfile.py CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md CITATION.cff 3 | prune benchmarks 4 | prune docs 5 | prune tests 6 | prune examples 7 | prune .github 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/graphix) 4 | ![License](https://img.shields.io/github/license/TeamGraphix/graphix) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/graphix) 6 | [![Downloads](https://static.pepy.tech/badge/graphix)](https://pepy.tech/project/graphix) 7 | [![Unitary Fund](https://img.shields.io/badge/Supported%20By-UNITARY%20FUND-brightgreen.svg)](https://unitary.fund/) 8 | [![DOI](https://zenodo.org/badge/573466585.svg)](https://zenodo.org/badge/latestdoi/573466585) 9 | [![CI](https://github.com/TeamGraphix/graphix/actions/workflows/ci.yml/badge.svg)](https://github.com/TeamGraphix/graphix/actions/workflows/ci.yml) 10 | [![codecov](https://codecov.io/gh/TeamGraphix/graphix/graph/badge.svg?token=E41MLUTYXU)](https://codecov.io/gh/TeamGraphix/graphix) 11 | [![Documentation Status](https://readthedocs.org/projects/graphix/badge/?version=latest)](https://graphix.readthedocs.io/en/latest/?badge=latest) 12 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 13 | 14 | **Graphix** is a measurement-based quantum computing (MBQC) software package, featuring 15 | 16 | - the measurement calculus framework with integrated graphical rewrite rules for Pauli measurement preprocessing 17 | - circuit-to-pattern transpiler, graph-based deterministic pattern generator and manual pattern generation 18 | - flow, gflow and pauliflow finding tools and graph visualization based on flows (see below) 19 | - statevector, density matrix and tensornetwork pattern simulation backends 20 | - QPU interface and fusion network extraction tool 21 | 22 | ## Installation 23 | 24 | Install `graphix` with `pip`: 25 | 26 | ```bash 27 | pip install graphix 28 | ``` 29 | 30 | Install together with device interface: 31 | 32 | ```bash 33 | pip install graphix[extra] 34 | ``` 35 | 36 | this will install `graphix` and interface for [IBMQ](https://github.com/TeamGraphix/graphix-ibmq) and [Perceval](https://github.com/TeamGraphix/graphix-perceval) to run MBQC patterns on superconducting and optical QPUs and their simulators. 37 | 38 | ## Using graphix 39 | 40 | ### generating pattern from a circuit 41 | 42 | ```python 43 | from graphix import Circuit 44 | 45 | circuit = Circuit(4) 46 | circuit.h(0) 47 | ... 48 | pattern = circuit.transpile().pattern 49 | pattern.draw_graph() 50 | ``` 51 | 52 | graph_flow 53 | 54 | note: this graph is generated from QAOA circuit, see [our example code](examples/qaoa.py). Arrows indicate the [_causal flow_](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.74.052310) of MBQC and dashed lines are the other edges of the graph. the vertical dashed partitions and the labels 'l:n' below indicate the execution _layers_ or the order in the graph (measurements should happen from left to right, and nodes in the same layer can be measured simultaneously), based on the partial order associated with the (maximally-delayed) flow. 55 | 56 | ### preprocessing Pauli measurements 57 | 58 | ```python 59 | pattern.perform_pauli_measurements() 60 | pattern.draw_graph() 61 | ``` 62 | 63 | graph_gflow 64 | 65 | (here, the graph is visualized based on [_generalized flow_](https://iopscience.iop.org/article/10.1088/1367-2630/9/8/250).) 66 | 67 | ### simulating the pattern 68 | 69 | ```python 70 | state_out = pattern.simulate_pattern(backend="statevector") 71 | ``` 72 | 73 | ### and more.. 74 | 75 | - See [demos](https://graphix.readthedocs.io/en/latest/gallery/index.html) showing other features of `graphix`. 76 | 77 | - Read the [tutorial](https://graphix.readthedocs.io/en/latest/tutorial.html) for more usage guides. 78 | 79 | - For theoretical background, read our quick introduction into [MBQC](https://graphix.readthedocs.io/en/latest/intro.html) and [LC-MBQC](https://graphix.readthedocs.io/en/latest/lc-mbqc.html). 80 | 81 | - Full API docs is [here](https://graphix.readthedocs.io/en/latest/references.html). 82 | 83 | ## Citing 84 | 85 | > Shinichi Sunami and Masato Fukushima, Graphix. (2023) 86 | 87 | ## Contributing 88 | 89 | We use [GitHub issues](https://github.com/TeamGraphix/graphix/issues) for tracking feature requests and bug reports. 90 | 91 | ## Discord Server 92 | 93 | Please visit [Unitary Fund's Discord server](https://discord.com/servers/unitary-fund-764231928676089909), where you can find a channel for `graphix` to ask questions. 94 | 95 | ## Core Contributors (alphabetical order) 96 | 97 | - Masato Fukushima (University of Tokyo, Fixstars Amplify) 98 | - Maxime Garnier (Inria Paris) 99 | - Thierry Martinez (Inria Paris) 100 | - Sora Shiratani (University of Tokyo, Fixstars Amplify) 101 | - Shinichi Sunami (University of Oxford) 102 | 103 | ## Acknowledgements 104 | 105 | We are proud to be supported by [unitary fund microgrant program](https://unitary.fund/grants.html). 106 | 107 |

108 | unitary-fund 109 |

110 | 111 | Special thanks to Fixstars Amplify: 112 | 113 |

114 | amplify 115 |

116 | 117 | ## License 118 | 119 | [Apache License 2.0](LICENSE) 120 | -------------------------------------------------------------------------------- /benchmarks/readme.rst: -------------------------------------------------------------------------------- 1 | Graphix benchmarks 2 | ================== 3 | 4 | Here are some benchmark of Graphix and related packages. 5 | -------------------------------------------------------------------------------- /benchmarks/statevec.py: -------------------------------------------------------------------------------- 1 | """ 2 | Statevector simulation of MBQC patterns 3 | ======================================= 4 | 5 | Here we benchmark our statevector simulator for MBQC. 6 | 7 | The methods and modules we use are the followings: 8 | 1. :meth:`graphix.pattern.Pattern.simulate_pattern` 9 | Pattern simulator with statevector backend. 10 | 2. :mod:`paddle_quantum.mbqc` 11 | Pattern simulation using :mod:`paddle_quantum.mbqc`. 12 | """ 13 | 14 | # %% 15 | # Firstly, let us import relevant modules: 16 | from __future__ import annotations 17 | 18 | from time import perf_counter 19 | 20 | import matplotlib.pyplot as plt 21 | import numpy as np 22 | from paddle import to_tensor 23 | from paddle_quantum.mbqc.qobject import Circuit as PaddleCircuit 24 | from paddle_quantum.mbqc.simulator import MBQC as PaddleMBQC # noqa: N811 25 | from paddle_quantum.mbqc.transpiler import transpile as paddle_transpile 26 | 27 | from graphix import Circuit 28 | 29 | rng = np.random.default_rng() 30 | 31 | # %% 32 | # Next, define a circuit to be transpiled into measurement pattern: 33 | 34 | 35 | def simple_random_circuit(nqubit, depth): 36 | r"""Generate a test circuit for benchmarking. 37 | 38 | This function generates a circuit with nqubit qubits and depth layers, 39 | having layers of CNOT and Rz gates with random placements. 40 | 41 | Parameters 42 | ---------- 43 | nqubit : int 44 | number of qubits 45 | depth : int 46 | number of layers 47 | 48 | Returns 49 | ------- 50 | circuit : graphix.transpiler.Circuit object 51 | generated circuit 52 | """ 53 | qubit_index = list(range(nqubit)) 54 | circuit = Circuit(nqubit) 55 | for _ in range(depth): 56 | rng.shuffle(qubit_index) 57 | for j in range(len(qubit_index) // 2): 58 | circuit.cnot(qubit_index[2 * j], qubit_index[2 * j + 1]) 59 | for j in range(len(qubit_index)): 60 | circuit.rz(qubit_index[j], 2 * np.pi * rng.random()) 61 | return circuit 62 | 63 | 64 | # %% 65 | # We define the test cases: shallow (depth=1) random circuits, only changing the number of qubits. 66 | 67 | DEPTH = 1 68 | test_cases = list(range(2, 22)) 69 | graphix_circuits = {} 70 | 71 | pattern_time = [] 72 | circuit_time = [] 73 | 74 | # %% 75 | # We then run simulations. 76 | # First, we run the pattern simulation using `graphix`. 77 | # For reference, we perform simple statevector simulation of the original gate network. 78 | # Since transpilation into MBQC involves a significant increase in qubit number, 79 | # the MBQC simulation is inherently slower as we will see. 80 | 81 | for width in test_cases: 82 | circuit = simple_random_circuit(width, DEPTH) 83 | graphix_circuits[width] = circuit 84 | pattern = circuit.transpile() 85 | pattern.standardize() 86 | pattern.minimize_space() 87 | nodes, edges = pattern.get_graph() 88 | nqubit = len(nodes) 89 | start = perf_counter() 90 | pattern.simulate_pattern(max_qubit_num=30) 91 | end = perf_counter() 92 | print(f"width: {width}, nqubit: {nqubit}, depth: {DEPTH}, time: {end - start}") 93 | pattern_time.append(end - start) 94 | start = perf_counter() 95 | circuit.simulate_statevector() 96 | end = perf_counter() 97 | circuit_time.append(end - start) 98 | 99 | 100 | # %% 101 | # Here we benchmark `paddle_quantum`, using the same original gate network and use `paddle_quantum.mbqc` module 102 | # to transpile into a measurement pattern. 103 | 104 | 105 | def translate_graphix_rc_into_paddle_quantum_circuit(graphix_circuit: Circuit) -> PaddleCircuit: 106 | """Translate graphix circuit into paddle_quantum circuit. 107 | 108 | Parameters 109 | ---------- 110 | graphix_circuit : Circuit 111 | graphix circuit 112 | 113 | Returns 114 | ------- 115 | paddle_quantum_circuit : PaddleCircuit 116 | paddle_quantum circuit 117 | """ 118 | paddle_quantum_circuit = PaddleCircuit(graphix_circuit.width) 119 | for instr in graphix_circuit.instruction: 120 | if instr.name == "CNOT": 121 | paddle_quantum_circuit.cnot(which_qubits=instr[1]) 122 | elif instr.name == "RZ": 123 | paddle_quantum_circuit.rz(which_qubit=instr[1], theta=to_tensor(instr[2], dtype="float64")) 124 | return paddle_quantum_circuit 125 | 126 | 127 | test_cases_for_paddle_quantum = list(range(2, 22)) 128 | paddle_quantum_time = [] 129 | 130 | for width in test_cases_for_paddle_quantum: 131 | graphix_circuit = graphix_circuits[width] 132 | paddle_quantum_circuit = translate_graphix_rc_into_paddle_quantum_circuit(graphix_circuit) 133 | pat = paddle_transpile(paddle_quantum_circuit) 134 | mbqc = PaddleMBQC() 135 | mbqc.set_pattern(pat) 136 | start = perf_counter() 137 | mbqc.run_pattern() 138 | end = perf_counter() 139 | paddle_quantum_time.append(end - start) 140 | 141 | print(f"width: {width}, depth: {DEPTH}, time: {end - start}") 142 | 143 | # %% 144 | # Lastly, we compare the simulation times. 145 | 146 | fig = plt.figure() 147 | ax = fig.add_subplot(111) 148 | 149 | ax.scatter( 150 | test_cases, circuit_time, label="direct statevector sim of original gate-based circuit (reference)", marker="x" 151 | ) 152 | ax.scatter(test_cases, pattern_time, label="graphix pattern simulator") 153 | ax.scatter(test_cases_for_paddle_quantum, paddle_quantum_time, label="paddle_quantum pattern simulator") 154 | ax.set( 155 | xlabel="Width of the original circuit", 156 | ylabel="time (s)", 157 | yscale="log", 158 | title="Time to simulate random circuits", 159 | ) 160 | fig.legend(bbox_to_anchor=(0.85, 0.9)) 161 | fig.show() 162 | 163 | # %% 164 | # MBQC simulation is a lot slower than the simulation of original gate network, since the number of qubit involved 165 | # is significantly larger. 166 | 167 | import importlib.metadata # noqa: E402 168 | 169 | # print package versions. 170 | [print(f"{pkg} - {importlib.metadata.version(pkg)}") for pkg in ["numpy", "graphix", "paddlepaddle", "paddle-quantum"]] 171 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Clean target to remove build directory 18 | clean: 19 | @rm -rf $(BUILDDIR)/* 20 | 21 | # Catch-all target: route all unknown targets to Sphinx using the new 22 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /docs/imgs/circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/circuit.png -------------------------------------------------------------------------------- /docs/imgs/classical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/classical.png -------------------------------------------------------------------------------- /docs/imgs/fam_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/fam_logo.png -------------------------------------------------------------------------------- /docs/imgs/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph.png -------------------------------------------------------------------------------- /docs/imgs/graph2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph2.png -------------------------------------------------------------------------------- /docs/imgs/graphH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graphH.png -------------------------------------------------------------------------------- /docs/imgs/graph_cnot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph_cnot.png -------------------------------------------------------------------------------- /docs/imgs/graph_rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph_rot.png -------------------------------------------------------------------------------- /docs/imgs/graph_space1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph_space1.png -------------------------------------------------------------------------------- /docs/imgs/graph_space2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graph_space2.png -------------------------------------------------------------------------------- /docs/imgs/graphsim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graphsim.png -------------------------------------------------------------------------------- /docs/imgs/graphsim2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/graphsim2.png -------------------------------------------------------------------------------- /docs/imgs/mbqc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/mbqc.png -------------------------------------------------------------------------------- /docs/imgs/noisy_mqbc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/noisy_mqbc.png -------------------------------------------------------------------------------- /docs/imgs/pattern_visualization_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/pattern_visualization_1.png -------------------------------------------------------------------------------- /docs/imgs/pattern_visualization_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/pattern_visualization_2.png -------------------------------------------------------------------------------- /docs/imgs/pattern_visualization_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/pattern_visualization_3.png -------------------------------------------------------------------------------- /docs/imgs/pauli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/pauli.png -------------------------------------------------------------------------------- /docs/imgs/qaoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/qaoa.png -------------------------------------------------------------------------------- /docs/imgs/transpile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/imgs/transpile.png -------------------------------------------------------------------------------- /docs/logo/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/black.png -------------------------------------------------------------------------------- /docs/logo/black_with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/black_with_name.png -------------------------------------------------------------------------------- /docs/logo/black_with_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/black_with_text.png -------------------------------------------------------------------------------- /docs/logo/graphix-logo.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/graphix-logo.pptx -------------------------------------------------------------------------------- /docs/logo/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/white.png -------------------------------------------------------------------------------- /docs/logo/white_with_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/logo/white_with_text.png -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "clean" ( 27 | echo Cleaning build directory... 28 | rmdir /s /q %BUILDDIR% 29 | exit /b 0 30 | ) 31 | 32 | if "%1" == "" goto help 33 | 34 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 35 | goto end 36 | 37 | :help 38 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 39 | 40 | :end 41 | popd 42 | -------------------------------------------------------------------------------- /docs/source/_static/black_with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/source/_static/black_with_name.png -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url("sg_gallery.css"); 2 | 3 | .sphx-glr-thumbnails { 4 | width: 100%; 5 | margin: 0px 0px 20px 0px; 6 | justify-content: space-around; 7 | display: grid; 8 | grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)) !important; 9 | gap: 15px; 10 | padding: 10px; 11 | } 12 | 13 | .sphx-glr-thumbcontainer { 14 | background: transparent; 15 | border-radius: 5px; 16 | box-shadow: 0 0 10px var(--sg-thumb-box-shadow-color); 17 | position: relative; 18 | box-sizing: border-box; 19 | width: 100%; 20 | padding: 10px; 21 | border: 1px solid transparent; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | gap: 7px; 26 | min-width: 330px; 27 | min-height: 270px; 28 | max-width: 340px; 29 | max-height: 300px; 30 | } 31 | -------------------------------------------------------------------------------- /docs/source/_static/css/my_theme.css: -------------------------------------------------------------------------------- 1 | @import url("theme.css"); 2 | 3 | .wy-nav-content { 4 | max-width: 100%; 5 | } 6 | 7 | .rst-content img:not([alt="https://mybinder.org/badge_logo.svg"]):not([alt="qft"]):not([alt="resource state"]):not([alt="resource state to perform computation"]):not([alt="simulation order"]):not([alt="simulation order after optimization"]):not([alt="translating from a circuit to a graph."]):not([alt="quantum and classical processing"]):not([alt="Pauli measurement of graph"]):not([alt="equivalent graphs"]):not([alt="gate-based and one-way qc"]) 8 | { 9 | max-width: 100%; 10 | min-width: 220px; 11 | min-height: 240px; 12 | } 13 | 14 | .rst-content img[alt="qft"] { 15 | max-width: 100%; 16 | min-width: 220px; 17 | min-height: 150px; 18 | } 19 | -------------------------------------------------------------------------------- /docs/source/_static/white_with_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/docs/source/_static/white_with_text.png -------------------------------------------------------------------------------- /docs/source/channels.rst: -------------------------------------------------------------------------------- 1 | 2 | Quantum channels and noise models 3 | +++++++++++++++++++++++++++++++++ 4 | 5 | Kraus channel 6 | ------------- 7 | 8 | .. currentmodule:: graphix.channels 9 | 10 | .. autoclass:: KrausChannel 11 | :members: 12 | 13 | .. autofunction:: dephasing_channel 14 | 15 | .. autofunction:: depolarising_channel 16 | 17 | .. autofunction:: pauli_channel 18 | 19 | .. autofunction:: two_qubit_depolarising_channel 20 | 21 | .. autofunction:: two_qubit_depolarising_tensor_channel 22 | 23 | 24 | Noise model classes 25 | ------------------- 26 | 27 | 28 | .. currentmodule:: graphix.noise_models.noise_model 29 | 30 | .. autoclass:: NoiseModel 31 | :members: 32 | 33 | .. currentmodule:: graphix.noise_models.noiseless_noise_model 34 | 35 | .. autoclass:: NoiselessNoiseModel 36 | :members: 37 | -------------------------------------------------------------------------------- /docs/source/clifford.rst: -------------------------------------------------------------------------------- 1 | 2 | Miscellaneous modules 3 | ===================== 4 | 5 | :mod:`graphix.clifford` module 6 | ++++++++++++++++++++++++++++++ 7 | 8 | .. automodule:: graphix.clifford 9 | 10 | .. currentmodule:: graphix.clifford 11 | 12 | .. autoclass:: graphix.clifford.Clifford 13 | 14 | .. data:: graphix.cliffford.CLIFFORD 15 | 16 | list of 24 unique single-qubit Clifford operators as numpy array. 17 | 18 | .. data:: graphix.clifford.CLIFFORD_MUL 19 | 20 | the matrix multiplication of single-qubit Clifford gates, expressed as a mapping of CLIFFORD indices. This is possible because multiplication of two Clifford gates result in a Clifford gate. 21 | 22 | .. data:: graphix.clifford.CLIFFORD_MEASURE 23 | 24 | The mapping of Pauli operators under conjugation by single-qubit Clifford gates, expressed as a mapping into Pauli operator indices (in CLIFFORD) and sign. 25 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | from __future__ import annotations 6 | from typing import Any, Literal 7 | 8 | from sphinx.application import Sphinx 9 | 10 | import os 11 | import sys 12 | 13 | # -- Project information ----------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 15 | 16 | project = "graphix" 17 | copyright = "2022, Team Graphix" 18 | author = "Shinichi Sunami" 19 | 20 | # -- General configuration --------------------------------------------------- 21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 22 | 23 | extensions = [ 24 | "sphinx.ext.intersphinx", 25 | "sphinx.ext.autodoc", 26 | "sphinx.ext.viewcode", 27 | "sphinx.ext.autosummary", 28 | "sphinx.ext.autosectionlabel", 29 | "sphinx.ext.napoleon", 30 | "sphinx_gallery.gen_gallery", 31 | ] 32 | 33 | templates_path = ["_templates"] 34 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 35 | autosectionlabel_prefix_document = True 36 | 37 | intersphinx_mapping = { 38 | "networkx": ("https://networkx.github.io/documentation/stable/", None), 39 | } 40 | 41 | sys.path.insert(0, os.path.abspath("../../")) 42 | 43 | 44 | def skip( 45 | app: Sphinx, 46 | what: Literal["module", "class", "exception", "function", "method", "attribute"], 47 | name: str, 48 | obj: Any, 49 | would_skip: bool, 50 | options: dict[str, bool], 51 | ) -> bool: 52 | if name == "__init__": 53 | return False 54 | return would_skip 55 | 56 | 57 | def setup(app: Sphinx) -> None: 58 | app.connect("autodoc-skip-member", skip) 59 | 60 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 63 | 64 | html_theme = "furo" 65 | 66 | html_title = " " # title for documentation (shown in sidebar, kept empty) 67 | 68 | html_static_path = ["_static"] 69 | 70 | html_context = { 71 | "mode": "production", 72 | } 73 | 74 | # code highlighting for light and dark themes 75 | pygments_style = "sphinx" 76 | pygments_dark_style = "monokai" 77 | 78 | # customizing theme options 79 | html_theme_options = { 80 | "light_logo": "black_with_name.png", 81 | "dark_logo": "white_with_text.png", 82 | } 83 | 84 | default_role = "any" 85 | 86 | sphinx_gallery_conf = { 87 | # path to your example scripts 88 | "examples_dirs": ["../../examples"], 89 | # path to where to save gallery generated output 90 | "gallery_dirs": ["gallery"], 91 | "filename_pattern": "/", 92 | "thumbnail_size": (800, 550), 93 | "parallel": True, 94 | } 95 | 96 | suppress_warnings = ["config.cache"] 97 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing to Graphix 2 | ======================= 3 | 4 | This page summarises useful tips for developing the project locally. 5 | 6 | Local checks 7 | ------------ 8 | 9 | * Run ``nox -s tests`` to execute the test suite. This mirrors what the 10 | continuous integration service runs. 11 | * Format the code with :command:`ruff` before committing:: 12 | 13 | ruff check --select I --fix . 14 | ruff format . 15 | 16 | Additional commands from the CI configuration are useful for replicating 17 | the testing environment locally:: 18 | 19 | pip install -c requirements-dev.txt nox 20 | nox --python 3.12 21 | 22 | pip install .[dev] 23 | pytest --cov=./graphix --cov-report=xml --cov-report=term 24 | 25 | VS Code configuration 26 | --------------------- 27 | 28 | Using `VS Code `_ helps catch issues early. A 29 | minimal ``.vscode/settings.json`` may look like:: 30 | 31 | { 32 | "python.formatting.provider": "ruff", 33 | "editor.codeActionsOnSave": { 34 | "source.organizeImports": true, 35 | "source.fixAll": true 36 | }, 37 | "python.analysis.typeCheckingMode": "basic" 38 | } 39 | 40 | These settings enable the linter, format the code on save and turn on basic 41 | static type checking through the Pylance extension. 42 | -------------------------------------------------------------------------------- /docs/source/data.rst: -------------------------------------------------------------------------------- 1 | Pattern data structure 2 | ====================== 3 | 4 | :mod:`graphix.command` module 5 | +++++++++++++++++++++++++++++ 6 | 7 | This module defines standard data structure for pattern commands. 8 | 9 | .. automodule:: graphix.command 10 | 11 | .. currentmodule:: graphix.command 12 | 13 | .. autoclass:: CommandKind 14 | 15 | .. autoclass:: N 16 | 17 | .. autoclass:: M 18 | 19 | .. autoclass:: E 20 | 21 | .. autoclass:: C 22 | 23 | .. autoclass:: X 24 | 25 | .. autoclass:: Z 26 | 27 | .. autoclass:: MeasureUpdate 28 | 29 | 30 | :mod:`graphix.fundamentals` module 31 | ++++++++++++++++++++++++++++++++++ 32 | 33 | This module defines standard data structure for Pauli operators. 34 | 35 | .. automodule:: graphix.fundamentals 36 | 37 | .. currentmodule:: graphix.fundamentals 38 | 39 | .. autoclass:: Axis 40 | :members: 41 | 42 | .. autoclass:: ComplexUnit 43 | :members: 44 | 45 | .. autoclass:: Sign 46 | :members: 47 | 48 | .. autoclass:: IXYZ 49 | :members: 50 | 51 | .. autoclass:: Plane 52 | :members: 53 | 54 | :mod:`graphix.pauli` module 55 | +++++++++++++++++++++++++++ 56 | 57 | This module defines standard data structure for Pauli operators. 58 | 59 | .. automodule:: graphix.pauli 60 | 61 | .. currentmodule:: graphix.pauli 62 | 63 | .. autoclass:: Pauli 64 | 65 | :mod:`graphix.instruction` module 66 | +++++++++++++++++++++++++++++++++ 67 | 68 | This module defines standard data structure for gate seqence (circuit model) used for :class:`graphix.transpiler.Circuit`. 69 | 70 | .. automodule:: graphix.instruction 71 | 72 | .. currentmodule:: graphix.instruction 73 | 74 | .. autoclass:: InstructionKind 75 | 76 | .. autoclass:: RX 77 | 78 | .. autoclass:: RZ 79 | 80 | .. autoclass:: RY 81 | 82 | .. autoclass:: M 83 | 84 | .. autoclass:: X 85 | 86 | .. autoclass:: Y 87 | 88 | .. autoclass:: Z 89 | 90 | .. autoclass:: S 91 | 92 | .. autoclass:: H 93 | 94 | .. autoclass:: SWAP 95 | 96 | .. autoclass:: CNOT 97 | 98 | :mod:`graphix.parameter` module 99 | +++++++++++++++++++++++++++++++ 100 | 101 | This module defines parameter objects and parameterized expressions. 102 | Parameterized expressions can appear in measurement angles in patterns 103 | and rotation angles in circuits, and they can be substituted with 104 | actual values. 105 | 106 | The module provides generic interfaces for parameters and expressions, 107 | as well as a simple :class:`Placeholder` class that can be used in 108 | affine expressions (:class:`AffineExpression`). Affine expressions are 109 | sufficient for transpiling and pattern optimizations (such as 110 | standardization, minimization, signal shifting, and Pauli 111 | preprocessing), but they do not support simulation. 112 | 113 | Parameter objects that support symbolic simulation with *sympy* are 114 | available in a separate package: 115 | https://github.com/TeamGraphix/graphix-symbolic. 116 | 117 | .. currentmodule:: graphix.parameter 118 | 119 | .. autoclass:: Expression 120 | 121 | .. autoclass:: Parameter 122 | 123 | .. autoclass:: AffineExpression 124 | 125 | .. autoclass:: Placeholder 126 | 127 | :mod:`graphix.states` module 128 | ++++++++++++++++++++++++++++ 129 | 130 | .. automodule:: graphix.states 131 | 132 | .. currentmodule:: graphix.states 133 | 134 | .. autoclass:: State 135 | -------------------------------------------------------------------------------- /docs/source/device_interface.rst: -------------------------------------------------------------------------------- 1 | Device Interface 2 | ================== 3 | 4 | :mod:`graphix.device_interface` module 5 | ++++++++++++++++++++++++++++++++++++++ 6 | 7 | .. automodule:: graphix.device_interface 8 | 9 | .. currentmodule:: graphix.device_interface 10 | 11 | .. autoclass:: PatternRunner 12 | 13 | .. automethod:: __init__ 14 | 15 | .. automethod:: run 16 | 17 | .. automethod:: retrieve_result 18 | -------------------------------------------------------------------------------- /docs/source/extraction.rst: -------------------------------------------------------------------------------- 1 | Graph state generation 2 | ====================== 3 | 4 | :mod:`graphix.extraction` module 5 | ++++++++++++++++++++++++++++++++ 6 | 7 | This module provides functions to extract clusters from a given graph state. 8 | 9 | .. automodule:: graphix.extraction 10 | 11 | .. currentmodule:: graphix.extraction 12 | 13 | .. autoclass:: ResourceGraph 14 | :members: 15 | 16 | .. autoclass:: ResourceType 17 | :members: 18 | 19 | .. autofunction:: get_fusion_network_from_graph 20 | 21 | .. autofunction:: create_resource_graph 22 | 23 | .. autofunction:: get_fusion_nodes 24 | -------------------------------------------------------------------------------- /docs/source/flow.rst: -------------------------------------------------------------------------------- 1 | flow and gflow 2 | ============== 3 | 4 | :mod:`graphix.gflow` module 5 | +++++++++++++++++++++++++++++++++++ 6 | 7 | This provides functions to find flow structures (causal flow, gflow, Pauli flow) in a graph, 8 | to verify if a given flow structure is valid, and to extract flow structures from a given pattern. 9 | 10 | .. automodule:: graphix.gflow 11 | 12 | .. currentmodule:: graphix.gflow 13 | 14 | .. autofunction:: find_flow 15 | 16 | .. autofunction:: find_gflow 17 | 18 | .. autofunction:: find_pauliflow 19 | 20 | .. autofunction:: verify_flow 21 | 22 | .. autofunction:: verify_gflow 23 | 24 | .. autofunction:: verify_pauliflow 25 | 26 | .. autofunction:: flow_from_pattern 27 | 28 | .. autofunction:: gflow_from_pattern 29 | 30 | .. autofunction:: pauliflow_from_pattern 31 | -------------------------------------------------------------------------------- /docs/source/generator.rst: -------------------------------------------------------------------------------- 1 | Pattern Generation 2 | ================== 3 | 4 | :mod:`graphix.transpiler` module 5 | ++++++++++++++++++++++++++++++++ 6 | 7 | .. automodule:: graphix.transpiler 8 | 9 | .. currentmodule:: graphix.transpiler 10 | 11 | .. autoclass:: Circuit 12 | 13 | .. automethod:: __init__ 14 | 15 | .. automethod:: transpile 16 | 17 | .. automethod:: simulate_statevector 18 | 19 | .. automethod:: cnot 20 | 21 | .. automethod:: h 22 | 23 | .. automethod:: s 24 | 25 | .. automethod:: x 26 | 27 | .. automethod:: y 28 | 29 | .. automethod:: z 30 | 31 | .. automethod:: rx 32 | 33 | .. automethod:: ry 34 | 35 | .. automethod:: rz 36 | 37 | .. automethod:: ccx 38 | 39 | .. automethod:: m 40 | 41 | .. autoclass:: TranspileResult 42 | 43 | .. autoclass:: SimulateResult 44 | 45 | :mod:`graphix.generator` module 46 | +++++++++++++++++++++++++++++++ 47 | 48 | .. automodule:: graphix.generator 49 | 50 | .. currentmodule:: graphix.generator 51 | 52 | .. autofunction:: graphix.generator.generate_from_graph 53 | -------------------------------------------------------------------------------- /docs/source/graphsim.rst: -------------------------------------------------------------------------------- 1 | Graph state simulator 2 | ===================== 3 | 4 | :mod:`graphix.graphsim` module 5 | +++++++++++++++++++++++++++++++++++ 6 | 7 | This provides an efficient graph state simulator using the decorated graph method. 8 | 9 | .. automodule:: graphix.graphsim 10 | 11 | .. currentmodule:: graphix.graphsim 12 | 13 | .. autoclass:: GraphState 14 | :members: 15 | :undoc-members: 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. graphix documentation master file, created by 2 | sphinx-quickstart on Sun Nov 20 00:43:54 2022. 3 | 4 | Graphix - optimizing and simulating MBQC 5 | ======================================== 6 | 7 | **Graphix** is an open-source library to generate, optimize and simulate the measurement-based quantum computing (MBQC) command sequence. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :caption: Documentation 12 | 13 | tutorial 14 | gallery/index 15 | references 16 | contributing 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | :caption: Introduction to LC-MBQC 21 | 22 | intro 23 | lc-mbqc 24 | -------------------------------------------------------------------------------- /docs/source/modifier.rst: -------------------------------------------------------------------------------- 1 | Pattern Manipulation 2 | ==================== 3 | 4 | :mod:`graphix.pattern` module 5 | +++++++++++++++++++++++++++++++++++ 6 | 7 | .. currentmodule:: graphix.pattern 8 | 9 | .. autoclass:: Pattern 10 | 11 | .. automethod:: __init__ 12 | 13 | .. automethod:: add 14 | 15 | .. automethod:: extend 16 | 17 | .. automethod:: clear 18 | 19 | .. automethod:: replace 20 | 21 | .. automethod:: reorder_output_nodes 22 | 23 | .. automethod:: reorder_input_nodes 24 | 25 | .. automethod:: simulate_pattern 26 | 27 | .. automethod:: get_max_degree 28 | 29 | .. automethod:: get_angles 30 | 31 | .. automethod:: get_vops 32 | 33 | .. automethod:: connected_nodes 34 | 35 | .. automethod:: run_pattern 36 | 37 | .. automethod:: perform_pauli_measurements 38 | 39 | .. automethod:: to_ascii 40 | 41 | .. automethod:: to_unicode 42 | 43 | .. automethod:: to_latex 44 | 45 | .. automethod:: standardize 46 | 47 | .. automethod:: shift_signals 48 | 49 | .. automethod:: is_standard 50 | 51 | .. automethod:: get_graph 52 | 53 | .. automethod:: parallelize_pattern 54 | 55 | .. automethod:: minimize_space 56 | 57 | .. automethod:: draw_graph 58 | 59 | .. automethod:: max_space 60 | 61 | .. automethod:: get_layers 62 | 63 | .. automethod:: to_qasm3 64 | 65 | 66 | .. autofunction:: measure_pauli 67 | -------------------------------------------------------------------------------- /docs/source/open_graph.rst: -------------------------------------------------------------------------------- 1 | Open Graph 2 | ====================== 3 | 4 | :mod:`graphix.opengraph` module 5 | +++++++++++++++++++++++++++++++ 6 | 7 | This module defines classes for defining MBQC patterns as Open Graphs. 8 | 9 | .. currentmodule:: graphix.opengraph 10 | 11 | .. autoclass:: OpenGraph 12 | 13 | .. autoclass:: Measurement 14 | -------------------------------------------------------------------------------- /docs/source/random_objects.rst: -------------------------------------------------------------------------------- 1 | Random objects 2 | ============== 3 | 4 | :mod:`graphix.random_objects` module 5 | ++++++++++++++++++++++++++++++++++++ 6 | 7 | This module provides functions to generate various random objects. 8 | 9 | .. currentmodule:: graphix.random_objects 10 | 11 | .. autofunction:: rand_herm 12 | 13 | .. autofunction:: rand_unit 14 | 15 | .. autofunction:: rand_dm 16 | 17 | .. autofunction:: rand_gauss_cpx_mat 18 | 19 | .. autofunction:: rand_channel_kraus 20 | 21 | .. autofunction:: rand_pauli_channel_kraus 22 | 23 | .. autofunction:: rand_gate 24 | 25 | .. autofunction:: rand_circuit 26 | -------------------------------------------------------------------------------- /docs/source/references.rst: -------------------------------------------------------------------------------- 1 | Module reference 2 | ================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | generator 8 | data 9 | modifier 10 | simulator 11 | graphsim 12 | extraction 13 | flow 14 | clifford 15 | device_interface 16 | visualization 17 | channels 18 | random_objects 19 | open_graph 20 | -------------------------------------------------------------------------------- /docs/source/simulator.rst: -------------------------------------------------------------------------------- 1 | Pattern Simulation 2 | ================== 3 | 4 | :mod:`graphix.simulator` module 5 | +++++++++++++++++++++++++++++++++++ 6 | 7 | .. currentmodule:: graphix.simulator 8 | 9 | .. autoclass:: PatternSimulator 10 | 11 | .. automethod:: __init__ 12 | 13 | .. automethod:: run 14 | 15 | 16 | Simulator backends 17 | ++++++++++++++++++ 18 | 19 | Tensor Network 20 | -------------------------- 21 | 22 | .. currentmodule:: graphix.sim.tensornet 23 | 24 | .. autoclass:: TensorNetworkBackend 25 | :members: 26 | 27 | .. autofunction:: gen_str 28 | 29 | .. autofunction:: outer_product 30 | 31 | Statevector 32 | ----------- 33 | 34 | .. currentmodule:: graphix.sim.statevec 35 | 36 | .. autoclass:: StatevectorBackend 37 | :members: 38 | 39 | .. autoclass:: Statevec 40 | :members: 41 | 42 | Density Matrix 43 | -------------- 44 | 45 | .. currentmodule:: graphix.sim.density_matrix 46 | 47 | .. autoclass:: DensityMatrixBackend 48 | :members: 49 | 50 | .. autoclass:: DensityMatrix 51 | :members: 52 | -------------------------------------------------------------------------------- /docs/source/visualization.rst: -------------------------------------------------------------------------------- 1 | Visualization tool 2 | ================== 3 | 4 | :mod:`graphix.visualization` module 5 | +++++++++++++++++++++++++++++++++++ 6 | 7 | This module provides functions to visualize the resource state of MBQC pattern. 8 | If flow or gflow exist, the tool take them into account and show the information flow as directed edges. 9 | 10 | .. currentmodule:: graphix.visualization 11 | 12 | .. autoclass:: GraphVisualizer 13 | :members: 14 | -------------------------------------------------------------------------------- /examples/deutsch_jozsa.py: -------------------------------------------------------------------------------- 1 | """ 2 | Preprocessing Clifford gates 3 | ============================ 4 | 5 | In this example, we implement the Deutsch-Jozsa algorithm which determines whether 6 | a function is *balanced* or *constant*. 7 | Since this algorithm is written only with Clifford gates, we can expect the preprocessing of Clifford gates 8 | would significantly improve the MBQC pattern simulation. 9 | You can find nice description of the algorithm `here `_. 10 | 11 | First, let us import relevant modules: 12 | """ 13 | 14 | # %% 15 | from __future__ import annotations 16 | 17 | import numpy as np 18 | 19 | from graphix import Circuit 20 | from graphix.command import CommandKind 21 | 22 | # %% 23 | # Now we implement the algorithm with quantum circuit, which we can transpile into MBQC. 24 | # As an example, we look at balanced oracle for 4 qubits. 25 | 26 | circuit = Circuit(4) 27 | 28 | # prepare all qubits in |0> for easier comparison with original algorithm 29 | for i in range(4): 30 | circuit.h(i) 31 | 32 | # initialization 33 | circuit.h(0) 34 | circuit.h(1) 35 | circuit.h(2) 36 | 37 | # prepare ancilla 38 | circuit.x(3) 39 | circuit.h(3) 40 | 41 | # balanced oracle - flip qubits 0 and 2 42 | circuit.x(0) 43 | circuit.x(2) 44 | 45 | # algorithm 46 | circuit.cnot(0, 3) 47 | circuit.cnot(1, 3) 48 | circuit.cnot(2, 3) 49 | 50 | circuit.x(0) 51 | circuit.x(2) 52 | 53 | circuit.h(0) 54 | circuit.h(1) 55 | circuit.h(2) 56 | 57 | # %% 58 | # Now let us transpile into MBQC measurement pattern and inspect the pattern sequence and graph state 59 | 60 | pattern = circuit.transpile().pattern 61 | print(pattern.to_ascii(left_to_right=True, limit=15)) 62 | pattern.draw_graph(flow_from_pattern=False) 63 | 64 | # %% 65 | # this seems to require quite a large graph state. 66 | # However, we know that Pauli measurements can be preprocessed with graph state simulator. 67 | # To do so, let us first standardize and shift signals, so that measurements are less interdependent. 68 | 69 | pattern.standardize() 70 | pattern.shift_signals() 71 | print(pattern.to_ascii(left_to_right=True, limit=15)) 72 | 73 | # %% 74 | # Now we preprocess all Pauli measurements 75 | 76 | pattern.perform_pauli_measurements() 77 | print( 78 | pattern.to_ascii( 79 | left_to_right=True, 80 | limit=16, 81 | target=[CommandKind.N, CommandKind.M, CommandKind.C], 82 | ) 83 | ) 84 | pattern.draw_graph(flow_from_pattern=True) 85 | 86 | # %% 87 | # Since all operations of the original circuit are Clifford, all measurements in the measurement pattern are Pauli measurements: 88 | # So the preprocessing has done all the necessary computations, and all nodes are isolated with no further measurements required. 89 | # Let us make sure the result is correct: 90 | 91 | out_state = pattern.simulate_pattern() 92 | state = circuit.simulate_statevector().statevec 93 | print("overlap of states: ", np.abs(np.dot(state.psi.flatten().conjugate(), out_state.psi.flatten()))) 94 | 95 | # %% 96 | -------------------------------------------------------------------------------- /examples/fusion_extraction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Designing fusion network to generate resource graph state 3 | ========================================================== 4 | 5 | In this example, we decompose a graph state into a set of GHZ and linear cluster resource states, 6 | such that fusion operations can be used on these 'micro-resource' states to obtain the desired graph state. 7 | This is an important compilation stage to perform MBQC on discrete-variable optical QPUs. 8 | 9 | The decomposition algorithm is based on [1]. 10 | 11 | [1] Zilk et al., A compiler for universal photonic quantum computers, 12 | 2022 `arXiv:2210.09251 `_ 13 | 14 | """ 15 | 16 | # %% 17 | from __future__ import annotations 18 | 19 | import itertools 20 | 21 | import graphix 22 | from graphix import extraction 23 | from graphix.extraction import get_fusion_network_from_graph 24 | 25 | # %% 26 | # Here we say we want a graph state with 9 nodes and 12 edges. 27 | # We can obtain resource graph for a measurement pattern by using :code:`nodes, edges = pattern.get_graph()`. 28 | gs = graphix.GraphState() 29 | nodes = [0, 1, 2, 3, 4, 5, 6, 7, 8] 30 | edges = [(0, 1), (1, 2), (2, 3), (3, 0), (3, 4), (0, 5), (4, 5), (5, 6), (6, 7), (7, 0), (7, 8), (8, 1)] 31 | gs.add_nodes_from(nodes) 32 | gs.add_edges_from(edges) 33 | gs.draw() 34 | 35 | # %% 36 | # Decomposition with GHZ and linear cluster resource states with no limitation in their sizes. 37 | get_fusion_network_from_graph(gs) 38 | 39 | # %% 40 | # If you want to know what nodes are fused in each resource states, 41 | # you can use :func:`~graphix.extraction.get_fusion_nodes` function. 42 | # Currently, we consider only type-I fusion. See [2] for the definition of fusion. 43 | # 44 | # [2] Daniel E. Browne and Terry Rudolph. Resource-efficient linear optical quantum computation. 45 | # Physical Review Letters, 95(1):010501, 2005. 46 | fused_graphs = get_fusion_network_from_graph(gs) 47 | for idx1, idx2 in itertools.combinations(range(len(fused_graphs)), 2): 48 | print( 49 | f"fusion nodes between resource state {idx1} and " 50 | f"resource state {idx2}: {extraction.get_fusion_nodes(fused_graphs[idx1], fused_graphs[idx2])}" 51 | ) 52 | 53 | # %% 54 | # You can also specify the maximum size of GHZ clusters and linear clusters available, 55 | # for more realistic fusion scheduling. 56 | get_fusion_network_from_graph(gs, max_ghz=4, max_lin=4) 57 | -------------------------------------------------------------------------------- /examples/ghz_with_tn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Using Tensor Network simulator 3 | ============================== 4 | 5 | In this example, we simulate a circuit to create Greenberger-Horne-Zeilinger(GHZ) state with a tensor network simulator. 6 | 7 | We will simulate the generation of 100-qubit GHZ state. 8 | Firstly, let us import relevant modules: 9 | """ 10 | 11 | # %% 12 | from __future__ import annotations 13 | 14 | import matplotlib.pyplot as plt 15 | import networkx as nx 16 | 17 | from graphix import Circuit 18 | 19 | n = 100 20 | print(f"{n}-qubit GHZ state generation") 21 | circuit = Circuit(n) 22 | 23 | # initialize to ``|0>`` state. 24 | for i in range(n): 25 | circuit.h(i) 26 | 27 | # GHZ generation 28 | circuit.h(0) 29 | for i in range(1, n): 30 | circuit.cnot(i - 1, i) 31 | 32 | # %% 33 | # Transpile into pattern 34 | 35 | pattern = circuit.transpile().pattern 36 | pattern.standardize() 37 | 38 | nodes, edges = pattern.get_graph() 39 | g = nx.Graph() 40 | g.add_nodes_from(nodes) 41 | g.add_edges_from(edges) 42 | print(f"Number of nodes: {len(nodes)}") 43 | print(f"Number of edges: {len(edges)}") 44 | pos = nx.spring_layout(g) 45 | nx.draw(g, pos=pos, node_size=15) 46 | plt.show() 47 | 48 | # %% 49 | # Calculate the amplitudes of ``|00...0>`` and ``|11...1>`` states. 50 | 51 | tn = pattern.simulate_pattern(backend="tensornetwork") 52 | print(f"The amplitude of |00...0>: {tn.get_basis_amplitude(0)}") 53 | print(f"The amplitude of |11...1>: {tn.get_basis_amplitude(2**n - 1)}") 54 | 55 | # %% 56 | -------------------------------------------------------------------------------- /examples/mbqc_vqe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Variational Quantum Eigensolver (VQE) with Measurement-Based Quantum Computing (MBQC) 3 | ===================================================================================== 4 | 5 | In this example, we solve a simple VQE problem using a measurement-based quantum 6 | computing (MBQC) approach. The Hamiltonian for the system is given by: 7 | 8 | .. math:: 9 | 10 | H = Z_0 Z_1 + X_0 + X_1 11 | 12 | where :math:`Z` and :math:`X` are the Pauli-Z and Pauli-X matrices, respectively. 13 | 14 | This Hamiltonian corresponds to a simple model system often used in quantum computing 15 | to demonstrate algorithms like VQE. The goal is to find the ground state energy of this 16 | Hamiltonian. 17 | 18 | We will build a parameterized quantum circuit and optimize its parameters to minimize 19 | the expectation value of the Hamiltonian, effectively finding the ground state energy. 20 | """ 21 | 22 | from __future__ import annotations 23 | 24 | import itertools 25 | import sys 26 | from timeit import timeit 27 | from typing import TYPE_CHECKING 28 | 29 | import numpy as np 30 | import numpy.typing as npt 31 | from scipy.optimize import minimize 32 | 33 | from graphix import Circuit 34 | from graphix.parameter import Placeholder 35 | from graphix.simulator import PatternSimulator 36 | 37 | if TYPE_CHECKING: 38 | from collections.abc import Iterable 39 | 40 | from graphix.pattern import Pattern 41 | from graphix.transpiler import Angle 42 | 43 | Z = np.array([[1, 0], [0, -1]]) 44 | X = np.array([[0, 1], [1, 0]]) 45 | 46 | 47 | # %% 48 | # Define the Hamiltonian for the VQE problem (Example: H = Z0Z1 + X0 + X1) 49 | def create_hamiltonian() -> npt.NDArray[np.complex128]: 50 | return np.kron(Z, Z) + np.kron(X, np.eye(2)) + np.kron(np.eye(2), X) 51 | 52 | 53 | if sys.version_info >= (3, 12): 54 | batched = itertools.batched 55 | else: 56 | # From https://docs.python.org/3/library/itertools.html#itertools.batched 57 | def batched(iterable, n): 58 | # batched('ABCDEFG', 3) → ABC DEF G 59 | if n < 1: 60 | raise ValueError("n must be at least one") 61 | iterator = iter(iterable) 62 | while batch := tuple(itertools.islice(iterator, n)): 63 | yield batch 64 | 65 | 66 | # %% 67 | # Function to build the VQE circuit 68 | def build_vqe_circuit(n_qubits: int, params: Iterable[Angle]) -> Circuit: 69 | circuit = Circuit(n_qubits) 70 | for i, (x, y, z) in enumerate(batched(params, n=3)): 71 | circuit.rx(i, x) 72 | circuit.ry(i, y) 73 | circuit.rz(i, z) 74 | for i in range(n_qubits - 1): 75 | circuit.cnot(i, i + 1) 76 | return circuit 77 | 78 | 79 | # %% 80 | class MBQCVQE: 81 | def __init__(self, n_qubits: int, hamiltonian: npt.NDArray): 82 | self.n_qubits = n_qubits 83 | self.hamiltonian = hamiltonian 84 | 85 | # %% 86 | # Function to build the MBQC pattern 87 | def build_mbqc_pattern(self, params: Iterable[Angle]) -> Pattern: 88 | circuit = build_vqe_circuit(self.n_qubits, params) 89 | pattern = circuit.transpile().pattern 90 | pattern.standardize() 91 | pattern.shift_signals() 92 | pattern.perform_pauli_measurements() # Perform Pauli measurements 93 | return pattern 94 | 95 | # %% 96 | # Function to simulate the MBQC circuit 97 | def simulate_mbqc(self, params: Iterable[float], backend="tensornetwork"): 98 | pattern = self.build_mbqc_pattern(params) 99 | simulator = PatternSimulator(pattern, backend=backend) 100 | if backend == "tensornetwork": 101 | simulator.run() # Simulate the MBQC circuit using tensor network 102 | tn = simulator.backend.state 103 | tn.default_output_nodes = pattern.output_nodes # Set the default_output_nodes attribute 104 | if tn.default_output_nodes is None: 105 | raise ValueError("Output nodes are not set for tensor network simulation.") 106 | return tn 107 | return simulator.run() # Simulate the MBQC circuit using other backends 108 | 109 | # %% 110 | # Function to compute the energy 111 | def compute_energy(self, params: Iterable[float]): 112 | # Simulate the MBQC circuit using tensor network backend 113 | tn = self.simulate_mbqc(params, backend="tensornetwork") 114 | # Compute the expectation value using MBQCTensornet.expectation_value 115 | return tn.expectation_value(self.hamiltonian, qubit_indices=range(self.n_qubits)) 116 | 117 | 118 | class MBQCVQEWithPlaceholders(MBQCVQE): 119 | def __init__(self, n_qubits: int, hamiltonian) -> None: 120 | super().__init__(n_qubits, hamiltonian) 121 | self.placeholders = tuple(Placeholder(f"{r}[{q}]") for q in range(n_qubits) for r in ("X", "Y", "Z")) 122 | self.pattern = super().build_mbqc_pattern(self.placeholders) 123 | 124 | def build_mbqc_pattern(self, params): 125 | return self.pattern.xreplace(dict(zip(self.placeholders, params))) 126 | 127 | 128 | # %% 129 | # Set parameters for VQE 130 | n_qubits = 2 131 | hamiltonian = create_hamiltonian() 132 | 133 | # %% 134 | # Instantiate the MBQCVQE class 135 | mbqc_vqe = MBQCVQEWithPlaceholders(n_qubits, hamiltonian) 136 | 137 | 138 | # %% 139 | # Define the cost function 140 | def cost_function(params): 141 | return mbqc_vqe.compute_energy(params) 142 | 143 | 144 | # %% 145 | # Random initial parameters 146 | rng = np.random.default_rng() 147 | initial_params = rng.random(n_qubits * 3) 148 | 149 | 150 | # %% 151 | # Perform the optimization using COBYLA 152 | def compute(): 153 | return minimize(cost_function, initial_params, method="COBYLA", options={"maxiter": 100}) 154 | 155 | 156 | result = compute() 157 | 158 | print(f"Optimized parameters: {result.x}") 159 | print(f"Optimized energy: {result.fun}") 160 | 161 | # %% 162 | # Compare with the analytical solution 163 | analytical_solution = -np.sqrt(2) - 1 164 | print(f"Analytical solution: {analytical_solution}") 165 | 166 | # %% 167 | # Compare performances between using parameterized circuits (with placeholders) or not 168 | 169 | mbqc_vqe = MBQCVQEWithPlaceholders(n_qubits, hamiltonian) 170 | time_with_placeholders = timeit(compute, number=2) 171 | print(f"Time with placeholders: {time_with_placeholders}") 172 | 173 | mbqc_vqe = MBQCVQE(n_qubits, hamiltonian) 174 | time_without_placeholders = timeit(compute, number=2) 175 | print(f"Time without placeholders: {time_without_placeholders}") 176 | -------------------------------------------------------------------------------- /examples/qaoa.py: -------------------------------------------------------------------------------- 1 | """ 2 | QAOA 3 | ==== 4 | 5 | Here we generate and optimize pattern for QAOA circuit. 6 | 7 | """ 8 | 9 | # %% 10 | from __future__ import annotations 11 | 12 | import networkx as nx 13 | import numpy as np 14 | 15 | from graphix import Circuit 16 | 17 | rng = np.random.default_rng() 18 | 19 | n = 4 20 | xi = rng.random(6) 21 | theta = rng.random(4) 22 | g = nx.complete_graph(n) 23 | circuit = Circuit(n) 24 | for i, (u, v) in enumerate(g.edges): 25 | circuit.cnot(u, v) 26 | circuit.rz(v, xi[i]) 27 | circuit.cnot(u, v) 28 | for v in g.nodes: 29 | circuit.rx(v, theta[v]) 30 | 31 | # %% 32 | # transpile and get the graph state 33 | 34 | pattern = circuit.transpile().pattern 35 | pattern.standardize() 36 | pattern.shift_signals() 37 | pattern.draw_graph(flow_from_pattern=False) 38 | 39 | 40 | # %% 41 | # perform Pauli measurements and plot the new (minimal) graph to perform the same quantum computation 42 | 43 | pattern.perform_pauli_measurements() 44 | pattern.draw_graph(flow_from_pattern=False) 45 | 46 | # %% 47 | # finally, simulate the QAOA circuit 48 | 49 | out_state = pattern.simulate_pattern() 50 | state = circuit.simulate_statevector().statevec 51 | print("overlap of states: ", np.abs(np.dot(state.psi.flatten().conjugate(), out_state.psi.flatten()))) 52 | # sphinx_gallery_thumbnail_number = 2 53 | 54 | # %% 55 | -------------------------------------------------------------------------------- /examples/qft_with_tn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Large-scale simulations with tensor network simulator 3 | ===================================================== 4 | 5 | In this example, we demonstrate simulation of MBQC involving 10k+ nodes. 6 | 7 | Firstly, let us import relevant modules and define the circuit: 8 | """ 9 | 10 | # %% 11 | from __future__ import annotations 12 | 13 | import numpy as np 14 | 15 | from graphix import Circuit 16 | 17 | 18 | def cp(circuit, theta, control, target): 19 | circuit.rz(control, theta / 2) 20 | circuit.rz(target, theta / 2) 21 | circuit.cnot(control, target) 22 | circuit.rz(target, -1 * theta / 2) 23 | circuit.cnot(control, target) 24 | 25 | 26 | def qft_rotations(circuit, n): 27 | circuit.h(n) 28 | for qubit in range(n + 1, circuit.width): 29 | cp(circuit, np.pi / 2 ** (qubit - n), qubit, n) 30 | 31 | 32 | def swap_registers(circuit, n): 33 | for qubit in range(n // 2): 34 | circuit.swap(qubit, n - qubit - 1) 35 | return circuit 36 | 37 | 38 | def qft(circuit, n): 39 | for i in range(n): 40 | qft_rotations(circuit, i) 41 | swap_registers(circuit, n) 42 | 43 | 44 | # %% 45 | # We will simulate 55-qubit QFT, which requires graph states with more than 10000 nodes. 46 | 47 | n = 55 48 | print(f"{n}-qubit QFT") 49 | circuit = Circuit(n) 50 | 51 | for i in range(n): 52 | circuit.h(i) 53 | qft(circuit, n) 54 | 55 | # standardize pattern 56 | pattern = circuit.transpile().pattern 57 | pattern.standardize() 58 | pattern.shift_signals() 59 | nodes, edges = pattern.get_graph() 60 | print(f"Number of nodes: {len(nodes)}") 61 | print(f"Number of edges: {len(edges)}") 62 | 63 | # %% 64 | # Using efficient graph state simulator `graphix.graphsim`, we can classically preprocess Pauli measurements. 65 | # We are currently improving the speed of this process by using rust-based graph manipulation backend. 66 | pattern.perform_pauli_measurements() 67 | 68 | 69 | # %% 70 | # To specify TN backend of the simulation, simply provide as a keyword argument. 71 | # here we do a very basic check that one of the statevector amplitudes is what it is expected to be: 72 | 73 | import time 74 | 75 | t1 = time.time() 76 | tn = pattern.simulate_pattern(backend="tensornetwork") 77 | value = tn.get_basis_amplitude(0) 78 | t2 = time.time() 79 | print("amplitude of |00...0> is ", value) 80 | print("1/2^n (true answer) is", 1 / 2**n) 81 | print("approximate execution time in seconds: ", t2 - t1) 82 | 83 | # %% 84 | -------------------------------------------------------------------------------- /examples/readme.rst: -------------------------------------------------------------------------------- 1 | Graphix examples 2 | ================ 3 | 4 | Here are a few examples of MBQC programming and simulation using graphix library. 5 | -------------------------------------------------------------------------------- /examples/rotation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example & visualizing graphs 3 | =================================== 4 | 5 | Here, we show a most basic MBQC proramming using graphix library. 6 | In this example, we consider trivial problem of the rotation of two qubits in ``|0>`` states. 7 | We show how transpiler (:class:`~graphix.transpiler.Circuit` class) can be used, 8 | and show the resulting meausrement pattern. 9 | 10 | In the next example, we describe our visualization tool :class:`~graphix.visualization.GraphVisualizer` 11 | and how to understand the plot. 12 | 13 | First, let us import relevant modules: 14 | """ 15 | 16 | # %% 17 | from __future__ import annotations 18 | 19 | import numpy as np 20 | 21 | from graphix import Circuit, Statevec 22 | from graphix.ops import Ops 23 | from graphix.states import BasicStates 24 | 25 | rng = np.random.default_rng() 26 | 27 | # %% 28 | # Here, :class:`~graphix.sim.statevec.Statevec` is our simple statevector simulator class. 29 | # Next, let us define the problem with a standard quantum circuit. 30 | # Note that in graphix all qubits starts in ``|+>`` states. For this example, 31 | # we use Hadamard gate (:meth:`graphix.transpiler.Circuit.h`) to start with ``|0>`` states instead. 32 | 33 | circuit = Circuit(2) 34 | 35 | # initialize qubits in |0>, not |+> 36 | circuit.h(1) 37 | circuit.h(0) 38 | 39 | # apply rotation gates 40 | theta = rng.random(2) 41 | circuit.rx(0, theta[0]) 42 | circuit.rx(1, theta[1]) 43 | 44 | # %% 45 | # Now we transpile into measurement pattern using :meth:`~graphix.transpiler.Circuit.transpile` method. 46 | # This returns :class:`~graphix.pattern.Pattern` object containing measurement pattern: 47 | 48 | pattern = circuit.transpile().pattern 49 | print(pattern.to_ascii(left_to_right=True, limit=10)) 50 | 51 | # %% 52 | # We can plot the graph state to run the above pattern. 53 | # Since there's no two-qubit gates applied to the two qubits in the original gate sequence, 54 | # we see decoupled 1D graphs representing the evolution of single qubits. 55 | # The arrows are the ``information flow `` 56 | # of the MBQC pattern, obtained using the flow-finding algorithm implemented in :class:`graphix.gflow.flow`. 57 | # Below we list the meaning of the node boundary and face colors. 58 | # 59 | # - Nodes with red boundaries are the *input nodes* where the computation starts. 60 | # - Nodes with gray color is the *output nodes* where the final state end up in. 61 | # - Nodes with blue color is the nodes that are measured in *Pauli basis*, one of *X*, *Y* or *Z* computational bases. 62 | # - Nodes in white are the ones measured in *non-Pauli basis*. 63 | # 64 | pattern.draw_graph(flow_from_pattern=False) 65 | 66 | # %% 67 | # we can directly simulate the measurement pattern, to obtain the output state. 68 | # Internally, we are executing the command sequence we inspected above on a statevector simulator. 69 | # We also have a tensornetwork simulation backend to handle larger MBQC patterns. see other examples for how to use it. 70 | 71 | out_state = pattern.simulate_pattern(backend="statevector") 72 | print(out_state.flatten()) 73 | 74 | # %% 75 | # Let us compare with statevector simulation of the original circuit: 76 | 77 | state = Statevec(nqubit=2, data=BasicStates.ZERO) # starts with |0> states 78 | state.evolve_single(Ops.rx(theta[0]), 0) 79 | state.evolve_single(Ops.rx(theta[1]), 1) 80 | print("overlap of states: ", np.abs(np.dot(state.psi.flatten().conjugate(), out_state.psi.flatten()))) 81 | 82 | # %% 83 | # Now let us compile more complex pattern and inspect the graph using the visualization tool. 84 | # Here, the additional edges with dotted lines are the ones that correspond to CNOT gates, 85 | # which creates entanglement between the 1D clusters (nodes connected with directed edges) 86 | # corresponding to the time evolution of a single qubit in the original circuit. 87 | circuit = Circuit(2) 88 | 89 | # apply rotation gates 90 | theta = rng.random(4) 91 | circuit.rz(0, theta[0]) 92 | circuit.rz(1, theta[1]) 93 | circuit.cnot(0, 1) 94 | circuit.s(0) 95 | circuit.cnot(1, 0) 96 | circuit.rz(1, theta[2]) 97 | circuit.cnot(1, 0) 98 | circuit.rz(0, theta[3]) 99 | 100 | pattern = circuit.transpile().pattern 101 | pattern.draw_graph(flow_from_pattern=False) 102 | # %% 103 | -------------------------------------------------------------------------------- /examples/visualization.py: -------------------------------------------------------------------------------- 1 | """ 2 | Visualizing the patterns and flows 3 | ================================== 4 | 5 | :class:`~graphix.visualization.GraphVisualizer` tool offers a wide selection of 6 | visualization methods for inspecting the causal structure of the graph associated 7 | with the pattern, graph or the (generalized-)flow. 8 | """ 9 | 10 | # %% 11 | # Causal flow 12 | # ----------- 13 | # First, let us inspect the flow and gflow associated with the resource graph of a pattern. 14 | # simply call :meth:`~graphix.pattern.Pattern.draw_graph` method. 15 | # Below we list the meaning of the node boundary and face colors. 16 | # 17 | # - Nodes with red boundaries are the *input nodes* where the computation starts. 18 | # - Nodes with gray color is the *output nodes* where the final state end up in. 19 | # - Nodes with blue color is the nodes that are measured in *Pauli basis*, one of *X*, *Y* or *Z* computational bases. 20 | # - Nodes in white are the ones measured in *non-Pauli basis*. 21 | # 22 | from __future__ import annotations 23 | 24 | import numpy as np 25 | 26 | from graphix import Circuit 27 | from graphix.fundamentals import Plane 28 | 29 | circuit = Circuit(3) 30 | circuit.cnot(0, 1) 31 | circuit.cnot(2, 1) 32 | circuit.rx(0, np.pi / 3) 33 | circuit.x(2) 34 | circuit.cnot(2, 1) 35 | pattern = circuit.transpile().pattern 36 | # note that this visualization is not always consistent with the correction set of pattern, 37 | # since we find the correction sets with flow-finding algorithms. 38 | pattern.draw_graph(flow_from_pattern=False, show_measurement_planes=True) 39 | 40 | # %% 41 | # next, show the gflow: 42 | 43 | pattern.perform_pauli_measurements(leave_input=True) 44 | pattern.draw_graph(flow_from_pattern=False, show_measurement_planes=True, node_distance=(1, 0.6)) 45 | 46 | 47 | # %% 48 | # Correction set ('xflow' and 'zflow' of pattern) 49 | # ----------------------------------------------- 50 | # next let us visualize the X and Z correction set in the pattern by :code:`flow_from_pattern=False` statement. 51 | # 52 | 53 | # node_distance argument specifies the scale of the node arrangement in x and y directions. 54 | pattern.draw_graph(flow_from_pattern=True, show_measurement_planes=True, node_distance=(0.7, 0.6)) 55 | 56 | # %% 57 | # Instead of the measurement planes, we can show the local Clifford of the resource graph. 58 | # see *clifford.py* for the details of the indices of each single-qubit Clifford operators. 59 | # 6 is the Hadamard and 8 is the :math:`\sqrt{iY}` operator. 60 | pattern.draw_graph(flow_from_pattern=True, show_local_clifford=True, node_distance=(0.7, 0.6)) 61 | 62 | # %% 63 | # Visualize based on the graph 64 | # ---------------------------- 65 | # The visualizer also works without the pattern. Simply supply the 66 | 67 | import networkx as nx 68 | 69 | from graphix.visualization import GraphVisualizer 70 | 71 | # graph with gflow but no flow 72 | nodes = [1, 2, 3, 4, 5, 6] 73 | edges = [(1, 4), (1, 6), (2, 4), (2, 5), (2, 6), (3, 5), (3, 6)] 74 | inputs = {1, 2, 3} 75 | outputs = {4, 5, 6} 76 | graph = nx.Graph() 77 | graph.add_nodes_from(nodes) 78 | graph.add_edges_from(edges) 79 | meas_planes = {1: Plane.XY, 2: Plane.XY, 3: Plane.XY} 80 | vis = GraphVisualizer(graph, inputs, outputs, meas_plane=meas_planes) 81 | vis.visualize(show_measurement_planes=True) 82 | 83 | # %% 84 | 85 | # graph with extended gflow but no flow 86 | nodes = [0, 1, 2, 3, 4, 5] 87 | edges = [(0, 1), (0, 2), (0, 4), (1, 5), (2, 4), (2, 5), (3, 5)] 88 | inputs = {0, 1} 89 | outputs = {4, 5} 90 | graph = nx.Graph() 91 | graph.add_nodes_from(nodes) 92 | graph.add_edges_from(edges) 93 | meas_planes = { 94 | 0: Plane.XY, 95 | 1: Plane.XY, 96 | 2: Plane.XZ, 97 | 3: Plane.YZ, 98 | } 99 | vis = GraphVisualizer(graph, inputs, outputs, meas_plane=meas_planes) 100 | vis.visualize(show_measurement_planes=True) 101 | 102 | # %% 103 | -------------------------------------------------------------------------------- /graphix/__init__.py: -------------------------------------------------------------------------------- 1 | """Optimize and simulate measurement-based quantum computation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from graphix.generator import generate_from_graph 6 | from graphix.graphsim import GraphState 7 | from graphix.pattern import Pattern 8 | from graphix.sim.statevec import Statevec 9 | from graphix.transpiler import Circuit 10 | 11 | __all__ = ["Circuit", "GraphState", "Pattern", "Statevec", "generate_from_graph"] 12 | -------------------------------------------------------------------------------- /graphix/clifford.py: -------------------------------------------------------------------------------- 1 | """24 Unique single-qubit Clifford gates and their multiplications, conjugations and Pauli conjugations.""" 2 | 3 | from __future__ import annotations 4 | 5 | import copy 6 | import math 7 | from enum import Enum 8 | from typing import TYPE_CHECKING, Any 9 | 10 | import numpy as np 11 | import typing_extensions 12 | 13 | from graphix._db import ( 14 | CLIFFORD, 15 | CLIFFORD_CONJ, 16 | CLIFFORD_HSZ_DECOMPOSITION, 17 | CLIFFORD_LABEL, 18 | CLIFFORD_MEASURE, 19 | CLIFFORD_MUL, 20 | CLIFFORD_TO_QASM3, 21 | ) 22 | from graphix.fundamentals import IXYZ, ComplexUnit 23 | from graphix.measurements import Domains 24 | from graphix.pauli import Pauli 25 | 26 | if TYPE_CHECKING: 27 | import numpy.typing as npt 28 | 29 | 30 | class Clifford(Enum): 31 | """Clifford gate.""" 32 | 33 | # MEMO: Cannot use ClassVar here 34 | I: Clifford 35 | X: Clifford 36 | Y: Clifford 37 | Z: Clifford 38 | S: Clifford 39 | SDG: Clifford 40 | H: Clifford 41 | 42 | _0 = 0 43 | _1 = 1 44 | _2 = 2 45 | _3 = 3 46 | _4 = 4 47 | _5 = 5 48 | _6 = 6 49 | _7 = 7 50 | _8 = 8 51 | _9 = 9 52 | _10 = 10 53 | _11 = 11 54 | _12 = 12 55 | _13 = 13 56 | _14 = 14 57 | _15 = 15 58 | _16 = 16 59 | _17 = 17 60 | _18 = 18 61 | _19 = 19 62 | _20 = 20 63 | _21 = 21 64 | _22 = 22 65 | _23 = 23 66 | 67 | @property 68 | def matrix(self) -> npt.NDArray[np.complex128]: 69 | """Return the matrix of the Clifford gate.""" 70 | return CLIFFORD[self.value] 71 | 72 | @staticmethod 73 | def try_from_matrix(mat: npt.NDArray[Any]) -> Clifford | None: 74 | """Find the Clifford gate from the matrix. 75 | 76 | Return `None` if not found. 77 | 78 | Notes 79 | ----- 80 | Global phase is ignored. 81 | """ 82 | if mat.shape != (2, 2): 83 | return None 84 | for ci in Clifford: 85 | mi = ci.matrix 86 | for piv, piv_ in zip(mat.flat, mi.flat): 87 | if math.isclose(abs(piv), 0): 88 | continue 89 | if math.isclose(abs(piv_), 0): 90 | continue 91 | if np.allclose(mat / piv, mi / piv_): 92 | return ci 93 | return None 94 | 95 | def __repr__(self) -> str: 96 | """Return the Clifford expression on the form of HSZ decomposition.""" 97 | formula = " @ ".join([f"Clifford.{gate}" for gate in self.hsz]) 98 | if len(self.hsz) == 1: 99 | return formula 100 | return f"({formula})" 101 | 102 | def __str__(self) -> str: 103 | """Return the name of the Clifford gate.""" 104 | return CLIFFORD_LABEL[self.value] 105 | 106 | @property 107 | def conj(self) -> Clifford: 108 | """Return the conjugate of the Clifford gate.""" 109 | return Clifford(CLIFFORD_CONJ[self.value]) 110 | 111 | @property 112 | def hsz(self) -> list[Clifford]: 113 | """Return a decomposition of the Clifford gate with the gates `H`, `S`, `Z`.""" 114 | return [Clifford(i) for i in CLIFFORD_HSZ_DECOMPOSITION[self.value]] 115 | 116 | @property 117 | def qasm3(self) -> tuple[str, ...]: 118 | """Return a decomposition of the Clifford gate as qasm3 gates.""" 119 | return CLIFFORD_TO_QASM3[self.value] 120 | 121 | def __matmul__(self, other: Clifford) -> Clifford: 122 | """Multiplication within the Clifford group (modulo unit factor).""" 123 | if isinstance(other, Clifford): 124 | return Clifford(CLIFFORD_MUL[self.value][other.value]) 125 | return NotImplemented 126 | 127 | def measure(self, pauli: Pauli) -> Pauli: 128 | """Compute C† P C.""" 129 | if pauli.symbol == IXYZ.I: 130 | return copy.deepcopy(pauli) 131 | table = CLIFFORD_MEASURE[self.value] 132 | if pauli.symbol == IXYZ.X: 133 | symbol, sign = table.x 134 | elif pauli.symbol == IXYZ.Y: 135 | symbol, sign = table.y 136 | elif pauli.symbol == IXYZ.Z: 137 | symbol, sign = table.z 138 | else: 139 | typing_extensions.assert_never(pauli.symbol) 140 | return pauli.unit * Pauli(symbol, ComplexUnit.from_properties(sign=sign)) 141 | 142 | def commute_domains(self, domains: Domains) -> Domains: 143 | """ 144 | Commute `X^sZ^t` with `C`. 145 | 146 | Given `X^sZ^t`, return `X^s'Z^t'` such that `X^sZ^tC = CX^s'Z^t'`. 147 | 148 | Note that applying the method to `self.conj` computes the reverse commutation: 149 | indeed, `C†X^sZ^t = (X^sZ^tC)† = (CX^s'Z^t')† = X^s'Z^t'C†`. 150 | """ 151 | s_domain = domains.s_domain.copy() 152 | t_domain = domains.t_domain.copy() 153 | for gate in self.hsz: 154 | if gate == Clifford.I: 155 | pass 156 | elif gate == Clifford.H: 157 | t_domain, s_domain = s_domain, t_domain 158 | elif gate == Clifford.S: 159 | t_domain ^= s_domain 160 | elif gate == Clifford.Z: 161 | pass 162 | else: # pragma: no cover 163 | raise RuntimeError(f"{gate} should be either I, H, S or Z.") 164 | return Domains(s_domain, t_domain) 165 | 166 | 167 | Clifford.I = Clifford(0) 168 | Clifford.X = Clifford(1) 169 | Clifford.Y = Clifford(2) 170 | Clifford.Z = Clifford(3) 171 | Clifford.S = Clifford(4) 172 | Clifford.SDG = Clifford(5) 173 | Clifford.H = Clifford(6) 174 | -------------------------------------------------------------------------------- /graphix/device_interface.py: -------------------------------------------------------------------------------- 1 | """Quantum hardware device interface. 2 | 3 | Runs MBQC command sequence on quantum hardware. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import TYPE_CHECKING, Any 9 | 10 | if TYPE_CHECKING: 11 | from graphix.pattern import Pattern 12 | 13 | 14 | class PatternRunner: 15 | """MBQC pattern runner. 16 | 17 | Executes the measurement pattern. 18 | """ 19 | 20 | def __init__(self, pattern: Pattern, backend: str = "ibmq", **kwargs) -> None: 21 | """Instantiate a pattern runner. 22 | 23 | Parameters 24 | ---------- 25 | pattern: :class:`graphix.pattern.Pattern` object 26 | MBQC pattern to be executed. 27 | backend: str 28 | execution backend (optional, default is 'ibmq') 29 | kwargs: dict 30 | keyword args for specified backend. 31 | """ 32 | self.pattern = pattern 33 | self.backend_name = backend 34 | 35 | if self.backend_name == "ibmq": 36 | try: 37 | from graphix_ibmq.runner import IBMQBackend 38 | except Exception as e: 39 | raise ImportError( 40 | "Failed to import graphix_ibmq. Please install graphix_ibmq by `pip install graphix-ibmq`." 41 | ) from e 42 | self.backend = IBMQBackend(pattern) 43 | try: 44 | instance = kwargs.get("instance", "ibm-q/open/main") 45 | resource = kwargs.get("resource") 46 | save_statevector = kwargs.get("save_statevector", False) 47 | optimization_level = kwargs.get("optimizer_level", 1) 48 | 49 | self.backend.get_backend(instance, resource) 50 | self.backend.to_qiskit(save_statevector) 51 | self.backend.transpile(optimization_level) 52 | self.shots = kwargs.get("shots", 1024) 53 | except Exception: # noqa: BLE001 # TODO: Resolve this 54 | save_statevector = kwargs.get("save_statevector", False) 55 | optimization_level = kwargs.get("optimizer_level", 1) 56 | self.backend.to_qiskit(save_statevector) 57 | self.shots = kwargs.get("shots", 1024) 58 | else: 59 | raise ValueError("unknown backend") 60 | 61 | def simulate(self, **kwargs) -> Any: 62 | """Perform the simulation. 63 | 64 | Parameters 65 | ---------- 66 | kwargs: dict 67 | keyword args for specified backend. 68 | 69 | Returns 70 | ------- 71 | result: Any 72 | the simulation result, 73 | in the representation depending on the backend used. 74 | """ 75 | if self.backend_name == "ibmq": 76 | shots = kwargs.get("shots", self.shots) 77 | noise_model = kwargs.get("noise_model") 78 | format_result = kwargs.get("format_result", True) 79 | 80 | result = self.backend.simulate(shots=shots, noise_model=noise_model, format_result=format_result) 81 | 82 | return result 83 | 84 | def run(self, **kwargs) -> Any: 85 | """Perform the execution. 86 | 87 | Parameters 88 | ---------- 89 | kwargs: dict 90 | keyword args for specified backend. 91 | 92 | Returns 93 | ------- 94 | result: Any 95 | the measurement result, 96 | in the representation depending on the backend used. 97 | """ 98 | if self.backend_name == "ibmq": 99 | shots = kwargs.get("shots", self.shots) 100 | format_result = kwargs.get("format_result", True) 101 | optimization_level = kwargs.get("optimizer_level", 1) 102 | 103 | result = self.backend.run(shots=shots, format_result=format_result, optimization_level=optimization_level) 104 | 105 | return result 106 | 107 | def retrieve_result(self, **kwargs) -> Any: 108 | """Retrieve the execution result. 109 | 110 | Parameters 111 | ---------- 112 | kwargs: dict 113 | keyword args for specified backend. 114 | 115 | Returns 116 | ------- 117 | result: Any 118 | the measurement result, 119 | in the representation depending on the backend used. 120 | """ 121 | if self.backend_name == "ibmq": 122 | job_id = kwargs.get("job_id") 123 | result = self.backend.retrieve_result(job_id) 124 | 125 | return result 126 | -------------------------------------------------------------------------------- /graphix/extraction.py: -------------------------------------------------------------------------------- 1 | """Functions to extract fusion network from a given graph state.""" 2 | 3 | from __future__ import annotations 4 | 5 | import copy 6 | import dataclasses 7 | import operator 8 | from enum import Enum 9 | 10 | import networkx as nx 11 | import numpy as np 12 | 13 | from graphix.graphsim import GraphState 14 | 15 | 16 | class ResourceType(Enum): 17 | """Resource type.""" 18 | 19 | GHZ = "GHZ" 20 | LINEAR = "LINEAR" 21 | NONE = None 22 | 23 | def __str__(self) -> str: 24 | """Return the name of the resource type.""" 25 | return self.name 26 | 27 | 28 | @dataclasses.dataclass 29 | class ResourceGraph: 30 | """Resource graph state object. 31 | 32 | Parameters 33 | ---------- 34 | cltype : :class:`ResourceType` object 35 | Type of the cluster. 36 | graph : :class:`~graphix.graphsim.GraphState` object 37 | Graph state of the cluster. 38 | """ 39 | 40 | cltype: ResourceType 41 | graph: GraphState 42 | 43 | def __eq__(self, other: object) -> bool: 44 | """Return `True` if two resource graphs are equal, `False` otherwise.""" 45 | if not isinstance(other, ResourceGraph): 46 | raise TypeError("cannot compare ResourceGraph with other object") 47 | 48 | return self.cltype == other.cltype and nx.utils.graphs_equal(self.graph, other.graph) # type: ignore[no-untyped-call] 49 | 50 | 51 | def get_fusion_network_from_graph( 52 | graph: GraphState, 53 | max_ghz: float = np.inf, 54 | max_lin: float = np.inf, 55 | ) -> list[ResourceGraph]: 56 | """Extract GHZ and linear cluster graph state decomposition of desired resource state :class:`~graphix.graphsim.GraphState`. 57 | 58 | Extraction algorithm is based on [1]. 59 | 60 | [1] Zilk et al., A compiler for universal photonic quantum computers, 2022 `arXiv:2210.09251 `_ 61 | 62 | Parameters 63 | ---------- 64 | graph : :class:`~graphix.graphsim.GraphState` object 65 | Graph state. 66 | phasedict : dict 67 | Dictionary of phases for each node. 68 | max_ghz: 69 | Maximum size of ghz clusters 70 | max_lin: 71 | Maximum size of linear clusters 72 | 73 | Returns 74 | ------- 75 | list 76 | List of :class:`ResourceGraph` objects. 77 | """ 78 | adjdict = {k: dict(copy.deepcopy(v)) for k, v in graph.adjacency()} 79 | 80 | number_of_edges = graph.number_of_edges() 81 | resource_list = [] 82 | neighbors_list = [] 83 | 84 | # Prepare a list sorted by number of neighbors to get the largest GHZ clusters first. 85 | for v, va in adjdict.items(): 86 | if len(va) > 2: 87 | neighbors_list.append((v, len(va))) 88 | # If there is an isolated node, add it to the list. 89 | if len(va) == 0: 90 | resource_list.append(create_resource_graph([v], root=v)) 91 | 92 | # Find GHZ graphs in the graph and remove their edges from the graph. 93 | # All nodes that have more than 2 edges become the roots of the GHZ clusters. 94 | for v, _ in sorted(neighbors_list, key=operator.itemgetter(1), reverse=True): 95 | if len(adjdict[v]) > 2: 96 | nodes = [v] 97 | while len(adjdict[v]) > 0 and len(nodes) < max_ghz: 98 | n, _ = adjdict[v].popitem() 99 | nodes.append(n) 100 | del adjdict[n][v] 101 | number_of_edges -= 1 102 | resource_list.append(create_resource_graph(nodes, root=v)) 103 | 104 | # Find Linear clusters in the remaining graph and remove their edges from the graph. 105 | while number_of_edges != 0: 106 | for v, va in adjdict.items(): 107 | if len(va) == 1: 108 | n = v 109 | nodes = [n] 110 | while len(adjdict[n]) > 0 and len(nodes) < max_lin: 111 | n2, _ = adjdict[n].popitem() 112 | nodes.append(n2) 113 | del adjdict[n2][n] 114 | number_of_edges -= 1 115 | n = n2 116 | 117 | # We define any cluster whose size is smaller than 4, a GHZ cluster 118 | if len(nodes) == 3: 119 | resource_list.append(create_resource_graph([nodes[1], nodes[0], nodes[2]], root=nodes[1])) 120 | elif len(nodes) == 2: 121 | resource_list.append(create_resource_graph(nodes, root=nodes[0])) 122 | else: 123 | resource_list.append(create_resource_graph(nodes)) 124 | 125 | # If a cycle exists in the graph, extract one 3-qubit ghz cluster from the cycle. 126 | for v, va in adjdict.items(): 127 | if len(va) == 2: 128 | neighbors = list(va.keys()) 129 | nodes = [v, *neighbors] 130 | del adjdict[neighbors[0]][v] 131 | del adjdict[neighbors[1]][v] 132 | del va[neighbors[0]] 133 | del va[neighbors[1]] 134 | number_of_edges -= 2 135 | 136 | resource_list.append(create_resource_graph(nodes, root=v)) 137 | break 138 | return resource_list 139 | 140 | 141 | def create_resource_graph(node_ids: list[int], root: int | None = None) -> ResourceGraph: 142 | """Create a resource graph state (GHZ or linear) from node ids. 143 | 144 | Parameters 145 | ---------- 146 | node_ids : list 147 | List of node ids. 148 | root : int 149 | Root of the ghz cluster. If None, it's a linear cluster. 150 | 151 | Returns 152 | ------- 153 | :class:`ResourceGraph` object 154 | `ResourceGraph` object. 155 | """ 156 | cluster_type = None 157 | edges = [] 158 | if root is not None: 159 | edges = [(root, i) for i in node_ids if i != root] 160 | cluster_type = ResourceType.GHZ 161 | else: 162 | edges = [(node_ids[i], node_ids[i + 1]) for i in range(len(node_ids)) if i + 1 < len(node_ids)] 163 | cluster_type = ResourceType.LINEAR 164 | tmp_graph = GraphState() 165 | tmp_graph.add_nodes_from(node_ids) 166 | tmp_graph.add_edges_from(edges) 167 | return ResourceGraph(cltype=cluster_type, graph=tmp_graph) 168 | 169 | 170 | def get_fusion_nodes(c1: ResourceGraph, c2: ResourceGraph) -> list[int]: 171 | """Get the nodes that are fused between two resource states. Currently, we consider only type-I fusion. 172 | 173 | See [2] for the definition of fusion operation. 174 | 175 | [2] Daniel E. Browne and Terry Rudolph. Resource-efficient linear optical quantum computation. Physical Review Letters, 95(1):010501, 2005. 176 | 177 | Parameters 178 | ---------- 179 | c1 : :class:`ResourceGraph` object 180 | First resource state to be fused. 181 | c2 : :class:`ResourceGraph` object 182 | Second resource state to be fused. 183 | 184 | Returns 185 | ------- 186 | list 187 | List of nodes that are fused between the two clusters. 188 | """ 189 | if not isinstance(c1, ResourceGraph) or not isinstance(c2, ResourceGraph): 190 | raise TypeError("c1 and c2 must be Cluster objects") 191 | 192 | if c1 == c2: 193 | return [] 194 | return [n for n in c1.graph.nodes if n in c2.graph.nodes] 195 | -------------------------------------------------------------------------------- /graphix/generator.py: -------------------------------------------------------------------------------- 1 | """MBQC pattern generator.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from graphix.command import E, M, N, X, Z 8 | from graphix.fundamentals import Plane 9 | from graphix.gflow import find_flow, find_gflow, find_odd_neighbor, get_layers 10 | from graphix.pattern import Pattern 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Iterable, Mapping, Sequence 14 | 15 | import networkx as nx 16 | import numpy as np 17 | import numpy.typing as npt 18 | 19 | 20 | def generate_from_graph( 21 | graph: nx.Graph[int], 22 | angles: Mapping[int, float] | Sequence[float] | npt.NDArray[np.float64], 23 | inputs: Iterable[int], 24 | outputs: Iterable[int], 25 | meas_planes: Mapping[int, Plane] | None = None, 26 | ) -> Pattern: 27 | r"""Generate the measurement pattern from open graph and measurement angles. 28 | 29 | This function takes an open graph ``G = (nodes, edges, input, outputs)``, 30 | specified by :class:`networkx.Graph` and two lists specifying input and output nodes. 31 | Currently we support XY-plane measurements. 32 | 33 | Searches for the flow in the open graph using :func:`graphix.gflow.find_flow` and if found, 34 | construct the measurement pattern according to the theorem 1 of [NJP 9, 250 (2007)]. 35 | 36 | Then, if no flow was found, searches for gflow using :func:`graphix.gflow.find_gflow`, 37 | from which measurement pattern can be constructed from theorem 2 of [NJP 9, 250 (2007)]. 38 | 39 | The constructed measurement pattern deterministically realize the unitary embedding 40 | 41 | .. math:: 42 | 43 | U = \left( \prod_i \langle +_{\alpha_i} |_i \right) E_G N_{I^C}, 44 | 45 | where the measurements (bras) with always :math:`\langle+|` bases determined by the measurement 46 | angles :math:`\alpha_i` are applied to the measuring nodes, 47 | i.e. the randomness of the measurement is eliminated by the added byproduct commands. 48 | 49 | .. seealso:: :func:`graphix.gflow.find_flow` :func:`graphix.gflow.find_gflow` :class:`graphix.pattern.Pattern` 50 | 51 | Parameters 52 | ---------- 53 | graph : :class:`networkx.Graph` 54 | Graph on which MBQC should be performed 55 | angles : dict 56 | measurement angles for each nodes on the graph (unit of pi), except output nodes 57 | inputs : list 58 | list of node indices for input nodes 59 | outputs : list 60 | list of node indices for output nodes 61 | meas_planes : dict 62 | optional: measurement planes for each nodes on the graph, except output nodes 63 | 64 | Returns 65 | ------- 66 | pattern : graphix.pattern.Pattern 67 | constructed pattern. 68 | """ 69 | inputs = list(inputs) 70 | outputs = list(outputs) 71 | measuring_nodes = list(set(graph.nodes) - set(outputs) - set(inputs)) 72 | 73 | meas_planes = dict.fromkeys(measuring_nodes, Plane.XY) if meas_planes is None else dict(meas_planes) 74 | 75 | # search for flow first 76 | f, l_k = find_flow(graph, set(inputs), set(outputs), meas_planes=meas_planes) 77 | if f is not None: 78 | # flow found 79 | depth, layers = get_layers(l_k) 80 | pattern = Pattern(input_nodes=inputs) 81 | for i in set(graph.nodes) - set(inputs): 82 | pattern.add(N(node=i)) 83 | for e in graph.edges: 84 | pattern.add(E(nodes=e)) 85 | measured = [] 86 | for i in range(depth, 0, -1): # i from depth, depth-1, ... 1 87 | for j in layers[i]: 88 | measured.append(j) 89 | pattern.add(M(node=j, angle=angles[j])) 90 | neighbors: set[int] = set() 91 | for k in f[j]: 92 | neighbors |= set(graph.neighbors(k)) 93 | for k in neighbors - {j}: 94 | # if k not in measured: 95 | pattern.add(Z(node=k, domain={j})) 96 | pattern.add(X(node=f[j].pop(), domain={j})) 97 | else: 98 | # no flow found - we try gflow 99 | g, l_k = find_gflow(graph, set(inputs), set(outputs), meas_planes=meas_planes) 100 | if g is not None: 101 | # gflow found 102 | depth, layers = get_layers(l_k) 103 | pattern = Pattern(input_nodes=inputs) 104 | for i in set(graph.nodes) - set(inputs): 105 | pattern.add(N(node=i)) 106 | for e in graph.edges: 107 | pattern.add(E(nodes=e)) 108 | for i in range(depth, 0, -1): # i from depth, depth-1, ... 1 109 | for j in layers[i]: 110 | pattern.add(M(node=j, plane=meas_planes[j], angle=angles[j])) 111 | odd_neighbors = find_odd_neighbor(graph, g[j]) 112 | for k in odd_neighbors - {j}: 113 | pattern.add(Z(node=k, domain={j})) 114 | for k in g[j] - {j}: 115 | pattern.add(X(node=k, domain={j})) 116 | else: 117 | raise ValueError("no flow or gflow found") 118 | 119 | pattern.reorder_output_nodes(outputs) 120 | return pattern 121 | -------------------------------------------------------------------------------- /graphix/linalg_validations.py: -------------------------------------------------------------------------------- 1 | """Validation functions for linear algebra.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TypeVar 6 | 7 | import numpy as np 8 | import numpy.typing as npt 9 | 10 | _T = TypeVar("_T", bound=np.generic) 11 | 12 | 13 | def is_square(matrix: npt.NDArray[_T]) -> bool: 14 | """Check if matrix is square.""" 15 | if matrix.ndim != 2: 16 | return False 17 | rows, cols = matrix.shape 18 | # Circumvent a regression in numpy 2.1. 19 | # Note that this regression is already fixed in numpy 2.2. 20 | # reveal_type(rows) -> Any 21 | # reveal_type(cols) -> Any 22 | assert isinstance(rows, int) 23 | assert isinstance(cols, int) 24 | return rows == cols 25 | 26 | 27 | def is_qubitop(matrix: npt.NDArray[_T]) -> bool: 28 | """Check if matrix is a square matrix with a power of 2 dimension.""" 29 | if not is_square(matrix): 30 | return False 31 | size, _ = matrix.shape 32 | # Circumvent a regression in numpy 2.1. 33 | # Note that this regression is already fixed in numpy 2.2. 34 | # reveal_type(size) -> Any 35 | assert isinstance(size, int) 36 | return size > 0 and size & (size - 1) == 0 37 | 38 | 39 | def is_hermitian(matrix: npt.NDArray[_T]) -> bool: 40 | """Check if matrix is hermitian.""" 41 | if not is_square(matrix): 42 | return False 43 | return np.allclose(matrix, matrix.transpose().conjugate()) 44 | 45 | 46 | def is_psd(matrix: npt.NDArray[_T], tol: float = 1e-15) -> bool: 47 | """ 48 | Check if a density matrix is positive semidefinite by diagonalizing. 49 | 50 | Parameters 51 | ---------- 52 | matrix : np.ndarray 53 | matrix to check 54 | tol : float 55 | tolerance on the small negatives. Default 1e-15. 56 | """ 57 | if not is_square(matrix): 58 | return False 59 | if tol < 0: 60 | raise ValueError("tol must be non-negative.") 61 | if not is_hermitian(matrix): 62 | return False 63 | evals = np.linalg.eigvalsh(matrix.astype(np.complex128)) 64 | return all(evals >= -tol) 65 | 66 | 67 | def is_unit_trace(matrix: npt.NDArray[_T]) -> bool: 68 | """Check if matrix has trace 1.""" 69 | if not is_square(matrix): 70 | return False 71 | return np.allclose(matrix.trace(), 1.0) 72 | -------------------------------------------------------------------------------- /graphix/measurements.py: -------------------------------------------------------------------------------- 1 | """Data structure for single-qubit measurements in MBQC.""" 2 | 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | import math 7 | from typing import NamedTuple, SupportsInt 8 | 9 | from graphix import utils 10 | from graphix.fundamentals import Axis, Plane, Sign 11 | 12 | # Ruff suggests to move this import to a type-checking block, but dataclass requires it here 13 | from graphix.parameter import ExpressionOrFloat # noqa: TC001 14 | 15 | 16 | @dataclasses.dataclass 17 | class Domains: 18 | """Represent `X^sZ^t` where s and t are XOR of results from given sets of indices.""" 19 | 20 | s_domain: set[int] 21 | t_domain: set[int] 22 | 23 | 24 | class Measurement(NamedTuple): 25 | """An MBQC measurement. 26 | 27 | :param angle: the angle of the measurement. Should be between [0, 2) 28 | :param plane: the measurement plane 29 | """ 30 | 31 | angle: float 32 | plane: Plane 33 | 34 | def isclose(self, other: Measurement, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: 35 | """Compare if two measurements have the same plane and their angles are close. 36 | 37 | Example 38 | ------- 39 | >>> from graphix.opengraph import Measurement 40 | >>> from graphix.fundamentals import Plane 41 | >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.XY)) 42 | True 43 | >>> Measurement(0.0, Plane.XY).isclose(Measurement(0.0, Plane.YZ)) 44 | False 45 | >>> Measurement(0.1, Plane.XY).isclose(Measurement(0.0, Plane.XY)) 46 | False 47 | """ 48 | return math.isclose(self.angle, other.angle, rel_tol=rel_tol, abs_tol=abs_tol) and self.plane == other.plane 49 | 50 | 51 | class PauliMeasurement(NamedTuple): 52 | """Pauli measurement.""" 53 | 54 | axis: Axis 55 | sign: Sign 56 | 57 | @staticmethod 58 | def try_from(plane: Plane, angle: ExpressionOrFloat) -> PauliMeasurement | None: 59 | """Return the Pauli measurement description if a given measure is Pauli.""" 60 | angle_double = 2 * angle 61 | if not isinstance(angle_double, SupportsInt) or not utils.is_integer(angle_double): 62 | return None 63 | angle_double_mod_4 = int(angle_double) % 4 64 | axis = plane.cos if angle_double_mod_4 % 2 == 0 else plane.sin 65 | sign = Sign.minus_if(angle_double_mod_4 >= 2) 66 | return PauliMeasurement(axis, sign) 67 | -------------------------------------------------------------------------------- /graphix/noise_models/__init__.py: -------------------------------------------------------------------------------- 1 | """Noise models.""" 2 | 3 | from __future__ import annotations 4 | 5 | from graphix.noise_models.noise_model import NoiseModel 6 | from graphix.noise_models.noiseless_noise_model import NoiselessNoiseModel 7 | 8 | __all__ = ["NoiseModel", "NoiselessNoiseModel"] 9 | -------------------------------------------------------------------------------- /graphix/noise_models/noise_model.py: -------------------------------------------------------------------------------- 1 | """Abstract interface for noise models. 2 | 3 | This module defines :class:`NoiseModel`, the base class used by 4 | :class:`graphix.simulator.PatternSimulator` when running noisy 5 | simulations. Child classes implement concrete noise processes by 6 | overriding the abstract methods defined here. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import abc 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from graphix.channels import KrausChannel 16 | from graphix.simulator import PatternSimulator 17 | 18 | 19 | class NoiseModel(abc.ABC): 20 | """Base class for all noise models.""" 21 | 22 | data: PatternSimulator 23 | 24 | # shared by all objects of the child class. 25 | def assign_simulator(self, simulator: PatternSimulator) -> None: 26 | """Assign the running simulator. 27 | 28 | Parameters 29 | ---------- 30 | simulator : :class:`~graphix.simulator.PatternSimulator` 31 | Simulator instance that will use this noise model. 32 | """ 33 | self.simulator = simulator 34 | 35 | @abc.abstractmethod 36 | def prepare_qubit(self) -> KrausChannel: 37 | """Return the preparation channel. 38 | 39 | Returns 40 | ------- 41 | KrausChannel 42 | Channel applied after single-qubit preparation. 43 | """ 44 | ... 45 | 46 | @abc.abstractmethod 47 | def entangle(self) -> KrausChannel: 48 | """Return the channel applied after entanglement. 49 | 50 | Returns 51 | ------- 52 | KrausChannel 53 | Channel modeling noise during the CZ gate. 54 | """ 55 | ... 56 | 57 | @abc.abstractmethod 58 | def measure(self) -> KrausChannel: 59 | """Return the measurement channel. 60 | 61 | Returns 62 | ------- 63 | KrausChannel 64 | Channel applied immediately before measurement. 65 | """ 66 | ... 67 | 68 | @abc.abstractmethod 69 | def confuse_result(self, result: bool) -> bool: 70 | """Return a possibly flipped measurement outcome. 71 | 72 | Parameters 73 | ---------- 74 | result : bool 75 | Ideal measurement result. 76 | 77 | Returns 78 | ------- 79 | bool 80 | Possibly corrupted result. 81 | """ 82 | 83 | @abc.abstractmethod 84 | def byproduct_x(self) -> KrausChannel: 85 | """Return the channel for X by-product corrections. 86 | 87 | Returns 88 | ------- 89 | KrausChannel 90 | Channel applied after an X correction. 91 | """ 92 | ... 93 | 94 | @abc.abstractmethod 95 | def byproduct_z(self) -> KrausChannel: 96 | """Return the channel for Z by-product corrections. 97 | 98 | Returns 99 | ------- 100 | KrausChannel 101 | Channel applied after a Z correction. 102 | """ 103 | ... 104 | 105 | @abc.abstractmethod 106 | def clifford(self) -> KrausChannel: 107 | """Return the channel for Clifford gates. 108 | 109 | Returns 110 | ------- 111 | KrausChannel 112 | Channel modeling the noise of Clifford operations. 113 | """ 114 | # NOTE might be different depending on the gate. 115 | ... 116 | 117 | @abc.abstractmethod 118 | def tick_clock(self) -> None: 119 | """Advance the simulator clock. 120 | 121 | This accounts for idle errors such as :math:`T_1` and :math:`T_2`. All 122 | commands between consecutive ``T`` instructions are considered 123 | simultaneous. 124 | """ 125 | ... 126 | -------------------------------------------------------------------------------- /graphix/noise_models/noiseless_noise_model.py: -------------------------------------------------------------------------------- 1 | """Noise model that introduces no errors. 2 | 3 | This class is useful for unit tests or benchmarks where deterministic 4 | behaviour is required. All methods simply return an identity 5 | :class:`~graphix.channels.KrausChannel`. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import numpy as np 11 | import typing_extensions 12 | 13 | from graphix.channels import KrausChannel, KrausData 14 | from graphix.noise_models.noise_model import NoiseModel 15 | 16 | 17 | class NoiselessNoiseModel(NoiseModel): 18 | """Noise model that performs no operation.""" 19 | 20 | @typing_extensions.override 21 | def prepare_qubit(self) -> KrausChannel: 22 | """Return the identity preparation channel. 23 | 24 | Returns 25 | ------- 26 | KrausChannel 27 | Identity channel :math:`I_2`. 28 | """ 29 | return KrausChannel([KrausData(1.0, np.eye(2))]) 30 | 31 | @typing_extensions.override 32 | def entangle(self) -> KrausChannel: 33 | """Return the identity channel for entangling operations. 34 | 35 | Returns 36 | ------- 37 | KrausChannel 38 | Identity channel :math:`I_4`. 39 | """ 40 | return KrausChannel([KrausData(1.0, np.eye(4))]) 41 | 42 | @typing_extensions.override 43 | def measure(self) -> KrausChannel: 44 | """Return the identity channel for measurements. 45 | 46 | Returns 47 | ------- 48 | KrausChannel 49 | Identity channel :math:`I_2`. 50 | """ 51 | return KrausChannel([KrausData(1.0, np.eye(2))]) 52 | 53 | @typing_extensions.override 54 | def confuse_result(self, result: bool) -> bool: 55 | """Return the unmodified measurement result. 56 | 57 | Parameters 58 | ---------- 59 | result : bool 60 | Ideal measurement outcome. 61 | 62 | Returns 63 | ------- 64 | bool 65 | Same as ``result``. 66 | """ 67 | return result 68 | 69 | @typing_extensions.override 70 | def byproduct_x(self) -> KrausChannel: 71 | """Return the identity channel for X corrections. 72 | 73 | Returns 74 | ------- 75 | KrausChannel 76 | Identity channel :math:`I_2`. 77 | """ 78 | return KrausChannel([KrausData(1.0, np.eye(2))]) 79 | 80 | @typing_extensions.override 81 | def byproduct_z(self) -> KrausChannel: 82 | """Return the identity channel for Z corrections. 83 | 84 | Returns 85 | ------- 86 | KrausChannel 87 | Identity channel :math:`I_2`. 88 | """ 89 | return KrausChannel([KrausData(1.0, np.eye(2))]) 90 | 91 | @typing_extensions.override 92 | def clifford(self) -> KrausChannel: 93 | """Return the identity channel for Clifford gates. 94 | 95 | Returns 96 | ------- 97 | KrausChannel 98 | Identity channel :math:`I_2`. 99 | """ 100 | return KrausChannel([KrausData(1.0, np.eye(2))]) 101 | 102 | @typing_extensions.override 103 | def tick_clock(self) -> None: 104 | """Advance the simulator clock without applying errors. 105 | 106 | Notes 107 | ----- 108 | This method is present for API compatibility and does not modify the 109 | internal state. See 110 | :meth:`~graphix.noise_models.noise_model.NoiseModel.tick_clock`. 111 | """ 112 | -------------------------------------------------------------------------------- /graphix/opengraph.py: -------------------------------------------------------------------------------- 1 | """Provides a class for open graphs.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | from typing import TYPE_CHECKING 7 | 8 | import networkx as nx 9 | 10 | from graphix.generator import generate_from_graph 11 | from graphix.measurements import Measurement 12 | 13 | if TYPE_CHECKING: 14 | from graphix.pattern import Pattern 15 | 16 | 17 | @dataclass(frozen=True) 18 | class OpenGraph: 19 | """Open graph contains the graph, measurement, and input and output nodes. 20 | 21 | This is the graph we wish to implement deterministically. 22 | 23 | :param inside: the underlying :class:`networkx.Graph` state 24 | :param measurements: a dictionary whose key is the ID of a node and the 25 | value is the measurement at that node 26 | :param inputs: an ordered list of node IDs that are inputs to the graph 27 | :param outputs: an ordered list of node IDs that are outputs of the graph 28 | 29 | Example 30 | ------- 31 | >>> import networkx as nx 32 | >>> from graphix.fundamentals import Plane 33 | >>> from graphix.opengraph import OpenGraph, Measurement 34 | >>> 35 | >>> inside_graph = nx.Graph([(0, 1), (1, 2), (2, 0)]) 36 | >>> 37 | >>> measurements = {i: Measurement(0.5 * i, Plane.XY) for i in range(2)} 38 | >>> inputs = [0] 39 | >>> outputs = [2] 40 | >>> og = OpenGraph(inside_graph, measurements, inputs, outputs) 41 | """ 42 | 43 | inside: nx.Graph[int] 44 | measurements: dict[int, Measurement] 45 | inputs: list[int] # Inputs are ordered 46 | outputs: list[int] # Outputs are ordered 47 | 48 | def __post_init__(self) -> None: 49 | """Validate the open graph.""" 50 | if not all(node in self.inside.nodes for node in self.measurements): 51 | raise ValueError("All measured nodes must be part of the graph's nodes.") 52 | if not all(node in self.inside.nodes for node in self.inputs): 53 | raise ValueError("All input nodes must be part of the graph's nodes.") 54 | if not all(node in self.inside.nodes for node in self.outputs): 55 | raise ValueError("All output nodes must be part of the graph's nodes.") 56 | if any(node in self.outputs for node in self.measurements): 57 | raise ValueError("Output node cannot be measured.") 58 | if len(set(self.inputs)) != len(self.inputs): 59 | raise ValueError("Input nodes contain duplicates.") 60 | if len(set(self.outputs)) != len(self.outputs): 61 | raise ValueError("Output nodes contain duplicates.") 62 | 63 | def isclose(self, other: OpenGraph, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> bool: 64 | """Return `True` if two open graphs implement approximately the same unitary operator. 65 | 66 | Ensures the structure of the graphs are the same and all 67 | measurement angles are sufficiently close. 68 | 69 | This doesn't check they are equal up to an isomorphism. 70 | 71 | """ 72 | if not nx.utils.graphs_equal(self.inside, other.inside): # type: ignore[no-untyped-call] 73 | return False 74 | 75 | if self.inputs != other.inputs or self.outputs != other.outputs: 76 | return False 77 | 78 | if set(self.measurements.keys()) != set(other.measurements.keys()): 79 | return False 80 | 81 | return all( 82 | m.isclose(other.measurements[node], rel_tol=rel_tol, abs_tol=abs_tol) 83 | for node, m in self.measurements.items() 84 | ) 85 | 86 | @staticmethod 87 | def from_pattern(pattern: Pattern) -> OpenGraph: 88 | """Initialise an `OpenGraph` object based on the resource-state graph associated with the measurement pattern.""" 89 | g: nx.Graph[int] = nx.Graph() 90 | nodes, edges = pattern.get_graph() 91 | g.add_nodes_from(nodes) 92 | g.add_edges_from(edges) 93 | 94 | inputs = pattern.input_nodes 95 | outputs = pattern.output_nodes 96 | 97 | meas_planes = pattern.get_meas_plane() 98 | meas_angles = pattern.get_angles() 99 | meas = {node: Measurement(meas_angles[node], meas_planes[node]) for node in meas_angles} 100 | 101 | return OpenGraph(g, meas, inputs, outputs) 102 | 103 | def to_pattern(self) -> Pattern: 104 | """Convert the `OpenGraph` into a `Pattern`. 105 | 106 | Will raise an exception if the open graph does not have flow, gflow, or 107 | Pauli flow. 108 | The pattern will be generated using maximally-delayed flow. 109 | """ 110 | g = self.inside.copy() 111 | inputs = self.inputs 112 | outputs = self.outputs 113 | meas = self.measurements 114 | 115 | angles = {node: m.angle for node, m in meas.items()} 116 | planes = {node: m.plane for node, m in meas.items()} 117 | 118 | return generate_from_graph(g, angles, inputs, outputs, planes) 119 | -------------------------------------------------------------------------------- /graphix/pauli.py: -------------------------------------------------------------------------------- 1 | """Pauli gates ± {1,j} × {I, X, Y, Z}.""" # noqa: RUF002 2 | 3 | from __future__ import annotations 4 | 5 | import dataclasses 6 | from typing import TYPE_CHECKING, ClassVar 7 | 8 | import typing_extensions 9 | 10 | from graphix.fundamentals import IXYZ, Axis, ComplexUnit, SupportsComplexCtor 11 | from graphix.ops import Ops 12 | from graphix.states import BasicStates 13 | 14 | if TYPE_CHECKING: 15 | from collections.abc import Iterator 16 | 17 | import numpy as np 18 | import numpy.typing as npt 19 | 20 | from graphix.states import PlanarState 21 | 22 | 23 | class _PauliMeta(type): 24 | def __iter__(cls) -> Iterator[Pauli]: 25 | """Iterate over all Pauli gates, including the unit.""" 26 | return Pauli.iterate() 27 | 28 | 29 | @dataclasses.dataclass(frozen=True) 30 | class Pauli(metaclass=_PauliMeta): 31 | r"""Pauli gate: ``u * {I, X, Y, Z}`` where u is a complex unit. 32 | 33 | Pauli gates can be multiplied with other Pauli gates (with ``@``), 34 | with complex units and unit constants (with ``*``), 35 | and can be negated. 36 | """ 37 | 38 | symbol: IXYZ = IXYZ.I 39 | unit: ComplexUnit = ComplexUnit.ONE 40 | I: ClassVar[Pauli] 41 | X: ClassVar[Pauli] 42 | Y: ClassVar[Pauli] 43 | Z: ClassVar[Pauli] 44 | 45 | @staticmethod 46 | def from_axis(axis: Axis) -> Pauli: 47 | """Return the Pauli associated to the given axis.""" 48 | return Pauli(IXYZ[axis.name]) 49 | 50 | @property 51 | def axis(self) -> Axis: 52 | """Return the axis associated to the Pauli. 53 | 54 | Fails if the Pauli is identity. 55 | """ 56 | if self.symbol == IXYZ.I: 57 | raise ValueError("I is not an axis.") 58 | return Axis[self.symbol.name] 59 | 60 | @property 61 | def matrix(self) -> npt.NDArray[np.complex128]: 62 | """Return the matrix of the Pauli gate.""" 63 | co = complex(self.unit) 64 | if self.symbol == IXYZ.I: 65 | return co * Ops.I 66 | if self.symbol == IXYZ.X: 67 | return co * Ops.X 68 | if self.symbol == IXYZ.Y: 69 | return co * Ops.Y 70 | if self.symbol == IXYZ.Z: 71 | return co * Ops.Z 72 | typing_extensions.assert_never(self.symbol) 73 | 74 | def eigenstate(self, binary: int = 0) -> PlanarState: 75 | """Return the eigenstate of the Pauli.""" 76 | if binary not in {0, 1}: 77 | raise ValueError("b must be 0 or 1.") 78 | if self.symbol == IXYZ.X: 79 | return BasicStates.PLUS if binary == 0 else BasicStates.MINUS 80 | if self.symbol == IXYZ.Y: 81 | return BasicStates.PLUS_I if binary == 0 else BasicStates.MINUS_I 82 | if self.symbol == IXYZ.Z: 83 | return BasicStates.ZERO if binary == 0 else BasicStates.ONE 84 | # Any state is eigenstate of the identity 85 | if self.symbol == IXYZ.I: 86 | return BasicStates.PLUS 87 | typing_extensions.assert_never(self.symbol) 88 | 89 | def _repr_impl(self, prefix: str | None) -> str: 90 | """Return ``repr`` string with an optional prefix.""" 91 | sym = self.symbol.name 92 | if prefix is not None: 93 | sym = f"{prefix}.{sym}" 94 | if self.unit == ComplexUnit.ONE: 95 | return sym 96 | if self.unit == ComplexUnit.MINUS_ONE: 97 | return f"-{sym}" 98 | if self.unit == ComplexUnit.J: 99 | return f"1j * {sym}" 100 | if self.unit == ComplexUnit.MINUS_J: 101 | return f"-1j * {sym}" 102 | typing_extensions.assert_never(self.unit) 103 | 104 | def __repr__(self) -> str: 105 | """Return a string representation of the Pauli.""" 106 | return self._repr_impl(self.__class__.__name__) 107 | 108 | def __str__(self) -> str: 109 | """Return a simplified string representation of the Pauli.""" 110 | return self._repr_impl(None) 111 | 112 | @staticmethod 113 | def _matmul_impl(lhs: IXYZ, rhs: IXYZ) -> Pauli: 114 | """Return the product of ``lhs`` and ``rhs`` ignoring units.""" 115 | if lhs == IXYZ.I: 116 | return Pauli(rhs) 117 | if rhs == IXYZ.I: 118 | return Pauli(lhs) 119 | if lhs == rhs: 120 | return Pauli() 121 | lr = (lhs, rhs) 122 | if lr == (IXYZ.X, IXYZ.Y): 123 | return Pauli(IXYZ.Z, ComplexUnit.J) 124 | if lr == (IXYZ.Y, IXYZ.X): 125 | return Pauli(IXYZ.Z, ComplexUnit.MINUS_J) 126 | if lr == (IXYZ.Y, IXYZ.Z): 127 | return Pauli(IXYZ.X, ComplexUnit.J) 128 | if lr == (IXYZ.Z, IXYZ.Y): 129 | return Pauli(IXYZ.X, ComplexUnit.MINUS_J) 130 | if lr == (IXYZ.Z, IXYZ.X): 131 | return Pauli(IXYZ.Y, ComplexUnit.J) 132 | if lr == (IXYZ.X, IXYZ.Z): 133 | return Pauli(IXYZ.Y, ComplexUnit.MINUS_J) 134 | raise RuntimeError("Unreachable.") # pragma: no cover 135 | 136 | def __matmul__(self, other: Pauli) -> Pauli: 137 | """Return the product of two Paulis.""" 138 | if isinstance(other, Pauli): 139 | return self._matmul_impl(self.symbol, other.symbol) * (self.unit * other.unit) 140 | return NotImplemented 141 | 142 | def __mul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: 143 | """Return the product of two Paulis.""" 144 | if u := ComplexUnit.try_from(other): 145 | return dataclasses.replace(self, unit=self.unit * u) 146 | return NotImplemented 147 | 148 | def __rmul__(self, other: ComplexUnit | SupportsComplexCtor) -> Pauli: 149 | """Return the product of two Paulis.""" 150 | return self.__mul__(other) 151 | 152 | def __neg__(self) -> Pauli: 153 | """Return the opposite.""" 154 | return dataclasses.replace(self, unit=-self.unit) 155 | 156 | @staticmethod 157 | def iterate(symbol_only: bool = False) -> Iterator[Pauli]: 158 | """Iterate over all Pauli gates. 159 | 160 | Parameters 161 | ---------- 162 | symbol_only (bool, optional): Exclude the unit in the iteration. Defaults to False. 163 | """ 164 | us = (ComplexUnit.ONE,) if symbol_only else tuple(ComplexUnit) 165 | for symbol in IXYZ: 166 | for unit in us: 167 | yield Pauli(symbol, unit) 168 | 169 | 170 | Pauli.I = Pauli(IXYZ.I) 171 | Pauli.X = Pauli(IXYZ.X) 172 | Pauli.Y = Pauli(IXYZ.Y) 173 | Pauli.Z = Pauli(IXYZ.Z) 174 | -------------------------------------------------------------------------------- /graphix/rng.py: -------------------------------------------------------------------------------- 1 | """Provide a default random-number generator if `None` is given.""" 2 | 3 | from __future__ import annotations 4 | 5 | import threading 6 | from typing import TYPE_CHECKING 7 | 8 | import numpy as np 9 | 10 | if TYPE_CHECKING: 11 | from numpy.random import Generator 12 | 13 | _rng_local = threading.local() 14 | 15 | 16 | def ensure_rng(rng: Generator | None = None) -> Generator: 17 | """Return a default random-number generator if `None` is given.""" 18 | if rng is not None: 19 | return rng 20 | stored: Generator | None = getattr(_rng_local, "rng", None) 21 | if stored is not None: 22 | return stored 23 | rng = np.random.default_rng() 24 | # MEMO: Cannot perform type check 25 | setattr(_rng_local, "rng", rng) # noqa: B010 26 | return rng 27 | -------------------------------------------------------------------------------- /graphix/sim/__init__.py: -------------------------------------------------------------------------------- 1 | """Simulation backends.""" 2 | -------------------------------------------------------------------------------- /graphix/states.py: -------------------------------------------------------------------------------- 1 | """Quantum states and operators.""" 2 | 3 | from __future__ import annotations 4 | 5 | import abc 6 | import dataclasses 7 | from abc import ABC 8 | from typing import ClassVar 9 | 10 | import numpy as np 11 | import numpy.typing as npt 12 | import typing_extensions 13 | 14 | from graphix.fundamentals import Plane 15 | 16 | 17 | # generic class State for all States 18 | # FIXME: Name conflict 19 | class State(ABC): 20 | """Abstract base class for single qubit states objects. 21 | 22 | Only requirement for concrete classes is to have 23 | a get_statevector() method that returns the statevector 24 | representation of the state 25 | """ 26 | 27 | @abc.abstractmethod 28 | def get_statevector(self) -> npt.NDArray[np.complex128]: 29 | """Return the state vector.""" 30 | 31 | def get_densitymatrix(self) -> npt.NDArray[np.complex128]: 32 | """Return the density matrix.""" 33 | # return DM in 2**n x 2**n dim (2x2 here) 34 | return np.outer(self.get_statevector(), self.get_statevector().conj()).astype(np.complex128, copy=False) 35 | 36 | 37 | @dataclasses.dataclass 38 | class PlanarState(State): 39 | """Light object used to instantiate backends. 40 | 41 | doesn't cover all possible states but this is 42 | covered in :class:`graphix.sim.statevec.Statevec` 43 | and :class:`graphix.sim.densitymatrix.DensityMatrix` 44 | constructors. 45 | 46 | :param plane: One of the three planes (XY, XZ, YZ) 47 | :type plane: :class:`graphix.pauli.Plane` 48 | :param angle: angle IN RADIANS 49 | :type angle: complex 50 | :return: State 51 | :rtype: :class:`graphix.states.State` object 52 | """ 53 | 54 | plane: Plane 55 | angle: float 56 | 57 | def __repr__(self) -> str: 58 | """Return a string representation of the planar state.""" 59 | return f"graphix.states.PlanarState({self.plane}, {self.angle})" 60 | 61 | def __str__(self) -> str: 62 | """Return a string description of the planar state.""" 63 | return f"PlanarState object defined in plane {self.plane} with angle {self.angle}." 64 | 65 | def get_statevector(self) -> npt.NDArray[np.complex128]: 66 | """Return the state vector.""" 67 | if self.plane == Plane.XY: 68 | return np.asarray([1 / np.sqrt(2), np.exp(1j * self.angle) / np.sqrt(2)], dtype=np.complex128) 69 | 70 | if self.plane == Plane.YZ: 71 | return np.asarray([np.cos(self.angle / 2), 1j * np.sin(self.angle / 2)], dtype=np.complex128) 72 | 73 | if self.plane == Plane.XZ: 74 | return np.asarray([np.cos(self.angle / 2), np.sin(self.angle / 2)], dtype=np.complex128) 75 | # other case never happens since exhaustive 76 | typing_extensions.assert_never(self.plane) 77 | 78 | 79 | # States namespace for input initialization. 80 | class BasicStates: 81 | """Basic states.""" 82 | 83 | ZERO: ClassVar[PlanarState] = PlanarState(Plane.XZ, 0) 84 | ONE: ClassVar[PlanarState] = PlanarState(Plane.XZ, np.pi) 85 | PLUS: ClassVar[PlanarState] = PlanarState(Plane.XY, 0) 86 | MINUS: ClassVar[PlanarState] = PlanarState(Plane.XY, np.pi) 87 | PLUS_I: ClassVar[PlanarState] = PlanarState(Plane.XY, np.pi / 2) 88 | MINUS_I: ClassVar[PlanarState] = PlanarState(Plane.XY, -np.pi / 2) 89 | # remove that in the end 90 | # need in TN backend 91 | VEC: ClassVar[list[PlanarState]] = [PLUS, MINUS, ZERO, ONE, PLUS_I, MINUS_I] 92 | -------------------------------------------------------------------------------- /graphix/utils.py: -------------------------------------------------------------------------------- 1 | """Utilities.""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | import typing 7 | from typing import TYPE_CHECKING, Any, ClassVar, Literal, SupportsInt, TypeVar 8 | 9 | import numpy as np 10 | import numpy.typing as npt 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Iterable, Iterator 14 | 15 | _T = TypeVar("_T") 16 | 17 | 18 | def check_list_elements(l: Iterable[_T], ty: type[_T]) -> None: 19 | """Check that every element of the list has the given type.""" 20 | for index, item in enumerate(l): 21 | if not isinstance(item, ty): 22 | raise TypeError(f"data[{index}] has type {type(item)} whereas {ty} is expected") 23 | 24 | 25 | def check_kind(cls: type, scope: dict[str, Any]) -> None: 26 | """Check that the class has a kind attribute.""" 27 | if not hasattr(cls, "kind"): 28 | msg = f"{cls.__name__} must have a tag attribute named kind." 29 | raise TypeError(msg) 30 | if sys.version_info < (3, 10): 31 | # MEMO: `inspect.get_annotations` unavailable 32 | return 33 | 34 | import inspect 35 | 36 | ann = inspect.get_annotations(cls, eval_str=True, locals=scope).get("kind") 37 | if ann is None: 38 | msg = "kind must be annotated." 39 | raise TypeError(msg) 40 | if typing.get_origin(ann) is not ClassVar: 41 | msg = "Tag attribute must be a class variable." 42 | raise TypeError(msg) 43 | (ann,) = typing.get_args(ann) 44 | if typing.get_origin(ann) is not Literal: 45 | msg = "Tag attribute must be a literal." 46 | raise TypeError(msg) 47 | 48 | 49 | def is_integer(value: SupportsInt) -> bool: 50 | """Return `True` if `value` is an integer, `False` otherwise.""" 51 | return value == int(value) 52 | 53 | 54 | G = TypeVar("G", bound=np.generic) 55 | 56 | 57 | @typing.overload 58 | def lock(data: npt.NDArray[Any]) -> npt.NDArray[np.complex128]: ... 59 | 60 | 61 | @typing.overload 62 | def lock(data: npt.NDArray[Any], dtype: type[G]) -> npt.NDArray[G]: ... 63 | 64 | 65 | def lock(data: npt.NDArray[Any], dtype: type = np.complex128) -> npt.NDArray[Any]: 66 | """Create a true immutable view. 67 | 68 | data must not have aliasing references, otherwise users can still turn on writeable flag of m. 69 | """ 70 | m: npt.NDArray[Any] = data.astype(dtype) 71 | m.flags.writeable = False 72 | v = m.view() 73 | assert not v.flags.writeable 74 | return v 75 | 76 | 77 | def iter_empty(it: Iterator[_T]) -> bool: 78 | """Check if an iterable is empty. 79 | 80 | Notes 81 | ----- 82 | This function consumes the iterator. 83 | """ 84 | return all(False for _ in it) 85 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Run tests with nox.""" 2 | 3 | from __future__ import annotations 4 | 5 | import nox 6 | from nox import Session 7 | 8 | 9 | @nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"]) 10 | def tests_minimal(session: Session) -> None: 11 | """Run the test suite with minimal dependencies.""" 12 | session.install("-e", ".") 13 | session.install("pytest", "pytest-mock", "psutil") 14 | session.run("pytest") 15 | 16 | 17 | @nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"]) 18 | def tests(session: Session) -> None: 19 | """Run the test suite with full dependencies.""" 20 | session.install("-e", ".[dev,extra]") 21 | session.run("pytest", "--doctest-modules") 22 | 23 | 24 | # TODO: Add 3.13 CI 25 | @nox.session(python=["3.9", "3.10", "3.11", "3.12"]) 26 | def tests_symbolic(session: Session) -> None: 27 | """Run the test suite of graphix-symbolic.""" 28 | session.install("-e", ".[dev]") 29 | # If you need a specific branch: 30 | # session.run("git", "clone", "-b", "branch-name", "https://github.com/TeamGraphix/graphix-symbolic") 31 | session.run("git", "clone", "https://github.com/TeamGraphix/graphix-symbolic") 32 | session.cd("graphix-symbolic") 33 | session.run("pytest") 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77", "wheel", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "graphix" 7 | authors = [{ name = "Shinichi Sunami", email = "shinichi.sunami@gmail.com" }] 8 | maintainers = [ 9 | { name = "Shinichi Sunami", email = "shinichi.sunami@gmail.com" }, 10 | ] 11 | license-files = ["LICENSE"] 12 | description = "Optimize and simulate measurement-based quantum computation" 13 | readme = "README.md" 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Environment :: Console", 17 | "Intended Audience :: Science/Research", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Operating System :: OS Independent", 24 | "Topic :: Scientific/Engineering :: Physics", 25 | ] 26 | requires-python = ">=3.9,<3.14" 27 | dynamic = ["version", "dependencies", "optional-dependencies"] 28 | 29 | [project.urls] 30 | Documentation = "https://graphix.readthedocs.io" 31 | "Bug Tracker" = "https://github.com/TeamGraphix/graphix/issues" 32 | 33 | [tool.setuptools_scm] 34 | version_file = "graphix/_version.py" 35 | 36 | [tool.setuptools.dynamic] 37 | dependencies = { file = ["requirements.txt"] } 38 | 39 | [tool.setuptools.dynamic.optional-dependencies] 40 | dev = { file = ["requirements-dev.txt"] } 41 | extra = { file = ["requirements-extra.txt"] } 42 | doc = { file = ["requirements-doc.txt"] } 43 | 44 | [tool.ruff] 45 | line-length = 120 46 | extend-exclude = ["docs"] 47 | 48 | [tool.ruff.lint] 49 | preview = true 50 | select = ["ALL"] 51 | extend-ignore = [ 52 | "C90", # Complexity 53 | "E74", # Ambiguous name 54 | "ERA", # Commmented out code 55 | "FBT", # Boolean positional arguments 56 | "FIX", # Fixme 57 | "PLR091", # Too many XXX 58 | "PLR0904", # Too many public methods 59 | "PLR2004", # Magic vavlue comparison 60 | "S101", # assert 61 | "T20", # print 62 | "TD", # Todo 63 | 64 | # Tentative ignores 65 | "ANN", # Missing annotations 66 | "CPY", # Copyright 67 | "DOC", # Docstring 68 | "E501", # Line too long 69 | "EM10", # Raise string 70 | "PLC0415", # Import not at the top level 71 | "PLR1702", # Too many nests 72 | "PLW1641", # __hash__ missing 73 | "PT011", # pytest raises too broad 74 | "SLF001", # Private access 75 | "TRY003", # Raise vanilla args 76 | 77 | # Conflicts with ruff format 78 | "COM812", 79 | "COM819", 80 | "D206", 81 | "D300", 82 | "E111", 83 | "E114", 84 | "E117", 85 | "ISC001", 86 | "ISC002", 87 | "Q000", 88 | "Q001", 89 | "Q002", 90 | "Q003", 91 | "W191", 92 | ] 93 | # Allow "α" (U+03B1 GREEK SMALL LETTER ALPHA) which could be confused for "a" 94 | allowed-confusables = ["α"] 95 | 96 | [tool.ruff.format] 97 | docstring-code-format = true 98 | 99 | [tool.ruff.lint.extend-per-file-ignores] 100 | "benchmarks/*.py" = [ 101 | "D", # Benchmarks follow Sphinx doc conventions 102 | "INP", # Missing __init__.py 103 | ] 104 | "examples/*.py" = [ 105 | "ARG", # Unused arguments 106 | "B018", # Useless expression 107 | "D", # Examples follow Sphinx doc conventions 108 | "E402", # Import not at top of file 109 | "INP", # Missing __init__.py 110 | ] 111 | "tests/*.py" = [ 112 | "D10", # Allow undocumented items 113 | "PLC2701", # Allow private imports 114 | "PLR6301", # self not used 115 | ] 116 | 117 | [tool.ruff.lint.pydocstyle] 118 | convention = "numpy" 119 | 120 | [tool.ruff.lint.flake8-tidy-imports] 121 | ban-relative-imports = "all" 122 | 123 | [tool.ruff.lint.isort] 124 | required-imports = ["from __future__ import annotations"] 125 | 126 | [tool.pytest.ini_options] 127 | addopts = ["--ignore=examples", "--ignore=docs", "--ignore=benchmarks"] 128 | # Silence cotengra warning 129 | filterwarnings = ["ignore:Couldn't import `kahypar`"] 130 | 131 | [tool.mypy] 132 | # Keep in sync with pyright 133 | files = ["*.py", "examples", "graphix", "tests"] 134 | exclude = [ 135 | '^examples/deutsch_jozsa\.py$', 136 | '^examples/ghz_with_tn\.py$', 137 | '^examples/mbqc_vqe\.py$', 138 | '^examples/qaoa\.py$', 139 | '^examples/qft_with_tn\.py$', 140 | '^examples/qnn\.py$', 141 | '^examples/rotation\.py$', 142 | '^examples/tn_simulation\.py$', 143 | '^examples/visualization\.py$', 144 | '^graphix/device_interface\.py$', 145 | '^graphix/gflow\.py$', 146 | '^graphix/linalg\.py$', 147 | '^graphix/pattern\.py$', 148 | '^graphix/random_objects\.py$', 149 | '^graphix/simulator\.py$', 150 | '^graphix/sim/base_backend\.py$', 151 | '^graphix/sim/density_matrix\.py$', 152 | '^graphix/sim/statevec\.py$', 153 | '^graphix/sim/tensornet\.py$', 154 | '^graphix/transpiler\.py$', 155 | '^graphix/visualization\.py$', 156 | '^tests/test_density_matrix\.py$', 157 | '^tests/test_gflow\.py$', 158 | '^tests/test_linalg\.py$', 159 | '^tests/test_noisy_density_matrix\.py$', 160 | '^tests/test_pattern\.py$', 161 | '^tests/test_random_utilities\.py$', 162 | '^tests/test_runner\.py$', 163 | '^tests/test_statevec\.py$', 164 | '^tests/test_statevec_backend\.py$', 165 | '^tests/test_tnsim\.py$', 166 | '^tests/test_transpiler\.py$', 167 | '^tests/test_visualization\.py$', 168 | ] 169 | follow_imports = "silent" 170 | follow_untyped_imports = true # required for qiskit, requires mypy >=1.14 171 | strict = true 172 | 173 | [tool.pyright] 174 | # Keep in sync with mypy 175 | include = ["*.py", "examples", "graphix", "tests"] 176 | exclude = [ 177 | "examples/deutsch_jozsa.py", 178 | "examples/ghz_with_tn.py", 179 | "examples/mbqc_vqe.py", 180 | "examples/qaoa.py", 181 | "examples/qft_with_tn.py", 182 | "examples/qnn.py", 183 | "examples/rotation.py", 184 | "examples/tn_simulation.py", 185 | "examples/visualization.py", 186 | "graphix/device_interface.py", 187 | "graphix/gflow.py", 188 | "graphix/linalg.py", 189 | "graphix/pattern.py", 190 | "graphix/random_objects.py", 191 | "graphix/simulator.py", 192 | "graphix/sim/base_backend.py", 193 | "graphix/sim/density_matrix.py", 194 | "graphix/sim/statevec.py", 195 | "graphix/sim/tensornet.py", 196 | "graphix/transpiler.py", 197 | "graphix/visualization.py", 198 | "tests/test_density_matrix.py", 199 | "tests/test_gflow.py", 200 | "tests/test_linalg.py", 201 | "tests/test_noisy_density_matrix.py", 202 | "tests/test_pattern.py", 203 | "tests/test_random_utilities.py", 204 | "tests/test_runner.py", 205 | "tests/test_statevec.py", 206 | "tests/test_statevec_backend.py", 207 | "tests/test_tnsim.py", 208 | "tests/test_transpiler.py", 209 | "tests/test_visualization.py", 210 | ] 211 | 212 | [tool.coverage.report] 213 | exclude_also = [ 214 | "if TYPE_CHECKING:", 215 | "raise NotImplementedError\\(.*\\)", 216 | "return NotImplemented", 217 | "typing_extensions.assert_never\\(.*\\)", 218 | "@abc.abstractmethod", 219 | ] 220 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Lint/format 2 | mypy 3 | pre-commit # for language-agnostic hooks 4 | pyright 5 | ruff==0.11.13 6 | 7 | # Stubs 8 | types-networkx 9 | types-psutil 10 | types-setuptools 11 | 12 | # Tests 13 | # Keep in sync with CI 14 | nox==2025.5.1 15 | psutil 16 | pytest 17 | pytest-cov 18 | pytest-mock 19 | 20 | # Optional dependencies 21 | qiskit>=1.0 22 | qiskit-aer 23 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | furo 2 | joblib # For parallel builds 3 | matplotlib 4 | sphinx 5 | sphinx-gallery 6 | -------------------------------------------------------------------------------- /requirements-extra.txt: -------------------------------------------------------------------------------- 1 | #graphix-ibmq 2 | #graphix-perceval 3 | pyzx==0.9.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | galois 2 | matplotlib 3 | networkx 4 | numpy>=2,<3 5 | opt_einsum 6 | quimb 7 | scipy 8 | sympy 9 | typing_extensions 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Blank setup.py for backward compatibility.""" 2 | 3 | from __future__ import annotations 4 | 5 | from setuptools import setup 6 | 7 | setup() 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamGraphix/graphix/1dc02d77138b7ed718d667e96b3aa2a116721dfe/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | import psutil 7 | 8 | if os.environ.get("NUMBA_NUM_THREADS") is None: 9 | # Prevent quimb from overwriting 10 | # Need to set as soon as possible 11 | os.environ["NUMBA_NUM_THREADS"] = f"{psutil.cpu_count(logical=False)}" 12 | 13 | import pytest 14 | from numpy.random import PCG64, Generator 15 | 16 | from graphix.random_objects import rand_circuit 17 | from graphix.transpiler import Circuit 18 | 19 | if TYPE_CHECKING: 20 | from graphix.pattern import Pattern 21 | 22 | SEED = 25 23 | DEPTH = 1 24 | 25 | 26 | @pytest.fixture 27 | def fx_rng() -> Generator: 28 | return Generator(PCG64(SEED)) 29 | 30 | 31 | @pytest.fixture 32 | def fx_bg() -> PCG64: 33 | return PCG64(SEED) 34 | 35 | 36 | @pytest.fixture 37 | def hadamardpattern() -> Pattern: 38 | circ = Circuit(1) 39 | circ.h(0) 40 | return circ.transpile().pattern 41 | 42 | 43 | @pytest.fixture 44 | def nqb(fx_rng: Generator) -> int: 45 | return int(fx_rng.integers(2, 5)) 46 | 47 | 48 | @pytest.fixture 49 | def rand_circ(nqb: int, fx_rng: Generator) -> Circuit: 50 | return rand_circuit(nqb, DEPTH, rng=fx_rng) 51 | 52 | 53 | @pytest.fixture 54 | def randpattern(rand_circ: Circuit) -> Pattern: 55 | return rand_circ.transpile().pattern 56 | -------------------------------------------------------------------------------- /tests/test_clifford.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import cmath 4 | import functools 5 | import itertools 6 | import math 7 | import operator 8 | import re 9 | from typing import TYPE_CHECKING, Final 10 | 11 | import numpy as np 12 | import pytest 13 | 14 | from graphix.clifford import Clifford 15 | from graphix.fundamentals import IXYZ, ComplexUnit, Sign 16 | from graphix.pauli import Pauli 17 | 18 | if TYPE_CHECKING: 19 | from numpy.random import Generator 20 | 21 | _QASM3_DB: Final = { 22 | "id": Clifford.I, 23 | "x": Clifford.X, 24 | "y": Clifford.Y, 25 | "z": Clifford.Z, 26 | "s": Clifford.S, 27 | "sdg": Clifford.SDG, 28 | "h": Clifford.H, 29 | } 30 | 31 | 32 | class TestClifford: 33 | def test_named(self) -> None: 34 | assert hasattr(Clifford, "I") 35 | assert hasattr(Clifford, "X") 36 | assert hasattr(Clifford, "Y") 37 | assert hasattr(Clifford, "Z") 38 | assert hasattr(Clifford, "S") 39 | assert hasattr(Clifford, "H") 40 | 41 | def test_iteration(self) -> None: 42 | """Test that Clifford iteration does not take (I, X, Y, Z, S, H) into account.""" 43 | assert len(Clifford) == 24 44 | assert len(frozenset(Clifford)) == 24 45 | 46 | @pytest.mark.parametrize("c", Clifford) 47 | def test_repr(self, c: Clifford) -> None: 48 | rep: str = repr(c) 49 | m = re.match(r"\((.*)\)", rep) 50 | rep = m.group(1) if m is not None else rep 51 | for term in rep.split(" @ "): 52 | assert term in { 53 | "Clifford.I", 54 | "Clifford.H", 55 | "Clifford.S", 56 | "Clifford.Z", 57 | } 58 | 59 | @pytest.mark.parametrize( 60 | ("c", "p"), 61 | itertools.product( 62 | Clifford, 63 | ( 64 | Pauli(sym, u) 65 | for sym in IXYZ 66 | for u in ( 67 | ComplexUnit.from_properties(sign=Sign.PLUS, is_imag=False), 68 | ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=False), 69 | ComplexUnit.from_properties(sign=Sign.PLUS, is_imag=True), 70 | ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=True), 71 | ) 72 | ), 73 | ), 74 | ) 75 | def test_measure(self, c: Clifford, p: Pauli) -> None: 76 | cm = c.matrix 77 | pm = p.matrix 78 | cpc = c.measure(p) 79 | if c == Clifford.I: 80 | # Prevent aliasing 81 | assert cpc is not p 82 | assert np.allclose(cpc.matrix, cm.conj().T @ pm @ cm) 83 | 84 | @pytest.mark.parametrize("c", Clifford) 85 | def test_qasm3(self, c: Clifford) -> None: 86 | cmul: Clifford = functools.reduce(operator.matmul, (_QASM3_DB[term] for term in reversed(c.qasm3))) 87 | assert cmul == c 88 | 89 | @pytest.mark.parametrize("c", Clifford) 90 | def test_try_from_matrix(self, fx_rng: Generator, c: Clifford) -> None: 91 | co = cmath.exp(2j * math.pi * fx_rng.uniform()) 92 | assert Clifford.try_from_matrix(co * c.matrix) == c 93 | 94 | def test_try_from_matrix_ng(self, fx_rng: Generator) -> None: 95 | assert Clifford.try_from_matrix(np.zeros((2, 3))) is None 96 | assert Clifford.try_from_matrix(fx_rng.normal(size=(2, 2))) is None 97 | -------------------------------------------------------------------------------- /tests/test_command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import math 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from graphix.clifford import Clifford 10 | from graphix.command import MeasureUpdate 11 | from graphix.fundamentals import Plane 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("plane", "s", "t", "clifford", "angle", "choice"), 16 | itertools.product( 17 | Plane, 18 | (False, True), 19 | (False, True), 20 | Clifford, 21 | (0, math.pi), 22 | (False, True), 23 | ), 24 | ) 25 | def test_measure_update( 26 | plane: Plane, 27 | s: bool, 28 | t: bool, 29 | clifford: Clifford, 30 | angle: float, 31 | choice: bool, 32 | ) -> None: 33 | measure_update = MeasureUpdate.compute(plane, s, t, clifford) 34 | new_angle = angle * measure_update.coeff + measure_update.add_term 35 | vec = measure_update.new_plane.polar(new_angle) 36 | op_mat = np.eye(2, dtype=np.complex128) / 2 37 | for i in range(3): 38 | op_mat += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 39 | 40 | if s: 41 | clifford = Clifford.X @ clifford 42 | if t: 43 | clifford = Clifford.Z @ clifford 44 | vec = plane.polar(angle) 45 | op_mat_ref = np.eye(2, dtype=np.complex128) / 2 46 | for i in range(3): 47 | op_mat_ref += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 48 | clifford_mat = clifford.matrix 49 | op_mat_ref = clifford_mat.conj().T @ op_mat_ref @ clifford_mat 50 | 51 | assert np.allclose(op_mat, op_mat_ref) or np.allclose(op_mat, -op_mat_ref) 52 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from graphix._db import ( 9 | CLIFFORD, 10 | CLIFFORD_CONJ, 11 | CLIFFORD_HSZ_DECOMPOSITION, 12 | CLIFFORD_MEASURE, 13 | CLIFFORD_MUL, 14 | ) 15 | from graphix.clifford import Clifford 16 | 17 | 18 | class TestCliffordDB: 19 | @pytest.mark.parametrize(("i", "j"), itertools.product(range(24), range(3))) 20 | def test_measure(self, i: int, j: int) -> None: 21 | pauli = CLIFFORD[j + 1] 22 | arr = CLIFFORD[i].conjugate().T @ pauli @ CLIFFORD[i] 23 | sym, sgn = CLIFFORD_MEASURE[i][j] 24 | arr_ = complex(sgn) * sym.matrix 25 | assert np.allclose(arr, arr_) 26 | 27 | @pytest.mark.parametrize(("i", "j"), itertools.product(range(24), range(24))) 28 | def test_multiplication(self, i: int, j: int) -> None: 29 | op = CLIFFORD[i] @ CLIFFORD[j] 30 | assert Clifford.try_from_matrix(op) == Clifford(CLIFFORD_MUL[i][j]) 31 | 32 | @pytest.mark.parametrize("i", range(24)) 33 | def test_conjugation(self, i: int) -> None: 34 | op = CLIFFORD[i].conjugate().T 35 | assert Clifford.try_from_matrix(op) == Clifford(CLIFFORD_CONJ[i]) 36 | 37 | @pytest.mark.parametrize("i", range(24)) 38 | def test_decomposition(self, i: int) -> None: 39 | op = np.eye(2, dtype=np.complex128) 40 | for j in CLIFFORD_HSZ_DECOMPOSITION[i]: 41 | op @= CLIFFORD[j] 42 | assert Clifford.try_from_matrix(op) == Clifford(i) 43 | 44 | @pytest.mark.parametrize("i", range(24)) 45 | def test_safety(self, i: int) -> None: 46 | with pytest.raises(TypeError): 47 | # Cannot replace 48 | CLIFFORD[i] = np.eye(2) # type: ignore[index] 49 | m = CLIFFORD[i] 50 | with pytest.raises(ValueError): 51 | # Cannot modify 52 | m[0, 0] = 42 53 | with pytest.raises(ValueError): 54 | # Cannot make it writeable 55 | m.flags.writeable = True 56 | v = m.view() 57 | with pytest.raises(ValueError): 58 | # Cannot create writeable view 59 | v.flags.writeable = True 60 | -------------------------------------------------------------------------------- /tests/test_extraction.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from graphix import extraction 4 | from graphix.graphsim import GraphState 5 | 6 | 7 | class TestExtraction: 8 | def test_cluster_extraction_one_ghz_cluster(self) -> None: 9 | gs = GraphState() 10 | nodes = [0, 1, 2, 3, 4] 11 | edges = [(0, 1), (0, 2), (0, 3), (0, 4)] 12 | gs.add_nodes_from(nodes) 13 | gs.add_edges_from(edges) 14 | clusters = extraction.get_fusion_network_from_graph(gs) 15 | 16 | assert len(clusters) == 1 17 | assert clusters[0] == extraction.ResourceGraph(cltype=extraction.ResourceType.GHZ, graph=gs) 18 | 19 | # we consider everything smaller than 4, a GHZ 20 | def test_cluster_extraction_small_ghz_cluster_1(self) -> None: 21 | gs = GraphState() 22 | nodes = [0, 1, 2] 23 | edges = [(0, 1), (1, 2)] 24 | gs.add_nodes_from(nodes) 25 | gs.add_edges_from(edges) 26 | clusters = extraction.get_fusion_network_from_graph(gs) 27 | 28 | assert len(clusters) == 1 29 | assert clusters[0] == extraction.ResourceGraph(cltype=extraction.ResourceType.GHZ, graph=gs) 30 | 31 | # we consider everything smaller than 4, a GHZ 32 | def test_cluster_extraction_small_ghz_cluster_2(self) -> None: 33 | gs = GraphState() 34 | nodes = [0, 1] 35 | edges = [(0, 1)] 36 | gs.add_nodes_from(nodes) 37 | gs.add_edges_from(edges) 38 | clusters = extraction.get_fusion_network_from_graph(gs) 39 | 40 | assert len(clusters) == 1 41 | assert clusters[0] == extraction.ResourceGraph(cltype=extraction.ResourceType.GHZ, graph=gs) 42 | 43 | def test_cluster_extraction_one_linear_cluster(self) -> None: 44 | gs = GraphState() 45 | nodes = [0, 1, 2, 3, 4, 5, 6] 46 | edges = [(0, 1), (1, 2), (2, 3), (5, 4), (4, 6), (6, 0)] 47 | gs.add_nodes_from(nodes) 48 | gs.add_edges_from(edges) 49 | clusters = extraction.get_fusion_network_from_graph(gs) 50 | 51 | assert len(clusters) == 1 52 | assert clusters[0] == extraction.ResourceGraph(cltype=extraction.ResourceType.LINEAR, graph=gs) 53 | 54 | def test_cluster_extraction_one_ghz_one_linear(self) -> None: 55 | gs = GraphState() 56 | nodes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 57 | edges = [(0, 1), (0, 2), (0, 3), (0, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)] 58 | gs.add_nodes_from(nodes) 59 | gs.add_edges_from(edges) 60 | clusters = extraction.get_fusion_network_from_graph(gs) 61 | assert len(clusters) == 2 62 | 63 | clusters_expected = [] 64 | lin_cluster = GraphState() 65 | lin_cluster.add_nodes_from([4, 5, 6, 7, 8, 9]) 66 | lin_cluster.add_edges_from([(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) 67 | clusters_expected.append(extraction.ResourceGraph(extraction.ResourceType.LINEAR, lin_cluster)) 68 | ghz_cluster = GraphState() 69 | ghz_cluster.add_nodes_from([0, 1, 2, 3, 4]) 70 | ghz_cluster.add_edges_from([(0, 1), (0, 2), (0, 3), (0, 4)]) 71 | clusters_expected.append(extraction.ResourceGraph(extraction.ResourceType.GHZ, ghz_cluster)) 72 | 73 | assert (clusters[0] == clusters_expected[0] and clusters[1] == clusters_expected[1]) or ( 74 | clusters[0] == clusters_expected[1] and clusters[1] == clusters_expected[0] 75 | ) 76 | 77 | def test_cluster_extraction_pentagonal_cluster(self) -> None: 78 | gs = GraphState() 79 | nodes = [0, 1, 2, 3, 4] 80 | edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] 81 | gs.add_nodes_from(nodes) 82 | gs.add_edges_from(edges) 83 | clusters = extraction.get_fusion_network_from_graph(gs) 84 | assert len(clusters) == 2 85 | assert ( 86 | clusters[0].cltype == extraction.ResourceType.GHZ and clusters[1].cltype == extraction.ResourceType.LINEAR 87 | ) or ( 88 | clusters[0].cltype == extraction.ResourceType.LINEAR and clusters[1].cltype == extraction.ResourceType.GHZ 89 | ) 90 | assert (len(clusters[0].graph.nodes) == 3 and len(clusters[1].graph.nodes) == 4) or ( 91 | len(clusters[0].graph.nodes) == 4 and len(clusters[1].graph.nodes) == 3 92 | ) 93 | 94 | def test_cluster_extraction_one_plus_two(self) -> None: 95 | gs = GraphState() 96 | nodes = [0, 1, 2] 97 | edges = [(0, 1)] 98 | gs.add_nodes_from(nodes) 99 | gs.add_edges_from(edges) 100 | clusters = extraction.get_fusion_network_from_graph(gs) 101 | assert len(clusters) == 2 102 | assert clusters[0].cltype == extraction.ResourceType.GHZ 103 | assert clusters[1].cltype == extraction.ResourceType.GHZ 104 | assert (len(clusters[0].graph.nodes) == 2 and len(clusters[1].graph.nodes) == 1) or ( 105 | len(clusters[0].graph.nodes) == 1 and len(clusters[1].graph.nodes) == 2 106 | ) 107 | -------------------------------------------------------------------------------- /tests/test_fundamentals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import math 5 | 6 | import pytest 7 | 8 | from graphix.fundamentals import Axis, ComplexUnit, Plane, Sign 9 | 10 | 11 | class TestSign: 12 | def test_str(self) -> None: 13 | assert str(Sign.PLUS) == "+" 14 | assert str(Sign.MINUS) == "-" 15 | 16 | def test_plus_if(self) -> None: 17 | assert Sign.plus_if(True) == Sign.PLUS 18 | assert Sign.plus_if(False) == Sign.MINUS 19 | 20 | def test_minus_if(self) -> None: 21 | assert Sign.minus_if(True) == Sign.MINUS 22 | assert Sign.minus_if(False) == Sign.PLUS 23 | 24 | def test_neg(self) -> None: 25 | assert -Sign.PLUS == Sign.MINUS 26 | assert -Sign.MINUS == Sign.PLUS 27 | 28 | def test_mul_sign(self) -> None: 29 | assert Sign.PLUS * Sign.PLUS == Sign.PLUS 30 | assert Sign.PLUS * Sign.MINUS == Sign.MINUS 31 | assert Sign.MINUS * Sign.PLUS == Sign.MINUS 32 | assert Sign.MINUS * Sign.MINUS == Sign.PLUS 33 | 34 | def test_mul_int(self) -> None: 35 | left = Sign.PLUS * 1 36 | assert isinstance(left, int) 37 | assert left == int(Sign.PLUS) 38 | right = 1 * Sign.PLUS 39 | assert isinstance(right, int) 40 | assert right == int(Sign.PLUS) 41 | 42 | left = Sign.MINUS * 1 43 | assert isinstance(left, int) 44 | assert left == int(Sign.MINUS) 45 | right = 1 * Sign.MINUS 46 | assert isinstance(right, int) 47 | assert right == int(Sign.MINUS) 48 | 49 | def test_mul_float(self) -> None: 50 | left = Sign.PLUS * 1.0 51 | assert isinstance(left, float) 52 | assert left == float(Sign.PLUS) 53 | right = 1.0 * Sign.PLUS 54 | assert isinstance(right, float) 55 | assert right == float(Sign.PLUS) 56 | 57 | left = Sign.MINUS * 1.0 58 | assert isinstance(left, float) 59 | assert left == float(Sign.MINUS) 60 | right = 1.0 * Sign.MINUS 61 | assert isinstance(right, float) 62 | assert right == float(Sign.MINUS) 63 | 64 | def test_mul_complex(self) -> None: 65 | left = Sign.PLUS * complex(1) 66 | assert isinstance(left, complex) 67 | assert left == complex(Sign.PLUS) 68 | right = complex(1) * Sign.PLUS 69 | assert isinstance(right, complex) 70 | assert right == complex(Sign.PLUS) 71 | 72 | left = Sign.MINUS * complex(1) 73 | assert isinstance(left, complex) 74 | assert left == complex(Sign.MINUS) 75 | right = complex(1) * Sign.MINUS 76 | assert isinstance(right, complex) 77 | assert right == complex(Sign.MINUS) 78 | 79 | def test_int(self) -> None: 80 | # Necessary to justify `type: ignore` 81 | assert isinstance(int(Sign.PLUS), int) 82 | assert isinstance(int(Sign.MINUS), int) 83 | 84 | 85 | class TestComplexUnit: 86 | def test_try_from(self) -> None: 87 | assert ComplexUnit.try_from(ComplexUnit.ONE) == ComplexUnit.ONE 88 | assert ComplexUnit.try_from(1) == ComplexUnit.ONE 89 | assert ComplexUnit.try_from(1.0) == ComplexUnit.ONE 90 | assert ComplexUnit.try_from(1.0 + 0.0j) == ComplexUnit.ONE 91 | assert ComplexUnit.try_from(3) is None 92 | 93 | def test_from_properties(self) -> None: 94 | assert ComplexUnit.from_properties() == ComplexUnit.ONE 95 | assert ComplexUnit.from_properties(is_imag=True) == ComplexUnit.J 96 | assert ComplexUnit.from_properties(sign=Sign.MINUS) == ComplexUnit.MINUS_ONE 97 | assert ComplexUnit.from_properties(sign=Sign.MINUS, is_imag=True) == ComplexUnit.MINUS_J 98 | 99 | @pytest.mark.parametrize(("sign", "is_imag"), itertools.product([Sign.PLUS, Sign.MINUS], [True, False])) 100 | def test_properties(self, sign: Sign, is_imag: bool) -> None: 101 | assert ComplexUnit.from_properties(sign=sign, is_imag=is_imag).sign == sign 102 | assert ComplexUnit.from_properties(sign=sign, is_imag=is_imag).is_imag == is_imag 103 | 104 | def test_complex(self) -> None: 105 | assert complex(ComplexUnit.ONE) == 1 106 | assert complex(ComplexUnit.J) == 1j 107 | assert complex(ComplexUnit.MINUS_ONE) == -1 108 | assert complex(ComplexUnit.MINUS_J) == -1j 109 | 110 | def test_str(self) -> None: 111 | assert str(ComplexUnit.ONE) == "1" 112 | assert str(ComplexUnit.J) == "1j" 113 | assert str(ComplexUnit.MINUS_ONE) == "-1" 114 | assert str(ComplexUnit.MINUS_J) == "-1j" 115 | 116 | @pytest.mark.parametrize(("lhs", "rhs"), itertools.product(ComplexUnit, ComplexUnit)) 117 | def test_mul_self(self, lhs: ComplexUnit, rhs: ComplexUnit) -> None: 118 | assert complex(lhs * rhs) == complex(lhs) * complex(rhs) 119 | 120 | def test_mul_number(self) -> None: 121 | assert ComplexUnit.ONE * 1 == ComplexUnit.ONE 122 | assert 1 * ComplexUnit.ONE == ComplexUnit.ONE 123 | assert ComplexUnit.ONE * 1.0 == ComplexUnit.ONE 124 | assert 1.0 * ComplexUnit.ONE == ComplexUnit.ONE 125 | assert ComplexUnit.ONE * complex(1) == ComplexUnit.ONE 126 | assert complex(1) * ComplexUnit.ONE == ComplexUnit.ONE 127 | 128 | def test_neg(self) -> None: 129 | assert -ComplexUnit.ONE == ComplexUnit.MINUS_ONE 130 | assert -ComplexUnit.J == ComplexUnit.MINUS_J 131 | assert -ComplexUnit.MINUS_ONE == ComplexUnit.ONE 132 | assert -ComplexUnit.MINUS_J == ComplexUnit.J 133 | 134 | 135 | _PLANE_INDEX = {Axis.X: 0, Axis.Y: 1, Axis.Z: 2} 136 | 137 | 138 | class TestPlane: 139 | @pytest.mark.parametrize("p", Plane) 140 | def test_polar_consistency(self, p: Plane) -> None: 141 | icos = _PLANE_INDEX[p.cos] 142 | isin = _PLANE_INDEX[p.sin] 143 | irest = 3 - icos - isin 144 | po = p.polar(1) 145 | assert po[icos] == pytest.approx(math.cos(1)) 146 | assert po[isin] == pytest.approx(math.sin(1)) 147 | assert po[irest] == 0 148 | 149 | def test_from_axes(self) -> None: 150 | assert Plane.from_axes(Axis.X, Axis.Y) == Plane.XY 151 | assert Plane.from_axes(Axis.Y, Axis.Z) == Plane.YZ 152 | assert Plane.from_axes(Axis.X, Axis.Z) == Plane.XZ 153 | assert Plane.from_axes(Axis.Y, Axis.X) == Plane.XY 154 | assert Plane.from_axes(Axis.Z, Axis.Y) == Plane.YZ 155 | assert Plane.from_axes(Axis.Z, Axis.X) == Plane.XZ 156 | 157 | def test_from_axes_ng(self) -> None: 158 | with pytest.raises(ValueError): 159 | Plane.from_axes(Axis.X, Axis.X) 160 | with pytest.raises(ValueError): 161 | Plane.from_axes(Axis.Y, Axis.Y) 162 | with pytest.raises(ValueError): 163 | Plane.from_axes(Axis.Z, Axis.Z) 164 | -------------------------------------------------------------------------------- /tests/test_generator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import networkx as nx 6 | import numpy as np 7 | import pytest 8 | 9 | from graphix.fundamentals import Plane 10 | from graphix.generator import generate_from_graph 11 | from graphix.random_objects import rand_gate 12 | 13 | if TYPE_CHECKING: 14 | from numpy.random import Generator 15 | 16 | 17 | class TestGenerator: 18 | def test_pattern_generation_determinism_flow(self, fx_rng: Generator) -> None: 19 | graph: nx.Graph[int] = nx.Graph([(0, 3), (1, 4), (2, 5), (1, 3), (2, 4), (3, 6), (4, 7), (5, 8)]) 20 | inputs = {0, 1, 2} 21 | outputs = {6, 7, 8} 22 | angles = fx_rng.normal(size=6) 23 | results = [] 24 | repeats = 3 # for testing the determinism of a pattern 25 | meas_planes = dict.fromkeys(range(6), Plane.XY) 26 | for _ in range(repeats): 27 | pattern = generate_from_graph(graph, angles, list(inputs), list(outputs), meas_planes=meas_planes) 28 | pattern.standardize() 29 | pattern.minimize_space() 30 | state = pattern.simulate_pattern(rng=fx_rng) 31 | results.append(state) 32 | combinations = [(0, 1), (0, 2), (1, 2)] 33 | for i, j in combinations: 34 | inner_product = np.dot(results[i].flatten(), results[j].flatten().conjugate()) 35 | assert abs(inner_product) == pytest.approx(1) 36 | 37 | def test_pattern_generation_determinism_gflow(self, fx_rng: Generator) -> None: 38 | graph: nx.Graph[int] = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (3, 6), (1, 6)]) 39 | inputs = {1, 3, 5} 40 | outputs = {2, 4, 6} 41 | angles = fx_rng.normal(size=6) 42 | meas_planes = dict.fromkeys(range(1, 6), Plane.XY) 43 | results = [] 44 | repeats = 3 # for testing the determinism of a pattern 45 | for _ in range(repeats): 46 | pattern = generate_from_graph(graph, angles, list(inputs), list(outputs), meas_planes=meas_planes) 47 | pattern.standardize() 48 | pattern.minimize_space() 49 | state = pattern.simulate_pattern(rng=fx_rng) 50 | results.append(state) 51 | combinations = [(0, 1), (0, 2), (1, 2)] 52 | for i, j in combinations: 53 | inner_product = np.dot(results[i].flatten(), results[j].flatten().conjugate()) 54 | assert abs(inner_product) == pytest.approx(1) 55 | 56 | def test_pattern_generation_flow(self, fx_rng: Generator) -> None: 57 | nqubits = 3 58 | depth = 2 59 | pairs = [(0, 1), (1, 2)] 60 | circuit = rand_gate(nqubits, depth, pairs, fx_rng) 61 | # transpile into graph 62 | pattern = circuit.transpile().pattern 63 | pattern.standardize() 64 | pattern.shift_signals() 65 | # get the graph and generate pattern again with flow algorithm 66 | nodes, edges = pattern.get_graph() 67 | g: nx.Graph[int] = nx.Graph() 68 | g.add_nodes_from(nodes) 69 | g.add_edges_from(edges) 70 | input_list = [0, 1, 2] 71 | angles: dict[int, float] = {} 72 | for cmd in pattern.get_measurement_commands(): 73 | assert isinstance(cmd.angle, float) 74 | angles[cmd.node] = float(cmd.angle) 75 | meas_planes = pattern.get_meas_plane() 76 | pattern2 = generate_from_graph(g, angles, input_list, pattern.output_nodes, meas_planes) 77 | # check that the new one runs and returns correct result 78 | pattern2.standardize() 79 | pattern2.shift_signals() 80 | pattern2.minimize_space() 81 | state = circuit.simulate_statevector().statevec 82 | state_mbqc = pattern2.simulate_pattern(rng=fx_rng) 83 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 84 | 85 | def test_pattern_generation_no_internal_nodes(self) -> None: 86 | g: nx.Graph[int] = nx.Graph() 87 | g.add_edges_from([(0, 1), (1, 2)]) 88 | pattern = generate_from_graph(g, {}, {0, 1, 2}, {0, 1, 2}, {}) 89 | assert pattern.get_graph() == ([0, 1, 2], [(0, 1), (1, 2)]) 90 | -------------------------------------------------------------------------------- /tests/test_graphsim.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import networkx as nx 4 | import numpy as np 5 | import numpy.typing as npt 6 | import pytest 7 | 8 | from graphix.clifford import Clifford 9 | from graphix.fundamentals import Plane 10 | from graphix.graphsim import GraphState 11 | from graphix.ops import Ops 12 | from graphix.sim.statevec import Statevec 13 | 14 | 15 | def get_state(g: GraphState) -> Statevec: 16 | node_list = list(g.nodes) 17 | nqubit = len(g.nodes) 18 | gstate = Statevec(nqubit=nqubit) 19 | imapping = {node_list[i]: i for i in range(nqubit)} 20 | mapping = [node_list[i] for i in range(nqubit)] 21 | for i, j in g.edges: 22 | gstate.entangle((imapping[i], imapping[j])) 23 | for i in range(nqubit): 24 | if g.nodes[mapping[i]]["sign"]: 25 | gstate.evolve_single(Ops.Z, i) 26 | for i in range(nqubit): 27 | if g.nodes[mapping[i]]["loop"]: 28 | gstate.evolve_single(Ops.S, i) 29 | for i in range(nqubit): 30 | if g.nodes[mapping[i]]["hollow"]: 31 | gstate.evolve_single(Ops.H, i) 32 | return gstate 33 | 34 | 35 | def meas_op( 36 | angle: float, vop: Clifford = Clifford.I, plane: Plane = Plane.XY, choice: int = 0 37 | ) -> npt.NDArray[np.complex128]: 38 | """Return the projection operator for given measurement angle and local Clifford op (VOP). 39 | 40 | .. seealso:: :mod:`graphix.clifford` 41 | 42 | Parameters 43 | ---------- 44 | angle : float 45 | original measurement angle in radian 46 | vop : int 47 | index of local Clifford (vop), see graphq.clifford.CLIFFORD 48 | plane : 'XY', 'YZ' or 'ZX' 49 | measurement plane on which angle shall be defined 50 | choice : 0 or 1 51 | choice of measurement outcome. measured eigenvalue would be (-1)**choice. 52 | 53 | Returns 54 | ------- 55 | op : numpy array 56 | projection operator 57 | 58 | """ 59 | assert choice in {0, 1} 60 | if plane == Plane.XY: 61 | vec = (np.cos(angle), np.sin(angle), 0) 62 | elif plane == Plane.YZ: 63 | vec = (0, np.cos(angle), np.sin(angle)) 64 | elif plane == Plane.XZ: 65 | vec = (np.cos(angle), 0, np.sin(angle)) 66 | op_mat = np.eye(2, dtype=np.complex128) / 2 67 | for i in range(3): 68 | op_mat += (-1) ** (choice) * vec[i] * Clifford(i + 1).matrix / 2 69 | return (vop.conj.matrix @ op_mat @ vop.matrix).astype(np.complex128, copy=False) 70 | 71 | 72 | class TestGraphSim: 73 | def test_fig2(self) -> None: 74 | """Three single-qubit measurements presented in Fig.2 of M. Elliot et al (2010).""" 75 | nqubit = 6 76 | edges = [(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)] 77 | g = GraphState(nodes=np.arange(nqubit), edges=edges) 78 | gstate = get_state(g) 79 | g.measure_x(0) 80 | gstate.evolve_single(meas_op(0), 0) # x meas 81 | gstate.normalize() 82 | gstate.remove_qubit(0) 83 | gstate2 = get_state(g) 84 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 85 | 86 | g.measure_y(1, choice=0) 87 | gstate.evolve_single(meas_op(0.5 * np.pi), 0) # y meas 88 | gstate.normalize() 89 | gstate.remove_qubit(0) 90 | gstate2 = get_state(g) 91 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 92 | 93 | g.measure_z(3) 94 | gstate.evolve_single(meas_op(0.5 * np.pi, plane=Plane.YZ), 1) # z meas 95 | gstate.normalize() 96 | gstate.remove_qubit(1) 97 | gstate2 = get_state(g) 98 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 99 | 100 | def test_e2(self) -> None: 101 | nqubit = 6 102 | edges = [(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)] 103 | g = GraphState(nodes=np.arange(nqubit), edges=edges) 104 | g.h(3) 105 | gstate = get_state(g) 106 | 107 | g.equivalent_graph_e2(3, 4) 108 | gstate2 = get_state(g) 109 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 110 | 111 | g.equivalent_graph_e2(4, 0) 112 | gstate3 = get_state(g) 113 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate3.flatten())) == pytest.approx(1) 114 | 115 | g.equivalent_graph_e2(4, 5) 116 | gstate4 = get_state(g) 117 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate4.flatten())) == pytest.approx(1) 118 | 119 | g.equivalent_graph_e2(0, 3) 120 | gstate5 = get_state(g) 121 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate5.flatten())) == pytest.approx(1) 122 | 123 | g.equivalent_graph_e2(0, 3) 124 | gstate6 = get_state(g) 125 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate6.flatten())) == pytest.approx(1) 126 | 127 | def test_e1(self) -> None: 128 | nqubit = 6 129 | edges = [(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)] 130 | g = GraphState(nodes=np.arange(nqubit), edges=edges) 131 | g.nodes[3]["loop"] = True 132 | gstate = get_state(g) 133 | g.equivalent_graph_e1(3) 134 | 135 | gstate2 = get_state(g) 136 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 137 | g.z(4) 138 | gstate = get_state(g) 139 | g.equivalent_graph_e1(4) 140 | gstate2 = get_state(g) 141 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate2.flatten())) == pytest.approx(1) 142 | g.equivalent_graph_e1(4) 143 | gstate3 = get_state(g) 144 | assert np.abs(np.dot(gstate.flatten().conjugate(), gstate3.flatten())) == pytest.approx(1) 145 | 146 | def test_local_complement(self) -> None: 147 | nqubit = 6 148 | edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] 149 | exp_edges = [(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (4, 0)] 150 | g = GraphState(nodes=np.arange(nqubit), edges=edges) 151 | g.local_complement(1) 152 | exp_g = GraphState(nodes=np.arange(nqubit), edges=exp_edges) 153 | assert nx.utils.graphs_equal(g, exp_g) # type:ignore[no-untyped-call] 154 | -------------------------------------------------------------------------------- /tests/test_opengraph.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import networkx as nx 4 | 5 | from graphix.fundamentals import Plane 6 | from graphix.measurements import Measurement 7 | from graphix.opengraph import OpenGraph 8 | 9 | 10 | # Tests whether an open graph can be converted to and from a pattern and be 11 | # successfully reconstructed. 12 | def test_open_graph_to_pattern() -> None: 13 | g: nx.Graph[int] 14 | g = nx.Graph([(0, 1), (1, 2)]) 15 | inputs = [0] 16 | outputs = [2] 17 | meas = {0: Measurement(0, Plane.XY), 1: Measurement(0, Plane.XY)} 18 | og = OpenGraph(g, meas, inputs, outputs) 19 | 20 | pattern = og.to_pattern() 21 | og_reconstructed = OpenGraph.from_pattern(pattern) 22 | 23 | assert og.isclose(og_reconstructed) 24 | 25 | # 0 -- 1 -- 2 26 | # | 27 | # 3 -- 4 -- 5 28 | g = nx.Graph([(0, 1), (1, 2), (1, 4), (3, 4), (4, 5)]) 29 | inputs = [0, 3] 30 | outputs = [2, 5] 31 | meas = { 32 | 0: Measurement(0, Plane.XY), 33 | 1: Measurement(1.0, Plane.XY), 34 | 3: Measurement(1.0, Plane.YZ), 35 | 4: Measurement(1.0, Plane.XY), 36 | } 37 | 38 | og = OpenGraph(g, meas, inputs, outputs) 39 | 40 | pattern = og.to_pattern() 41 | og_reconstructed = OpenGraph.from_pattern(pattern) 42 | 43 | assert og.isclose(og_reconstructed) 44 | -------------------------------------------------------------------------------- /tests/test_pauli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | from graphix.fundamentals import Axis, ComplexUnit, Sign 9 | from graphix.pauli import Pauli 10 | 11 | 12 | class TestPauli: 13 | def test_from_axis(self) -> None: 14 | assert Pauli.from_axis(Axis.X) == Pauli.X 15 | assert Pauli.from_axis(Axis.Y) == Pauli.Y 16 | assert Pauli.from_axis(Axis.Z) == Pauli.Z 17 | 18 | def test_axis(self) -> None: 19 | with pytest.raises(ValueError): 20 | _ = Pauli.I.axis 21 | assert Pauli.X.axis == Axis.X 22 | assert Pauli.Y.axis == Axis.Y 23 | assert Pauli.Z.axis == Axis.Z 24 | 25 | @pytest.mark.parametrize( 26 | ("u", "p"), 27 | itertools.product(ComplexUnit, Pauli), 28 | ) 29 | def test_unit_mul(self, u: ComplexUnit, p: Pauli) -> None: 30 | assert np.allclose((u * p).matrix, complex(u) * p.matrix) 31 | 32 | @pytest.mark.parametrize( 33 | ("a", "b"), 34 | itertools.product(Pauli, Pauli), 35 | ) 36 | def test_matmul(self, a: Pauli, b: Pauli) -> None: 37 | assert np.allclose((a @ b).matrix, a.matrix @ b.matrix) 38 | 39 | @pytest.mark.parametrize("p", Pauli.iterate(symbol_only=True)) 40 | def test_repr(self, p: Pauli) -> None: 41 | pstr = f"Pauli.{p.symbol.name}" 42 | assert repr(p) == pstr 43 | assert repr(1 * p) == pstr 44 | assert repr(1j * p) == f"1j * {pstr}" 45 | assert repr(-1 * p) == f"-{pstr}" 46 | assert repr(-1j * p) == f"-1j * {pstr}" 47 | 48 | @pytest.mark.parametrize("p", Pauli.iterate(symbol_only=True)) 49 | def test_str(self, p: Pauli) -> None: 50 | pstr = p.symbol.name 51 | assert str(p) == pstr 52 | assert str(1 * p) == pstr 53 | assert str(1j * p) == f"1j * {pstr}" 54 | assert str(-1 * p) == f"-{pstr}" 55 | assert str(-1j * p) == f"-1j * {pstr}" 56 | 57 | @pytest.mark.parametrize("p", Pauli) 58 | def test_neg(self, p: Pauli) -> None: 59 | pneg = -p 60 | assert pneg == -p 61 | 62 | def test_iterate_true(self) -> None: 63 | cmp = list(Pauli.iterate(symbol_only=True)) 64 | assert len(cmp) == 4 65 | assert cmp[0] == Pauli.I 66 | assert cmp[1] == Pauli.X 67 | assert cmp[2] == Pauli.Y 68 | assert cmp[3] == Pauli.Z 69 | 70 | def test_iterate_false(self) -> None: 71 | cmp = list(Pauli.iterate(symbol_only=False)) 72 | assert len(cmp) == 16 73 | assert cmp[0] == Pauli.I 74 | assert cmp[1] == 1j * Pauli.I 75 | assert cmp[2] == -1 * Pauli.I 76 | assert cmp[3] == -1j * Pauli.I 77 | assert cmp[4] == Pauli.X 78 | assert cmp[5] == 1j * Pauli.X 79 | assert cmp[6] == -1 * Pauli.X 80 | assert cmp[7] == -1j * Pauli.X 81 | assert cmp[8] == Pauli.Y 82 | assert cmp[9] == 1j * Pauli.Y 83 | assert cmp[10] == -1 * Pauli.Y 84 | assert cmp[11] == -1j * Pauli.Y 85 | assert cmp[12] == Pauli.Z 86 | assert cmp[13] == 1j * Pauli.Z 87 | assert cmp[14] == -1 * Pauli.Z 88 | assert cmp[15] == -1j * Pauli.Z 89 | 90 | def test_iter_meta(self) -> None: 91 | it = Pauli.iterate(symbol_only=False) 92 | it_ = iter(Pauli) 93 | for p, p_ in zip(it, it_): 94 | assert p == p_ 95 | assert all(False for _ in it) 96 | assert all(False for _ in it_) 97 | 98 | @pytest.mark.parametrize(("p", "b"), itertools.product(Pauli.iterate(symbol_only=True), [0, 1])) 99 | def test_eigenstate(self, p: Pauli, b: int) -> None: 100 | ev = float(Sign.plus_if(b == 0)) if p != Pauli.I else 1 101 | evec = p.eigenstate(b).get_statevector() 102 | assert np.allclose(p.matrix @ evec, ev * evec) 103 | 104 | def test_eigenstate_invalid(self) -> None: 105 | with pytest.raises(ValueError): 106 | _ = Pauli.I.eigenstate(2) 107 | -------------------------------------------------------------------------------- /tests/test_pretty_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | 5 | import pytest 6 | from numpy.random import PCG64, Generator 7 | 8 | from graphix import command, instruction 9 | from graphix.clifford import Clifford 10 | from graphix.fundamentals import Plane 11 | from graphix.pattern import Pattern 12 | from graphix.pretty_print import OutputFormat, pattern_to_str 13 | from graphix.random_objects import rand_circuit 14 | from graphix.transpiler import Circuit 15 | 16 | 17 | def test_circuit_repr() -> None: 18 | circuit = Circuit(width=3, instr=[instruction.H(0), instruction.RX(1, math.pi), instruction.CCX(0, (1, 2))]) 19 | assert repr(circuit) == "Circuit(width=3, instr=[H(0), RX(1, pi), CCX(0, (1, 2))])" 20 | 21 | 22 | def j_alpha() -> Pattern: 23 | return Pattern(input_nodes=[1], cmds=[command.N(2), command.E((1, 2)), command.M(1), command.X(2, domain={1})]) 24 | 25 | 26 | def test_pattern_repr_j_alpha() -> None: 27 | p = j_alpha() 28 | assert repr(p) == "Pattern(input_nodes=[1], cmds=[N(2), E((1, 2)), M(1), X(2, {1})], output_nodes=[2])" 29 | 30 | 31 | def test_pattern_pretty_print_j_alpha() -> None: 32 | p = j_alpha() 33 | assert str(p) == "X(2,{1}) M(1) E(1,2) N(2)" 34 | assert p.to_unicode() == "X₂¹ M₁ E₁₋₂ N₂" 35 | assert p.to_latex() == r"\(X_{2}^{1}\,M_{1}\,E_{1,2}\,N_{2}\)" 36 | 37 | 38 | def example_pattern() -> Pattern: 39 | return Pattern( 40 | cmds=[ 41 | command.N(1), 42 | command.N(2), 43 | command.N(3), 44 | command.N(10), 45 | command.N(4), 46 | command.E((1, 2)), 47 | command.C(1, Clifford.H), 48 | command.M(1, Plane.YZ, 0.5), 49 | command.M(2, Plane.XZ, -0.25), 50 | command.M(10, Plane.XZ, -0.25), 51 | command.M(3, Plane.XY, 0.1, s_domain={1, 10}, t_domain={2}), 52 | command.M(4, s_domain={1}, t_domain={2, 3}), 53 | ] 54 | ) 55 | 56 | 57 | def test_pattern_repr_example() -> None: 58 | p = example_pattern() 59 | assert ( 60 | repr(p) 61 | == "Pattern(cmds=[N(1), N(2), N(3), N(10), N(4), E((1, 2)), C(1, Clifford.H), M(1, Plane.YZ, 0.5), M(2, Plane.XZ, -0.25), M(10, Plane.XZ, -0.25), M(3, angle=0.1, s_domain={1, 10}, t_domain={2}), M(4, s_domain={1}, t_domain={2, 3})])" 62 | ) 63 | 64 | 65 | def test_pattern_pretty_print_example() -> None: 66 | p = example_pattern() 67 | assert ( 68 | str(p) 69 | == "{2,3}[M(4)]{1} {2}[M(3,pi/10)]{1,10} M(10,XZ,-pi/4) M(2,XZ,-pi/4) M(1,YZ,pi/2) C(1,H) E(1,2) N(4) N(10) N(3) N(2) N(1)" 70 | ) 71 | assert p.to_unicode() == "₂₊₃[M₄]¹ ₂[M₃(π/10)]¹⁺¹⁰ M₁₀(XZ,-π/4) M₂(XZ,-π/4) M₁(YZ,π/2) C₁(H) E₁₋₂ N₄ N₁₀ N₃ N₂ N₁" 72 | assert ( 73 | p.to_latex() 74 | == r"\({}_{2,3}[M_{4}]^{1}\,{}_{2}[M_{3}^{\frac{\pi}{10}}]^{1,10}\,M_{10}^{XZ,-\frac{\pi}{4}}\,M_{2}^{XZ,-\frac{\pi}{4}}\,M_{1}^{YZ,\frac{\pi}{2}}\,C_{1}^{H}\,E_{1,2}\,N_{4}\,N_{10}\,N_{3}\,N_{2}\,N_{1}\)" 75 | ) 76 | assert ( 77 | pattern_to_str(p, output=OutputFormat.ASCII, limit=9, left_to_right=True) 78 | == "N(1) N(2) N(3) N(10) N(4) E(1,2) C(1,H) M(1,YZ,pi/2)...(4 more commands)" 79 | ) 80 | 81 | 82 | @pytest.mark.parametrize("jumps", range(1, 11)) 83 | @pytest.mark.parametrize("output", list(OutputFormat)) 84 | def test_pattern_pretty_print_random(fx_bg: PCG64, jumps: int, output: OutputFormat) -> None: 85 | rng = Generator(fx_bg.jumped(jumps)) 86 | rand_pat = rand_circuit(5, 5, rng=rng).transpile().pattern 87 | pattern_to_str(rand_pat, output) 88 | -------------------------------------------------------------------------------- /tests/test_pyzx.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.util # Use fully-qualified import to avoid name conflict (util) 4 | import random 5 | from copy import deepcopy 6 | from typing import TYPE_CHECKING 7 | 8 | import numpy as np 9 | import pytest 10 | from numpy.random import PCG64, Generator 11 | 12 | from graphix.opengraph import OpenGraph 13 | from graphix.random_objects import rand_circuit 14 | from graphix.transpiler import Circuit 15 | 16 | if TYPE_CHECKING: 17 | from pyzx.graph.base import BaseGraph 18 | 19 | SEED = 123 20 | 21 | 22 | def _pyzx_notfound() -> bool: 23 | return importlib.util.find_spec("pyzx") is None 24 | 25 | 26 | @pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") 27 | def test_graph_equality() -> None: 28 | from pyzx.generate import cliffordT as clifford_t # noqa: N813 29 | 30 | from graphix.pyzx import from_pyzx_graph 31 | 32 | random.seed(SEED) 33 | g = clifford_t(4, 10, 0.1) 34 | 35 | og1 = from_pyzx_graph(g) 36 | 37 | g_copy = deepcopy(g) 38 | og2 = from_pyzx_graph(g_copy) 39 | 40 | assert og1.isclose(og2) 41 | 42 | 43 | def assert_reconstructed_pyzx_graph_equal(g: BaseGraph[int, tuple[int, int]]) -> None: 44 | """Convert a graph to and from an Open graph and then checks the resulting pyzx graph is equal to the original.""" 45 | import pyzx as zx 46 | 47 | from graphix.pyzx import from_pyzx_graph, to_pyzx_graph 48 | 49 | zx.simplify.to_graph_like(g) 50 | 51 | g_copy = deepcopy(g) 52 | og = from_pyzx_graph(g_copy) 53 | reconstructed_pyzx_graph = to_pyzx_graph(og) 54 | 55 | # The "tensorfy" function break if the rows aren't set for some reason 56 | for v in reconstructed_pyzx_graph.vertices(): 57 | reconstructed_pyzx_graph.set_row(v, 2) 58 | 59 | for v in g.vertices(): 60 | g.set_row(v, 2) 61 | ten = zx.tensorfy(g) 62 | ten_graph = zx.tensorfy(reconstructed_pyzx_graph) 63 | assert zx.compare_tensors(ten, ten_graph) 64 | 65 | 66 | # Tests that compiling from a pyzx graph to an OpenGraph returns the same 67 | # graph. Only works with small circuits up to 4 qubits since PyZX's `tensorfy` 68 | # function seems to consume huge amount of memory for larger qubit 69 | @pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") 70 | def test_random_clifford_t() -> None: 71 | from pyzx.generate import cliffordT as clifford_t # noqa: N813 72 | 73 | for _ in range(15): 74 | g = clifford_t(4, 10, 0.1) 75 | assert_reconstructed_pyzx_graph_equal(g) 76 | 77 | 78 | @pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") 79 | @pytest.mark.parametrize("jumps", range(1, 11)) 80 | def test_random_circuit(fx_bg: PCG64, jumps: int) -> None: 81 | from graphix.pyzx import from_pyzx_graph, to_pyzx_graph 82 | 83 | rng = Generator(fx_bg.jumped(jumps)) 84 | nqubits = 5 85 | depth = 5 86 | circuit = rand_circuit(nqubits, depth, rng) 87 | pattern = circuit.transpile().pattern 88 | opengraph = OpenGraph.from_pattern(pattern) 89 | zx_graph = to_pyzx_graph(opengraph) 90 | opengraph2 = from_pyzx_graph(zx_graph) 91 | pattern2 = opengraph2.to_pattern() 92 | pattern.perform_pauli_measurements() 93 | pattern.minimize_space() 94 | state = pattern.simulate_pattern() 95 | pattern2.perform_pauli_measurements() 96 | pattern2.minimize_space() 97 | state2 = pattern2.simulate_pattern() 98 | assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) 99 | 100 | 101 | @pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") 102 | def test_rz() -> None: 103 | import pyzx as zx 104 | 105 | from graphix.pyzx import from_pyzx_graph 106 | 107 | circuit = Circuit(2) 108 | circuit.rz(0, np.pi / 4) 109 | pattern = circuit.transpile().pattern 110 | circ = zx.qasm("qreg q[2]; rz(pi / 4) q[0];") # type: ignore[attr-defined] 111 | g = circ.to_graph() 112 | og = from_pyzx_graph(g) 113 | pattern_zx = og.to_pattern() 114 | state = pattern.simulate_pattern() 115 | state_zx = pattern_zx.simulate_pattern() 116 | assert np.abs(np.dot(state_zx.flatten().conjugate(), state.flatten())) == pytest.approx(1) 117 | 118 | 119 | # Issue #235 120 | @pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") 121 | def test_full_reduce_toffoli() -> None: 122 | import pyzx as zx 123 | 124 | from graphix.pyzx import from_pyzx_graph, to_pyzx_graph 125 | 126 | c = Circuit(3) 127 | c.ccx(0, 1, 2) 128 | p = c.transpile().pattern 129 | og = OpenGraph.from_pattern(p) 130 | pyg = to_pyzx_graph(og) 131 | pyg.normalize() 132 | pyg_copy = deepcopy(pyg) 133 | zx.simplify.full_reduce(pyg) 134 | pyg.normalize() 135 | t = zx.tensorfy(pyg) 136 | t2 = zx.tensorfy(pyg_copy) 137 | assert zx.compare_tensors(t, t2) 138 | og2 = from_pyzx_graph(pyg) 139 | p2 = og2.to_pattern() 140 | s = p.simulate_pattern() 141 | s2 = p2.simulate_pattern() 142 | print(np.abs(np.dot(s.flatten().conj(), s2.flatten()))) 143 | -------------------------------------------------------------------------------- /tests/test_random_utilities.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | import numpy as np 6 | import pytest 7 | 8 | import graphix.random_objects as randobj 9 | from graphix import linalg_validations as lv 10 | from graphix.channels import KrausChannel 11 | from graphix.ops import Ops 12 | from graphix.sim.density_matrix import DensityMatrix 13 | 14 | if TYPE_CHECKING: 15 | from numpy.random import Generator 16 | 17 | 18 | class TestUtilities: 19 | def test_rand_herm(self, fx_rng: Generator) -> None: 20 | tmp = randobj.rand_herm(fx_rng.integers(2, 20), fx_rng) 21 | assert np.allclose(tmp, tmp.conj().T) 22 | 23 | # TODO : work on that. Verify an a random vector and not at the operator level... 24 | 25 | def test_rand_unit(self, fx_rng: Generator) -> None: 26 | d = fx_rng.integers(2, 20) 27 | tmp = randobj.rand_unit(d, fx_rng) 28 | print(type(tmp), tmp.dtype) 29 | 30 | # different default values for testing.assert_allclose and all_close! 31 | assert np.allclose(tmp @ tmp.conj().T, np.eye(d), atol=1e-15) 32 | assert np.allclose(tmp.conj().T @ tmp, np.eye(d), atol=1e-15) 33 | 34 | def test_random_channel_success(self, fx_rng: Generator) -> None: 35 | nqb = int(fx_rng.integers(1, 5)) 36 | dim = 2**nqb # fx_rng.integers(2, 8) 37 | 38 | # no rank feature 39 | channel = randobj.rand_channel_kraus(dim=dim, rng=fx_rng) 40 | 41 | assert isinstance(channel, KrausChannel) 42 | assert channel[0].operator.shape == (dim, dim) 43 | assert channel.nqubit == nqb 44 | assert len(channel) == dim**2 45 | 46 | rk = int(fx_rng.integers(1, dim**2 + 1)) 47 | channel = randobj.rand_channel_kraus(dim=dim, rank=rk, rng=fx_rng) 48 | 49 | assert isinstance(channel, KrausChannel) 50 | assert channel[0].operator.shape == (dim, dim) 51 | assert channel.nqubit == nqb 52 | assert len(channel) == rk 53 | 54 | def test_random_channel_fail(self, fx_rng: Generator) -> None: 55 | # incorrect rank type 56 | with pytest.raises(TypeError): 57 | _ = randobj.rand_channel_kraus(dim=2**2, rank=3.0, rng=fx_rng) 58 | 59 | # null rank 60 | with pytest.raises(ValueError): 61 | _ = randobj.rand_channel_kraus(dim=2**2, rank=0, rng=fx_rng) 62 | 63 | def test_rand_gauss_cpx(self, fx_rng: Generator) -> None: 64 | nsample = int(1e4) 65 | 66 | dim = fx_rng.integers(2, 20) 67 | tmp = [randobj.rand_gauss_cpx_mat(dim=dim, rng=fx_rng) for _ in range(nsample)] 68 | 69 | dimset = {i.shape for i in tmp} 70 | assert len(dimset) == 1 71 | assert next(iter(dimset)) == (dim, dim) 72 | 73 | def test_check_psd_success(self, fx_rng: Generator) -> None: 74 | # Generate a random mixed state from state vectors with same probability 75 | # We know this is PSD 76 | 77 | nqb = fx_rng.integers(2, 7) 78 | 79 | dim = 2**nqb 80 | m = fx_rng.integers(1, dim) 81 | 82 | dm = np.zeros((dim,) * 2, dtype=np.complex128) 83 | 84 | # TODO optimize that 85 | for _ in range(m): 86 | psi = fx_rng.uniform(size=dim) + 1j * fx_rng.uniform(size=dim) 87 | psi /= np.sqrt(np.sum(np.abs(psi) ** 2)) 88 | dm += np.outer(psi, psi.conj()) / m 89 | 90 | assert lv.is_psd(dm) 91 | 92 | def test_check_psd_fail(self, fx_rng: Generator) -> None: 93 | # not hermitian 94 | # don't use dim = 2, too easy to have a PSD matrix. 95 | # NOTE useless test since eigvalsh treats the matrix as hermitian and takes only the L or U part 96 | 97 | lst = fx_rng.integers(5, 20) 98 | 99 | mat = fx_rng.uniform(size=(lst, lst)) + 1j * fx_rng.uniform(size=(lst, lst)) 100 | 101 | # eigvalsh doesn't raise a LinAlgError since just use upper or lower part of the matrix. 102 | # instead Value error 103 | assert not lv.is_psd(mat) 104 | 105 | # hermitian but not positive eigenvalues 106 | mat = randobj.rand_herm(lst, rng=fx_rng) 107 | 108 | assert not lv.is_psd(mat) 109 | 110 | def test_rand_dm(self, fx_rng: Generator) -> None: 111 | # needs to be power of 2 dimension since builds a DM object 112 | dm = randobj.rand_dm(2 ** fx_rng.integers(2, 5), rng=fx_rng) 113 | 114 | assert isinstance(dm, DensityMatrix) 115 | assert lv.is_qubitop(dm.rho) 116 | assert lv.is_hermitian(dm.rho) 117 | assert lv.is_psd(dm.rho) 118 | assert lv.is_unit_trace(dm.rho) 119 | 120 | # try with incorrect dimension 121 | def test_rand_dm_fail(self, fx_rng: Generator) -> None: 122 | with pytest.raises(ValueError): 123 | _ = randobj.rand_dm(2 ** fx_rng.integers(2, 5) + 1, rng=fx_rng) 124 | 125 | def test_rand_dm_rank(self, fx_rng: Generator) -> None: 126 | rk = 3 127 | dm = randobj.rand_dm(2 ** fx_rng.integers(2, 5), rank=rk, rng=fx_rng) 128 | 129 | assert isinstance(dm, DensityMatrix) 130 | assert lv.is_qubitop(dm.rho) 131 | assert lv.is_hermitian(dm.rho) 132 | assert lv.is_psd(dm.rho) 133 | assert lv.is_unit_trace(dm.rho) 134 | 135 | evals = np.linalg.eigvalsh(dm.rho) 136 | 137 | evals[np.abs(evals) < 1e-15] = 0 138 | 139 | assert rk == np.count_nonzero(evals) 140 | 141 | # TODO move that somewhere else? 142 | def test_pauli_tensor_ops(self, fx_rng: Generator) -> None: 143 | nqb = int(fx_rng.integers(2, 6)) 144 | pauli_tensor_ops = Ops.build_tensor_pauli_ops(nqb) 145 | 146 | assert len(pauli_tensor_ops) == 4**nqb 147 | 148 | dims = np.array([i.shape for i in pauli_tensor_ops]) 149 | # or np.apply_along_axis ? 150 | assert np.all(dims == (2**nqb, 2**nqb)) 151 | 152 | def test_pauli_tensor_ops_fail(self, fx_rng: Generator) -> None: 153 | with pytest.raises(TypeError): 154 | _ = Ops.build_tensor_pauli_ops(fx_rng.integers(2, 6) + 0.5) 155 | 156 | with pytest.raises(ValueError): 157 | _ = Ops.build_tensor_pauli_ops(0) 158 | 159 | def test_random_pauli_channel_success(self, fx_rng: Generator) -> None: 160 | nqb = int(fx_rng.integers(2, 6)) 161 | rk = int(fx_rng.integers(1, 2**nqb + 1)) 162 | pauli_channel = randobj.rand_pauli_channel_kraus(dim=2**nqb, rank=rk, rng=fx_rng) # default is full rank 163 | 164 | assert isinstance(pauli_channel, KrausChannel) 165 | assert pauli_channel.nqubit == nqb 166 | assert len(pauli_channel) == rk 167 | 168 | def test_random_pauli_channel_fail(self, fx_rng: Generator) -> None: 169 | nqb = 3 170 | rk = 2 171 | with pytest.raises(TypeError): 172 | randobj.rand_pauli_channel_kraus(dim=2**nqb, rank=rk + 0.5, rng=fx_rng) 173 | 174 | with pytest.raises(TypeError): 175 | randobj.rand_pauli_channel_kraus(dim=2**nqb + 0.5, rank=rk, rng=fx_rng) 176 | 177 | with pytest.raises(ValueError): 178 | randobj.rand_pauli_channel_kraus(dim=2**nqb, rank=-3, rng=fx_rng) 179 | 180 | with pytest.raises(ValueError): 181 | randobj.rand_pauli_channel_kraus(dim=2**nqb + 1, rank=rk, rng=fx_rng) 182 | -------------------------------------------------------------------------------- /tests/test_rng.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from concurrent.futures import ThreadPoolExecutor 4 | from threading import Thread 5 | 6 | import numpy as np 7 | 8 | from graphix.rng import ensure_rng 9 | 10 | 11 | def test_identity() -> None: 12 | rng = np.random.default_rng() 13 | assert ensure_rng(rng) is rng 14 | 15 | 16 | def test_new_thread() -> None: 17 | t = Thread(target=ensure_rng) 18 | t.start() 19 | 20 | 21 | def test_threadpool() -> None: 22 | with ThreadPoolExecutor() as executor: 23 | tasks = executor.map(lambda _: ensure_rng(), range(100)) 24 | for _ in tasks: 25 | pass 26 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import TYPE_CHECKING 5 | 6 | import numpy as np 7 | import numpy.typing as npt 8 | import pytest 9 | 10 | from graphix.device_interface import PatternRunner 11 | from graphix.transpiler import Circuit 12 | 13 | if TYPE_CHECKING: 14 | from collections.abc import Collection 15 | 16 | from pytest_mock import MockerFixture 17 | 18 | try: 19 | import qiskit 20 | from qiskit_aer import Aer 21 | except ModuleNotFoundError: 22 | pass 23 | 24 | 25 | def modify_statevector(statevector: npt.ArrayLike, output_qubit: Collection[int]) -> npt.NDArray: 26 | statevector = np.asarray(statevector) 27 | n = round(np.log2(len(statevector))) 28 | new_statevector = np.zeros(2 ** len(output_qubit), dtype=complex) 29 | for i in range(len(statevector)): 30 | i_str = format(i, f"0{n}b") 31 | new_idx = "" 32 | for idx in output_qubit: 33 | new_idx += i_str[n - idx - 1] 34 | new_statevector[int(new_idx, 2)] += statevector[i] 35 | return new_statevector 36 | 37 | 38 | class TestPatternRunner: 39 | @pytest.mark.skipif(sys.modules.get("qiskit") is None, reason="qiskit not installed") 40 | def test_ibmq_backend(self, mocker: MockerFixture) -> None: 41 | # circuit in qiskit 42 | qc = qiskit.QuantumCircuit(3) 43 | qc.h(0) 44 | qc.rx(1.23, 1) 45 | qc.rz(1.23, 2) 46 | qc.cx(0, 1) 47 | qc.cx(1, 2) 48 | qc.save_statevector() 49 | sim = Aer.get_backend("aer_simulator") 50 | new_qc = qiskit.transpile(qc, sim) 51 | job = sim.run(new_qc) 52 | result = job.result() 53 | 54 | runner = mocker.Mock() 55 | 56 | runner.IBMQBackend().circ = qc 57 | runner.IBMQBackend().simulate.return_value = result 58 | runner.IBMQBackend().circ_output = [0, 1, 2] 59 | 60 | sys.modules["graphix_ibmq.runner"] = runner 61 | 62 | # circuit in graphix 63 | circuit = Circuit(3) 64 | circuit.h(1) 65 | circuit.h(2) 66 | circuit.rx(1, 1.23) 67 | circuit.rz(2, 1.23) 68 | circuit.cnot(0, 1) 69 | circuit.cnot(1, 2) 70 | 71 | pattern = circuit.transpile().pattern 72 | state = pattern.simulate_pattern() 73 | 74 | runner = PatternRunner(pattern, backend="ibmq", save_statevector=True) 75 | sim_result = runner.simulate(format_result=False) 76 | state_qiskit = sim_result.get_statevector(runner.backend.circ) 77 | state_qiskit_mod = modify_statevector(state_qiskit, runner.backend.circ_output) 78 | 79 | assert np.abs(np.dot(state_qiskit_mod.conjugate(), state.flatten())) == pytest.approx(1) 80 | -------------------------------------------------------------------------------- /tests/test_statevec.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | from typing import TYPE_CHECKING 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from graphix.fundamentals import Plane 10 | from graphix.sim.statevec import Statevec 11 | from graphix.states import BasicStates, PlanarState 12 | 13 | if TYPE_CHECKING: 14 | from numpy.random import Generator 15 | 16 | 17 | class TestStatevec: 18 | """Test for Statevec class. Particularly new constructor.""" 19 | 20 | # test injitializing one qubit in plus state 21 | def test_default_success(self) -> None: 22 | vec = Statevec(nqubit=1) 23 | assert np.allclose(vec.psi, np.array([1, 1] / np.sqrt(2))) 24 | assert len(vec.dims()) == 1 25 | 26 | def test_basicstates_success(self) -> None: 27 | # minus 28 | vec = Statevec(nqubit=1, data=BasicStates.MINUS) 29 | assert np.allclose(vec.psi, np.array([1, -1] / np.sqrt(2))) 30 | assert len(vec.dims()) == 1 31 | 32 | # zero 33 | vec = Statevec(nqubit=1, data=BasicStates.ZERO) 34 | assert np.allclose(vec.psi, np.array([1, 0]), rtol=0, atol=1e-15) 35 | assert len(vec.dims()) == 1 36 | 37 | # one 38 | vec = Statevec(nqubit=1, data=BasicStates.ONE) 39 | assert np.allclose(vec.psi, np.array([0, 1]), rtol=0, atol=1e-15) 40 | assert len(vec.dims()) == 1 41 | 42 | # plus_i 43 | vec = Statevec(nqubit=1, data=BasicStates.PLUS_I) 44 | assert np.allclose(vec.psi, np.array([1, 1j] / np.sqrt(2))) 45 | assert len(vec.dims()) == 1 46 | 47 | # minus_i 48 | vec = Statevec(nqubit=1, data=BasicStates.MINUS_I) 49 | assert np.allclose(vec.psi, np.array([1, -1j] / np.sqrt(2))) 50 | assert len(vec.dims()) == 1 51 | 52 | # even more tests? 53 | def test_default_tensor_success(self, fx_rng: Generator) -> None: 54 | nqb = fx_rng.integers(2, 5) 55 | print(f"nqb is {nqb}") 56 | vec = Statevec(nqubit=nqb) 57 | assert np.allclose(vec.psi, np.ones((2,) * nqb) / (np.sqrt(2)) ** nqb) 58 | assert len(vec.dims()) == nqb 59 | 60 | vec = Statevec(nqubit=nqb, data=BasicStates.MINUS_I) 61 | sv_list = [BasicStates.MINUS_I.get_statevector() for _ in range(nqb)] 62 | sv = functools.reduce(np.kron, sv_list) 63 | assert np.allclose(vec.psi, sv.reshape((2,) * nqb)) 64 | assert len(vec.dims()) == nqb 65 | 66 | # tensor of same state 67 | rand_angle = fx_rng.random() * 2 * np.pi 68 | rand_plane = fx_rng.choice(np.array(Plane)) 69 | state = PlanarState(rand_plane, rand_angle) 70 | vec = Statevec(nqubit=nqb, data=state) 71 | sv_list = [state.get_statevector() for _ in range(nqb)] 72 | sv = functools.reduce(np.kron, sv_list) 73 | assert np.allclose(vec.psi, sv.reshape((2,) * nqb)) 74 | assert len(vec.dims()) == nqb 75 | 76 | # tensor of different states 77 | rand_angles = fx_rng.random(nqb) * 2 * np.pi 78 | rand_planes = fx_rng.choice(np.array(Plane), nqb) 79 | states = [PlanarState(plane=i, angle=j) for i, j in zip(rand_planes, rand_angles)] 80 | vec = Statevec(nqubit=nqb, data=states) 81 | sv_list = [state.get_statevector() for state in states] 82 | sv = functools.reduce(np.kron, sv_list) 83 | assert np.allclose(vec.psi, sv.reshape((2,) * nqb)) 84 | assert len(vec.dims()) == nqb 85 | 86 | def test_data_success(self, fx_rng: Generator) -> None: 87 | nqb = fx_rng.integers(2, 5) 88 | length = 2**nqb 89 | rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length) 90 | rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2)) 91 | vec = Statevec(data=rand_vec) 92 | assert np.allclose(vec.psi, rand_vec.reshape((2,) * nqb)) 93 | assert len(vec.dims()) == nqb 94 | 95 | # fail: incorrect len 96 | def test_data_dim_fail(self, fx_rng: Generator) -> None: 97 | length = 5 98 | rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length) 99 | rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2)) 100 | with pytest.raises(ValueError): 101 | _vec = Statevec(data=rand_vec) 102 | 103 | # fail: with less qubit than number of qubits inferred from a correct state vect 104 | def test_data_dim_fail_mismatch(self, fx_rng: Generator) -> None: 105 | nqb = 3 106 | rand_vec = fx_rng.random(2**nqb) + 1j * fx_rng.random(2**nqb) 107 | rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2)) 108 | with pytest.raises(ValueError): 109 | _vec = Statevec(nqubit=2, data=rand_vec) 110 | 111 | # fail: not normalized 112 | def test_data_norm_fail(self, fx_rng: Generator) -> None: 113 | nqb = fx_rng.integers(2, 5) 114 | length = 2**nqb 115 | rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length) 116 | with pytest.raises(ValueError): 117 | _vec = Statevec(data=rand_vec) 118 | 119 | def test_defaults_to_one(self) -> None: 120 | vec = Statevec() 121 | assert len(vec.dims()) == 1 122 | 123 | # try copying Statevec input 124 | def test_copy_success(self, fx_rng: Generator) -> None: 125 | nqb = fx_rng.integers(2, 5) 126 | length = 2**nqb 127 | rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length) 128 | rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2)) 129 | test_vec = Statevec(data=rand_vec) 130 | # try to copy it 131 | vec = Statevec(data=test_vec) 132 | 133 | assert np.allclose(vec.psi, test_vec.psi) 134 | assert len(vec.dims()) == len(test_vec.dims()) 135 | 136 | # try calling with incorrect number of qubits compared to inferred one 137 | def test_copy_fail(self, fx_rng: Generator) -> None: 138 | nqb = fx_rng.integers(2, 5) 139 | length = 2**nqb 140 | rand_vec = fx_rng.random(length) + 1j * fx_rng.random(length) 141 | rand_vec /= np.sqrt(np.sum(np.abs(rand_vec) ** 2)) 142 | test_vec = Statevec(data=rand_vec) 143 | 144 | with pytest.raises(ValueError): 145 | _vec = Statevec(nqubit=length - 1, data=test_vec) 146 | -------------------------------------------------------------------------------- /tests/test_transpiler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.random import PCG64, Generator 6 | 7 | from graphix.fundamentals import Plane 8 | from graphix.random_objects import rand_circuit, rand_gate 9 | from graphix.transpiler import Circuit 10 | 11 | 12 | class TestTranspilerUnitGates: 13 | def test_cnot(self, fx_rng: Generator) -> None: 14 | circuit = Circuit(2) 15 | circuit.cnot(0, 1) 16 | pattern = circuit.transpile().pattern 17 | state = circuit.simulate_statevector().statevec 18 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 19 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 20 | 21 | def test_hadamard(self, fx_rng: Generator) -> None: 22 | circuit = Circuit(1) 23 | circuit.h(0) 24 | pattern = circuit.transpile().pattern 25 | state = circuit.simulate_statevector().statevec 26 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 27 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 28 | 29 | def test_s(self, fx_rng: Generator) -> None: 30 | circuit = Circuit(1) 31 | circuit.s(0) 32 | pattern = circuit.transpile().pattern 33 | state = circuit.simulate_statevector().statevec 34 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 35 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 36 | 37 | def test_x(self, fx_rng: Generator) -> None: 38 | circuit = Circuit(1) 39 | circuit.x(0) 40 | pattern = circuit.transpile().pattern 41 | state = circuit.simulate_statevector().statevec 42 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 43 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 44 | 45 | def test_y(self, fx_rng: Generator) -> None: 46 | circuit = Circuit(1) 47 | circuit.y(0) 48 | pattern = circuit.transpile().pattern 49 | state = circuit.simulate_statevector().statevec 50 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 51 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 52 | 53 | def test_z(self, fx_rng: Generator) -> None: 54 | circuit = Circuit(1) 55 | circuit.z(0) 56 | pattern = circuit.transpile().pattern 57 | state = circuit.simulate_statevector().statevec 58 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 59 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 60 | 61 | def test_rx(self, fx_rng: Generator) -> None: 62 | theta = fx_rng.uniform() * 2 * np.pi 63 | circuit = Circuit(1) 64 | circuit.rx(0, theta) 65 | pattern = circuit.transpile().pattern 66 | state = circuit.simulate_statevector().statevec 67 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 68 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 69 | 70 | def test_ry(self, fx_rng: Generator) -> None: 71 | theta = fx_rng.uniform() * 2 * np.pi 72 | circuit = Circuit(1) 73 | circuit.ry(0, theta) 74 | pattern = circuit.transpile().pattern 75 | state = circuit.simulate_statevector().statevec 76 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 77 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 78 | 79 | def test_rz(self, fx_rng: Generator) -> None: 80 | theta = fx_rng.uniform() * 2 * np.pi 81 | circuit = Circuit(1) 82 | circuit.rz(0, theta) 83 | pattern = circuit.transpile().pattern 84 | state = circuit.simulate_statevector().statevec 85 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 86 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 87 | 88 | def test_i(self, fx_rng: Generator) -> None: 89 | circuit = Circuit(1) 90 | circuit.i(0) 91 | pattern = circuit.transpile().pattern 92 | state = circuit.simulate_statevector().statevec 93 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 94 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 95 | 96 | @pytest.mark.parametrize("jumps", range(1, 11)) 97 | def test_ccx(self, fx_bg: PCG64, jumps: int) -> None: 98 | rng = Generator(fx_bg.jumped(jumps)) 99 | nqubits = 4 100 | depth = 6 101 | circuit = rand_circuit(nqubits, depth, rng, use_ccx=True) 102 | pattern = circuit.transpile().pattern 103 | pattern.minimize_space() 104 | state = circuit.simulate_statevector().statevec 105 | state_mbqc = pattern.simulate_pattern(rng=rng) 106 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 107 | 108 | def test_transpiled(self, fx_rng: Generator) -> None: 109 | nqubits = 2 110 | depth = 1 111 | pairs = [(i, np.mod(i + 1, nqubits)) for i in range(nqubits)] 112 | circuit = rand_gate(nqubits, depth, pairs, fx_rng, use_rzz=True) 113 | pattern = circuit.transpile().pattern 114 | state = circuit.simulate_statevector().statevec 115 | state_mbqc = pattern.simulate_pattern(rng=fx_rng) 116 | assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1) 117 | 118 | def test_measure(self) -> None: 119 | circuit = Circuit(2) 120 | circuit.h(1) 121 | circuit.cnot(0, 1) 122 | circuit.m(0, Plane.XY, 0.5) 123 | _ = circuit.transpile() 124 | 125 | def simulate_and_measure() -> int: 126 | circuit_simulate = circuit.simulate_statevector() 127 | assert circuit_simulate.classical_measures[0] == (circuit_simulate.statevec.psi[0][1].imag > 0) 128 | return circuit_simulate.classical_measures[0] 129 | 130 | nb_shots = 10000 131 | count = sum(1 for _ in range(nb_shots) if simulate_and_measure()) 132 | assert abs(count - nb_shots / 2) < nb_shots / 20 133 | 134 | def test_add_extend(self) -> None: 135 | circuit = Circuit(3) 136 | circuit.ccx(0, 1, 2) 137 | circuit.rzz(0, 1, 2) 138 | circuit.cnot(0, 1) 139 | circuit.swap(0, 1) 140 | circuit.h(0) 141 | circuit.s(0) 142 | circuit.x(0) 143 | circuit.y(0) 144 | circuit.z(0) 145 | circuit.i(0) 146 | circuit.m(0, Plane.XY, 0.5) 147 | circuit.rx(1, 0.5) 148 | circuit.ry(2, 0.5) 149 | circuit.rz(1, 0.5) 150 | circuit2 = Circuit(3, instr=circuit.instruction) 151 | assert circuit.instruction == circuit2.instruction 152 | -------------------------------------------------------------------------------- /tests/test_visualization.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import networkx as nx 4 | 5 | from graphix import gflow, transpiler, visualization 6 | 7 | 8 | def test_get_pos_from_flow(): 9 | circuit = transpiler.Circuit(1) 10 | circuit.h(0) 11 | pattern = circuit.transpile().pattern 12 | nodes, edges = pattern.get_graph() 13 | g = nx.Graph() 14 | g.add_nodes_from(nodes) 15 | g.add_edges_from(edges) 16 | vin = pattern.input_nodes if pattern.input_nodes is not None else [] 17 | vout = pattern.output_nodes 18 | meas_planes = pattern.get_meas_plane() 19 | meas_angles = pattern.get_angles() 20 | local_clifford = pattern.get_vops() 21 | vis = visualization.GraphVisualizer(g, vin, vout, meas_planes, meas_angles, local_clifford) 22 | f, l_k = gflow.find_flow(g, set(vin), set(vout), meas_planes) 23 | pos = vis.get_pos_from_flow(f, l_k) 24 | assert pos is not None 25 | --------------------------------------------------------------------------------