├── tests
├── ir
│ ├── __init__.py
│ ├── test_non_unitary.py
│ ├── ir_equality_test_base.py
│ ├── test_two_qubit_gate.py
│ └── semantics
│ │ ├── test_canonical_gate_semantic.py
│ │ └── test_matrix_gate_semantic.py
├── instructions
│ ├── test_instructions.py
│ ├── test_init.py
│ ├── test_reset.py
│ ├── test_wait.py
│ └── test_measure.py
├── integration
│ ├── __init__.py
│ ├── test_rydberg.py
│ └── test_starmon_7.py
├── __init__.py
├── passes
│ ├── mapper
│ │ ├── test_mapping.py
│ │ ├── test_general_mapper.py
│ │ └── test_qubit_remapper.py
│ ├── validator
│ │ ├── test_primitive_gate_validator.py
│ │ └── test_interaction_validator.py
│ └── decomposer
│ │ ├── test_xzx_decomposer.py
│ │ ├── test_yxy_decomposer.py
│ │ ├── test_xyx_decomposer.py
│ │ ├── test_cz_decomposer.py
│ │ ├── test_yzy_decomposer.py
│ │ ├── test_cnot_decomposer.py
│ │ ├── test_zyz_decomposer.py
│ │ ├── test_swap2cnot_decomposer.py
│ │ ├── test_cnot2cz_decomposer.py
│ │ ├── test_aba_decomposer.py
│ │ ├── test_zxz_decomposer.py
│ │ └── test_swap2cz_decomposer.py
├── test_registers.py
├── docs
│ └── compilation-passes
│ │ ├── test_mapper.py
│ │ ├── test_merger.py
│ │ └── test_validator.py
├── reindexer
│ └── test_qubit_reindexer.py
├── test_circuit_matrix_calculator.py
├── test_circuit.py
├── test_asm_declaration.py
└── utils
│ └── test_matrix_expander.py
├── opensquirrel
├── passes
│ ├── __init__.py
│ ├── exporter
│ │ ├── cqasmv1_exporter
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ └── general_exporter.py
│ ├── merger
│ │ ├── __init__.py
│ │ └── single_qubit_gates_merger.py
│ ├── router
│ │ ├── __init__.py
│ │ ├── general_router.py
│ │ ├── shortest_path_router.py
│ │ ├── heuristics.py
│ │ └── astar_router.py
│ ├── validator
│ │ ├── __init__.py
│ │ ├── general_validator.py
│ │ ├── primitive_gate_validator.py
│ │ └── interaction_validator.py
│ ├── mapper
│ │ ├── __init__.py
│ │ ├── general_mapper.py
│ │ ├── utils.py
│ │ ├── mapping.py
│ │ ├── check_mapper.py
│ │ ├── simple_mappers.py
│ │ └── qubit_remapper.py
│ └── decomposer
│ │ ├── swap2cnot_decomposer.py
│ │ ├── __init__.py
│ │ ├── cnot2cz_decomposer.py
│ │ ├── swap2cz_decomposer.py
│ │ ├── cnot_decomposer.py
│ │ ├── mckay_decomposer.py
│ │ ├── cz_decomposer.py
│ │ └── general_decomposer.py
├── writer
│ └── __init__.py
├── reader
│ └── __init__.py
├── reindexer
│ ├── __init__.py
│ └── qubit_reindexer.py
├── utils
│ ├── list.py
│ ├── identity_filter.py
│ ├── __init__.py
│ ├── context.py
│ └── general_math.py
├── ir
│ ├── semantics
│ │ ├── gate_semantic.py
│ │ ├── __init__.py
│ │ ├── controlled_gate.py
│ │ ├── matrix_gate.py
│ │ └── canonical_gate.py
│ ├── default_gates
│ │ └── __init__.py
│ ├── __init__.py
│ ├── statement.py
│ ├── unitary.py
│ ├── control_instruction.py
│ ├── two_qubit_gate.py
│ └── non_unitary.py
├── exceptions.py
├── __init__.py
├── circuit_matrix_calculator.py
├── default_gate_modifiers.py
├── common.py
└── default_instructions.py
├── docs
├── circuit-builder
│ ├── index.md
│ └── instructions
│ │ ├── control-instructions.md
│ │ ├── non-unitaries.md
│ │ └── gates.md
├── compilation-passes
│ ├── decomposition
│ │ ├── cnot-decomposer.md
│ │ ├── cz-decomposer.md
│ │ ├── mckay-decomposer.md
│ │ ├── index.md
│ │ ├── aba-decomposer.md
│ │ └── predefined-decomposers.md
│ ├── mapping
│ │ ├── hardcoded-mapper.md
│ │ ├── random-mapper.md
│ │ ├── identity-mapper.md
│ │ ├── index.md
│ │ └── qgym-mapper.md
│ ├── validation
│ │ ├── index.md
│ │ ├── interaction-validator.md
│ │ └── primitive-gate-validator.md
│ ├── merging
│ │ └── index.md
│ ├── exporting
│ │ └── index.md
│ ├── routing
│ │ ├── index.md
│ │ └── shortest-path-router.md
│ └── index.md
├── _static
│ ├── cnot2cz.png
│ ├── swap2cnot.png
│ ├── swap2cz.png
│ ├── terminal_icon.png
│ ├── overview_diagram.png
│ ├── devcontainer_docker_icon.png
│ └── devcontainer_docker_icon_2.png
├── installation.md
├── javascripts
│ └── mathjax.js
├── index.md
└── tutorial
│ └── writing-out-and-exporting.md
├── data
├── qgym_mapper
│ ├── TRPO_tuna5_2e5.zip
│ └── TRPO_starmon7_5e5.zip
└── static.json
├── .devcontainer
├── devcontainer.json
└── Dockerfile
├── .vscode
├── settings.json
└── launch.json
├── .github
├── dependabot.yaml
└── workflows
│ ├── docs.yml
│ ├── release.yaml
│ └── tests.yaml
├── example
└── README.md
├── .editorconfig
├── tox.ini
├── scripts
└── gen_reference_page.py
└── CONTRIBUTING.md
/tests/ir/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opensquirrel/passes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opensquirrel/writer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/instructions/test_instructions.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/opensquirrel/passes/exporter/cqasmv1_exporter/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/circuit-builder/index.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/cnot-decomposer.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/cz-decomposer.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/mckay-decomposer.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
--------------------------------------------------------------------------------
/tests/integration/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | DataType = dict[str, Any]
4 |
--------------------------------------------------------------------------------
/docs/_static/cnot2cz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/cnot2cz.png
--------------------------------------------------------------------------------
/docs/_static/swap2cnot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/swap2cnot.png
--------------------------------------------------------------------------------
/docs/_static/swap2cz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/swap2cz.png
--------------------------------------------------------------------------------
/docs/compilation-passes/mapping/hardcoded-mapper.md:
--------------------------------------------------------------------------------
1 | With this mapper, the initial mapping is simply hardcoded.
2 |
--------------------------------------------------------------------------------
/docs/_static/terminal_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/terminal_icon.png
--------------------------------------------------------------------------------
/docs/_static/overview_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/overview_diagram.png
--------------------------------------------------------------------------------
/data/qgym_mapper/TRPO_tuna5_2e5.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/data/qgym_mapper/TRPO_tuna5_2e5.zip
--------------------------------------------------------------------------------
/data/qgym_mapper/TRPO_starmon7_5e5.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/data/qgym_mapper/TRPO_starmon7_5e5.zip
--------------------------------------------------------------------------------
/docs/_static/devcontainer_docker_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/devcontainer_docker_icon.png
--------------------------------------------------------------------------------
/docs/_static/devcontainer_docker_icon_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuTech-Delft/OpenSquirrel/HEAD/docs/_static/devcontainer_docker_icon_2.png
--------------------------------------------------------------------------------
/docs/compilation-passes/mapping/random-mapper.md:
--------------------------------------------------------------------------------
1 | This mapper pass generates a random initial mapping from virtual qubits from physical qubits.
2 |
--------------------------------------------------------------------------------
/opensquirrel/reader/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.reader.libqasm_parser import LibQasmParser
2 |
3 | __all__ = [
4 | "LibQasmParser",
5 | ]
6 |
--------------------------------------------------------------------------------
/opensquirrel/reindexer/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.reindexer.qubit_reindexer import get_reindexed_circuit
2 |
3 | __all__ = [
4 | "get_reindexed_circuit",
5 | ]
6 |
--------------------------------------------------------------------------------
/docs/compilation-passes/mapping/identity-mapper.md:
--------------------------------------------------------------------------------
1 | The Identity Mapper simply maps each virtual qubit on the circuit to its corresponding physical qubit on the device,
2 | index-wise.
3 |
--------------------------------------------------------------------------------
/opensquirrel/passes/merger/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.merger.single_qubit_gates_merger import SingleQubitGatesMerger
2 |
3 | __all__ = [
4 | "SingleQubitGatesMerger",
5 | ]
6 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OpenSquirrel",
3 | "build": {
4 | "dockerfile": "Dockerfile",
5 | "args": {
6 | "USER": "pydev"
7 | }
8 | },
9 | "remoteUser": "pydev"
10 | }
11 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pathlib
3 |
4 | PROJECT_ROOT_PATH = pathlib.Path(__file__).parents[1]
5 |
6 | with pathlib.Path(PROJECT_ROOT_PATH / "data" / "static.json").open("r") as f:
7 | STATIC_DATA = json.load(f)
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.testing.pytestEnabled": true,
3 | "python.testing.pytestArgs": [
4 | "-p", "no:cov",
5 | "tests"
6 | ],
7 | "python.testing.unittestEnabled": false,
8 | "python.testing.nosetestsEnabled": false
9 | }
10 |
--------------------------------------------------------------------------------
/opensquirrel/passes/router/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.router.astar_router import AStarRouter
2 | from opensquirrel.passes.router.shortest_path_router import ShortestPathRouter
3 |
4 | __all__ = [
5 | "AStarRouter",
6 | "ShortestPathRouter",
7 | ]
8 |
--------------------------------------------------------------------------------
/opensquirrel/utils/list.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import functools
4 | import operator
5 | from typing import Any
6 |
7 |
8 | def flatten_list(list_to_flatten: list[list[Any]]) -> list[Any]:
9 | return functools.reduce(operator.iadd, list_to_flatten, [])
10 |
--------------------------------------------------------------------------------
/opensquirrel/passes/validator/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.validator.interaction_validator import InteractionValidator
2 | from opensquirrel.passes.validator.primitive_gate_validator import PrimitiveGateValidator
3 |
4 | __all__ = [
5 | "InteractionValidator",
6 | "PrimitiveGateValidator",
7 | ]
8 |
--------------------------------------------------------------------------------
/opensquirrel/passes/exporter/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.exporter.cqasmv1_exporter.cqasmv1_exporter import CqasmV1Exporter
2 | from opensquirrel.passes.exporter.quantify_scheduler_exporter import QuantifySchedulerExporter
3 |
4 | __all__ = [
5 | "CqasmV1Exporter",
6 | "QuantifySchedulerExporter",
7 | ]
8 |
--------------------------------------------------------------------------------
/opensquirrel/ir/semantics/gate_semantic.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from opensquirrel.ir import IRNode
4 |
5 |
6 | class GateSemantic(IRNode, ABC):
7 | @abstractmethod
8 | def is_identity(self) -> bool:
9 | pass
10 |
11 | @abstractmethod
12 | def __repr__(self) -> str:
13 | pass
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | reviewers:
8 | - QuTech-Delft/cqasm
9 | - package-ecosystem: "pip"
10 | directory: "/"
11 | schedule:
12 | interval: "daily"
13 | reviewers:
14 | - QuTech-Delft/cqasm
15 |
--------------------------------------------------------------------------------
/opensquirrel/utils/identity_filter.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Iterable
4 | from typing import TYPE_CHECKING
5 |
6 | if TYPE_CHECKING:
7 | from opensquirrel.ir import Gate
8 |
9 |
10 | def filter_out_identities(gates: Iterable[Gate]) -> list[Gate]:
11 | return [gate for gate in gates if not gate.is_identity()]
12 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | _OpenSquirrel_ is available through the Python Package Index ([PyPI]()).
4 |
5 | Accordingly, installation is as easy as ABC:
6 | ```shell
7 | $ pip install opensquirrel
8 | ```
9 |
10 | You can check if the package is installed by importing it:
11 | ```python
12 | import opensquirrel
13 | ```
14 |
--------------------------------------------------------------------------------
/opensquirrel/passes/exporter/general_exporter.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 | from opensquirrel import Circuit
5 |
6 |
7 | class Exporter(ABC):
8 | def __init__(self, **kwargs: Any) -> None:
9 | """Generic router class"""
10 |
11 | @abstractmethod
12 | def export(self, circuit: Circuit) -> Any:
13 | raise NotImplementedError
14 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.mapper.mip_mapper import MIPMapper
2 | from opensquirrel.passes.mapper.qgym_mapper import QGymMapper
3 | from opensquirrel.passes.mapper.simple_mappers import HardcodedMapper, IdentityMapper, RandomMapper
4 |
5 | __all__ = [
6 | "HardcodedMapper",
7 | "IdentityMapper",
8 | "MIPMapper",
9 | "QGymMapper",
10 | "RandomMapper",
11 | ]
12 |
--------------------------------------------------------------------------------
/docs/compilation-passes/validation/index.md:
--------------------------------------------------------------------------------
1 | Validator passes in OpenSquirrel are meant to provide some tools to check whether a quantum circuit is
2 | executable given the constraints imposed by the target backend.
3 | OpenSquirrel facilitates the following validation passes:
4 |
5 | - [Interaction validator](interaction-validator.md) (`InteractionValidator`)
6 | - [Primitive gate validator](primitive-gate-validator.md) (`PrimitiveGateValidator`)
7 |
--------------------------------------------------------------------------------
/opensquirrel/passes/validator/general_validator.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 | from opensquirrel.ir import IR
5 |
6 |
7 | class Validator(ABC):
8 | def __init__(self, **kwargs: Any) -> None: ...
9 |
10 | @abstractmethod
11 | def validate(self, ir: IR) -> None:
12 | """Base validate method to be implemented by inheriting validator classes."""
13 | raise NotImplementedError
14 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Python: Debug Pytest (No Coverage)",
6 | "type": "python",
7 | "request": "launch",
8 | "module": "pytest",
9 | "args": [
10 | "-p", "no:cov",
11 | "tests",
12 | "-v",
13 | "-s"
14 | ],
15 | "console": "integratedTerminal",
16 | "justMyCode": true
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/tests/passes/mapper/test_mapping.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel.passes.mapper.mapping import Mapping
4 |
5 |
6 | class TestMapping:
7 | def test_1_physical_qubit(self) -> None:
8 | Mapping([0])
9 |
10 | def test_2_physical_qubits(self) -> None:
11 | Mapping([0, 1])
12 |
13 | def test_incorrect(self) -> None:
14 | with pytest.raises(ValueError, match="the mapping is incorrect"):
15 | Mapping([0, 2])
16 |
--------------------------------------------------------------------------------
/docs/javascripts/mathjax.js:
--------------------------------------------------------------------------------
1 | window.MathJax = {
2 | tex: {
3 | inlineMath: [["\\(", "\\)"], ["$", "$"]],
4 | displayMath: [["\\[", "\\]"], ["$$", "$$"]],
5 | processEscapes: true,
6 | processEnvironments: true
7 | },
8 | options: {
9 | ignoreHtmlClass: ".*|",
10 | processHtmlClass: "arithmatex"
11 | }
12 | };
13 |
14 | document$.subscribe(() => {
15 | MathJax.startup.output.clearCache()
16 | MathJax.typesetClear()
17 | MathJax.texReset()
18 | MathJax.typesetPromise()
19 | })
20 |
--------------------------------------------------------------------------------
/opensquirrel/passes/router/general_router.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 | from opensquirrel import Connectivity
5 | from opensquirrel.ir import IR
6 |
7 |
8 | class Router(ABC):
9 | def __init__(self, connectivity: Connectivity, **kwargs: Any) -> None:
10 | self._connectivity = connectivity
11 | """Generic router class"""
12 |
13 | @abstractmethod
14 | def route(self, ir: IR, qubit_register_size: int) -> IR:
15 | raise NotImplementedError
16 |
--------------------------------------------------------------------------------
/opensquirrel/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.utils.general_math import acos, are_axes_consecutive
2 | from opensquirrel.utils.identity_filter import filter_out_identities
3 | from opensquirrel.utils.list import flatten_list
4 | from opensquirrel.utils.matrix_expander import can1, expand_ket, get_matrix, get_reduced_ket
5 |
6 | __all__ = [
7 | "acos",
8 | "are_axes_consecutive",
9 | "can1",
10 | "expand_ket",
11 | "filter_out_identities",
12 | "flatten_list",
13 | "get_matrix",
14 | "get_matrix",
15 | "get_reduced_ket",
16 | ]
17 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 |
2 | The documentation for the code can be found in various Jupyter notebooks located in the `tutorials` folder.
3 |
4 | `Jupyter` and `sympy` are required to access the notebooks. These can be installed all at once by running:
5 |
6 | ```shell
7 | $ pip install opensquirrel[examples]
8 | ```
9 |
10 | Alternatively, one can install both packages manually:
11 | ```shell
12 | $ pip install jupyter sympy
13 | ```
14 |
15 | To open the `Jupyter` notebooks, one can run
16 |
17 | ```shell
18 | jupyter notebook
19 | ```
20 |
21 | in the `example/tutorials` folder.
22 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | charset = utf-8
12 |
13 | # Override for python
14 | [*.py]
15 | indent_style = space
16 | indent_size = 4
17 |
18 | # Override for markdown
19 | [*.{md}]
20 | trim_trailing_whitespace = false
21 | indent_style = space
22 | indent_size = 2
23 |
24 | # Override for Vue
25 | [*.{js,json*}]
26 | indent_style = space
27 | indent_size = 2
28 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/general_mapper.py:
--------------------------------------------------------------------------------
1 | """This module contains generic mapping components."""
2 |
3 | from __future__ import annotations
4 |
5 | from abc import ABC, abstractmethod
6 | from typing import TYPE_CHECKING, Any
7 |
8 | if TYPE_CHECKING:
9 | from opensquirrel.ir import IR
10 | from opensquirrel.passes.mapper.mapping import Mapping
11 |
12 |
13 | class Mapper(ABC):
14 | """Base class for the Mapper pass."""
15 |
16 | def __init__(self, **kwargs: Any) -> None: ...
17 |
18 | @abstractmethod
19 | def map(self, ir: IR, qubit_register_size: int) -> Mapping:
20 | raise NotImplementedError
21 |
--------------------------------------------------------------------------------
/opensquirrel/passes/router/shortest_path_router.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import networkx as nx
4 |
5 | from opensquirrel import Connectivity
6 | from opensquirrel.ir import IR
7 | from opensquirrel.passes.router.common import PathFinderType, ProcessSwaps
8 | from opensquirrel.passes.router.general_router import Router
9 |
10 |
11 | class ShortestPathRouter(Router):
12 | def __init__(self, connectivity: Connectivity, **kwargs: Any) -> None:
13 | super().__init__(connectivity, **kwargs)
14 |
15 | def route(self, ir: IR, qubit_register_size: int) -> IR:
16 | pathfinder: PathFinderType = nx.shortest_path
17 | return ProcessSwaps.process_swaps(ir, qubit_register_size, self._connectivity, pathfinder)
18 |
--------------------------------------------------------------------------------
/docs/compilation-passes/merging/index.md:
--------------------------------------------------------------------------------
1 | Merger passes in OpenSquirrel are used to merge gates into single operations.
2 | Their main purpose is to reduce the circuit depth.
3 |
4 | Note that the gate that results from merging two gates will in general be an arbitrary operation, _i.e._,
5 | not be a _known_ gate.
6 | In most cases, subsequent [decomposition](../decomposition/index.md) of the gates will be required in order to execute
7 | the circuit on a target backend.
8 | The kind of decomposition pass required will depend on the primitive gate set that the intended backend supports.
9 |
10 | OpenSquirrel currently facilitates the following merge pass:
11 |
12 | - [Single-qubit gates merger](single-qubit-gates-merger.md) (`SingleQubitGatesMerger`)
13 |
--------------------------------------------------------------------------------
/opensquirrel/ir/semantics/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.ir.semantics.bsr import (
2 | BlochSphereRotation,
3 | BsrAngleParam,
4 | BsrFullParams,
5 | BsrNoParams,
6 | BsrUnitaryParams,
7 | )
8 | from opensquirrel.ir.semantics.canonical_gate import CanonicalAxis, CanonicalGateSemantic
9 | from opensquirrel.ir.semantics.controlled_gate import ControlledGateSemantic
10 | from opensquirrel.ir.semantics.matrix_gate import MatrixGateSemantic
11 |
12 | __all__ = [
13 | "BlochSphereRotation",
14 | "BsrAngleParam",
15 | "BsrFullParams",
16 | "BsrNoParams",
17 | "BsrUnitaryParams",
18 | "CanonicalAxis",
19 | "CanonicalGateSemantic",
20 | "ControlledGateSemantic",
21 | "MatrixGateSemantic",
22 | ]
23 |
--------------------------------------------------------------------------------
/opensquirrel/utils/context.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Generator
2 | from contextlib import contextmanager
3 | from typing import Any
4 |
5 |
6 | @contextmanager
7 | def temporary_class_attr(cls: type[Any], attr: str, value: Any) -> Generator[None, None, None]:
8 | """Context method to temporarily assign a value to a class attribute.
9 |
10 | The assigned value will only be held within the context.
11 |
12 | Args:
13 | cls: Class of which the class attribute value is to be assigned.
14 | attr: Name of class attribute.
15 | value: Value to assign to class attribute (must be correct type).
16 | """
17 | original_value = getattr(cls, attr)
18 | setattr(cls, attr, value)
19 | try:
20 | yield
21 | finally:
22 | setattr(cls, attr, original_value)
23 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-bookworm
2 |
3 | # Build environment variables
4 | ENV DEBIAN_FRONTEND noninteractive
5 |
6 | # Install system dependencies
7 | RUN apt-get update -y && \
8 | apt-get install -y --no-install-recommends \
9 | sudo && \
10 | apt-get clean
11 |
12 | # Set up user
13 | ARG USER=pydev
14 | ENV PATH /home/${USER}/.local/bin:$PATH
15 | RUN groupadd -r ${USER} && \
16 | useradd -m -r -s /bin/bash -g ${USER} ${USER} && \
17 | echo -n "${USER}:${USER}" | chpasswd && \
18 | echo "${USER} ALL = NOPASSWD: ALL" > /etc/sudoers.d/${USER} && \
19 | chmod 440 /etc/sudoers.d/${USER}
20 |
21 | USER ${USER}
22 |
23 | # Install additional python packages
24 | RUN pip install --upgrade pip wheel setuptools tox pipx
25 |
26 | # Install uv
27 | RUN pipx install uv
28 |
29 | USER root
30 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/utils.py:
--------------------------------------------------------------------------------
1 | import networkx as nx
2 |
3 | from opensquirrel.ir import IR, Gate
4 |
5 |
6 | def make_interaction_graph(ir: IR) -> nx.Graph:
7 | interaction_graph = nx.Graph()
8 | gates = (statement for statement in ir.statements if isinstance(statement, Gate))
9 |
10 | for gate in gates:
11 | target_qubits = gate.get_qubit_operands()
12 | match len(target_qubits):
13 | case 1:
14 | continue
15 |
16 | case 2:
17 | interaction_graph.add_edge(*target_qubits)
18 |
19 | case _:
20 | msg = (
21 | f"the gate {gate} acts on more than 2 qubits. "
22 | "The gate must be decomposed before an interaction graph can be made",
23 | )
24 | raise ValueError(msg)
25 |
26 | return interaction_graph
27 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | isolated_build = false
3 | env_list = lint, type, test
4 | toxworkdir = /var/tmp/opensquirrel/.tox
5 |
6 | [testenv]
7 | skip_install = true
8 | allowlist_externals = uv
9 | parallel_show_output = true
10 | ignore_errors = true
11 | setenv =
12 | UV_USE_ACTIVE_ENV = 1
13 | commands_pre =
14 | uv sync --group export --group qgym_mapper
15 |
16 | [testenv:lint]
17 | description = run linters
18 | commands =
19 | uv run ruff check
20 | uv run ruff format --check
21 |
22 | [testenv:type]
23 | description = run mypy
24 | commands =
25 | uv run mypy opensquirrel tests --strict
26 |
27 | [testenv:fix]
28 | description = run fixing linters
29 | commands =
30 | uv run ruff check --fix
31 | uv run ruff format
32 |
33 | [testenv:test]
34 | description = run unit tests
35 | commands =
36 | uv run pytest . -vv --cov --cov-report=term-missing --cov-report=xml
37 |
--------------------------------------------------------------------------------
/opensquirrel/ir/default_gates/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.ir.default_gates.single_qubit_gates import (
2 | X90,
3 | Y90,
4 | Z90,
5 | H,
6 | I,
7 | MinusX90,
8 | MinusY90,
9 | MinusZ90,
10 | Rn,
11 | Rx,
12 | Ry,
13 | Rz,
14 | S,
15 | SDagger,
16 | T,
17 | TDagger,
18 | U,
19 | X,
20 | Y,
21 | Z,
22 | )
23 | from opensquirrel.ir.default_gates.two_qubit_gates import CNOT, CR, CZ, SWAP, CRk
24 |
25 | __all__ = [
26 | "CNOT",
27 | "CR",
28 | "CZ",
29 | "SWAP",
30 | "X90",
31 | "Y90",
32 | "Z90",
33 | "CRk",
34 | "H",
35 | "I",
36 | "MinusX90",
37 | "MinusY90",
38 | "MinusZ90",
39 | "Rn",
40 | "Rx",
41 | "Ry",
42 | "Rz",
43 | "S",
44 | "SDagger",
45 | "T",
46 | "TDagger",
47 | "U",
48 | "X",
49 | "Y",
50 | "Z",
51 | ]
52 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/swap2cnot_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from opensquirrel import CNOT
6 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
7 |
8 | if TYPE_CHECKING:
9 | from opensquirrel.ir import Gate
10 |
11 |
12 | class SWAP2CNOTDecomposer(Decomposer):
13 | """Predefined decomposition of SWAP gate to 3 CNOT gates.
14 | ---x--- ----•---[X]---•----
15 | | → | | |
16 | ---x--- ---[X]---•---[X]---
17 | Note:
18 | This decomposition preserves the global phase of the SWAP gate.
19 | """
20 |
21 | def decompose(self, gate: Gate) -> list[Gate]:
22 | if gate.name != "SWAP":
23 | return [gate]
24 | qubit0, qubit1 = gate.get_qubit_operands()
25 | return [
26 | CNOT(qubit0, qubit1),
27 | CNOT(qubit1, qubit0),
28 | CNOT(qubit0, qubit1),
29 | ]
30 |
--------------------------------------------------------------------------------
/docs/circuit-builder/instructions/control-instructions.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
3 | # Control Instructions
4 |
5 | | Name | Operator | Description | Example |
6 | |------------|----------------|--------------------------------------------------|-------------------------------------------------------------------------|
7 | | [Barrier](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/control_instructions/barrier_instruction.html) | _barrier_ | Barrier gate | `builder.barrier(0)` |
8 | | [Wait](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/control_instructions/wait_instruction.html) | _wait_ | Wait gate | `builder.wait(0)` |
9 |
--------------------------------------------------------------------------------
/opensquirrel/exceptions.py:
--------------------------------------------------------------------------------
1 | """This module contains all custom exception used by ``OpenSquirrel``."""
2 |
3 | from typing import Any
4 |
5 |
6 | class UnsupportedGateError(Exception):
7 | """Should be raised when a gate is not supported."""
8 |
9 | def __init__(self, gate: Any, *args: Any) -> None:
10 | """Init of the ``UnsupportedGateError``.
11 |
12 | Args:
13 | gate: Gate that is not supported.
14 | """
15 | super().__init__(f"{gate} is not supported", *args)
16 |
17 |
18 | class ExporterError(Exception):
19 | """Should be raised when a circuit cannot be exported to the desired output format."""
20 |
21 |
22 | class NoRoutingPathError(Exception):
23 | """Should be raised when no routing path is available between qubits."""
24 |
25 | def __init__(self, message: str, *args: Any) -> None:
26 | """Init of the ``NoRoutingPathError``.
27 |
28 | Args:
29 | message: Error message.
30 | """
31 | super().__init__(message, *args)
32 |
--------------------------------------------------------------------------------
/tests/test_registers.py:
--------------------------------------------------------------------------------
1 | from opensquirrel import Circuit
2 | from opensquirrel.passes.decomposer import McKayDecomposer
3 | from opensquirrel.passes.merger.single_qubit_gates_merger import SingleQubitGatesMerger
4 |
5 |
6 | def test_qubit_variable_b_and_bit_variable_q() -> None:
7 | circuit = Circuit.from_string(
8 | """
9 | version 3.0
10 |
11 | qubit[2] b
12 | bit[2] q
13 | X b[0]
14 | q[0] = measure b[0]
15 |
16 | H b[1]
17 | CNOT b[1], b[00]
18 | q[1] = measure b[1]
19 | q[0] = measure b[0]
20 | """,
21 | )
22 | circuit.merge(merger=SingleQubitGatesMerger())
23 | circuit.decompose(decomposer=McKayDecomposer())
24 | assert (
25 | str(circuit)
26 | == """version 3.0
27 |
28 | qubit[2] q
29 | bit[2] b
30 |
31 | X90 q[0]
32 | X90 q[0]
33 | b[0] = measure q[0]
34 | Rz(1.5707963) q[1]
35 | X90 q[1]
36 | Rz(1.5707963) q[1]
37 | CNOT q[1], q[0]
38 | b[1] = measure q[1]
39 | b[0] = measure q[0]
40 | """
41 | )
42 |
--------------------------------------------------------------------------------
/opensquirrel/utils/general_math.py:
--------------------------------------------------------------------------------
1 | import cmath
2 | import math
3 |
4 | import numpy as np
5 | from numpy.typing import NDArray
6 |
7 |
8 | def acos(value: float) -> float:
9 | """Fix float approximations like 1.0000000000002, which acos does not like."""
10 | value = max(min(value, 1.0), -1.0)
11 | return math.acos(value)
12 |
13 |
14 | def are_axes_consecutive(axis_a_index: int, axis_b_index: int) -> bool:
15 | """Check if axis 'a' immediately precedes axis 'b' (in a circular fashion [x, y, z, x...])."""
16 | return axis_a_index - axis_b_index in (-1, 2)
17 |
18 |
19 | def matrix_from_u_gate_params(theta: float, phi: float, lmbda: float) -> NDArray[np.complex128]:
20 | """Convert the U-gate to a matrix using its parameters."""
21 | return np.array(
22 | [
23 | [math.cos(theta / 2), -cmath.exp(1j * lmbda) * math.sin(theta / 2)],
24 | [cmath.exp(1j * phi) * math.sin(theta / 2), cmath.exp(1j * (phi + lmbda)) * math.cos(theta / 2)],
25 | ],
26 | dtype=np.complex128,
27 | )
28 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.passes.decomposer.aba_decomposer import (
2 | XYXDecomposer,
3 | XZXDecomposer,
4 | YXYDecomposer,
5 | YZYDecomposer,
6 | ZXZDecomposer,
7 | ZYZDecomposer,
8 | )
9 | from opensquirrel.passes.decomposer.cnot2cz_decomposer import CNOT2CZDecomposer
10 | from opensquirrel.passes.decomposer.cnot_decomposer import CNOTDecomposer
11 | from opensquirrel.passes.decomposer.cz_decomposer import CZDecomposer
12 | from opensquirrel.passes.decomposer.mckay_decomposer import McKayDecomposer
13 | from opensquirrel.passes.decomposer.swap2cnot_decomposer import SWAP2CNOTDecomposer
14 | from opensquirrel.passes.decomposer.swap2cz_decomposer import SWAP2CZDecomposer
15 |
16 | __all__ = [
17 | "CNOT2CZDecomposer",
18 | "CNOTDecomposer",
19 | "CZDecomposer",
20 | "McKayDecomposer",
21 | "SWAP2CNOTDecomposer",
22 | "SWAP2CZDecomposer",
23 | "XYXDecomposer",
24 | "XZXDecomposer",
25 | "YXYDecomposer",
26 | "YZYDecomposer",
27 | "ZXZDecomposer",
28 | "ZYZDecomposer",
29 | ]
30 |
--------------------------------------------------------------------------------
/scripts/gen_reference_page.py:
--------------------------------------------------------------------------------
1 | """
2 | Automatically add all OpenSquirrel Python modules to docs/reference.md whenever the mkdocs documentation is built.
3 | """
4 |
5 | from pathlib import Path
6 |
7 | import mkdocs_gen_files
8 |
9 | nav = mkdocs_gen_files.Nav()
10 |
11 | root = Path(__file__).parent.parent
12 | src = root / "opensquirrel"
13 |
14 | for path in sorted(src.rglob("*.py")):
15 | module_path = path.relative_to(root).with_suffix("")
16 | doc_path = path.relative_to(src).with_suffix(".md")
17 | full_doc_path = Path("reference", doc_path)
18 |
19 | parts = tuple(module_path.parts)
20 |
21 | if parts[-1] in ("__init__", "__main__"):
22 | continue
23 |
24 | nav[parts] = doc_path.as_posix()
25 |
26 | with mkdocs_gen_files.open(full_doc_path, "w") as fd:
27 | ident = ".".join(parts)
28 | fd.write(f"::: {ident}")
29 |
30 | mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))
31 |
32 | with mkdocs_gen_files.open("reference/reference.md", "w") as nav_file:
33 | nav_file.writelines(nav.build_literate_nav())
34 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/cnot2cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | from opensquirrel import CZ, Ry
7 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
8 |
9 | if TYPE_CHECKING:
10 | from opensquirrel.ir import Gate
11 |
12 |
13 | class CNOT2CZDecomposer(Decomposer):
14 | """Predefined decomposition of CNOT gate to CZ gate with Y rotations.
15 |
16 | ---•--- -----------------•----------------
17 | | → |
18 | ---⊕--- --[Ry(-pi/2)]---[Z]---[Ry(pi/2)]--
19 |
20 | Note:
21 | This decomposition preserves the global phase of the CNOT gate.
22 | """
23 |
24 | def decompose(self, gate: Gate) -> list[Gate]:
25 | if gate.name != "CNOT":
26 | return [gate]
27 |
28 | control_qubit, target_qubit = gate.get_qubit_operands()
29 | return [
30 | Ry(target_qubit, -pi / 2),
31 | CZ(control_qubit, target_qubit),
32 | Ry(target_qubit, pi / 2),
33 | ]
34 |
--------------------------------------------------------------------------------
/data/static.json:
--------------------------------------------------------------------------------
1 | {
2 | "backends": {
3 | "spin-2-plus": {
4 | "connectivity": {
5 | "0": [1],
6 | "1": [0]
7 | },
8 | "primitive_gate_set": ["I", "X90", "mX90", "Y90", "mY90", "Rz", "CZ", "measure", "wait", "init", "barrier"]
9 | },
10 | "starmon-7": {
11 | "connectivity": {
12 | "0": [2, 3],
13 | "1": [3, 4],
14 | "2": [0, 5],
15 | "3": [0, 1, 5, 6],
16 | "4": [1, 6],
17 | "5": [2, 3],
18 | "6": [3, 4]
19 | },
20 | "primitive_gate_set": ["I", "H", "X", "X90", "mX90", "Y", "Y90", "mY90", "Z", "S", "Sdag", "T", "Tdag", "Rx", "Ry", "Rz", "CNOT", "CZ", "CR", "CRk", "SWAP", "measure", "wait", "init", "barrier"]
21 | },
22 | "tuna-5": {
23 | "connectivity": {
24 | "0": [2],
25 | "1": [2],
26 | "2": [0, 1, 3, 4],
27 | "3": [2],
28 | "4": [2]
29 | },
30 | "primitive_gate_set": ["I", "X", "X90", "mX90", "Y", "Y90", "mY90", "Z", "S", "Sdag", "T", "Tdag", "Rx", "Ry", "Rz", "CNOT", "CZ", "measure", "wait", "init", "barrier"]
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/instructions/test_init.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import Circuit, CircuitBuilder
4 | from opensquirrel.ir import Init
5 |
6 |
7 | @pytest.mark.parametrize(
8 | ("cqasm_string", "expected_result"),
9 | [
10 | (
11 | "version 3.0; qubit[2] q; init q[1]; init q[0]",
12 | "version 3.0\n\nqubit[2] q\n\ninit q[1]\ninit q[0]\n",
13 | ),
14 | (
15 | "version 3.0; qubit[4] q; init q[2:3]; init q[1, 0]",
16 | "version 3.0\n\nqubit[4] q\n\ninit q[2]\ninit q[3]\ninit q[1]\ninit q[0]\n",
17 | ),
18 | ],
19 | ids=["init", "init sgmq"],
20 | )
21 | def test_init_as_cqasm_string(cqasm_string: str, expected_result: str) -> None:
22 | circuit = Circuit.from_string(cqasm_string)
23 | assert str(circuit) == expected_result
24 |
25 |
26 | def test_init_in_circuit_builder() -> None:
27 | builder = CircuitBuilder(2)
28 | builder.init(0).init(1)
29 | circuit = builder.to_circuit()
30 | assert circuit.qubit_register_size == 2
31 | assert circuit.qubit_register_name == "q"
32 | assert circuit.ir.statements == [Init(0), Init(1)]
33 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/index.md:
--------------------------------------------------------------------------------
1 | _Gate decomposition_ is a fundamental process in quantum compilation that involves breaking down complex quantum gates
2 | into a sequence of simpler, more elementary gates that can be directly implemented on quantum hardware.
3 | This step is crucial because most quantum processors can only perform a limited set of basic operations,
4 | such as single-qubit rotations and two-qubit entangling gates like the `CNOT` gate.
5 |
6 | The importance of gate decomposition lies in its ability to translate high-level quantum algorithms into executable
7 | instructions for quantum hardware. By decomposing complex gates into a series of elementary gates,
8 | the quantum compiler ensures that the algorithm can be run on the available hardware,
9 | regardless of its specific constraints and capabilities.
10 | This process ensures that the quantum algorithm is broken down into a series of gates that match the native gate set of
11 | the hardware.
12 |
13 | More in depth decomposition tutorials can be found in the [decomposition example Jupyter notebook](https://github.com/QuTech-Delft/OpenSquirrel/blob/develop/example/decompositions.ipynb).
14 |
--------------------------------------------------------------------------------
/tests/instructions/test_reset.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import Circuit, CircuitBuilder
4 | from opensquirrel.ir import Reset
5 |
6 |
7 | @pytest.mark.parametrize(
8 | ("cqasm_string", "expected_result"),
9 | [
10 | (
11 | "version 3.0; qubit[2] q; reset q[1]; reset q[0]",
12 | "version 3.0\n\nqubit[2] q\n\nreset q[1]\nreset q[0]\n",
13 | ),
14 | (
15 | "version 3.0; qubit[4] q; reset q[2:3]; reset q[1, 0]",
16 | "version 3.0\n\nqubit[4] q\n\nreset q[2]\nreset q[3]\nreset q[1]\nreset q[0]\n",
17 | ),
18 | ],
19 | ids=["reset", "reset sgmq"],
20 | )
21 | def test_reset_as_cqasm_string(cqasm_string: str, expected_result: str) -> None:
22 | circuit = Circuit.from_string(cqasm_string)
23 | assert str(circuit) == expected_result
24 |
25 |
26 | def test_reset_in_circuit_builder() -> None:
27 | builder = CircuitBuilder(2)
28 | builder.reset(0).reset(1)
29 | circuit = builder.to_circuit()
30 | assert circuit.qubit_register_size == 2
31 | assert circuit.qubit_register_name == "q"
32 | assert circuit.ir.statements == [Reset(0), Reset(1)]
33 |
--------------------------------------------------------------------------------
/opensquirrel/ir/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.ir.control_instruction import Barrier, ControlInstruction, Wait
2 | from opensquirrel.ir.expression import Axis, AxisLike, Bit, BitLike, Float, Int, Qubit, QubitLike, String, SupportsStr
3 | from opensquirrel.ir.ir import IR, IRNode, IRVisitor
4 | from opensquirrel.ir.non_unitary import Init, Measure, NonUnitary, Reset
5 | from opensquirrel.ir.semantics.gate_semantic import GateSemantic
6 | from opensquirrel.ir.statement import AsmDeclaration, Instruction, Statement
7 | from opensquirrel.ir.unitary import Gate, Unitary, compare_gates
8 |
9 | __all__ = [
10 | "IR",
11 | "AsmDeclaration",
12 | "Axis",
13 | "AxisLike",
14 | "Barrier",
15 | "Bit",
16 | "BitLike",
17 | "ControlInstruction",
18 | "Float",
19 | "Gate",
20 | "GateSemantic",
21 | "IRNode",
22 | "IRVisitor",
23 | "Init",
24 | "Instruction",
25 | "Int",
26 | "Measure",
27 | "NonUnitary",
28 | "Qubit",
29 | "QubitLike",
30 | "Reset",
31 | "Statement",
32 | "String",
33 | "SupportsStr",
34 | "Unitary",
35 | "Wait",
36 | "compare_gates",
37 | ]
38 |
--------------------------------------------------------------------------------
/opensquirrel/ir/semantics/controlled_gate.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Any
4 |
5 | from opensquirrel.ir.semantics.gate_semantic import GateSemantic
6 |
7 | if TYPE_CHECKING:
8 | from opensquirrel.ir import IRVisitor
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 |
11 |
12 | class ControlledGateSemantic(GateSemantic):
13 | def __init__(self, target_gate: SingleQubitGate) -> None:
14 | self.target_gate = target_gate
15 |
16 | def is_identity(self) -> bool:
17 | return self.target_gate.is_identity()
18 |
19 | def __repr__(self) -> str:
20 | from opensquirrel.default_instructions import default_gate_set
21 |
22 | if self.target_gate.name in default_gate_set:
23 | return f"ControlledGateSemantic(target_gate={self.target_gate.name}(qubit={self.target_gate.qubit.index}))"
24 | return (
25 | f"ControlledGateSemantic(target_gate={self.target_gate.name}"
26 | f"(qubit={self.target_gate.qubit.index}, bsr={self.target_gate.bsr}))"
27 | )
28 |
29 | def accept(self, visitor: IRVisitor) -> Any:
30 | return visitor.visit_controlled_gate_semantic(self)
31 |
--------------------------------------------------------------------------------
/tests/ir/test_non_unitary.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import pytest
4 |
5 | from opensquirrel.ir import Bit, Measure, Qubit
6 |
7 |
8 | class TestMeasure:
9 | @pytest.fixture
10 | def measure(self) -> Measure:
11 | return Measure(42, 42, axis=(0, 0, 1))
12 |
13 | def test_repr(self, measure: Measure) -> None:
14 | expected_repr = "Measure(qubit=Qubit[42], bit=Bit[42], axis=Axis[0. 0. 1.])"
15 | assert repr(measure) == expected_repr
16 |
17 | def test_equality(self, measure: Measure) -> None:
18 | measure_eq = Measure(42, 42, axis=(0, 0, 1))
19 | assert measure == measure_eq
20 |
21 | @pytest.mark.parametrize(
22 | "other_measure",
23 | [Measure(43, 43, axis=(0, 0, 1)), Measure(42, 42, axis=(1, 0, 0)), "test"],
24 | ids=["qubit", "axis", "type"],
25 | )
26 | def test_inequality(self, measure: Measure, other_measure: Measure | str) -> None:
27 | assert measure != other_measure
28 |
29 | def test_get_bit_operands(self, measure: Measure) -> None:
30 | assert measure.get_bit_operands() == [Bit(42)]
31 |
32 | def test_get_qubit_operands(self, measure: Measure) -> None:
33 | assert measure.get_qubit_operands() == [Qubit(42)]
34 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/aba-decomposer.md:
--------------------------------------------------------------------------------
1 | One of the most common single qubit decomposition techniques is the ZYZ decomposition.
2 | This technique decomposes a quantum gate into an `Rz`, `Ry` and `Rz` gate in that order.
3 | The decompositions are found in `opensquirrel.passes.decomposer`,
4 | an example can be seen below where a `H`, `Z`, `Y`, and `Rx` gate are all decomposed on a single qubit circuit.
5 |
6 | ```python
7 | from opensquirrel.circuit_builder import CircuitBuilder
8 | from opensquirrel.passes.decomposer import ZYZDecomposer
9 | import math
10 |
11 | builder = CircuitBuilder(qubit_register_size=1)
12 | builder.H(0).Z(0).Y(0).Rx(0, math.pi / 3)
13 | circuit = builder.to_circuit()
14 |
15 | circuit.decompose(decomposer=ZYZDecomposer())
16 |
17 | print(circuit)
18 | ```
19 | _Output_:
20 |
21 | version 3.0
22 |
23 | qubit[1] q
24 |
25 | Rz(3.1415927) q[0]
26 | Ry(1.5707963) q[0]
27 | Rz(3.1415927) q[0]
28 | Ry(3.1415927) q[0]
29 | Rz(1.5707963) q[0]
30 | Ry(1.0471976) q[0]
31 | Rz(-1.5707963) q[0]
32 |
33 | Similarly, the decomposer can be used on individual gates.
34 |
35 | ```python
36 | from opensquirrel.passes.decomposer import ZYZDecomposer
37 | from opensquirrel import H
38 |
39 | print(ZYZDecomposer().decompose(H(0)))
40 | ```
41 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/swap2cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | from opensquirrel import CZ, Ry
7 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
8 |
9 | if TYPE_CHECKING:
10 | from opensquirrel.ir import Gate
11 |
12 |
13 | class SWAP2CZDecomposer(Decomposer):
14 | """Predefined decomposition of SWAP gate to Ry rotations and 3 CZ gates.
15 | ---x--- -------------•-[Ry(-pi/2)]-•-[Ry(+pi/2)]-•-------------
16 | | → | | |
17 | ---x--- -[Ry(-pi/2)]-•-[Ry(+pi/2)]-•-[Ry(-pi/2)]-•-[Ry(+pi/2)]-
18 | Note:
19 | This decomposition preserves the global phase of the SWAP gate.
20 | """
21 |
22 | def decompose(self, gate: Gate) -> list[Gate]:
23 | if gate.name != "SWAP":
24 | return [gate]
25 | qubit0, qubit1 = gate.get_qubit_operands()
26 | return [
27 | Ry(qubit1, -pi / 2),
28 | CZ(qubit0, qubit1),
29 | Ry(qubit1, pi / 2),
30 | Ry(qubit0, -pi / 2),
31 | CZ(qubit1, qubit0),
32 | Ry(qubit0, pi / 2),
33 | Ry(qubit1, -pi / 2),
34 | CZ(qubit0, qubit1),
35 | Ry(qubit1, pi / 2),
36 | ]
37 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # OpenSquirrel
2 |
3 | This site contains the documentation for OpenSquirrel, _i.e._, a flexible quantum program compiler.
4 | OpenSquirrel chooses a _modular_, over a _configurable_, approach to prepare and optimize quantum circuits for
5 | heterogeneous target architectures.
6 |
7 | It has a user-friendly interface and is straightforwardly extensible with custom-made readers,
8 | compiler passes, and exporters.
9 | As a quantum circuit compiler, it is fully aware of the semantics of each gate and arbitrary quantum gates can be
10 | constructed manually.
11 | It understands the quantum programming language cQASM 3 and will support additional quantum programming languages in the
12 | future.
13 | It is developed in modern Python and follows best practices.
14 |
15 | \[[GitHub repository]()\]
16 | \[[PyPI]()\]
17 |
18 | ## Table of Contents
19 |
20 | - [Tutorial](tutorial/index.md)
21 | - [Circuit builder](circuit-builder/index.md)
22 | - [Compilation passes](compilation-passes/index.md)
23 | - [API documentation](reference/reference.md)
24 |
25 | ## Authors
26 |
27 | Quantum Inspire ()
28 |
29 | ## Acknowledgements
30 |
31 | The Quantum Inspire project (by QuTech: a collaboration of TNO and TU Delft)
32 |
--------------------------------------------------------------------------------
/opensquirrel/__init__.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.circuit import Circuit
2 | from opensquirrel.circuit_builder import CircuitBuilder
3 | from opensquirrel.ir import (
4 | Barrier,
5 | Init,
6 | Measure,
7 | Reset,
8 | Wait,
9 | )
10 | from opensquirrel.ir.default_gates import (
11 | CNOT,
12 | CR,
13 | CZ,
14 | SWAP,
15 | X90,
16 | Y90,
17 | Z90,
18 | CRk,
19 | H,
20 | I,
21 | MinusX90,
22 | MinusY90,
23 | MinusZ90,
24 | Rn,
25 | Rx,
26 | Ry,
27 | Rz,
28 | S,
29 | SDagger,
30 | T,
31 | TDagger,
32 | U,
33 | X,
34 | Y,
35 | Z,
36 | )
37 |
38 | __all__ = [
39 | "CNOT",
40 | "CR",
41 | "CZ",
42 | "SWAP",
43 | "X90",
44 | "Y90",
45 | "Z90",
46 | "Barrier",
47 | "CRk",
48 | "Circuit",
49 | "CircuitBuilder",
50 | "Connectivity",
51 | "H",
52 | "I",
53 | "Init",
54 | "Measure",
55 | "MinusX90",
56 | "MinusY90",
57 | "MinusZ90",
58 | "Reset",
59 | "Rn",
60 | "Rx",
61 | "Ry",
62 | "Rz",
63 | "S",
64 | "SDagger",
65 | "T",
66 | "TDagger",
67 | "U",
68 | "Wait",
69 | "X",
70 | "Y",
71 | "Z",
72 | ]
73 |
74 | from importlib.metadata import version
75 |
76 | __version__ = version("opensquirrel")
77 |
78 | Connectivity = dict[str, list[int]]
79 |
--------------------------------------------------------------------------------
/opensquirrel/passes/validator/primitive_gate_validator.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from opensquirrel.ir import IR, Instruction
4 | from opensquirrel.passes.validator.general_validator import Validator
5 |
6 |
7 | class PrimitiveGateValidator(Validator):
8 | def __init__(self, primitive_gate_set: list[str], **kwargs: Any) -> None:
9 | super().__init__(**kwargs)
10 | self.primitive_gate_set = primitive_gate_set
11 |
12 | def validate(self, ir: IR) -> None:
13 | """
14 | Check if all unitary gates in the circuit are part of the primitive gate set.
15 |
16 | Args:
17 | ir (IR): The intermediate representation of the circuit to be checked.
18 |
19 | Raises:
20 | ValueError: If any unitary gate in the circuit is not part of the primitive gate set.
21 | """
22 | gates_not_in_primitive_gate_set = [
23 | statement.name
24 | for statement in ir.statements
25 | if isinstance(statement, Instruction) and statement.name not in self.primitive_gate_set
26 | ]
27 | if gates_not_in_primitive_gate_set:
28 | unsupported_gates = list(set(gates_not_in_primitive_gate_set))
29 | error_message = "the following gates are not in the primitive gate set: " + ", ".join(unsupported_gates)
30 | raise ValueError(error_message)
31 |
--------------------------------------------------------------------------------
/opensquirrel/circuit_matrix_calculator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import numpy as np
6 | from numpy.typing import NDArray
7 |
8 | from opensquirrel.ir import Gate, IRVisitor
9 | from opensquirrel.utils.matrix_expander import get_matrix
10 |
11 | if TYPE_CHECKING:
12 | from opensquirrel.circuit import Circuit
13 |
14 |
15 | class _CircuitMatrixCalculator(IRVisitor):
16 | def __init__(self, qubit_register_size: int) -> None:
17 | self.qubit_register_size = qubit_register_size
18 | self.matrix = np.eye(1 << self.qubit_register_size, dtype=np.complex128)
19 |
20 | def visit_gate(self, gate: Gate) -> None:
21 | big_matrix = get_matrix(gate, qubit_register_size=self.qubit_register_size)
22 | self.matrix = np.asarray(big_matrix @ self.matrix, dtype=np.complex128)
23 |
24 |
25 | def get_circuit_matrix(circuit: Circuit) -> NDArray[np.complex128]:
26 | """Compute the (large) unitary matrix corresponding to the circuit.
27 |
28 | This matrix has 4**n elements, where n is the number of qubits. Result is stored as a numpy array of complex
29 | numbers.
30 |
31 | Returns:
32 | Matrix representation of the circuit.
33 | """
34 | impl = _CircuitMatrixCalculator(circuit.qubit_register_size)
35 |
36 | circuit.ir.accept(impl)
37 |
38 | return impl.matrix
39 |
--------------------------------------------------------------------------------
/docs/circuit-builder/instructions/non-unitaries.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
3 | # Non-Unitary Instructions
4 |
5 | | Name | Operator | Description | Example |
6 | |------------|----------------|--------------------------------------------------|-------------------------------------------------------------------------|
7 | | [Init](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/non_unitary_instructions/init_instruction.html) | _init_ | Initialize certain qubits in $\|0>$ | `builder.init(0)` |
8 | | [Measure](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/non_unitary_instructions/measure_instruction.html) | _measure_ | Measure qubit argument | `builder.measure(0)` |
9 | | [Reset](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/non_unitary_instructions/reset_instruction.html) | _reset_ | Reset a qubit's state to $\|0>$ | `builder.reset(0)` |
10 |
--------------------------------------------------------------------------------
/tests/docs/compilation-passes/test_mapper.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 |
3 | import pytest
4 |
5 | from opensquirrel import CircuitBuilder
6 | from opensquirrel.passes.mapper import QGymMapper
7 |
8 | if importlib.util.find_spec("qgym") is None:
9 | pytest.skip("qgym not installed; skipping QGym mapper tests", allow_module_level=True)
10 |
11 | if importlib.util.find_spec("stable_baselines3") is None and importlib.util.find_spec("sb3_contrib") is None:
12 | pytest.skip("stable-baselines3 and sb3_contrib not installed; skipping QGym mapper tests", allow_module_level=True)
13 |
14 |
15 | class TestQGymMapper:
16 | def test_qgym_mapper(self) -> None:
17 | agent_path = "data/qgym_mapper/TRPO_tuna5_2e5.zip"
18 |
19 | connectivity = {
20 | "0": [2],
21 | "1": [2],
22 | "2": [0, 1, 3, 4],
23 | "3": [2],
24 | "4": [2],
25 | }
26 |
27 | qgym_mapper = QGymMapper(agent_class="TRPO", agent_path=agent_path, connectivity=connectivity)
28 |
29 | builder = CircuitBuilder(5)
30 | builder.H(0)
31 | builder.CNOT(0, 1)
32 | builder.H(2)
33 | builder.CNOT(1, 2)
34 | builder.CNOT(2, 4)
35 | builder.CNOT(3, 4)
36 | circuit = builder.to_circuit()
37 |
38 | initial_circuit_str = str(circuit)
39 |
40 | circuit.map(mapper=qgym_mapper)
41 |
42 | assert str(circuit) != initial_circuit_str
43 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/mapping.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 |
6 | class Mapping:
7 | """A Mapping is a dictionary where:
8 | - the keys are virtual qubit indices (from 0 to virtual_qubit_register_size-1), and
9 | - the values are physical qubit indices.
10 |
11 | Args:
12 | physical_qubit_register: a list of physical qubit indices.
13 |
14 | Raises:
15 | ValueError: If the mapping is incorrect.
16 | """
17 |
18 | def __init__(self, physical_qubit_register: list[int]) -> None:
19 | self.data: dict[int, int] = dict(enumerate(physical_qubit_register))
20 | if (self.data.keys()) != set(self.data.values()):
21 | msg = "the mapping is incorrect"
22 | raise ValueError(msg)
23 |
24 | def __eq__(self, other: Any) -> bool:
25 | if not isinstance(other, Mapping):
26 | return False
27 | return self.data == other.data
28 |
29 | def __getitem__(self, key: int) -> int:
30 | return self.data[key]
31 |
32 | def __len__(self) -> int:
33 | return len(self.data)
34 |
35 | def size(self) -> int:
36 | return len(self.data)
37 |
38 | def items(self) -> list[tuple[int, int]]:
39 | return list(self.data.items())
40 |
41 | def keys(self) -> list[int]:
42 | return list(self.data.keys())
43 |
44 | def values(self) -> list[int]:
45 | return list(self.data.values())
46 |
--------------------------------------------------------------------------------
/tests/ir/ir_equality_test_base.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from typing import TYPE_CHECKING
5 |
6 | import numpy as np
7 | from numpy.typing import NDArray
8 |
9 | from opensquirrel import Circuit, circuit_matrix_calculator
10 | from opensquirrel.common import are_matrices_equivalent_up_to_global_phase
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import IR
14 |
15 |
16 | def check_equivalence_up_to_global_phase(matrix_a: NDArray[np.complex128], matrix_b: NDArray[np.complex128]) -> None:
17 | assert are_matrices_equivalent_up_to_global_phase(matrix_a, matrix_b)
18 |
19 |
20 | def modify_circuit_and_check(
21 | circuit: Circuit,
22 | action: Callable[[IR, int], None],
23 | expected_circuit: Circuit | None = None,
24 | ) -> None:
25 | """
26 | Checks whether the action preserves:
27 | - the number of qubits,
28 | - the qubit register name(s),
29 | - the circuit matrix up to a global phase factor.
30 | """
31 | # Store matrix before decompositions.
32 | expected_matrix = circuit_matrix_calculator.get_circuit_matrix(circuit)
33 |
34 | action(circuit.ir, circuit.qubit_register_size)
35 |
36 | # Get matrix after decompositions.
37 | actual_matrix = circuit_matrix_calculator.get_circuit_matrix(circuit)
38 |
39 | check_equivalence_up_to_global_phase(actual_matrix, expected_matrix)
40 |
41 | if expected_circuit is not None:
42 | assert circuit == expected_circuit
43 |
--------------------------------------------------------------------------------
/opensquirrel/passes/router/heuristics.py:
--------------------------------------------------------------------------------
1 | # This module defines basic distance metrics that can be used as heuristics in routing algorithms.
2 |
3 | from enum import Enum
4 |
5 |
6 | class DistanceMetric(Enum):
7 | MANHATTAN = "manhattan"
8 | EUCLIDEAN = "euclidean"
9 | CHEBYSHEV = "chebyshev"
10 |
11 |
12 | def calculate_distance(q0_index: int, q1_index: int, num_columns: int, distance_metric: DistanceMetric) -> float:
13 | """
14 | Calculate the distance between two qubits based on the specified distance metric.
15 | Args:
16 | q0_index (int): The index of the first qubit.
17 | q1_index (int): The index of the second qubit.
18 | num_columns (int): The number of columns in the grid.
19 | distance_metric (DistanceMetric): Distance metric to be used (Manhattan, Euclidean, or Chebyshev).
20 | Returns:
21 | float: The distance between the two qubits.
22 | """
23 | x1, y1 = divmod(q0_index, num_columns)
24 | x2, y2 = divmod(q1_index, num_columns)
25 |
26 | match distance_metric:
27 | case DistanceMetric.MANHATTAN:
28 | return abs(x1 - x2) + abs(y1 - y2)
29 |
30 | case DistanceMetric.EUCLIDEAN:
31 | return float(((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5)
32 |
33 | case DistanceMetric.CHEBYSHEV:
34 | return max(abs(x1 - x2), abs(y1 - y2))
35 |
36 | case _:
37 | msg = "Invalid distance metric. Choose Manhattan, Euclidean, or Chebyshev."
38 | raise ValueError(msg)
39 |
--------------------------------------------------------------------------------
/opensquirrel/passes/router/astar_router.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Any
3 |
4 | import networkx as nx
5 |
6 | from opensquirrel import Connectivity
7 | from opensquirrel.ir import IR
8 | from opensquirrel.passes.router.common import PathFinderType, ProcessSwaps
9 | from opensquirrel.passes.router.general_router import Router
10 | from opensquirrel.passes.router.heuristics import DistanceMetric, calculate_distance
11 |
12 |
13 | class AStarRouter(Router):
14 | def __init__(
15 | self, connectivity: Connectivity, distance_metric: DistanceMetric | None = None, **kwargs: Any
16 | ) -> None:
17 | super().__init__(connectivity, **kwargs)
18 | self._distance_metric = distance_metric
19 |
20 | def route(self, ir: IR, qubit_register_size: int) -> IR:
21 | pathfinder: PathFinderType = self._astar_pathfinder
22 | return ProcessSwaps.process_swaps(ir, qubit_register_size, self._connectivity, pathfinder)
23 |
24 | def _astar_pathfinder(self, graph: nx.Graph, source: int, target: int) -> Any:
25 | num_available_qubits = max(graph.nodes) + 1
26 | num_columns = math.ceil(math.sqrt(num_available_qubits))
27 | return nx.astar_path(
28 | graph,
29 | source=source,
30 | target=target,
31 | heuristic=lambda q0_index, q1_index: calculate_distance(
32 | q0_index, q1_index, num_columns, self._distance_metric
33 | )
34 | if self._distance_metric
35 | else None,
36 | )
37 |
--------------------------------------------------------------------------------
/opensquirrel/ir/statement.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 | from opensquirrel.ir.expression import Expression, Qubit, String, SupportsStr
5 | from opensquirrel.ir.ir import IRNode, IRVisitor
6 |
7 |
8 | class Statement(IRNode, ABC):
9 | pass
10 |
11 |
12 | class AsmDeclaration(Statement):
13 | """``AsmDeclaration`` is used to define an assembly declaration statement in the IR.
14 |
15 | Args:
16 | backend_name: Name of the backend that is to process the provided backend code.
17 | backend_code: (Assembly) code to be processed by the specified backend.
18 | """
19 |
20 | def __init__(
21 | self,
22 | backend_name: SupportsStr,
23 | backend_code: SupportsStr,
24 | ) -> None:
25 | self.backend_name = String(backend_name)
26 | self.backend_code = String(backend_code)
27 | Statement.__init__(self)
28 |
29 | def accept(self, visitor: IRVisitor) -> Any:
30 | visitor.visit_statement(self)
31 | return visitor.visit_asm_declaration(self)
32 |
33 |
34 | class Instruction(Statement, ABC):
35 | def __init__(self, name: str) -> None:
36 | self.name = name
37 |
38 | @property
39 | @abstractmethod
40 | def arguments(self) -> tuple[Expression, ...]:
41 | pass
42 |
43 | @abstractmethod
44 | def get_qubit_operands(self) -> list[Qubit]:
45 | pass
46 |
47 | def accept(self, visitor: IRVisitor) -> Any:
48 | return visitor.visit_instruction(self)
49 |
--------------------------------------------------------------------------------
/docs/compilation-passes/mapping/index.md:
--------------------------------------------------------------------------------
1 | _Qubit mapping_, also known as _initial mapping_, is a critical step in the quantum compilation process.
2 | It involves assigning logical qubits, which are used in the quantum algorithm,
3 | to physical qubits available on the quantum hardware.
4 | This mapping is essential because the physical qubits on a quantum processor have specific connectivity constraints,
5 | meaning not all qubits can directly interact with each other.
6 | The initial mapping must respect these constraints to ensure that the required two-qubit gates can be executed without
7 | violating the hardware's connectivity limitations.
8 |
9 | A poor initial mapping can lead to a high number of SWAP operations,
10 | which are used to move qubits into positions where they can interact.
11 | SWAP operations increase the circuit depth and introduce additional errors.
12 | An optimal initial mapping minimizes the need for these operations,
13 | thereby reducing the overall error rate and improving the fidelity of the quantum computation.
14 | Efficient qubit mapping can significantly enhance the performance of the quantum circuit.
15 | By strategically placing qubits, the compiler can reduce the number of additional operations required,
16 | leading to faster and more reliable quantum computations.
17 |
18 | The following mapping passes are available in Opensquirrel:
19 |
20 | - [Hardcoded Mapper](hardcoded-mapper.md) (`HardcodedMapper`)
21 | - [Identity Mapper](identity-mapper.md) (`IdentitiyMapper`)
22 | - [Random Mapper](random-mapper.md) (`RandomMapper`)
23 | - [QGym Mapper](qgym-mapper.md) (`QGymMapper`)
24 |
--------------------------------------------------------------------------------
/tests/ir/test_two_qubit_gate.py:
--------------------------------------------------------------------------------
1 | from math import pi
2 |
3 | import numpy as np
4 | import pytest
5 |
6 | from opensquirrel.ir import Qubit
7 | from opensquirrel.ir.semantics import BlochSphereRotation, ControlledGateSemantic, MatrixGateSemantic
8 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
9 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
10 |
11 |
12 | class TestTwoQubitGate:
13 | @pytest.fixture
14 | def gate(self) -> TwoQubitGate:
15 | cnot_matrix = [
16 | [1, 0, 0, 0],
17 | [0, 1, 0, 0],
18 | [0, 0, 0, 1],
19 | [0, 0, 1, 0],
20 | ]
21 | return TwoQubitGate(42, 100, gate_semantic=MatrixGateSemantic(cnot_matrix))
22 |
23 | def test_get_qubit_operands(self, gate: TwoQubitGate) -> None:
24 | assert gate.get_qubit_operands() == [Qubit(42), Qubit(100)]
25 |
26 | def test_same_qubits(self) -> None:
27 | with pytest.raises(ValueError, match="qubit0 and qubit1 cannot be the same"):
28 | TwoQubitGate(0, 0, gate_semantic=MatrixGateSemantic(np.eye(4, dtype=np.complex128)))
29 |
30 | def test_controlled_gate_with_mismatch_in_target_and_target_gate_qubit(self) -> None:
31 | with pytest.raises(ValueError, match="the qubit from the target gate does not match with 'qubit1'"):
32 | TwoQubitGate(
33 | 0,
34 | 1,
35 | gate_semantic=ControlledGateSemantic(
36 | SingleQubitGate(0, BlochSphereRotation([0, 0, 1], angle=pi, phase=pi / 2))
37 | ),
38 | )
39 |
--------------------------------------------------------------------------------
/opensquirrel/ir/semantics/matrix_gate.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import numpy as np
4 | from numpy.typing import ArrayLike, DTypeLike, NDArray
5 |
6 | from opensquirrel.common import ATOL, repr_round
7 | from opensquirrel.ir import IRVisitor
8 | from opensquirrel.ir.semantics.gate_semantic import GateSemantic
9 |
10 |
11 | class MatrixGateSemantic(GateSemantic):
12 | def __init__(self, matrix: ArrayLike | list[list[int | DTypeLike]]) -> None:
13 | self.matrix = np.asarray(matrix, dtype=np.complex128)
14 |
15 | number_of_rows, number_of_cols = self.matrix.shape
16 | if number_of_cols != number_of_rows:
17 | msg = (
18 | f"incorrect matrix shape. The number of rows should be equal to the number of columns, but"
19 | f"{number_of_rows=} and {number_of_cols=}. "
20 | )
21 | raise ValueError(msg)
22 |
23 | if number_of_cols & (number_of_cols - 1) != 0:
24 | msg = "incorrect matrix shape. The number of rows/columns should be a power of 2."
25 | raise ValueError(msg)
26 |
27 | def is_identity(self) -> bool:
28 | return np.allclose(self.matrix, np.eye(self.matrix.shape[0]), atol=ATOL)
29 |
30 | def __array__(self, *args: Any, **kwargs: Any) -> NDArray[np.complex128]:
31 | return np.asarray(self.matrix, *args, **kwargs)
32 |
33 | def __repr__(self) -> str:
34 | return f"MatrixGateSemantic(matrix={repr_round(self.matrix)})"
35 |
36 | def accept(self, visitor: IRVisitor) -> Any:
37 | return visitor.visit_matrix_gate_semantic(self)
38 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - "develop"
8 | release:
9 | types:
10 | - created
11 |
12 | env:
13 | UV_USE_ACTIVE_ENV: 1
14 |
15 | jobs:
16 | build:
17 | name: Publish documentation
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v6
22 |
23 | - name: Set up Python
24 | uses: actions/setup-python@v6
25 | with:
26 | python-version: "3.11"
27 |
28 | - name: Install uv
29 | uses: yezz123/setup-uv@v4
30 | with:
31 | uv-version: "0.8.12"
32 |
33 | - name: Install package with doc dependencies
34 | run: |
35 | uv sync --group docs
36 |
37 | - name: Configure Git
38 | run: |
39 | git config --global user.name "${{ github.actor }}"
40 | git config --global user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
41 | git fetch origin gh-pages:gh-pages
42 |
43 | - name: Dry run
44 | if: github.event_name == 'pull_request'
45 | run: |
46 | uv run mkdocs build
47 |
48 | - name: Upload and tag as latest
49 | if: github.ref == 'refs/heads/develop'
50 | run: |
51 | uv run mike deploy --push latest
52 | uv run mike set-default --push latest
53 |
54 | - name: Upload and tag as git tag
55 | if: github.event_name == 'release' && github.event.action == 'created'
56 | run: |
57 | uv run mike deploy --push ${{ github.ref_name }}
58 | uv run mike set-default --push latest
59 |
--------------------------------------------------------------------------------
/docs/compilation-passes/exporting/index.md:
--------------------------------------------------------------------------------
1 | Instead of writing the circuit out to the [default cQASM format](https://qutech-delft.github.io/cQASM-spec/),
2 | one can also use a custom exporter pass to export the circuit to a particular output format.
3 |
4 | Exporting can be done by calling the `export` method on the circuit object and providing the desired exporter
5 | `exporter` as an input argument to the call, _e.g._,
6 |
7 | !!! example ""
8 |
9 | ```python
10 | from opensquirrel import CQasmV1Exporter
11 |
12 | exported_circuit = circuit.export(exporter=CQasmV1Exporter)
13 | ```
14 |
15 | As shown in the example above, the exported circuit is given as the return value.
16 |
17 | The following exporting passes are available in OpenSquirrel:
18 |
19 | - [cQASMv1 exporter](cqasm-v1-exporter.md) (`CQasmV1Exporter`)
20 | - [quantify-scheduler exporter](quantify-scheduler-exporter.md) (`QuantifySchedulerExporter`)
21 |
22 | !!! warning "Unsupported language features"
23 |
24 | Note that certain features of the [cQASM language](https://qutech-delft.github.io/cQASM-spec/) may not be supported
25 | by the language to which the circuit is exported.
26 | These features are either processed by the exporter (_e.g._ control instructions),
27 | an error is raised, or some features will simply be lost/ignored and lose their intended effect.
28 | Especially, certain gates may not have a counterpart in the language that is exported to
29 | _e.g._ the general `Rn` gate.
30 | One could circumvent this latter issue by decomposing the circuit into gates that are supported.
31 | Make sure to consult the documentation on the particular exporters to understand the exporting process and result.
32 |
--------------------------------------------------------------------------------
/tests/integration/test_rydberg.py:
--------------------------------------------------------------------------------
1 | from opensquirrel import Circuit
2 |
3 |
4 | class TestRydberg:
5 | def test_circuit(self) -> None:
6 | circuit = Circuit.from_string(
7 | """version 3.0
8 |
9 | qubit[9] q
10 |
11 | asm(Rydberg) '''
12 | INIT(p(0,1)) q[0]
13 | INIT(p(1,1)) q[1]
14 | INIT(p(1,2)) q[2]
15 | INIT(p(2,0)) q[3]
16 | INIT(p(2,1)) q[4]
17 | INIT(p(2,2)) q[5]
18 | INIT(p(3,3)) q[6]
19 | INIT(p(3,1)) q[7]
20 | INIT(p(4,0)) q[8]
21 | '''
22 |
23 | X q
24 |
25 | asm(Rydberg) '''
26 | RG(r(0.00046748015548948326, -0.9711667423688995, 0.15759622123497696)) q[0]
27 | RG(r(0.001868075691355584, -0.9423334847377992, 0.15759622123497696)) q[0]
28 | RG(r(0.004196259096889474, -0.9135002271066988, 0.15759622123497696)) q[0]
29 | '''
30 |
31 | X q
32 | """,
33 | )
34 | # Compiler configuration is yet to be defined for the Rydberg backend.
35 | assert (
36 | str(circuit)
37 | == """version 3.0
38 |
39 | qubit[9] q
40 |
41 | asm(Rydberg) '''
42 | INIT(p(0,1)) q[0]
43 | INIT(p(1,1)) q[1]
44 | INIT(p(1,2)) q[2]
45 | INIT(p(2,0)) q[3]
46 | INIT(p(2,1)) q[4]
47 | INIT(p(2,2)) q[5]
48 | INIT(p(3,3)) q[6]
49 | INIT(p(3,1)) q[7]
50 | INIT(p(4,0)) q[8]
51 | '''
52 | X q[0]
53 | X q[1]
54 | X q[2]
55 | X q[3]
56 | X q[4]
57 | X q[5]
58 | X q[6]
59 | X q[7]
60 | X q[8]
61 | asm(Rydberg) '''
62 | RG(r(0.00046748015548948326, -0.9711667423688995, 0.15759622123497696)) q[0]
63 | RG(r(0.001868075691355584, -0.9423334847377992, 0.15759622123497696)) q[0]
64 | RG(r(0.004196259096889474, -0.9135002271066988, 0.15759622123497696)) q[0]
65 | '''
66 | X q[0]
67 | X q[1]
68 | X q[2]
69 | X q[3]
70 | X q[4]
71 | X q[5]
72 | X q[6]
73 | X q[7]
74 | X q[8]
75 | """
76 | )
77 |
--------------------------------------------------------------------------------
/tests/instructions/test_wait.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import CNOT, Circuit, CircuitBuilder, H
4 | from opensquirrel.ir import Barrier, Wait
5 |
6 |
7 | @pytest.mark.parametrize(
8 | ("cqasm_string", "expected_result"),
9 | [
10 | (
11 | "version 3.0; qubit[2] q; wait(3) q[1]; wait(1) q[0]",
12 | "version 3.0\n\nqubit[2] q\n\nwait(3) q[1]\nwait(1) q[0]\n",
13 | ),
14 | (
15 | "version 3.0; qubit[4] q; wait(3) q[2:3]; wait(1) q[1, 0]",
16 | "version 3.0\n\nqubit[4] q\n\nwait(3) q[2]\nwait(3) q[3]\nwait(1) q[1]\nwait(1) q[0]\n",
17 | ),
18 | ],
19 | ids=["wait", "wait sgmq"],
20 | )
21 | def test_wait_as_cqasm_string(cqasm_string: str, expected_result: str) -> None:
22 | circuit = Circuit.from_string(cqasm_string)
23 | assert str(circuit) == expected_result
24 |
25 |
26 | def test_wait_in_circuit_builder() -> None:
27 | builder = CircuitBuilder(2)
28 | builder.wait(0, 3).wait(1, 1)
29 | circuit = builder.to_circuit()
30 | assert circuit.qubit_register_size == 2
31 | assert circuit.qubit_register_name == "q"
32 | assert circuit.ir.statements == [Wait(0, 3), Wait(1, 1)]
33 |
34 |
35 | def test_wait_in_instruction_context() -> None:
36 | builder = CircuitBuilder(2)
37 | builder.H(0).H(1).wait(0, 1).barrier(1).wait(1, 3).barrier(0).CNOT(0, 1).wait(0, 3)
38 | circuit = builder.to_circuit()
39 | assert circuit.qubit_register_size == 2
40 | assert circuit.qubit_register_name == "q"
41 | assert circuit.ir.statements == [
42 | H(0),
43 | H(1),
44 | Wait(0, 1),
45 | Barrier(1),
46 | Wait(1, 3),
47 | Barrier(0),
48 | CNOT(0, 1),
49 | Wait(0, 3),
50 | ]
51 |
--------------------------------------------------------------------------------
/tests/passes/mapper/test_general_mapper.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Any
4 |
5 | import pytest
6 |
7 | from opensquirrel import Circuit, CircuitBuilder
8 | from opensquirrel.passes.mapper import HardcodedMapper
9 | from opensquirrel.passes.mapper.general_mapper import Mapper
10 | from opensquirrel.passes.mapper.mapping import Mapping
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import IR
14 |
15 |
16 | class TestMapper:
17 | def test_implementation(self) -> None:
18 | class Mapper2(Mapper):
19 | def __init__(self, qubit_register_size: int, **kwargs: Any) -> None:
20 | super().__init__(**kwargs)
21 | self._qubit_register = qubit_register_size
22 |
23 | def map(self, ir: IR, qubit_register_size: int) -> Mapping:
24 | return Mapping(list(range(self._qubit_register)))
25 |
26 | Mapper2(qubit_register_size=1)
27 |
28 |
29 | class TestMapQubits:
30 | @pytest.fixture
31 | def circuit(self) -> Circuit:
32 | builder = CircuitBuilder(3, 1)
33 | builder.H(0)
34 | builder.CNOT(0, 1)
35 | builder.CNOT(1, 2)
36 | builder.measure(0, 0)
37 | return builder.to_circuit()
38 |
39 | @pytest.fixture
40 | def remapped_circuit(self) -> Circuit:
41 | builder = CircuitBuilder(3, 1)
42 | builder.H(1)
43 | builder.CNOT(1, 0)
44 | builder.CNOT(0, 2)
45 | builder.measure(1, 0)
46 | return builder.to_circuit()
47 |
48 | def test_circuit_map(self, circuit: Circuit, remapped_circuit: Circuit) -> None:
49 | mapper = HardcodedMapper(mapping=Mapping([1, 0, 2]))
50 | circuit.map(mapper=mapper)
51 | assert circuit == remapped_circuit
52 |
--------------------------------------------------------------------------------
/docs/compilation-passes/routing/index.md:
--------------------------------------------------------------------------------
1 | The qubit routing pass is a crucial step in the process of quantum compilation.
2 | It ensures that two-qubit interactions can be executed given a certain target backend.
3 |
4 | On quantum processing units (QPUs) qubits are often arranged in specific topologies where only certain pairs of qubits
5 | can directly interact.
6 | Which qubits can interact is given by a mapping called the backend connectivity, _e.g._:
7 |
8 | === "Linear"
9 |
10 | ```python
11 | connectivity = {
12 | "0": [1],
13 | "1": [0, 2],
14 | "2": [1, 3],
15 | "3": [2, 4],
16 | "4": [3]
17 | }
18 | ```
19 |
20 | === "Star-shaped"
21 |
22 | ```python
23 | connectivity = {
24 | "0": [2],
25 | "1": [2],
26 | "2": [0, 1, 3, 4],
27 | "3": [2],
28 | "4": [2]
29 | }
30 | ```
31 |
32 | === "Diamond-shaped"
33 |
34 | ```python
35 | connectivity = {
36 | "0": [1, 2],
37 | "1": [0, 3, 4],
38 | "2": [0, 4, 5],
39 | "3": [1, 4, 6],
40 | "4": [1, 2, 6, 7],
41 | "5": [2, 4, 7],
42 | "6": [3, 4, 8],
43 | "7": [4, 5, 8],
44 | "8": [6, 7],
45 | }
46 | ```
47 |
48 | The routing pass modifies the quantum circuit by inserting operations—typically _SWAP_ gates—
49 | that distribute the qubits such that the defined interactions can take place between connected qubits.
50 | In other words, it ensures that all qubit interactions respect the connectivity constraints,
51 | making the circuit executable on the target backend.
52 |
53 | The following routing passes are available in Opensquirrel:
54 |
55 | - [A* router](a-star-router.md) (`AStarRouter`)
56 | - [Shortest-path router](shortest-path-router.md) (`ShortestPathRouter`)
57 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We recommend working on a feature branch and pull request from there.
2 |
3 | ## Requirements
4 |
5 | - `uv`,
6 | - `PyCharm` (recommended).
7 |
8 | ## Creating a feature branch
9 |
10 | Make sure your environment contains all the updated versions of the dependencies.
11 |
12 | From an OpenSquirrel checkout:
13 |
14 | ```
15 | $ uv sync
16 | ```
17 |
18 | And that you base your feature branch off an updated `develop`.
19 |
20 | ```
21 | $ git checkout develop
22 | $ git fetch origin
23 | $ git pull
24 | $ git branch
25 | ```
26 |
27 | ## Before creating the pull request
28 |
29 | Make sure the tests and the following linters pass.
30 | Using `tox` (started from an OpenSquirrel checkout):
31 |
32 | ```
33 | $ tox -e fix,type,test
34 | ```
35 |
36 | ## Setting the Python interpreter (PyCharm)
37 |
38 | You can choose the Python interpreter from the `uv` environment.
39 |
40 | - Go to `Settings` > `Project: OpenSquirrel` > `Python Interpreter`.
41 | - Click on `Add Interpeter`, and then select `Add Local Interpreter`.
42 | - Select `uv Environment`, and then `Existing environment`.
43 | - Click on `...` to navigate to the `Interpreter` binary.
44 |
45 | ## Running/Debugging tests (PyCharm)
46 |
47 | To run/debug all tests:
48 |
49 | - Right-click on the `tests` folder of the Project tree.
50 | - Click `Run 'pytest' in tests` or `Debug 'pytest' in tests`.
51 |
52 | This will also create a `Run/Debug Configuration`.
53 |
54 | ### Troubleshooting
55 |
56 | If breakpoints are not hit during debugging:
57 |
58 | - Go to `Run/Debug Configurations`.
59 | - Add `--no-cov` in the `Additional arguments` text box.
60 |
61 | This issue may be due to the code coverage module _hijacking_ the tracing mechanism
62 | (check [this link](https://stackoverflow.com/a/56235965/260313) for a more detailed explanation).
63 |
--------------------------------------------------------------------------------
/opensquirrel/passes/validator/interaction_validator.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from typing import Any
3 |
4 | from opensquirrel.ir import IR, Instruction, Qubit
5 | from opensquirrel.passes.validator.general_validator import Validator
6 |
7 |
8 | class InteractionValidator(Validator):
9 | def __init__(self, connectivity: dict[str, list[int]], **kwargs: Any) -> None:
10 | super().__init__(**kwargs)
11 | self.connectivity = connectivity
12 |
13 | def validate(self, ir: IR) -> None:
14 | """
15 | Check if the circuit interactions faciliate a 1-to-1 mapping to the target hardware.
16 |
17 | Args:
18 | ir (IR): The intermediate representation of the circuit to be checked.
19 |
20 | Raises:
21 | ValueError: If the circuit can't be mapped to the target hardware.
22 | """
23 | non_executable_interactions = []
24 | for statement in ir.statements:
25 | if not isinstance(statement, Instruction):
26 | continue
27 | args = statement.arguments
28 | if args and len(args) > 1 and all(isinstance(arg, Qubit) for arg in args):
29 | qubit_args = [arg for arg in args if isinstance(arg, Qubit)]
30 | qubit_index_pairs = [(q0.index, q1.index) for q0, q1 in itertools.pairwise(qubit_args)]
31 | for i, j in qubit_index_pairs:
32 | if j not in self.connectivity.get(str(i), []):
33 | non_executable_interactions.append((i, j))
34 |
35 | if non_executable_interactions:
36 | error_message = (
37 | f"the following qubit interactions in the circuit prevent a 1-to-1 mapping:"
38 | f"{set(non_executable_interactions)}"
39 | )
40 | raise ValueError(error_message)
41 |
--------------------------------------------------------------------------------
/tests/passes/validator/test_primitive_gate_validator.py:
--------------------------------------------------------------------------------
1 | # Tests for primitive gate validator pass
2 | import pytest
3 |
4 | from opensquirrel import CircuitBuilder
5 | from opensquirrel.circuit import Circuit
6 | from opensquirrel.passes.validator import PrimitiveGateValidator
7 |
8 |
9 | @pytest.fixture
10 | def validator() -> PrimitiveGateValidator:
11 | primitive_gate_set = ["I", "U", "X90", "mX90", "Y90", "mY90", "Rz", "CZ"]
12 | return PrimitiveGateValidator(primitive_gate_set)
13 |
14 |
15 | @pytest.fixture
16 | def circuit_with_matching_gate_set() -> Circuit:
17 | builder = CircuitBuilder(5)
18 | builder.I(0)
19 | builder.U(0, 1, 2, 3)
20 | builder.X90(1)
21 | builder.mX90(2)
22 | builder.Y90(3)
23 | builder.mY90(4)
24 | builder.Rz(0, 2)
25 | builder.CZ(1, 2)
26 | return builder.to_circuit()
27 |
28 |
29 | @pytest.fixture
30 | def circuit_with_unmatching_gate_set() -> Circuit:
31 | builder = CircuitBuilder(5)
32 | builder.I(0)
33 | builder.U(0, 1, 2, 3)
34 | builder.X90(1)
35 | builder.mX90(2)
36 | builder.Y90(3)
37 | builder.mY90(4)
38 | builder.Rz(0, 2)
39 | builder.CZ(1, 2)
40 | builder.H(0)
41 | builder.CNOT(1, 2)
42 | return builder.to_circuit()
43 |
44 |
45 | def test_matching_gates(validator: PrimitiveGateValidator, circuit_with_matching_gate_set: Circuit) -> None:
46 | try:
47 | validator.validate(circuit_with_matching_gate_set.ir)
48 | except ValueError:
49 | pytest.fail("validate() raised ValueError unexpectedly")
50 |
51 |
52 | def test_non_matching_gates(validator: PrimitiveGateValidator, circuit_with_unmatching_gate_set: Circuit) -> None:
53 | with pytest.raises(ValueError, match=r"the following gates are not in the primitive gate set:.*"):
54 | validator.validate(circuit_with_unmatching_gate_set.ir)
55 |
--------------------------------------------------------------------------------
/tests/docs/compilation-passes/test_merger.py:
--------------------------------------------------------------------------------
1 | from math import pi
2 |
3 | from opensquirrel import CircuitBuilder
4 | from opensquirrel.passes.decomposer import McKayDecomposer
5 | from opensquirrel.passes.merger import SingleQubitGatesMerger
6 |
7 |
8 | class TestSingleQubitGatesMerger:
9 | def test_four_rx_to_one_rx(self) -> None:
10 | builder = CircuitBuilder(1)
11 | for _ in range(4):
12 | builder.Rx(0, pi / 4)
13 | circuit = builder.to_circuit()
14 |
15 | circuit.merge(merger=SingleQubitGatesMerger())
16 |
17 | assert (
18 | str(circuit)
19 | == """version 3.0
20 |
21 | qubit[1] q
22 |
23 | Rx(3.1415927) q[0]
24 | """
25 | )
26 |
27 | def test_no_merge_across(self) -> None:
28 | builder = CircuitBuilder(2, 2)
29 | builder.Ry(0, pi / 2).X(0).CNOT(0, 1).H(0).X(1).barrier(1).H(0).X(1).measure(0, 0).H(0).X(1)
30 | circuit = builder.to_circuit()
31 |
32 | circuit.merge(merger=SingleQubitGatesMerger())
33 |
34 | assert (
35 | str(circuit)
36 | == """version 3.0
37 |
38 | qubit[2] q
39 | bit[2] b
40 |
41 | H q[0]
42 | CNOT q[0], q[1]
43 | H q[0]
44 | X q[1]
45 | barrier q[1]
46 | H q[0]
47 | b[0] = measure q[0]
48 | H q[0]
49 | """
50 | )
51 |
52 | def test_increase_circuit_depth(self) -> None:
53 | builder = CircuitBuilder(1)
54 | builder.Rx(0, pi / 3).Ry(0, pi / 5)
55 | circuit = builder.to_circuit()
56 |
57 | circuit.merge(merger=SingleQubitGatesMerger())
58 | circuit.decompose(decomposer=McKayDecomposer())
59 |
60 | assert (
61 | str(circuit)
62 | == """version 3.0
63 |
64 | qubit[1] q
65 |
66 | Rz(-2.2688338) q[0]
67 | X90 q[0]
68 | Rz(1.9872376) q[0]
69 | X90 q[0]
70 | Rz(-1.2436334) q[0]
71 | """
72 | )
73 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Build and Publish wheels
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - "release-*"
8 | release:
9 | types:
10 | - created
11 |
12 | env:
13 | UV_USE_ACTIVE_ENV: 1
14 |
15 | jobs:
16 | publish:
17 | name: Publish assets
18 | runs-on: "ubuntu-latest"
19 | permissions:
20 | id-token: write
21 | steps:
22 | - uses: actions/checkout@master
23 | - name: Set up Python
24 | uses: actions/setup-python@v6
25 | with:
26 | python-version: "3.11"
27 | - name: Install uv
28 | uses: yezz123/setup-uv@v4
29 | with:
30 | uv-version: "0.8.12"
31 |
32 | # Setting the proper version
33 | - name: Get previous Tag
34 | if: contains(github.ref, 'refs/heads/release-')
35 | id: previous_tag
36 | uses: WyriHaximus/github-action-get-previous-tag@v1
37 | with:
38 | fallback: 0.1.0
39 | - name: Set Build version
40 | if: contains(github.ref, 'refs/heads/release-')
41 | run: uv version "${{ steps.previous_tag.outputs.tag }}.dev${{ github.run_number }}"
42 | - name: Set Release version
43 | if: github.event_name == 'release' && github.event.action == 'created'
44 | run: uv version ${{ github.ref_name }}
45 |
46 | # Build package
47 | - name: Build uv package
48 | run: uv build
49 |
50 | # Publishing the package
51 | - name: Publish distribution 📦 to Test PyPI
52 | if: contains(github.ref, 'refs/heads/release-')
53 | uses: pypa/gh-action-pypi-publish@release/v1
54 | with:
55 | repository-url: https://test.pypi.org/legacy/
56 | verbose: true
57 | - name: Publish distribution 📦 to PyPI
58 | if: github.event_name == 'release' && github.event.action == 'created'
59 | uses: pypa/gh-action-pypi-publish@release/v1
60 |
--------------------------------------------------------------------------------
/opensquirrel/ir/unitary.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC, abstractmethod
4 | from collections.abc import Sequence
5 | from typing import TYPE_CHECKING, Any
6 |
7 | from opensquirrel.common import are_matrices_equivalent_up_to_global_phase
8 | from opensquirrel.ir.statement import Instruction
9 |
10 | if TYPE_CHECKING:
11 | from opensquirrel.ir import IRVisitor
12 | from opensquirrel.ir.expression import Qubit
13 |
14 |
15 | class Unitary(Instruction, ABC):
16 | def __init__(self, name: str) -> None:
17 | Instruction.__init__(self, name)
18 |
19 |
20 | class Gate(Unitary, ABC):
21 | def __init__(self, name: str) -> None:
22 | Unitary.__init__(self, name)
23 |
24 | @staticmethod
25 | def _check_repeated_qubit_operands(qubits: Sequence[Qubit]) -> bool:
26 | return len(qubits) != len(set(qubits))
27 |
28 | @abstractmethod
29 | def get_qubit_operands(self) -> list[Qubit]:
30 | pass
31 |
32 | @abstractmethod
33 | def is_identity(self) -> bool:
34 | pass
35 |
36 | def accept(self, visitor: IRVisitor) -> Any:
37 | return visitor.visit_gate(self)
38 |
39 | def __eq__(self, other: object) -> bool:
40 | if not isinstance(other, Gate):
41 | return False
42 | return compare_gates(self, other)
43 |
44 |
45 | def compare_gates(g1: Gate, g2: Gate) -> bool:
46 | union_mapping = [q.index for q in list(set(g1.get_qubit_operands()) | set(g2.get_qubit_operands()))]
47 |
48 | from opensquirrel.circuit_matrix_calculator import get_circuit_matrix
49 | from opensquirrel.reindexer import get_reindexed_circuit
50 |
51 | matrix_g1 = get_circuit_matrix(get_reindexed_circuit([g1], union_mapping))
52 | matrix_g2 = get_circuit_matrix(get_reindexed_circuit([g2], union_mapping))
53 |
54 | return are_matrices_equivalent_up_to_global_phase(matrix_g1, matrix_g2)
55 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/check_mapper.py:
--------------------------------------------------------------------------------
1 | """This module contains checks for the ``Mapper`` pass."""
2 |
3 | from __future__ import annotations
4 |
5 | from copy import deepcopy
6 |
7 | from opensquirrel.circuit import Circuit
8 | from opensquirrel.ir import IR, Measure
9 | from opensquirrel.ir.default_gates import I
10 | from opensquirrel.ir.semantics import BlochSphereRotation, ControlledGateSemantic
11 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
12 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
13 | from opensquirrel.passes.mapper.general_mapper import Mapper
14 | from opensquirrel.register_manager import BitRegister, QubitRegister, RegisterManager
15 |
16 |
17 | def _check_scenario(circuit: Circuit, mapper: Mapper) -> None:
18 | """Check if the given scenario can be mapped.
19 |
20 | Args:
21 | circuit: Circuit containing the scenario to check against.
22 | mapper: Mapper to use.
23 | """
24 | ir_copy = deepcopy(circuit.ir)
25 | circuit.map(mapper)
26 | assert circuit.ir == ir_copy, "A Mapper pass should not change the IR"
27 |
28 |
29 | def check_mapper(mapper: Mapper) -> None:
30 | """Check if the `mapper` complies with the OpenSquirrel requirements.
31 |
32 | If a ``Mapper`` implementation passes these checks, it should be compatible with the ``Circuit.map`` method.
33 |
34 | Args:
35 | mapper: Mapper to check.
36 | """
37 | assert isinstance(mapper, Mapper)
38 |
39 | register_manager = RegisterManager(QubitRegister(10), BitRegister(10))
40 | ir = IR()
41 | circuit = Circuit(register_manager, ir)
42 | _check_scenario(circuit, mapper)
43 |
44 | ir = IR()
45 | ir.add_gate(SingleQubitGate(qubit=42, gate_semantic=BlochSphereRotation((1, 0, 0), 1, 2)))
46 | ir.add_gate(TwoQubitGate(42, 100, gate_semantic=ControlledGateSemantic(I(100))))
47 | ir.add_non_unitary(Measure(42, 42, (0, 0, 1)))
48 | Circuit(register_manager, ir)
49 | _check_scenario(circuit, mapper)
50 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_xzx_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Rz, S, X, Z
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import XZXDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> XZXDecomposer:
16 | return XZXDecomposer()
17 |
18 |
19 | def test_identity(decomposer: XZXDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (S(0), [Rz(0, pi / 2)]),
31 | (Z(0), [Rz(0, pi)]),
32 | (Rz(0, 0.9), [Rz(0, 0.9)]),
33 | (X(0), [Rx(0, pi)]),
34 | (Rx(0, 0.123), [Rx(0, 0.123)]),
35 | (H(0), [Rx(0, pi / 2), Rz(0, pi / 2), Rx(0, pi / 2)]),
36 | (
37 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
38 | [Rx(0, 0.43035280630630446), Rz(0, -1.030183660156084), Rx(0, -0.7456524007888308)],
39 | ),
40 | ],
41 | ids=["CNOT", "CR", "S", "Y", "Ry", "X", "Rx", "H", "arbitrary"],
42 | )
43 | def test_xzx_decomposer(
44 | decomposer: XZXDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
45 | ) -> None:
46 | decomposed_gate = decomposer.decompose(gate)
47 | check_gate_replacement(gate, decomposed_gate)
48 | assert decomposer.decompose(gate) == expected_result
49 |
50 |
51 | def test_find_unused_index() -> None:
52 | xzx_decomp = XZXDecomposer()
53 | missing_index = xzx_decomp._find_unused_index()
54 |
55 | assert missing_index == 1
56 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_yxy_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Ry, S, X, Y
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import YXYDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> YXYDecomposer:
16 | return YXYDecomposer()
17 |
18 |
19 | def test_identity(decomposer: YXYDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (S(0), [Ry(0, pi / 2), Rx(0, pi / 2), Ry(0, -pi / 2)]),
31 | (Y(0), [Ry(0, pi)]),
32 | (Ry(0, 0.9), [Ry(0, 0.9)]),
33 | (X(0), [Rx(0, pi)]),
34 | (Rx(0, 0.123), [Rx(0, 0.123)]),
35 | (H(0), [Ry(0, pi / 4), Rx(0, pi), Ry(0, -pi / 4)]),
36 | (
37 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
38 | [Ry(0, 0.9412144817800217), Rx(0, -0.893533136099803), Ry(0, -1.5568770630164868)],
39 | ),
40 | ],
41 | ids=["CNOT", "CR", "S", "Y", "Ry", "X", "Rx", "H", "arbitrary"],
42 | )
43 | def test_yxy_decomposer(
44 | decomposer: YXYDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
45 | ) -> None:
46 | decomposed_gate = decomposer.decompose(gate)
47 | check_gate_replacement(gate, decomposed_gate)
48 | assert decomposer.decompose(gate) == expected_result
49 |
50 |
51 | def test_find_unused_index() -> None:
52 | yxy_decomp = YXYDecomposer()
53 | missing_index = yxy_decomp._find_unused_index()
54 |
55 | assert missing_index == 2
56 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_xyx_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import math
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Ry, S, X, Y
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import XYXDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> XYXDecomposer:
16 | return XYXDecomposer()
17 |
18 |
19 | def test_identity(decomposer: XYXDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (S(0), [Rx(0, -math.pi / 2), Ry(0, math.pi / 2), Rx(0, math.pi / 2)]),
31 | (Y(0), [Ry(0, math.pi)]),
32 | (Ry(0, 0.9), [Ry(0, 0.9)]),
33 | (X(0), [Rx(0, math.pi)]),
34 | (Rx(0, 0.123), [Rx(0, 0.123)]),
35 | (H(0), [Ry(0, math.pi / 2), Rx(0, math.pi)]),
36 | (
37 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
38 | [Rx(0, -1.140443520488592), Ry(0, -1.030183660156084), Rx(0, 0.8251439260060653)],
39 | ),
40 | ],
41 | ids=["CNOT", "CR", "S", "Y", "Ry", "X", "Rx", "H", "arbitrary"],
42 | )
43 | def test_xyx_decomposer(
44 | decomposer: XYXDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
45 | ) -> None:
46 | decomposed_gate = decomposer.decompose(gate)
47 | check_gate_replacement(gate, decomposed_gate)
48 | assert decomposer.decompose(gate) == expected_result
49 |
50 |
51 | def test_find_unused_index() -> None:
52 | xyx_decomp = XYXDecomposer()
53 | missing_index = xyx_decomp._find_unused_index()
54 |
55 | assert missing_index == 2
56 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import math
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from opensquirrel import CNOT, CR, CZ, CRk, H, Ry, Rz
9 | from opensquirrel.passes.decomposer import CZDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import Gate
14 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
15 |
16 |
17 | @pytest.fixture
18 | def decomposer() -> CZDecomposer:
19 | return CZDecomposer()
20 |
21 |
22 | @pytest.mark.parametrize(("gate", "expected_result"), [(H(0), [H(0)]), (Rz(0, 2.345), [Rz(0, 2.345)])])
23 | def test_ignores_1q_gates(decomposer: CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
24 | check_gate_replacement(gate, expected_result)
25 | assert decomposer.decompose(gate) == expected_result
26 |
27 |
28 | def test_preserves_CZ(decomposer: CZDecomposer) -> None: # noqa: N802
29 | gate = CZ(0, 1)
30 | decomposed_gate = decomposer.decompose(gate)
31 | check_gate_replacement(gate, decomposed_gate)
32 | assert decomposed_gate == [CZ(0, 1)]
33 |
34 |
35 | def test_CNOT(decomposer: CZDecomposer) -> None: # noqa: N802
36 | gate = CNOT(0, 1)
37 | decomposed_gate = decomposer.decompose(gate)
38 | check_gate_replacement(gate, decomposed_gate)
39 | assert decomposed_gate == [Ry(1, -math.pi / 2), CZ(0, 1), Ry(1, math.pi / 2)]
40 |
41 |
42 | @pytest.mark.parametrize(
43 | "controlled_gate",
44 | [
45 | CR(0, 1, math.pi / 2),
46 | CR(0, 1, math.pi / 4),
47 | CR(0, 1, 1 / math.sqrt(2)),
48 | CRk(0, 1, 1),
49 | CRk(0, 1, 2),
50 | CRk(0, 1, 16),
51 | ],
52 | ids=["CR_1", "CR_2", "CR_3", "CRk_1", "CRk_2", "CRk_3"],
53 | )
54 | def test_controlled_gates(decomposer: CZDecomposer, controlled_gate: TwoQubitGate) -> None:
55 | decomposed_gate = decomposer.decompose(controlled_gate)
56 | check_gate_replacement(controlled_gate, decomposed_gate)
57 |
--------------------------------------------------------------------------------
/tests/ir/semantics/test_canonical_gate_semantic.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import numpy.testing
3 | import pytest
4 | from numpy.typing import NDArray
5 |
6 | from opensquirrel.ir import GateSemantic
7 | from opensquirrel.ir.semantics import CanonicalAxis, CanonicalGateSemantic
8 |
9 |
10 | class TestCanonicalAxis:
11 | @pytest.mark.parametrize(
12 | ("axis", "restricted_axis"),
13 | [
14 | (np.array([1, 1, 1], dtype=np.float64), np.array([0, 0, 0], dtype=np.float64)),
15 | (np.array([-1, -1, -1], dtype=np.float64), np.array([0, 0, 0], dtype=np.float64)),
16 | (np.array([1, 0, 0], dtype=np.float64), np.array([0, 0, 0], dtype=np.float64)),
17 | (np.array([3 / 4, 1 / 4, 0], dtype=np.float64), np.array([1 / 4, 1 / 4, 0], dtype=np.float64)),
18 | (np.array([5 / 8, 3 / 8, 0], dtype=np.float64), np.array([3 / 8, 3 / 8, 0], dtype=np.float64)),
19 | (np.array([3 / 4, 3 / 4, 3 / 4], dtype=np.float64), np.array([1 / 4, 1 / 4, 1 / 4], dtype=np.float64)),
20 | (np.array([1 / 2, 3 / 4, 3 / 4], dtype=np.float64), np.array([1 / 2, 1 / 4, 1 / 4], dtype=np.float64)),
21 | (np.array([64 / 2, 32 / 4, 33 / 4], dtype=np.float64), np.array([1 / 4, 0, 0], dtype=np.float64)),
22 | ],
23 | )
24 | def test_restrict_to_weyl_chamber(self, axis: NDArray[np.float64], restricted_axis: NDArray[np.float64]) -> None:
25 | numpy.testing.assert_array_almost_equal(CanonicalAxis.restrict_to_weyl_chamber(axis), restricted_axis)
26 |
27 |
28 | class TestCanonicalGateSemantic:
29 | @pytest.fixture
30 | def semantic(self) -> CanonicalGateSemantic:
31 | return CanonicalGateSemantic((0, 0, 0))
32 |
33 | def test_eq(self, semantic: CanonicalGateSemantic) -> None:
34 | assert semantic.is_identity()
35 |
36 | def test_init(self, semantic: CanonicalGateSemantic) -> None:
37 | assert isinstance(semantic, GateSemantic)
38 | assert hasattr(semantic, "axis")
39 | assert isinstance(semantic.axis, CanonicalAxis)
40 |
--------------------------------------------------------------------------------
/tests/ir/semantics/test_matrix_gate_semantic.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 |
4 | from opensquirrel.ir.semantics import MatrixGateSemantic
5 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
6 |
7 |
8 | class TestMatrixGateSemantic:
9 | @pytest.fixture
10 | def gate(self) -> TwoQubitGate:
11 | cnot_matrix = [
12 | [1, 0, 0, 0],
13 | [0, 1, 0, 0],
14 | [0, 0, 0, 1],
15 | [0, 0, 1, 0],
16 | ]
17 | return TwoQubitGate(42, 100, gate_semantic=MatrixGateSemantic(cnot_matrix))
18 |
19 | def test_array_like(self) -> None:
20 | gate = TwoQubitGate(
21 | 0, 1, gate_semantic=MatrixGateSemantic([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
22 | )
23 | assert repr(gate) == (
24 | "TwoQubitGate(qubits=[(Qubit[0], Qubit[1])], gate_semantic=MatrixGateSemantic(matrix="
25 | "[[1.+0.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 1.+0.j 0.+0.j 0.+0.j] "
26 | "[0.+0.j 0.+0.j 0.+0.j 1.+0.j] [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]))"
27 | )
28 |
29 | def test_incorrect_array(self) -> None:
30 | with pytest.raises(ValueError, match=r".* inhomogeneous shape after .*") as e_info:
31 | TwoQubitGate(0, 1, gate_semantic=MatrixGateSemantic([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 0]]))
32 | assert "setting an array element with a sequence." in str(e_info.value)
33 |
34 | def test_repr(self, gate: TwoQubitGate) -> None:
35 | assert repr(gate) == (
36 | "TwoQubitGate(qubits=[(Qubit[42], Qubit[100])], gate_semantic=MatrixGateSemantic(matrix="
37 | "[[1.+0.j 0.+0.j 0.+0.j 0.+0.j] [0.+0.j 1.+0.j 0.+0.j 0.+0.j] "
38 | "[0.+0.j 0.+0.j 0.+0.j 1.+0.j] [0.+0.j 0.+0.j 1.+0.j 0.+0.j]]))"
39 | )
40 |
41 | def test_is_identity(self, gate: TwoQubitGate) -> None:
42 | assert TwoQubitGate(42, 100, gate_semantic=MatrixGateSemantic(np.eye(4, dtype=np.complex128))).is_identity()
43 | assert not gate.is_identity()
44 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_yzy_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Ry, Rz, S, X, Y
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import YZYDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> YZYDecomposer:
16 | return YZYDecomposer()
17 |
18 |
19 | def test_identity(decomposer: YZYDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (S(0), [Rz(0, pi / 2)]),
31 | (Y(0), [Ry(0, pi)]),
32 | (Ry(0, 0.9), [Ry(0, 0.9)]),
33 | (X(0), [Ry(0, -pi / 2), Rz(0, pi), Ry(0, pi / 2)]),
34 | (Rx(0, 0.123), [Ry(0, -pi / 2), Rz(0, 0.12300000000000022), Ry(0, pi / 2)]),
35 | (H(0), [Ry(0, -pi / 4), Rz(0, pi), Ry(0, pi / 4)]),
36 | (
37 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
38 | [Ry(0, -0.6295818450148737), Rz(0, -0.893533136099803), Ry(0, 0.013919263778408464)],
39 | ),
40 | ],
41 | ids=["CNOT", "CR", "S", "Y", "Ry", "X", "Rx", "H", "arbitrary"],
42 | )
43 | def test_yzy_decomposer(
44 | decomposer: YZYDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
45 | ) -> None:
46 | decomposed_gate = decomposer.decompose(gate)
47 | check_gate_replacement(gate, decomposed_gate)
48 | assert decomposer.decompose(gate) == expected_result
49 |
50 |
51 | def test_find_unused_index() -> None:
52 | yzy_decomp = YZYDecomposer()
53 | missing_index = yzy_decomp._find_unused_index()
54 |
55 | assert missing_index == 0
56 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_cnot_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi, sqrt
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from opensquirrel import CNOT, CR, CZ, CRk, H, Ry, Rz
9 | from opensquirrel.passes.decomposer import CNOTDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import Gate
14 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
15 |
16 |
17 | @pytest.fixture
18 | def decomposer() -> CNOTDecomposer:
19 | return CNOTDecomposer()
20 |
21 |
22 | @pytest.mark.parametrize(("gate", "expected_result"), [(H(0), [H(0)]), (Rz(0, 2.345), [Rz(0, 2.345)])])
23 | def test_ignores_1q_gates(decomposer: CNOTDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
24 | check_gate_replacement(gate, expected_result)
25 | assert decomposer.decompose(gate) == expected_result
26 |
27 |
28 | def test_preserves_CNOT(decomposer: CNOTDecomposer) -> None: # noqa: N802
29 | gate = CNOT(0, 1)
30 | decomposed_gate = decomposer.decompose(gate)
31 | check_gate_replacement(gate, decomposed_gate)
32 | assert decomposed_gate == [CNOT(0, 1)]
33 |
34 |
35 | def test_CZ(decomposer: CNOTDecomposer) -> None: # noqa: N802
36 | gate = CZ(0, 1)
37 | decomposed_gate = decomposer.decompose(gate)
38 | check_gate_replacement(gate, decomposed_gate)
39 | assert decomposed_gate == [Rz(1, pi), Ry(1, pi / 2), CNOT(0, 1), Ry(1, -pi / 2), Rz(1, pi)]
40 |
41 |
42 | @pytest.mark.parametrize(
43 | "controlled_gate",
44 | [
45 | CR(0, 1, pi / 2),
46 | CR(0, 1, pi / 4),
47 | CR(0, 1, 1 / sqrt(2)),
48 | CRk(0, 1, 1),
49 | CRk(0, 1, 2),
50 | CRk(0, 1, 16),
51 | ],
52 | ids=["CR_1", "CR_2", "CR_3", "CRk_1", "CRk_2", "CRk_3"],
53 | )
54 | def test_controlled_gates(decomposer: CNOTDecomposer, controlled_gate: TwoQubitGate) -> None:
55 | decomposed_gate = decomposer.decompose(controlled_gate)
56 | check_gate_replacement(controlled_gate, decomposed_gate)
57 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_zyz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Ry, Rz, X, Y, Z
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import ZYZDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> ZYZDecomposer:
16 | return ZYZDecomposer()
17 |
18 |
19 | def test_identity(decomposer: ZYZDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (X(0), [Rz(0, pi / 2), Ry(0, pi), Rz(0, -pi / 2)]),
31 | (Rx(0, 0.9), [Rz(0, pi / 2), Ry(0, 0.9), Rz(0, -pi / 2)]),
32 | (Y(0), [Ry(0, pi)]),
33 | (Ry(0, 0.9), [Ry(0, 0.9)]),
34 | (Z(0), [Rz(0, pi)]),
35 | (Rz(0, 0.123), [Rz(0, 0.123)]),
36 | (H(0), [Rz(0, pi), Ry(0, pi / 2)]),
37 | (
38 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
39 | [Rz(0, 0.018644578210710527), Ry(0, -0.6209410696845807), Rz(0, -0.9086506397909061)],
40 | ),
41 | ],
42 | ids=["CNOT", "CR", "X", "Rx", "Y", "Ry", "Z", "Rz", "H", "arbitrary"],
43 | )
44 | def test_zyz_decomposer(
45 | decomposer: ZYZDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
46 | ) -> None:
47 | decomposed_gate = decomposer.decompose(gate)
48 | check_gate_replacement(gate, decomposed_gate)
49 | assert decomposer.decompose(gate) == expected_result
50 |
51 |
52 | def test_find_unused_index() -> None:
53 | zyz_decomp = ZYZDecomposer()
54 | missing_index = zyz_decomp._find_unused_index()
55 |
56 | assert missing_index == 0
57 |
--------------------------------------------------------------------------------
/docs/compilation-passes/validation/interaction-validator.md:
--------------------------------------------------------------------------------
1 | This pass checks whether all interactions in the circuit, _i.e._ two-qubit gates, are executable given the backend
2 | connectivity.
3 | If certain interactions are not possible, the validator will throw a `ValueError`,
4 | specifying which interactions cannot be executed.
5 |
6 | The interaction validator (`InteractionValidator`) can be used in the following manner.
7 |
8 | _Check the [circuit builder](../../circuit-builder/index.md) on how to generate a circuit._
9 |
10 | ```python
11 | from opensquirrel import CircuitBuilder
12 | from opensquirrel.passes.validator import InteractionValidator
13 | ```
14 |
15 | ```python
16 | connectivity = {
17 | "0": [1, 2],
18 | "1": [0, 2, 3],
19 | "2": [0, 1, 4],
20 | "3": [1, 4],
21 | "4": [2, 3]
22 | }
23 |
24 | builder = CircuitBuilder(5)
25 | builder.H(0)
26 | builder.CNOT(0, 1)
27 | builder.H(2)
28 | builder.CNOT(1, 2)
29 | builder.CNOT(2, 4)
30 | builder.CNOT(3, 4)
31 | circuit = builder.to_circuit()
32 |
33 | interaction_validator = InteractionValidator(connectivity=connectivity)
34 | circuit.validate(validator=interaction_validator)
35 | ```
36 |
37 | In the scenario above, there will be no output since all qubit interactions are executable given the connectivity.
38 | On the other hand, the circuit below will raise an error (`ValueError`) as certain interactions are not possible.
39 |
40 | ```python
41 | builder = CircuitBuilder(5)
42 | builder.H(0)
43 | builder.CNOT(0, 1)
44 | builder.CNOT(0, 3)
45 | builder.H(2)
46 | builder.CNOT(1, 2)
47 | builder.CNOT(1, 3)
48 | builder.CNOT(2, 3)
49 | builder.CNOT(3, 4)
50 | builder.CNOT(0, 4)
51 | circuit = builder.to_circuit()
52 |
53 | circuit.validate(validator=interaction_validator)
54 | ```
55 |
56 | !!! example ""
57 |
58 | `ValueError: the following qubit interactions in the circuit prevent a 1-to-1 mapping:{(2, 3), (0, 3), (0, 4)}`
59 |
60 | !!! note "Resolving the error"
61 |
62 | The circuit can be redefined to only contain interactions between connected qubits or a
63 | [routing pass](../routing/index.md) can be used to resolve the error.
64 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_swap2cnot_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from opensquirrel import CNOT, CR, CZ, SWAP, CRk, H, Ry
9 | from opensquirrel.passes.decomposer import SWAP2CNOTDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import Gate
14 |
15 |
16 | @pytest.fixture
17 | def decomposer() -> SWAP2CNOTDecomposer:
18 | return SWAP2CNOTDecomposer()
19 |
20 |
21 | @pytest.mark.parametrize(
22 | ("gate", "expected_result"),
23 | [
24 | (H(0), [H(0)]),
25 | (Ry(0, 2.345), [Ry(0, 2.345)]),
26 | ],
27 | ids=["Hadamard", "rotation_gate"],
28 | )
29 | def test_ignores_1q_gates(decomposer: SWAP2CNOTDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
30 | check_gate_replacement(gate, expected_result)
31 | assert decomposer.decompose(gate) == expected_result
32 |
33 |
34 | @pytest.mark.parametrize(
35 | ("gate", "expected_result"),
36 | [
37 | (CNOT(0, 1), [CNOT(0, 1)]),
38 | (CR(0, 1, pi), [CR(0, 1, pi)]),
39 | (CRk(0, 1, 2), [CRk(0, 1, 2)]),
40 | (CZ(0, 1), [CZ(0, 1)]),
41 | ],
42 | ids=["CNOT_gate", "CR_gate", "CRk_gate", "CZ_gate"],
43 | )
44 | def test_ignores_2q_gates(decomposer: SWAP2CNOTDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
45 | check_gate_replacement(gate, expected_result)
46 | assert decomposer.decompose(gate) == expected_result
47 |
48 |
49 | @pytest.mark.parametrize(
50 | ("gate", "expected_result"),
51 | [
52 | (SWAP(0, 1), [CNOT(0, 1), CNOT(1, 0), CNOT(0, 1)]),
53 | (SWAP(1, 0), [CNOT(1, 0), CNOT(0, 1), CNOT(1, 0)]),
54 | ],
55 | ids=["SWAP_0_1", "SWAP_1_0"],
56 | )
57 | def test_decomposes_SWAP(decomposer: SWAP2CNOTDecomposer, gate: Gate, expected_result: list[Gate]) -> None: # noqa: N802
58 | decomposed_gate = decomposer.decompose(gate)
59 | check_gate_replacement(gate, decomposed_gate)
60 | assert decomposed_gate == expected_result
61 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_cnot2cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import math
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from opensquirrel import CNOT, CR, CZ, SWAP, CRk, H, Ry
9 | from opensquirrel.passes.decomposer import CNOT2CZDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import Gate
14 |
15 |
16 | @pytest.fixture
17 | def decomposer() -> CNOT2CZDecomposer:
18 | return CNOT2CZDecomposer()
19 |
20 |
21 | @pytest.mark.parametrize(
22 | ("gate", "expected_result"),
23 | [
24 | (H(0), [H(0)]),
25 | (Ry(0, 2.345), [Ry(0, 2.345)]),
26 | ],
27 | ids=["Hadamard", "rotation_gate"],
28 | )
29 | def test_ignores_1q_gates(decomposer: CNOT2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
30 | check_gate_replacement(gate, expected_result)
31 | assert decomposer.decompose(gate) == expected_result
32 |
33 |
34 | @pytest.mark.parametrize(
35 | ("gate", "expected_result"),
36 | [
37 | (CR(0, 1, math.pi), [CR(0, 1, math.pi)]),
38 | (CRk(0, 1, 2), [CRk(0, 1, 2)]),
39 | (CZ(0, 1), [CZ(0, 1)]),
40 | (SWAP(0, 1), [SWAP(0, 1)]),
41 | ],
42 | ids=["CR_gate", "CRk_gate", "CZ_gate", "SWAP_gate"],
43 | )
44 | def test_ignores_2q_gates(decomposer: CNOT2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
45 | check_gate_replacement(gate, expected_result)
46 | assert decomposer.decompose(gate) == expected_result
47 |
48 |
49 | @pytest.mark.parametrize(
50 | ("gate", "expected_result"),
51 | [
52 | (CNOT(0, 1), [Ry(1, -math.pi / 2), CZ(0, 1), Ry(1, math.pi / 2)]),
53 | (CNOT(1, 0), [Ry(0, -math.pi / 2), CZ(1, 0), Ry(0, math.pi / 2)]),
54 | ],
55 | ids=["CNOT_0_1", "CNOT_1_0"],
56 | )
57 | def test_decomposes_CNOT(decomposer: CNOT2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None: # noqa: N802
58 | decomposed_gate = decomposer.decompose(gate)
59 | check_gate_replacement(gate, decomposed_gate)
60 | assert decomposed_gate == expected_result
61 |
--------------------------------------------------------------------------------
/tests/passes/validator/test_interaction_validator.py:
--------------------------------------------------------------------------------
1 | # Tests for routing validator pass
2 |
3 | import pytest
4 |
5 | from opensquirrel import CircuitBuilder
6 | from opensquirrel.circuit import Circuit
7 | from opensquirrel.ir import AsmDeclaration
8 | from opensquirrel.passes.validator import InteractionValidator
9 |
10 |
11 | @pytest.fixture
12 | def validator() -> InteractionValidator:
13 | connectivity = {"0": [1, 2], "1": [0, 2, 3], "2": [0, 1, 4], "3": [1, 4], "4": [2, 3]}
14 | return InteractionValidator(connectivity)
15 |
16 |
17 | @pytest.fixture
18 | def circuit1() -> Circuit:
19 | builder = CircuitBuilder(5)
20 | builder.H(0)
21 | builder.CNOT(0, 1)
22 | builder.H(2)
23 | builder.CNOT(1, 2)
24 | builder.CNOT(2, 4)
25 | builder.CNOT(3, 4)
26 | return builder.to_circuit()
27 |
28 |
29 | @pytest.fixture
30 | def circuit2() -> Circuit:
31 | builder = CircuitBuilder(5)
32 | builder.H(0)
33 | builder.CNOT(0, 1)
34 | builder.CNOT(0, 3)
35 | builder.H(2)
36 | builder.CNOT(1, 2)
37 | builder.CNOT(1, 3)
38 | builder.CNOT(2, 3)
39 | builder.CNOT(3, 4)
40 | builder.CNOT(0, 4)
41 | return builder.to_circuit()
42 |
43 |
44 | def test_routing_checker_possible_1to1_mapping(validator: InteractionValidator, circuit1: Circuit) -> None:
45 | try:
46 | validator.validate(circuit1.ir)
47 | except ValueError:
48 | pytest.fail("route() raised ValueError unexpectedly")
49 |
50 |
51 | def test_routing_checker_impossible_1to1_mapping(validator: InteractionValidator, circuit2: Circuit) -> None:
52 | with pytest.raises(
53 | ValueError, match=r"the following qubit interactions in the circuit prevent a 1-to-1 mapping:.*"
54 | ):
55 | validator.validate(circuit2.ir)
56 |
57 |
58 | def test_ignore_asm(validator: InteractionValidator) -> None:
59 | builder = CircuitBuilder(2)
60 | builder.H(0)
61 | builder.asm("backend_name", r"backend_code")
62 | builder.CNOT(0, 1)
63 | circuit = builder.to_circuit()
64 | validator.validate(circuit.ir)
65 |
66 | assert len([statement for statement in circuit.ir.statements if isinstance(statement, AsmDeclaration)]) == 1
67 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | env:
10 | UV_USE_ACTIVE_ENV: 1
11 |
12 | jobs:
13 | lint:
14 | name: Static analysis
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v6
18 | - name: Set up Python
19 | uses: actions/setup-python@v6
20 | with:
21 | python-version: "3.11"
22 | - name: Install uv
23 | uses: yezz123/setup-uv@v4
24 | with:
25 | uv-version: "0.8.12"
26 | - name: Install tox
27 | run: pip install tox
28 | - name: run tox lint and type
29 | run: tox -e lint,type
30 | unit-test:
31 | name: Unit testing
32 | needs: lint
33 | strategy:
34 | fail-fast: false
35 | matrix:
36 | os:
37 | - ubuntu-latest
38 | - macos-latest
39 | - windows-latest
40 | python-version:
41 | - "3.10"
42 | - "3.11"
43 | - "3.12"
44 | - "3.13"
45 | runs-on: ${{ matrix.os }}
46 | steps:
47 | - uses: actions/checkout@v6
48 | - name: Set up Python
49 | uses: actions/setup-python@v6
50 | with:
51 | python-version: ${{ matrix.python-version }}
52 | - name: Install uv
53 | uses: yezz123/setup-uv@v4
54 | with:
55 | uv-version: "0.8.12"
56 | - name: Install tox
57 | run: pip install tox
58 | - name: run tox test
59 | run: tox -e test
60 | complete:
61 | # see https://github.community/t/status-check-for-a-matrix-jobs/127354/7
62 | name: Report status
63 | needs: [lint, unit-test]
64 | if: ${{ always() }}
65 | runs-on: ubuntu-latest
66 | steps:
67 | - name: Check all job status
68 | # see https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#needs-context
69 | # see https://stackoverflow.com/a/67532120/4907315
70 | if: >-
71 | ${{
72 | contains(needs.*.result, 'failure')
73 | || contains(needs.*.result, 'cancelled')
74 | || contains(needs.*.result, 'skipped')
75 | }}
76 | run: exit 1
77 |
--------------------------------------------------------------------------------
/docs/compilation-passes/validation/primitive-gate-validator.md:
--------------------------------------------------------------------------------
1 | When developing quantum algorithms, their compilation on a specific device depends on whether the hardware supports the
2 | operations implemented on the circuit.
3 |
4 | To this end, the primitive gate validator pass checks whether the quantum gates in the
5 | quantum circuit are present in the primitive gate set of the target backend.
6 | If this is not the case, the validator will throw a `ValueError`,
7 | specifying which gates in the circuit are not in the provided primitive gate set.
8 |
9 | Below are some examples of using the primitive gate validator (`PrimitiveGateValidator`).
10 |
11 | _Check the [circuit builder](../../circuit-builder/index.md) on how to generate a circuit._
12 |
13 | ```python
14 | from opensquirrel import CircuitBuilder
15 | from opensquirrel.passes.validator import PrimitiveGateValidator
16 | ```
17 |
18 | ```python
19 | from math import pi
20 | pgs = ["I", "Rx", "Ry", "Rz", "CZ"]
21 |
22 | builder = CircuitBuilder(5)
23 | builder.Rx(pi / 2)
24 | builder.Ry(1, -pi / 2)
25 | builder.CZ(0, 1)
26 | builder.Ry(1, pi / 2)
27 | circuit = builder.to_circuit()
28 |
29 | circuit.validate(validator=PrimitiveGateValidator(primitive_gate_set=pgs))
30 | ```
31 |
32 | In the scenario above, there will be no output, as all gates in the circuit are in the primitive gate set.
33 | On the other hand, the circuit below will raise an error (`ValueError`) as certain gates are not supported,
34 | given the backend primitive gate set (`pgs`).
35 |
36 | ```python
37 | pgs = ["I", "X90", "mX90", "Y90", "mY90", "Rz", "CZ"]
38 |
39 | builder = CircuitBuilder(5)
40 | builder.I(0)
41 | builder.X90(1)
42 | builder.mX90(2)
43 | builder.Y90(3)
44 | builder.mY90(4)
45 | builder.Rz(0, 2)
46 | builder.CZ(1, 2)
47 | builder.H(0)
48 | builder.CNOT(1, 2)
49 | circuit = builder.to_circuit()
50 |
51 | circuit.validate(validator=PrimitiveGateValidator(primitive_gate_set=pgs))
52 | ```
53 |
54 | !!! example ""
55 |
56 | `ValueError: the following gates are not in the primitive gate set: ['H', 'CNOT']`
57 |
58 | !!! note "Resolving the error"
59 |
60 | The upsupported gates can be replaced manually, or a [decomposition pass](../decomposition/index.md) can be used to
61 | resolve the error.
62 |
63 |
--------------------------------------------------------------------------------
/opensquirrel/ir/control_instruction.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any, SupportsInt
3 |
4 | from opensquirrel.ir.expression import Expression, Int, Qubit, QubitLike
5 | from opensquirrel.ir.ir import IRVisitor
6 | from opensquirrel.ir.statement import Instruction
7 |
8 |
9 | class ControlInstruction(Instruction, ABC):
10 | def __init__(self, qubit: QubitLike, name: str) -> None:
11 | Instruction.__init__(self, name)
12 | self.qubit = Qubit(qubit)
13 |
14 | @property
15 | @abstractmethod
16 | def arguments(self) -> tuple[Expression, ...]:
17 | pass
18 |
19 | def get_qubit_operands(self) -> list[Qubit]:
20 | return [self.qubit]
21 |
22 |
23 | class Barrier(ControlInstruction):
24 | def __init__(self, qubit: QubitLike) -> None:
25 | ControlInstruction.__init__(self, qubit=qubit, name="barrier")
26 | self.qubit = Qubit(qubit)
27 |
28 | def __repr__(self) -> str:
29 | return f"{self.__class__.__name__}(qubit={self.qubit})"
30 |
31 | def __eq__(self, other: object) -> bool:
32 | return isinstance(other, Barrier) and self.qubit == other.qubit
33 |
34 | @property
35 | def arguments(self) -> tuple[Expression, ...]:
36 | return (self.qubit,)
37 |
38 | def accept(self, visitor: IRVisitor) -> Any:
39 | visitor.visit_control_instruction(self)
40 | return visitor.visit_barrier(self)
41 |
42 |
43 | class Wait(ControlInstruction):
44 | def __init__(self, qubit: QubitLike, time: SupportsInt) -> None:
45 | ControlInstruction.__init__(self, qubit=qubit, name="wait")
46 | self.qubit = Qubit(qubit)
47 | self.time = Int(time)
48 |
49 | def __repr__(self) -> str:
50 | return f"{self.name}(qubit={self.qubit}, time={self.time})"
51 |
52 | def __eq__(self, other: object) -> bool:
53 | return isinstance(other, Wait) and self.qubit == other.qubit and self.time == other.time
54 |
55 | @property
56 | def arguments(self) -> tuple[Expression, ...]:
57 | return self.qubit, self.time
58 |
59 | def accept(self, visitor: IRVisitor) -> Any:
60 | visitor.visit_control_instruction(self)
61 | return visitor.visit_wait(self)
62 |
--------------------------------------------------------------------------------
/docs/compilation-passes/index.md:
--------------------------------------------------------------------------------
1 | Compilation passes are essential steps in the process of converting a high-level quantum algorithm,
2 | _i.e._ quantum circuits, into a hardware-specific executable format.
3 | This process, known as _quantum compilation_,
4 | involves several stages to ensure that the quantum circuit can be executed efficiently on a given quantum hardware.
5 |
6 | Compilation passes include various optimization techniques and transformations applied to the quantum circuit.
7 | These passes can involve _qubit routing_, _initial mapping_, _gate decomposition_, or _error correction_.
8 | These optimization steps are essential to the execution of quantum algorithms.
9 | Often times, the design of quantum algorithms does not take into account the constraints or limitations imposed by the
10 | target hardware, such as qubit coupling map or native gate set.
11 |
12 | These passes are therefore needed to ensure that an initial circuit is converted to a version that adheres to the
13 | requirements of the hardware.
14 | They can easily be applied using the following methods on the `circuit` object:
15 |
16 | - decompose
17 | - export
18 | - map
19 | - merge
20 | - route
21 | - validate
22 |
23 | ## Types of passes
24 |
25 | Given the methods stated above, the following types of passes are available:
26 |
27 | - [Decomposer](decomposition/index.md)
28 | - [Exporter](exporting/index.md)
29 | - [Mapper](mapping/index.md)
30 | - [Merger](merging/index.md)
31 | - [Router](routing/index.md)
32 | - [Validator](validation/index.md)
33 |
34 | !!! note "Integrated passes"
35 |
36 | The [reader](../tutorial/creating-a-circuit.md) and [writer](../tutorial/writing-out-and-exporting.md) passes are
37 | integrated in particular functionalities of the circuit.
38 | They are not applied in the same way as the passes mentioned above, _i.e._,
39 | by passing them as an argument when calling one of the aforementiond methods on the circuit.
40 | Instead, the reader and writer are executed when one parses a [cQASM](https://qutech-delft.github.io/cQASM-spec)
41 | string or writes out the circuit to a cQASM string, respectively.
42 | The reader is invoked when using the `Circuit.from_string` method,
43 | and the writer is invoked when converting the circuit to a string with `str` or printing it out with `print`.
44 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/simple_mappers.py:
--------------------------------------------------------------------------------
1 | """This module contains the following simple mappers:
2 |
3 | * IdentityMapper
4 | * HardcodedMapper
5 | * RandomMapper
6 | """
7 |
8 | from __future__ import annotations
9 |
10 | import random
11 | from typing import TYPE_CHECKING, Any
12 |
13 | from opensquirrel.passes.mapper.general_mapper import Mapper
14 | from opensquirrel.passes.mapper.mapping import Mapping
15 |
16 | if TYPE_CHECKING:
17 | from opensquirrel.ir import IR
18 |
19 |
20 | class IdentityMapper(Mapper):
21 | def __init__(self, **kwargs: Any) -> None:
22 | """An ``IdentityMapper`` maps each virtual qubit to exactly the same physical qubit."""
23 | super().__init__(**kwargs)
24 |
25 | def map(self, ir: IR, qubit_register_size: int) -> Mapping:
26 | """Create identity mapping."""
27 | return Mapping(list(range(qubit_register_size)))
28 |
29 |
30 | class HardcodedMapper(Mapper):
31 | def __init__(self, mapping: Mapping, **kwargs: Any) -> None:
32 | """
33 | A ``HardcodedMapper`` maps each virtual qubit to a hardcoded physical qubit
34 |
35 | Args:
36 | mapping: The mapping from virtual to physical qubits
37 | """
38 | super().__init__(**kwargs)
39 | self._mapping = mapping
40 |
41 | def map(self, ir: IR, qubit_register_size: int) -> Mapping:
42 | """Return the hardcoded mapping."""
43 | if qubit_register_size != self._mapping.size():
44 | msg = f"qubit register size ({qubit_register_size}) and mapping size ({self._mapping.size()}) differ"
45 | raise ValueError(msg)
46 | return self._mapping
47 |
48 |
49 | class RandomMapper(Mapper):
50 | def __init__(self, seed: int | None = None, **kwargs: Any) -> None:
51 | """
52 | A ``RandomMapper`` maps each virtual qubit to a random physical qubit.
53 |
54 | Args:
55 | seed: Random seed for reproducible results
56 | """
57 | super().__init__(**kwargs)
58 | self.seed = seed
59 |
60 | def map(self, ir: IR, qubit_register_size: int) -> Mapping:
61 | """Create a random mapping."""
62 | if self.seed:
63 | random.seed(self.seed)
64 |
65 | physical_qubit_register = list(range(qubit_register_size))
66 | random.shuffle(physical_qubit_register)
67 | return Mapping(physical_qubit_register)
68 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_aba_decomposer.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from collections.abc import Callable
3 |
4 | import numpy as np
5 | import pytest
6 |
7 | from opensquirrel import U
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import aba_decomposer as aba
11 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer, check_gate_replacement
12 |
13 | ABA_DECOMPOSER_LIST = [
14 | aba.XYXDecomposer,
15 | aba.XZXDecomposer,
16 | aba.YXYDecomposer,
17 | aba.YZYDecomposer,
18 | aba.ZXZDecomposer,
19 | aba.ZYZDecomposer,
20 | ]
21 |
22 |
23 | @pytest.mark.parametrize("aba_decomposer", ABA_DECOMPOSER_LIST)
24 | def test_specific_bloch_rotation(aba_decomposer: Callable[..., Decomposer]) -> None:
25 | decomposer = aba_decomposer()
26 | axis = [-0.53825, -0.65289, -0.53294]
27 | angle = 1.97871
28 |
29 | arbitrary_operation = SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(axis=axis, angle=angle, phase=0))
30 |
31 | decomposed_arbitrary_operation = decomposer.decompose(arbitrary_operation)
32 | check_gate_replacement(arbitrary_operation, decomposed_arbitrary_operation)
33 |
34 | u_gate = U(0, 1, 2, 3)
35 | decomposed_u_gate = decomposer.decompose(u_gate)
36 | check_gate_replacement(u_gate, decomposed_u_gate)
37 |
38 |
39 | @pytest.mark.parametrize("aba_decomposer", ABA_DECOMPOSER_LIST)
40 | def test_all_octants_of_bloch_sphere_rotation(aba_decomposer: Callable[..., Decomposer]) -> None:
41 | decomposer = aba_decomposer()
42 | steps = 5
43 | phase_steps = 3
44 | coordinates = np.linspace(-1, 1, num=steps)
45 | angles = np.linspace(-2 * np.pi, 2 * np.pi, num=steps)
46 | phases = np.linspace(-np.pi, np.pi, num=phase_steps)
47 | axes = [[x, y, z] for x, y, z in list(itertools.permutations(coordinates, 3)) if [x, y, z] != [0, 0, 0]]
48 |
49 | for axis in axes:
50 | for angle in angles:
51 | for phase in phases:
52 | arbitrary_operation = SingleQubitGate(
53 | qubit=0, gate_semantic=BlochSphereRotation(axis=axis, angle=angle, phase=phase)
54 | )
55 | decomposed_arbitrary_operation = decomposer.decompose(arbitrary_operation)
56 | check_gate_replacement(arbitrary_operation, decomposed_arbitrary_operation)
57 |
--------------------------------------------------------------------------------
/opensquirrel/default_gate_modifiers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from typing import TYPE_CHECKING, Any, SupportsFloat
5 |
6 | from opensquirrel.ir.semantics import BlochSphereRotation, ControlledGateSemantic
7 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
8 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
9 | from opensquirrel.utils.context import temporary_class_attr
10 |
11 | if TYPE_CHECKING:
12 | from opensquirrel.ir import QubitLike
13 |
14 |
15 | class GateModifier:
16 | pass
17 |
18 |
19 | class InverseGateModifier(GateModifier):
20 | def __init__(self, gate_generator: Callable[..., SingleQubitGate]) -> None:
21 | self.gate_generator = gate_generator
22 |
23 | def __call__(self, *args: Any) -> SingleQubitGate:
24 | with temporary_class_attr(BlochSphereRotation, attr="normalize_angle_params", value=False):
25 | gate: SingleQubitGate = self.gate_generator(*args)
26 | modified_angle = gate.bsr.angle * -1
27 | modified_phase = gate.bsr.phase * -1
28 | return SingleQubitGate(
29 | gate.qubit, BlochSphereRotation(axis=gate.bsr.axis, angle=modified_angle, phase=modified_phase)
30 | )
31 |
32 |
33 | class PowerGateModifier(GateModifier):
34 | def __init__(self, exponent: SupportsFloat, gate_generator: Callable[..., SingleQubitGate]) -> None:
35 | self.exponent = exponent
36 | self.gate_generator = gate_generator
37 |
38 | def __call__(self, *args: Any) -> SingleQubitGate:
39 | with temporary_class_attr(BlochSphereRotation, attr="normalize_angle_params", value=False):
40 | gate: SingleQubitGate = self.gate_generator(*args)
41 | modified_angle = gate.bsr.angle * float(self.exponent)
42 | modified_phase = gate.bsr.phase * float(self.exponent)
43 | return SingleQubitGate(
44 | gate.qubit, BlochSphereRotation(axis=gate.bsr.axis, angle=modified_angle, phase=modified_phase)
45 | )
46 |
47 |
48 | class ControlGateModifier(GateModifier):
49 | def __init__(self, gate_generator: Callable[..., SingleQubitGate]) -> None:
50 | self.gate_generator = gate_generator
51 |
52 | def __call__(self, control: QubitLike, *args: Any) -> TwoQubitGate:
53 | gate: SingleQubitGate = self.gate_generator(*args)
54 | return TwoQubitGate(control, gate.qubit, gate_semantic=ControlledGateSemantic(gate))
55 |
--------------------------------------------------------------------------------
/opensquirrel/common.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import tau
4 | from typing import TYPE_CHECKING, SupportsFloat
5 |
6 | import numpy as np
7 | from numpy.typing import NDArray
8 |
9 | if TYPE_CHECKING:
10 | from opensquirrel.ir.expression import BaseAxis
11 |
12 |
13 | ATOL = 0.000_000_1
14 | REPR_DECIMALS = 5
15 |
16 |
17 | def normalize_angle(x: SupportsFloat) -> float:
18 | r"""Normalize the angle to be in between the range of $(-\pi, \pi]$.
19 |
20 | Args:
21 | x: value to normalize.
22 |
23 | Returns:
24 | The normalized angle.
25 | """
26 | x = float(x)
27 | t = x - tau * (x // tau + 1)
28 | if t < -tau / 2 + ATOL:
29 | t += tau
30 | elif t > tau / 2:
31 | t -= tau
32 | return t
33 |
34 |
35 | def are_matrices_equivalent_up_to_global_phase(
36 | matrix_a: NDArray[np.complex128], matrix_b: NDArray[np.complex128]
37 | ) -> bool:
38 | """Checks whether two matrices are equivalent up to a global phase.
39 |
40 | Args:
41 | matrix_a: first matrix.
42 | matrix_b: second matrix.
43 |
44 | Returns:
45 | Whether two matrices are equivalent up to a global phase.
46 | """
47 | first_non_zero = next(
48 | (i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL
49 | )
50 |
51 | if abs(matrix_b[first_non_zero]) < ATOL:
52 | return False
53 |
54 | phase_difference = matrix_a[first_non_zero] / matrix_b[first_non_zero]
55 |
56 | return np.allclose(matrix_a, phase_difference * matrix_b, atol=ATOL)
57 |
58 |
59 | def is_identity_matrix_up_to_a_global_phase(matrix: NDArray[np.complex128]) -> bool:
60 | """Checks whether matrix is an identity matrix up to a global phase.
61 |
62 | Args:
63 | matrix: matrix to check.
64 | Returns:
65 | Whether matrix is an identity matrix up to a global phase.
66 | """
67 | return are_matrices_equivalent_up_to_global_phase(matrix, np.eye(matrix.shape[0], dtype=np.complex128))
68 |
69 |
70 | def repr_round(value: float | BaseAxis | NDArray[np.complex128], decimals: int = REPR_DECIMALS) -> str:
71 | """
72 | Given a numerical value (of type `float`, `Axis`, or `NDArray[np.complex128]`):
73 | - rounds it to `REPR_DECIMALS`,
74 | - converts it to string, and
75 | - removes the newlines.
76 |
77 | Returns:
78 | A single-line string representation of a numerical value.
79 | """
80 | return f"{np.round(value, decimals)}".replace("\n", "")
81 |
--------------------------------------------------------------------------------
/tests/reindexer/test_qubit_reindexer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import Y90, Circuit, CircuitBuilder, X
8 | from opensquirrel.ir import Gate, Measure
9 | from opensquirrel.ir.semantics import BlochSphereRotation, ControlledGateSemantic, MatrixGateSemantic
10 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
11 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
12 | from opensquirrel.reindexer.qubit_reindexer import get_reindexed_circuit
13 |
14 |
15 | def circuit_1_reindexed() -> Circuit:
16 | builder = CircuitBuilder(2)
17 | builder.Y90(1)
18 | builder.X(0)
19 | return builder.to_circuit()
20 |
21 |
22 | def replacement_gates_1() -> list[Gate]:
23 | return [Y90(1), X(3)]
24 |
25 |
26 | def replacement_gates_2() -> list[Gate | Measure]:
27 | return [
28 | Measure(1, 1),
29 | SingleQubitGate(qubit=3, gate_semantic=BlochSphereRotation(axis=(0, 0, 1), angle=pi, phase=pi / 2)),
30 | TwoQubitGate(
31 | qubit0=0,
32 | qubit1=3,
33 | gate_semantic=MatrixGateSemantic([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]),
34 | ),
35 | TwoQubitGate(qubit0=1, qubit1=2, gate_semantic=ControlledGateSemantic(target_gate=X(2))),
36 | ]
37 |
38 |
39 | def circuit_2_reindexed() -> Circuit:
40 | builder = CircuitBuilder(4, 4)
41 | builder.measure(0, 0)
42 | builder.ir.add_gate(
43 | SingleQubitGate(qubit=2, gate_semantic=BlochSphereRotation(axis=(0, 0, 1), angle=pi, phase=pi / 2))
44 | )
45 | builder.ir.add_gate(
46 | TwoQubitGate(1, 2, gate_semantic=MatrixGateSemantic([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]))
47 | )
48 | builder.ir.add_gate(TwoQubitGate(0, 3, gate_semantic=ControlledGateSemantic(target_gate=X(3))))
49 | return builder.to_circuit()
50 |
51 |
52 | @pytest.mark.parametrize(
53 | ("replacement_gates", "qubit_indices", "bit_register_size", "circuit_reindexed"),
54 | [
55 | (replacement_gates_1(), [3, 1], 0, circuit_1_reindexed()),
56 | (replacement_gates_2(), [1, 0, 3, 2], 4, circuit_2_reindexed()),
57 | ],
58 | ids=["circuit1", "circuit2"],
59 | )
60 | def test_get_reindexed_circuit(
61 | replacement_gates: list[Gate],
62 | qubit_indices: list[int],
63 | bit_register_size: int,
64 | circuit_reindexed: Circuit,
65 | ) -> None:
66 | circuit = get_reindexed_circuit(replacement_gates, qubit_indices, bit_register_size)
67 | assert circuit == circuit_reindexed
68 |
--------------------------------------------------------------------------------
/tests/test_circuit_matrix_calculator.py:
--------------------------------------------------------------------------------
1 | from math import pi
2 | from typing import Any
3 |
4 | import numpy as np
5 | import pytest
6 | from numpy.typing import NDArray
7 |
8 | from opensquirrel import CircuitBuilder
9 | from opensquirrel.circuit_matrix_calculator import get_circuit_matrix
10 |
11 |
12 | @pytest.mark.parametrize(
13 | ("builder", "expected_matrix"),
14 | [
15 | (CircuitBuilder(1).H(0), np.sqrt(0.5) * np.array([[1, 1], [1, -1]])),
16 | (CircuitBuilder(1).H(0).H(0), np.eye(2)),
17 | (CircuitBuilder(1).H(0).H(0).H(0), np.sqrt(0.5) * np.array([[1, 1], [1, -1]])),
18 | (
19 | CircuitBuilder(1).U(0, pi / 2, 0, pi),
20 | np.array([[0.70710678, 0.70710678], [0.70710678, -0.70710678]]),
21 | ),
22 | (
23 | CircuitBuilder(2).H(0).X(1),
24 | np.sqrt(0.5) * np.array([[0, 0, 1, 1], [0, 0, 1, -1], [1, 1, 0, 0], [1, -1, 0, 0]]),
25 | ),
26 | (
27 | CircuitBuilder(2).H(1).X(0),
28 | np.sqrt(0.5) * np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, -1], [1, 0, -1, 0]]),
29 | ),
30 | (CircuitBuilder(2).CNOT(1, 0), [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]),
31 | (CircuitBuilder(2).CNOT(0, 1), [[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]),
32 | (
33 | CircuitBuilder(2).H(0).CNOT(0, 1),
34 | np.sqrt(0.5) * np.array([[1, 1, 0, 0], [0, 0, 1, -1], [0, 0, 1, 1], [1, -1, 0, 0]]),
35 | ),
36 | (
37 | CircuitBuilder(3).H(0).CNOT(0, 2),
38 | np.sqrt(0.5)
39 | * np.array(
40 | [
41 | [1, 1, 0, 0, 0, 0, 0, 0],
42 | [0, 0, 0, 0, 1, -1, 0, 0],
43 | [0, 0, 1, 1, 0, 0, 0, 0],
44 | [0, 0, 0, 0, 0, 0, 1, -1],
45 | [0, 0, 0, 0, 1, 1, 0, 0],
46 | [1, -1, 0, 0, 0, 0, 0, 0],
47 | [0, 0, 0, 0, 0, 0, 1, 1],
48 | [0, 0, 1, -1, 0, 0, 0, 0],
49 | ],
50 | ),
51 | ),
52 | ],
53 | ids=[
54 | "H[0]",
55 | "H[0]H[0]",
56 | "H[0]H[0]H[0]",
57 | "U(pi / 2, 0, pi)[0]",
58 | "H[0]X[1]",
59 | "H[1]X[0]",
60 | "CNOT[1,0]",
61 | "CNOT[0,1]",
62 | "H[0]CNOT[0,1]",
63 | "H[0]CNOT[0,2]",
64 | ],
65 | )
66 | def test_get_circuit_matrix(builder: CircuitBuilder, expected_matrix: NDArray[Any]) -> None:
67 | circuit = builder.to_circuit()
68 | matrix = get_circuit_matrix(circuit)
69 | np.testing.assert_almost_equal(matrix, expected_matrix)
70 |
--------------------------------------------------------------------------------
/opensquirrel/default_instructions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from opensquirrel.ir import (
6 | Barrier,
7 | Init,
8 | Measure,
9 | Reset,
10 | Wait,
11 | )
12 | from opensquirrel.ir.default_gates import (
13 | CNOT,
14 | CR,
15 | CZ,
16 | SWAP,
17 | X90,
18 | Y90,
19 | Z90,
20 | CRk,
21 | H,
22 | I,
23 | MinusX90,
24 | MinusY90,
25 | MinusZ90,
26 | Rn,
27 | Rx,
28 | Ry,
29 | Rz,
30 | S,
31 | SDagger,
32 | T,
33 | TDagger,
34 | U,
35 | X,
36 | Y,
37 | Z,
38 | )
39 |
40 | if TYPE_CHECKING:
41 | from opensquirrel.ir import ControlInstruction, Gate, Instruction, NonUnitary
42 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
43 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
44 |
45 | default_bsr_without_params_set: dict[str, type[SingleQubitGate]] = {
46 | "H": H,
47 | "I": I,
48 | "S": S,
49 | "Sdag": SDagger,
50 | "T": T,
51 | "Tdag": TDagger,
52 | "X": X,
53 | "X90": X90,
54 | "Y": Y,
55 | "Y90": Y90,
56 | "Z": Z,
57 | "Z90": Z90,
58 | "mX90": MinusX90,
59 | "mY90": MinusY90,
60 | "mZ90": MinusZ90,
61 | }
62 |
63 | default_bsr_with_param_set: dict[str, type[SingleQubitGate]] = {
64 | "Rx": Rx,
65 | "Ry": Ry,
66 | "Rz": Rz,
67 | "U": U,
68 | "Rn": Rn,
69 | }
70 |
71 | default_single_qubit_gate_set: dict[str, type[SingleQubitGate]] = {
72 | **default_bsr_without_params_set,
73 | **default_bsr_with_param_set,
74 | }
75 |
76 | default_two_qubit_gate_set: dict[str, type[TwoQubitGate]] = {
77 | "CNOT": CNOT,
78 | "CR": CR,
79 | "CRk": CRk,
80 | "CZ": CZ,
81 | "SWAP": SWAP,
82 | }
83 |
84 | default_gate_alias_set = {
85 | "Hadamard": H,
86 | "Identity": I,
87 | }
88 | default_gate_set: dict[str, type[Gate]] = {
89 | **default_single_qubit_gate_set,
90 | **default_two_qubit_gate_set,
91 | **default_gate_alias_set,
92 | }
93 |
94 | default_non_unitary_set: dict[str, type[NonUnitary]] = {
95 | "init": Init,
96 | "measure": Measure,
97 | "reset": Reset,
98 | }
99 | default_control_instruction_set: dict[str, type[ControlInstruction]] = {
100 | "barrier": Barrier,
101 | "wait": Wait,
102 | }
103 | default_instruction_set: dict[str, type[Instruction]] = {
104 | **default_gate_set,
105 | **default_non_unitary_set,
106 | **default_control_instruction_set,
107 | }
108 |
109 |
110 | def is_anonymous_gate(name: str) -> bool:
111 | return name not in default_gate_set
112 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_zxz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 |
5 | import pytest
6 |
7 | from opensquirrel import CNOT, CR, H, I, Rx, Ry, Rz, X, Y, Z
8 | from opensquirrel.ir.semantics import BlochSphereRotation
9 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
10 | from opensquirrel.passes.decomposer import ZXZDecomposer
11 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
12 |
13 |
14 | @pytest.fixture
15 | def decomposer() -> ZXZDecomposer:
16 | return ZXZDecomposer()
17 |
18 |
19 | def test_identity(decomposer: ZXZDecomposer) -> None:
20 | gate = I(0)
21 | decomposed_gate = decomposer.decompose(gate)
22 | assert decomposed_gate == []
23 |
24 |
25 | @pytest.mark.parametrize(
26 | ("gate", "expected_result"),
27 | [
28 | (CNOT(0, 1), [CNOT(0, 1)]),
29 | (CR(2, 3, 2.123), [CR(2, 3, 2.123)]),
30 | (X(0), [Rx(0, pi)]),
31 | (Rx(0, 0.9), [Rx(0, 0.9)]),
32 | (Y(0), [Rz(0, -pi / 2), Rx(0, pi), Rz(0, pi / 2)]),
33 | (Ry(0, 0.9), [Rz(0, -pi / 2), Rx(0, 0.9000000000000004), Rz(0, pi / 2)]),
34 | (Z(0), [Rz(0, pi)]),
35 | (Rz(0, 0.123), [Rz(0, 0.123)]),
36 | (H(0), [Rz(0, pi / 2), Rx(0, pi / 2), Rz(0, pi / 2)]),
37 | (
38 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(angle=5.21, axis=(1, 2, 3), phase=0.324)),
39 | [Rz(0, -1.5521517485841891), Rx(0, -0.6209410696845807), Rz(0, 0.662145687003993)],
40 | ),
41 | ],
42 | ids=["CNOT", "CR", "X", "Rx", "Y", "Ry", "Z", "Rz", "H", "arbitrary"],
43 | )
44 | def test_zxz_decomposer(
45 | decomposer: ZXZDecomposer, gate: SingleQubitGate, expected_result: list[SingleQubitGate]
46 | ) -> None:
47 | decomposed_gate = decomposer.decompose(gate)
48 | check_gate_replacement(gate, decomposed_gate)
49 | assert decomposer.decompose(gate) == expected_result
50 |
51 |
52 | @pytest.mark.parametrize(
53 | "gate",
54 | [
55 | Ry(0, -pi / 2),
56 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(axis=[0.0, 1.0, 0.0], angle=-pi / 2, phase=0.0)),
57 | SingleQubitGate(qubit=0, gate_semantic=BlochSphereRotation(axis=[0.0, -1.0, 0.0], angle=pi / 2, phase=0.0)),
58 | ],
59 | ids=["Ry(-pi/2)", "BSR_1 of Ry(-pi/2)", "BSR_2 of Ry(-pi/2)"],
60 | )
61 | def test_specific_gate(decomposer: ZXZDecomposer, gate: SingleQubitGate) -> None:
62 | decomposed_gate = decomposer.decompose(gate)
63 | check_gate_replacement(gate, decomposed_gate)
64 |
65 |
66 | def test_find_unused_index() -> None:
67 | zxz_decomp = ZXZDecomposer()
68 | missing_index = zxz_decomp._find_unused_index()
69 |
70 | assert missing_index == 1
71 |
--------------------------------------------------------------------------------
/tests/passes/decomposer/test_swap2cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 |
8 | from opensquirrel import CNOT, CR, CZ, SWAP, CRk, H, Ry
9 | from opensquirrel.passes.decomposer import SWAP2CZDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import check_gate_replacement
11 |
12 | if TYPE_CHECKING:
13 | from opensquirrel.ir import Gate
14 |
15 |
16 | @pytest.fixture
17 | def decomposer() -> SWAP2CZDecomposer:
18 | return SWAP2CZDecomposer()
19 |
20 |
21 | @pytest.mark.parametrize(
22 | ("gate", "expected_result"),
23 | [
24 | (H(0), [H(0)]),
25 | (Ry(0, 2.345), [Ry(0, 2.345)]),
26 | ],
27 | ids=["Hadamard", "rotation_gate"],
28 | )
29 | def test_ignores_1q_gates(decomposer: SWAP2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
30 | check_gate_replacement(gate, expected_result)
31 | assert decomposer.decompose(gate) == expected_result
32 |
33 |
34 | @pytest.mark.parametrize(
35 | ("gate", "expected_result"),
36 | [
37 | (CNOT(0, 1), [CNOT(0, 1)]),
38 | (CR(0, 1, pi), [CR(0, 1, pi)]),
39 | (CRk(0, 1, 2), [CRk(0, 1, 2)]),
40 | (CZ(0, 1), [CZ(0, 1)]),
41 | ],
42 | ids=["CNOT_gate", "CR_gate", "CRk_gate", "CZ_gate"],
43 | )
44 | def test_ignores_2q_gates(decomposer: SWAP2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None:
45 | check_gate_replacement(gate, expected_result)
46 | assert decomposer.decompose(gate) == expected_result
47 |
48 |
49 | @pytest.mark.parametrize(
50 | ("gate", "expected_result"),
51 | [
52 | (
53 | SWAP(0, 1),
54 | [
55 | Ry(1, -pi / 2),
56 | CZ(0, 1),
57 | Ry(1, pi / 2),
58 | Ry(0, -pi / 2),
59 | CZ(1, 0),
60 | Ry(0, pi / 2),
61 | Ry(1, -pi / 2),
62 | CZ(0, 1),
63 | Ry(1, pi / 2),
64 | ],
65 | ),
66 | (
67 | SWAP(1, 0),
68 | [
69 | Ry(0, -pi / 2),
70 | CZ(1, 0),
71 | Ry(0, pi / 2),
72 | Ry(1, -pi / 2),
73 | CZ(0, 1),
74 | Ry(1, pi / 2),
75 | Ry(0, -pi / 2),
76 | CZ(1, 0),
77 | Ry(0, pi / 2),
78 | ],
79 | ),
80 | ],
81 | ids=["SWAP_0_1", "SWAP_1_0"],
82 | )
83 | def test_decomposes_SWAP(decomposer: SWAP2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None: # noqa: N802
84 | decomposed_gate = decomposer.decompose(gate)
85 | check_gate_replacement(gate, decomposed_gate)
86 | assert decomposed_gate == expected_result
87 |
--------------------------------------------------------------------------------
/opensquirrel/passes/mapper/qubit_remapper.py:
--------------------------------------------------------------------------------
1 | from opensquirrel.circuit import Circuit
2 | from opensquirrel.ir import (
3 | IR,
4 | Barrier,
5 | IRVisitor,
6 | NonUnitary,
7 | Qubit,
8 | Wait,
9 | )
10 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
11 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
12 | from opensquirrel.passes.mapper.mapping import Mapping
13 |
14 |
15 | class _QubitRemapper(IRVisitor):
16 | """
17 | Remap a whole IR.
18 |
19 | A new IR where the qubit indices are replaced by the values passed in mapping.
20 | _E.g._, for mapping = [3, 1, 0, 2]:
21 | - Qubit(index=0) becomes Qubit(index=3),
22 | - Qubit(index=1) becomes Qubit(index=1),
23 | - Qubit(index=2) becomes Qubit(index=0), and
24 | - Qubit(index=3) becomes Qubit(index=2).
25 |
26 | Args:
27 | mapping: a list of qubit indices, _e.g._, [3, 1, 0, 2]
28 |
29 | """
30 |
31 | def __init__(self, mapping: Mapping) -> None:
32 | self.mapping = mapping
33 |
34 | def visit_qubit(self, qubit: Qubit) -> Qubit:
35 | qubit.index = self.mapping[qubit.index]
36 | return qubit
37 |
38 | def visit_non_unitary(self, non_unitary: NonUnitary) -> NonUnitary:
39 | non_unitary.qubit.accept(self)
40 | return non_unitary
41 |
42 | def visit_barrier(self, barrier: Barrier) -> Barrier:
43 | barrier.qubit.accept(self)
44 | return barrier
45 |
46 | def visit_wait(self, wait: Wait) -> Wait:
47 | wait.qubit.accept(self)
48 | return wait
49 |
50 | def visit_single_qubit_gate(self, gate: SingleQubitGate) -> SingleQubitGate:
51 | gate.qubit.accept(self)
52 | return gate
53 |
54 | def visit_two_qubit_gate(self, gate: TwoQubitGate) -> TwoQubitGate:
55 | for operand in gate.get_qubit_operands():
56 | operand.accept(self)
57 |
58 | if gate.controlled is not None:
59 | gate.controlled.target_gate.qubit.accept(self)
60 | return gate
61 |
62 |
63 | def get_remapped_ir(circuit: Circuit, mapping: Mapping) -> IR:
64 | if len(mapping) > circuit.qubit_register_size:
65 | msg = "mapping is larger than the qubit register size"
66 | raise ValueError(msg)
67 | qubit_remapper = _QubitRemapper(mapping)
68 | replacement_ir = circuit.ir
69 | for statement in replacement_ir.statements:
70 | statement.accept(qubit_remapper)
71 | return replacement_ir
72 |
73 |
74 | def remap_ir(circuit: Circuit, mapping: Mapping) -> None:
75 | if len(mapping) > circuit.qubit_register_size:
76 | msg = "mapping is larger than the qubit register size"
77 | raise ValueError(msg)
78 | qubit_remapper = _QubitRemapper(mapping)
79 | for statement in circuit.ir.statements:
80 | statement.accept(qubit_remapper)
81 |
--------------------------------------------------------------------------------
/tests/docs/compilation-passes/test_validator.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import CircuitBuilder
4 | from opensquirrel.passes.validator import InteractionValidator, PrimitiveGateValidator
5 |
6 |
7 | class TestInteractionValidator:
8 | @pytest.fixture
9 | def connectivity(self) -> dict[str, list[int]]:
10 | return {"0": [1, 2], "1": [0, 2, 3], "2": [0, 1, 4], "3": [1, 4], "4": [2, 3]}
11 |
12 | @pytest.fixture
13 | def interaction_validator(self, connectivity: dict[str, list[int]]) -> InteractionValidator:
14 | return InteractionValidator(connectivity)
15 |
16 | def test_example_1(self, interaction_validator: InteractionValidator) -> None:
17 | builder = CircuitBuilder(5)
18 | builder.H(0)
19 | builder.CNOT(0, 1)
20 | builder.H(2)
21 | builder.CNOT(1, 2)
22 | builder.CNOT(2, 4)
23 | builder.CNOT(3, 4)
24 | circuit = builder.to_circuit()
25 | circuit.validate(validator=interaction_validator)
26 |
27 | def test_example_2(self, interaction_validator: InteractionValidator) -> None:
28 | builder = CircuitBuilder(5)
29 | builder.H(0)
30 | builder.CNOT(0, 1)
31 | builder.CNOT(0, 3)
32 | builder.H(2)
33 | builder.CNOT(1, 2)
34 | builder.CNOT(1, 3)
35 | builder.CNOT(2, 3)
36 | builder.CNOT(3, 4)
37 | builder.CNOT(0, 4)
38 | circuit = builder.to_circuit()
39 |
40 | with pytest.raises(
41 | ValueError,
42 | match=r"the following qubit interactions in the circuit prevent a 1-to-1"
43 | r" mapping:\{\(2, 3\), \(0, 3\), \(0, 4\)\}",
44 | ):
45 | circuit.validate(validator=interaction_validator)
46 |
47 |
48 | class TestPrimitiveGateValidator:
49 | def test_example_1(self) -> None:
50 | from math import pi
51 |
52 | pgs = ["I", "Rx", "Ry", "Rz", "CZ"]
53 |
54 | builder = CircuitBuilder(5)
55 | builder.Rx(0, pi / 2)
56 | builder.Ry(1, -pi / 2)
57 | builder.CZ(0, 1)
58 | builder.Ry(1, pi / 2)
59 | circuit = builder.to_circuit()
60 |
61 | circuit.validate(validator=PrimitiveGateValidator(primitive_gate_set=pgs))
62 |
63 | def test_example_2(self) -> None:
64 | pgs = ["I", "X90", "mX90", "Y90", "mY90", "Rz", "CZ"]
65 |
66 | builder = CircuitBuilder(5)
67 | builder.I(0)
68 | builder.X90(1)
69 | builder.mX90(2)
70 | builder.Y90(3)
71 | builder.mY90(4)
72 | builder.Rz(0, 2)
73 | builder.CZ(1, 2)
74 | builder.H(0)
75 | builder.CNOT(1, 2)
76 | circuit = builder.to_circuit()
77 |
78 | with pytest.raises(ValueError, match=r"the following gates are not in the primitive gate set: ['H', 'CNOT']"):
79 | circuit.validate(validator=PrimitiveGateValidator(primitive_gate_set=pgs))
80 |
--------------------------------------------------------------------------------
/tests/passes/mapper/test_qubit_remapper.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import CircuitBuilder
4 | from opensquirrel.circuit import Circuit
5 | from opensquirrel.passes.mapper.mapping import Mapping
6 | from opensquirrel.passes.mapper.qubit_remapper import get_remapped_ir, remap_ir
7 |
8 |
9 | class TestRemapper:
10 | @pytest.fixture
11 | def circuit_3(self) -> Circuit:
12 | builder = CircuitBuilder(3)
13 | builder.H(0)
14 | builder.CNOT(0, 1)
15 | builder.H(2)
16 | return builder.to_circuit()
17 |
18 | @pytest.fixture
19 | def circuit_3_remapped(self) -> Circuit:
20 | builder = CircuitBuilder(3)
21 | builder.H(2)
22 | builder.CNOT(2, 1)
23 | builder.H(0)
24 | return builder.to_circuit()
25 |
26 | @pytest.fixture
27 | def circuit_4(self) -> Circuit:
28 | builder = CircuitBuilder(4)
29 | builder.H(0)
30 | builder.CNOT(0, 1)
31 | builder.X(2)
32 | builder.Y(3)
33 | return builder.to_circuit()
34 |
35 | @pytest.fixture
36 | def circuit_4_remapped(self) -> Circuit:
37 | builder = CircuitBuilder(4)
38 | builder.H(3)
39 | builder.CNOT(3, 1)
40 | builder.X(0)
41 | builder.Y(2)
42 | return builder.to_circuit()
43 |
44 | @pytest.fixture
45 | def mapping_3(self) -> Mapping:
46 | return Mapping([2, 1, 0])
47 |
48 | @pytest.fixture
49 | def mapping_4(self) -> Mapping:
50 | return Mapping([3, 1, 0, 2])
51 |
52 | def test_get_remapped_ir_raise_value_error(self, circuit_3: Circuit, mapping_4: Mapping) -> None:
53 | with pytest.raises(ValueError, match="mapping is larger than the qubit register size"):
54 | get_remapped_ir(circuit_3, mapping_4)
55 |
56 | def test_get_remapped_ir_3_ok(self, circuit_3: Circuit, circuit_3_remapped: Circuit, mapping_3: Mapping) -> None:
57 | circuit_3.ir = get_remapped_ir(circuit_3, mapping_3)
58 | assert circuit_3 == circuit_3_remapped
59 |
60 | def test_get_remapped_ir_4_ok(self, circuit_4: Circuit, circuit_4_remapped: Circuit, mapping_4: Mapping) -> None:
61 | circuit_4.ir = get_remapped_ir(circuit_4, mapping_4)
62 | assert circuit_4 == circuit_4_remapped
63 |
64 | def test_remap_ir_raise_value_error(self, circuit_3: Circuit, mapping_4: Mapping) -> None:
65 | with pytest.raises(ValueError, match="mapping is larger than the qubit register size"):
66 | remap_ir(circuit_3, mapping_4)
67 |
68 | def test_remap_ir_3_ok(self, circuit_3: Circuit, circuit_3_remapped: Circuit, mapping_3: Mapping) -> None:
69 | remap_ir(circuit_3, mapping_3)
70 | assert circuit_3 == circuit_3_remapped
71 |
72 | def test_remap_ir_4_ok(self, circuit_4: Circuit, circuit_4_remapped: Circuit, mapping_4: Mapping) -> None:
73 | remap_ir(circuit_4, mapping_4)
74 | assert circuit_4 == circuit_4_remapped
75 |
--------------------------------------------------------------------------------
/opensquirrel/passes/merger/single_qubit_gates_merger.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | from opensquirrel import I
4 | from opensquirrel.ir import IR, AsmDeclaration, Barrier, Instruction, Qubit
5 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
6 | from opensquirrel.passes.merger.general_merger import Merger
7 |
8 |
9 | class SingleQubitGatesMerger(Merger):
10 | def merge(self, ir: IR, qubit_register_size: int) -> None:
11 | """Merge all consecutive 1-qubit gates in the circuit.
12 | Gates obtained from merging other gates become anonymous gates.
13 |
14 | Args:
15 | ir: Intermediate representation of the circuit.
16 | qubit_register_size: Size of the qubit register
17 |
18 | """
19 | accumulators_per_qubit: dict[Qubit, SingleQubitGate] = {
20 | Qubit(qubit_index): I(qubit_index) for qubit_index in range(qubit_register_size)
21 | }
22 |
23 | statement_index = 0
24 | while statement_index < len(ir.statements):
25 | statement = ir.statements[statement_index]
26 |
27 | # Accumulate consecutive Bloch sphere rotations
28 | instruction: Instruction = cast("Instruction", statement)
29 | if isinstance(instruction, SingleQubitGate):
30 | already_accumulated = accumulators_per_qubit[instruction.qubit]
31 | composed = already_accumulated * instruction
32 | accumulators_per_qubit[instruction.qubit] = composed
33 | del ir.statements[statement_index]
34 | continue
35 |
36 | def insert_accumulated_bloch_sphere_rotations(qubits: list[Qubit]) -> None:
37 | nonlocal statement_index
38 | for qubit in qubits:
39 | if not accumulators_per_qubit[qubit].is_identity():
40 | ir.statements.insert(statement_index, accumulators_per_qubit[qubit])
41 | accumulators_per_qubit[qubit] = I(qubit)
42 | statement_index += 1
43 |
44 | # For barrier directives, insert all accumulated Bloch sphere rotations
45 | # For other instructions, insert accumulated Bloch sphere rotations on qubits used by those instructions
46 | # In any case, reset the dictionary entry for the inserted accumulated Bloch sphere rotations
47 | if isinstance(instruction, Barrier) or isinstance(statement, AsmDeclaration):
48 | insert_accumulated_bloch_sphere_rotations([Qubit(i) for i in range(qubit_register_size)])
49 | else:
50 | insert_accumulated_bloch_sphere_rotations(instruction.get_qubit_operands())
51 | statement_index += 1
52 |
53 | for accumulated_bloch_sphere_rotation in accumulators_per_qubit.values():
54 | if not accumulated_bloch_sphere_rotation.is_identity():
55 | ir.statements.append(accumulated_bloch_sphere_rotation)
56 |
--------------------------------------------------------------------------------
/docs/circuit-builder/instructions/gates.md:
--------------------------------------------------------------------------------
1 | !!! note "Coming soon"
2 |
3 | # [Unitary Instructions](https://qutech-delft.github.io/cQASM-spec/latest/language_specification/statements/instructions/unitary_instructions.html)
4 |
5 | | Name | Operator | Description | Example |
6 | |------------|----------------|--------------------------------------------------|----------------------------|
7 | | I | _I_ | Identity gate | `builder.I(0)` |
8 | | H | _H_ | Hadamard gate | `builder.H(0)` |
9 | | X | _X_ | Pauli-X | `builder.X(0)` |
10 | | X90 | _X90_| Rotation around the x-axis of $\frac{\pi}{2}$ | `builder.X90(0)` |
11 | | mX90 | _X-90_| Rotation around the x-axis of $\frac{-\pi}{2}$ | `builder.mX90(0)` |
12 | | Y | _Y_ | Pauli-Y | `builder.Y(0)` |
13 | | Y90 | _Y90_| Rotation around the y-axis of $\frac{\pi}{2}$ | `builder.Y90(0)` |
14 | | mY90 | _Y-90_| Rotation around the y-axis of $\frac{-\pi}{2}$ | `builder.mY90(0)` |
15 | | Z | _Z_ | Pauli-Z | `builder.Z(0)` |
16 | | S | _S_ | Phase gate | `builder.S(0)` |
17 | | Sdag | _S†_| S dagger gate | `builder.Sdag(0)` |
18 | | T | _T_ | T | `builder.T(0)` |
19 | | Tdag | _T†_| T dagger gate | `builder.Tdag(0)` |
20 | | Rx | _Rx($\theta$)_| Arbitrary rotation around x-axis | `builder.Rx(0, 0.23)` |
21 | | Ry | _Ry($\theta$)_| Arbitrary rotation around y-axis | `builder.Ry(0, 0.23)` |
22 | | Rz | _Rz($\theta$)_ | Arbitrary rotation around z-axis | `builder.Rz(0, 2)` |
23 | | Rn | _Rn(nx, ny, nz, $\theta$, $\phi$g)_ | Arbitrary rotation around specified axis | `builder.Rn(0)` |
24 | | CZ | _CZ_ | Controlled-Z, Controlled-Phase | `builder.CZ(1, 2)` |
25 | | CR | _CR(\theta)_ | Controlled phase shift (arbitrary angle) | `builder.CR(0, 1, 3.1415)` |
26 | | CRk | _CRk(k)_ | Controlled phase shift ($\frac{\pi}{2^{k-1}}$) | `builder.CRk(1, 0, 2)` |
27 | | SWAP | _SWAP_ | SWAP gate | `builder.SWAP(1, 2)` |
28 | | CNOT | _CNOT_ | Controlled-NOT gate | `builder.CNOT(1, 2)` |
29 |
--------------------------------------------------------------------------------
/docs/compilation-passes/decomposition/predefined-decomposers.md:
--------------------------------------------------------------------------------
1 | #### 1. Predefined decomposition
2 |
3 | The first kind of decomposition is when you want to replace a particular gate in the circuit,
4 | like the `CNOT` gate, with a fixed list of gates.
5 | It is commonly known that `CNOT` can be decomposed as `H`-`CZ`-`H`.
6 | This decomposition is demonstrated below using a Python _lambda function_,
7 | which requires the same parameters as the gate that is decomposed:
8 |
9 | ```python
10 | from opensquirrel.circuit import Circuit
11 | from opensquirrel import CNOT, H, CZ
12 |
13 | circuit = Circuit.from_string(
14 | """
15 | version 3.0
16 | qubit[3] q
17 |
18 | X q[0:2] // Note that this notation is expanded in OpenSquirrel.
19 | CNOT q[0], q[1]
20 | Ry q[2], 6.78
21 | """
22 | )
23 | circuit.replace(
24 | CNOT,
25 | lambda control, target:
26 | [
27 | H(target),
28 | CZ(control, target),
29 | H(target),
30 | ]
31 | )
32 |
33 | print(circuit)
34 | ```
35 | _Output_:
36 |
37 | version 3.0
38 |
39 | qubit[3] q
40 |
41 | X q[0]
42 | X q[1]
43 | X q[2]
44 | H q[1]
45 | CZ q[0], q[1]
46 | H q[1]
47 | Ry(6.78) q[2]
48 |
49 | OpenSquirrel will check whether the provided decomposition is correct.
50 | For instance, an exception is thrown if we forget the final Hadamard,
51 | or H gate, in our custom-made decomposition:
52 |
53 | ```python
54 | from opensquirrel.circuit import Circuit
55 | from opensquirrel import CNOT, CZ, H
56 |
57 | circuit = Circuit.from_string(
58 | """
59 | version 3.0
60 | qubit[3] q
61 |
62 | X q[0:2]
63 | CNOT q[0], q[1]
64 | Ry q[2], 6.78
65 | """
66 | )
67 | try:
68 | circuit.replace(
69 | CNOT,
70 | lambda control, target:
71 | [
72 | H(target),
73 | CZ(control, target),
74 | ]
75 | )
76 | except Exception as e:
77 | print(e)
78 | ```
79 | _Output_:
80 |
81 | replacement for gate CNOT does not preserve the quantum state
82 |
83 | ##### _`CNOT` to `CZ` decomposer_
84 |
85 | The decomposition of the `CNOT` gate into a `CZ` gate (with additional single-qubit gates) is used frequently.
86 | To this end a `CNOT2CZDecomposer` has been implemented that decomposes any `CNOT`s in a circuit to a
87 | `Ry(-π/2)`-`CZ`-`Ry(π/2)`. The decomposition is illustrated in the image below.
88 |
89 | 
90 |
91 | `Ry` gates are used instead of, _e.g._, `H` gates, as they are, generally,
92 | more likely to be supported already by target backends.
93 |
94 | ##### _`SWAP` to `CNOT` decomposer_
95 |
96 | The `SWAP2CNOTDecomposer` implements the predefined decomposition of the `SWAP` gate into 3 `CNOT` gates.
97 | The decomposition is illustrated in the image below.
98 |
99 | 
100 |
101 | ##### _`SWAP` to `CZ` decomposer_
102 |
103 | The `SWAP2CZDecomposer` implements the predefined decomposition of the `SWAP` gate into `Ry` rotations and 3 `CZ`
104 | gates.
105 | The decomposition is illustrated in the image below.
106 |
107 | 
108 |
--------------------------------------------------------------------------------
/opensquirrel/ir/two_qubit_gate.py:
--------------------------------------------------------------------------------
1 | from functools import cached_property
2 | from typing import Any
3 |
4 | import numpy as np
5 |
6 | from opensquirrel.ir import Gate, IRVisitor, Qubit, QubitLike
7 | from opensquirrel.ir.expression import Expression
8 | from opensquirrel.ir.semantics import CanonicalGateSemantic, ControlledGateSemantic, MatrixGateSemantic
9 | from opensquirrel.ir.semantics.gate_semantic import GateSemantic
10 | from opensquirrel.utils import get_matrix
11 |
12 |
13 | class TwoQubitGate(Gate):
14 | def __init__(
15 | self, qubit0: QubitLike, qubit1: QubitLike, gate_semantic: GateSemantic, name: str = "TwoQubitGate"
16 | ) -> None:
17 | Gate.__init__(self, name)
18 | self.qubit0 = Qubit(qubit0)
19 | self.qubit1 = Qubit(qubit1)
20 |
21 | self._controlled = gate_semantic if isinstance(gate_semantic, ControlledGateSemantic) else None
22 | self._matrix = gate_semantic if isinstance(gate_semantic, MatrixGateSemantic) else None
23 | self._canonical = gate_semantic if isinstance(gate_semantic, CanonicalGateSemantic) else None
24 | self.gate_semantic = gate_semantic
25 |
26 | if self._controlled and (self.qubit1 != self._controlled.target_gate.qubit):
27 | msg = "the qubit from the target gate does not match with 'qubit1'."
28 | raise ValueError(msg)
29 |
30 | if self._check_repeated_qubit_operands([self.qubit0, self.qubit1]):
31 | msg = "qubit0 and qubit1 cannot be the same"
32 | raise ValueError(msg)
33 |
34 | def __repr__(self) -> str:
35 | return f"TwoQubitGate(qubits=[{self.qubit0, self.qubit1}], gate_semantic={self.gate_semantic})"
36 |
37 | @cached_property
38 | def matrix(self) -> MatrixGateSemantic:
39 | if self._matrix:
40 | return self._matrix
41 |
42 | if self.controlled:
43 | self._matrix = MatrixGateSemantic(get_matrix(self, 2))
44 | return self._matrix
45 |
46 | if self.canonical:
47 | from opensquirrel.utils.matrix_expander import can2
48 |
49 | return MatrixGateSemantic(can2(self.canonical.axis))
50 | return MatrixGateSemantic(np.eye(4))
51 |
52 | @cached_property
53 | def canonical(self) -> CanonicalGateSemantic | None:
54 | return self._canonical
55 |
56 | @cached_property
57 | def controlled(self) -> ControlledGateSemantic | None:
58 | return self._controlled
59 |
60 | def accept(self, visitor: IRVisitor) -> Any:
61 | visit_parent = super().accept(visitor)
62 | return visit_parent if visit_parent is not None else visitor.visit_two_qubit_gate(self)
63 |
64 | @property
65 | def arguments(self) -> tuple[Expression, ...]:
66 | return (self.qubit0, self.qubit1)
67 |
68 | def get_qubit_operands(self) -> list[Qubit]:
69 | return [self.qubit0, self.qubit1]
70 |
71 | def is_identity(self) -> bool:
72 | if self.controlled:
73 | return self.controlled.is_identity()
74 | if self.matrix:
75 | return self.matrix.is_identity()
76 | if self.canonical:
77 | return self.canonical.is_identity()
78 | return False
79 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/cnot_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | from opensquirrel import CNOT, Ry, Rz, X
7 | from opensquirrel.common import ATOL
8 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
9 | from opensquirrel.passes.decomposer import ZYZDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
11 | from opensquirrel.utils.identity_filter import filter_out_identities
12 |
13 | if TYPE_CHECKING:
14 | from opensquirrel.ir import Gate
15 |
16 |
17 | class CNOTDecomposer(Decomposer):
18 | """
19 | Decomposes 2-qubit controlled unitary gates to CNOT + Rz/Ry.
20 | Applying single-qubit gate fusion after this pass might be beneficial.
21 |
22 | Source of the math: https://threeplusone.com/pubs/on_gates.pdf, chapter 7.5 "ABC decomposition"
23 | """
24 |
25 | def decompose(self, g: Gate) -> list[Gate]:
26 | if not isinstance(g, TwoQubitGate):
27 | return [g]
28 |
29 | if not g.controlled:
30 | # Do nothing, this is not a controlled unitary gate.
31 | return [g]
32 |
33 | control_qubit = g.qubit0
34 | target_qubit = g.qubit1
35 | target_gate = g.controlled.target_gate
36 |
37 | # Perform ZYZ decomposition on the target gate.
38 | # This gives us an ABC decomposition (U = AXBXC, ABC = I) of the target gate.
39 | # See https://threeplusone.com/pubs/on_gates.pdf
40 |
41 | # Try special case first, see https://arxiv.org/pdf/quant-ph/9503016.pdf lemma 5.5
42 | controlled_rotation_times_x = target_gate * X(target_qubit)
43 | theta0_with_x, theta1_with_x, theta2_with_x = ZYZDecomposer().get_decomposition_angles(
44 | controlled_rotation_times_x.bsr.axis,
45 | controlled_rotation_times_x.bsr.angle,
46 | )
47 | if abs((theta0_with_x - theta2_with_x) % (2 * pi)) < ATOL:
48 | # The decomposition can use a single CNOT according to the lemma.
49 | A = [Ry(target_qubit, -theta1_with_x / 2), Rz(target_qubit, -theta2_with_x)] # noqa: N806
50 | B = [Rz(target_qubit, theta2_with_x), Ry(target_qubit, theta1_with_x / 2)] # noqa: N806
51 | return filter_out_identities(
52 | [
53 | *B,
54 | CNOT(control_qubit, target_qubit),
55 | *A,
56 | Rz(control_qubit, target_gate.bsr.phase - pi / 2),
57 | ],
58 | )
59 |
60 | theta0, theta1, theta2 = ZYZDecomposer().get_decomposition_angles(target_gate.bsr.axis, target_gate.bsr.angle)
61 |
62 | A = [Ry(target_qubit, theta1 / 2), Rz(target_qubit, theta2)] # noqa: N806
63 | B = [Rz(target_qubit, -(theta0 + theta2) / 2), Ry(target_qubit, -theta1 / 2)] # noqa: N806
64 | C = [Rz(target_qubit, (theta0 - theta2) / 2)] # noqa: N806
65 |
66 | return filter_out_identities(
67 | [
68 | *C,
69 | CNOT(control_qubit, target_qubit),
70 | *B,
71 | CNOT(control_qubit, target_qubit),
72 | *A,
73 | Rz(control_qubit, target_gate.bsr.phase),
74 | ],
75 | )
76 |
--------------------------------------------------------------------------------
/docs/tutorial/writing-out-and-exporting.md:
--------------------------------------------------------------------------------
1 | ## Writing out
2 |
3 | OpenSquirrel's native tongue is
4 | [cQASM](https://qutech-delft.github.io/cQASM-spec/).
5 | Accordingly, it is straightforward to write out a circuit, since the string representation of a circuit is a cQASM
6 | string.
7 |
8 | Use the Python built-in methods `str` or `print` to obtain the cQASM string of the circuit.
9 |
10 | In the case of the example program that we compiled in the [previous section](applying-compilation-passes.md),
11 | we simply do the following to write out the circuit:
12 |
13 | ```python
14 | # To return the cQASM string
15 | cqasm_string = str(circuit)
16 |
17 | # To print the cQASM string
18 | print(circuit)
19 | ```
20 |
21 | ## Exporting
22 |
23 | Alternatively, it is possible to [export](../compilation-passes/exporting/index.md) the circuit to a
24 | different format.
25 | This can be done by using the `export` method with the desired format as an input argument.
26 |
27 | For instance, if we want to export our circuit to
28 | [cQASM 1.0](https://libqasm.readthedocs.io/) (given by the export format `CQASM_V1`) we write the following:
29 |
30 | ```python
31 | from opensquirrel.passes.exporter import CqasmV1Exporter
32 |
33 | exported_circuit = circuit.export(exporter=CqasmV1Exporter())
34 | ```
35 |
36 | This uses the [cQASMv1 exporter](../compilation-passes/exporting/cqasm-v1-exporter.md) to export the
37 | circuit to a cQASM 1.0 string.
38 |
39 | ??? example "`print(exported_circuit) # Compiled program in terms of cQASM 1.0`"
40 |
41 | ```linenums="1"
42 | version 1.0
43 |
44 | qubits 3
45 |
46 | prep_z q[0]
47 | prep_z q[1]
48 | prep_z q[2]
49 | rz q[0], 1.5707963
50 | x90 q[0]
51 | rz q[0], 1.5707963
52 | rz q[1], 1.5707963
53 | x90 q[1]
54 | rz q[1], -1.5707963
55 | cz q[0], q[1]
56 | rz q[1], -1.5707963
57 | x90 q[1]
58 | rz q[1], 1.5707963
59 | rz q[0], 1.5707963
60 | x90 q[0]
61 | rz q[0], -1.5707963
62 | cz q[1], q[0]
63 | rz q[0], -1.5707963
64 | x90 q[0]
65 | rz q[0], 1.5707963
66 | rz q[1], 1.5707963
67 | x90 q[1]
68 | rz q[1], -1.5707963
69 | cz q[0], q[1]
70 | rz q[1], -1.5707963
71 | x90 q[1]
72 | rz q[1], 1.5707963
73 | rz q[2], 1.5707963
74 | x90 q[2]
75 | rz q[2], -1.5707963
76 | cz q[1], q[2]
77 | rz q[2], -1.5707963
78 | x90 q[2]
79 | rz q[2], 1.5707963
80 | barrier q[1, 0, 2]
81 | measure_z q[1]
82 | measure_z q[2]
83 | ```
84 |
85 | Note that there may be language constructs that do not have a straightforward translation from cQASM to the chosen
86 | format. For example, [cQASM 1.0](https://libqasm.readthedocs.io/) does not support the declaration of bit registers.
87 | Consequently, any information regarding bit registers and variables will be _lost-in-translation_, _e.g._,
88 | in cQASM 1.0, measurement outcomes cannot be written to a specific bit register variable even if this has been done in
89 | the original cQASM program.
90 |
91 | !!! warning "Translation is not always straightforward"
92 |
93 | Make sure to check the documentation on the specific
94 | [exporters](../compilation-passes/exporting/index.md) to understand how the translation is done,
95 | since not all language constructs can be straightforwardly translated from
96 | [cQASM](https://qutech-delft.github.io/cQASM-spec/) into any alternative format.
97 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/mckay_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import atan2, cos, pi, sin, sqrt
4 |
5 | from opensquirrel import X90, I, Rz
6 | from opensquirrel.common import ATOL, normalize_angle
7 | from opensquirrel.ir import Axis, Gate
8 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
9 | from opensquirrel.passes.decomposer import ZXZDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
11 |
12 |
13 | class McKayDecomposer(Decomposer):
14 | def decompose(self, g: Gate) -> list[Gate]:
15 | """Return the McKay decomposition of a 1-qubit gate as a list of gates.
16 | gate ----> Rz.Rx(pi/2).Rz.Rx(pi/2).Rz
17 |
18 | The global phase is deemed _irrelevant_, therefore a simulator backend might produce different output.
19 | The results should be equivalent modulo global phase.
20 | Notice that, if the gate is Rz or X90, it will not be decomposed further, since they are natively used
21 | in the McKay decomposition.
22 |
23 | Relevant literature: https://arxiv.org/abs/1612.00858
24 | """
25 | if not isinstance(g, SingleQubitGate) or g == X90(g.qubit):
26 | return [g]
27 |
28 | if abs(g.bsr.angle) < ATOL:
29 | return [I(g.qubit)]
30 |
31 | if g.bsr.axis[0] == 0 and g.bsr.axis[1] == 0:
32 | rz_angle = float(g.bsr.angle * g.bsr.axis[2])
33 | return [Rz(g.qubit, rz_angle)]
34 |
35 | zxz_decomposition = ZXZDecomposer().decompose(g)
36 | zxz_angle = 0.0
37 | if len(zxz_decomposition) >= 2:
38 | zxz_angle = next(
39 | gate.bsr.angle
40 | for gate in zxz_decomposition
41 | if isinstance(gate, SingleQubitGate) and gate.bsr.axis == Axis(1, 0, 0)
42 | )
43 |
44 | if abs(zxz_angle - pi / 2) < ATOL:
45 | return [
46 | X90(g.qubit) if isinstance(gate, SingleQubitGate) and gate.bsr.axis == Axis(1, 0, 0) else gate
47 | for gate in zxz_decomposition
48 | ]
49 |
50 | # McKay decomposition
51 | za_mod = sqrt(cos(g.bsr.angle / 2) ** 2 + (g.bsr.axis[2] * sin(g.bsr.angle / 2)) ** 2)
52 | zb_mod = abs(sin(g.bsr.angle / 2)) * sqrt(g.bsr.axis[0] ** 2 + g.bsr.axis[1] ** 2)
53 |
54 | theta = pi - 2 * atan2(zb_mod, za_mod)
55 |
56 | alpha = atan2(-sin(g.bsr.angle / 2) * g.bsr.axis[2], cos(g.bsr.angle / 2))
57 | beta = atan2(-sin(g.bsr.angle / 2) * g.bsr.axis[0], -sin(g.bsr.angle / 2) * g.bsr.axis[1])
58 |
59 | lam = beta - alpha
60 | phi = -beta - alpha - pi
61 |
62 | lam = normalize_angle(lam)
63 | phi = normalize_angle(phi)
64 | theta = normalize_angle(theta)
65 |
66 | decomposed_g: list[Gate] = []
67 |
68 | if abs(theta) < ATOL and lam == phi:
69 | decomposed_g.extend((X90(g.qubit), X90(g.qubit)))
70 | return decomposed_g
71 |
72 | if abs(lam) > ATOL:
73 | decomposed_g.append(Rz(g.qubit, lam))
74 |
75 | decomposed_g.append(X90(g.qubit))
76 |
77 | if abs(theta) > ATOL:
78 | decomposed_g.append(Rz(g.qubit, theta))
79 |
80 | decomposed_g.append(X90(g.qubit))
81 |
82 | if abs(phi) > ATOL:
83 | decomposed_g.append(Rz(g.qubit, phi))
84 |
85 | return decomposed_g
86 |
--------------------------------------------------------------------------------
/docs/compilation-passes/mapping/qgym-mapper.md:
--------------------------------------------------------------------------------
1 | The [QGym](https://github.com/QuTech-Delft/qgym) package functions in a manner similar to the well-known [gym](https://gymnasium.farama.org/) package,
2 | in the sense that it provides a number of environments on which reinforcement learning (RL) agents can be applied.
3 | The main purpose of qgym is to develop reinforcement learning environments which represent various passes of the
4 | [OpenQL framework](https://arxiv.org/abs/2005.13283).
5 |
6 | The package offers RL-based environments resembling quantum compilation steps,
7 | namely for initial mapping, qubit routing, and gate scheduling.
8 | The environments offer all the relevant components needed to train agents,
9 | including states and action spaces, and (customizable) reward functions (basically all the components required by a
10 | Markov Decision Process).
11 | Furthermore, the actual training of the agents is handled by the
12 | [StableBaselines3](https://github.com/DLR-RM/stable-baselines3) python package,
13 | which offers reliable, customizable, out of the box Pytorch implementations of DRL agents.
14 |
15 | The initial mapping problem is translated to a RL context within QGym in the following manner.
16 | The setup begins with a fixed connection graph (an undirected graph representation of the hardware connectivity),
17 | static across all episodes.
18 | Each episode introduces a novel, randomly generated interaction graph (undirected graph representation of the qubit
19 | interactions within the circuit) for the agent to observe,
20 | alongside an initially empty mapping.
21 | At every step, the agent can map a virtual qubit to a physical qubit until the mapping is fully established.
22 | In theory, this process enables the training of agents that are capable of managing various interaction graphs on a
23 | predetermined connectivity.
24 | Both the interaction and connection graphs are easily represented via
25 | [Networkx](https://networkx.org/) graphs.
26 |
27 | At the moment, the following DRL agents can be used to map circuits in Opensquirrel:
28 |
29 | - Proximal Policy Optimization (PPO)
30 | - Advantage Actor-Critic (A2C)
31 | - Trust Region Policy Optimization (TRPO)
32 | - Recurrent PPO
33 | - PPO with illegal action masking
34 |
35 | The last three agents in the list above can be imported from the extension/experimental package of StableBaselines3,
36 | namely [sb3-contrib](https://github.com/Stable-Baselines-Team/stable-baselines3-contrib).
37 |
38 | The following code snippet demonstrates the usage of the `QGymMapper`.
39 |
40 | We assume that the `connectivity` of the target backend QPU is known,
41 | as well as that a `TRPO.zip` file, containing the weights of a trained agent,
42 | is available in the working directory.
43 |
44 | ```python
45 | from opensquirrel.passes.mapper import QGymMapper
46 | from opensquirrel import CircuitBuilder
47 | import networkx as nx
48 | import json
49 |
50 | connectivity = {
51 | "0": [2],
52 | "1": [2],
53 | "2": [0, 1, 3, 4],
54 | "3": [2],
55 | "4": [2],
56 | }
57 |
58 | qgym_mapper = QGymMapper(
59 | agent_class="TRPO",
60 | agent_path="path-to-agent/TRPO.zip",
61 | connectivity=connectivity
62 | )
63 |
64 | builder = CircuitBuilder(5)
65 | builder.H(0)
66 | builder.CNOT(0, 1)
67 | builder.H(2)
68 | builder.CNOT(1, 2)
69 | builder.CNOT(2, 4)
70 | builder.CNOT(3, 4)
71 | circuit = builder.to_circuit()
72 |
73 | circuit.map(mapper = qgym_mapper)
74 | ```
75 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/cz_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from math import pi
4 | from typing import TYPE_CHECKING
5 |
6 | from opensquirrel import CZ, Rx, Ry, Rz, Z
7 | from opensquirrel.common import ATOL
8 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
9 | from opensquirrel.passes.decomposer import XYXDecomposer
10 | from opensquirrel.passes.decomposer.general_decomposer import Decomposer
11 | from opensquirrel.utils.identity_filter import filter_out_identities
12 |
13 | if TYPE_CHECKING:
14 | from opensquirrel.ir import Gate
15 |
16 |
17 | class CZDecomposer(Decomposer):
18 | """
19 | Decomposes 2-qubit controlled unitary gates to CZ + Rx/Ry.
20 | Applying single-qubit gate fusion after this pass might be beneficial.
21 |
22 | Source of the math: https://threeplusone.com/pubs/on_gates.pdf, chapter 7.5 "ABC decomposition"
23 | """
24 |
25 | def decompose(self, g: Gate) -> list[Gate]:
26 | if not isinstance(g, TwoQubitGate):
27 | return [g]
28 |
29 | if not g.controlled:
30 | # Do nothing:
31 | # - BlochSphereRotation's are only single-qubit,
32 | # - decomposing MatrixGate is currently not supported.
33 | return [g]
34 |
35 | control_qubit, target_qubit = g.get_qubit_operands()
36 | target_gate = g.controlled.target_gate
37 |
38 | # Perform XYX decomposition on the target gate.
39 | # This gives us an ABC decomposition (U = exp(i phase) * AZBZC, ABC = I) of the target gate.
40 | # See https://threeplusone.com/pubs/on_gates.pdf
41 |
42 | # Try special case first, see https://arxiv.org/pdf/quant-ph/9503016.pdf lemma 5.5
43 | # Note that here V = Rx(a) * Ry(th) * Rx(a) * Z to create V = AZBZ, with AB = I
44 | controlled_rotation_times_z = target_gate * Z(target_qubit)
45 | theta0_with_z, theta1_with_z, theta2_with_z = XYXDecomposer().get_decomposition_angles(
46 | controlled_rotation_times_z.bsr.axis,
47 | controlled_rotation_times_z.bsr.angle,
48 | )
49 | if abs((theta0_with_z - theta2_with_z) % (2 * pi)) < ATOL:
50 | # The decomposition can use a single CZ according to the lemma.
51 | A = [Ry(target_qubit, theta1_with_z / 2), Rx(target_qubit, theta2_with_z)] # noqa: N806
52 | B = [Rx(target_qubit, -theta2_with_z), Ry(target_qubit, -theta1_with_z / 2)] # noqa: N806
53 | return filter_out_identities(
54 | [
55 | *B,
56 | CZ(control_qubit, target_qubit),
57 | *A,
58 | Rz(control_qubit, target_gate.bsr.phase - pi / 2),
59 | ],
60 | )
61 |
62 | theta0, theta1, theta2 = XYXDecomposer().get_decomposition_angles(target_gate.bsr.axis, target_gate.bsr.angle)
63 |
64 | A = [Ry(target_qubit, theta1 / 2), Rx(target_qubit, theta2)] # noqa: N806
65 | B = [Rx(target_qubit, -(theta0 + theta2) / 2), Ry(target_qubit, -theta1 / 2)] # noqa: N806
66 | C = [Rx(target_qubit, (theta0 - theta2) / 2)] # noqa: N806
67 |
68 | return filter_out_identities(
69 | [
70 | *C,
71 | CZ(control_qubit, target_qubit),
72 | *B,
73 | CZ(control_qubit, target_qubit),
74 | *A,
75 | Rz(control_qubit, target_gate.bsr.phase),
76 | ],
77 | )
78 |
--------------------------------------------------------------------------------
/tests/test_circuit.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import CircuitBuilder
4 | from opensquirrel.circuit import MeasurementToBitMap
5 | from opensquirrel.ir import AsmDeclaration
6 |
7 |
8 | def test_asm_filter() -> None:
9 | builder = CircuitBuilder(2)
10 | builder.asm("BackendTest_1", """backend code""") # other name
11 | builder.H(0)
12 | builder.asm("TestBackend", """backend code""") # exact relevant name
13 | builder.CNOT(0, 1)
14 | builder.asm("TestBackend_1", """backend code""") # relevant name variant 1
15 | builder.CNOT(0, 1)
16 | builder.asm("testbackend", """backend code""") # lowercase name
17 | builder.H(0)
18 | builder.asm("_TestBackend_2", """backend code""") # relevant name variant 2
19 | builder.to_circuit()
20 | circuit = builder.to_circuit()
21 |
22 | asm_statements = [statement for statement in circuit.ir.statements if isinstance(statement, AsmDeclaration)]
23 | assert len(circuit.ir.statements) == 9
24 | assert len(asm_statements) == 5
25 |
26 | relevant_backend_name = "TestBackend"
27 | circuit.asm_filter(relevant_backend_name)
28 |
29 | asm_statements = [statement for statement in circuit.ir.statements if isinstance(statement, AsmDeclaration)]
30 | assert len(circuit.ir.statements) == 7
31 | assert len(asm_statements) == 3
32 |
33 | for statement in asm_statements:
34 | assert relevant_backend_name in str(statement.backend_name)
35 |
36 |
37 | def test_instruction_count() -> None:
38 | builder = CircuitBuilder(2)
39 | builder.H(0)
40 | builder.CNOT(0, 1)
41 | circuit = builder.to_circuit()
42 | counts = circuit.instruction_count
43 | assert counts == {"H": 1, "CNOT": 1}
44 |
45 | # non-unitaries
46 | builder = CircuitBuilder(2, bit_register_size=2)
47 | builder.barrier(1)
48 | builder.init(0)
49 | builder.measure(0, 0)
50 | circuit = builder.to_circuit()
51 | counts = circuit.instruction_count
52 | assert counts == {"barrier": 1, "init": 1, "measure": 1}
53 |
54 | # asm statements
55 | builder = CircuitBuilder(2)
56 | builder.barrier(0)
57 | builder.asm("asm_name", "asm_code")
58 | builder.barrier(1)
59 | circuit = builder.to_circuit()
60 | counts = circuit.instruction_count
61 | assert counts == {"barrier": 2}
62 |
63 |
64 | @pytest.mark.parametrize(
65 | ("builder", "m2b_mapping"),
66 | [
67 | (
68 | CircuitBuilder(3, 3).X(0).Y(1).Z(2),
69 | {},
70 | ),
71 | (
72 | CircuitBuilder(3, 3).measure(0, 2).measure(2, 0),
73 | {"0": [2], "2": [0]},
74 | ),
75 | (
76 | CircuitBuilder(3, 3).measure(2, 2).measure(1, 2).measure(0, 2),
77 | {"0": [2], "1": [2], "2": [2]},
78 | ),
79 | (
80 | CircuitBuilder(3, 3).measure(1, 1).measure(0, 0).measure(1, 1).measure(0, 0),
81 | {"0": [0, 0], "1": [1, 1]},
82 | ),
83 | (
84 | CircuitBuilder(3, 3).X(0).measure(0, 0).X(1).measure(1, 1).X(2).measure(2, 0).X(0).measure(0, 2),
85 | {"0": [0, 2], "1": [1], "2": [0]},
86 | ),
87 | ],
88 | ids=["no_measurement", "no_measurement_on_1_qubit", "overwriting_bit_1", "overwriting_bit_2", "example_circuit"],
89 | )
90 | def test_measurement_to_bit_mapping(builder: CircuitBuilder, m2b_mapping: MeasurementToBitMap) -> None:
91 | circuit = builder.to_circuit()
92 | assert circuit.measurement_to_bit_map == m2b_mapping
93 |
--------------------------------------------------------------------------------
/opensquirrel/reindexer/qubit_reindexer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Iterable
4 | from typing import TYPE_CHECKING
5 |
6 | from opensquirrel.ir import (
7 | IR,
8 | Barrier,
9 | Gate,
10 | Init,
11 | IRVisitor,
12 | Measure,
13 | Reset,
14 | Wait,
15 | )
16 | from opensquirrel.ir.semantics import ControlledGateSemantic
17 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
18 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
19 | from opensquirrel.register_manager import BitRegister, QubitRegister, RegisterManager
20 |
21 | if TYPE_CHECKING:
22 | from opensquirrel.circuit import Circuit
23 |
24 |
25 | class _QubitReindexer(IRVisitor):
26 | """
27 | Reindex a whole IR.
28 | A new IR where the qubit indices are replaced by their positions in qubit indices.
29 | _E.g._, for mapping = [3, 1]:
30 | - Qubit(index=1) becomes Qubit(index=1), and
31 | - Qubit(index=3) becomes Qubit(index=0).
32 |
33 | Args:
34 | qubit_indices: a list of qubit indices, e.g. [3, 1]
35 |
36 | Returns:
37 |
38 | """
39 |
40 | def __init__(self, qubit_indices: list[int]) -> None:
41 | self.qubit_indices = qubit_indices
42 |
43 | def visit_init(self, init: Init) -> Init:
44 | return Init(qubit=self.qubit_indices.index(init.qubit.index))
45 |
46 | def visit_measure(self, measure: Measure) -> Measure:
47 | return Measure(qubit=self.qubit_indices.index(measure.qubit.index), bit=measure.bit, axis=measure.axis)
48 |
49 | def visit_reset(self, reset: Reset) -> Reset:
50 | qubit_to_reset = self.qubit_indices.index(reset.qubit.index)
51 | return Reset(qubit=qubit_to_reset)
52 |
53 | def visit_barrier(self, barrier: Barrier) -> Barrier:
54 | return Barrier(qubit=self.qubit_indices.index(barrier.qubit.index))
55 |
56 | def visit_wait(self, wait: Wait) -> Wait:
57 | return Wait(qubit=self.qubit_indices.index(wait.qubit.index), time=wait.time)
58 |
59 | def visit_single_qubit_gate(self, gate: SingleQubitGate) -> SingleQubitGate:
60 | gate.qubit.accept(self)
61 | return SingleQubitGate(qubit=self.qubit_indices.index(gate.qubit.index), gate_semantic=gate.bsr)
62 |
63 | def visit_two_qubit_gate(self, gate: TwoQubitGate) -> TwoQubitGate:
64 | qubit0 = self.qubit_indices.index(gate.qubit0.index)
65 | qubit1 = self.qubit_indices.index(gate.qubit1.index)
66 |
67 | if gate.controlled:
68 | target_gate = gate.controlled.target_gate.accept(self)
69 | return TwoQubitGate(qubit0=qubit0, qubit1=qubit1, gate_semantic=ControlledGateSemantic(target_gate))
70 |
71 | return TwoQubitGate(qubit0=qubit0, qubit1=qubit1, gate_semantic=gate.gate_semantic)
72 |
73 |
74 | def get_reindexed_circuit(
75 | replacement_gates: Iterable[Gate],
76 | qubit_indices: list[int],
77 | bit_register_size: int = 0,
78 | ) -> Circuit:
79 | from opensquirrel.circuit import Circuit
80 |
81 | qubit_reindexer = _QubitReindexer(qubit_indices)
82 | qubit_register = QubitRegister(len(qubit_indices))
83 | bit_register = BitRegister(bit_register_size)
84 | register_manager = RegisterManager(qubit_register, bit_register)
85 | replacement_ir = IR()
86 | for gate in replacement_gates:
87 | gate_with_reindexed_qubits = gate.accept(qubit_reindexer)
88 | replacement_ir.add_gate(gate_with_reindexed_qubits)
89 | return Circuit(register_manager, replacement_ir)
90 |
--------------------------------------------------------------------------------
/opensquirrel/ir/non_unitary.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Any
3 |
4 | import numpy as np
5 |
6 | from opensquirrel.common import ATOL
7 | from opensquirrel.ir.expression import Axis, AxisLike, Bit, BitLike, Expression, Qubit, QubitLike
8 | from opensquirrel.ir.ir import IRVisitor
9 | from opensquirrel.ir.statement import Instruction
10 |
11 |
12 | class NonUnitary(Instruction, ABC):
13 | def __init__(self, qubit: QubitLike, name: str) -> None:
14 | Instruction.__init__(self, name)
15 | self.qubit = Qubit(qubit)
16 |
17 | @property
18 | @abstractmethod
19 | def arguments(self) -> tuple[Expression, ...]:
20 | pass
21 |
22 | def get_qubit_operands(self) -> list[Qubit]:
23 | return [self.qubit]
24 |
25 | def accept(self, visitor: IRVisitor) -> Any:
26 | return visitor.visit_non_unitary(self)
27 |
28 |
29 | class Measure(NonUnitary):
30 | def __init__(self, qubit: QubitLike, bit: BitLike, axis: AxisLike = (0, 0, 1)) -> None:
31 | NonUnitary.__init__(self, qubit=qubit, name="measure")
32 | self.qubit = Qubit(qubit)
33 | self.bit = Bit(bit)
34 | self.axis = Axis(axis)
35 |
36 | def __repr__(self) -> str:
37 | return f"{self.__class__.__name__}(qubit={self.qubit}, bit={self.bit}, axis={self.axis})"
38 |
39 | def __eq__(self, other: object) -> bool:
40 | return (
41 | isinstance(other, Measure) and self.qubit == other.qubit and np.allclose(self.axis, other.axis, atol=ATOL)
42 | )
43 |
44 | @property
45 | def arguments(self) -> tuple[Expression, ...]:
46 | return self.qubit, self.bit, self.axis
47 |
48 | def accept(self, visitor: IRVisitor) -> Any:
49 | non_unitary_visit = super().accept(visitor)
50 | return non_unitary_visit if non_unitary_visit is not None else visitor.visit_measure(self)
51 |
52 | def get_bit_operands(self) -> list[Bit]:
53 | return [self.bit]
54 |
55 |
56 | class Init(NonUnitary):
57 | def __init__(self, qubit: QubitLike) -> None:
58 | NonUnitary.__init__(self, qubit=qubit, name="init")
59 | self.qubit = Qubit(qubit)
60 |
61 | def __repr__(self) -> str:
62 | return f"{self.__class__.__name__}(qubit={self.qubit})"
63 |
64 | def __eq__(self, other: object) -> bool:
65 | return isinstance(other, Init) and self.qubit == other.qubit
66 |
67 | @property
68 | def arguments(self) -> tuple[Expression, ...]:
69 | return (self.qubit,)
70 |
71 | def accept(self, visitor: IRVisitor) -> Any:
72 | non_unitary_visit = super().accept(visitor)
73 | return non_unitary_visit if non_unitary_visit is not None else visitor.visit_init(self)
74 |
75 |
76 | class Reset(NonUnitary):
77 | def __init__(self, qubit: QubitLike) -> None:
78 | NonUnitary.__init__(self, qubit=qubit, name="reset")
79 | self.qubit = Qubit(qubit)
80 |
81 | def __repr__(self) -> str:
82 | return f"{self.__class__.__name__}(qubit={self.qubit})"
83 |
84 | def __eq__(self, other: object) -> bool:
85 | return isinstance(other, Reset) and self.qubit == other.qubit
86 |
87 | @property
88 | def arguments(self) -> tuple[Expression, ...]:
89 | return (self.qubit,)
90 |
91 | def accept(self, visitor: IRVisitor) -> Any:
92 | non_unitary_visit = super().accept(visitor)
93 | return non_unitary_visit if non_unitary_visit is not None else visitor.visit_reset(self)
94 |
--------------------------------------------------------------------------------
/opensquirrel/passes/decomposer/general_decomposer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC, abstractmethod
4 | from collections.abc import Callable, Iterable
5 | from typing import Any
6 |
7 | from opensquirrel.circuit_matrix_calculator import get_circuit_matrix
8 | from opensquirrel.common import are_matrices_equivalent_up_to_global_phase, is_identity_matrix_up_to_a_global_phase
9 | from opensquirrel.default_instructions import is_anonymous_gate
10 | from opensquirrel.ir import IR, Gate
11 | from opensquirrel.reindexer import get_reindexed_circuit
12 |
13 |
14 | class Decomposer(ABC):
15 | def __init__(self, **kwargs: Any) -> None: ...
16 |
17 | @abstractmethod
18 | def decompose(self, gate: Gate) -> list[Gate]:
19 | raise NotImplementedError()
20 |
21 |
22 | def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> None:
23 | gate_qubit_indices = [q.index for q in gate.get_qubit_operands()]
24 | replacement_gates_qubit_indices = set()
25 | replaced_matrix = get_circuit_matrix(get_reindexed_circuit([gate], gate_qubit_indices))
26 |
27 | if is_identity_matrix_up_to_a_global_phase(replaced_matrix):
28 | return
29 |
30 | for g in replacement_gates:
31 | replacement_gates_qubit_indices.update([q.index for q in g.get_qubit_operands()])
32 |
33 | if set(gate_qubit_indices) != replacement_gates_qubit_indices:
34 | msg = f"replacement for gate {gate.name} does not seem to operate on the right qubits"
35 | raise ValueError(msg)
36 |
37 | replacement_matrix = get_circuit_matrix(get_reindexed_circuit(replacement_gates, gate_qubit_indices))
38 |
39 | if not are_matrices_equivalent_up_to_global_phase(replaced_matrix, replacement_matrix):
40 | msg = f"replacement for gate {gate.name} does not preserve the quantum state"
41 | raise ValueError(msg)
42 |
43 |
44 | def decompose(ir: IR, decomposer: Decomposer) -> None:
45 | """Applies `decomposer` to every gate in the circuit, replacing each gate by the output of `decomposer`.
46 | When `decomposer` decides to not decomposer a gate, it needs to return a list with the intact gate as single
47 | element.
48 | """
49 | statement_index = 0
50 | while statement_index < len(ir.statements):
51 | statement = ir.statements[statement_index]
52 |
53 | if not isinstance(statement, Gate):
54 | statement_index += 1
55 | continue
56 |
57 | gate = statement
58 | replacement_gates: list[Gate] = decomposer.decompose(statement)
59 | check_gate_replacement(gate, replacement_gates)
60 |
61 | ir.statements[statement_index : statement_index + 1] = replacement_gates
62 | statement_index += len(replacement_gates)
63 |
64 |
65 | class _GenericReplacer(Decomposer):
66 | def __init__(self, gate_type: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
67 | self.gate_type = gate_type
68 | self.replacement_gates_function = replacement_gates_function
69 |
70 | def decompose(self, gate: Gate) -> list[Gate]:
71 | if is_anonymous_gate(gate.name) or type(gate) is not self.gate_type:
72 | return [gate]
73 | return self.replacement_gates_function(*gate.arguments)
74 |
75 |
76 | def replace(ir: IR, gate: type[Gate], replacement_gates_function: Callable[..., list[Gate]]) -> None:
77 | """Does the same as decomposer, but only applies to a given gate."""
78 | generic_replacer = _GenericReplacer(gate, replacement_gates_function)
79 |
80 | decompose(ir, generic_replacer)
81 |
--------------------------------------------------------------------------------
/opensquirrel/ir/semantics/canonical_gate.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import numpy as np
4 | from numpy.typing import NDArray
5 |
6 | from opensquirrel.ir import AxisLike, IRVisitor
7 | from opensquirrel.ir.expression import BaseAxis
8 | from opensquirrel.ir.semantics.gate_semantic import GateSemantic
9 |
10 |
11 | class CanonicalAxis(BaseAxis):
12 | @staticmethod
13 | def parse(axis: AxisLike) -> NDArray[np.float64]:
14 | """Parse and validate an ``AxisLike``.
15 |
16 | Check if the `axis` can be cast to a 1DArray of length 3, raise an error otherwise.
17 | After casting to an array, the elements of the canonical axis are restricted to the Weyl chamber.
18 |
19 | Args:
20 | axis: ``AxisLike`` to validate and parse.
21 |
22 | Returns:
23 | Parsed axis represented as a 1DArray of length 3.
24 | """
25 | if isinstance(axis, CanonicalAxis):
26 | return axis.value
27 |
28 | try:
29 | axis = np.asarray(axis, dtype=float)
30 | except (ValueError, TypeError) as e:
31 | msg = "axis requires an ArrayLike"
32 | raise TypeError(msg) from e
33 | axis = axis.flatten()
34 | if len(axis) != 3:
35 | msg = f"axis requires an ArrayLike of length 3, but received an ArrayLike of length {len(axis)}"
36 | raise ValueError(msg)
37 |
38 | return CanonicalAxis.restrict_to_weyl_chamber(axis)
39 |
40 | @staticmethod
41 | def restrict_to_weyl_chamber(axis: NDArray[np.float64]) -> NDArray[np.float64]:
42 | """Restrict the given axis to the Weyl chamber. The six rules that are
43 | (implicitly) used are:
44 | 1. The canonical parameters are periodic with a period of 2 (neglecting
45 | a global phase).
46 | 2. Can(tx, ty, tz) ~ Can(tx - 1, ty, tz) (for any parameter)
47 | 3. Can(tx, ty, tz) ~ Can(tx, -ty, -tz) (for any pair of parameters)
48 | 4. Can(tx, ty, tz) ~ Can(ty, tx, tz) (for any pair of parameters)
49 | 5. Can(tx, ty, 0) ~ Can(1 - tx, ty, 0)
50 | 6. Can(tx, ty, tz) x Can(tx', ty', tz') = Can(tx + tx', ty + ty', tz + tz')
51 | (here x represents matrix multiplication)
52 |
53 | Based on the rules described in Chapter 5 of https://threeplusone.com/pubs/on_gates.pdf
54 | """
55 | axis = (axis + 1) % 2 - 1
56 |
57 | while (axis < 0).any():
58 | axis = np.where(axis < 0, axis - 1, axis)
59 | axis = (axis + 1) % 2 - 1
60 |
61 | axis = np.sort(axis)[::-1]
62 | match sum(t > 1 / 2 for t in axis):
63 | case 1:
64 | axis[0] = 1 - axis[0]
65 | case 2:
66 | axis[0], axis[2] = axis[2], axis[0]
67 | axis[1:] = 1 - axis[1:]
68 | case 3:
69 | axis = 1 - axis
70 |
71 | return np.sort(axis)[::-1]
72 |
73 | def accept(self, visitor: IRVisitor) -> Any:
74 | return visitor.visit_canonical_axis(self)
75 |
76 |
77 | class CanonicalGateSemantic(GateSemantic):
78 | def __init__(self, axis: AxisLike) -> None:
79 | self.axis = CanonicalAxis(axis)
80 |
81 | def __repr__(self) -> str:
82 | return f"CanonicalGateSemantic(axis={self.axis})"
83 |
84 | def is_identity(self) -> bool:
85 | return self.axis == CanonicalAxis((0, 0, 0))
86 |
87 | def accept(self, visitor: IRVisitor) -> Any:
88 | return visitor.visit_canonical_gate_semantic(self)
89 |
--------------------------------------------------------------------------------
/tests/test_asm_declaration.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from opensquirrel import Circuit, CircuitBuilder
4 | from opensquirrel.passes.merger import SingleQubitGatesMerger
5 | from opensquirrel.reader import LibQasmParser
6 |
7 |
8 | def test_empty_raw_text_string() -> None:
9 | circuit = Circuit.from_string(
10 | """
11 | version 3.0
12 |
13 | qubit q
14 |
15 | asm(TestBackend) '''
16 | '''
17 | """,
18 | )
19 | assert (
20 | str(circuit)
21 | == """version 3.0
22 |
23 | qubit[1] q
24 |
25 | asm(TestBackend) '''
26 | '''
27 | """
28 | )
29 |
30 |
31 | def test_single_line() -> None:
32 | circuit = Circuit.from_string(
33 | """
34 | version 3
35 |
36 | qubit[2] q
37 |
38 | H q[0]
39 |
40 | asm(TestBackend) ''' a ' " {} () [] b '''
41 |
42 | CNOT q[0], q[1]
43 | """,
44 | )
45 | assert (
46 | str(circuit)
47 | == """version 3.0
48 |
49 | qubit[2] q
50 |
51 | H q[0]
52 | asm(TestBackend) ''' a ' " {} () [] b '''
53 | CNOT q[0], q[1]
54 | """
55 | )
56 |
57 |
58 | def test_multi_line() -> None:
59 | circuit = Circuit.from_string(
60 | """
61 | version 3
62 |
63 | qubit[2] q
64 |
65 | H q[0]
66 |
67 | asm(TestBackend) '''
68 | a ' " {} () [] b
69 | // This is a single line comment which ends on the newline.
70 | /* This is a multi-
71 | line comment block */
72 | '''
73 |
74 | CNOT q[0], q[1]
75 | """,
76 | )
77 | assert (
78 | str(circuit)
79 | == """version 3.0
80 |
81 | qubit[2] q
82 |
83 | H q[0]
84 | asm(TestBackend) '''
85 | a ' " {} () [] b
86 | // This is a single line comment which ends on the newline.
87 | /* This is a multi-
88 | line comment block */
89 | '''
90 | CNOT q[0], q[1]
91 | """
92 | )
93 |
94 |
95 | def test_invalid_backend_name() -> None:
96 | with pytest.raises(
97 | OSError,
98 | match=r"Error at :5:13..16: mismatched input '100' expecting IDENTIFIER",
99 | ):
100 | LibQasmParser().circuit_from_string(
101 | """version 3.0
102 |
103 | qubit q
104 |
105 | asm(100) '''
106 | '''
107 | """
108 | )
109 |
110 |
111 | def test_invalid_raw_text_string() -> None:
112 | with pytest.raises(
113 | OSError,
114 | match=r"Error at :5:26..29: mismatched input ''''' expecting RAW_TEXT_STRING",
115 | ):
116 | LibQasmParser().circuit_from_string(
117 | """version 3
118 |
119 | qubit q
120 |
121 | asm(TestBackend) ''' a ' " {} () [] b
122 | """
123 | )
124 |
125 |
126 | def test_asm_circuit_builder() -> None:
127 | builder = CircuitBuilder(2)
128 | builder.H(0)
129 | builder.asm("TestBackend", """ a ' " {} () [] b """)
130 | builder.CNOT(0, 1)
131 | circuit = builder.to_circuit()
132 | assert (
133 | str(circuit)
134 | == """version 3.0
135 |
136 | qubit[2] q
137 |
138 | H q[0]
139 | asm(TestBackend) ''' a ' " {} () [] b '''
140 | CNOT q[0], q[1]
141 | """
142 | )
143 |
144 |
145 | def test_no_merging_across_asm() -> None:
146 | builder = CircuitBuilder(2)
147 | builder.H(0)
148 | builder.Y90(1)
149 | builder.asm("TestBackend", """ a ' " {} () [] b """)
150 | builder.H(0)
151 | builder.X90(1)
152 | circuit = builder.to_circuit()
153 | circuit.merge(merger=SingleQubitGatesMerger())
154 | assert (
155 | str(circuit)
156 | == """version 3.0
157 |
158 | qubit[2] q
159 |
160 | H q[0]
161 | Y90 q[1]
162 | asm(TestBackend) ''' a ' " {} () [] b '''
163 | H q[0]
164 | X90 q[1]
165 | """
166 | )
167 |
--------------------------------------------------------------------------------
/tests/instructions/test_measure.py:
--------------------------------------------------------------------------------
1 | from opensquirrel import Circuit
2 | from opensquirrel.passes.decomposer import McKayDecomposer
3 | from opensquirrel.passes.merger.single_qubit_gates_merger import SingleQubitGatesMerger
4 |
5 |
6 | def test_measure() -> None:
7 | circuit = Circuit.from_string(
8 | """
9 | version 3.0
10 |
11 | qubit[2] q
12 | bit[2] b
13 |
14 | b[0, 1] = measure q[0, 1]
15 | """,
16 | )
17 | assert (
18 | str(circuit)
19 | == """version 3.0
20 |
21 | qubit[2] q
22 | bit[2] b
23 |
24 | b[0] = measure q[0]
25 | b[1] = measure q[1]
26 | """
27 | )
28 |
29 |
30 | def test_consecutive_measures() -> None:
31 | circuit = Circuit.from_string(
32 | """
33 | version 3.0
34 |
35 | qubit[3] q
36 | bit[3] b
37 |
38 | H q[0]
39 | H q[1]
40 | H q[2]
41 | b[0] = measure q[0]
42 | b[2] = measure q[2]
43 | b[1] = measure q[1]
44 | """,
45 | )
46 | assert (
47 | str(circuit)
48 | == """version 3.0
49 |
50 | qubit[3] q
51 | bit[3] b
52 |
53 | H q[0]
54 | H q[1]
55 | H q[2]
56 | b[0] = measure q[0]
57 | b[2] = measure q[2]
58 | b[1] = measure q[1]
59 | """
60 | )
61 |
62 |
63 | def test_measures_unrolling() -> None:
64 | circuit = Circuit.from_string(
65 | """
66 | version 3.0
67 |
68 | qubit[6] q
69 | bit[6] b
70 |
71 | H q[0]
72 | CNOT q[0], q[1]
73 | b[1, 4] = measure q[1, 4]
74 | b[2:5] = measure q[0:3]
75 | b = measure q
76 | """,
77 | )
78 | assert (
79 | str(circuit)
80 | == """version 3.0
81 |
82 | qubit[6] q
83 | bit[6] b
84 |
85 | H q[0]
86 | CNOT q[0], q[1]
87 | b[1] = measure q[1]
88 | b[4] = measure q[4]
89 | b[2] = measure q[0]
90 | b[3] = measure q[1]
91 | b[4] = measure q[2]
92 | b[5] = measure q[3]
93 | b[0] = measure q[0]
94 | b[1] = measure q[1]
95 | b[2] = measure q[2]
96 | b[3] = measure q[3]
97 | b[4] = measure q[4]
98 | b[5] = measure q[5]
99 | """
100 | )
101 |
102 |
103 | def test_measure_order() -> None:
104 | circuit = Circuit.from_string(
105 | """
106 | version 3.0
107 |
108 | qubit[2] q
109 | bit[2] b
110 |
111 | Rz(-pi/3) q[0]
112 | Rz(pi/2) q[1]
113 | b[1, 0] = measure q[1, 0]
114 | """,
115 | )
116 | circuit.merge(merger=SingleQubitGatesMerger())
117 | circuit.decompose(decomposer=McKayDecomposer())
118 | assert (
119 | str(circuit)
120 | == """version 3.0
121 |
122 | qubit[2] q
123 | bit[2] b
124 |
125 | Rz(1.5707963) q[1]
126 | b[1] = measure q[1]
127 | Rz(-1.0471976) q[0]
128 | b[0] = measure q[0]
129 | """
130 | )
131 |
132 |
133 | def test_multiple_qubit_bit_definitions_and_mid_circuit_measure_instructions() -> None:
134 | circuit = Circuit.from_string(
135 | """
136 | version 3.0
137 |
138 | qubit q0
139 | bit b0
140 | X q0
141 | b0 = measure q0
142 |
143 | qubit q1
144 | bit b1
145 | H q1
146 | CNOT q1, q0
147 | b1 = measure q1
148 | b0 = measure q0
149 | """,
150 | )
151 | circuit.merge(merger=SingleQubitGatesMerger())
152 | circuit.decompose(decomposer=McKayDecomposer())
153 | assert (
154 | str(circuit)
155 | == """version 3.0
156 |
157 | qubit[2] q
158 | bit[2] b
159 |
160 | X90 q[0]
161 | X90 q[0]
162 | b[0] = measure q[0]
163 | Rz(1.5707963) q[1]
164 | X90 q[1]
165 | Rz(1.5707963) q[1]
166 | CNOT q[1], q[0]
167 | b[1] = measure q[1]
168 | b[0] = measure q[0]
169 | """
170 | )
171 |
--------------------------------------------------------------------------------
/docs/compilation-passes/routing/shortest-path-router.md:
--------------------------------------------------------------------------------
1 | The shortest-path routing pass (`ShortestPathRouter`) ensures that qubit interactions in a circuit can be executed
2 | given the target backend connectivity.
3 | It inserts the necessary SWAP gates along the shortest path,
4 | moving the qubits closer together so the intended operation can be performed.
5 | This approach aims to minimize the number of SWAPs required for each interaction
6 | by using the `shortest_path` method from the `networkx` package.
7 | While it uses a straightforward algorithm, it may result in an overly increased circuit depth.
8 |
9 | The following examples showcase the usage of the shortest-path routing pass.
10 | Note that the backend connectivity is required as an input argument.
11 |
12 | _Check the [circuit builder](../../circuit-builder/index.md) on how to generate the circuit._
13 |
14 | ```python
15 | from opensquirrel import CircuitBuilder
16 | from opensquirrel.passes.router import ShortestPathRouter
17 | ```
18 |
19 | ```python
20 | connectivity = {"0": [1], "1": [0, 2], "2": [1, 3], "3": [2, 4], "4": [3]}
21 |
22 | builder = CircuitBuilder(5)
23 | builder.CNOT(0, 1)
24 | builder.CNOT(1, 2)
25 | builder.CNOT(2, 3)
26 | builder.CNOT(3, 4)
27 | builder.CNOT(0, 4)
28 | circuit = builder.to_circuit()
29 |
30 | shortest_path_router = ShortestPathRouter(connectivity=connectivity)
31 | circuit.route(router=shortest_path_router)
32 | ```
33 |
34 | ??? example "`print(circuit)`"
35 |
36 | ```
37 | version 3.0
38 |
39 | qubit[5] q
40 |
41 | CNOT q[0], q[1]
42 | CNOT q[1], q[2]
43 | CNOT q[2], q[3]
44 | CNOT q[3], q[4]
45 | SWAP q[0], q[1]
46 | SWAP q[1], q[2]
47 | SWAP q[2], q[3]
48 | CNOT q[3], q[4]
49 | ```
50 |
51 | ```python
52 | connectivity = {
53 | "0": [1, 2],
54 | "1": [0, 3],
55 | "2": [0, 4],
56 | "3": [1, 5],
57 | "4": [2, 5],
58 | "5": [3, 4, 6],
59 | "6": [5]
60 | }
61 |
62 | builder = CircuitBuilder(7)
63 | builder.CNOT(0, 6)
64 | builder.CNOT(1, 5)
65 | builder.CNOT(2, 4)
66 | builder.CNOT(3, 6)
67 | builder.CNOT(0, 2)
68 | builder.CNOT(1, 3)
69 | builder.CNOT(4, 5)
70 | builder.CNOT(5, 6)
71 | circuit = builder.to_circuit()
72 |
73 | shortest_path_router = ShortestPathRouter(connectivity=connectivity)
74 | circuit.route(router=shortest_path_router)
75 | ```
76 |
77 | ??? example "`print(circuit)`"
78 |
79 | ```
80 | version 3.0
81 |
82 | qubit[7] q
83 |
84 | SWAP q[0], q[1]
85 | SWAP q[1], q[3]
86 | SWAP q[3], q[5]
87 | CNOT q[5], q[6]
88 | SWAP q[0], q[1]
89 | CNOT q[1], q[5]
90 | CNOT q[2], q[4]
91 | SWAP q[0], q[1]
92 | SWAP q[1], q[3]
93 | SWAP q[3], q[5]
94 | CNOT q[5], q[6]
95 | SWAP q[3], q[1]
96 | SWAP q[1], q[0]
97 | CNOT q[0], q[2]
98 | SWAP q[1], q[3]
99 | CNOT q[3], q[3]
100 | SWAP q[4], q[2]
101 | SWAP q[2], q[0]
102 | CNOT q[0], q[5]
103 | SWAP q[1], q[3]
104 | SWAP q[3], q[5]
105 | CNOT q[5], q[6]
106 | ```
107 |
108 | If, based on the connectivity, a certain interaction is not possible, the shortest-path router will throw an error;
109 | as shown in the following example where qubits 0 and 1 are disconnected from qubits 2 and 3.
110 |
111 | ```python
112 | connectivity = {"0": [1], "1": [0], "2": [3], "3": [2]}
113 |
114 | builder = CircuitBuilder(4)
115 | builder.CNOT(0, 2)
116 | builder.CNOT(3, 1)
117 | circuit = builder.to_circuit()
118 |
119 | shortest_path_router = ShortestPathRouter(connectivity=connectivity)
120 | circuit.route(router=shortest_path_router)
121 | ```
122 |
123 | !!! example ""
124 |
125 | `NoRoutingPathError: No routing path available between qubit 0 and qubit 2`
126 |
--------------------------------------------------------------------------------
/tests/utils/test_matrix_expander.py:
--------------------------------------------------------------------------------
1 | from math import pi, sqrt
2 | from typing import Any
3 |
4 | import numpy as np
5 | import pytest
6 | from numpy.typing import NDArray
7 |
8 | from opensquirrel.ir import AxisLike
9 | from opensquirrel.ir.semantics import (
10 | BlochSphereRotation,
11 | CanonicalGateSemantic,
12 | ControlledGateSemantic,
13 | MatrixGateSemantic,
14 | )
15 | from opensquirrel.ir.single_qubit_gate import SingleQubitGate
16 | from opensquirrel.ir.two_qubit_gate import TwoQubitGate
17 | from opensquirrel.utils import get_matrix
18 |
19 |
20 | def test_bloch_sphere_rotation() -> None:
21 | gate = SingleQubitGate(0, BlochSphereRotation(axis=(0.8, -0.3, 1.5), angle=0.9468, phase=2.533))
22 | np.testing.assert_almost_equal(
23 | get_matrix(gate, 2),
24 | [
25 | [-0.50373461 + 0.83386635j, 0.05578802 + 0.21864595j, 0, 0],
26 | [0.18579927 + 0.12805072j, -0.95671077 + 0.18381011j, 0, 0],
27 | [0, 0, -0.50373461 + 0.83386635j, 0.05578802 + 0.21864595j],
28 | [0, 0, 0.18579927 + 0.12805072j, -0.95671077 + 0.18381011j],
29 | ],
30 | )
31 |
32 |
33 | def test_controlled_gate() -> None:
34 | gate = TwoQubitGate(
35 | 2,
36 | 0,
37 | gate_semantic=ControlledGateSemantic(
38 | target_gate=SingleQubitGate(0, BlochSphereRotation(axis=(1, 0, 0), angle=pi, phase=pi / 2))
39 | ),
40 | )
41 | np.testing.assert_almost_equal(
42 | get_matrix(gate, 3),
43 | [
44 | [1, 0, 0, 0, 0, 0, 0, 0],
45 | [0, 1, 0, 0, 0, 0, 0, 0],
46 | [0, 0, 1, 0, 0, 0, 0, 0],
47 | [0, 0, 0, 1, 0, 0, 0, 0],
48 | [0, 0, 0, 0, 0, 1, 0, 0],
49 | [0, 0, 0, 0, 1, 0, 0, 0],
50 | [0, 0, 0, 0, 0, 0, 0, 1],
51 | [0, 0, 0, 0, 0, 0, 1, 0],
52 | ],
53 | )
54 |
55 |
56 | def test_matrix_gate() -> None:
57 | gate = TwoQubitGate(
58 | 1,
59 | 2,
60 | gate_semantic=MatrixGateSemantic(
61 | [
62 | [1, 0, 0, 0],
63 | [0, 0, 1, 0],
64 | [0, 1, 0, 0],
65 | [0, 0, 0, 1],
66 | ],
67 | ),
68 | )
69 | np.testing.assert_almost_equal(
70 | get_matrix(gate, 3),
71 | [
72 | [1, 0, 0, 0, 0, 0, 0, 0],
73 | [0, 1, 0, 0, 0, 0, 0, 0],
74 | [0, 0, 0, 0, 1, 0, 0, 0],
75 | [0, 0, 0, 0, 0, 1, 0, 0],
76 | [0, 0, 1, 0, 0, 0, 0, 0],
77 | [0, 0, 0, 1, 0, 0, 0, 0],
78 | [0, 0, 0, 0, 0, 0, 1, 0],
79 | [0, 0, 0, 0, 0, 0, 0, 1],
80 | ],
81 | )
82 |
83 |
84 | @pytest.mark.parametrize(
85 | ("axis", "expected_matrix"),
86 | [
87 | ((0, 0, 0), np.eye(4)),
88 | (
89 | (1 / 2, 0, 0),
90 | np.array(
91 | [
92 | [1 / sqrt(2), 0, 0, -1j / sqrt(2)],
93 | [0, 1 / sqrt(2), -1j / sqrt(2), 0],
94 | [0, -1j / sqrt(2), 1 / sqrt(2), 0],
95 | [-1j / sqrt(2), 0, 0, 1 / sqrt(2)],
96 | ]
97 | ),
98 | ),
99 | ((1 / 2, 1 / 2, 0), np.array([[1, 0, 0, 0], [0, 0, -1j, 0], [0, -1j, 0, 0], [0, 0, 0, 1]])),
100 | (
101 | (1 / 2, 1 / 2, 1 / 2),
102 | np.exp(-1j * np.pi / 4) * np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]),
103 | ),
104 | ],
105 | )
106 | def test_canonical_gate(axis: AxisLike, expected_matrix: NDArray[Any]) -> None:
107 | gate = TwoQubitGate(0, 1, gate_semantic=CanonicalGateSemantic(axis))
108 |
109 | np.testing.assert_almost_equal(get_matrix(gate, 2), expected_matrix)
110 |
--------------------------------------------------------------------------------
/tests/integration/test_starmon_7.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | import pytest
4 |
5 | from opensquirrel import Circuit
6 | from opensquirrel.passes.exporter import CqasmV1Exporter
7 | from opensquirrel.passes.validator import InteractionValidator, PrimitiveGateValidator
8 | from tests import STATIC_DATA
9 | from tests.integration import DataType
10 |
11 | BACKEND_ID = "starmon-7"
12 |
13 |
14 | class TestStarmon7:
15 | @pytest.fixture
16 | def data(self) -> DataType:
17 | """
18 | Starmon-7 chip topology:
19 | 1 = 4 = 6
20 | \\ //
21 | 3
22 | // \\
23 | 0 = 2 = 5
24 | """
25 | return cast("DataType", STATIC_DATA["backends"][BACKEND_ID])
26 |
27 | def test_complete_circuit(self, data: DataType) -> None:
28 | circuit = Circuit.from_string(
29 | """
30 | // Version statement
31 | version 3.0
32 |
33 | // This is a single line comment which ends on the newline.
34 | // The cQASM string must begin with the version instruction (apart from any preceding comments).
35 |
36 | /* This is a multi-
37 | line comment block */
38 |
39 | // (Qu)bit declaration
40 | qubit[7] q // Starmon-7 has a 7-qubit register
41 | bit[14] b
42 |
43 | // Initialization
44 | init q
45 |
46 | // Single-qubit gates
47 | I q[0]
48 | H q[1]
49 | X q[2]
50 | X90 q[3]
51 | mX90 q[4]
52 | Y q[5]
53 | Y90 q[6]
54 | mY90 q[0]
55 | Z q[1]
56 | S q[2]
57 | Sdag q[3]
58 | T q[4]
59 | Tdag q[5]
60 | Rx(pi/2) q[6]
61 | Ry(pi/2) q[0]
62 | Rz(tau) q[1]
63 |
64 | barrier q // to ensure all measurements occur simultaneously
65 | // Mid-circuit measurement
66 | b[0:6] = measure q
67 |
68 | // Two-qubit gates
69 | CNOT q[0], q[2]
70 | CZ q[1], q[4]
71 | CR(pi) q[5], q[3]
72 | CRk(2) q[3], q[6]
73 | SWAP q[5], q[2]
74 |
75 | // Control instructions
76 | barrier q
77 | wait(3) q
78 |
79 | // Final measurement
80 | b[7:13] = measure q
81 | """,
82 | )
83 | circuit.validate(validator=InteractionValidator(**data))
84 | circuit.validate(validator=PrimitiveGateValidator(**data))
85 | exported_circuit = circuit.export(exporter=CqasmV1Exporter())
86 |
87 | assert (
88 | exported_circuit
89 | == """version 1.0
90 |
91 | qubits 7
92 |
93 | prep_z q[0]
94 | prep_z q[1]
95 | prep_z q[2]
96 | prep_z q[3]
97 | prep_z q[4]
98 | prep_z q[5]
99 | prep_z q[6]
100 | i q[0]
101 | h q[1]
102 | x q[2]
103 | x90 q[3]
104 | mx90 q[4]
105 | y q[5]
106 | y90 q[6]
107 | my90 q[0]
108 | z q[1]
109 | s q[2]
110 | sdag q[3]
111 | t q[4]
112 | tdag q[5]
113 | rx q[6], 1.5707963
114 | ry q[0], 1.5707963
115 | rz q[1], 0.0
116 | barrier q[0, 1, 2, 3, 4, 5, 6]
117 | measure_z q[0]
118 | measure_z q[1]
119 | measure_z q[2]
120 | measure_z q[3]
121 | measure_z q[4]
122 | measure_z q[5]
123 | measure_z q[6]
124 | cnot q[0], q[2]
125 | cz q[1], q[4]
126 | cr(3.1415927) q[5], q[3]
127 | crk(2) q[3], q[6]
128 | swap q[5], q[2]
129 | barrier q[0, 1, 2, 3, 4, 5, 6]
130 | wait q[0], 3
131 | wait q[1], 3
132 | wait q[2], 3
133 | wait q[3], 3
134 | wait q[4], 3
135 | wait q[5], 3
136 | wait q[6], 3
137 | measure_z q[0]
138 | measure_z q[1]
139 | measure_z q[2]
140 | measure_z q[3]
141 | measure_z q[4]
142 | measure_z q[5]
143 | measure_z q[6]
144 | """
145 | )
146 |
--------------------------------------------------------------------------------