From dbaa9cb96c9e0ed410544a20aac1f21af7d9d8bf Mon Sep 17 00:00:00 2001 From: burgholzer Date: Fri, 26 Jan 2024 16:44:28 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20switch=20to=20mqt-core=20Python=20p?= =?UTF-8?q?ackage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: burgholzer --- .pre-commit-config.yaml | 3 +- cmake/ExternalDependencies.cmake | 13 +++ noxfile.py | 22 +++++ pyproject.toml | 17 ++-- src/mqt/qmap/__init__.py | 33 +++++--- src/mqt/qmap/clifford_synthesis.py | 25 ++---- src/mqt/qmap/compile.py | 15 +++- src/mqt/qmap/pyqmap.pyi | 17 ++-- src/python/CMakeLists.txt | 12 ++- src/python/bindings.cpp | 116 ++++++++++---------------- test/python/test_cliffordsynthesis.py | 7 -- test/python/test_hybridmap.py | 7 +- 12 files changed, 154 insertions(+), 133 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 948ea6469..7e0248e73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,7 +83,8 @@ repos: - numpy - pytest - rustworkx - - mqt.qcec + - mqt.core + - mqt.qcec @ git+https://github.com/cda-tum/mqt-qcec.git@use-mqt-core-package # Check for spelling - repo: https://github.com/crate-ci/typos diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 38e0bae61..445941494 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -11,6 +11,19 @@ if(NOT Z3_FOUND) endif() if(BUILD_MQT_QMAP_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( diff --git a/noxfile.py b/noxfile.py index 91549cb13..3abbc4da7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -67,6 +67,19 @@ def _run_tests( posargs.append("--cov-config=pyproject.toml") session.install(*BUILD_REQUIREMENTS, *install_args, env=env) + + # install mqt.core from source to avoid ABI incompatibilities + session.install( + "--no-build-isolation", + "mqt.core @ git+https://github.com/cda-tum/mqt-core@shared-libs", + "--no-binary", + "mqt.core", + *install_args, + env={ + "CMAKE_GENERATOR": "Ninja", + }, + ) + install_arg = f"-ve.[{','.join(_extras)}]" session.install("--no-build-isolation", "--reinstall-package", "mqt.qmap", install_arg, *install_args, env=env) session.run("pytest", *run_args, *posargs, env=env) @@ -99,6 +112,15 @@ def docs(session: nox.Session) -> None: serve = args.builder == "html" and session.interactive extra_installs = ["sphinx-autobuild"] if serve else [] session.install(*BUILD_REQUIREMENTS, *extra_installs) + + # install mqt.core from source to avoid ABI incompatibilities + session.install( + "--no-build-isolation", + "mqt.core @ git+https://github.com/cda-tum/mqt-core@shared-libs", + "--no-binary", + "mqt.core", + ) + session.install("--no-build-isolation", "-ve.[docs]", "--reinstall-package", "mqt.qmap") session.chdir("docs") diff --git a/pyproject.toml b/pyproject.toml index 8a684bd51..deff1be2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,9 @@ [build-system] requires = [ - "scikit-build-core>=0.10.1", - "setuptools-scm>=7", - "pybind11>=2.13.5", + "scikit-build-core>=0.10.1", + "setuptools-scm>=7", + "pybind11>=2.13.5", + "mqt.core @ git+https://github.com/cda-tum/mqt-core.git@shared-libs", ] build-backend = "scikit_build_core.build" @@ -41,7 +42,7 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "qiskit[qasm3-import]>=1.0.0", + "mqt.core[qiskit] @ git+https://github.com/cda-tum/mqt-core.git@shared-libs", "rustworkx[all]>=0.14.0", "importlib_resources>=5.0; python_version < '3.10'", "typing_extensions>=4.6; python_version < '3.10'", @@ -315,13 +316,17 @@ environment = { Z3_ROOT="/opt/python/cp311-cp311/lib/python3.11/site-packages/z3 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}", + """auditwheel repair -w {dest_dir} {wheel} \ +--exclude libmqt-core-ir.so.2.6 \ +--exclude libmqt-core-circuit-optimizer.so.2.6 \ +--exclude libmqt-core-na.so.2.6""", ] [tool.cibuildwheel.macos] environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } +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" +repair-wheel-command = """delvewheel repair -w {dest_dir} {wheel} --namespace-pkg mqt --no-dll \"mqt-core-ir.dll;mqt-core-circuit-optimizer.dll;mqt-core-na.dll\"""" environment = { CMAKE_GENERATOR = "Ninja", SKBUILD_CMAKE_ARGS="--fresh" } diff --git a/src/mqt/qmap/__init__.py b/src/mqt/qmap/__init__.py index f3d695174..21b4673f9 100644 --- a/src/mqt/qmap/__init__.py +++ b/src/mqt/qmap/__init__.py @@ -6,17 +6,30 @@ from __future__ import annotations -import os import sys -from pathlib import Path -if sys.platform == "win32" and sys.version_info > (3, 8, 0) and "Z3_ROOT" in os.environ: - lib_path = Path(os.environ["Z3_ROOT"]) / "lib" - if lib_path.exists(): - os.add_dll_directory(str(lib_path)) - bin_path = Path(os.environ["Z3_ROOT"]) / "bin" - if bin_path.exists(): - os.add_dll_directory(str(bin_path)) +# 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)) + + if sys.version_info > (3, 8, 0) and "Z3_ROOT" in os.environ: + lib_path = Path(os.environ["Z3_ROOT"]) / "lib" + if lib_path.exists(): + os.add_dll_directory(str(lib_path)) + bin_path = Path(os.environ["Z3_ROOT"]) / "bin" + if bin_path.exists(): + os.add_dll_directory(str(bin_path)) + + _dll_patch() + del _dll_patch from ._version import version as __version__ from .clifford_synthesis import optimize_clifford, synthesize_clifford @@ -39,7 +52,6 @@ MappingResults, Method, NeutralAtomHybridArchitecture, - QuantumComputation, SwapReduction, SynthesisConfiguration, SynthesisResults, @@ -67,7 +79,6 @@ "MappingResults", "Method", "NeutralAtomHybridArchitecture", - "QuantumComputation", "SubarchitectureOrder", "SwapReduction", "SynthesisConfiguration", diff --git a/src/mqt/qmap/clifford_synthesis.py b/src/mqt/qmap/clifford_synthesis.py index 444d32afb..c2fab1ca5 100644 --- a/src/mqt/qmap/clifford_synthesis.py +++ b/src/mqt/qmap/clifford_synthesis.py @@ -2,33 +2,26 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .compile import CircuitInputType from qiskit import QuantumCircuit, qasm3 from qiskit.quantum_info import Clifford, PauliList from qiskit.transpiler.layout import TranspileLayout +from mqt.core import load + from .compile import extract_initial_layout_from_qasm from .pyqmap import ( CliffordSynthesizer, - QuantumComputation, SynthesisConfiguration, SynthesisResults, Tableau, ) -def _import_circuit(circuit: str | QuantumCircuit | QuantumComputation) -> QuantumComputation: - """Import a circuit from a string, a QuantumCircuit, or a QuantumComputation.""" - if isinstance(circuit, QuantumCircuit): - return QuantumComputation.from_qiskit(circuit) - if isinstance(circuit, str): - if circuit.endswith(".qasm"): - return QuantumComputation.from_file(circuit) - return QuantumComputation.from_qasm_str(circuit) - return circuit - - def _reverse_paulis(paulis: list[str]) -> list[str]: return [s[0] + s[:0:-1] if s[0] in "+-" else s[::-1] for s in paulis] @@ -84,11 +77,9 @@ def _circuit_from_qasm(qasm: str) -> QuantumCircuit: """Create a proper :class:`qiskit.QuantumCircuit` from a QASM string (including layout information).""" circ = qasm3.loads(qasm) layout = extract_initial_layout_from_qasm(qasm, circ.qregs) - circ._layout = TranspileLayout( # noqa: SLF001 initial_layout=layout, input_qubit_mapping=layout.get_virtual_bits() ) - return circ @@ -138,7 +129,7 @@ def synthesize_clifford( def optimize_clifford( - circuit: str | QuantumCircuit | QuantumComputation, + circuit: CircuitInputType, initial_tableau: str | Clifford | PauliList | Tableau | None = None, include_destabilizers: bool = False, **kwargs: Any, # noqa: ANN401 @@ -168,7 +159,7 @@ def optimize_clifford( """ config = _config_from_kwargs(kwargs) - qc = _import_circuit(circuit) + qc = load(circuit) if initial_tableau is not None: synthesizer = CliffordSynthesizer(_import_tableau(initial_tableau, include_destabilizers), qc) else: diff --git a/src/mqt/qmap/compile.py b/src/mqt/qmap/compile.py index 84c38a4ae..1a7448ff5 100644 --- a/src/mqt/qmap/compile.py +++ b/src/mqt/qmap/compile.py @@ -2,18 +2,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union from qiskit import QuantumCircuit, QuantumRegister, qasm3 from qiskit.transpiler import Layout, TranspileLayout if TYPE_CHECKING: + from os import PathLike + from qiskit.providers import Backend from qiskit.providers.models import BackendProperties from qiskit.transpiler.target import Target + from mqt.core.ir import QuantumComputation + from .visualization import SearchVisualizer + CircuitInputType = Union[QuantumComputation, str, PathLike[str], QuantumCircuit] + +from mqt.core import load + from .load_architecture import load_architecture from .load_calibration import load_calibration from .pyqmap import ( @@ -59,7 +67,7 @@ def extract_initial_layout_from_qasm(qasm: str, qregs: list[QuantumRegister]) -> def compile( # noqa: A001 - circ: QuantumCircuit | str, + circ: CircuitInputType, arch: str | Arch | Architecture | Backend | None, calibration: str | BackendProperties | Target | None = None, method: str | Method = "heuristic", @@ -182,7 +190,8 @@ def compile( # noqa: A001 config.lookaheads = lookaheads config.lookahead_factor = lookahead_factor - results = map(circ, architecture, config) + qc = load(circ) + results = map(qc, architecture, config) circ = qasm3.loads(results.mapped_circuit) layout = extract_initial_layout_from_qasm(results.mapped_circuit, circ.qregs) diff --git a/src/mqt/qmap/pyqmap.pyi b/src/mqt/qmap/pyqmap.pyi index 34efe68bb..4e1ed397b 100644 --- a/src/mqt/qmap/pyqmap.pyi +++ b/src/mqt/qmap/pyqmap.pyi @@ -1,6 +1,6 @@ from typing import Any, ClassVar, overload -from qiskit import QuantumCircuit +from mqt.core.ir import QuantumComputation class Arch: __members__: ClassVar[dict[Arch, int]] = ... # read-only @@ -393,7 +393,7 @@ class SwapReduction: @property def value(self) -> int: ... -def map(circ: str | QuantumCircuit, arch: Architecture, config: Configuration) -> MappingResults: ... # noqa: A001 +def map(circ: QuantumComputation, arch: Architecture, config: Configuration) -> MappingResults: ... # noqa: A001 class TargetMetric: __members__: ClassVar[dict[TargetMetric, int]] = ... # read-only @@ -488,15 +488,6 @@ class SynthesisResults: @property def two_qubit_gates(self) -> int: ... -class QuantumComputation: - def __init__(self) -> None: ... - @staticmethod - def from_file(file: str) -> QuantumComputation: ... - @staticmethod - def from_qasm_str(qasm: str) -> QuantumComputation: ... - @staticmethod - def from_qiskit(circuit: QuantumCircuit) -> QuantumComputation: ... - class Tableau: @overload def __init__(self, n: int, include_stabilizers: bool = False) -> None: ... @@ -586,7 +577,9 @@ class HybridNAMapper: def get_init_hw_pos(self) -> dict[int, int]: ... def get_mapped_qc(self) -> str: ... def get_mapped_qc_aod(self) -> str: ... - def map(self, circ: object, initial_mapping: InitialCircuitMapping = ..., verbose: bool = ...) -> None: ... + def map( + self, circ: QuantumComputation, initial_mapping: InitialCircuitMapping = ..., verbose: bool = ... + ) -> None: ... def map_qasm_file( self, filename: str, initial_mapping: InitialCircuitMapping = ..., verbose: bool = ... ) -> None: ... diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index dcf182660..f949f1e11 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -6,6 +6,15 @@ endif() list(APPEND CMAKE_INSTALL_RPATH ${BASEPOINT} ${BASEPOINT}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +list( + APPEND + CMAKE_INSTALL_RPATH + ${BASEPOINT}/../core/${CMAKE_INSTALL_LIBDIR} + ${BASEPOINT}/../core/lib + ${BASEPOINT}/../core/lib64 + ${BASEPOINT}/../../core/${CMAKE_INSTALL_LIBDIR} + ${BASEPOINT}/../../core/lib + ${BASEPOINT}/../../core/lib64) pybind11_add_module( pyqmap @@ -21,10 +30,9 @@ target_link_libraries( PRIVATE MQT::QMapExact MQT::QMapHeuristic MQT::QMapCliffordSynthesis - MQT::CorePython + MQT::QMapHybrid MQT::ProjectOptions MQT::ProjectWarnings - MQT::QMapHybrid pybind11_json) # Install directive for scikit-build-core diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp index 7b12a6ca1..c21684cbb 100644 --- a/src/python/bindings.cpp +++ b/src/python/bindings.cpp @@ -3,55 +3,67 @@ // See README.md or go to https://github.com/cda-tum/qmap for more information. // +#include "Architecture.hpp" +#include "Definitions.hpp" +#include "Mapper.hpp" +#include "MappingResults.hpp" #include "cliffordsynthesis/CliffordSynthesizer.hpp" +#include "cliffordsynthesis/Configuration.hpp" +#include "cliffordsynthesis/Results.hpp" +#include "cliffordsynthesis/Tableau.hpp" +#include "cliffordsynthesis/TargetMetric.hpp" +#include "configuration/AvailableArchitecture.hpp" +#include "configuration/CommanderGrouping.hpp" +#include "configuration/Configuration.hpp" +#include "configuration/EarlyTermination.hpp" +#include "configuration/Encoding.hpp" +#include "configuration/Heuristic.hpp" +#include "configuration/InitialLayout.hpp" +#include "configuration/Layering.hpp" +#include "configuration/LookaheadHeuristic.hpp" +#include "configuration/Method.hpp" +#include "configuration/SwapReduction.hpp" #include "exact/ExactMapper.hpp" #include "heuristic/HeuristicMapper.hpp" #include "hybridmap/HybridNeutralAtomMapper.hpp" -#include "hybridmap/NeutralAtomScheduler.hpp" -#include "nlohmann/json.hpp" -#include "pybind11/pybind11.h" -#include "pybind11/stl.h" -#include "pybind11_json/pybind11_json.hpp" -#include "python/qiskit/QuantumCircuit.hpp" -#include "string" +#include "hybridmap/NeutralAtomArchitecture.hpp" +#include "hybridmap/NeutralAtomUtils.hpp" +#include "ir/QuantumComputation.hpp" +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace py = pybind11; using namespace pybind11::literals; -void loadQC(qc::QuantumComputation& qc, const py::object& circ) { - try { - if (py::isinstance(circ)) { - auto&& file = circ.cast(); - qc.import(file); - } else { - qc::qiskit::QuantumCircuit::import(qc, circ); - } - } catch (std::exception const& e) { - std::stringstream ss{}; - ss << "Could not import circuit: " << e.what(); - throw std::invalid_argument(ss.str()); - } -} - // c++ binding function -MappingResults map(const py::object& circ, Architecture& arch, +MappingResults map(const qc::QuantumComputation& circ, Architecture& arch, Configuration& config) { - qc::QuantumComputation qc{}; - - loadQC(qc, circ); - if (config.useTeleportation) { config.teleportationQubits = - std::min((arch.getNqubits() - qc.getNqubits()) & ~1U, + std::min((arch.getNqubits() - circ.getNqubits()) & ~1U, static_cast(8)); } std::unique_ptr mapper; try { if (config.method == Method::Heuristic) { - mapper = std::make_unique(qc, arch); + mapper = std::make_unique(circ, arch); } else if (config.method == Method::Exact) { - mapper = std::make_unique(qc, arch); + mapper = std::make_unique(circ, arch); } } catch (std::exception const& e) { std::stringstream ss{}; @@ -659,35 +671,6 @@ PYBIND11_MODULE(pyqmap, m, py::mod_gil_not_used()) { "Constructs a tableau from two lists of Pauli strings, the Stabilizers" "and Destabilizers."); - auto quantumComputation = py::class_( - m, "QuantumComputation", - "A class for the intermediate representation of quantum circuits in the " - "Munich Quantum Toolkit."); - quantumComputation.def_static( - "from_file", - [](const std::string& filename) { - return qc::QuantumComputation(filename); - }, - "filename"_a, "Reads a quantum circuit from a file."); - quantumComputation.def_static( - "from_qasm_str", - [](const std::string& qasm) { - std::stringstream ss(qasm); - qc::QuantumComputation qc{}; - qc.import(ss, qc::Format::OpenQASM3); - return qc; - }, - "qasm"_a, "Reads a quantum circuit from a qasm string."); - quantumComputation.def_static( - "from_qiskit", - [](const py::object& circuit) { - qc::QuantumComputation qc{}; - qc::qiskit::QuantumCircuit::import(qc, circuit); - return qc; - }, - "circuit"_a, - "Reads a quantum circuit from a Qiskit :class:`QuantumCircuit`."); - auto synthesizer = py::class_( m, "CliffordSynthesizer", "A class for synthesizing Clifford circuits."); @@ -855,17 +838,10 @@ PYBIND11_MODULE(pyqmap, m, py::mod_gil_not_used()) { .def( "get_init_hw_pos", &na::NeutralAtomMapper::getInitHwPos, "Get the initial hardware positions, required to create an animation") - .def( - "map", - [](na::NeutralAtomMapper& mapper, const py::object& circ, - na::InitialMapping initialMapping, bool verbose) { - qc::QuantumComputation qc{}; - loadQC(qc, circ); - mapper.mapAndConvert(qc, initialMapping, verbose); - }, - "Map a quantum circuit to the neutral atom quantum computer", - "circ"_a, "initial_mapping"_a = na::InitialMapping::Identity, - "verbose"_a = false) + .def("map", &na::NeutralAtomMapper::mapAndConvert, + "Map a quantum circuit to the neutral atom quantum computer", + "circ"_a, "initial_mapping"_a = na::InitialMapping::Identity, + "verbose"_a = false) .def( "map_qasm_file", [](na::NeutralAtomMapper& mapper, const std::string& filename, diff --git a/test/python/test_cliffordsynthesis.py b/test/python/test_cliffordsynthesis.py index 7480ee3b0..bb38fcc55 100644 --- a/test/python/test_cliffordsynthesis.py +++ b/test/python/test_cliffordsynthesis.py @@ -227,13 +227,6 @@ def bell_circuit() -> QuantumCircuit: return circ -def test_optimize_quantum_computation(bell_circuit: QuantumCircuit) -> None: - """Test that we can optimize an MQT QuantumComputation.""" - qc = qmap.QuantumComputation.from_qiskit(bell_circuit) - circ, _ = qmap.optimize_clifford(circuit=qc) - assert qcec.verify(circ, bell_circuit).considered_equivalent() - - def test_optimize_from_qasm_file(bell_circuit: QuantumCircuit) -> None: """Test that we can optimize from a QASM file.""" qasm2.dump(bell_circuit, Path("bell.qasm")) diff --git a/test/python/test_hybridmap.py b/test/python/test_hybridmap.py index 8be2e0d63..29fb81852 100644 --- a/test/python/test_hybridmap.py +++ b/test/python/test_hybridmap.py @@ -5,8 +5,8 @@ from pathlib import Path import pytest -from qiskit import QuantumCircuit +from mqt.core import load from mqt.qmap import HybridMapperParameters, HybridNAMapper, NeutralAtomHybridArchitecture arch_dir = Path(__file__).parent.parent / "hybridmap" / "architectures" @@ -48,8 +48,7 @@ def test_hybrid_na_mapper( ) mapper = HybridNAMapper(arch, params=params) - # Create a simple circuit - qc = QuantumCircuit.from_qasm_file(str(circuit_dir / circuit_filename)) + qc = load(circuit_dir / circuit_filename) mapper.map(qc) results = mapper.schedule(create_animation_csv=False) @@ -70,7 +69,7 @@ def test_keep_alive() -> None: """Test the keep alive feature of the python bindings.""" mapper = _nested_mapper_create() - qc = QuantumCircuit.from_qasm_file(str(circuit_dir / "dj_nativegates_rigetti_qiskit_opt3_10.qasm")) + qc = load(circuit_dir / "dj_nativegates_rigetti_qiskit_opt3_10.qasm") mapper.map(qc) results = mapper.schedule(create_animation_csv=False)