Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
clang-version: 21
cmake-args: -DBUILD_MQT_QUSAT_BINDINGS=ON
files-changed-only: true
install-pkgs: "pybind11==3.0.1"
install-pkgs: "nanobind==2.10.2"
setup-python: true
setup-z3: true
cpp-linter-extra-args: "-std=c++20"
Expand Down Expand Up @@ -140,6 +140,7 @@ jobs:
uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@d6314c45667c131055a0389afc110e8dedc6da3f # v1.17.11
with:
setup-z3: true
check-stubs: true

build-sdist:
name: 🚀 CD
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ repos:
- id: disallow-caps
name: Disallow improper capitalization
language: pygrep
entry: PyBind|Numpy|Cmake|CCache|Github|PyTest|Mqt|Tum|MQTopt|MQTref
entry: Nanobind|Numpy|Cmake|CCache|Github|PyTest|Mqt|Tum|MQTopt|MQTref
exclude: .pre-commit-config.yaml

# Check best practices for scientific Python code
Expand Down
11 changes: 7 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ if(BUILD_MQT_QUSAT_BINDINGS)
# ensure that the BINDINGS option is set
set(BINDINGS
ON
CACHE BOOL "Enable settings related to Python bindings" FORCE)
# cmake-lint: disable=C0103
CACHE INTERNAL "Enable settings related to Python bindings")
# Some common settings for finding Python
set(Python_FIND_VIRTUALENV
FIRST
CACHE STRING "Give precedence to virtualenvs when searching for Python")
# cmake-lint: disable=C0103
set(Python_FIND_FRAMEWORK
LAST
CACHE STRING "Prefer Brew/Conda to Apple framework Python")
set(Python_ARTIFACTS_INTERACTIVE
ON
CACHE BOOL "Prevent multiple searches for Python and instead cache the results.")
Expand All @@ -37,7 +39,8 @@ if(BUILD_MQT_QUSAT_BINDINGS)
endif()

# top-level call to find Python
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module)
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module
${SKBUILD_SABI_COMPONENT})
endif()

# Add path for custom modules
Expand Down
6 changes: 3 additions & 3 deletions bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ list(
${BASEPOINT}/../../core/lib
${BASEPOINT}/../../core/lib64)

