├── 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 | ![image](../../_static/cnot2cz.png) 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 | ![image](../../_static/swap2cnot.png) 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 | ![image](../../_static/swap2cz.png) 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 | --------------------------------------------------------------------------------