Skip to content

Commit 6802cfd

Browse files
authored
✨ Switch to mqt-core Python package (#432)
## 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.
2 parents 379c211 + 0987b8f commit 6802cfd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3017
-2865
lines changed

.github/workflows/cd.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
pattern: cibw-*
3333
path: dist
3434
merge-multiple: true
35-
- name: Generate artifact attestation for sdist and wheel(s)
35+
- name: Generate artifact attestation for sdist and wheels
3636
uses: actions/attest-build-provenance@v2
3737
with:
3838
subject-path: "dist/*"

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ repos:
107107
- nox
108108
- numpy
109109
- pytest
110+
- mqt.core>=3.0.0b4
110111

111112
# Check for spelling
112113
- repo: https://github.com/crate-ci/typos

cmake/ExternalDependencies.cmake

+15-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ include(FetchContent)
44
set(FETCH_PACKAGES "")
55

66
if(BUILD_MQT_QCEC_BINDINGS)
7+
# Manually detect the installed mqt-core package.
8+
execute_process(
9+
COMMAND "${Python_EXECUTABLE}" -m mqt.core --cmake_dir
10+
OUTPUT_STRIP_TRAILING_WHITESPACE
11+
OUTPUT_VARIABLE mqt-core_DIR
12+
ERROR_QUIET)
13+
14+
# Add the detected directory to the CMake prefix path.
15+
if(mqt-core_DIR)
16+
list(APPEND CMAKE_PREFIX_PATH "${mqt-core_DIR}")
17+
message(STATUS "Found mqt-core package: ${mqt-core_DIR}")
18+
endif()
19+
720
if(NOT SKBUILD)
821
# Manually detect the installed pybind11 package.
922
execute_process(
@@ -20,9 +33,9 @@ if(BUILD_MQT_QCEC_BINDINGS)
2033
endif()
2134

2235
# cmake-format: off
23-
set(MQT_CORE_VERSION 2.7.1
36+
set(MQT_CORE_VERSION 3.0.0
2437
CACHE STRING "MQT Core version")
25-
set(MQT_CORE_REV "5c3cb8edf0f43663a8edc7ae77c753926b466802"
38+
set(MQT_CORE_REV "eaedadc689f13eabe8d504e23e0b038f0ddc49af"
2639
CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
2740
set(MQT_CORE_REPO_OWNER "cda-tum"
2841
CACHE STRING "MQT Core repository owner (change when using a fork)")

docs/source/library/VerifyCompilation.rst

+1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ Compilation Flow Profile Generation
2222
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`.
2323
These profiles are generated from IBM Qiskit using the :func:`.generate_profile` method.
2424

25+
.. currentmodule:: mqt.qcec.compilation_flow_profiles
2526
.. autofunction:: generate_profile

noxfile.py

+10
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ def _run_tests(
5757
"build",
5858
"--only-group",
5959
"test",
60+
# Build mqt-core from source to work around pybind believing that two
61+
# compiled extensions might not be binary compatible.
62+
# This will be fixed in a new pybind11 release that includes https://github.com/pybind/pybind11/pull/5439.
63+
"--no-binary-package",
64+
"mqt-core",
6065
*install_args,
6166
env=env,
6267
)
@@ -119,6 +124,11 @@ def docs(session: nox.Session) -> None:
119124
"build",
120125
"--only-group",
121126
"docs",
127+
# Build mqt-core from source to work around pybind believing that two
128+
# compiled extensions might not be binary compatible.
129+
# This will be fixed in a new pybind11 release that includes https://github.com/pybind/pybind11/pull/5439.
130+
"--no-binary-package",
131+
"mqt-core",
122132
env=env,
123133
)
124134

pyproject.toml

+36-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ requires = [
33
"scikit-build-core>=0.10.7",
44
"setuptools-scm>=8.1",
55
"pybind11>=2.13.6",
6+
"mqt.core>=3.0.0b4",
67
]
78
build-backend = "scikit_build_core.build"
89

@@ -39,12 +40,19 @@ classifiers = [
3940
]
4041
requires-python = ">=3.9"
4142
dependencies = [
43+
"mqt.core>=3.0.0b4",
4244
"importlib_resources>=5.0; python_version < '3.10'",
4345
"typing_extensions>=4.2; python_version < '3.11'", # used for typing.Unpack
44-
"qiskit[qasm3-import]>=1.0.0",
46+
"numpy>=2.1; python_version >= '3.13'",
47+
"numpy>=1.26; python_version >= '3.12'",
48+
"numpy>=1.24; python_version >= '3.11'",
49+
"numpy>=1.22",
4550
]
4651
dynamic = ["version"]
4752

53+
[project.optional-dependencies]
54+
qiskit = ["qiskit[qasm3-import]>=1.0.0"]
55+
4856
[project.urls]
4957
Homepage = "https://github.com/cda-tum/mqt-qcec"
5058
Documentation = "https://mqt.readthedocs.io/projects/qcec"
@@ -136,6 +144,8 @@ report.exclude_also = [
136144
'if TYPE_CHECKING:',
137145
'raise AssertionError',
138146
'raise NotImplementedError',
147+
'def __dir__()', # Ignore __dir__ method that exists mainly for better IDE support
148+
'@overload' # Overloads are only for static typing
139149
]
140150

141151

@@ -275,15 +285,34 @@ manylinux-aarch64-image = "manylinux_2_28"
275285
manylinux-ppc64le-image = "manylinux_2_28"
276286
manylinux-s390x-image = "manylinux_2_28"
277287

288+
# The mqt-core shared libraries are provided by the mqt-core Python package.
289+
# They should not be vendorized into the mqt-qcec wheel. This requires
290+
# excluding the shared libraries from the repair process.
291+
278292
[tool.cibuildwheel.linux]
279293
environment = { DEPLOY="ON" }
294+
# The SOVERSION needs to be updated when the shared libraries are updated.
295+
repair-wheel-command = """auditwheel repair -w {dest_dir} {wheel} \
296+
--exclude libmqt-core-ir.so.3.0 \
297+
--exclude libmqt-core-qasm.so.3.0 \
298+
--exclude libmqt-core-circuit-optimizer.so.3.0 \
299+
--exclude libmqt-core-algorithms.so.3.0 \
300+
--exclude libmqt-core-dd.so.3.0 \
301+
--exclude libmqt-core-zx.so.3.0"""
280302

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

284307
[tool.cibuildwheel.windows]
285-
before-build = "pip install delvewheel>=1.7.3"
286-
repair-wheel-command = "delvewheel repair -v -w {dest_dir} {wheel} --namespace-pkg mqt"
308+
before-build = "uv pip install delvewheel>=1.9.0"
309+
repair-wheel-command = """delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt \
310+
--exclude mqt-core-ir.dll \
311+
--exclude mqt-core-qasm.dll \
312+
--exclude mqt-core-circuit-optimizer.dll \
313+
--exclude mqt-core-algorithms.dll \
314+
--exclude mqt-core-dd.dll \
315+
--exclude mqt-core-zx.dll"""
287316
environment = { CMAKE_ARGS = "-T ClangCL" }
288317

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

327+
298328
[tool.uv.sources]
299329
mqt-qcec = { workspace = true }
300330

@@ -303,10 +333,11 @@ build = [
303333
"pybind11>=2.13.6",
304334
"scikit-build-core>=0.10.7",
305335
"setuptools-scm>=8.1",
336+
"mqt-core>=3.0.0b4",
306337
]
307338
docs = [
308339
"furo>=2024.8.6",
309-
"qiskit[visualization]>=1.0.0",
340+
"qiskit[qasm3-import,visualization]>=1.0.0",
310341
"setuptools-scm>=8.1",
311342
"sphinx-autoapi>=3.4.0",
312343
"sphinx-copybutton>=0.5.2",
@@ -321,6 +352,7 @@ docs = [
321352
test = [
322353
"pytest>=8.3.4",
323354
"pytest-cov>=6",
355+
"qiskit[qasm3-import]>=1.0.0",
324356
]
325357
dev = [
326358
{include-group = "build"},

src/mqt/qcec/__init__.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,25 @@
66

77
from __future__ import annotations
88

9+
import sys
10+
11+
# under Windows, make sure to add the appropriate DLL directory to the PATH
12+
if sys.platform == "win32":
13+
14+
def _dll_patch() -> None:
15+
"""Add the DLL directory to the PATH."""
16+
import os
17+
import sysconfig
18+
from pathlib import Path
19+
20+
bin_dir = Path(sysconfig.get_paths()["purelib"]) / "mqt" / "core" / "bin"
21+
os.add_dll_directory(str(bin_dir))
22+
23+
_dll_patch()
24+
del _dll_patch
25+
926
from ._version import version as __version__
10-
from .compilation_flow_profiles import AncillaMode, generate_profile
27+
from .compilation_flow_profiles import AncillaMode
1128
from .pyqcec import (
1229
ApplicationScheme,
1330
Configuration,
@@ -26,7 +43,6 @@
2643
"EquivalenceCriterion",
2744
"StateType",
2845
"__version__",
29-
"generate_profile",
3046
"verify",
3147
"verify_compilation",
3248
]

src/mqt/qcec/_compat/optional.py

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Optional dependency tester.
2+
3+
Inspired by Qiskit's LazyDependencyManager
4+
https://github.com/Qiskit/qiskit/blob/f13673b05edf98263f80a174d2e13a118b4acda7/qiskit/utils/lazy_tester.py#L44
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import contextlib
10+
import importlib
11+
import typing
12+
import warnings
13+
14+
__all__ = ["HAS_QISKIT", "OptionalDependencyTester"]
15+
16+
17+
def __dir__() -> list[str]:
18+
return __all__
19+
20+
21+
class OptionalDependencyTester:
22+
"""A manager for optional dependencies to assert their availability.
23+
24+
This class is used to lazily test for the availability of optional dependencies.
25+
It can be used in Boolean contexts to check if the dependency is available.
26+
"""
27+
28+
def __init__(self, module: str, *, msg: str | None = None) -> None:
29+
"""Construct a new optional dependency tester.
30+
31+
Args:
32+
module: the name of the module to test for.
33+
msg: an extra message to include in the error raised if this is required.
34+
"""
35+
self._module = module
36+
self._bool: bool | None = None
37+
self._msg = msg
38+
39+
def _is_available(self) -> bool:
40+
"""Test the availability of the module.
41+
42+
Returns:
43+
``True`` if the module is available, ``False`` otherwise.
44+
"""
45+
try:
46+
importlib.import_module(self._module)
47+
except ImportError as exc: # pragma: no cover
48+
warnings.warn(
49+
f"Module '{self._module}' failed to import with: {exc!r}",
50+
category=UserWarning,
51+
stacklevel=2,
52+
)
53+
return False
54+
else:
55+
return True
56+
57+
def __bool__(self) -> bool:
58+
"""Check if the dependency is available.
59+
60+
Returns:
61+
``True`` if the dependency is available, ``False`` otherwise.
62+
"""
63+
if self._bool is None:
64+
self._bool = self._is_available()
65+
return self._bool
66+
67+
def require_now(self, feature: str) -> None:
68+
"""Eagerly attempt to import the dependency and raise an exception if it cannot be imported.
69+
70+
Args:
71+
feature: the feature that is requiring this dependency.
72+
73+
Raises:
74+
ImportError: if the dependency cannot be imported.
75+
"""
76+
if self:
77+
return
78+
message = f"The '{self._module}' library is required to {feature}."
79+
if self._msg:
80+
message += f" {self._msg}."
81+
raise ImportError(message)
82+
83+
@contextlib.contextmanager
84+
def disable_locally(self) -> typing.Generator[None, None, None]:
85+
"""Create a context during which the value of the dependency manager will be ``False``.
86+
87+
Yields:
88+
None
89+
"""
90+
previous = self._bool
91+
self._bool = False
92+
try:
93+
yield
94+
finally:
95+
self._bool = previous
96+
97+
98+
HAS_QISKIT = OptionalDependencyTester(
99+
"qiskit",
100+
msg="Please install the `mqt.qcec[qiskit]` extra or a compatible version of Qiskit to use functionality related to its functionality.",
101+
)

0 commit comments

Comments
 (0)