Skip to content

Commit

Permalink
✨ Switch to mqt-core Python package (#432)
Browse files Browse the repository at this point in the history
## Description

This is an alternative to #355 and marks the final transition to the
`mqt-core` Python package. See #355 and #352 for some history on this
topic.

In addition to directly using the MQT Core Python package, this PR makes
Qiskit an optional dependency of MQT QCEC. All core functionality is now
covered MQT-internally.

## Checklist:

<!---
This checklist serves as a reminder of a couple of things that ensure
your pull request will be merged swiftly.
-->

- [x] The pull request only contains commits that are related to it.
- [x] I have added appropriate tests and documentation.
- [x] I have made sure that all CI jobs on GitHub pass.
- [x] The pull request introduces no new warnings and follows the
project's style guidelines.
  • Loading branch information
burgholzer authored Feb 18, 2025
2 parents 379c211 + 0987b8f commit 6802cfd
Show file tree
Hide file tree
Showing 79 changed files with 3,017 additions and 2,865 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
pattern: cibw-*
path: dist
merge-multiple: true
- name: Generate artifact attestation for sdist and wheel(s)
- name: Generate artifact attestation for sdist and wheels
uses: actions/attest-build-provenance@v2
with:
subject-path: "dist/*"
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ repos:
- nox
- numpy
- pytest
- mqt.core>=3.0.0b4

# Check for spelling
- repo: https://github.com/crate-ci/typos
Expand Down
17 changes: 15 additions & 2 deletions cmake/ExternalDependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ include(FetchContent)
set(FETCH_PACKAGES "")

if(BUILD_MQT_QCEC_BINDINGS)
# Manually detect the installed mqt-core package.
execute_process(
COMMAND "${Python_EXECUTABLE}" -m mqt.core --cmake_dir
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE mqt-core_DIR
ERROR_QUIET)

# Add the detected directory to the CMake prefix path.
if(mqt-core_DIR)
list(APPEND CMAKE_PREFIX_PATH "${mqt-core_DIR}")
message(STATUS "Found mqt-core package: ${mqt-core_DIR}")
endif()

if(NOT SKBUILD)
# Manually detect the installed pybind11 package.
execute_process(
Expand All @@ -20,9 +33,9 @@ if(BUILD_MQT_QCEC_BINDINGS)
endif()

# cmake-format: off
set(MQT_CORE_VERSION 2.7.1
set(MQT_CORE_VERSION 3.0.0
CACHE STRING "MQT Core version")
set(MQT_CORE_REV "5c3cb8edf0f43663a8edc7ae77c753926b466802"
set(MQT_CORE_REV "eaedadc689f13eabe8d504e23e0b038f0ddc49af"
CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
set(MQT_CORE_REPO_OWNER "cda-tum"
CACHE STRING "MQT Core repository owner (change when using a fork)")
Expand Down
1 change: 1 addition & 0 deletions docs/source/library/VerifyCompilation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Compilation Flow Profile Generation
QCEC provides dedicated compilation flow profiles for IBM Qiskit which can be used to efficiently verify the results of compilation flow results :cite:p:`burgholzer2020verifyingResultsIBM`.
These profiles are generated from IBM Qiskit using the :func:`.generate_profile` method.

.. currentmodule:: mqt.qcec.compilation_flow_profiles
.. autofunction:: generate_profile
10 changes: 10 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ def _run_tests(
"build",
"--only-group",
"test",
# Build mqt-core from source to work around pybind believing that two
# compiled extensions might not be binary compatible.
# This will be fixed in a new pybind11 release that includes https://github.com/pybind/pybind11/pull/5439.
"--no-binary-package",
"mqt-core",
*install_args,
env=env,
)
Expand Down Expand Up @@ -119,6 +124,11 @@ def docs(session: nox.Session) -> None:
"build",
"--only-group",
"docs",
# Build mqt-core from source to work around pybind believing that two
# compiled extensions might not be binary compatible.
# This will be fixed in a new pybind11 release that includes https://github.com/pybind/pybind11/pull/5439.
"--no-binary-package",
"mqt-core",
env=env,
)

Expand Down
40 changes: 36 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ requires = [
"scikit-build-core>=0.10.7",
"setuptools-scm>=8.1",
"pybind11>=2.13.6",
"mqt.core>=3.0.0b4",
]
build-backend = "scikit_build_core.build"

Expand Down Expand Up @@ -39,12 +40,19 @@ classifiers = [
]
requires-python = ">=3.9"
dependencies = [
"mqt.core>=3.0.0b4",
"importlib_resources>=5.0; python_version < '3.10'",
"typing_extensions>=4.2; python_version < '3.11'", # used for typing.Unpack
"qiskit[qasm3-import]>=1.0.0",
"numpy>=2.1; python_version >= '3.13'",
"numpy>=1.26; python_version >= '3.12'",
"numpy>=1.24; python_version >= '3.11'",
"numpy>=1.22",
]
dynamic = ["version"]

[project.optional-dependencies]
qiskit = ["qiskit[qasm3-import]>=1.0.0"]

[project.urls]
Homepage = "https://github.com/cda-tum/mqt-qcec"
Documentation = "https://mqt.readthedocs.io/projects/qcec"
Expand Down Expand Up @@ -136,6 +144,8 @@ report.exclude_also = [
'if TYPE_CHECKING:',
'raise AssertionError',
'raise NotImplementedError',
'def __dir__()', # Ignore __dir__ method that exists mainly for better IDE support
'@overload' # Overloads are only for static typing
]


Expand Down Expand Up @@ -275,15 +285,34 @@ manylinux-aarch64-image = "manylinux_2_28"
manylinux-ppc64le-image = "manylinux_2_28"
manylinux-s390x-image = "manylinux_2_28"

# The mqt-core shared libraries are provided by the mqt-core Python package.
# They should not be vendorized into the mqt-qcec wheel. This requires
# excluding the shared libraries from the repair process.

[tool.cibuildwheel.linux]
environment = { DEPLOY="ON" }
# The SOVERSION needs to be updated when the shared libraries are updated.
repair-wheel-command = """auditwheel repair -w {dest_dir} {wheel} \
--exclude libmqt-core-ir.so.3.0 \
--exclude libmqt-core-qasm.so.3.0 \
--exclude libmqt-core-circuit-optimizer.so.3.0 \
--exclude libmqt-core-algorithms.so.3.0 \
--exclude libmqt-core-dd.so.3.0 \
--exclude libmqt-core-zx.so.3.0"""

[tool.cibuildwheel.macos]
environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" }
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --ignore-missing-dependencies"

[tool.cibuildwheel.windows]
before-build = "pip install delvewheel>=1.7.3"
repair-wheel-command = "delvewheel repair -v -w {dest_dir} {wheel} --namespace-pkg mqt"
before-build = "uv pip install delvewheel>=1.9.0"
repair-wheel-command = """delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt \
--exclude mqt-core-ir.dll \
--exclude mqt-core-qasm.dll \
--exclude mqt-core-circuit-optimizer.dll \
--exclude mqt-core-algorithms.dll \
--exclude mqt-core-dd.dll \
--exclude mqt-core-zx.dll"""
environment = { CMAKE_ARGS = "-T ClangCL" }

[[tool.cibuildwheel.overrides]]
Expand All @@ -295,6 +324,7 @@ environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
required-version = ">=0.5.20"
reinstall-package = ["mqt.qcec"]


[tool.uv.sources]
mqt-qcec = { workspace = true }

Expand All @@ -303,10 +333,11 @@ build = [
"pybind11>=2.13.6",
"scikit-build-core>=0.10.7",
"setuptools-scm>=8.1",
"mqt-core>=3.0.0b4",
]
docs = [
"furo>=2024.8.6",
"qiskit[visualization]>=1.0.0",
"qiskit[qasm3-import,visualization]>=1.0.0",
"setuptools-scm>=8.1",
"sphinx-autoapi>=3.4.0",
"sphinx-copybutton>=0.5.2",
Expand All @@ -321,6 +352,7 @@ docs = [
test = [
"pytest>=8.3.4",
"pytest-cov>=6",
"qiskit[qasm3-import]>=1.0.0",
]
dev = [
{include-group = "build"},
Expand Down
20 changes: 18 additions & 2 deletions src/mqt/qcec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@

from __future__ import annotations

import sys

# under Windows, make sure to add the appropriate DLL directory to the PATH
if sys.platform == "win32":

def _dll_patch() -> None:
"""Add the DLL directory to the PATH."""
import os
import sysconfig
from pathlib import Path

bin_dir = Path(sysconfig.get_paths()["purelib"]) / "mqt" / "core" / "bin"
os.add_dll_directory(str(bin_dir))

_dll_patch()
del _dll_patch

from ._version import version as __version__
from .compilation_flow_profiles import AncillaMode, generate_profile
from .compilation_flow_profiles import AncillaMode
from .pyqcec import (
ApplicationScheme,
Configuration,
Expand All @@ -26,7 +43,6 @@
"EquivalenceCriterion",
"StateType",
"__version__",
"generate_profile",
"verify",
"verify_compilation",
]
101 changes: 101 additions & 0 deletions src/mqt/qcec/_compat/optional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Optional dependency tester.
Inspired by Qiskit's LazyDependencyManager
https://github.com/Qiskit/qiskit/blob/f13673b05edf98263f80a174d2e13a118b4acda7/qiskit/utils/lazy_tester.py#L44
"""

from __future__ import annotations

import contextlib
import importlib
import typing
import warnings

__all__ = ["HAS_QISKIT", "OptionalDependencyTester"]


def __dir__() -> list[str]:
return __all__


class OptionalDependencyTester:
"""A manager for optional dependencies to assert their availability.
This class is used to lazily test for the availability of optional dependencies.
It can be used in Boolean contexts to check if the dependency is available.
"""

def __init__(self, module: str, *, msg: str | None = None) -> None:
"""Construct a new optional dependency tester.
Args:
module: the name of the module to test for.
msg: an extra message to include in the error raised if this is required.
"""
self._module = module
self._bool: bool | None = None
self._msg = msg

def _is_available(self) -> bool:
"""Test the availability of the module.
Returns:
``True`` if the module is available, ``False`` otherwise.
"""
try:
importlib.import_module(self._module)
except ImportError as exc: # pragma: no cover
warnings.warn(
f"Module '{self._module}' failed to import with: {exc!r}",
category=UserWarning,
stacklevel=2,
)
return False
else:
return True

def __bool__(self) -> bool:
"""Check if the dependency is available.
Returns:
``True`` if the dependency is available, ``False`` otherwise.
"""
if self._bool is None:
self._bool = self._is_available()
return self._bool

def require_now(self, feature: str) -> None:
"""Eagerly attempt to import the dependency and raise an exception if it cannot be imported.
Args:
feature: the feature that is requiring this dependency.
Raises:
ImportError: if the dependency cannot be imported.
"""
if self:
return
message = f"The '{self._module}' library is required to {feature}."
if self._msg:
message += f" {self._msg}."
raise ImportError(message)

@contextlib.contextmanager
def disable_locally(self) -> typing.Generator[None, None, None]:
"""Create a context during which the value of the dependency manager will be ``False``.
Yields:
None
"""
previous = self._bool
self._bool = False
try:
yield
finally:
self._bool = previous


HAS_QISKIT = OptionalDependencyTester(
"qiskit",
msg="Please install the `mqt.qcec[qiskit]` extra or a compatible version of Qiskit to use functionality related to its functionality.",
)
Loading

0 comments on commit 6802cfd

Please sign in to comment.