add_mqt_python_binding(
add_mqt_python_binding_nanobind(
QUSAT
${MQT_QUSAT_TARGET_NAME}-bindings
bindings.cpp
Expand All @@ -34,8 +34,8 @@ add_mqt_python_binding(
INSTALL_DIR
.
LINK_LIBS
MQT::QuSAT
pybind11_json)
MQT::QuSAT)

target_compile_definitions(${MQT_QUSAT_TARGET_NAME}-bindings PRIVATE Z3_FOUND)

# Install directive for scikit-build-core
Expand Down
48 changes: 34 additions & 14 deletions bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
*/

#include "SatEncoder.hpp"
#include "ir/QuantumComputation.hpp"

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11_json/pybind11_json.hpp> // IWYU pragma: keep
#include <nanobind/nanobind.h>
#include <nanobind/stl/optional.h> // NOLINT(misc-include-cleaner)
#include <nanobind/stl/string.h> // NOLINT(misc-include-cleaner)
#include <nanobind/stl/vector.h> // NOLINT(misc-include-cleaner)

namespace py = pybind11;
namespace nl = nlohmann;
using namespace pybind11::literals;
namespace nb = nanobind;
using namespace nb::literals;

nl::basic_json<> checkEquivalence(qc::QuantumComputation& qc1,
qc::QuantumComputation& qc2,
Expand All @@ -26,7 +28,8 @@ nl::basic_json<> checkEquivalence(qc::QuantumComputation& qc1,
try {
results["equivalent"] = encoder.testEqual(qc1, qc2, inputs);
} catch (std::exception const& e) {
py::print("Could not check equivalence: ", e.what());
nb::print(
("Could not check equivalence: " + std::string(e.what())).c_str());
return {};
}
results["statistics"] = encoder.getStats().to_json();
Expand All @@ -38,16 +41,33 @@ std::string printDIMACS(qc::QuantumComputation& qc) {
return encoder.generateDIMACS(qc);
}

PYBIND11_MODULE(MQT_QUSAT_MODULE_NAME, m, py::mod_gil_not_used()) {
NB_MODULE(MQT_QUSAT_MODULE_NAME, m) {
nb::module_::import_("mqt.core.ir");

m.doc() =
"Python interface for the MQT QuSAT quantum circuit satisfiability tool";

m.def("check_equivalence", &checkEquivalence,
"Check the equivalence of two clifford circuits for the given inputs."
"If no inputs are given, the all zero state is used as input.",
"circ1"_a, "circ2"_a, "inputs"_a = std::vector<std::string>());
m.def(
"check_equivalence",
[](qc::QuantumComputation& circ1, qc::QuantumComputation& circ2,
const std::optional<std::vector<std::string>>& inputs) {
const nb::module_ json = nb::module_::import_("json");
const nb::object loads = json.attr("loads");
if (!inputs.has_value()) {
return loads(
checkEquivalence(circ1, circ2, std::vector<std::string>{})
.dump());
}
return loads(checkEquivalence(circ1, circ2, inputs.value()).dump());
},
"circ1"_a, "circ2"_a, "inputs"_a = nb::none(),
nb::sig("def check_equivalence(circ1: mqt.core.ir.QuantumComputation, "
"circ2: mqt.core.ir.QuantumComputation, inputs: "
"collections.abc.Sequence[str] | None = None) "
"-> dict[str, typing.Any]"),
"Check the equivalence of two clifford circuits for the given inputs. "
"If no inputs are given, the all zero state is used as input.");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

m.def("generate_dimacs", &printDIMACS,
"Output the DIMACS CNF representation from Z3 of the given circuit.",
"circ"_a);
m.def("generate_dimacs", &printDIMACS, "circ"_a,
"Output the DIMACS CNF representation from Z3 of the given circuit.");
}
29 changes: 6 additions & 23 deletions cmake/ExternalDependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,15 @@ if(BUILD_MQT_QUSAT_BINDINGS)
message(STATUS "Found mqt-core package: ${mqt-core_DIR}")
endif()

if(NOT SKBUILD)
# Manually detect the installed pybind11 package.
execute_process(
COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE pybind11_DIR)

# Add the detected directory to the CMake prefix path.
list(APPEND CMAKE_PREFIX_PATH "${pybind11_DIR}")
endif()

# add pybind11 library
find_package(pybind11 3.0.1 CONFIG REQUIRED)
execute_process(
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE nanobind_ROOT)
find_package(nanobind CONFIG REQUIRED)
endif()

# cmake-format: off
set(MQT_CORE_MINIMUM_VERSION 3.3.1
set(MQT_CORE_MINIMUM_VERSION 3.4.0
CACHE STRING "MQT Core minimum version")
set(MQT_CORE_VERSION 3.4.0
CACHE STRING "MQT Core version")
Expand Down Expand Up @@ -72,14 +64,5 @@ if(BUILD_MQT_QUSAT_TESTS)
list(APPEND FETCH_PACKAGES googletest)
endif()

if(BUILD_MQT_QUSAT_BINDINGS)
# add pybind11_json library
FetchContent_Declare(
pybind11_json
GIT_REPOSITORY https://github.com/pybind/pybind11_json
FIND_PACKAGE_ARGS)
list(APPEND FETCH_PACKAGES pybind11_json)
endif()

# Make all declared dependencies available.
FetchContent_MakeAvailable(${FETCH_PACKAGES})
47 changes: 47 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import shutil
import sys
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING

import nox
Expand Down Expand Up @@ -173,5 +174,51 @@ def docs(session: nox.Session) -> None:
)


@nox.session(reuse_venv=True, venv_backend="uv")
def stubs(session: nox.Session) -> None:
"""Generate type stubs for Python bindings using nanobind."""
env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
session.run(
"uv",
"sync",
"--no-dev",
"--group",
"build",
env=env,
)

package_root = Path(__file__).parent / "python" / "mqt" / "qusat"

session.run(
"python",
"-m",
"nanobind.stubgen",
"--recursive",
"--include-private",
"--output-dir",
str(package_root),
"--module",
"mqt.qusat.pyqusat",
)

pyi_files = list(package_root.glob("**/*.pyi"))

if not pyi_files:
session.warn("No .pyi files found")
return

if shutil.which("prek") is None:
session.install("prek")

# Allow both 0 (no issues) and 1 as success codes for fixing up stubs
success_codes = [0, 1]
session.run("prek", "run", "license-tools", "--files", *pyi_files, external=True, success_codes=success_codes)
session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True, success_codes=success_codes)
session.run("prek", "run", "ruff-format", "--files", *pyi_files, external=True, success_codes=success_codes)

# Run ruff-check again to ensure everything is clean
session.run("prek", "run", "ruff-check", "--files", *pyi_files, external=True)

Comment thread
coderabbitai[bot] marked this conversation as resolved.

if __name__ == "__main__":
nox.main()
24 changes: 16 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[build-system]
requires = [
"pybind11>=3.0.1",
"nanobind>=2.10.2",
"scikit-build-core>=0.11.6",
"setuptools-scm>=9.2.2",
"mqt.core~=3.3.1",
"mqt.core~=3.4.0",
]
build-backend = "scikit_build_core.build"

Expand Down Expand Up @@ -40,7 +40,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"mqt.core~=3.3.1",
"mqt.core~=3.4.0",
]
dynamic = ["version"]

Expand All @@ -63,6 +63,9 @@ wheel.install-dir = "mqt/qusat"
# Explicitly set the package directory
wheel.packages = ["python/mqt"]

# Enable Stable ABI builds for CPython 3.12+
wheel.py-api = "cp312"

# Set required Ninja version
ninja.version = ">=1.10"

Expand Down Expand Up @@ -293,9 +296,9 @@ before-all = "/opt/python/cp311-cp311/bin/pip install z3-solver==4.12.6"
repair-wheel-command = [
"export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/python/cp311-cp311/lib/python3.11/site-packages/z3/lib",
"""auditwheel repair -w {dest_dir} {wheel} \
--exclude libmqt-core-ir.so.3.3 \
--exclude libmqt-core-qasm.so.3.3 \
--exclude libmqt-core-circuit-optimizer.so.3.3""",
--exclude libmqt-core-ir.so.3.4 \
--exclude libmqt-core-qasm.so.3.4 \
--exclude libmqt-core-circuit-optimizer.so.3.4""",
]

[tool.cibuildwheel.macos]
Expand All @@ -309,6 +312,11 @@ repair-wheel-command = """delvewheel repair -w {dest_dir} {wheel} --namespace-pk
--exclude mqt-core-qasm.dll \
--exclude mqt-core-circuit-optimizer.dll"""

[[tool.cibuildwheel.overrides]]
select = "cp312-*"
inherit.repair-wheel-command = "append"
repair-wheel-command = "uvx abi3audit --strict --report {wheel}"


[tool.uv]
required-version = ">=0.6.9"
Expand All @@ -324,10 +332,10 @@ mqt-qusat = { workspace = true }

[dependency-groups]
build = [
"pybind11>=3.0.1",
"nanobind>=2.10.2",
"scikit-build-core>=0.11.6",
"setuptools-scm>=9.2.2",
"mqt.core~=3.3.1",
"mqt.core~=3.4.0",
]
docs = [
"furo>=2025.09.25",
Expand Down
15 changes: 8 additions & 7 deletions python/mqt/qusat/pyqusat.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
#
# Licensed under the MIT License

from collections.abc import Sequence
from typing import Any

from mqt.core.ir import QuantumComputation
import mqt.core.ir

def check_equivalence(
circ1: QuantumComputation,
circ2: QuantumComputation,
) -> dict[str, Any]: ...
def generate_dimacs(
circ: QuantumComputation,
) -> str: ...
circ1: mqt.core.ir.QuantumComputation, circ2: mqt.core.ir.QuantumComputation, inputs: Sequence[str] | None = None
) -> dict[str, Any]:
"""Check the equivalence of two clifford circuits for the given inputs. If no inputs are given, the all zero state is used as input."""
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def generate_dimacs(circ: mqt.core.ir.QuantumComputation) -> str:
"""Output the DIMACS CNF representation from Z3 of the given circuit."""
Loading
Loading