parityos-cudaq 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ pip-wheel-metadata/
21
+ share/python-wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+ MANIFEST
26
+
27
+ # Installer logs
28
+ pip-log.txt
29
+ pip-delete-this-directory.txt
30
+
31
+ # Unit test / coverage reports
32
+ htmlcov/
33
+ .tox/
34
+ .nox/
35
+ .coverage
36
+ .coverage.*
37
+ .cache
38
+ coverage.xml
39
+ report.xml
40
+ *.cover
41
+ *.py,cover
42
+ .hypothesis/
43
+ .pytest_cache/
44
+
45
+ # Sphinx documentation
46
+ **/docs/_build/
47
+ **/docs/**/generated/
48
+
49
+ # Jupyter Notebook
50
+ .ipynb_checkpoints
51
+
52
+ # pyenv
53
+ .python-version
54
+
55
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
56
+ __pypackages__/
57
+
58
+ # Celery
59
+ celerybeat-schedule
60
+ celerybeat.pid
61
+
62
+ # Environments
63
+ *.env
64
+ .venv
65
+ env/
66
+ venv/
67
+ ENV/
68
+ env.bak/
69
+ venv.bak/
70
+ uv.lock
71
+
72
+ # mypy
73
+ .mypy_cache/
74
+ .dmypy.json
75
+ dmypy.json
76
+
77
+ # Pyre type checker
78
+ .pyre/
79
+
80
+ # IDE stuff
81
+ *.iws
82
+ *.iml
83
+ **/.idea/
84
+ **/.idea/**
85
+ **/.vscode
86
+
87
+ # Apple stuff
88
+ .DS_Store
89
+
90
+ # artifacts
91
+ .png
92
+ .tar
93
+
94
+ # uv
95
+ uv.toml
96
+
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.1.0] - 2026-06-10
10
+
11
+ ### Added
12
+
13
+ - Separate packages for each exporter (#132)
@@ -0,0 +1,37 @@
1
+ ParityOS Client Software License Terms (BSD-3-Clause)
2
+ =====================================================
3
+
4
+ Copyright 2023 Parity Quantum Computing GmbH (ParityQC)
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ ===============================================================================
32
+
33
+ Parity Quantum Computing GmbH
34
+ Rennweg 1 / Top 314 / 6020 Innsbruck, Austria
35
+ info@parityqc.com / www.parityqc.com
36
+
37
+ ===============================================================================
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: parityos-cudaq
3
+ Version: 0.1.0
4
+ Summary: CudaQ extension for ParityOS
5
+ Project-URL: Homepage, https://parityqc.com/
6
+ Author-email: ParityQC <parityos@parityqc.com>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE.txt
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: attrs
16
+ Requires-Dist: parityos>=3.0.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # ParityOS CudaQ
20
+
21
+ ParityOS extension adding an interface to [cudaq](https://developer.nvidia.com/cuda-q).
22
+
23
+ ## Installation
24
+
25
+ It is recommended to install this package in a separate Python virtual environment. For example:
26
+
27
+ ```shell
28
+ # To create a standard Python virtual environment:
29
+ python -m venv my_new_venv && source my_new_venv/bin/activate
30
+ # Alternatively, to create a Anaconda/Miniconda environment:
31
+ conda create --name my_new_conda_env python=<version> && conda activate my_new_conda_env
32
+ # or a pyenv environment
33
+ pyenv virtualenv <version> my_new_venv && pyenv activate my_new_venv
34
+ # or a uv managed environment
35
+ uv venv -p <version>
36
+ ```
37
+ where `<version>` is a python version and one of `[3.11, 3.12, 3.13]`.
38
+
39
+ After activating the virtual environment, install via your favorite package manager, e.g.:
40
+
41
+ ```shell
42
+ # using pip
43
+ pip install parityos-cudaq
44
+ # using uv
45
+ uv pip install parityos-cudaq
46
+ ```
47
+
48
+ ## Exporter
49
+
50
+ `parityos-cudaq` offers an exporter of ParityOS to quantum circuits to the
51
+ [Quake dialect](https://nvidia.github.io/cuda-quantum/latest/specification/quake-dialect.html) .
52
+
53
+ ```python
54
+ from parityos.bits import get_q
55
+ from parityos.operators.circuit import Circuit
56
+ from parityos.operators.controlled_operator import CNOT
57
+ from parityos.operators.elementary_operator import H, Z
58
+ from parityos.operators.rotation_operator import RX
59
+ from parityos_cudaq.exporter import export_parityos_to_quake
60
+
61
+ # Create circuit qubits.
62
+ qubits = [get_q(name) for name in ("a", "b", "c")]
63
+
64
+ # Create the following circuit as sequence of gates, followed by a measurement of all qubits.
65
+ # ┌───────┐┌───┐┌───┐ ┌─┐
66
+ # qa: ┤ Rx(2) ├┤ H ├┤ Z ├───┤M├
67
+ # └───────┘├───┤└┬─┬┘ └╥┘
68
+ # qb: ─────────┤ X ├─┤M├─────╫─
69
+ # ┌───┐ └─┬─┘ └╥┘ ┌─┐ ║
70
+ # qc: ──┤ H ├────■────╫──┤M├─╫─
71
+ # └───┘ ║ └╥┘ ║
72
+ # c: 3/═══════════════╩═══╩══╩═
73
+ # 1 2 0
74
+ circuit = Circuit(
75
+ [
76
+ RX(qubits[0], 2),
77
+ H(qubits[0]),
78
+ H(qubits[2]),
79
+ CNOT(qubits[2], qubits[1]),
80
+ Z(qubits[0]),
81
+ ]
82
+ ).measure_all()
83
+
84
+ # Export to quake.
85
+ qiskit_result = export_parityos_to_quake(circuit, False)
86
+
87
+ print(qiskit_result.module)
88
+ # // Generated by ParityOS
89
+ # module {
90
+ # func.func @parityos_circuit() {
91
+ # // Allocate register
92
+ # %n_qubits = arith.constant 3 : i64
93
+ # %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
94
+ # %q_0 = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
95
+ # %q_1 = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
96
+ # %q_2 = quake.extract_ref %register[2] : (!quake.veq<?>) -> !quake.ref
97
+ #
98
+ # %angle_0 = arith.constant 2.0 : f64
99
+ # quake.rx (%angle_0) %q_0 : (f64, !quake.ref) -> ()
100
+ # quake.h %q_0 : (!quake.ref) -> ()
101
+ # quake.h %q_2 : (!quake.ref) -> ()
102
+ # quake.x [%q_2] %q_1 : (!quake.ref, !quake.ref) -> ()
103
+ # quake.z %q_0 : (!quake.ref) -> ()
104
+ # %m_result_0 = quake.mz %q_0 : (!quake.ref) -> !quake.measure
105
+ #
106
+ # %m_result_1 = quake.mz %q_1 : (!quake.ref) -> !quake.measure
107
+ #
108
+ # %m_result_2 = quake.mz %q_2 : (!quake.ref) -> !quake.measure
109
+ #
110
+ # return
111
+ # }
112
+ # }
113
+
114
+ print(qiskit_result.qubit_map) # Maps ParityOS qubits to quake Qubit integer labels
115
+ # {Qubit(id='a'): 0, Qubit(id='b'): 1, Qubit(id='c'): 2}
116
+ ```
117
+
118
+ ## License
119
+
120
+ This software package is made available under the 3-Clause BSD License.
121
+ See `License.txt` for details.
@@ -0,0 +1,103 @@
1
+ # ParityOS CudaQ
2
+
3
+ ParityOS extension adding an interface to [cudaq](https://developer.nvidia.com/cuda-q).
4
+
5
+ ## Installation
6
+
7
+ It is recommended to install this package in a separate Python virtual environment. For example:
8
+
9
+ ```shell
10
+ # To create a standard Python virtual environment:
11
+ python -m venv my_new_venv && source my_new_venv/bin/activate
12
+ # Alternatively, to create a Anaconda/Miniconda environment:
13
+ conda create --name my_new_conda_env python=<version> && conda activate my_new_conda_env
14
+ # or a pyenv environment
15
+ pyenv virtualenv <version> my_new_venv && pyenv activate my_new_venv
16
+ # or a uv managed environment
17
+ uv venv -p <version>
18
+ ```
19
+ where `<version>` is a python version and one of `[3.11, 3.12, 3.13]`.
20
+
21
+ After activating the virtual environment, install via your favorite package manager, e.g.:
22
+
23
+ ```shell
24
+ # using pip
25
+ pip install parityos-cudaq
26
+ # using uv
27
+ uv pip install parityos-cudaq
28
+ ```
29
+
30
+ ## Exporter
31
+
32
+ `parityos-cudaq` offers an exporter of ParityOS to quantum circuits to the
33
+ [Quake dialect](https://nvidia.github.io/cuda-quantum/latest/specification/quake-dialect.html) .
34
+
35
+ ```python
36
+ from parityos.bits import get_q
37
+ from parityos.operators.circuit import Circuit
38
+ from parityos.operators.controlled_operator import CNOT
39
+ from parityos.operators.elementary_operator import H, Z
40
+ from parityos.operators.rotation_operator import RX
41
+ from parityos_cudaq.exporter import export_parityos_to_quake
42
+
43
+ # Create circuit qubits.
44
+ qubits = [get_q(name) for name in ("a", "b", "c")]
45
+
46
+ # Create the following circuit as sequence of gates, followed by a measurement of all qubits.
47
+ # ┌───────┐┌───┐┌───┐ ┌─┐
48
+ # qa: ┤ Rx(2) ├┤ H ├┤ Z ├───┤M├
49
+ # └───────┘├───┤└┬─┬┘ └╥┘
50
+ # qb: ─────────┤ X ├─┤M├─────╫─
51
+ # ┌───┐ └─┬─┘ └╥┘ ┌─┐ ║
52
+ # qc: ──┤ H ├────■────╫──┤M├─╫─
53
+ # └───┘ ║ └╥┘ ║
54
+ # c: 3/═══════════════╩═══╩══╩═
55
+ # 1 2 0
56
+ circuit = Circuit(
57
+ [
58
+ RX(qubits[0], 2),
59
+ H(qubits[0]),
60
+ H(qubits[2]),
61
+ CNOT(qubits[2], qubits[1]),
62
+ Z(qubits[0]),
63
+ ]
64
+ ).measure_all()
65
+
66
+ # Export to quake.
67
+ qiskit_result = export_parityos_to_quake(circuit, False)
68
+
69
+ print(qiskit_result.module)
70
+ # // Generated by ParityOS
71
+ # module {
72
+ # func.func @parityos_circuit() {
73
+ # // Allocate register
74
+ # %n_qubits = arith.constant 3 : i64
75
+ # %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
76
+ # %q_0 = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
77
+ # %q_1 = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
78
+ # %q_2 = quake.extract_ref %register[2] : (!quake.veq<?>) -> !quake.ref
79
+ #
80
+ # %angle_0 = arith.constant 2.0 : f64
81
+ # quake.rx (%angle_0) %q_0 : (f64, !quake.ref) -> ()
82
+ # quake.h %q_0 : (!quake.ref) -> ()
83
+ # quake.h %q_2 : (!quake.ref) -> ()
84
+ # quake.x [%q_2] %q_1 : (!quake.ref, !quake.ref) -> ()
85
+ # quake.z %q_0 : (!quake.ref) -> ()
86
+ # %m_result_0 = quake.mz %q_0 : (!quake.ref) -> !quake.measure
87
+ #
88
+ # %m_result_1 = quake.mz %q_1 : (!quake.ref) -> !quake.measure
89
+ #
90
+ # %m_result_2 = quake.mz %q_2 : (!quake.ref) -> !quake.measure
91
+ #
92
+ # return
93
+ # }
94
+ # }
95
+
96
+ print(qiskit_result.qubit_map) # Maps ParityOS qubits to quake Qubit integer labels
97
+ # {Qubit(id='a'): 0, Qubit(id='b'): 1, Qubit(id='c'): 2}
98
+ ```
99
+
100
+ ## License
101
+
102
+ This software package is made available under the 3-Clause BSD License.
103
+ See `License.txt` for details.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "parityos-cudaq"
7
+ dynamic = ["version"]
8
+ description = "CudaQ extension for ParityOS"
9
+ readme = "README.md"
10
+ license = "BSD-3-Clause"
11
+ license-files = ["LICENSE.txt"]
12
+ requires-python = ">=3.11"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Typing :: Typed",
19
+ ]
20
+
21
+ dependencies = [
22
+ "attrs",
23
+ "parityos>=3.0.0",
24
+ ]
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "filecheck",
29
+ "cudaq",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "ParityQC"
34
+ email = "parityos@parityqc.com"
35
+
36
+ [project.urls]
37
+ Homepage = "https://parityqc.com/"
38
+
39
+ [tool.hatch.version]
40
+ path = "src/parityos_cudaq/_version.py"
@@ -0,0 +1,3 @@
1
+ from ._version import __version__
2
+
3
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ # This file is automatically managed by hatch, do not edit manually.
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,366 @@
1
+ """Exporter for translating ParityOS circuits to the Nvidia CUDA-Q Quake MLIR dialect."""
2
+ # ParityQC © 2020-2026. See the LICENSE file in the top level directory for details.
3
+
4
+ from __future__ import annotations
5
+
6
+ from types import MappingProxyType
7
+ from typing import Generic, final
8
+
9
+ from attrs import frozen
10
+ from parityos.bits import Qubit, QubitT
11
+ from parityos.operators.circuit import CircuitLike
12
+ from parityos.operators.conditional_operator import ConditionalOperator
13
+ from parityos.operators.controlled_operator import ControlledOperator
14
+ from parityos.operators.measurement import MX, MZ, Measurement, Reset
15
+ from parityos.operators.operator import Operator
16
+ from parityos.operators.pauli_basis import XBasis, ZBasis
17
+ from parityos.operators.rotation_operator import HasAngle
18
+ from parityos.utils.exceptions import ParityOSNotSupportedError
19
+
20
+
21
+ @frozen
22
+ class QuakeExportResult:
23
+ """Result of exporting a ParityOS circuit to a Quake IR program.
24
+
25
+ The exported module will extract qubit reference like this:
26
+ ```
27
+ %q_0 = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
28
+ ^^^^ ^
29
+ insignificant variable name meaningful register index
30
+ ```
31
+ """
32
+
33
+ #: Exported Quake program.
34
+ module: str
35
+ #: Mapping from parityos qubits to their indices in the quake register.
36
+ qubit_map: dict[Qubit, int]
37
+
38
+
39
+ def export_parityos_to_quake(
40
+ parityos_circuit: CircuitLike[Qubit], emulate_nvqpp: bool
41
+ ) -> QuakeExportResult:
42
+ """Export a ParityOS circuit to Quake.
43
+
44
+ If the circuit name is a legal name (contains only alphanumerics and underscores, but does not
45
+ start with a numeric), the ``func.func`` is given the circuit name as symbol name, prefixed with
46
+ "@". Otherwise, the ``func.func`` symbol name defaults to ``@parityos_circuit``.
47
+
48
+ Example:
49
+ Bell state circuit:
50
+ ```
51
+ module {
52
+ func.func @parityos_circuit() {
53
+ // Allocate register
54
+ %n_qubits = arith.constant 2 : i64
55
+ %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
56
+ %q_0 = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
57
+ %q_1 = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
58
+
59
+ quake.h %q_0 : (!quake.ref) -> ()
60
+ quake.x [%q_0] %q_1 : (!quake.ref, !quake.ref) -> ()
61
+
62
+ %m_result_0 = quake.mz %q_0 : (!quake.ref) -> !quake.measure
63
+ %m_result_1 = quake.mz %q_1 : (!quake.ref) -> !quake.measure
64
+ }
65
+ }
66
+ ```
67
+
68
+ Args:
69
+ parityos_circuit: Circuit to be exported.
70
+ emulate_nvqpp: Emulate the output of the nvqpp compiler from CUDA-Q.
71
+ Enabling this adds some attributes to the output module and function.
72
+ TODO: We do not know yet whether this will be needed in any context.
73
+ May be removed in the future.
74
+ """
75
+ exporter = _QuakeExporter(emulate_nvqpp)
76
+ exporter.export(parityos_circuit)
77
+
78
+ return QuakeExportResult(
79
+ module=exporter.module,
80
+ qubit_map=exporter.qubit_map,
81
+ )
82
+
83
+
84
+ @final
85
+ class _QuakeExporter(Generic[QubitT]):
86
+ """Exports a ParityOS circuit to the Cuda-Q Quake IR."""
87
+
88
+ # a map between ParityOS to Quake operations names
89
+ _PARITY_OS_TO_QUAKE = MappingProxyType(
90
+ {
91
+ # 1 target, 0 params:
92
+ "x": "x",
93
+ "y": "y",
94
+ "z": "z",
95
+ "h": "h",
96
+ "s": "s",
97
+ "sdg": "s<adj>",
98
+ "t": "t",
99
+ "tdg": "t<adj>",
100
+ # 1 target, 1 param:
101
+ "rx": "rx",
102
+ "ry": "ry",
103
+ "rz": "rz",
104
+ # 2 targets, 0 params
105
+ "swap": "swap",
106
+ }
107
+ )
108
+
109
+ @staticmethod
110
+ def _lookup_quake_name(parityos_name: str) -> str:
111
+ """Look up the corresponding Quake operator name for a
112
+ given parityos gate.
113
+
114
+ Args:
115
+ parityos_name: Name of the ParityOS gate to translate into Quake.
116
+
117
+ Returns:
118
+ A string containing the name of the corresponding Quake gate.
119
+
120
+ Raises:
121
+ ParityOSNotSupportedError: When the gate is not supported for export.
122
+ """
123
+ if parityos_name not in _QuakeExporter._PARITY_OS_TO_QUAKE:
124
+ raise ParityOSNotSupportedError(f"Gate {parityos_name} not supported for Quake export")
125
+
126
+ return _QuakeExporter._PARITY_OS_TO_QUAKE[parityos_name]
127
+
128
+ def __init__(self, emulate_nvqpp: bool) -> None:
129
+ """Create a new Quake Exporter object.
130
+
131
+ Args:
132
+ emulate_nvqpp: Emulate the output of the nvqpp compiler from CUDA-Q.
133
+ Enabling this adds some attributes to the output module and function.
134
+ """
135
+ self._module: str = ""
136
+ self._qubit_map: dict[QubitT, str] = dict()
137
+ self._qubit_index_map: dict[QubitT, int] = dict()
138
+ self._emulate_nvqpp = emulate_nvqpp
139
+ self._indentation = " " * 4
140
+ self._angle_count = 0
141
+ self._measurement_count = 0
142
+
143
+ @property
144
+ def module(self) -> str:
145
+ return self._module
146
+
147
+ @property
148
+ def qubit_map(self) -> dict[QubitT, int]:
149
+ """Mapping of ParityOS Qubit to Quake register index."""
150
+ return self._qubit_index_map
151
+
152
+ def export(self, circuit: CircuitLike[QubitT]) -> None:
153
+ """Export ParityOS Circuit to a Quake program.
154
+
155
+ Should only be called once.
156
+
157
+ Args:
158
+ circuit: ParityOS Circuit to be exported to Quake.
159
+
160
+ Returns:
161
+ Translated Quake program.
162
+ """
163
+ self._add_header(circuit)
164
+
165
+ self._add_allocation(circuit)
166
+
167
+ for op in circuit.operators:
168
+ self._add_operator(op)
169
+
170
+ self._add_termination()
171
+
172
+ @staticmethod
173
+ def _is_legal_circuit_name(name: str):
174
+ """Checks if a circuit name is a valid alphanumeric identifier.
175
+
176
+ A valid identifier must be non-empty, must not start with a digit,
177
+ and can only contain alphanumeric characters or underscores.
178
+
179
+ Args:
180
+ name: A circuit name to be validated.
181
+
182
+ Returns:
183
+ ``True`` if the name is a valid identifier, ``False`` otherwise.
184
+ """
185
+ return (
186
+ len(name) > 0 and not name[0].isnumeric() and all(c.isalnum() or c in "_" for c in name)
187
+ )
188
+
189
+ def _add_header(self, circuit: CircuitLike[Qubit]):
190
+ """Add the correct header string to the current conversion output.
191
+
192
+ Args:
193
+ circuit: The circuit to be exported.
194
+ """
195
+ circuit_name = (
196
+ circuit.name if self._is_legal_circuit_name(circuit.name) else "parityos_circuit"
197
+ )
198
+
199
+ if self._emulate_nvqpp:
200
+ funcfunc_name = f"__nvqpp__mlirgen__{circuit_name}"
201
+ # The C++ name mangling is to be read as follows:
202
+ # "_ZN" indicates a C++ function.
203
+ # Next comes the function namespace and name. To know where the namespace ends and
204
+ # where the name begins, the string is prefixed by the length of the namespace.
205
+ # Here, by cuda-q convention, the namespace is the circuit name, and the function name
206
+ # is "cl".
207
+ # The last two characters indicate void return type ("E") and void arguments ("v").
208
+ circuit_name_mangled = f"_ZN{len(circuit_name)}{circuit_name}clEv"
209
+ self._module = (
210
+ "// Generated by ParityOS\n"
211
+ + f'module attributes {{qtx.mangled_name_map = {{{funcfunc_name} = "{circuit_name_mangled}"}}}} {{\n'
212
+ + f' func.func @{funcfunc_name}() attributes {{"cudaq-entrypoint", "cudaq-kernel"}} {{\n'
213
+ )
214
+ else:
215
+ funcfunc_name = f"{circuit_name}"
216
+ self._module = (
217
+ "// Generated by ParityOS\n" + "module {\n" + f" func.func @{funcfunc_name}() {{\n"
218
+ )
219
+
220
+ def _add_termination(self):
221
+ """Add a return statement to the current conversion output."""
222
+ self._add_line("return")
223
+ self._module += " }\n}\n"
224
+
225
+ def _add_line(self, text: str):
226
+ """Add one line to the current conversion output."""
227
+ self._module += f"{self._indentation}{text}\n"
228
+
229
+ def _add_comment(self, comment: str):
230
+ """Add a comment to the current conversion output."""
231
+ self._add_line(f"// {comment}")
232
+
233
+ def _add_allocation(self, circuit: CircuitLike[QubitT]) -> None:
234
+ """Allocate the qubits required for the circuit using Quake dialect."""
235
+ qubits = circuit.ordered_qubits
236
+ self._add_comment("Allocate register")
237
+ self._add_line(f"%n_qubits = arith.constant {len(qubits)} : i64")
238
+ self._add_line("%register = quake.alloca !quake.veq<?>[%n_qubits : i64]")
239
+
240
+ for index, qubit in enumerate(qubits):
241
+ self._qubit_map[qubit] = f"%q_{index}"
242
+ self._qubit_index_map[qubit] = index
243
+ self._add_line(
244
+ f"%q_{index} = quake.extract_ref %register[{index}] : (!quake.veq<?>) -> !quake.ref"
245
+ )
246
+
247
+ self._add_line("")
248
+
249
+ def _add_operator(self, operator: Operator[QubitT]) -> None:
250
+ """Convert an operator to the Quake dialect.
251
+
252
+ Raises:
253
+ ParityOSNotSupportedError: When the operator is a conditional one.
254
+ """
255
+ if isinstance(operator, ConditionalOperator):
256
+ raise ParityOSNotSupportedError("Conditional Operator not supported.")
257
+
258
+ if isinstance(operator, Measurement):
259
+ self._add_measurement(operator)
260
+ return
261
+
262
+ if isinstance(operator, Reset):
263
+ self._add_reset(operator)
264
+ return
265
+
266
+ controlled_qubits: tuple[QubitT, ...] | None
267
+ target_qubits: tuple[QubitT, ...]
268
+ quake_operation_name: str
269
+
270
+ if isinstance(operator, ControlledOperator):
271
+ controlled_qubits = tuple(sorted(operator.control_qubits))
272
+ target_qubits = operator.ordered_target_qubits
273
+ quake_operation_name = self._lookup_quake_name(operator.target_operator.name)
274
+ else:
275
+ controlled_qubits = None
276
+ target_qubits = operator.ordered_qubits
277
+ quake_operation_name = self._lookup_quake_name(operator.name)
278
+ angle = _extract_gate_angle(operator)
279
+ angle_var = None
280
+ n_qubits = len(operator.qubits)
281
+
282
+ # Code generation
283
+ if angle is not None:
284
+ angle_var = self._new_angle_var()
285
+ self._add_line(f"{angle_var} = arith.constant {angle} : f64")
286
+ self._add_line(
287
+ f"quake.{quake_operation_name} "
288
+ + (f"({angle_var}) " if angle is not None else "")
289
+ + (
290
+ f"[{', '.join([self._qubit_map[q] for q in controlled_qubits])}] "
291
+ if controlled_qubits is not None
292
+ else ""
293
+ )
294
+ + f"{', '.join([self._qubit_map[q] for q in target_qubits])} : ("
295
+ + ("f64, " if angle is not None else "")
296
+ + ", ".join(["!quake.ref"] * n_qubits)
297
+ + ") -> ()"
298
+ )
299
+
300
+ def _new_angle_var(self) -> str:
301
+ """Generate a unique angle variable name, including % prefix."""
302
+ angle_var = f"%angle_{self._angle_count}"
303
+ self._angle_count += 1
304
+ return angle_var
305
+
306
+ def _new_measurement_var(self) -> str:
307
+ """Generate a unique measurement result variable name, including % prefix."""
308
+ measurement_var = f"%m_result_{self._measurement_count}"
309
+ self._measurement_count += 1
310
+ return measurement_var
311
+
312
+ def _add_measurement(self, measure: Measurement[QubitT, XBasis | ZBasis]) -> None:
313
+ """Convert a measurement operatin to the Quake dialect.
314
+
315
+ Args:
316
+ measure: the qubits to be measured and the basis they should be measured on
317
+
318
+ Raises:
319
+ ParityOSNotSupportedError: If a partial measurement is triggered or in a basis different
320
+ from X or Z.
321
+ """
322
+ if len(measure.qubits) == 1:
323
+ target = self._qubit_map[measure.qubit]
324
+ source_type = "!quake.ref"
325
+ target_type = "!quake.measure"
326
+ elif len(measure.qubits) != len(self._qubit_map):
327
+ target = "%register"
328
+ source_type = "!quake.veq<?>"
329
+ target_type = "!cc.stdvec<!quake.measure>"
330
+ else:
331
+ raise ParityOSNotSupportedError(
332
+ "Only measurement of single qubits or the entire register is supported."
333
+ )
334
+
335
+ if isinstance(measure, MX):
336
+ op_name = "mx"
337
+ elif isinstance(measure, MZ):
338
+ op_name = "mz"
339
+ else:
340
+ raise ParityOSNotSupportedError(f"Unsupported measurement '{measure.name}'.")
341
+
342
+ self._add_line(
343
+ f"{self._new_measurement_var()} = "
344
+ + f"quake.{op_name} {target} : ({source_type}) -> {target_type}"
345
+ )
346
+ self._add_line("")
347
+
348
+ def _add_reset(self, reset: Reset[QubitT]):
349
+ """Convert a set of reset operatin to the Quake dialect."""
350
+ for qubit in reset.ordered_qubits:
351
+ self._add_line(f"quake.reset {self._qubit_map[qubit]} : (!quake.ref) -> ()")
352
+
353
+
354
+ def _extract_gate_angle(operator: Operator[QubitT]) -> float | None:
355
+ """Extract the angle of a rotation gate, or return `None` if no angle exists.
356
+
357
+ Raises:
358
+ ParityOSNotSupportedError: If the angle is not an integer or float.
359
+ """
360
+ if not isinstance(operator, HasAngle): # noqa: SIM108
361
+ return None
362
+
363
+ if not isinstance(operator.angle, float | int):
364
+ raise ParityOSNotSupportedError("Symbolic parameters not supported.")
365
+
366
+ return float(operator.angle)
File without changes
File without changes
@@ -0,0 +1,294 @@
1
+ """This file contains the quake circuit equivalent of the ParityOS circuits defined in
2
+ parityos_circuits.py, given in FILECHECK match syntax.
3
+
4
+ In addition, a few more ParityOS test circuits are defined.
5
+ """
6
+ # ParityQC © 2025. See the LICENSE file in the top level directory for details.
7
+
8
+ from parityos.bits import Qubit
9
+ from parityos.bits import get_q as q
10
+ from parityos.operators.circuit import Circuit
11
+ from parityos.operators.controlled_operator import CCX, CRX
12
+ from parityos.operators.elementary_operator import SDg, Swap, TDg
13
+
14
+ ######################################################
15
+ ## Additional Circuits
16
+ ######################################################
17
+
18
+ # 17
19
+ pc17 = Circuit(
20
+ [
21
+ SDg(q(0)),
22
+ TDg(q(0)),
23
+ ]
24
+ ).measure_all()
25
+
26
+ # 18
27
+ pc18 = Circuit([CRX(q(0), q(1), 1.0), Swap((q(1), q(2))), CCX((q(0), q(2)), q(3))]).measure_all()
28
+
29
+
30
+ ADDITIONAL_PARITYOS_CIRCUITS: list[Circuit[Qubit]] = [pc17, pc18]
31
+
32
+
33
+ """
34
+ This string is used by the filecheck tests for matching.
35
+
36
+ # 0:
37
+ # ┌───┐
38
+ # 0: ┤ X ├
39
+ # └───┘
40
+ CHECK-LABEL: CIRCUIT NUMBER 0
41
+ CHECK: %n_qubits = arith.constant 1 : i64
42
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
43
+ CHECK: %[[q0:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
44
+ CHECK-DAG: quake.x %[[q0]] : (!quake.ref) -> ()
45
+ CHECK: return
46
+
47
+ # 1
48
+ # ┌───────┐
49
+ # 1: ┤ Rx(2) ├
50
+ # └───────┘
51
+ CHECK-LABEL: CIRCUIT NUMBER 1
52
+ CHECK: %n_qubits = arith.constant 1 : i64
53
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
54
+ CHECK: %[[q1:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
55
+ CHECK-DAG: %[[a0:.+]] = arith.constant 2.0 : f64
56
+ CHECK-DAG: quake.rx (%[[a0]]) %[[q1]] : (f64, !quake.ref) -> ()
57
+ CHECK: return
58
+
59
+ # 2
60
+ # ┌───────────────────┐
61
+ # a: ┤ Rz(2*phi - theta) ├
62
+ # └───────────────────┘
63
+ CHECK-LABEL: CIRCUIT NUMBER 2
64
+ CHECK: NOT SUPPORTED
65
+ Symbolics not allowed
66
+
67
+ # 3
68
+ # ┌─────────────┐
69
+ # (0, 0): ┤ Rx(2*theta) ├
70
+ # └──────┬──────┘
71
+ # c: ───────■───────
72
+ CHECK-LABEL: CIRCUIT NUMBER 3
73
+ CHECK: NOT SUPPORTED
74
+ Symbolics not allowed
75
+
76
+ # 4
77
+ # ┌───────┐┌───┐┌───┐
78
+ # a: ┤ Rx(2) ├┤ H ├┤ Z ├
79
+ # └───────┘├───┤└───┘
80
+ # b: ─────────┤ X ├─────
81
+ # ┌───┐ └─┬─┘
82
+ # c: ──┤ X ├────■───────
83
+ # └───┘
84
+ CHECK-LABEL: CIRCUIT NUMBER 4
85
+ CHECK: %n_qubits = arith.constant 3 : i64
86
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
87
+ CHECK-DAG: %[[a:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
88
+ CHECK-DAG: %[[b:.+]] = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
89
+ CHECK-DAG: %[[c:.+]] = quake.extract_ref %register[2] : (!quake.veq<?>) -> !quake.ref
90
+ CHECK-DAG: %[[angle:.+]] = arith.constant 2.0 : f64
91
+ CHECK-DAG: quake.rx (%[[angle]]) %[[a]] : (f64, !quake.ref) -> ()
92
+ CHECK-DAG: quake.h %[[a]] : (!quake.ref) -> ()
93
+ CHECK-DAG: quake.x %[[c]] : (!quake.ref) -> ()
94
+ CHECK-DAG: quake.x [%[[c]]] %[[b]] : (!quake.ref, !quake.ref) -> ()
95
+ CHECK-DAG: quake.z %[[a]] : (!quake.ref) -> ()
96
+ CHECK: return
97
+
98
+ # 5
99
+ # ┌───┐┌────────────────── ───────┐
100
+ # 0: ┤ H ├┤ ──■── ├──────
101
+ # ├───┤│ If-0 c[0] ^ c[1] ┌─┴─┐ End-0 │
102
+ # 1: ┤ H ├┤ ┤ X ├ ├───■──
103
+ # └───┘└────────╥───────── └───┘ ───────┘ ┌─┴─┐
104
+ # 2: ──────────────╫─────────────────────────┤ X ├
105
+ # ┌───╨────┐ └───┘
106
+ # c: 2/══════════╡ [expr] ╞═════════════════════════
107
+ # └────────┘
108
+ CHECK-LABEL: CIRCUIT NUMBER 5
109
+ CHECK: NOT SUPPORTED
110
+ Classical conditions not supported
111
+
112
+ # 6
113
+ # ┌───┐┌─┐
114
+ # 0: ┤ H ├┤M├──────
115
+ # ├───┤└╥┘┌─┐
116
+ # 1: ┤ H ├─╫─┤M├───
117
+ # ├───┤ ║ └╥┘┌─┐
118
+ # 2: ┤ H ├─╫──╫─┤M├
119
+ # └───┘ ║ ║ └╥┘
120
+ # c: 3/══════╩══╩══╩═
121
+ # 0 1 2
122
+ CHECK-LABEL: CIRCUIT NUMBER 6
123
+ CHECK: %n_qubits = arith.constant 3 : i64
124
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
125
+ CHECK-DAG: %[[q0:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
126
+ CHECK-DAG: %[[q1:.+]] = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
127
+ CHECK-DAG: %[[q2:.+]] = quake.extract_ref %register[2] : (!quake.veq<?>) -> !quake.ref
128
+ CHECK-DAG: quake.h %[[q0]] : (!quake.ref) -> ()
129
+ CHECK-DAG: quake.h %[[q1]] : (!quake.ref) -> ()
130
+ CHECK-DAG: quake.h %[[q2]] : (!quake.ref) -> ()
131
+ CHECK-DAG: %[[m0:.+]] = quake.mz %[[q0]] : (!quake.ref) -> !quake.measure
132
+ CHECK-DAG: %[[m1:.+]] = quake.mz %[[q1]] : (!quake.ref) -> !quake.measure
133
+ CHECK-DAG: %[[m2:.+]] = quake.mz %[[q2]] : (!quake.ref) -> !quake.measure
134
+ CHECK: return
135
+
136
+ # 7
137
+ # ┌───┐┌───┐┌─┐┌───┐ ┌─┐
138
+ # a: ┤ Z ├┤ H ├┤M├┤ H ├───────────────────────────────────────────────────────┤M├───
139
+ # ├───┤├───┤└╥┘└┬─┬┘┌───┐ ┌───┐└╥┘┌─┐
140
+ # b: ┤ X ├┤ H ├─╫──┤M├─┤ H ├─────────────────────────────────────────────┤ Z ├─╫─┤M├
141
+ # └───┘└───┘ ║ └╥┘ └───┘┌──────────────────────────── ┌───┐ ───────┐ └┬─┬┘ ║ └╥┘
142
+ # c: ───────────╫───╫───────┤ If-0 (c[1] | c[3]) == true ┤ X ├ End-0 ├──┤M├──╫──╫─
143
+ # ║ ║ └─────────────╥────────────── └───┘ ───────┘ └╥┘ ║ ║
144
+ # ║ ║ ┌───╨────┐ ║ ║ ║
145
+ # c: 5/═══════════╩═══╩═════════════════╡ [expr] ╞═══════════════════════════╩═══╩══╩═
146
+ # 1 3 └────────┘ 0 2 4
147
+ CHECK-LABEL: CIRCUIT NUMBER 7
148
+ CHECK: NOT SUPPORTED
149
+ Classical conditions not supported
150
+
151
+ # 8
152
+ # ┌───┐
153
+ # 0: ┤ X ├──■──────────────────────────────────────────■─
154
+ # └─┬─┘┌─┴─┐┌──────────────┐ ┌───────────┐ │
155
+ # 1: ──┼──┤ X ├┤0 ├─────┤0 ├──────┼─
156
+ # │ └───┘│ Rxx(-2*phi) │ │ │ │
157
+ # 2: ──┼───────┤1 ├──■──┤ Rzz(phi) ├──■───■─
158
+ # │ └──────────────┘ │ │ │ │ │
159
+ # 3: ──■─────────────────────────┼──┤1 ├──┼───┼─
160
+ # ┌─┴─┐└─────┬─────┘ │ │
161
+ # 4: ──────────────────────────┤ X ├──────■────────■───■─
162
+ # └───┘ ┌─┴─┐
163
+ # 5: ────────────────────────────────────────────┤ Y ├───
164
+ # └───┘
165
+ CHECK-LABEL: CIRCUIT NUMBER 8
166
+ CHECK: NOT SUPPORTED
167
+ RZZ gate not supported
168
+
169
+ # 9
170
+ # ┌───┐ ┌───┐
171
+ # 0: ┤ H ├─|0>─┤ H ├
172
+ # └───┘ └───┘
173
+ CHECK-LABEL: CIRCUIT NUMBER 9
174
+ CHECK: %n_qubits = arith.constant 1 : i64
175
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
176
+ CHECK-DAG: %[[q0:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
177
+ CHECK-DAG: quake.h %[[q0]] : (!quake.ref) -> ()
178
+ CHECK-DAG: quake.reset %[[q0]] : (!quake.ref) -> ()
179
+ CHECK-DAG: quake.h %[[q0]] : (!quake.ref) -> ()
180
+ CHECK: return
181
+
182
+ # 10
183
+ # ┌────┐┌───┐┌─────┐┌──────┐┌─────┐┌───┐
184
+ # 0: ┤ √X ├┤ T ├┤ Tdg ├┤ √Xdg ├┤ Sdg ├┤ S ├
185
+ # └────┘└───┘└─────┘└──────┘└─────┘└───┘
186
+ CHECK-LABEL: CIRCUIT NUMBER 10
187
+ CHECK: NOT SUPPORTED
188
+ SX gate not supported
189
+
190
+ # 11
191
+ # ┌────────────┐
192
+ # 0: ───────────┤0 ├
193
+ # │ Rxx(0.25) │
194
+ # 1: ─■─────────┤1 ├
195
+ # │ZZ(0.75) └────────────┘
196
+ # 2: ─■───────────────────────
197
+ CHECK-LABEL: CIRCUIT NUMBER 11
198
+ CHECK: NOT SUPPORTED
199
+ RZZ gate not supported
200
+
201
+ # 12
202
+ # ┌────── ┌───┐ ───────┐
203
+ # 0: ──┤ If-0 ─┤ X ├ End-0 ├─
204
+ # └──╥─── └───┘ ───────┘
205
+ # ┌────╨────┐
206
+ # c: 1/╡ c_0=0x1 ╞═══════════════
207
+ # └─────────┘
208
+ CHECK-LABEL: CIRCUIT NUMBER 12
209
+ CHECK: NOT SUPPORTED
210
+ Classical conditions not supported
211
+
212
+ # 13
213
+ # ┌────── ┌───┐ ───────┐
214
+ # q: ──┤ If-0 ─┤ X ├ End-0 ├─
215
+ # └──╥─── └───┘ ───────┘
216
+ # ┌────╨────┐
217
+ # c: 1/╡ c_0=0x0 ╞═══════════════
218
+ # └─────────┘
219
+ CHECK-LABEL: CIRCUIT NUMBER 13
220
+ CHECK: NOT SUPPORTED
221
+ Classical conditions not supported
222
+
223
+ # 14
224
+ # ┌──────────────────────────── ┌───┐ ───────┐ ┌─────────────────────────────────── ┌───┐ ───────┐
225
+ # q: ┤ If-0 (c[0] | c[2]) == true ┤ X ├ End-0 ├─┤ If-0 (c[0] ^ c[1] ^ c[2]) == true ┤ Z ├ End-0 ├─
226
+ # └─────────────╥────────────── └───┘ ───────┘ └─────────────────╥───────────────── └───┘ ───────┘
227
+ # ┌───╨────┐ ┌───╨────┐
228
+ # c: 3/══════════╡ [expr] ╞═══════════════════════════════════════╡ [expr] ╞════════════════════════════
229
+ # └────────┘ └────────┘
230
+ CHECK-LABEL: CIRCUIT NUMBER 14
231
+ CHECK: NOT SUPPORTED
232
+ Classical conditions not supported
233
+
234
+ # 15
235
+ # ┌─────────────────────────────────────────┐ ┌─────────────┐ ┌───────────────────────────────────┐
236
+ # q_0: ┤0 ├───┤ Rx(2*theta) ├───┤0 ├
237
+ # │ │ └─────────────┘ │ │
238
+ # q_1: ┤1 ├─────────────────────┤1 ├
239
+ # │ │ │ exp(-it (IIII + IYXZ + XZYI))(1) │
240
+ # q_2: ┤2 exp(-it (IIYX + XZYI + ZIZI))(0.5*phi) ├─────────────────────┤2 ├
241
+ # │ │┌───────────────────┐│ │
242
+ # q_3: ┤ ├┤ Ry((-3.14)*theta) ├┤3 ├
243
+ # │ │└───────────────────┘└───────────────────────────────────┘
244
+ # q_4: ┤3 ├──────────────────────────────────────────────────────────
245
+ # └─────────────────────────────────────────┘
246
+ CHECK-LABEL: CIRCUIT NUMBER 15
247
+ CHECK: NOT SUPPORTED
248
+ Symbolics not supported
249
+
250
+ # 16
251
+ # ┌───────┐┌──────────────────────┐
252
+ # q_0: ┤ Rx(π) ├┤0 ├
253
+ # └───────┘│ (XX+YY)((-2)*phi,0) │
254
+ # q_1: ─────────┤1 ├
255
+ # └──────────────────────┘
256
+ CHECK-LABEL: CIRCUIT NUMBER 16
257
+ CHECK: NOT SUPPORTED
258
+ Symbolics not supported
259
+
260
+ # 17
261
+ # ░ ░ ┌───┐
262
+ # q_0: ──────░───────░─┤ H ├
263
+ # ┌───┐ ░ ┌───┐ ░ └───┘
264
+ # q_1: ┤ X ├─░─┤ X ├─░──────
265
+ # └───┘ ░ └───┘ ░
266
+ # q_2: ──────────────░───■──
267
+ # ░ ┌─┴─┐
268
+ # q_3: ──────────────░─┤ X ├
269
+ # ░ └───┘
270
+ CHECK-LABEL: CIRCUIT NUMBER 17
271
+ CHECK: NOT SUPPORTED
272
+ Barriers not supported
273
+
274
+ CHECK-LABEL: CIRCUIT NUMBER 18
275
+ CHECK: %n_qubits = arith.constant 1 : i64
276
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
277
+ CHECK-DAG: %[[q0:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
278
+ CHECK-DAG: quake.s<adj> %[[q0]] : (!quake.ref) -> ()
279
+ CHECK-DAG: quake.t<adj> %[[q0]] : (!quake.ref) -> ()
280
+ CHECK: return
281
+
282
+ CHECK-LABEL: CIRCUIT NUMBER 19
283
+ CHECK: %n_qubits = arith.constant 4 : i64
284
+ CHECK: %register = quake.alloca !quake.veq<?>[%n_qubits : i64]
285
+ CHECK: %[[q1:.+]] = quake.extract_ref %register[0] : (!quake.veq<?>) -> !quake.ref
286
+ CHECK: %[[q2:.+]] = quake.extract_ref %register[1] : (!quake.veq<?>) -> !quake.ref
287
+ CHECK: %[[q3:.+]] = quake.extract_ref %register[2] : (!quake.veq<?>) -> !quake.ref
288
+ CHECK: %[[q4:.+]] = quake.extract_ref %register[3] : (!quake.veq<?>) -> !quake.ref
289
+ CHECK-DAG: %[[angle:.+]] = arith.constant 1.0 : f64
290
+ CHECK-DAG: quake.rx (%[[angle]]) [%[[q1]]] %[[q2]] : (f64, !quake.ref, !quake.ref) -> ()
291
+ CHECK-DAG: quake.swap %[[q2]], %[[q3]] : (!quake.ref, !quake.ref) -> ()
292
+ CHECK-DAG: quake.x [%[[q1]], %[[q3]]] %[[q4]] : (!quake.ref, !quake.ref, !quake.ref) -> ()
293
+ CHECK: return
294
+ """
@@ -0,0 +1,87 @@
1
+ # ParityQC © 2025. See the LICENSE file in the top level directory for details.
2
+ import shutil
3
+ import subprocess
4
+ from typing import final
5
+
6
+ import pytest
7
+ from filecheck.matcher import Matcher
8
+ from filecheck.options import Options
9
+ from parityos.bits import Qubit
10
+ from parityos.operators.circuit import CircuitLike
11
+ from parityos.utils.exceptions import ParityOSNotSupportedError
12
+ from parityos_cudaq.exporter import export_parityos_to_quake
13
+ from parityos_testutils.exporters.parityos_circuits import PARITYOS_CIRCUITS
14
+
15
+ from .quake_circuits import ADDITIONAL_PARITYOS_CIRCUITS
16
+
17
+ CUSTOM_PARITYOS_CIRCUITS = PARITYOS_CIRCUITS + ADDITIONAL_PARITYOS_CIRCUITS
18
+
19
+
20
+ def check_qudaq_opt_installation() -> bool:
21
+ """Checks if qudaq-opt is on the PATH."""
22
+ return bool(shutil.which("cudaq-opt"))
23
+
24
+
25
+ @final
26
+ class TestQuakeExporterCudaQ:
27
+ """Class to test that cudaq understands the exporter output.
28
+
29
+ These tests require the cudaq-opt binary to be available on the PATH, which is not expected in the CI.
30
+ To support them,
31
+ - Install a cudaq-opt binary.
32
+ - Make sure that cudaq-opt is on the PATH during python execution, such as the /venv/bin
33
+ directory.
34
+ """
35
+
36
+ @pytest.mark.skipif(not check_qudaq_opt_installation(), reason="cudaq-opt not installed")
37
+ @pytest.mark.parametrize(
38
+ "parityos_circuit",
39
+ CUSTOM_PARITYOS_CIRCUITS,
40
+ ids=[i for i, _ in enumerate(CUSTOM_PARITYOS_CIRCUITS)],
41
+ )
42
+ def test_qudaq_opt_successful_execution(self, parityos_circuit: CircuitLike[Qubit]):
43
+ """Tests successful qudaq-opt round trip on exported circuits."""
44
+ try:
45
+ quake_module = export_parityos_to_quake(parityos_circuit, True).module
46
+ except ParityOSNotSupportedError:
47
+ # Unsupported
48
+ return
49
+
50
+ cudaq_binary = str(shutil.which("cudaq-opt")) # The subprocess has no access to the PATH.
51
+ try:
52
+ subprocess.run(
53
+ [cudaq_binary],
54
+ input=quake_module,
55
+ capture_output=True,
56
+ text=True,
57
+ check=True, # Raise exception on non-zero exit code
58
+ )
59
+ except subprocess.CalledProcessError as e:
60
+ pytest.fail(
61
+ f"Test circuit did not pass cudaq-opt round trip (likely invalid syntax): {e}"
62
+ )
63
+
64
+
65
+ @final
66
+ class TestQuakeExportWithFileCheck:
67
+ """Class to run FileCheck tests."""
68
+
69
+ def test_quake_export(self, tmp_path):
70
+ from .quake_circuits import __file__ as match_filename
71
+
72
+ tmp_filename = tmp_path / "_quake_output.tmp.qke"
73
+ with open(tmp_filename, "w") as tmp_file:
74
+ for i, parityos_circuit in enumerate(CUSTOM_PARITYOS_CIRCUITS):
75
+ tmp_file.write(f"// CIRCUIT NUMBER {i}\n")
76
+ try:
77
+ export_result = export_parityos_to_quake(parityos_circuit, True)
78
+ tmp_file.write(export_result.module)
79
+ except ParityOSNotSupportedError:
80
+ tmp_file.write("// NOT SUPPORTED\n")
81
+
82
+ filecheck_options = Options(match_filename=match_filename, input_file=str(tmp_filename))
83
+ matcher = Matcher.from_opts(filecheck_options)
84
+ exitcode = matcher.run()
85
+
86
+ if exitcode != 0:
87
+ pytest.fail(f"Filecheck failed with exit code {exitcode}")