diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5c6ac4a..e0bfb12 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [ubuntu-24.04, ubuntu-24.04-arm, macos-14] + runs-on: [ubuntu-24.04, ubuntu-24.04-arm, macos-15] uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 with: runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fed2ca..421f7c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,39 @@ jobs: name: ๐Ÿ” Change uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-change-detection.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 + python-tests: + name: ๐Ÿ Test + needs: change-detection + if: fromJSON(needs.change-detection.outputs.run-python-tests) + strategy: + fail-fast: false + matrix: + runs-on: [ubuntu-24.04, ubuntu-24.04-arm, macos-15] + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 + with: + runs-on: ${{ matrix.runs-on }} + setup-mlir: true + llvm-version: "f8cb7987c64dcffb72414a40560055cb717dbf74" + + python-coverage: + name: ๐Ÿ Coverage + needs: [change-detection, python-tests] + if: fromJSON(needs.change-detection.outputs.run-python-tests) + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-coverage.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 + permissions: + contents: read + id-token: write + + python-linter: + name: ๐Ÿ Lint + needs: change-detection + if: fromJSON(needs.change-detection.outputs.run-python-tests) + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 + with: + enable-ty: true + setup-mlir: true + llvm-version: "f8cb7987c64dcffb72414a40560055cb717dbf74" + build-sdist: name: ๐Ÿš€ CD (sdist) needs: change-detection @@ -32,22 +65,13 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [ubuntu-24.04, ubuntu-24.04-arm, macos-14] + runs-on: [ubuntu-24.04, ubuntu-24.04-arm, macos-15] uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11 with: runs-on: ${{ matrix.runs-on }} setup-mlir: true llvm-version: "f8cb7987c64dcffb72414a40560055cb717dbf74" - mlir-tests: - name: ๐Ÿ‰ Test - needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) - permissions: - contents: read - id-token: write - uses: ./.github/workflows/reusable-mlir-tests.yml - cpp-linter: name: ๐Ÿ‡จโ€Œ Lint needs: change-detection @@ -74,9 +98,11 @@ jobs: contents: read needs: - change-detection + - python-coverage + - python-tests + - python-linter - build-sdist - build-wheel - - mlir-tests - cpp-linter runs-on: ubuntu-latest steps: @@ -85,12 +111,12 @@ jobs: with: allowed-skips: >- ${{ - fromJSON(needs.change-detection.outputs.run-cd) - && '' || 'build-sdist,build-wheel,' + fromJSON(needs.change-detection.outputs.run-python-tests) + && '' || 'python-tests,python-coverage,python-linter,' }} ${{ - fromJSON(needs.change-detection.outputs.run-cpp-tests) - && '' || 'mlir-tests,' + fromJSON(needs.change-detection.outputs.run-cd) + && '' || 'build-sdist,build-wheel,' }} ${{ fromJSON(needs.change-detection.outputs.run-cpp-linter) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3af6e82..a51f14a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,9 @@ ci: autofix_commit_msg: "๐ŸŽจ pre-commit fixes" skip: [mypy, ty-check] +default_language_version: + python: "3.13" + repos: # Ensure uv lock file is up-to-date - repo: https://github.com/astral-sh/uv-pre-commit @@ -77,7 +80,7 @@ repos: rev: v1.19.1 hooks: - id: mypy - files: ^(python/mqt|test/python|noxfile.py) + files: ^(python/mqt|test|noxfile.py) args: [] additional_dependencies: - nox diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f03889f..07d7be6 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,15 +11,14 @@ build: tools: python: "3.13" apt_packages: - - jq - - zstd + - graphviz jobs: post_checkout: # Skip docs build if the commit message contains "skip ci" - (git --no-pager log --pretty="tformat:%s -- %b" -1 | grep -viq "skip ci") || exit 183 # Skip docs build if there are no changes related to docs - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ include/ python/ .github/contributing* .github/support* .readthedocs.yaml; + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ include/ lib/ python/ .readthedocs.yaml; then exit 183; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 047b995..fab3162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,21 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ๐Ÿ Introduce Python package providing Catalyst plugin utilities and device configuration ([#20]) ([**@flowerthrower**]) +- ๐Ÿงช Add comprehensive round-trip Python integration tests ([#20]) ([**@flowerthrower**]) - ๐Ÿ”Œ Add MLIR plugin for connecting MQT Core with Catalyst ([#3]) ([**@flowerthrower**], [**@burgholzer**]) - ๐Ÿ“ฆ Set up the initial repo structure and configuration ([#1]) ([**@flowerthrower**], [**@burgholzer**]) +### Changed + +- ๐Ÿ”„ Migrate testing infrastructure from LIT/MLIR-level to Python/pytest ([#20]) ([**@flowerthrower**]) +- ๐Ÿ‘ท Update CI/CD macOS runners to `macos-15` ([#20]) ([**@flowerthrower**]) +- ๐Ÿ“ฆ Bump `mqt-core` version to `v3.4.0` ([#20]) ([**@flowerthrower**]) + +### Removed + +- ๐Ÿ—‘๏ธ Remove LIT/MLIR test infrastructure and files ([#20]) ([**@flowerthrower**]) + ## Initial discussions _๐Ÿ“š Refer to the [original MQT Core PR] for initial discussions and decisions leading to this project._ @@ -24,6 +36,7 @@ _๐Ÿ“š Refer to the [original MQT Core PR] for initial discussions and decisions +[#20]: https://github.com/munich-quantum-toolkit/core-plugins-catalyst/pull/20 [#3]: https://github.com/munich-quantum-toolkit/core-plugins-catalyst/pull/3 [#1]: https://github.com/munich-quantum-toolkit/core-plugins-catalyst/pull/1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 896973d..66cfae4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,4 +35,3 @@ include_directories(${MLIR_INCLUDE_DIRS}) add_subdirectory(include) add_subdirectory(lib) -add_subdirectory(test) diff --git a/README.md b/README.md index a81726c..843169a 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ uv venv .venv . .venv/bin/activate # Install Catalyst and build the plugin -uv pip install pennylane-catalyst>0.12.0 +uv pip install pennylane-catalyst==0.13.0 uv sync --verbose --active --config-settings=cmake.define.CMAKE_BUILD_TYPE=Release @@ -124,42 +124,78 @@ uv sync --verbose --active --config-settings=cmake.define.LLVM_DIR="$LLVM_DIR" ``` -### 3) Use the MQT plugin with your PennyLane code +### 3) Use the MQT plugin and explore intermediate MLIR representations The MQT plugin provides device configuration utilities to prevent Catalyst from decomposing gates into unitary matrices, enabling lossless roundtrip conversions. -**Important:** Use `get_device()` from the MQT plugin instead of `qml.device()` directly: +You can inspect the intermediate MLIR representations during the roundtrip between `CatalystQuantum` and `MQTOpt` dialects. + +```python +from __future__ import annotations +from pathlib import Path +from typing import Any -```python3 -import catalyst import pennylane as qml from catalyst.passes import apply_pass from mqt.core.plugins.catalyst import get_device # Use get_device() to configure the device for MQT plugin compatibility -# This prevents gates from being decomposed into unitary matrices device = get_device("lightning.qubit", wires=2) +# Wrap your circuit with the conversion passes @apply_pass("mqt.mqtopt-to-catalystquantum") @apply_pass("mqt.catalystquantum-to-mqtopt") @qml.qnode(device) def circuit() -> None: qml.Hadamard(wires=[0]) qml.CNOT(wires=[0, 1]) - # Controlled gates will NOT be decomposed to matrices - qml.ctrl(qml.PauliX(wires=0), control=1) - catalyst.measure(0) - catalyst.measure(1) +# JIT compile using qjit @qml.qjit(target="mlir", autograph=True) def module() -> None: return circuit() -# Get the optimized MLIR representation -mlir_output = module.mlir_opt +# --- Custom pipeline to capture intermediate MLIR --- +custom_pipeline = [ + # Only use the two MQT passes for demonstration + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), +] + + +# JIT compilation with intermediate MLIR files saved +@qml.qjit(target="mlir", autograph=True, keep_intermediate=2, pipelines=custom_pipeline) +def module() -> Any: + return circuit() + + +# Trigger compilation and optimized MLIR generation +module.mlir_opt + +# Catalyst writes intermediate MLIR files to the current working directory +mlir_dir = Path.cwd() +catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" +mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" +mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + +# Read MLIR files +with catalyst_mlir.open("r", encoding="utf-8") as f: + mlir_before_conversion = f.read() +with mlir_to_mqtopt.open("r", encoding="utf-8") as f: + mlir_after_conversion = f.read() +with mlir_to_catalyst.open("r", encoding="utf-8") as f: + mlir_after_roundtrip = f.read() + +# Print MLIR contents +print("=== MLIR before conversion to MQTOpt ===") +print(mlir_before_conversion) +print("=== MLIR after conversion to MQTOpt ===") +print(mlir_after_conversion) +print("=== MLIR after roundtrip back to CatalystQuantum ===") +print(mlir_after_roundtrip) ``` **Alternative:** You can also configure an existing device: @@ -180,7 +216,7 @@ The MQT Core Catalyst Plugin is compatible with Python version 3.11 and newer. The MQT Core Catalyst Plugin relies on some external dependencies: - [llvm/llvm-project](https://github.com/llvm/llvm-project): A toolkit for the construction of highly optimized compilers, optimizers, and run-time environments (specific revision: `f8cb7987c64dcffb72414a40560055cb717dbf74`). -- [PennyLaneAI/catalyst](https://github.com/PennyLaneAI/catalyst): A package that enables just-in-time (JIT) compilation of hybrid quantum-classical programs implemented with PennyLane (version > 0.12.0). +- [PennyLaneAI/catalyst](https://github.com/PennyLaneAI/catalyst): A package that enables just-in-time (JIT) compilation of hybrid quantum-classical programs implemented with PennyLane (version == 0.13.0). - [MQT Core](https://github.com/munich-quantum-toolkit/core-plugins-catalyst): Provides the MQTOpt MLIR dialect and supporting infrastructure. Note, both LLVM/MLIR and Catalyst are currently restricted to specific versions. You must build LLVM/MLIR locally from the exact revision specified above and configure CMake to use it (see installation instructions). diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index aaed078..8b5d2f3 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -11,11 +11,11 @@ include(FetchContent) # cmake-format: off -set(MQT_CORE_MINIMUM_VERSION 3.3.3 +set(MQT_CORE_MINIMUM_VERSION 3.4.0 CACHE STRING "MQT Core minimum version") -set(MQT_CORE_VERSION 3.3.3 +set(MQT_CORE_VERSION 3.4.0 CACHE STRING "MQT Core version") -set(MQT_CORE_REV "8c9f6ab24968401e450812fc0ff7d05b5ae07a63" +set(MQT_CORE_REV "6bcc01e7d135058c6439c64fdd5f14b65ab88816" CACHE STRING "MQT Core identifier (tag, branch or commit hash)") set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit" CACHE STRING "MQT Core repository owner (change when using a fork)") diff --git a/pyproject.toml b/pyproject.toml index da32f43..1f6b46a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,9 @@ Issues = "https://github.com/munich-quantum-toolkit/core-plugins-catalyst/issues Discussions = "https://github.com/munich-quantum-toolkit/core-plugins-catalyst/discussions" PyPI = "https://pypi.org/project/mqt-core-plugins-catalyst/" +[project.entry-points."catalyst.passes_resolution"] +"mqt.passes" = "mqt.core.plugins.catalyst" + [tool.scikit-build] # Protect the configuration against future changes in scikit-build-core minimum-version = "build-system.requires" @@ -120,11 +123,15 @@ strict = true addopts = [ "-ra", "--numprocesses=auto", # Automatically use all available CPU cores for parallel testing + "--dist=loadfile", # Run tests from the same file sequentially (tests share MLIR files) while parallelizing across files ] filterwarnings = [ + "error", + 'ignore:.*The jaxlib.hlo_helpers submodule is deprecated.*:DeprecationWarning:', + "ignore:.*got an unexpected keyword argument '___pyct_anno'.*:DeprecationWarning:", ] log_level = "INFO" -testpaths = ["test/python"] +testpaths = ["test"] [tool.coverage] @@ -145,7 +152,7 @@ report.exclude_also = [ ] [tool.mypy] -files = ["python/mqt", "test/python", "noxfile.py"] +files = ["python/mqt", "test", "noxfile.py"] mypy_path = ["$MYPY_CONFIG_FILE_DIR/python"] python_version = "3.11" warn_unused_configs = true @@ -156,7 +163,7 @@ explicit_package_bases = true warn_unreachable = true [[tool.mypy.overrides]] -module = ["pytest_console_scripts.*"] +module = ["pytest_console_scripts.*", "pennylane.*", "catalyst.*"] ignore_missing_imports = true [tool.ruff] @@ -185,8 +192,8 @@ future-annotations = true known-first-party = ["mqt.core.plugins.catalyst"] [tool.ruff.lint.per-file-ignores] -"test/python/**" = ["T20", "INP001"] -"test/lit.cfg.py" = ["INP001"] +"test/**" = ["T20", "INP001"] +"test/test_plugin.py" = ["E501"] "docs/**" = ["T20", "INP001"] "noxfile.py" = ["T20", "TID251"] "*.pyi" = ["D418", "DOC202", "PYI011", "PYI021"] @@ -230,8 +237,8 @@ build = "cp3*" skip = ["*-musllinux_*", "cp314*"] # No CPython 3.14 support yet archs = "auto64" test-groups = ["test"] -test-sources = ["test/python"] -test-command = "pytest test/python" +test-sources = ["test"] +test-command = "pytest test" build-frontend = "build[uv]" [tool.cibuildwheel.linux] @@ -263,7 +270,12 @@ environments = [ "sys_platform == 'darwin' and platform_machine == 'arm64' and python_version < '3.14'", "sys_platform == 'linux' and python_version < '3.14'", ] -reinstall-package = ["mqt-core-plugins-catalyst"] +cache-keys = [ + { file = "pyproject.toml" }, + { git = { commit = true, tags = true } }, + { file = "lib/**/*"}, + { file = "include/**/*"}, +] [tool.ty.terminal] @@ -273,9 +285,6 @@ error-on-warning = true [tool.ty.src] exclude = [ "docs/**", - "eval/**", - "mlir/**", - "test/lit.cfg.py", ] @@ -310,7 +319,6 @@ test = [ dev = [ {include-group = "build"}, {include-group = "test"}, - "lit>=18.1.8", "nox>=2025.11.12", "ty==0.0.10", ] diff --git a/python/mqt/core/plugins/catalyst/__init__.py b/python/mqt/core/plugins/catalyst/__init__.py new file mode 100644 index 0000000..4dccca6 --- /dev/null +++ b/python/mqt/core/plugins/catalyst/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""MQT Catalyst Plugin.""" + +from __future__ import annotations + +from .device import configure_device_for_mqt, get_device +from .plugin import get_catalyst_plugin_abs_path, name2pass + +__all__ = [ + "configure_device_for_mqt", + "get_catalyst_plugin_abs_path", + "get_device", + "name2pass", +] diff --git a/python/mqt/core/plugins/catalyst/device.py b/python/mqt/core/plugins/catalyst/device.py new file mode 100644 index 0000000..1dbd795 --- /dev/null +++ b/python/mqt/core/plugins/catalyst/device.py @@ -0,0 +1,104 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Device utilities for MQT Catalyst Plugin. + +This module provides utilities to configure PennyLane devices for use with the MQT plugin, +preventing Catalyst from decomposing gates into quantum.unitary operations with matrix parameters. +""" + +from __future__ import annotations + +from typing import Any + +import pennylane as qml +from pennylane.devices.capabilities import DeviceCapabilities + +__all__ = ["configure_device_for_mqt", "get_device"] + + +def __dir__() -> list[str]: + return __all__ + + +def configure_device_for_mqt(device: qml.devices.Device) -> qml.devices.Device: + """Configure a PennyLane device to work optimally with the MQT plugin. + + This function modifies device capabilities to prevent Catalyst from decomposing + controlled gates (like qml.ctrl(PauliX)) into quantum.unitary operations with + explicit matrix parameters. Instead, gates remain as quantum.custom operations + that can be converted by the MQT plugin. + + Args: + device: A PennyLane device instance to configure. + + Returns: + The same device instance with modified capabilities. + + Raises: + ValueError: If the device does not have a config_filepath attribute set. + + Example: + >>> import pennylane as qml + >>> from mqt.core.plugins.catalyst.device import configure_device_for_mqt + >>> dev = qml.device("lightning.qubit", wires=2) + >>> dev = configure_device_for_mqt(dev) + >>> @qml.qnode(dev) + ... def circuit(): + ... qml.ctrl(qml.PauliX(wires=0), control=1) # Will become CNOT, not matrix + ... return qml.state() + """ + # Load the original capabilities from the device's config file + if hasattr(device, "config_filepath") and device.config_filepath is not None: + toml_file = device.config_filepath + else: + msg = "Device does not have a config_filepath attribute set." + raise ValueError(msg) + + caps = DeviceCapabilities.from_toml_file(toml_file, "qjit") + + # Remove QubitUnitary from operations to prevent matrix decomposition + if "QubitUnitary" in caps.operations: + del caps.operations["QubitUnitary"] + + # Clear _to_matrix_ops to avoid Catalyst validation at qjit_device.py:322 + # which requires QubitUnitary support if _to_matrix_ops is set + if hasattr(device, "_to_matrix_ops"): + device._to_matrix_ops = set() # noqa: SLF001 # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + + # Set the qjit_capabilities hook so QJITDevice uses our modified capabilities + # This bypasses the normal TOML loading in _load_device_capabilities + setattr(device, "qjit_capabilities", caps) # noqa: B010 + + return device + + +def get_device(device_name: str, **kwargs: Any) -> qml.devices.Device: # noqa: ANN401 + """Create and configure a PennyLane device for use with the MQT plugin. + + This is a convenience function that creates a device and automatically configures + it to work optimally with the MQT plugin, preventing unnecessary decomposition to + unitary matrices. + + Args: + device_name: The name of the PennyLane device (e.g., "lightning.qubit"). + **kwargs: Additional keyword arguments passed to qml.device(). + + Returns: + A configured PennyLane device ready for use with MQT conversion passes. + + Example: + >>> from mqt.core.plugins.catalyst.device import get_device + >>> dev = get_device("lightning.qubit", wires=2) + >>> @qml.qnode(dev) + ... def circuit(): + ... qml.ctrl(qml.PauliX(wires=0), control=1) + ... return qml.state() + """ + device = qml.device(device_name, **kwargs) + return configure_device_for_mqt(device) diff --git a/python/mqt/core/plugins/catalyst/plugin.py b/python/mqt/core/plugins/catalyst/plugin.py new file mode 100644 index 0000000..feaea6e --- /dev/null +++ b/python/mqt/core/plugins/catalyst/plugin.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Utility functions for the MQT Catalyst Plugin.""" + +import site +from importlib.resources import files +from pathlib import Path + +__all__ = ["get_catalyst_plugin_abs_path", "name2pass"] + + +def __dir__() -> list[str]: + return __all__ + + +def get_catalyst_plugin_abs_path() -> Path: + """Locate the mqt-catalyst-plugin shared library. + + Returns: + The absolute path to the plugin shared library. + + Raises: + FileNotFoundError: If the plugin library is not found. + """ + # Core library name without platform-specific extensions + plugin_lib = "mqt-core-plugins-catalyst" + + # Iterate over files in the package directory + package_path = files("mqt.core.plugins.catalyst") + for file in package_path.iterdir(): + if file.is_file() and plugin_lib in file.name: + return Path(str(file)) + + # For editable installs, search site-packages directly + site_dirs = site.getsitepackages() + user_site = site.getusersitepackages() + if user_site: + site_dirs = [user_site, *site_dirs] + for site_pkg in site_dirs: + site_pkg_dir = Path(site_pkg) / "mqt" / "core" / "plugins" / "catalyst" + if site_pkg_dir.exists(): + for file in site_pkg_dir.iterdir(): + if file.is_file() and plugin_lib in file.name: + return file + + # Provide helpful error message + msg = ( + f"Could not locate catalyst plugin library.\n" + f"Searched for files containing: {plugin_lib}\n" + f"In package directory: {package_path}\n" + f"And in site-packages: {site_dirs}\n" + f"Ensure the package is properly installed with: pip install -e ." + ) + raise FileNotFoundError(msg) + + +def name2pass(name: str) -> tuple[Path, str]: + """Convert a pass name to its plugin path and pass name (required by Catalyst). + + Args: + name: The name of the pass, e.g., "mqt-core-round-trip". + + Returns: + A tuple containing the absolute path to the plugin and the pass name. + """ + return get_catalyst_plugin_abs_path(), name diff --git a/sitecustomize.py b/sitecustomize.py new file mode 100644 index 0000000..954185c --- /dev/null +++ b/sitecustomize.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Site customization shim to enable multiprocess coverage collection in tests. + +See https://coverage.readthedocs.io/en/latest/subprocess.html. +""" + +from __future__ import annotations + +try: + import coverage + + coverage.process_startup() +except ImportError: + # The 'coverage' module is optional + # If it is not installed, we do not enable multiprocess coverage collection + pass diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index 491c444..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -# Configure lit.site.cfg.py from template -configure_lit_site_cfg( - ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in - ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py - MAIN_CONFIG - ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py - DEPENDS - ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in - ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py) - -# Dependencies needed for lit tests -set(MQT_CATALYST_PLUGIN_TEST_DEPENDS FileCheck not mqt-core-plugins-catalyst) - -# Target that just builds the dependencies (for CI build step) -add_custom_target(mqt-core-plugins-catalyst-lit-test-build-only - DEPENDS ${MQT_CATALYST_PLUGIN_TEST_DEPENDS}) -set_target_properties(mqt-core-plugins-catalyst-lit-test-build-only PROPERTIES FOLDER "Tests") - -# Target that runs the lit tests -add_lit_testsuite( - mqt-core-plugins-catalyst-lit-test "Running the mqt-core-plugins-catalyst lit tests" - ${CMAKE_CURRENT_BINARY_DIR} DEPENDS ${MQT_CATALYST_PLUGIN_TEST_DEPENDS}) -set_target_properties(mqt-core-plugins-catalyst-lit-test PROPERTIES FOLDER "Tests") diff --git a/test/Conversion/mqtopt_pauli.mlir b/test/Conversion/mqtopt_pauli.mlir deleted file mode 100644 index b6cceb8..0000000 --- a/test/Conversion/mqtopt_pauli.mlir +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -// All rights reserved. -// -// SPDX-License-Identifier: MIT -// -// Licensed under the MIT License - -// RUN: catalyst --tool=opt \ -// RUN: --load-pass-plugin=%mqt_plugin_path% \ -// RUN: --load-dialect-plugin=%mqt_plugin_path% \ -// RUN: --catalyst-pipeline="builtin.module(mqtopt-to-catalystquantum)" \ -// RUN: %s | FileCheck %s - - -// ============================================================================ -// Pauli family (X, Y, Z) and controlled variants -// Tests both static and dynamic allocation/extraction -// Groups: Allocation & extraction / Uncontrolled / Controlled / Reinsertion -// ============================================================================ -module { - // CHECK-LABEL: func.func @testMQTOptToCatalystQuantumPauliGates - func.func @testMQTOptToCatalystQuantumPauliGates(%n : i64, %idx : i64) { - // --- Dynamic allocation & extraction ------------------------------------------------------- - // CHECK: %[[SIZE_IDX:.*]] = arith.index_cast %arg0 : i64 to index - // CHECK: %[[SIZE_I64:.*]] = arith.index_cast %[[SIZE_IDX]] : index to i64 - // CHECK: %[[QREG:.*]] = quantum.alloc(%[[SIZE_I64]]) : !quantum.reg - // CHECK: %[[C0:.*]] = arith.constant 0 : index - // CHECK: %[[IDX0:.*]] = arith.index_cast %[[C0]] : index to i64 - // CHECK: %[[Q0:.*]] = quantum.extract %[[QREG]][%[[IDX0]]] : !quantum.reg -> !quantum.bit - // CHECK: %[[C1:.*]] = arith.constant 1 : index - // CHECK: %[[IDX1:.*]] = arith.index_cast %[[C1]] : index to i64 - // CHECK: %[[Q1:.*]] = quantum.extract %[[QREG]][%[[IDX1]]] : !quantum.reg -> !quantum.bit - // CHECK: %[[ARG1_IDX:.*]] = arith.index_cast %arg1 : i64 to index - // CHECK: %[[ARG1_I64:.*]] = arith.index_cast %[[ARG1_IDX]] : index to i64 - // CHECK: %[[Q2:.*]] = quantum.extract %[[QREG]][%[[ARG1_I64]]] : !quantum.reg -> !quantum.bit - - // --- Uncontrolled Pauli gates -------------------------------------------------------------- - // CHECK: %[[X:.*]] = quantum.custom "PauliX"() %[[Q0]] : !quantum.bit - // CHECK: %[[Y:.*]] = quantum.custom "PauliY"() %[[X]] : !quantum.bit - // CHECK: %[[Z:.*]] = quantum.custom "PauliZ"() %[[Y]] : !quantum.bit - // CHECK: %[[I:.*]] = quantum.custom "Identity"() %[[Z]] : !quantum.bit - - // CHECK: %[[TRUE:.*]] = arith.constant true - // CHECK: %[[CNOT_T:.*]], %[[CNOT_C:.*]] = quantum.custom "CNOT"() %[[I]] ctrls(%[[Q1]]) ctrlvals(%[[TRUE]]{{.*}}) : !quantum.bit ctrls !quantum.bit - // CHECK: %[[CY_T:.*]], %[[CY_C:.*]] = quantum.custom "CY"() %[[CNOT_T]] ctrls(%[[CNOT_C]]) ctrlvals(%[[TRUE]]{{.*}}) : !quantum.bit ctrls !quantum.bit - // CHECK: %[[CZ_T:.*]], %[[CZ_C:.*]] = quantum.custom "CZ"() %[[CY_T]] ctrls(%[[CY_C]]) ctrlvals(%[[TRUE]]{{.*}}) : !quantum.bit ctrls !quantum.bit - // CHECK: %[[I_T:.*]], %[[I_C:.*]] = quantum.custom "Identity"() %[[CZ_T]] ctrls(%[[CZ_C]]) ctrlvals(%[[TRUE]]{{.*}}) : !quantum.bit ctrls !quantum.bit - // CHECK: %[[TOF_T:.*]], %[[TOF_C:.*]]:2 = quantum.custom "Toffoli"() %[[I_T]] ctrls(%[[I_C]], %[[Q2]]) ctrlvals(%[[TRUE]]{{.*}}, %[[TRUE]]{{.*}}) : !quantum.bit ctrls !quantum.bit, !quantum.bit - - // --- Reinsertion ---------------------------------------------------------------------------- - // CHECK: %[[C0_FINAL:.*]] = arith.index_cast %[[C0]] : index to i64 - // CHECK: quantum.insert %[[QREG]][%[[C0_FINAL]]], %[[TOF_T]] : !quantum.reg, !quantum.bit - // CHECK: %[[C1_FINAL:.*]] = arith.index_cast %[[C1]] : index to i64 - // CHECK: quantum.insert %[[QREG]][%[[C1_FINAL]]], %[[TOF_C]]#0 : !quantum.reg, !quantum.bit - // CHECK: %[[ARG1_FINAL:.*]] = arith.index_cast %[[ARG1_IDX]] : index to i64 - // CHECK: quantum.insert %[[QREG]][%[[ARG1_FINAL]]], %[[TOF_C]]#1 : !quantum.reg, !quantum.bit - // CHECK: quantum.dealloc %[[QREG]] : !quantum.reg - - // Prepare qubits with dynamic allocation - %size_cast = arith.index_cast %n : i64 to index - %r0_0 = memref.alloc(%size_cast) : memref - %i0 = arith.constant 0 : index - %q0_0 = memref.load %r0_0[%i0] : memref - %i1 = arith.constant 1 : index - %q1_0 = memref.load %r0_0[%i1] : memref - %idx_cast = arith.index_cast %idx : i64 to index - %q2_0 = memref.load %r0_0[%idx_cast] : memref - - // Non-controlled Pauli gates - %q0_1 = mqtopt.x() %q0_0 : !mqtopt.Qubit - %q0_2 = mqtopt.y() %q0_1 : !mqtopt.Qubit - %q0_3 = mqtopt.z() %q0_2 : !mqtopt.Qubit - %q0_4 = mqtopt.i() %q0_3 : !mqtopt.Qubit - - // Controlled Pauli gates - %q0_5, %q1_1 = mqtopt.x() %q0_4 ctrl %q1_0 : !mqtopt.Qubit ctrl !mqtopt.Qubit - %q0_6, %q1_2 = mqtopt.y() %q0_5 ctrl %q1_1 : !mqtopt.Qubit ctrl !mqtopt.Qubit - %q0_7, %q1_3 = mqtopt.z() %q0_6 ctrl %q1_2 : !mqtopt.Qubit ctrl !mqtopt.Qubit - %q0_8, %q1_4 = mqtopt.i() %q0_7 ctrl %q1_3 : !mqtopt.Qubit ctrl !mqtopt.Qubit - %q0_9, %q1_5, %q2_1 = mqtopt.x() %q0_8 ctrl %q1_4, %q2_0 : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit - - // Release qubits - memref.store %q0_9, %r0_0[%i0] : memref - memref.store %q1_5, %r0_0[%i1] : memref - memref.store %q2_1, %r0_0[%idx_cast] : memref - memref.dealloc %r0_0 : memref - return - } -} diff --git a/test/Conversion/quantum_entangling.mlir b/test/Conversion/quantum_entangling.mlir deleted file mode 100644 index 095ce7f..0000000 --- a/test/Conversion/quantum_entangling.mlir +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -// All rights reserved. -// -// SPDX-License-Identifier: MIT -// -// Licensed under the MIT License - -// RUN: catalyst --tool=opt \ -// RUN: --load-pass-plugin=%mqt_plugin_path% \ -// RUN: --load-dialect-plugin=%mqt_plugin_path% \ -// RUN: --catalyst-pipeline="builtin.module(catalystquantum-to-mqtopt)" \ -// RUN: %s | FileCheck %s - - -// ============================================================================ -// Entangling gates (SWAP, ISWAP, ECR) and controlled variants -// Groups: Allocation & extraction / Uncontrolled / Controlled / Reinsertion -// ============================================================================ -module { - // CHECK-LABEL: func.func @testCatalystQuantumToMQTOptEntanglingGates - func.func @testCatalystQuantumToMQTOptEntanglingGates() { - // --- Allocation & extraction --------------------------------------------------------------- - // CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<3x!mqtopt.Qubit> - // CHECK: %[[C0:.*]] = arith.constant 0 : index - // CHECK: %[[Q0:.*]] = memref.load %[[ALLOC]][%[[C0]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C1:.*]] = arith.constant 1 : index - // CHECK: %[[Q1:.*]] = memref.load %[[ALLOC]][%[[C1]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C2:.*]] = arith.constant 2 : index - // CHECK: %[[Q2:.*]] = memref.load %[[ALLOC]][%[[C2]]] : memref<3x!mqtopt.Qubit> - - // --- Uncontrolled entangling gates --------------------------------------------------------- - // CHECK: %[[SW:.*]]:2 = mqtopt.swap(static [] mask []) %[[Q0]], %[[Q1]] : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[IS:.*]]:2 = mqtopt.iswap(static [] mask []) %[[SW]]#0, %[[SW]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[ISD:.*]]:2 = mqtopt.iswapdg(static [] mask []) %[[IS]]#0, %[[IS]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[ECR:.*]]:2 = mqtopt.ecr(static [] mask []) %[[ISD]]#0, %[[ISD]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - - // --- Controlled entangling gates ----------------------------------------------------------- - // CHECK: %[[CSW_T:.*]]:2, %[[CSW_C:.*]] = mqtopt.swap(static [] mask []) %[[ECR]]#0, %[[ECR]]#1 ctrl %[[Q2]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - // CHECK: %[[CISW_T:.*]]:2, %[[CISW_C:.*]] = mqtopt.iswap(static [] mask []) %[[CSW_T]]#0, %[[CSW_T]]#1 ctrl %[[CSW_C]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - // CHECK: %[[CISWD_T:.*]]:2, %[[CISWD_C:.*]] = mqtopt.iswapdg(static [] mask []) %[[CISW_T]]#0, %[[CISW_T]]#1 ctrl %[[CISW_C]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - // CHECK: %[[CECR_T:.*]]:2, %[[CECR_C:.*]] = mqtopt.ecr(static [] mask []) %[[CISWD_T]]#0, %[[CISWD_T]]#1 ctrl %[[CISWD_C]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - - // --- Reinsertion --------------------------------------------------------------------------- - // CHECK: %[[C0_FINAL:.*]] = arith.constant 0 : index - // CHECK: memref.store %[[CECR_T]]#0, %[[ALLOC]][%[[C0_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C1_FINAL:.*]] = arith.constant 1 : index - // CHECK: memref.store %[[CECR_T]]#1, %[[ALLOC]][%[[C1_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C2_FINAL:.*]] = arith.constant 2 : index - // CHECK: memref.store %[[CECR_C]], %[[ALLOC]][%[[C2_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: memref.dealloc %[[ALLOC]] : memref<3x!mqtopt.Qubit> - - // Prepare qubits - %qreg = quantum.alloc( 3) : !quantum.reg - %q0 = quantum.extract %qreg[ 0] : !quantum.reg -> !quantum.bit - %q1 = quantum.extract %qreg[ 1] : !quantum.reg -> !quantum.bit - %q2 = quantum.extract %qreg[ 2] : !quantum.reg -> !quantum.bit - - // Uncontrolled permutation gates - %q0_sw, %q1_sw = quantum.custom "SWAP"() %q0, %q1 : !quantum.bit, !quantum.bit - %q0_is, %q1_is = quantum.custom "ISWAP"() %q0_sw, %q1_sw : !quantum.bit, !quantum.bit - %q0_isd, %q1_isd = quantum.custom "ISWAP"() %q0_is, %q1_is adj : !quantum.bit, !quantum.bit - %q0_ecr, %q1_ecr = quantum.custom "ECR"() %q0_isd, %q1_isd : !quantum.bit, !quantum.bit - - // Controlled permutation gates - %true = arith.constant true - %q0_csw, %q1_csw, %q2_csw = quantum.custom "SWAP"() %q0_ecr, %q1_ecr ctrls(%q2) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - %q0_cis, %q1_cis, %q2_cis = quantum.custom "ISWAP"() %q0_csw, %q1_csw ctrls(%q2_csw) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - %q0_cisd, %q1_cisd, %q2_cisd = quantum.custom "ISWAP"() %q0_cis, %q1_cis adj ctrls(%q2_cis) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - %q0_cecr, %q1_cecr, %q2_cecr = quantum.custom "ECR"() %q0_cisd, %q1_cisd ctrls(%q2_cisd) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - - // Release qubits - %qreg1 = quantum.insert %qreg[ 0], %q0_cecr : !quantum.reg, !quantum.bit - %qreg2 = quantum.insert %qreg1[ 1], %q1_cecr : !quantum.reg, !quantum.bit - %qreg3 = quantum.insert %qreg2[ 2], %q2_cecr : !quantum.reg, !quantum.bit - quantum.dealloc %qreg3 : !quantum.reg - return - } -} diff --git a/test/Conversion/quantum_ising.mlir b/test/Conversion/quantum_ising.mlir deleted file mode 100644 index 588dd26..0000000 --- a/test/Conversion/quantum_ising.mlir +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -// All rights reserved. -// -// SPDX-License-Identifier: MIT -// -// Licensed under the MIT License - -// RUN: catalyst --tool=opt \ -// RUN: --load-pass-plugin=%mqt_plugin_path% \ -// RUN: --load-dialect-plugin=%mqt_plugin_path% \ -// RUN: --catalyst-pipeline="builtin.module(catalystquantum-to-mqtopt)" \ -// RUN: %s | FileCheck %s - - -// ============================================================================ -// Ising-type gates and controlled variants -// Tests both static and dynamic parameters in a single function -// Groups: Allocation & extraction / Uncontrolled / Controlled / Reinsertion -// ============================================================================ -module { - // CHECK-LABEL: func.func @testCatalystQuantumToMQTOptIsingGates - func.func @testCatalystQuantumToMQTOptIsingGates(%dynAngle : f64) { - // --- Allocation & extraction --------------------------------------------------------------- - // CHECK: %cst = arith.constant 3.000000e-01 : f64 - // CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<3x!mqtopt.Qubit> - // CHECK: %[[C0:.*]] = arith.constant 0 : index - // CHECK: %[[Q0:.*]] = memref.load %[[ALLOC]][%[[C0]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C1:.*]] = arith.constant 1 : index - // CHECK: %[[Q1:.*]] = memref.load %[[ALLOC]][%[[C1]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C2:.*]] = arith.constant 2 : index - // CHECK: %[[Q2:.*]] = memref.load %[[ALLOC]][%[[C2]]] : memref<3x!mqtopt.Qubit> - - // --- Uncontrolled with static parameter (converted to dynamic, except pi) ------------------------------ - // CHECK: %[[XY:.*]]:2 = mqtopt.xx_plus_yy(%cst static [3.141593e+00] mask [false, true]) %[[Q0]], %[[Q1]] : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[XX:.*]]:2 = mqtopt.rxx(%cst static [] mask [false]) %[[XY]]#0, %[[XY]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[YY:.*]]:2 = mqtopt.ryy(%cst static [] mask [false]) %[[XX]]#0, %[[XX]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[ZZ:.*]]:2 = mqtopt.rzz(%cst static [] mask [false]) %[[YY]]#0, %[[YY]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - - // --- Uncontrolled with dynamic parameter --------------------------------------------------- - // CHECK: %[[XY2:.*]]:2 = mqtopt.xx_plus_yy(%arg0 static [3.141593e+00] mask [false, true]) %[[ZZ]]#0, %[[ZZ]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - // CHECK: %[[XX2:.*]]:2 = mqtopt.rxx(%arg0 static [] mask [false]) %[[XY2]]#0, %[[XY2]]#1 : !mqtopt.Qubit, !mqtopt.Qubit - - // --- Controlled with dynamic parameter ----------------------------------------------------- - // CHECK: %[[CYY_T:.*]]:2, %[[CYY_C:.*]] = mqtopt.ryy(%arg0 static [] mask [false]) %[[XX2]]#0, %[[XX2]]#1 ctrl %[[Q2]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - // CHECK: %[[CZZ_T:.*]]:2, %[[CZZ_C:.*]] = mqtopt.rzz(%arg0 static [] mask [false]) %[[CYY_T]]#0, %[[CYY_T]]#1 ctrl %[[CYY_C]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit - - // --- Reinsertion --------------------------------------------------------------------------- - // CHECK: %[[C0_FINAL:.*]] = arith.constant 0 : index - // CHECK: memref.store %[[CZZ_T]]#0, %[[ALLOC]][%[[C0_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C1_FINAL:.*]] = arith.constant 1 : index - // CHECK: memref.store %[[CZZ_T]]#1, %[[ALLOC]][%[[C1_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: %[[C2_FINAL:.*]] = arith.constant 2 : index - // CHECK: memref.store %[[CZZ_C]], %[[ALLOC]][%[[C2_FINAL]]] : memref<3x!mqtopt.Qubit> - // CHECK: memref.dealloc %[[ALLOC]] : memref<3x!mqtopt.Qubit> - - // Prepare qubits - %staticAngle = arith.constant 3.000000e-01 : f64 - %qreg = quantum.alloc( 3) : !quantum.reg - %q0 = quantum.extract %qreg[ 0] : !quantum.reg -> !quantum.bit - %q1 = quantum.extract %qreg[ 1] : !quantum.reg -> !quantum.bit - %q2 = quantum.extract %qreg[ 2] : !quantum.reg -> !quantum.bit - - // Uncontrolled Ising gates with static parameter - %q0_xy, %q1_xy = quantum.custom "IsingXY"(%staticAngle) %q0, %q1 : !quantum.bit, !quantum.bit - %q0_xx, %q1_xx = quantum.custom "IsingXX"(%staticAngle) %q0_xy, %q1_xy : !quantum.bit, !quantum.bit - %q0_yy, %q1_yy = quantum.custom "IsingYY"(%staticAngle) %q0_xx, %q1_xx : !quantum.bit, !quantum.bit - %q0_zz, %q1_zz = quantum.custom "IsingZZ"(%staticAngle) %q0_yy, %q1_yy : !quantum.bit, !quantum.bit - - // Uncontrolled Ising gates with dynamic parameter - %q0_xy2, %q1_xy2 = quantum.custom "IsingXY"(%dynAngle) %q0_zz, %q1_zz : !quantum.bit, !quantum.bit - %q0_xx2, %q1_xx2 = quantum.custom "IsingXX"(%dynAngle) %q0_xy2, %q1_xy2 : !quantum.bit, !quantum.bit - - // Controlled Ising gates with dynamic parameter - %true = arith.constant true - %q0_cyy, %q1_cyy, %q2_cyy = quantum.custom "IsingYY"(%dynAngle) %q0_xx2, %q1_xx2 ctrls(%q2) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - %q0_czz, %q1_czz, %q2_czz = quantum.custom "IsingZZ"(%dynAngle) %q0_cyy, %q1_cyy ctrls(%q2_cyy) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit - - // Release qubits - %qreg1 = quantum.insert %qreg[ 0], %q0_czz : !quantum.reg, !quantum.bit - %qreg2 = quantum.insert %qreg1[ 1], %q1_czz : !quantum.reg, !quantum.bit - %qreg3 = quantum.insert %qreg2[ 2], %q2_czz : !quantum.reg, !quantum.bit - quantum.dealloc %qreg3 : !quantum.reg - return - } -} diff --git a/test/Conversion/quantum_param.mlir b/test/Conversion/quantum_param.mlir deleted file mode 100644 index f0f567e..0000000 --- a/test/Conversion/quantum_param.mlir +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -// All rights reserved. -// -// SPDX-License-Identifier: MIT -// -// Licensed under the MIT License - -// RUN: catalyst --tool=opt \ -// RUN: --load-pass-plugin=%mqt_plugin_path% \ -// RUN: --load-dialect-plugin=%mqt_plugin_path% \ -// RUN: --catalyst-pipeline="builtin.module(catalystquantum-to-mqtopt)" \ -// RUN: %s | FileCheck %s - - -// ============================================================================ -// Parameterized gates RX/RY/RZ, PhaseShift and controlled variants -// Tests both static (compile-time constant) and dynamic (runtime) parameters -// Groups: Allocation & extraction / Uncontrolled / Controlled / Reinsertion -// ============================================================================ -module { - // CHECK-LABEL: func.func @testCatalystQuantumToMQTOptParameterizedGates - func.func @testCatalystQuantumToMQTOptParameterizedGates(%dynAngle : f64) { - // --- Static allocation & extraction --------------------------------------------------------------- - // CHECK: %[[ALLOC:.*]] = memref.alloc() : memref<2x!mqtopt.Qubit> - // CHECK: %[[C0:.*]] = arith.constant 0 : index - // CHECK: %[[Q0:.*]] = memref.load %[[ALLOC]][%[[C0]]] : memref<2x!mqtopt.Qubit> - // CHECK: %[[C1:.*]] = arith.constant 1 : index - // CHECK: %[[Q1:.*]] = memref.load %[[ALLOC]][%[[C1]]] : memref<2x!mqtopt.Qubit> - - // --- Uncontrolled with static parameter (converted to dynamic) --------------------------------------------------- - // CHECK: %[[RX:.*]] = mqtopt.rx(%cst static [] mask [false]) %[[Q0]] : !mqtopt.Qubit - // CHECK: %[[RY:.*]] = mqtopt.ry(%cst static [] mask [false]) %[[RX]] : !mqtopt.Qubit - // CHECK: %[[RZ:.*]] = mqtopt.rz(%cst static [] mask [false]) %[[RY]] : !mqtopt.Qubit - // CHECK: %[[PS:.*]] = mqtopt.p(%cst static [] mask [false]) %[[RZ]] : !mqtopt.Qubit - - // --- Controlled with static parameter (converted to dynamic) ------------------------------------------------------ - // CHECK: %[[CRX_T:.*]], %[[CRX_C:.*]] = mqtopt.rx(%cst static [] mask [false]) %[[PS]] ctrl %[[Q1]] : !mqtopt.Qubit ctrl !mqtopt.Qubit - // CHECK: %[[CRY_T:.*]], %[[CRY_C:.*]] = mqtopt.ry(%cst static [] mask [false]) %[[CRX_T]] ctrl %[[CRX_C]] : !mqtopt.Qubit ctrl !mqtopt.Qubit - - // --- Uncontrolled with dynamic parameter ------------------------------------------------------------------------- - // CHECK: %[[RX2:.*]] = mqtopt.rx(%arg0 static [] mask [false]) %[[CRY_T]] : !mqtopt.Qubit - // CHECK: %[[RY2:.*]] = mqtopt.ry(%arg0 static [] mask [false]) %[[RX2]] : !mqtopt.Qubit - - // --- Controlled with dynamic parameter ---------------------------------------------------------------------------- - // CHECK: %[[CRZ_T:.*]], %[[CRZ_C:.*]] = mqtopt.rz(%arg0 static [] mask [false]) %[[RY2]] ctrl %[[CRY_C]] : !mqtopt.Qubit ctrl !mqtopt.Qubit - - // --- Reinsertion --------------------------------------------------------------------------- - // CHECK: %[[C0_FINAL:.*]] = arith.constant 0 : index - // CHECK: memref.store %[[CRZ_T]], %[[ALLOC]][%[[C0_FINAL]]] : memref<2x!mqtopt.Qubit> - // CHECK: %[[C1_FINAL:.*]] = arith.constant 1 : index - // CHECK: memref.store %[[CRZ_C]], %[[ALLOC]][%[[C1_FINAL]]] : memref<2x!mqtopt.Qubit> - // CHECK: memref.dealloc %[[ALLOC]] : memref<2x!mqtopt.Qubit> - - // Prepare qubits - %staticAngle = arith.constant 3.000000e-01 : f64 - %qreg = quantum.alloc( 2) : !quantum.reg - %q0 = quantum.extract %qreg[ 0] : !quantum.reg -> !quantum.bit - %q1 = quantum.extract %qreg[ 1] : !quantum.reg -> !quantum.bit - - // Uncontrolled parameterized gates with static parameter - %q0_rx = quantum.custom "RX"(%staticAngle) %q0 : !quantum.bit - %q0_ry = quantum.custom "RY"(%staticAngle) %q0_rx : !quantum.bit - %q0_rz = quantum.custom "RZ"(%staticAngle) %q0_ry : !quantum.bit - %q0_p = quantum.custom "PhaseShift"(%staticAngle) %q0_rz : !quantum.bit - - // Controlled parameterized gates with static parameter - %true = arith.constant true - %q0_crx, %q1_crx = quantum.custom "RX"(%staticAngle) %q0_p ctrls(%q1) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit - %q0_cry, %q1_cry = quantum.custom "RY"(%staticAngle) %q0_crx ctrls(%q1_crx) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit - - // Uncontrolled parameterized gates with dynamic parameter - %q0_rx2 = quantum.custom "RX"(%dynAngle) %q0_cry : !quantum.bit - %q0_ry2 = quantum.custom "RY"(%dynAngle) %q0_rx2 : !quantum.bit - - // Controlled parameterized gates with dynamic parameter - %q0_crz, %q1_crz = quantum.custom "RZ"(%dynAngle) %q0_ry2 ctrls(%q1_cry) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit - - // Release qubits - %qreg1 = quantum.insert %qreg[ 0], %q0_crz : !quantum.reg, !quantum.bit - %qreg2 = quantum.insert %qreg1[ 1], %q1_crz : !quantum.reg, !quantum.bit - quantum.dealloc %qreg2 : !quantum.reg - return - } -} diff --git a/test/lit.cfg.py b/test/lit.cfg.py deleted file mode 100644 index 38c0151..0000000 --- a/test/lit.cfg.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -"""LIT Configuration file for mqt-core-plugins-catalyst. - -This file configures the LLVM LIT testing infrastructure for MLIR tests. - -Note: `config` and `lit_config` are injected by LIT at runtime. -""" - -from __future__ import annotations - -from pathlib import Path - -import lit.formats -from lit.llvm import llvm_config - -# Use `lit_config` to access `config` from lit.site.cfg.py -config = globals().get("config") -if config is None: - msg = "LIT config object is missing. Ensure lit.site.cfg.py is loaded first." - raise RuntimeError(msg) - -config.name = "MQT Catalyst Plugin Lit Tests" -config.test_format = lit.formats.ShTest(execute_external=False) - -# Define the file extensions to treat as test files. -config.suffixes = [".mlir"] - -# Define the root path of where to look for tests. -config.test_source_root = Path(__file__).parent - -# Define where to execute tests (and produce the output). -config.test_exec_root = Path(config.mqt_core_plugins_catalyst_test_dir) - -# Add LLVM tools (FileCheck, not, etc.) -tool_dirs = [config.llvm_tools_dir] -tools = ["not", "FileCheck"] -llvm_config.add_tool_substitutions(tools, tool_dirs) - -# Add substitution for the MQT plugin path -config.substitutions.append(("%mqt_plugin_path%", config.mqt_plugin_path)) diff --git a/test/lit.site.cfg.py.in b/test/lit.site.cfg.py.in deleted file mode 100644 index 1a2d0c9..0000000 --- a/test/lit.site.cfg.py.in +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -@LIT_SITE_CFG_IN_HEADER@ - -config.llvm_tools_dir = lit_config.substitute("@LLVM_TOOLS_DIR@") -config.cmake_build_type = "@CMAKE_BUILD_TYPE@" -config.mqt_core_plugins_catalyst_test_dir = "@PROJECT_BINARY_DIR@/test" -config.mqt_plugin_path = "@PROJECT_BINARY_DIR@/lib/mqt-core-plugins-catalyst@CMAKE_SHARED_LIBRARY_SUFFIX@" - -import lit.llvm -lit.llvm.initialize(lit_config, config) - -# Let the main config do the real work. -lit_config.load_config(config, "@PROJECT_SOURCE_DIR@/test/lit.cfg.py") diff --git a/test/python/test_placeholder.py b/test/python/test_placeholder.py deleted file mode 100644 index 2d3490e..0000000 --- a/test/python/test_placeholder.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -"""Test module placeholder. - -This module provides a single trivial test. -""" - - -def test_placeholder() -> None: - """A trivial test that always passes. - - It exists only to satisfy automated checks until real tests are added. - """ - assert True diff --git a/test/test_plugin.py b/test/test_plugin.py new file mode 100644 index 0000000..550627f --- /dev/null +++ b/test/test_plugin.py @@ -0,0 +1,964 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for MQT plugin execution with PennyLane and Catalyst. + +These tests check that the MQT plugin conversion passes execute successfully +for various gate categories, mirroring the MLIR conversion tests. They verify +that the full lossless roundtrip (CatalystQuantum โ†’ MQTOpt โ†’ CatalystQuantum) +works correctly. The tests use FileCheck (from LLVM) to verify the generated MLIR output. + +Environment Variables: + FILECHECK_PATH: Optional path to FileCheck binary if not in PATH +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pennylane as qml +import pytest +from catalyst.passes import apply_pass + +from mqt.core.plugins.catalyst import get_device + +if TYPE_CHECKING: + from collections.abc import Generator + + +@pytest.fixture(autouse=True) +def _cleanup_mlir_files() -> Generator[None, None, None]: + """Clean up MLIR files before and after each test to ensure test isolation. + + Yields: + None + """ + _cleanup_mlir_artifacts() + yield + _cleanup_mlir_artifacts() + + +def _cleanup_mlir_artifacts() -> None: + """Clean up MLIR intermediate files and directories created by Catalyst. + + Catalyst creates module_N directories when keep_intermediate is used. + This function removes all such directories to prevent accumulation. + """ + mlir_dir = Path.cwd() + # Remove all module_N directories + for module_dir in mlir_dir.glob("module_*"): + if module_dir.is_dir(): + shutil.rmtree(module_dir) + # Remove any loose .mlir files + for mlir_file in mlir_dir.glob("*.mlir"): + mlir_file.unlink() + + +@lru_cache(maxsize=1) +def _get_filecheck() -> str | None: + """Locate the FileCheck binary. Caches the result for future calls. + + Returns: + The path to the FileCheck binary, or None if not found. + """ + return _find_filecheck() + + +def _find_filecheck() -> str | None: + candidates: list[Path] = [] + + # 1. Explicit override (if CI ever sets it) + if os.environ.get("FILECHECK_PATH"): + candidates.append(Path(os.environ["FILECHECK_PATH"])) + + # 2. LLVM install prefix (best signal) + if os.environ.get("LLVM_INSTALL_PREFIX"): + candidates.append(Path(os.environ["LLVM_INSTALL_PREFIX"]) / "bin" / "FileCheck") + + # 3. LLVM_ROOT (common in setup-mlir) + if os.environ.get("LLVM_ROOT"): + candidates.append(Path(os.environ["LLVM_ROOT"]) / "bin" / "FileCheck") + + # 4. CMake-style LLVM_DIR + if os.environ.get("LLVM_DIR"): + candidates.append(Path(os.environ["LLVM_DIR"]) / ".." / ".." / ".." / "bin" / "FileCheck") + + # 5. CMake-style MLIR_DIR + if os.environ.get("MLIR_DIR"): + candidates.append(Path(os.environ["MLIR_DIR"]) / ".." / ".." / ".." / "bin" / "FileCheck") + + # 6. PATH (last resort) + path_hit = shutil.which("FileCheck") + if path_hit: + candidates.append(Path(path_hit)) + + for c in candidates: + if c.exists() and c.is_file(): + return str(c.resolve()) + + return None + + +def _run_filecheck( + mlir_content: str, + check_patterns: str, + test_name: str = "test", +) -> None: + """Run FileCheck on MLIR content using CHECK patterns from a string. + + Args: + mlir_content: The MLIR output to verify + check_patterns: String containing FileCheck directives (lines starting with // CHECK) + test_name: Name of the test (for error messages) + + Raises: + RuntimeError: If FileCheck is not found + AssertionError: If FileCheck validation fails + """ + filecheck = _get_filecheck() + + if filecheck is None: + msg = ( + "FileCheck not found.\n" + "Tried FILECHECK_PATH, LLVM_INSTALL_PREFIX, LLVM_ROOT, " + "LLVM_DIR, MLIR_DIR, and PATH.\n" + "Ensure LLVM is available or disable FileCheck-based tests." + ) + raise RuntimeError(msg) + + with tempfile.NamedTemporaryFile( + encoding="utf-8", + mode="w", + suffix=".mlir", + delete=False, + ) as check_file: + check_file.write(check_patterns) + check_file_path = check_file.name + + try: + result = subprocess.run( # noqa: S603 + [filecheck, check_file_path, "--allow-unused-prefixes"], + input=mlir_content.encode(), + capture_output=True, + check=False, + timeout=30, + ) + + if result.returncode != 0: + stderr = result.stderr.decode(errors="replace") if result.stderr else "Unknown error" + msg = ( + f"FileCheck failed for {test_name}:\n" + f"{stderr}\n\n" + f"MLIR Output (first 2000 chars):\n" + f"{mlir_content[:2000]}..." + ) + raise AssertionError(msg) + finally: + Path(check_file_path).unlink() + + +def test_paulix_roundtrip() -> None: + """Test roundtrip conversion of the PauliX gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + # Non-controlled + qml.X(wires=0) + qml.PauliX(wires=0) + # Controlled + qml.ctrl(qml.PauliX(wires=0), control=1) + qml.CNOT(wires=[1, 0]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + # Verify the roundtrip completes successfully + mlir_opt = module.mlir_opt + assert mlir_opt + + # Find where MLIR files are generated (relative to cwd where pytest is run) + # Catalyst generates MLIR files in the current working directory + mlir_dir = Path.cwd() + + # Read the intermediate MLIR files + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + # Verify original CatalystQuantum + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliX"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliX"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CNOT"() %[[Q1_0:.*]], %[[Q0_1:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CNOT"() %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "PauliX: CatalystQuantum") + + # Verify CatalystQuantum โ†’ MQTOpt conversion + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.x(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]] = mqtopt.x(static [] mask []) %[[Q0_1:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = mqtopt.x(static [] mask []) %[[Q0_2:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = mqtopt.x(static [] mask []) %[[Q0_3:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "PauliX: CatalystQuantum to MQTOpt") + + # Verify MQTOpt โ†’ CatalystQuantum conversion + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliX"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliX"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "CNOT"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = quantum.custom "CNOT"() %[[Q0_3:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_6) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "PauliX: MQTOpt to CatalystQuantum") + + +def test_pauliy_roundtrip() -> None: + """Test roundtrip conversion of the PauliY gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + # Non-controlled + qml.Y(wires=0) + qml.PauliY(wires=0) + # Controlled + qml.ctrl(qml.PauliY(wires=0), control=1) + qml.CY(wires=[1, 0]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + # Verify the roundtrip completes successfully + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + # Verify original CatalystQuantum + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliY"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliY"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CY"() %[[Q1_0:.*]], %[[Q0_2:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CY"() %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "PauliY: CatalystQuantum") + + # Verify CatalystQuantum โ†’ MQTOpt conversion + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.y(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]] = mqtopt.y(static [] mask []) %[[Q0_1:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = mqtopt.y(static [] mask []) %[[Q0_2:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = mqtopt.y(static [] mask []) %[[Q0_3:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "PauliY: CatalystQuantum to MQTOpt") + + # Verify MQTOpt โ†’ CatalystQuantum conversion + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliY"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliY"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "CY"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = quantum.custom "CY"() %[[Q0_3:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_6) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "PauliY: MQTOpt to CatalystQuantum") + + +def test_pauliz_roundtrip() -> None: + """Test roundtrip conversion of the PauliZ gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + # Non-controlled + qml.Z(wires=0) + qml.PauliZ(wires=0) + # Controlled + qml.ctrl(qml.PauliZ(wires=0), control=1) + qml.CZ(wires=[1, 0]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + # Verify the roundtrip completes successfully + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + # Verify original CatalystQuantum + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliZ"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliZ"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CZ"() %[[Q1_0:.*]], %[[Q0_2:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CZ"() %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "PauliZ: CatalystQuantum") + + # Verify CatalystQuantum โ†’ MQTOpt conversion + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.z(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]] = mqtopt.z(static [] mask []) %[[Q0_1:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = mqtopt.z(static [] mask []) %[[Q0_2:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = mqtopt.z(static [] mask []) %[[Q0_3:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "PauliZ: CatalystQuantum to MQTOpt") + + # Verify MQTOpt โ†’ CatalystQuantum conversion + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PauliZ"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "PauliZ"() %[[Q0_1:.*]] : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "CZ"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q1_2:.*]] = quantum.custom "CZ"() %[[Q0_3:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_6) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "PauliZ: MQTOpt to CatalystQuantum") + + +def test_hadamard_roundtrip() -> None: + """Test roundtrip conversion of the Hadamard gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.Hadamard(wires=0) + qml.ctrl(qml.Hadamard(wires=0), control=1) + qml.CH(wires=[1, 0]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "Hadamard"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "Hadamard"() %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%extracted_5) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "Hadamard"() %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%extracted_7) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "Hadamard: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.h(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = mqtopt.h(static [] mask []) %[[Q0_1:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = mqtopt.h(static [] mask []) %[[Q0_2:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "Hadamard: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "Hadamard"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "Hadamard"() %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "Hadamard"() %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_8) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "Hadamard: MQTOpt to CatalystQuantum") + + +def test_s_gate_roundtrip() -> None: + """Test roundtrip conversion of the S gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.S(wires=0) + qml.adjoint(qml.S(wires=0)) + qml.ctrl(qml.S(wires=0), control=1) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "S"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "S"() %[[Q0_1:.*]] adj : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "S"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%extracted_6) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "S: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.s(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]] = mqtopt.sdg(static [] mask []) %[[Q0_1:.*]] : !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "S: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "S"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "S"() %[[Q0_1:.*]] adj : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "S"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "S: MQTOpt to CatalystQuantum") + + +def test_t_gate_roundtrip() -> None: + """Test roundtrip conversion of the T gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.T(wires=0) + qml.adjoint(qml.T(wires=0)) + qml.ctrl(qml.T(wires=0), control=1) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "T"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "T"() %[[Q0_1:.*]] adj : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "T"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%extracted_6) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "T: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.t(static [] mask []) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]] = mqtopt.tdg(static [] mask []) %[[Q0_1:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = mqtopt.t(static [] mask []) %[[Q0_2:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "T: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "T"() %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]] = quantum.custom "T"() %[[Q0_1:.*]] adj : !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_1:.*]] = quantum.custom "T"() %[[Q0_2:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "T: MQTOpt to CatalystQuantum") + + +def test_rx_gate_roundtrip() -> None: + """Test roundtrip conversion of the RX gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=3)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.RX(0.5, wires=0) + qml.ctrl(qml.RX(0.5, wires=0), control=1) + qml.CRX(0.5, wires=[1, 0]) + qml.ctrl(qml.CRX(0.5, wires=[1, 0]), control=2) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RX"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRX"({{.*}}) %[[Q1_0:.*]], %[[Q0_1:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRX"(%extracted_7) %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q21_0:.*]]:2 = quantum.custom "RX"(%extracted_12) %[[Q10_0:.*]]#1 ctrls(%3, %[[Q10_0:.*]]#0) ctrlvals(%extracted_13, %extracted_14) : !quantum.bit ctrls !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "RX: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.rx({{.*}}) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = mqtopt.rx({{.*}}) %[[Q0_1:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = mqtopt.rx({{.*}}) %[[Q0_2:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q12:.*]]:2 = mqtopt.rx(%[[THETA:.*]] static [] mask [false]) %[[Q0_3:.*]] ctrl %[[Q1_2:.*]], %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit""" + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "RX: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RX"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "CRX"({{.*}}) %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals([[TRUE0:.*]]) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "CRX"(%extracted_7) %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals([[TRUE0:.*]]) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q12_3:.*]]:2 = quantum.custom "RX"(%[[THETA:.*]]) %[[Q0_3:.*]] ctrls(%[[Q1_2:.*]], %[[Q1_1:.*]]) ctrlvals(%[[TRUE0:.*]], %[[TRUE1:.*]]) : !quantum.bit ctrls !quantum.bit, !quantum.bit """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "RX: MQTOpt to CatalystQuantum") + + +def test_ry_gate_roundtrip() -> None: + """Test roundtrip conversion of the RY gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=3)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.RY(0.5, wires=0) + qml.ctrl(qml.RY(0.5, wires=0), control=1) + qml.CRY(0.5, wires=[1, 0]) + qml.ctrl(qml.CRY(0.5, wires=[1, 0]), control=2) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RY"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRY"({{.*}}) %[[Q1_0:.*]], %[[Q0_1:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRY"(%extracted_7) %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q21_0:.*]]:2 = quantum.custom "RY"(%extracted_12) %[[Q10_0:.*]]#1 ctrls(%3, %[[Q10_0:.*]]#0) ctrlvals(%extracted_13, %extracted_14) : !quantum.bit ctrls !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "RY: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.ry({{.*}}) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = mqtopt.ry({{.*}}) %[[Q0_1:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = mqtopt.ry(%extracted_7 static [] mask [false]) %[[Q0_2:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q12:.*]]:2 = mqtopt.ry(%[[THETA:.*]] static [] mask [false]) %[[Q0_3:.*]] ctrl %[[Q1_2:.*]], %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "RY: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RY"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "CRY"({{.*}}) %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "CRY"(%extracted_7) %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_8) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q12_3:.*]]:2 = quantum.custom "RY"(%[[THETA:.*]]) %[[Q0_3:.*]] ctrls(%[[Q1_2:.*]], %[[Q1_1:.*]]) ctrlvals(%[[TRUE0:.*]], %[[TRUE1:.*]]) : !quantum.bit ctrls !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "RY: MQTOpt to CatalystQuantum") + + +def test_rz_gate_roundtrip() -> None: + """Test roundtrip conversion of the RZ gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=3)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.RZ(0.5, wires=0) + qml.ctrl(qml.RZ(0.5, wires=0), control=1) + qml.CRZ(0.5, wires=[1, 0]) + qml.ctrl(qml.CRZ(0.5, wires=[1, 0]), control=2) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RZ"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRZ"({{.*}}) %[[Q1_0:.*]], %[[Q0_1:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "CRZ"(%extracted_7) %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q21_0:.*]]:2 = quantum.custom "RZ"(%extracted_12) %[[Q10_0:.*]]#1 ctrls(%3, %[[Q10_0:.*]]#0) ctrlvals(%extracted_13, %extracted_14) : !quantum.bit ctrls !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "RZ: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.rz({{.*}}) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = mqtopt.rz({{.*}}) %[[Q0_1:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = mqtopt.rz(%extracted_7 static [] mask [false]) %[[Q0_2:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_4:.*]], %[[Q12:.*]]:2 = mqtopt.rz(%[[THETA:.*]] static [] mask [false]) %[[Q0_3:.*]] ctrl %[[Q1_2:.*]], %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "RZ: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "RZ"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "CRZ"({{.*}}) %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "CRZ"(%extracted_7) %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_8) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_4:.*]], %[[Q12_3:.*]]:2 = quantum.custom "RZ"(%[[THETA:.*]]) %[[Q0_3:.*]] ctrls(%[[Q1_2:.*]], %[[Q1_1:.*]]) ctrlvals(%[[TRUE0:.*]], %[[TRUE1:.*]]) : !quantum.bit ctrls !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "RZ: MQTOpt to CatalystQuantum") + + +def test_phaseshift_gate_roundtrip() -> None: + """Test roundtrip conversion of the PhaseShift gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=2)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.PhaseShift(0.5, wires=0) + qml.ctrl(qml.PhaseShift(0.5, wires=0), control=1) + qml.ControlledPhaseShift(0.5, wires=[1, 0]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PhaseShift"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "ControlledPhaseShift"({{.*}}) %[[Q1_0:.*]], %[[Q0_1:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q10_0:.*]]:2 = quantum.custom "ControlledPhaseShift"(%extracted_7) %[[Q10_0:.*]]#0, %[[Q10_0:.*]]#1 : !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "PhaseShift: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q0_1:.*]] = mqtopt.p({{.*}}) %[[Q0_0:.*]] : !mqtopt.Qubit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = mqtopt.p({{.*}}) %[[Q0_1:.*]] ctrl %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = mqtopt.p({{.*}} static [] mask [false]) %[[Q0_2:.*]] ctrl %[[Q1_1:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "PhaseShift: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q0_1:.*]] = quantum.custom "PhaseShift"({{.*}}) %[[Q0_0:.*]] : !quantum.bit + //CHECK: %[[Q0_2:.*]], %[[Q1_1:.*]] = quantum.custom "ControlledPhaseShift"({{.*}}) %[[Q0_1:.*]] ctrls(%[[Q1_0:.*]]) ctrlvals(%true) : !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q0_3:.*]], %[[Q1_2:.*]] = quantum.custom "ControlledPhaseShift"({{.*}}) %[[Q0_2:.*]] ctrls(%[[Q1_1:.*]]) ctrlvals(%true_8) : !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "PhaseShift: MQTOpt to CatalystQuantum") + + +def test_swap_gate_roundtrip() -> None: + """Test roundtrip conversion of the SWAP gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=3)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.SWAP(wires=[0, 1]) + qml.ctrl(qml.SWAP(wires=[0, 1]), control=2) + qml.CSWAP(wires=[2, 0, 1]) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q01_0:.*]]:2 = quantum.custom "SWAP"() %[[Q0_0:.*]], %[[Q1_0:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q201_0:.*]]:3 = quantum.custom "CSWAP"() %[[Q2_0:.*]], %[[Q01_0:.*]]#0, %[[Q01_0:.*]]#1 : !quantum.bit, !quantum.bit, !quantum.bit + //CHECK: %[[Q201_0:.*]]:3 = quantum.custom "CSWAP"() %[[Q201_0:.*]]#0, %[[Q201_0:.*]]#1, %[[Q201_0:.*]]#2 : !quantum.bit, !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "SWAP: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q01_1:.*]]:2 = mqtopt.swap(static [] mask []) %[[Q0_0:.*]], %[[Q1_0:.*]] : !mqtopt.Qubit, !mqtopt.Qubit + //CHECK: %[[Q01_2:.*]]:2, %[[Q2_1:.*]] = mqtopt.swap(static [] mask []) %[[Q01_1:.*]]#0, %[[Q01_1:.*]]#1 ctrl %[[Q2_0:.*]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit + //CHECK: %[[Q01_3:.*]]:2, %[[Q2_2:.*]] = mqtopt.swap(static [] mask []) %[[Q01_2:.*]]#0, %[[Q01_2:.*]]#1 ctrl %[[Q2_1:.*]] : !mqtopt.Qubit, !mqtopt.Qubit ctrl !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "SWAP: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q01_1:.*]]:2 = quantum.custom "SWAP"() %[[Q0_0:.*]], %[[Q1_0:.*]] : !quantum.bit, !quantum.bit + //CHECK: %[[Q01_2:.*]]:2, %[[Q2_1:.*]] = quantum.custom "CSWAP"() %[[Q01_1:.*]]#0, %[[Q01_1:.*]]#1 ctrls(%[[Q2_0:.*]]) ctrlvals(%true) : !quantum.bit, !quantum.bit ctrls !quantum.bit + //CHECK: %[[Q01_3:.*]]:2, %[[Q2_2:.*]] = quantum.custom "CSWAP"() %[[Q01_2:.*]]#0, %[[Q01_2:.*]]#1 ctrls(%[[Q2_1:.*]]) ctrlvals(%true_7) : !quantum.bit, !quantum.bit ctrls !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "SWAP: MQTOpt to CatalystQuantum") + + +def test_toffoli_gate_roundtrip() -> None: + """Test roundtrip conversion of the Toffoli gate. + + Raises: + FileNotFoundError: If intermediate MLIR files are not found + """ + + @apply_pass("mqt.mqtopt-to-catalystquantum") # type: ignore[untyped-decorator] + @apply_pass("mqt.catalystquantum-to-mqtopt") # type: ignore[untyped-decorator] + @qml.qnode(get_device("lightning.qubit", wires=4)) # type: ignore[untyped-decorator] + def circuit() -> None: + qml.Toffoli(wires=[0, 1, 2]) + qml.ctrl(qml.Toffoli(wires=[0, 1, 2]), control=3) + + custom_pipeline = [ + ("to-mqtopt", ["builtin.module(catalystquantum-to-mqtopt)"]), + ("to-catalystquantum", ["builtin.module(mqtopt-to-catalystquantum)"]), + ] + + @qml.qjit(target="mlir", pipelines=custom_pipeline, autograph=True, keep_intermediate=2) # type: ignore[untyped-decorator] + def module() -> Any: # noqa: ANN401 + return circuit() + + mlir_opt = module.mlir_opt + assert mlir_opt + + mlir_dir = Path.cwd() + catalyst_mlir = mlir_dir / "0_catalyst_module.mlir" + mlir_to_mqtopt = mlir_dir / "1_CatalystQuantumToMQTOpt.mlir" + mlir_to_catalyst = mlir_dir / "4_MQTOptToCatalystQuantum.mlir" + + if not catalyst_mlir.exists() or not mlir_to_mqtopt.exists() or not mlir_to_catalyst.exists(): + available_files = list(mlir_dir.glob("*.mlir")) + msg = f"Expected MLIR files not found in {mlir_dir}.\nAvailable files: {[f.name for f in available_files]}" + raise FileNotFoundError(msg) + + mlir_before = Path(catalyst_mlir).read_text(encoding="utf-8") + mlir_after_mqtopt = Path(mlir_to_mqtopt).read_text(encoding="utf-8") + mlir_after_roundtrip = Path(mlir_to_catalyst).read_text(encoding="utf-8") + + check_mlir_before = """ + //CHECK: %[[Q012_0:.*]]:3 = quantum.custom "Toffoli"() %[[Q0_0:.*]], %[[Q1_0:.*]], %[[Q2_0:.*]] : !quantum.bit, !quantum.bit, !quantum.bit + //CHECK: %[[Q2_1:.*]], %[[Q301_0:.*]]:3 = quantum.custom "PauliX"() %[[Q012_0:.*]]#2 ctrls(%[[Q3_0:.*]], %[[Q012_0:.*]]#0, %[[Q012_0:.*]]#1) ctrlvals(%extracted_9, %extracted_10, %extracted_11) : !quantum.bit ctrls !quantum.bit, !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_before, check_mlir_before, "Toffoli: CatalystQuantum") + + check_after_mqtopt = """ + //CHECK: %[[Q2_1:.*]], %[[Q01_1:.*]]:2 = mqtopt.x(static [] mask []) %[[Q2_0:.*]] ctrl %[[Q0_0:.*]], %[[Q1_0:.*]] : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit + //CHECK: %[[Q2_2:.*]], %[[Q301_1:.*]]:3 = mqtopt.x(static [] mask []) %[[Q2_1:.*]] ctrl %[[Q3_0:.*]], %[[Q01_1:.*]]#0, %[[Q01_1:.*]]#1 : !mqtopt.Qubit ctrl !mqtopt.Qubit, !mqtopt.Qubit, !mqtopt.Qubit + """ + _run_filecheck(mlir_after_mqtopt, check_after_mqtopt, "Toffoli: CatalystQuantum to MQTOpt") + + check_after_catalyst = """ + //CHECK: %[[Q2_1:.*]], %[[Q01_1:.*]]:2 = quantum.custom "Toffoli"() %[[Q2_0:.*]] ctrls(%[[Q0_0:.*]], %[[Q1_0:.*]]) ctrlvals(%true, %true) : !quantum.bit ctrls !quantum.bit, !quantum.bit + //CHECK: %[[Q2_2:.*]], %[[Q301_1:.*]]:3 = quantum.custom "PauliX"() %[[Q2_1:.*]] ctrls(%[[Q3_0:.*]], %[[Q01_1:.*]]#0, %[[Q01_1:.*]]#1) ctrlvals(%true_12, %true_12, %true_12) : !quantum.bit ctrls !quantum.bit, !quantum.bit, !quantum.bit + """ + _run_filecheck(mlir_after_roundtrip, check_after_catalyst, "Toffoli: MQTOpt to CatalystQuantum") diff --git a/test/test_plugin_setup.py b/test/test_plugin_setup.py new file mode 100644 index 0000000..ca4f380 --- /dev/null +++ b/test/test_plugin_setup.py @@ -0,0 +1,132 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for MQT plugin setup with PennyLane and Catalyst. + +These tests only check that the MQT plugin is correctly installed and +can be used in various ways with PennyLane (they do NOT execute any pass). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import MagicMock, patch + +import pennylane as qml +import pytest +from catalyst import pipeline +from catalyst.passes import apply_pass, apply_pass_plugin + +from mqt.core.plugins.catalyst import configure_device_for_mqt, get_catalyst_plugin_abs_path, get_device + +if TYPE_CHECKING: + from pennylane.measurements.state import StateMP + + +def test_mqt_plugin() -> None: + """Generate MLIR for the MQT plugin. + + Does not execute the pass. + """ + plugin_path = str(get_catalyst_plugin_abs_path()) + + @apply_pass("mqt-core-round-trip") # type: ignore[untyped-decorator] + @qml.qnode(qml.device("null.qubit", wires=0)) # type: ignore[untyped-decorator] + def qnode() -> StateMP: + return qml.state() + + @qml.qjit(pass_plugins={plugin_path}, dialect_plugins={plugin_path}, target="mlir") # type: ignore[untyped-decorator] + def module() -> StateMP: + return qnode() + + assert "mqt-core-round-trip" in module.mlir + + +def test_mqt_plugin_no_preregistration() -> None: + """Generate MLIR for the MQT plugin. + + No need to register the plugin ahead of time in the qjit decorator. + """ + plugin_path = str(get_catalyst_plugin_abs_path()) + + @apply_pass_plugin(plugin_path, "mqt-core-round-trip") # type: ignore[untyped-decorator] + @qml.qnode(qml.device("null.qubit", wires=0)) # type: ignore[untyped-decorator] + def qnode() -> StateMP: + return qml.state() + + @qml.qjit(target="mlir") # type: ignore[untyped-decorator] + def module() -> StateMP: + return qnode() + + assert "mqt-core-round-trip" in module.mlir + + +def test_mqt_entry_point() -> None: + """Generate MLIR for the MQT plugin via entry-point.""" + + @apply_pass("mqt.mqt-core-round-trip") # type: ignore[untyped-decorator] + @qml.qnode(qml.device("null.qubit", wires=0)) # type: ignore[untyped-decorator] + def qnode() -> StateMP: + return qml.state() + + @qml.qjit(target="mlir") # type: ignore[untyped-decorator] + def module() -> StateMP: + return qnode() + + assert "mqt-core-round-trip" in module.mlir + + +def test_mqt_dictionary() -> None: + """Generate MLIR for the MQT plugin via pipeline dictionary.""" + + @pipeline({"mqt.mqt-core-round-trip": {}}) # type: ignore[untyped-decorator] + @qml.qnode(qml.device("null.qubit", wires=0)) # type: ignore[untyped-decorator] + def qnode() -> StateMP: + return qml.state() + + @qml.qjit(target="mlir") # type: ignore[untyped-decorator] + def module() -> StateMP: + return qnode() + + assert "mqt-core-round-trip" in module.mlir + + +def test_get_catalyst_plugin_abs_path_not_found() -> None: + """Test that get_catalyst_plugin_abs_path raises FileNotFoundError when library is missing.""" + with ( + patch("mqt.core.plugins.catalyst.plugin.files") as mock_files, + patch("mqt.core.plugins.catalyst.plugin.site.getsitepackages", return_value=["/fake/site-packages"]), + ): + # Configure the mock to return a path that has no library files + mock_package_path = MagicMock() + mock_lib_path = MagicMock() + mock_lib_path.is_file.return_value = False + mock_package_path.__truediv__.return_value = mock_lib_path + mock_files.return_value = mock_package_path + + with pytest.raises(FileNotFoundError, match="Could not locate catalyst plugin library"): + get_catalyst_plugin_abs_path() + + +def test_configure_device_for_mqt_no_config() -> None: + """Test that configure_device_for_mqt raises ValueError when device has no config_filepath.""" + dev = MagicMock(spec=qml.devices.Device) + dev.config_filepath = None + with pytest.raises(ValueError, match=r"Device does not have a config_filepath attribute set\."): + configure_device_for_mqt(dev) + + +def test_get_device_no_config() -> None: + """Test that get_device raises ValueError when the created device has no config_filepath.""" + with patch("pennylane.device") as mock_qml_device: + mock_dev = MagicMock(spec=qml.devices.Device) + mock_dev.config_filepath = None + mock_qml_device.return_value = mock_dev + + with pytest.raises(ValueError, match=r"Device does not have a config_filepath attribute set\."): + get_device("some.device") diff --git a/uv.lock b/uv.lock index 27a3fff..ef1687f 100644 --- a/uv.lock +++ b/uv.lock @@ -830,15 +830,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, ] -[[package]] -name = "lit" -version = "18.1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/b4/d7e210971494db7b9a9ac48ff37dfa59a8b14c773f9cf47e6bda58411c0d/lit-18.1.8.tar.gz", hash = "sha256:47c174a186941ae830f04ded76a3444600be67d5e5fb8282c3783fba671c4edb", size = 161127, upload-time = "2024-06-25T14:33:14.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/b36f150fa7c5bcc96a31a4d19a20fddbd1d965b6f02510b57a3bb8d4b930/lit-18.1.8-py3-none-any.whl", hash = "sha256:a873ff7acd76e746368da32eb7355625e2e55a2baaab884c9cc130f2ee0300f7", size = 96365, upload-time = "2024-06-25T14:33:12.101Z" }, -] - [[package]] name = "markdown-it-py" version = "3.0.0" @@ -977,7 +968,6 @@ build = [ { name = "setuptools-scm", marker = "(python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, ] dev = [ - { name = "lit", marker = "(python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, { name = "nox", marker = "(python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, { name = "pennylane-catalyst", marker = "(python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, { name = "pytest", marker = "(python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, @@ -1022,7 +1012,6 @@ build = [ { name = "setuptools-scm", specifier = ">=9.2.2" }, ] dev = [ - { name = "lit", specifier = ">=18.1.8" }, { name = "nox", specifier = ">=2025.11.12" }, { name = "pennylane-catalyst", specifier = "~=0.13.0" }, { name = "pytest", specifier = ">=9.0.1" },